Grouped basis and state metadata#
Basis functions can be constructed separately for different parts of the full domain—for example, a detailed inner region and a coarser outer region—and then combined into a single basis array. The combined array remembers which group and partition each basis region came from, so states can still be selected and analysed by group after they have been assembled.
BasisLayout provides this assembly step. It combines disjoint,
partition-local label maps into the one flat basis consumed by a bucket
operator. Alongside that flat map it produces the stable state metadata
basis_group, basis_partition, and region_in_partition.
This is currently a lower-level Python construction API. Import it from
openghg_inversions.basis.layout when an application has already generated
or loaded the partition label arrays. The implementation is eager and
in-memory, and it does not infer a remainder partition.
The cells on this page are executed when the documentation is built. You can
download them as a Jupyter notebook
to modify and run locally.
Build an inner/outer layout#
Each partition uses positive integers for its own local region labels. Zero, negative, and NaN values mean that a cell is outside that partition. The two arrays below are disjoint, and the explicitly named remainder covers every cell outside the inner partition.
import numpy as np
import xarray as xr
from openghg_inversions.basis.basis_functions import BasisFunctions
from openghg_inversions.basis.layout import BasisLayout, BasisPartition
coords = {"lat": [51.0, 52.0], "lon": [-2.0, -1.0]}
inner_labels = xr.DataArray(
[[1, 2], [0, 0]],
dims=("lat", "lon"),
coords=coords,
name="inner_labels",
)
remainder_labels = xr.DataArray(
[[0, 0], [1, 1]],
dims=("lat", "lon"),
coords=coords,
name="remainder_labels",
)
inner = BasisPartition(name="generated_inner", labels=inner_labels, group="inner")
remainder = BasisPartition(
name="explicit_remainder",
labels=remainder_labels,
group="outer",
)
layout = BasisLayout(partitions=(inner, remainder), state_dim="region")
result = layout.to_flat_basis()
result.basis_flat.values
array([[1, 2],
[3, 3]])
The combined map has raw basis_label values 1, 2, and 3. The metadata is
keyed by those raw labels before an operator applies its final state-label
policy. Selecting only the small state-aligned variables keeps the spatial
coordinates out of the display.
result.state_metadata[["basis_group", "basis_partition", "region_in_partition"]]
<xarray.Dataset> Size: 96B
Dimensions: (basis_label: 3)
Coordinates:
* basis_label (basis_label) int64 24B 1 2 3
Data variables:
basis_group (basis_label) object 24B 'inner' 'inner' 'outer'
basis_partition (basis_label) object 24B 'generated_inner' ... 'expl...
region_in_partition (basis_label) int64 24B 1 2 1
Attributes:
state_dim: regionThe exact byte counts in an xarray display can vary by platform; the important contract is the coordinate values and their order.
Remainders are explicit#
Leaving out the remainder is an error rather than a request for
BasisLayout to invent one.
try:
BasisLayout(partitions=(inner,), state_dim="region").to_flat_basis()
except ValueError as error:
uncovered_error = str(error)
print(uncovered_error)
BasisLayout partitions leave 2 grid cells unmapped.
Attach the metadata to an operator#
Pass both outputs to BasisFunctions.from_flat_basis. The
state_metadata option is forwarded to BucketBasisOperator.
flux = xr.DataArray(
np.ones((2, 2, 1)),
dims=("lat", "lon", "time"),
coords={**coords, "time": ["2020-01-01"]},
name="flux",
)
basis_functions = BasisFunctions.from_flat_basis(
result.basis_flat,
flux,
region_labels="range0",
operator_kwargs={
"state_dim": "region",
"state_metadata": result.state_metadata,
},
)
matrix = basis_functions.operator.basis_matrix
print("Raw basis labels:", result.state_metadata.basis_label.values)
print("Operator state labels:", matrix.region.values)
matrix.coords.to_dataset()[
["basis_group", "basis_partition", "region_in_partition"]
].compute()
Raw basis labels: [1 2 3]
Operator state labels: [0 1 2]
<xarray.Dataset> Size: 96B
Dimensions: (region: 3)
Coordinates:
* region (region) int64 24B 0 1 2
basis_group (region) object 24B 'inner' 'inner' 'outer'
basis_partition (region) object 24B 'generated_inner' ... 'explicit_...
region_in_partition (region) int64 24B 1 2 1
Data variables:
*empty*Here the raw basis labels are [1, 2, 3], while region_labels="range0"
makes the final operator state coordinate [0, 1, 2]. Metadata follows the
states through that relabelling.
Select states by group#
Because the grouping fields are coordinates on the state dimension, normal xarray operations can select an inner or outer subset.
state = xr.DataArray(
[0.9, 1.1, 1.0],
dims="region",
coords={
"region": matrix.region.values,
"basis_group": ("region", matrix.basis_group.values),
"basis_partition": ("region", matrix.basis_partition.values),
"region_in_partition": ("region", matrix.region_in_partition.values),
},
name="scaling",
)
inner_state = state.where(state.basis_group == "inner", drop=True)
outer_state = state.where(state.basis_group == "outer", drop=True)
print("Inner states:", inner_state.region.values)
print("Outer states:", outer_state.region.values)
print("Outer partition:", outer_state.basis_partition.item())
Inner states: [0 1]
Outer states: [2]
Outer partition: explicit_remainder
Round-trip preservation#
BasisFunctions preserves the state metadata in its versioned DataTree
representation. save and load use the same representation for NetCDF
or Zarr artifacts.
restored = BasisFunctions.from_datatree(basis_functions.to_datatree())
restored_matrix = restored.operator.basis_matrix
{
"region": restored_matrix.region.equals(matrix.region),
"basis_group": restored_matrix.basis_group.equals(matrix.basis_group),
"region_in_partition": restored_matrix.region_in_partition.equals(
matrix.region_in_partition
),
}
{'region': True, 'basis_group': True, 'region_in_partition': True}
Stability boundary#
This page documents the implemented layout artifact, state metadata, operator
attachment, and serialization contract. BasisLayout remains an eager,
in-memory, lower-level Python API and does not infer a remainder.
Separate group-specific weight or sensitivity construction, grouped priors, public RHIME configuration, and a full extension API are outside this contract. Those broader changes are outside the implemented layout API described here.