TNO Intern

Commit 1c1db822 authored by Hen Brett's avatar Hen Brett 🐔
Browse files

Using a JCLASS utcproperties instance instead of a python dictionary to handle...

Using a JCLASS utcproperties instance instead of a python dictionary to handle the properties of the simulation
parent 6d269e4f
Loading
Loading
Loading
Loading
+55 −105
Original line number Diff line number Diff line
@@ -10,38 +10,19 @@ from pythermogis.thermogis_classes.jvm_start import start_jvm


def calculate_doublet_performance(input_data: xr.Dataset,
                                  input_params: dict = None,
                                  utc_properties = None,
                                  rng_seed = None,
                                  p_values: List[float] = [50.0]) -> xr.Dataset:
    """
    Perform a ThermoGIS Doublet performance simulation. This will occur across all dimensions of the input_data (ie. input data can have a single value for each required variable, or it can be 1Dimensional or a 2Dimensional grid)

    The default doublet simulation settings are for the ThermoGIS BaseCase; but major input parameters can be set using the input_params variable. This is a dictonary which by default contain the following keys:
    input_params = {"hp_minimum_injection_temperature": 15,
         "return_temperature": 30,
         "surface_temperature": 10,
         "degrees_per_km": 31,
         "max_cooling_temperature_range": 100,
         "stimKhMax": 20,
         "use_stimulation": False,
         "use_heat_pump": False,
         "calculate_cop": True,
         "hp_application_mode": False,
         "hp_direct_heat_input_temp": 70.0,
         "utc_cutoff_shallow": 5.1,
         "utc_cutoff_deep": 6.5,
         "utc_cutoff_depth": 4000.0,
         "rng_seed": np.random.randint(low=0, high=10000)}
    To change only one of these input parameters you can provide a dictionary with a single key to this method, and it will use the default values for the rest of the model parameters.
    e.g. input_params = {"use_stimulation":True}


    :param input_data:
        A xr.Dataset (input_data) with the required variables: "thickness_mean", "thickness_sd", "porosity", "ntg", "depth", "ln_permeability_mean", "ln_permeability_sd", Performance will be calculated across all dimensions.
        Optional Extra parameters: "temperature", "mask".
        If no temperature values are provided, temperature will be calculated using a gradient specified by the input_params dictionary and the depth variable.
        If mask values are provided, then any non-nan values in the mask variable will be set to zero across all variables in the returned output_data object.
    :param input_params:
        A dictionary containing parameters for use by the doublet calculation, If no input_params are provided then the values for the default thermogis scenario are used (the "BaseCase" scenario).
    :param utc_properties:
    :param rng_seed:
    :param p_values:
        A list of p_values for the doublet calculation to perform over; if no p_values are provided then the default value of P50 is used.
    :return output_data:
@@ -54,6 +35,9 @@ def calculate_doublet_performance(input_data: xr.Dataset,
    # Check that all essential variables are provided
    validate_input_data(input_data)

    if utc_properties is None: # Instantiate utc_properties if none is provided
        utc_properties = instantiate_utc_properties()

    # convert p_values list to a xarray DataArray; needed to ensure the dimensionality of the calculations
    p_values = xr.DataArray(
        data=p_values,
@@ -62,12 +46,9 @@ def calculate_doublet_performance(input_data: xr.Dataset,
            p_value=(["p_value"], p_values),
        ))

    # Apply provided input_params; or return the base case
    input_params = apply_input_params(input_params)

    # Generate temperature values from gradient if no temperature provided
    if "temperature" not in input_data:
        input_data["temperature"] = calculate_temperature_from_gradient(input_data.depth, input_data.thickness_mean, input_params["degrees_per_km"], input_params["surface_temperature"])
        input_data["temperature"] = calculate_temperature_from_gradient(input_data.depth, input_data.thickness_mean, utc_properties.tempGradient(), utc_properties.surfaceTemperature())

    # If no mask grid is provided, then provide a dummy mask grid with only nan
    if "mask" not in input_data:
@@ -93,9 +74,8 @@ def calculate_doublet_performance(input_data: xr.Dataset,
    # Calculate transmissivity scaled by ntg and converted to Dm
    output_data[f"transmissivity_with_ntg"] = (output_data[f"transmissivity"] * input_data.ntg) / 1e3


    # Instantiate ThermoGIS doublet
    doublet = instantiate_thermogis_doublet(input_params)
    doublet = instantiate_thermogis_doublet(utc_properties, rng_seed)

    # Calculate the doublet performance across all dimensions
    output_data_arrays = xr.apply_ufunc(calculate_performance_of_single_location,
@@ -107,7 +87,7 @@ def calculate_doublet_performance(input_data: xr.Dataset,
                                        input_data.temperature,
                                        output_data.transmissivity,
                                        output_data.transmissivity_with_ntg,
                                        kwargs={"doublet": doublet, "input_params": input_params},
                                        kwargs={"doublet": doublet, "utc_properties": utc_properties},
                                        input_core_dims=[[], [], [], [], [], [], [], []],
                                        output_core_dims=[[], [], [], [], [], [], [], [], [], [], [], []],
                                        vectorize=True
@@ -143,41 +123,11 @@ def validate_input_data(input_data: xr.Dataset):
    if len(missing_variables) > 0:
        raise ValueError(f"provided input Dataset does not contain the following required variables: {missing_variables}")


def apply_input_params(input_params: dict) -> dict:
    """
    if input_params is None, return the basecase input_params, if input_params contains keys, then change these keys in the basecase input params to the values provided by the user
    :param input_params:
    :return:
    """
    input_params_basecase = {"hp_minimum_injection_temperature": 15,
                             "return_temperature": 30,
                             "surface_temperature": 10,
                             "degrees_per_km": 31,
                             "max_cooling_temperature_range": 100,
                             "stimKhMax": 20,
                             "use_stimulation": False,
                             "use_heat_pump": False,
                             "calculate_cop": True,
                             "hp_application_mode": False,
                             "hp_direct_heat_input_temp": 70.0,
                             "utc_cutoff_shallow": 5.1,
                             "utc_cutoff_deep": 6.5,
                             "utc_cutoff_depth": 4000.0,
                             "rng_seed": np.random.randint(low=0, high=10000)}

    if input_params is None:  # If no input_params provided, return the basecase
        return input_params_basecase
    else:  # If input_params provided, then go through the keys in the basecase dictionary, and any keys in common can be changed to the user provided input_params
        return {key: input_params.get(key, input_params_basecase[key]) for key in input_params_basecase}




def calculate_performance_of_single_location(mask: float, depth: float, thickness: float, porosity: float, ntg: float, temperature: float, transmissivity: float, transmissivity_with_ntg: float, doublet=None,
                                             input_params: dict = None) -> float:
def calculate_performance_of_single_location(mask: float, depth: float, thickness: float, porosity: float, ntg: float, temperature: float, transmissivity: float, transmissivity_with_ntg: float, doublet: JClass = None ,
                                             utc_properties: JClass = None) -> float:
    """
        Calculate the performance of a doublet at a single location
    :param utc_properties:
    :param mask:
        mask value, if not nan set all output to 0.0
    :param depth:
