Skip to content

enzax.reaction

Provides Reaction, a class representing enzyme-catalysed reactions.

Reaction

Abstract definition of a reaction.

A reaction is an equinox Module with a stoichiometry field and a __call__ method. stoichiometry maps species ids to stoichiometric coefficients (negative for substrates and positive for products) and is always passed by keyword. The __call__ method takes in a 1 dimensional array of concentrations and an arbitrary PyTree of other inputs, returning a scalar value representing the reaction's flux.

water_stoichiometry, also passed by keyword, says how much water the reaction consumes or produces, since water is not a species.

A reaction refers to its parameters by label. Two reactions that use the same label share a value, allowing sharing of parameter values between reactions, or for the same parameter value to be used in different roles in the same reaction. Labels are resolved to positions in the model's flat parameter arrays once, when the model is constructed:

  1. get_labels reports every label the reaction refers to, grouped by what the labels are. The model collects these from all its reactions, via get_labels_by_parameter, to work out its parameter labels.
  2. get_input_indexes turns those labels into index arrays, given the finished labels. The result is static and is stored on the model.
  3. get_input gathers the actual values, once per flux evaluation.

get_input_indexes must build its index bundle itself rather than leaving the model to assemble one, so that each reaction's ragged n_rxn_* axes are bound in their own jaxtyping scope.

Source code in enzax/reaction.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
class Reaction(Module, ABC):
    """Abstract definition of a reaction.

    A reaction is an equinox
    [Module](https://docs.kidger.site/equinox/api/module/module/) with a
    `stoichiometry` field and a `__call__` method. `stoichiometry` maps
    species ids to stoichiometric coefficients (negative for substrates and
    positive for products) and is always passed by keyword. The `__call__`
    method takes in a 1 dimensional array of concentrations and an arbitrary
    PyTree of other inputs, returning a scalar value representing the
    reaction's flux.

    `water_stoichiometry`, also passed by keyword, says how much water the
    reaction consumes or produces, since water is not a species.

    A reaction refers to its parameters by label. Two reactions that
    use the same label share a value, allowing sharing of parameter values
    between reactions, or for the same parameter value to be used in different
    roles in the same reaction. Labels are resolved to positions in the model's
    flat parameter arrays once, when the model is constructed:

    1. `get_labels` reports every label the reaction refers to, grouped by
       what the labels are. The model collects these from all its reactions,
       via `get_labels_by_parameter`, to work out its parameter labels.
    2. `get_input_indexes` turns those labels into index arrays, given the
       finished labels. The result is static and is stored on the model.
    3. `get_input` gathers the actual values, once per flux evaluation.

    `get_input_indexes` must build its index bundle itself rather than leaving
    the model to assemble one, so that each reaction's ragged `n_rxn_*` axes
    are bound in their own jaxtyping scope.
    """

    stoichiometry: dict[str, float] = field(kw_only=True)
    water_stoichiometry: float = field(kw_only=True, default=0.0)

    def get_species(self) -> tuple[str, ...]:
        """Get every species this reaction names, in declaration order.

        The model builds its species list from its stoichiometry and this, so
        a species that takes part in no reaction -- an allosteric effector,
        say -- is still one of the model's species. A reaction that only
        ever mentions its own reactants has nothing to add.
        """
        return ()

    @abstractmethod
    def get_labels(self, scope: ReactionScope) -> ReactionLabels:
        """Get the parameter labels this reaction refers to."""
        ...

    def get_labels_by_parameter(self, scope: ReactionScope) -> ParamLabelling:
        """Get the labels this reaction refers to, keyed by parameter."""
        return self.get_labels(scope).by_parameter()

    @abstractmethod
    def get_input_indexes(
        self,
        scope: ReactionScope,
        labelling: ParamLabelling,
    ) -> PyTree: ...

    @abstractmethod
    def get_input(self, parameters: ParamDict, ix: PyTree) -> PyTree: ...

    @abstractmethod
    def __call__(
        self,
        conc: ConcArray,
        rate_equation_input: PyTree,
    ) -> Scalar: ...

get_species()

Get every species this reaction names, in declaration order.

The model builds its species list from its stoichiometry and this, so a species that takes part in no reaction -- an allosteric effector, say -- is still one of the model's species. A reaction that only ever mentions its own reactants has nothing to add.

Source code in enzax/reaction.py
202
203
204
205
206
207
208
209
210
def get_species(self) -> tuple[str, ...]:
    """Get every species this reaction names, in declaration order.

    The model builds its species list from its stoichiometry and this, so
    a species that takes part in no reaction -- an allosteric effector,
    say -- is still one of the model's species. A reaction that only
    ever mentions its own reactants has nothing to add.
    """
    return ()

get_labels(scope) abstractmethod

Get the parameter labels this reaction refers to.

Source code in enzax/reaction.py
212
213
214
215
@abstractmethod
def get_labels(self, scope: ReactionScope) -> ReactionLabels:
    """Get the parameter labels this reaction refers to."""
    ...

get_labels_by_parameter(scope)

Get the labels this reaction refers to, keyed by parameter.

Source code in enzax/reaction.py
217
218
219
def get_labels_by_parameter(self, scope: ReactionScope) -> ParamLabelling:
    """Get the labels this reaction refers to, keyed by parameter."""
    return self.get_labels(scope).by_parameter()

ReactionLabels

The parameter labels a reaction refers to, grouped by what they are.

A reaction defines its own subclass, with one field per group of labels it declares, and by_parameter says which flat array each group is gathered from. That is the only place the correspondence is recorded: a reaction's get_input gathers from the same arrays.

Source code in enzax/reaction.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class ReactionLabels(ABC):
    """The parameter labels a reaction refers to, grouped by what they are.

    A reaction defines its own subclass, with one field per group of
    labels it declares, and `by_parameter` says which flat array each group is
    gathered from. That is the only place the correspondence is recorded: a
    reaction's `get_input` gathers from the same arrays.
    """  # noqa: E501

    @abstractmethod
    def by_parameter(self) -> ParamLabelling:
        """Regroup the labels by the parameter each one lives in.

        :return: a mapping from parameter to the labels this reaction
            gathers from it, in gather order.
        """
        ...

by_parameter() abstractmethod

Regroup the labels by the parameter each one lives in.

Returns:

Type Description
ParamLabelling

a mapping from parameter to the labels this reaction gathers from it, in gather order.

Source code in enzax/reaction.py
156
157
158
159
160
161
162
163
@abstractmethod
def by_parameter(self) -> ParamLabelling:
    """Regroup the labels by the parameter each one lives in.

    :return: a mapping from parameter to the labels this reaction
        gathers from it, in gather order.
    """
    ...

ReactionScope dataclass

What a reaction needs to know about the reaction it belongs to.

Built once per reaction at model construction and handed to Reaction.get_labels and Reaction.get_input_indexes.

Note that species, stoichiometry and species_to_dgf_ix have length n_species, where n_species is the model's total number of species.

Source code in enzax/reaction.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@dataclass(frozen=True)
class ReactionScope:
    """What a reaction needs to know about the reaction it belongs to.

    Built once per reaction at model construction and handed to
    `Reaction.get_labels` and `Reaction.get_input_indexes`.

    Note that `species`, `stoichiometry` and `species_to_dgf_ix` have length
    n_species, where n_species is the model's total number of species.
    """

    reaction_id: str
    species: tuple[str, ...]
    stoichiometry: StaticSpeciesArr
    species_to_dgf_ix: SpeciesIx
    water_dgf: float
    algebraic_variables: tuple[str, ...] = ()

get_substrates(scope)

Get the reaction's substrates, in species order.

