Source code for aquakin.plant.translators

"""State translators: convert a stream from one kinetic model to another.

For single-model plants like BSM1, every translator is the
:class:`IdentityTranslator`. The interface exists so that BSM2-style
plants — which use ASM1 in the activated-sludge tanks and ADM1 in the
anaerobic digester — can plug an ASM↔ADM mapping into the framework
without touching plant assembly code.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

import jax.numpy as jnp

if TYPE_CHECKING:  # pragma: no cover
    from aquakin.core.model import CompiledModel


[docs] @runtime_checkable class StateTranslator(Protocol): """Maps a concentration vector from one model's species ordering to another's. Must be AD-clean (used inside the plant RHS). Attributes ---------- source_model : CompiledModel The kinetic model whose species ordering the input concentration vector follows. target_model : CompiledModel The kinetic model the output concentration vector is expressed in. """ source_model: CompiledModel target_model: CompiledModel
[docs] def translate(self, C_source: jnp.ndarray, digester_pH=None) -> jnp.ndarray: """Map ``C_source`` to the target model's species ordering. ``digester_pH`` optionally supplies the digester's instantaneous, state-derived pH for a translator whose mapping has a pH-dependent (charge-balance) term; a translator without one ignores it. The plant supplies it when the translator declares ``needs_dest_pH``. """ ...
def translator_coupling_pattern(translator, n_states: int = 96, seed: int = 0): """Structural coupling of a translator: which target species each source species can influence, as a boolean ``(n_target, n_source)`` matrix. This is what lets a translator participate in the plant's colored-Jacobian sparsity pattern (issue #388): a cross-model edge (ASM<->ADM) introduces couplings that live in the *translator*, not in either model, and that are regime-dependent (an interface's greedy nitrogen-budget allocation switches branches with the influent), so a numerical probe at one operating point misses the branches that activate at another. The pattern is derived by **forward-AD of ``translate`` unioned over many diverse source states** (each component scaled over a wide multiplicative range), so every min/max allocation branch is exercised and the result is a structural superset, not a single-state snapshot. The map is small and cheap, so a far denser sample than a stiff plant solve could afford is used. A translator may **override** this by defining its own ``coupling_pattern()`` method -- e.g. a declarative translator built from per-species expressions could emit its pattern exactly from that declaration. This is the extension point that lets a user add a custom cross-model translator and have it work with the colored solver automatically. """ import jax import jax.numpy as jnp import numpy as np override = getattr(translator, "coupling_pattern", None) if callable(override): return np.asarray(override(), dtype=bool) src = translator.source_model n_src = src.n_species n_tgt = translator.target_model.n_species base = np.maximum(np.abs(np.asarray(src.default_concentrations())), 1e-3) # fixed representative pH for the (value-independent) coupling structure fj = jax.jit(lambda c: jax.jacfwd(lambda x: translator.translate(x, 7.0))(c)) rng = np.random.default_rng(seed) P = np.zeros((n_tgt, n_src), dtype=bool) for _ in range(n_states): c = jnp.asarray(base * 10.0 ** rng.uniform(-2.0, 2.0, size=n_src)) P |= np.abs(np.asarray(fj(c))) > 0.0 return P
[docs] class IdentityTranslator: """Pass-through translator for when source and target models are the same — the only kind of translator BSM1 needs. The plant inserts one of these automatically on any connection whose source and target units share a model reference, so users don't normally need to instantiate it directly. """
[docs] def __init__(self, model: CompiledModel) -> None: self.source_model = model self.target_model = model
def translate(self, C_source: jnp.ndarray, digester_pH=None) -> jnp.ndarray: return C_source
[docs] def coupling_pattern(self): """The identity map couples each species only to itself.""" import numpy as np return np.eye(self.source_model.n_species, dtype=bool)