@@ -196,8 +146,6 @@ def calculate_performance_of_single_location(mask: float, depth: float, thicknes
        transmissivity * ntg of the aquifer
    :param doublet:
        a ThermoGIS doublet class
    :param input_params:
        a dictionary containing extra parameters for running the doublet simulation
    :return:
        the values "power", "heat_pump_power", "capex", "opex", "utc", "npv", "hprod", "cop", "cophp", "pres", "flow_rate", "welld" from the doublet calculation

@@ -209,7 +157,7 @@ def calculate_performance_of_single_location(mask: float, depth: float, thicknes
    if not np.isnan(mask):
        return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0

    set_doublet_parameters(doublet, transmissivity_with_ntg, depth, porosity, ntg, temperature, input_params)
    set_doublet_parameters(doublet, transmissivity_with_ntg, depth, porosity, ntg, temperature, utc_properties)

    # The Java routine which calculates DoubletPerformance, for more detail on the simulation inspect the Java source code
    doublet.calculateDoubletPerformance(-9999.0, thickness, transmissivity)
@@ -218,10 +166,10 @@ def calculate_performance_of_single_location(mask: float, depth: float, thicknes
        return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0

    # calculate net-present-value using the utc-cutoffs
    if depth > input_params["utc_cutoff_depth"]:
        utc_cut = input_params["utc_cutoff_deep"]
    if depth > utc_properties.utcDeepDepth():
        utc_cut = utc_properties.utcCutoffDeep()
    else:
        utc_cut = input_params["utc_cutoff_shallow"]
        utc_cut = utc_properties.utcCutoff()

    hprod = doublet.economicalData.getDiscountedHeatProducedP()
    npv = 1e-6 * (utc_cut - doublet.getUtcPeurctkWh()) * 3.6 * hprod * (1 - doublet.economicalData.getTaxRate())
@@ -247,65 +195,67 @@ def calculate_performance_of_single_location(mask: float, depth: float, thicknes
        "cophp"], output_values["pres"], output_values["flow_rate"], output_values["welld"]


def instantiate_thermogis_doublet(input_params: dict):
def instantiate_utc_properties_builder() -> JClass:
    """
    The UTCPropertiesBuilder is a java class which builds the utc_properties instance; the builder itself can be used to parse scenario xml files or to change specific parameters to the scenario you wish to simulate
    :return: (JClass, UTCPropertiesBuilder) utc_properties_builder
    """
    start_jvm()
    return JClass("thermogis.properties.builders.UTCPropertiesBuilder")()

def instantiate_utc_properties() -> JClass:
    """
    Start the JVM (if necessary) and return a baseCase UTC properties instance by instantiating the UTC Properties Builder and building
    :return:  (JClass, UTCProperties) utc_properties
    """
    utc_properties_builder = instantiate_utc_properties_builder()

    utc_properties = utc_properties_builder.build()

    return utc_properties

def instantiate_thermogis_doublet(utc_properties, rng_seed = None) -> JClass:
    """
    Instantiate a ThermoGIS Doublet class, with a set random seed if provided
    :param utc_properties:
    :param rng_seed:
    :return:
        doublet, a JPype instantiated object of the ThermoGISDoublet class
    """
    # Start the jvm
    start_jvm()
    # Instantiate doublet class
    Logger = JClass("logging.Logger")
    Mockito = JClass("org.mockito.Mockito")
    RNG = JClass("tno.geoenergy.stochastic.RandomNumberGenerator")
    ThermoGISDoublet = JClass("thermogis.calc.doublet.ThermoGisDoublet")
    UTCPropertiesBuilder = JClass("thermogis.properties.builders.UTCPropertiesBuilder")

    # Instantiate the UTC properties class
    propsBuilder = UTCPropertiesBuilder()
    utc_properties = (propsBuilder
                      .setUseStimulation(input_params["use_stimulation"])
                      .setUseHeatPump(input_params["use_heat_pump"])
                      .setCalculateCop(input_params["calculate_cop"])
                      .setHpApplicationMode(input_params["hp_application_mode"])
                      .setHpDirectHeatInputTemp(input_params["hp_direct_heat_input_temp"])
                      .build())

    # Instantiate random number generator:
    rng = RNG(input_params["rng_seed"])

    # Create an instance of a ThermoGISDoublet
    doublet = ThermoGISDoublet(Mockito.mock(Logger), rng, utc_properties)
    if rng_seed is not None:
        rng = RNG(rng_seed)
    else:
        rng = RNG()

    # Set parameters that do not change across cells
    doublet.doubletCalc1DData.setSurfaceTemperature(input_params["surface_temperature"])
    doublet.doubletCalc1DData.setUseHeatPump(input_params["use_heat_pump"])
    doublet.doubletCalc1DData.setDhReturnTemp(input_params["return_temperature"])
    doublet = ThermoGISDoublet(Mockito.mock(Logger), rng, utc_properties)

    # Set parameters that do not change across simulations
    doublet.doubletCalc1DData.setSurfaceTemperature(utc_properties.surfaceTemperature())
    doublet.doubletCalc1DData.setUseHeatPump(utc_properties.useHeatPump())
    doublet.doubletCalc1DData.setDhReturnTemp(utc_properties.dhReturnTemp())

    return doublet

def set_doublet_parameters(doublet, transmissivity_with_ntg: float, depth: float, porosity: float, ntg: float, temperature: float, input_params: dict):
def set_doublet_parameters(doublet, transmissivity_with_ntg: float, depth: float, porosity: float, ntg: float, temperature: float, utc_Properties: JClass):
    """
    For a single location sets the necessary data on the doublet class, to then run a doublet simulation
    :param utc_Properties:
    :param doublet:
    :param transmissivity_with_ntg:
    :param depth:
    :param porosity:
    :param ntg:
    :param temperature:
    :param useStimulation:
    :param stimKhMax:
    :param surface_temperature:
    :param return_temperature:
    :param use_heat_pump:
    :param max_cooling_temperature_range:
    :param hp_minimum_injection_temperature:
    :return:
    """
    if not input_params["use_stimulation"] or transmissivity_with_ntg > input_params["stimKhMax"]:
    if not utc_Properties.useStimulation() or transmissivity_with_ntg > utc_Properties.stimKhMax():
        doublet.setNoStimulation()

    doublet.doubletCalc1DData.setDepth(depth)
@@ -313,10 +263,10 @@ def set_doublet_parameters(doublet, transmissivity_with_ntg: float, depth: float
    doublet.doubletCalc1DData.setNtg(ntg)
    doublet.doubletCalc1DData.setReservoirTemp(temperature)

    if not input_params["use_heat_pump"]:
        doublet.doubletCalc1DData.setInjectionTemp(np.max([temperature - input_params["max_cooling_temperature_range"], input_params["return_temperature"]]))
    elif input_params["use_heat_pump"] and input_params["calculate_cop"] and not input_params["hp_application_mode"]:
        doublet.doubletCalc1DData.setInjectionTemp(doublet.calculateInjectionTempWithHeatPump(temperature, input_params["hp_direct_heat_input_temp"]))
    if not utc_Properties.useHeatPump():
        doublet.doubletCalc1DData.setInjectionTemp(np.max([temperature - utc_Properties.maxCoolingTempRange(),  utc_Properties.dhReturnTemp()]))
    elif utc_Properties.useHeatPump() and utc_Properties.calculateCop() and not utc_Properties.hpApplicationMode():
        doublet.doubletCalc1DData.setInjectionTemp(doublet.calculateInjectionTempWithHeatPump(temperature, utc_Properties.hpDirectHeatInputTemp()))
    else:
        doublet.doubletCalc1DData.setInjectionTemp(np.max([temperature - input_params["max_cooling_temperature_range"], input_params["hp_minimum_injection_temperature"]]))
        doublet.doubletCalc1DData.setInjectionTemp(np.max([temperature - utc_Properties.maxCoolingTempRange(), utc_Properties.hpMinimumInjectionTemperature()]))
+2 −2
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@ def get_jvm_path() -> str:
    """
    jvm_path = Path(os.getenv("JAVA_HOME")) / "bin" / "server" / "jvm.dll"
    if not jvm_path.exists():
        raise FileNotFoundError(f"Java executable not found at {jvm_path}")
        raise FileNotFoundError(f"Java executable not found at {jvm_path}, check that you have installed a JVM correctly and set the JAVA_HOME environment variable.")
    return str(jvm_path)


@@ -20,7 +20,7 @@ def get_thermogis_jar_path() -> str:
    """
    thermogis_jar_path = Path(os.getenv("THERMOGIS_JAR"))
    if not thermogis_jar_path.exists():
        raise FileNotFoundError(f"Java executable not found at {thermogis_jar_path}")
        raise FileNotFoundError(f"Jar file not found at {thermogis_jar_path}, check that you have downloaded the ThermoGIS Jar and set the THERMOGIS_JAR environment variable.")
    return str(thermogis_jar_path)


+200 −0

File added.

Preview size limit exceeded, changes collapsed.

+200 −0

File added.

Preview size limit exceeded, changes collapsed.

+200 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading