Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## Code changes


- Added modern `run_rhime` and shared-basis `run_rhime_multisector` pipelines, RHIME CLI entry points, RHIME config template, modern result/spec objects, and focused tests for the new public runners. [#398](https://github.com/openghg/openghg_inversions/issues/398)
- Made concat-gather handling of mismatched site data variables order-independent, added an opt-in drop policy used by `make_inv_inputs`, and added lightweight regression tests for issue #394. [#394](https://github.com/openghg/openghg_inversions/issues/394)
- Fix bug which was assigninig the wrong times to inversion flux outputs in non-standard cases, such as 3-monthly inversions. [#PR 387](https://github.com/openghg/openghg_inversions/pull/387)
- Fix small bug where postprocessing was failing if country codes in file didn't match exactly those in `paris_regions_dict`. [#PR 377](https://github.com/openghg/openghg_inversions/pull/377)
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,37 @@ Solutions to this are:

For an overview of OpenGHG inversions, see this [primer](docs/getting_started.md).

### Modern RHIME entry points

New RHIME runs can be launched without calling an internal source file path:

```python
from openghg_inversions.rhime import run_rhime, run_rhime_multisector

result = run_rhime(
species="ch4",
sites=["TAC"],
averaging_period=["1h"],
domain="EUROPE",
start_date="2019-01-01",
end_date="2019-01-02",
output_path="outputs",
output_name="example",
flux_sources=["total-ukghg-edgar7"],
)
```

For SLURM batch scripts and installed environments, use the console entry point:

```bash
openghg-inversions run-rhime 2019-01-01 2019-01-02 -c rhime.ini --output-path outputs
openghg-inversions run-rhime-multisector 2019-01-01 2019-01-02 -c rhime_multisector.ini
```

The new RHIME config template is available at
`openghg_inversions/config/templates/rhime_template.ini`. New configs should use
`flux_sources`; legacy `emissions_name` is accepted when `flux_sources` is absent.

### Passing parameters to the inversion

Keyword arguments are propagated as follows:
Expand Down
1 change: 1 addition & 0 deletions openghg_inversions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""OpenGHG inversions."""
84 changes: 84 additions & 0 deletions openghg_inversions/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Command line interface for OpenGHG inversions."""

from __future__ import annotations

import argparse
import json
from typing import Any


def _add_run_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("start", help="Start date string of the format YYYY-MM-DD", nargs="?")
parser.add_argument("end", help="End date string of the format YYYY-MM-DD", nargs="?")
parser.add_argument("-c", "--config", help="Name including path of configuration file", required=True)
parser.add_argument(
"--kwargs",
type=json.loads,
help="Pass keyword arguments to the RHIME function, e.g. '{\"nit\": 10}'.",
)
parser.add_argument("--output-path", help="Path to write results to.")


def _command_kwargs(args: argparse.Namespace) -> dict[str, Any]:
"""Create keyword overrides from parsed CLI arguments."""
kwargs: dict[str, Any] = {}
if args.start:
kwargs["start_date"] = args.start
if args.end:
kwargs["end_date"] = args.end
if args.output_path:
kwargs["output_path"] = args.output_path
if args.kwargs:
kwargs.update(args.kwargs)
return kwargs


def _run_rhime_command(args: argparse.Namespace) -> None:
"""Run the standard RHIME command with lazy imports for fast help output."""
from openghg_inversions.rhime import run_rhime

run_rhime(config_file=args.config, **_command_kwargs(args))


def _run_rhime_multisector_command(args: argparse.Namespace) -> None:
"""Run the multi-sector RHIME command with lazy imports for fast help output."""
from openghg_inversions.rhime import run_rhime_multisector

run_rhime_multisector(config_file=args.config, **_command_kwargs(args))


def build_parser() -> argparse.ArgumentParser:
"""Build the OpenGHG inversions CLI argument parser.

Returns:
Configured argument parser.
"""
parser = argparse.ArgumentParser(prog="openghg-inversions", description="OpenGHG inversions CLI")
subparsers = parser.add_subparsers(dest="command", required=True)

run_parser = subparsers.add_parser("run-rhime", help="Run a standard RHIME inversion")
_add_run_args(run_parser)
run_parser.set_defaults(func=_run_rhime_command)

run_multi_parser = subparsers.add_parser(
"run-rhime-multisector", help="Run a shared-basis multi-sector RHIME inversion"
)
_add_run_args(run_multi_parser)
run_multi_parser.set_defaults(func=_run_rhime_multisector_command)

return parser


def main(argv: list[str] | None = None) -> None:
"""Run the OpenGHG inversions CLI.

Args:
argv: Optional argument vector. Defaults to ``sys.argv`` when omitted.
"""
parser = build_parser()
args = parser.parse_args(argv)
args.func(args)


if __name__ == "__main__":
main()
78 changes: 78 additions & 0 deletions openghg_inversions/config/templates/rhime_template.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
; =======================================================================================
; OpenGHG Inversions RHIME configuration file
; =======================================================================================
; New RHIME configs prefer flux_sources. Legacy emissions_name is still accepted when
; flux_sources is absent.

[INPUT.MEASUREMENTS]
species = ""
sites = []
averaging_period = []
start_date = " "
end_date = " "
inlet = None
instrument = None
calibration_scale = None
obs_data_level = None
filters = []

[INPUT.STORES]
bc_store = "user"
obs_store = "user"
footprint_store = "user"
emissions_store = "user"

[INPUT.PRIORS]
domain = " "
met_model = None
fp_model = None
fp_height = None
fp_species = None
flux_sources = []
bc_input = None

[INPUT.BASIS_CASE]
basis_algorithm = "weighted"
bc_basis_case = "NESW"
fp_basis_case = None
nbasis = 100
basis_directory = None
bc_basis_directory = None
country_file = None
country_directory = None

[RHIME.PDF]
x_prior = {"pdf": "lognormal", "mean": 1.0, "stdev": 1.0}
sector_priors = None
bc_prior = {"pdf": "truncatednormal", "mu": 1.0, "sigma": 0.05, "lower": 0.0}
sigma_prior = {"pdf": "uniform", "lower": 0.1, "upper": 3.0}
add_offset = False
offset_prior = {"pdf": "normal", "mu": 0, "sigma": 1}

[RHIME.SPLIT]
bc_freq = None
sigma_freq = None
sigma_per_site = True

[RHIME.ITERATIONS]
nit = 1000
burn = 0
tune = 1000
nchain = 4

[RHIME.OPTIONS]
averaging_error = True
min_error = 0.0
fix_basis_outer_regions = False
use_bc = True
nuts_sampler = "pymc"
save_trace = False
save_inversion_output = True
pollution_events_from_obs = False
no_model_error = False
sampler_kwargs = {}

[RHIME.OUTPUT]
output_path = " "
output_name = "rhime"
output_format = "inv_out"
12 changes: 8 additions & 4 deletions openghg_inversions/inversion_data/get_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,21 @@ def add_obs_error(sites: list[str], fp_all: dict, add_averaging_error: bool = Tr
# TODO: do we want to fill missing values in repeatability or variability?
for site in sites:
ds = fp_all[site]
mf_long_name = ds.mf.attrs.get("long_name", "")
mf_units = ds.mf.attrs.get("units", None)

variability_missing = False
if "mf_variability" not in ds:
ds["mf_variability"] = xr.zeros_like(ds.mf)
ds["mf_variability"].attrs["long_name"] = ds.mf.attrs.get("long_name", "") + "_variability"
variability_missing = True
ds["mf_variability"].attrs["long_name"] = mf_long_name + "_variability"
ds["mf_variability"].attrs["units"] = mf_units

if "mf_repeatability" not in ds:
if variability_missing:
raise ValueError(f"Obs data for site {site} is missing both repeatability and variability.")

ds["mf_repeatability"] = xr.zeros_like(ds.mf_variability)
ds["mf_repeatability"].attrs["long_name"] = ds.mf.attrs.get("long_name", "") + "_repeatability"

ds["mf_error"] = ds["mf_variability"]

Expand All @@ -90,8 +92,10 @@ def add_obs_error(sites: list[str], fp_all: dict, add_averaging_error: bool = Tr
else:
ds["mf_error"] = ds["mf_repeatability"]

ds["mf_error"].attrs["long_name"] = ds.mf.attrs.get("long_name", "") + "_error"
ds["mf_error"].attrs["units"] = ds.mf.attrs.get("units", None)
ds["mf_repeatability"].attrs["long_name"] = mf_long_name + "_repeatability"
ds["mf_repeatability"].attrs["units"] = mf_units
ds["mf_error"].attrs["long_name"] = mf_long_name + "_error"
ds["mf_error"].attrs["units"] = mf_units

# warnings/info for debugging
err0 = (ds["mf_error"] == 0) | (
Expand Down
9 changes: 6 additions & 3 deletions openghg_inversions/inversion_inputs.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Functions for creating the inputs needed by PyMC."""

import datetime as dt
import numbers
from typing import Any, Iterable, Literal

import numpy as np
Expand Down Expand Up @@ -116,12 +117,14 @@ def make_sigma_freq(
def add_min_error(
ds: xr.Dataset,
fp_data: dict[str, Any],
min_error: str | dict[str, float] | float = 0.0,
min_error: str | dict[str, float] | int | float = 0.0,
min_error_per_site: bool = True,
) -> xr.Dataset:
"""Add min_error to combined Dataset."""
min_error_data: xr.DataArray | float | np.ndarray
if isinstance(min_error, float) or (isinstance(min_error, np.ndarray) and min_error.ndim == 0):
if isinstance(min_error, numbers.Real) and not isinstance(min_error, bool):
min_error_data = float(min_error) * xr.ones_like(ds.mf)
elif isinstance(min_error, np.ndarray) and min_error.ndim == 0:
min_error_data = min_error * xr.ones_like(ds.mf)
elif isinstance(min_error, dict):
sites = [k for k in fp_data if not k.startswith(".")]
Expand Down Expand Up @@ -274,7 +277,7 @@ def make_inv_inputs(
sites: list[str] | None = None,
bc_freq: Literal["monthly"] | str | None = None,
sigma_freq: Literal["monthly"] | str | None = None,
min_error: str | dict[str, float] | float = 0.0,
min_error: str | dict[str, float] | int | float = 0.0,
min_error_per_site: bool = True,
start_date: DatetimeLike | None = None,
) -> xr.Dataset:
Expand Down
Loading
Loading