TNO Intern

Commit 47d24dee authored by Hen Brett's avatar Hen Brett 🐔
Browse files

Adding the CLI

parent cc43c338
Loading
Loading
Loading
Loading
+72 −27
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ environments:
      - conda: https://conda.anaconda.org/conda-forge/linux-64/cryptography-44.0.3-py313h6556f6e_0.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2
      - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.0-h5888daf_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
@@ -70,6 +71,7 @@ environments:
      - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/nh3-0.2.21-py39h77e2912_1.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.2.5-py313h17eae1a_0.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py313h9c9eb82_1.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.3-py313ha87cce1_3.conda
@@ -107,9 +109,9 @@ environments:
      - conda: https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.10-pyhd8ed1ab_1.conda
      - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/twine-6.1.0-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.3-pyhf21524f_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.3-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.3-h1a15894_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.2-pyhff008b6_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.2-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.2-h801b22e_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.13.2-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.4.0-pyhd8ed1ab_0.conda
@@ -159,6 +161,7 @@ environments:
      - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.3-py313hd8ed1ab_101.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda
@@ -194,6 +197,7 @@ environments:
      - conda: https://conda.anaconda.org/conda-forge/noarch/more-itertools-10.7.0-pyhd8ed1ab_0.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/nh3-0.2.21-py39he870945_1.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.2.5-py313hefb8edb_0.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/openpyxl-3.1.5-py313he57e174_1.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.0-ha4e3fda_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.2.3-py313hf91d08e_3.conda
@@ -230,9 +234,9 @@ environments:
      - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h62715c5_1.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/twine-6.1.0-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.3-pyhf21524f_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.3-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.3-h1a15894_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.2-pyhff008b6_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.2-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.2-h801b22e_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.13.2-pyh29332c3_0.conda
      - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda
      - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda
@@ -747,6 +751,17 @@ packages:
  - pkg:pypi/docutils?source=hash-mapping
  size: 402700
  timestamp: 1733217860944
- conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda
  sha256: 2209534fbf2f70c20661ff31f57ab6a97b82ee98812e8a2dcb2b36a0d345727c
  md5: 71bf9646cbfabf3022c8da4b6b4da737
  depends:
  - python >=3.9
  license: MIT
  license_family: MIT
  purls:
  - pkg:pypi/et-xmlfile?source=hash-mapping
  size: 21908
  timestamp: 1733749746332
- conda: https://conda.anaconda.org/conda-forge/linux-64/expat-2.7.0-h5888daf_0.conda
  sha256: dd5530ddddca93b17318838b97a2c9d7694fa4d57fc676cf0d06da649085e57a
  md5: d6845ae4dea52a2f90178bf1829a21f8
@@ -1772,6 +1787,36 @@ packages:
  - pkg:pypi/numpy?source=hash-mapping
  size: 7108203
  timestamp: 1745120126721
- conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py313h9c9eb82_1.conda
  sha256: 940b03a15c0c0758b352078621ed8b2d982a29af6fccd27fe5c6764727a6f4de
  md5: fa6ac78dbc7b71ca9f599f8a3f4b5b32
  depends:
  - et_xmlfile
  - libgcc >=13
  - python >=3.13.0rc1,<3.14.0a0
  - python_abi 3.13.* *_cp313
  license: MIT
  license_family: MIT
  purls:
  - pkg:pypi/openpyxl?source=hash-mapping
  size: 483786
  timestamp: 1725461014573
- conda: https://conda.anaconda.org/conda-forge/win-64/openpyxl-3.1.5-py313he57e174_1.conda
  sha256: 9c79a234b21e0e46cd710fcfd2458519b9503cddd91508006c7355a8c3c6495f
  md5: 53bb97fa4d46a1bab5fa19560d08b989
  depends:
  - et_xmlfile
  - python >=3.13.0rc1,<3.14.0a0
  - python_abi 3.13.* *_cp313
  - ucrt >=10.0.20348.0
  - vc >=14.2,<15
  - vc14_runtime >=14.29.30139
  license: MIT
  license_family: MIT
  purls:
  - pkg:pypi/openpyxl?source=hash-mapping
  size: 483476
  timestamp: 1725461014622
- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.0-h7b32b05_1.conda
  sha256: b4491077c494dbf0b5eaa6d87738c22f2154e9277e5293175ec187634bd808a0
  md5: de356753cfdbffcde5bb1e86e3aa6cd0
@@ -2143,7 +2188,7 @@ packages:
- pypi: .
  name: pythermogis
  version: 0.1.16
  sha256: dd2a33fbc2e874adf284964e5d506123121a633c11fa41c03fa1cfd957c35340
  sha256: 1aaabb9e4bdcbdc10110f40a1c550c76b8065d2160a81a8c0d00420eeeef82b0
  requires_dist:
  - jpype1>=1.5.2,<2
  - xarray==2024.9.0.*
@@ -2819,49 +2864,49 @@ packages:
  - pkg:pypi/twine?source=hash-mapping
  size: 40401
  timestamp: 1737553658703
- conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.3-pyhf21524f_0.conda
  sha256: 8cd849ceb5e2f50481b1f30f083ee134fac706a56d7879c61248f0aadad4ea5b
  md5: b4bed8eb8dd4fe076f436e5506d31673
- conda: https://conda.anaconda.org/conda-forge/noarch/typer-0.15.2-pyhff008b6_0.conda
  sha256: fa6eeb42e3bddff74126dd61b01b21a3f4f4791368e93bc5a5775563542b2d4e
  md5: 1152565b06e3dc27794c3c11f1050005
  depends:
  - typer-slim-standard ==0.15.3 h1a15894_0
  - typer-slim-standard ==0.15.2 h801b22e_0
  - python >=3.9
  - python
  license: MIT
  license_family: MIT
  purls:
  - pkg:pypi/typer?source=compressed-mapping
  size: 77044
  timestamp: 1745886712803
- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.3-pyh29332c3_0.conda
  sha256: 1768d1d9914d4237b0a1ae8bcb30dace44ac80b9ab1516a2d429d0b27ad70ab9
  md5: 20c0f2ae932004d7118c172eeb035cea
  - pkg:pypi/typer?source=hash-mapping
  size: 76158
  timestamp: 1740697495168
- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-0.15.2-pyh29332c3_0.conda
  sha256: c094713560bfacab0539c863010a5223171d9980cbd419cc799e474ae15aca08
  md5: 7c8d9609e2cfe08dd7672e10fe7e7de9
  depends:
  - python >=3.9
  - click >=8.0.0
  - typing_extensions >=3.7.4.3
  - python
  constrains:
  - typer 0.15.3.*
  - typer 0.15.2.*
  - rich >=10.11.0
  - shellingham >=1.3.0
  license: MIT
  license_family: MIT
  purls:
  - pkg:pypi/typer-slim?source=compressed-mapping
  size: 46152
  timestamp: 1745886712803
- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.3-h1a15894_0.conda
  sha256: 72f77e8e61b28058562f2782cf32ff84f14f6c11c6cea7a3fe2839d34654ea45
  md5: 120216d3a2e51dfbb87bbba173ebf210
  - pkg:pypi/typer-slim?source=hash-mapping
  size: 45866
  timestamp: 1740697495167
- conda: https://conda.anaconda.org/conda-forge/noarch/typer-slim-standard-0.15.2-h801b22e_0.conda
  sha256: 79b6b34e90e50e041908939d53053f69285714b0082a0370fba6ab3b38315c8d
  md5: ea164fc4e03f61f7ff3c1166001969af
  depends:
  - typer-slim ==0.15.3 pyh29332c3_0
  - typer-slim ==0.15.2 pyh29332c3_0
  - rich
  - shellingham
  license: MIT
  license_family: MIT
  purls: []
  size: 5411
  timestamp: 1745886712803
  size: 5409
  timestamp: 1740697495168
- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.13.2-pyh29332c3_0.conda
  sha256: a8aaf351e6461de0d5d47e4911257e25eec2fa409d71f3b643bb2f748bde1c08
  md5: 83fc6ae00127671e301c9f44254c31b8
