Concrete RHIME Model#
This page makes the model graph behind run_rhime() and
run_rhime_multisector() explicit. It has two purposes:
show the concrete statistical model and its PyMC names;
show how the standard model can be reconstructed from public component helpers.
The current builders#
Each recipe directly composes one readable concrete graph. The standard graph
lives beside run_rhime() in openghg_inversions.rhime.standard; the
multisector graph lives beside run_rhime_multisector() in
openghg_inversions.rhime.multisector:
RhimeModelSpec + canonical inversion inputs
-> concrete standard or multisector builder
-> public linear-component helpers
-> total flux contribution, mu
-> boundary, offset, error, and likelihood components
-> PyMC model
Stability contract#
The concrete builders are the production reference implementations. Their explicit PyMC code is the primary model definition for scientific review, auditing, and user confidence. Shared source selection and ordinary scientific components remain separate helpers, while model-specific composition stays in the recipe that runs it.
Standard single-flux model#
Let H be the flux sensitivity matrix with dimensions
(region, nmeasure) in canonical inversion inputs. The builder transposes it
to observation-first order before registering it as PyMC data. The flux model
is
With the default prior, x is the physical deterministic transform of a
standard-normal x_latent:
When boundary-condition scaling is enabled,
An optional global, site, or site-by-period offset contributes offset. The
mean of the observed distribution is therefore
where omitted components are left out of the sum.
The configuration template explicitly selects
mismatch_model="pollution_event". This preserves the fractional-error
equation used by run_hbmcmc.py for users who start from that template.
Direct runner calls must make a likelihood selection explicitly. The concrete
model recipe has no mismatch default: parameter resolution converts the
selector to PollutionEventSettings in the serializable model specification
before construction. Select
mismatch_model="additive_sigma" for an absolute concentration-scale
mismatch instead; this is a resolved model option and does not use the custom
likelihood_builder extension point. Additive sigma does not select the
prepared min_error input unless use_minimum_error_floor=True is also
set. Aggregation error is disabled by default. Let
\(P\) be the pollution event and let sigma be the observation-aligned
fractional model-error parameter. With the default
pollution_events_from_obs=False,
When pollution_events_from_obs=True, the modern recipe derives the
pollution event from observations after removing the complete baseline,
including any boundary and offset contributions. Thus
\(P=|Y-(\mu_{bc}+\mathrm{offset})|\) when a baseline exists; without one,
it uses \(P=|Y|+10^{-6}\operatorname{mean}(Y)\).
The run_hbmcmc.py compatibility path retains its historical boundary-only
variant: \(P=|Y-\mu_{bc}|\), even when an offset is also included in
\(\mu_{\mathrm{obs}}\). That exception preserves existing configurations;
it is not the scientific default for new RHIME recipes.
Select mismatch_model="fixed_error" to omit inferred mismatch error. Its
likelihood scale uses the reported observation error and does not select
min_error.
run_hbmcmc.py privately preserves the different historical floor and
unused-variable details of its two no_model_error routes.
Aggregation covariance is an explicit advanced opt-in. If a caller selects a prepared covariance \(C_{agg}\) with marginal variance \(v_{agg}=\operatorname{diag}(C_{agg})\), the marginal floor and observed distribution become
Selecting aggregation error says only that this fixed covariance should enter the likelihood. It does not perform, imply, or verify a coherent prior and forward-model transformation. Covariance obtained by marginalising native states must therefore be supplied together with the matching transformed prior and forward operator. Merely finding an aggregation-error array in prepared inputs does not opt a run into this model.
The default priors are:
Quantity |
Prior |
PyMC variables |
|---|---|---|
Flux scaling |
Lognormal with mean 1 and standard deviation 1, reparameterized |
|
Boundary scaling |
Truncated normal with mean 1, standard deviation 0.05, and lower bound 0 |
|
Model error |
Uniform from 0.1 to 3 |
|
Optional site/global offset |
Normal with mean 0 and standard deviation 1 |
|
This table describes the Python builder default used when x_prior is
omitted. The shipped RHIME config template instead supplies an explicit
x_prior without reparameterise=True. That config therefore creates
x directly, without x_latent. Add "reparameterise": True to the
config prior to request the API-default parameterization shown here.
The important default model-data and deterministic names are:
Name |
Role |
Canonical input |
|---|---|---|
|
Flux sensitivity data |
|
|
Flux contribution |
|
|
Boundary sensitivity data |
|
|
Boundary contribution |
|
|
Observed mole fraction data |
|
|
Observation error data |
|
|
Minimum error data |
|
|
Observation-aligned error scale |
RHIME error model |
|
Observed random variable |
Normal likelihood |
CO2 coherent-reduction model#
The public openghg_inversions.rhime.build_co2_model() recipe
consumes the labelled products of a coherent state reduction. Let
H_alpha be the retained-state sensitivity, m_alpha and C_alpha its
arithmetic prior mean and covariance, and b_fixed the fixed affine prior
contribution. The core retained-state terms are
The affine term is part of coherent prior closure; it is not an atmospheric
boundary condition. An explicit state-activity policy omits inactive elements
from the sampled correlated vector while restoring their exact fixed values
in the full public flux_scaling vector and in co2_flux_contribution.
The CO2 likelihood uses the explicit AggregationError
selected from prepared inputs. With reported observation standard deviation
s_y, optional known mismatch s_fixed, and optional inferred additive
mismatch sigma, its covariance is
after applying min_error as a floor on the total marginal standard
deviation. OpenGHG Inversions does not default s_fixed to 1 ppm. The
Verification Games fixed-only policy passes fixed_model_mismatch=1.0 and
no_model_error=True visibly. A runnable CO2 configuration and resolver are
tracked in OPE-79.
CO2 grouped inner and outer states#
Same-grid inner and outer fluxes use one ordinary retained state and one
co2_sensitivity. Their geography is labelled metadata, not a second model
component. The state axis retains basis_group, basis_partition, and
region_in_partition coordinates; select basis_group == "outer" rather
than inferring outer entries from integer ranges or positions.
Fixed versus inferred outer entries use the ordinary
StateActivity contract. To preserve the
fixed-at-one behavior, pass
StateActivity(fixed_groups=("outer",), fixed_value=1.0). Leaving the outer
group active infers those entries with the same correlated LogNormal state as
the inner entries. Exact-zero sensitivity pruning composes with either policy.
The builder accepts one prepared
CorrelatedLognormalPrior over the
complete ordered state. Group-specific arithmetic moments must be aligned and
assembled before graph construction. A full prior may contain inner/outer
cross-covariance; it remains part of that one prior and is not also added to
the observation covariance.
co2_flux_contribution contains the complete flux prediction. The model does
not construct separate inner or outer terms. Outputs can reconstruct either
view by selecting the corresponding basis_group entries from the stored
sensitivity and flux_scaling state.
With optional boundary conditions and offset present, the likelihood mean is
An output may report a composite baseline by reconstructing the outer flux view
and adding it to mu_bc + offset. This is an output policy only: boundary
conditions, offsets, and outer flux remain distinct scientific terms. Sector
combinations are likewise a general state-grouping choice rather than an
outer-region model option.
The model builder accepts explicit scientific arrays rather than a dataset.
For durable prepared artifacts, openghg_inversions.rhime.run_rhime_co2()
is the public replay seam: it validates and materializes the selected arrays,
resolves aggregation error, calls the explicit builder, samples, and stores a
JSON variable-role and model-provenance manifest on the returned
InferenceData. A prepared fixed_model_mismatch is preserved when the
runner argument is None; an explicit scalar or labelled vector overrides
it. Persist gathered-state traces with
openghg_inversions.serialization.save_inferencedata(), which uses the
same MultiIndex-safe boundary as standard and multisector RHIME outputs.
The current prepared-input runner does not accept or construct an
outer-specific object. It constructs the complete retained prior from the
prepared arithmetic mean and covariance, then forwards the prepared activity
policy to the builder.
Equivalent construction from public helpers#
The default production builder constructs the graph in the same direct style shown below. A researcher does not need to construct a private plan to write an equivalent concrete single-flux model. The following uses public component helpers:
from openghg_inversions.models import (
add_linear_component,
prepare_linear_sensitivity,
registered_model,
)
from openghg_inversions.models.pollution_event import add_pollution_event_likelihood
from openghg_inversions.observation_error import resolve_aggregation_error
from openghg_inversions.sigma import SigmaAlignment
x_prior = {
"pdf": "lognormal",
"mean": 1.0,
"stdev": 1.0,
"reparameterise": True,
}
bc_prior = {
"pdf": "truncatednormal",
"mu": 1.0,
"sigma": 0.05,
"lower": 0.0,
}
sigma_prior = {"pdf": "uniform", "lower": 0.0, "upper": 0.1}
sigma_alignment = SigmaAlignment.from_observations(
inv_inputs["mf"],
frequency=None,
per_site=True,
)
flux_sensitivity = prepare_linear_sensitivity(inv_inputs["H"])
boundary_sensitivity = prepare_linear_sensitivity(inv_inputs["H_bc"])
with registered_model() as model:
flux = add_linear_component(
flux_sensitivity,
data_name="hx",
prior_args=x_prior,
var_name="x",
output_name="mu",
output_dim="nmeasure",
)
boundary = add_linear_component(
boundary_sensitivity,
data_name="hbc",
prior_args=bc_prior,
var_name="bc",
output_name="mu_bc",
output_dim="nmeasure",
)
pollution_mean = flux.output
baseline_mean = boundary.output
modelled_mean = pollution_mean + baseline_mean
add_pollution_event_likelihood(
observations=inv_inputs["mf"],
observation_error=inv_inputs["mf_error"],
minimum_error=inv_inputs["min_error"],
aggregation_error=resolve_aggregation_error(inv_inputs, "none"),
mean=modelled_mean,
pollution_mean=pollution_mean,
pollution_event_baseline=baseline_mean,
sigma_alignment=sigma_alignment,
sigma_prior=sigma_prior,
power=1.99,
pollution_events_from_obs=False,
no_model_error=False,
output_dim="nmeasure",
)
This example is deliberately concrete and editable. It is suitable when a
model developer needs to change graph construction directly. It does not
automatically participate in the complete run_rhime output pipeline; that
pipeline still selects one of the built-in model builders.
prepare_linear_sensitivity is the single owning boundary for exact-zero column
inspection. It removes those columns from the backend sensitivity while retaining
their full labelled-state mapping; state_activity=None therefore samples
every retained state. An explicit StateActivity fixes or groups scientific
states but cannot restore a structurally absent column. For shared or
correlated states, use apply_linear_sensitivity to apply another prepared
forward operator without constructing a second prior.
Multisector model#
For the current shared-basis multisector model, each sector has an independent state and forward contribution:
If the normalized PyMC suffix for sector s is ff, its variables are
x_ff and mu_ff and its sensitivity data is hx_ff. Source values select
the corresponding H slices; sector names provide model identities. They
are not required to be the same strings.
Every sector state and every reparameterization-generated latent must have a unique backend name. Concrete composition relies on PyMC to reject duplicate generated names.
Names and generated names#
PyMC model variables currently share one flat effective namespace. A prior can
create more names than the requested base name. In particular, a
reparameterized lognormal requested as x_ff creates both x_ff and
x_ff_latent.
Both names should be treated as reserved for that prior. No data variable, other state, forward-term deterministic, or total should use either name. PyMC enforces this during concrete composition. The shared observation-component helper still relies on conventional names for boundary, offset, error, and likelihood components; there is not yet one allocator for the complete model namespace.
Generated-name reporting, whole-model allocation, and component namespaces are not implemented. They are tracked in issue #532.
Alternative models and likelihoods#
Direct-Python likelihood builders enter through run_rhime,
run_rhime_multisector, or run_rhime_from_prepared_inputs.
Complete-model builders remain available only at the prepared-input boundary.
Callables are never read from configuration or stored on RhimeModelSpec or
RhimeRunSpec, so model and run specs remain serializable. There is no
entry-point or config-file plugin registry.
A concrete recipe owns the complete forward-model mean: pollution, baseline,
and optional offset contributions are composed visibly and packaged as named
forward terms before the shared built-in dispatcher is invoked. The runner
stores one typed settings value on RhimeModelSpec; the dispatcher calls the
ordinary built-in equation with only that likelihood’s inputs. A custom caller
instead supplies a mean-only callable. Every likelihood receives the completed
concentration, prepared observations and reported observation error, a
validated AggregationError, and output dimension. Built-in pollution-event
scaling additionally receives the named pollution and baseline terms.
likelihood_kwargs is reserved for custom callables.
The builder adds and returns the canonical observed variable y and also
adds the canonical marginal error scale epsilon.
likelihood_kwargs is valid only when a custom likelihood builder is active.
The runner expands the mapping into that callable and records it with the
callable identity in result and saved builder metadata.
The editable example in Customising RHIME implements a fixed-error Student-t likelihood using only those common inputs. Pass it directly to the ordinary runner:
from my_project.likelihoods import likelihood_builder
from openghg_inversions.rhime import run_rhime
result = run_rhime(
config_file="config.ini",
mismatch_model=None,
likelihood_builder=likelihood_builder,
)
This example supports independent fixed aggregation-error representations. A
custom likelihood with additional options declares them in its own signature,
and the runner supplies only those values through likelihood_kwargs.
Pass add_offset=True and offset_args={"per_site": False} as Python or
configuration options to combine it with one global scalar offset. The
default per_site=True retains the existing site or site-period offset
design.
Keep scientific customization implementations in one tested location. The Customising RHIME guide contains the editable Student-t example and the minimal ordinary-runner call; this concrete-model page does not duplicate it.
Ordinary likelihood builders keep the y and epsilon names used by
sampling and postprocessing. The runner records the likelihood builder’s
module and qualified name, so direct-Python likelihoods remain identifiable in
persisted inversion outputs.
A complete model builder is an advanced escape hatch available only through
run_rhime_from_prepared_inputs. It receives a
RhimeModelBuilderContext containing the validated RhimePreparedInputs,
updated RhimeRunSpec, and validated single- versus multi-sector mode, and
returns a RhimeModelBuildResult. Ordinary standard and multisector recipes
never construct or consume this context. Complete builders also bypass the
ordinary recipe-owned materialization step, so they must select, validate, and
materialize any lazy arrays they consume:
from importlib.metadata import version
import pymc as pm
from openghg_inversions.rhime import (
RhimeModelBuilderContext,
RhimeModelBuildResult,
run_rhime_from_prepared_inputs,
)
from openghg_inversions.models import (
add_coords,
registered_model,
)
def complete_model(
context: RhimeModelBuilderContext,
) -> RhimeModelBuildResult:
data = context.prepared_inputs.inv_inputs
with registered_model() as model:
add_coords(data.coords, model_dims=("nmeasure",))
mean = pm.Normal("custom_mean", mu=0.0, sigma=10.0)
pm.Normal(
"custom_y",
mu=mean,
sigma=data.mf_error.values,
observed=data.mf.values,
dims="nmeasure",
)
return RhimeModelBuildResult(
model=model,
variable_roles={
"observation": "mf",
"observation_error": "mf_error",
"concentration": "custom_y",
},
supported_output_formats=("none", "inv_out"),
metadata={
"package": "my-rhime-models",
"version": version("my-rhime-models"),
"model": "complete_model_v1",
},
)
result = run_rhime_from_prepared_inputs(
prepared_inputs=prepared,
run_spec=run_spec,
model_builder=complete_model,
)
The compatibility rules are explicit:
a complete builder must return a concrete
pm.Modeland a non-empty role manifest;concentrationis required;every declared role name must exist in either the model or prepared inversion inputs;
builders must construct their graph with
registered_model()before callingadd_coordsor public model components, soRhimeSamplercan restore MultiIndexes and auxiliary scientific coordinates;builder metadata must be JSON serializable, and external packages should record their package version and stable model identity there;
custom builders support only
output_format="none"unless they explicitly declare more formats; declaring a format promises that the trace, roles, basis layout, and variables required by that output really are present;model_builderandlikelihood_builderare mutually exclusive; anda component that does not exist, such as inferred model error in a fixed-error model, is omitted from the role manifest rather than represented by a magic name.
The built-in standard and multisector builders produce the same
RhimeModelBuildResult contract on RhimeResult.model_build_result. Their
sector roles use keys such as flux_scale:FF and
flux_contribution:FF. The low-level recipe builders return a plain
pm.Model; the corresponding *_model_result wrappers add the runner and
output metadata contract.
Structurally distinct observation-channel models belong in readable named recipes with concrete builders. The active design guidance is Developing RHIME models; the retired semantic-model work in issue #528 remains research evidence rather than production architecture.
Customization boundaries#
The current customization levels are:
- Supported high-level options
Priors, boundary conditions, offsets, sigma alignment, and existing error options supplied through the public RHIME builders and model spec.
- Supported low-level components
Public functions in
openghg_inversions.modelscan be composed inside a user-ownedpm.Modelas shown above.- Recipe-local model composition
Copy or modify the readable concrete builder in
openghg_inversions.rhime.standardoropenghg_inversions.rhime.multisectorwhen an existing option or shared component is insufficient.