Skip to content

Balances

balances

Conditional scalar balance identities derived from reaction topology.

This analysis uses the compiled unit-transfer incidence structure, without sampling propensities or inferring physical conservation from state names.

BalanceCoverageGap

Bases: NamedTuple

An omission or unsupported contribution preventing issued laws.

Attributes:

Name Type Description
origin str

Location in compiler metadata or reaction artifacts.

reason str

Machine-readable coverage limitation.

name str | None

Associated reaction/state name when known.

reaction_gap ReactionGap | None

Original compiler omission, retaining its selectors/reason.

ReactionBalanceLaw

Bases: NamedTuple

A conditional identity d(coefficients @ state)/dt = source fluxes.

Attributes:

Name Type Description
name str

Component label stable within this report's compiled state order.

coefficients tuple[int, ...]

Exact integer weights in the report's state order.

reaction_names tuple[str, ...]

All reaction identities covered by the analysis.

sources tuple[ReactionBalanceSource, ...]

Nonzero external terms, resolved by reaction name.

assumptions tuple[str, ...]

Conditions needed to apply this structural identity.

origin Literal['compiled_unit_transfers']

Compiler unit-transfer derivation, rather than a user assertion.

closed property

Whether this identity has no represented external source term.

ReactionBalanceReport

Bases: NamedTuple

Immutable scalar balance analysis with explicit scope and coverage.

Attributes:

Name Type Description
state_names tuple[str, ...]

Coefficient-vector coordinate order.

laws tuple[ReactionBalanceLaw, ...]

Issued identities; empty if any coverage gap exists.

coverage_gaps tuple[BalanceCoverageGap, ...]

Unsupported/omitted dynamics or inconsistent metadata.

state_meta tuple[tuple[str, StateDeclaration], ...]

State declarations in compiled state order; unknowns stay absent.

axis_meta tuple[tuple[str, AxisDeclaration], ...]

Retained scientific axis declarations; no weights are inferred.

assumptions tuple[str, ...]

Shared applicability conditions, including unknown physical units.

scope Literal['scalar_unit_transfers']

Bounded analysis capability, excluding general stoichiometry/meshes.

fully_covered property

Whether this bounded analysis covers the supplied structure.

ReactionBalanceSource

Bases: NamedTuple

One external flux term in a component's balance equation.

Attributes:

Name Type Description
reaction_name str

Source-only reaction whose propensity supplies the flux.

coefficient int

Exact multiplier of that propensity in the balance.

target_state str

Compiled scalar state receiving the source.

reaction_balances(compiled)

Derive conditional scalar totals from fully represented unit transfers.

Each internal incidence column has one consumed and one produced unit. Component-indicator vectors therefore give an exact integer left-nullspace basis. Source-only channels appear explicitly on the right-hand side of their component balances. Any unsupported contribution prevents all laws; omitted terms are never assigned zero flux. Propensities are not evaluated.

Parameters:

Name Type Description Default
compiled CompiledRhs

Compiled RHS with complete named scalar reaction artifacts.

required

Returns:

Type Description
ReactionBalanceReport

Immutable identities, coverage records, declarations and assumptions.

ReactionBalanceReport

Physical interpretation and numerical/runtime realization require

ReactionBalanceReport

independent qualification; fully_covered is a structural result.

Raises:

Type Description
TypeError

If compiled is not a CompiledRhs container.

Examples:

>>> from op_system import compile_spec
>>> compiled = compile_spec({
...     "kind": "transitions",
...     "state": ["S", "I"],
...     "transitions": [
...         {"name": "move", "from": "S", "to": "I", "rate": "k"},
...     ],
... })
>>> report = reaction_balances(compiled)
>>> report.fully_covered, len(report.laws)
(True, 1)
>>> report.laws[0].coefficients, report.laws[0].closed
((1, 1), True)
Source code in src/op_system/balances.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
def reaction_balances(compiled: CompiledRhs) -> ReactionBalanceReport:
    """Derive conditional scalar totals from fully represented unit transfers.

    Each internal incidence column has one consumed and one produced unit.
    Component-indicator vectors therefore give an exact integer left-nullspace
    basis. Source-only channels appear explicitly on the right-hand side of
    their component balances. Any unsupported contribution prevents all laws;
    omitted terms are never assigned zero flux. Propensities are not evaluated.

    Args:
        compiled: Compiled RHS with complete named scalar reaction artifacts.

    Returns:
        Immutable identities, coverage records, declarations and assumptions.
        Physical interpretation and numerical/runtime realization require
        independent qualification; ``fully_covered`` is a structural result.

    Raises:
        TypeError: If ``compiled`` is not a CompiledRhs container.

    Examples:
        >>> from op_system import compile_spec
        >>> compiled = compile_spec({
        ...     "kind": "transitions",
        ...     "state": ["S", "I"],
        ...     "transitions": [
        ...         {"name": "move", "from": "S", "to": "I", "rate": "k"},
        ...     ],
        ... })
        >>> report = reaction_balances(compiled)
        >>> report.fully_covered, len(report.laws)
        (True, 1)
        >>> report.laws[0].coefficients, report.laws[0].closed
        ((1, 1), True)
    """
    if not isinstance(compiled, CompiledRhs):
        msg = "compiled must be a CompiledRhs"
        raise TypeError(msg)
    states = tuple(compiled.state_names)
    gaps = _coverage(compiled)
    components = () if gaps else _components(states, compiled.reactions)
    gaps.extend(_scientific_coverage(compiled, components))
    names = tuple(reaction.name for reaction in compiled.reactions)
    laws = (
        ()
        if gaps
        else tuple(
            ReactionBalanceLaw(
                name=f"component:{index}",
                coefficients=tuple(int(name in component) for name in states),
                reaction_names=names,
                sources=tuple(
                    ReactionBalanceSource(reaction.name, 1, reaction.to_base)
                    for reaction in compiled.reactions
                    if reaction.from_base is None and reaction.to_base in component
                ),
                assumptions=_ASSUMPTIONS,
            )
            for index, component in enumerate(components)
        )
    )
    return ReactionBalanceReport(
        states,
        laws,
        tuple(gaps),
        tuple(
            (name, compiled.state_meta[name])
            for name in states
            if name in compiled.state_meta
        ),
        tuple(compiled.axis_meta.items()),
        _ASSUMPTIONS,
    )