+2 −1
Original line number Diff line number Diff line
@@ -60,4 +60,5 @@ twine = ">=6.1.0,<7"
pip = ">=25.0.1,<26"
sphinx = ">=8.2.3,<9"
sphinx_rtd_theme = ">=3.0.1,<4"
typer = ">=0.15.3,<0.16"
 No newline at end of file
openpyxl = ">=3.1.5,<4"
typer = "==0.15.2"
+45 −35
Original line number Diff line number Diff line
import time
import traceback
import typing as t
from pathlib import Path
import json
from typing import Optional, List

import numpy as np
import xarray as xr
import typer

@@ -10,27 +12,6 @@ app = typer.Typer(pretty_exceptions_enable=False)
calculate = typer.Typer(pretty_exceptions_enable=False)
app.add_typer(calculate, name="calculate")

def run_script(func: t.Callable[[str], None], config_file_path: str) -> None:
    """
    Wrapper around the script functions, which adds error handling.
    """
    status = "not started"
    module_name = getattr(func, "__module__", "unknown").split(".")[-1]
    typer.echo(f"Start of the {module_name} script")

    start_time = time.time()
    try:
        func(config_file_path)
        status = "finished"
    except Exception as e:
        typer.echo(f"An error occurred: {e}", err=True)
        typer.echo(traceback.format_exc(), err=True)
        status = "failed"
    finally:
        end_time = time.time()
        duration = end_time - start_time
        typer.echo(f"{module_name} {status} after: {duration:.2f} seconds")


@calculate.command()
def info():
@@ -38,26 +19,55 @@ def info():
    print("You can run commands by typing:\n")
    print("\t tg simulate-doublet")


@calculate.command()
def simulate_doublet(
        depth: float,
        thickness_mean: float,
        thickness_sd: float,
        ntg: float,
        porosity: float,
        permeability:float
        depth: float = typer.Option(500, help="The depth of the aquifer, +ive downwards, units: [m]"),
        temperature: float = typer.Option(None, help="The temperature of the aquifer, if not provided this will be calculated using a Temperature gradient of 8°C + 31°C/km with the depth parameter,"),
        thickness: float = typer.Option(500, help="The thickness of the aquifer, units: [m]"),
        ntg: float = typer.Option(0.5, help="The net-to-gross of the aquifer, units: [0-1]"),
        porosity: float = typer.Option(0.5, help="The porosity of the aquifer, between 0-1 (1 = 100%), units: [0-1]"),
        permeability:float = typer.Option(0.5, help="The permeability of the aquifer, units: [mD]"),
        output_file: Path = typer.Option(None, help="A file to output the data to, accepted filetypes are: .nc, .csv, .xlsx, .json"),
        verbose: bool = typer.Option(False, help="print the input data and output data to terminal")
) -> None:
    """Simulate a Geothermal Doublet, with no stochastics (only a P50 simulation)"""

    # instantiate the input_data dataset from the input
    input_data = xr.Dataset({
        "depth": ((), depth),
        "thickness_mean": ((), thickness_mean),
        "thickness_sd": ((), thickness_sd),
        "thickness_mean": ((), thickness),
        "thickness_sd": ((), 0.0),
        "ntg": ((), ntg),
        "porosity": ((), porosity),
        "ln_permeability_mean": ((), permeability),
        "ln_permeability_mean": ((), np.log(permeability)),
        "ln_permeability_sd": ((), 0.0),
    })
    run_script(calculate_doublet_performance, input_data)

    if temperature is not None:
        input_data["temperature"] = ((), temperature)

    if verbose:
        print("\n---simulation input---")
        print(input_data)

    output_data = calculate_doublet_performance(input_data)

    if verbose:
        print("\n---simulation output---")
        [print(f"{var}:\n{output_data[var].values}\n") for var in output_data.data_vars]

    if output_file is not None:
        if output_file.suffix == ".nc":
            output_data.to_netcdf(output_file)
        elif output_file.suffix == ".csv":
            output_data.to_dataframe().to_csv(output_file)
        elif output_file.suffix == ".xlsx":
            output_data.to_dataframe().to_excel(output_file)
        elif output_file.suffix == ".json":
            with open(output_file, "w") as f:
                json.dump(output_data.to_dict(), f)
        else:
            typer.echo(typer.style("Error: Invalid output file type provided, currently supported: .nc, .csv, .json", fg=typer.colors.RED, bold=True))