Source code in enzax/reaction.py
64
65
66
def get_substrates(scope: ReactionScope) -> tuple[str, ...]:
    """Get the reaction's substrates, in species order."""
    return select_species(scope.species, scope.stoichiometry < 0.0)

get_products(scope)

Get the reaction's products, in species order.

Source code in enzax/reaction.py
69
70
71
def get_products(scope: ReactionScope) -> tuple[str, ...]:
    """Get the reaction's products, in species order."""
    return select_species(scope.species, scope.stoichiometry > 0.0)

get_reactants(scope)

Get the reaction's substrates and products, in species order.

Source code in enzax/reaction.py
74
75
76
def get_reactants(scope: ReactionScope) -> tuple[str, ...]:
    """Get the reaction's substrates and products, in species order."""
    return select_species(scope.species, scope.stoichiometry != 0.0)

get_species_positions(scope, species_ids)

Get the positions of some species in the model's species list.

Source code in enzax/reaction.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def get_species_positions(
    scope: ReactionScope,
    species_ids: Iterable[str],
) -> Int[np.ndarray, " _"]:
    """Get the positions of some species in the model's species list."""
    ids = list(species_ids)
    unknown = [s for s in ids if s not in scope.species]
    if unknown:
        msg = (
            f"Reaction {scope.reaction_id} refers to species {unknown}, "
            "which are not in the model."
        )
        raise ValueError(msg)
    return np.array([scope.species.index(s) for s in ids], dtype=INDEX_DTYPE)

get_reaction_label(declared, reaction_id)

Get the label of a value that a reaction has one of, e.g. its kcat.

The default is the reaction's own id, so two reactions share such a value by declaring the same label for it.

Source code in enzax/reaction.py
79
80
81
82
83
84
85
def get_reaction_label(declared: str | None, reaction_id: str) -> str:
    """Get the label of a value that a reaction has one of, e.g. its kcat.

    The default is the reaction's own id, so two reactions share such a value
    by declaring the same label for it.
    """
    return reaction_id if declared is None else declared

get_species_label(prefix, reaction_id, species_id)

Get the default label of a value a reaction has one of per species.

For example, the label for g6p's Michaelis constant in the PGI reaction could be get_species_label("km", "PGI", "g6p"), i.e. "km|PGI|g6p".

Source code in enzax/reaction.py
88
89
90
91
92
93
94
def get_species_label(prefix: str, reaction_id: str, species_id: str) -> str:
    """Get the default label of a value a reaction has one of per species.

    For example, the label for g6p's Michaelis constant in the PGI reaction
    could be `get_species_label("km", "PGI", "g6p")`, i.e. `"km|PGI|g6p"`.
    """
    return f"{prefix}{SEP}{reaction_id}{SEP}{species_id}"

get_species_labels(declaration, prefix, reaction_id, what)

Normalise a species declaration into a {species: label} dict.

A sequence of species ids means "these species, with default labels"; a mapping gives each species an explicit label, which is how a species points at a shared value or at another parameter's value.

Source code in enzax/reaction.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def get_species_labels(
    declaration: Sequence[str] | Mapping[str, str] | None,
    prefix: str,
    reaction_id: str,
    what: str,
) -> dict[str, str]:
    """Normalise a species declaration into a `{species: label}` dict.

    A sequence of species ids means "these species, with default labels"; a
    mapping gives each species an explicit label, which is how a species points
    at a shared value or at another parameter's value.
    """
    if declaration is None:
        return {}
    if isinstance(declaration, str):
        msg = (
            f"Reaction {reaction_id}'s {what} declaration is the string "
            f"{declaration!r}. Use a list of species ids, or a mapping from "
            "species id to parameter label."
        )
        raise ValueError(msg)
    if isinstance(declaration, Mapping):
        labels = dict(declaration)
    else:
        labels = {
            species_id: get_species_label(prefix, reaction_id, species_id)
            for species_id in declaration
        }
    check_species_labels_are_distinct(labels, reaction_id, what)
    return labels