if __name__ == "__main__":
+11 −1
Original line number Diff line number Diff line
@@ -12,7 +12,8 @@ from thermogis_classes.utc_properties import instantiate_utc_properties_builder
def calculate_doublet_performance(input_data: xr.Dataset,
                                  utc_properties = None,
                                  rng_seed = None,
                                  p_values: List[float] = [50.0]) -> xr.Dataset:
                                  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)

@@ -122,6 +123,15 @@ 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}")

    if (input_data["thickness_mean"] < 0.0).any():
        raise ValueError(f"provided input Dataset contains a negative thickness_mean value. thickness_mean must always be >= 0.0 in [m]")
    if (input_data["thickness_sd"] < 0.0).any():
        raise ValueError(f"provided input Dataset contains a negative thickness_sd value. thickness_sd must always be >= 0.0 in [m]")
    if (input_data["porosity"] > 1.0).any() or (input_data["porosity"] < 0.0).any():
        raise ValueError(f"provided input Dataset contains a porosity value > 1.0. porosity must always be between 0.0 and 1.0 (100% porosity = 1.0)")
    if (input_data["ntg"] > 1.0).any() or (input_data["ntg"] < 0.0).any():
        raise ValueError(f"provided input Dataset contains a ntg value > 1.0. ntg must always be between 0.0 and 1.0 (100% ntg = 1.0)")

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:
    """
+13 −4
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ def generate_thickness_permeability_transmissivity_for_pvalues(thickness_mean: f
    -transmissivity

    Note: Transmissivity is a function of ln(permeability) * thickness, and so no analytical solution exists to combine these two probability distributions and so the transmissivity distribution has to be sampled.
    UNLESS pvalue = P50; then the analytical solution for Transmissivity = (np.log(mean_thickness) + ln_permeability_mean)

    :param thickness_mean:
    :param thickness_sd:
@@ -22,18 +23,26 @@ def generate_thickness_permeability_transmissivity_for_pvalues(thickness_mean: f
    """
    if np.isnan(thickness_mean) | np.isnan(thickness_sd) | np.isnan(ln_permeability_mean) | np.isnan(ln_permeability_sd):
        return np.full(len(p_values), np.nan), np.full(len(p_values), np.nan), np.full(len(p_values), np.nan)
    if len(p_values) == 1 and p_values[0] == 50:    # The only value where we analytically know the output is the P50
        return np.array([thickness_mean]), np.array([ln_permeability_mean]), np.array([np.exp(np.log(thickness_mean) + ln_permeability_mean)])

    p_value_fractions = 1 - p_values / 100


    if thickness_sd == 0:
        thickness_pvalues =  np.full(len(p_value_fractions), thickness_mean)
        thickness_samples = np.full(nSamples, thickness_mean)
    else:
        thickness_dist = stats.norm(loc=thickness_mean, scale=thickness_sd)
        thickness_pvalues = thickness_dist.ppf(p_value_fractions)
        thickness_samples = thickness_dist.rvs(nSamples)
        thickness_samples = np.clip(thickness_samples, a_min=0.01, a_max=None)


    ln_permeability_dist = stats.norm(loc=ln_permeability_mean, scale=ln_permeability_sd)
    permeability_pvalues = np.exp(ln_permeability_dist.ppf(p_value_fractions))

    # Sampling method for transmissivity
    thickness_samples = thickness_dist.rvs(nSamples)
    thickness_samples = np.clip(thickness_samples, a_min=0.01, a_max=None)
    transmissivity_samples = np.sort(np.exp(ln_permeability_dist.rvs(nSamples) + np.log(thickness_samples)))

    sample_indexes = np.array(p_value_fractions * nSamples)