Skip to content

Generalized mass action and S-system power-law models

A biochemical system of power-law rates, in the S-system or the generalized mass action form.

  • Name: biochemical_systems_theory
  • Field: biology/systems_biology
  • Formalism: ordinary differential equations
  • References: Raman 2021; Savageau 1976; Voit 2000

Factory

@register_model(
    field='biology/systems_biology',
    tags=('biochemical systems theory', 'power law', 'S-system', 'generalized mass action', 'ode'),
    references=(RAMAN, 'Savageau 1976', 'Voit 2000'),
    defaults={'until': 20.0, 'dt': 0.05},
)
def biochemical_systems_theory(
    x: Sequence[float] = (0.5, 1.0, 4.0),
    *,
    form: str = 's-system',
    alpha: Sequence[float] = PATHWAY_ALPHA,
    beta: Sequence[float] = PATHWAY_BETA,
    g: Sequence[Sequence[float]] = PATHWAY_G,
    h: Sequence[Sequence[float]] = PATHWAY_H,
    stoichiometry: Sequence[Sequence[float]] = PATHWAY_STOICHIOMETRY,
    gamma: Sequence[float] = PATHWAY_GAMMA,
    orders: Sequence[Sequence[float]] = PATHWAY_ORDERS,
) -> System:
    """Return a biochemical system of power-law rates, in the S-system or the generalized mass action form.

    The S-system form gives every species one production and one degradation,
    `dx_i/dt = alpha_i prod_j x_j^g_ij - beta_i prod_j x_j^h_ij`. The generalized mass action form sums power-law
    fluxes, `dx_i/dt = sum_j s_ij gamma_j prod_k x_k^f_jk`. The book states only the two forms. The default is a
    choice, an unbranched pathway `X1 -> X2 -> X3` whose end product inhibits the supply of `X1`, written in both forms
    with the same rates, `dX1/dt = 2 X3^-0.5 - X1^0.5`, `dX2/dt = X1^0.5 - X2^0.5`, and `dX3/dt = X2^0.5 - X3^0.5`.
    The steady state of an S-system solves the linear system `(g - h) log x = log(beta / alpha)`, here `x = (2, 2, 2)`.

    Args:
        x:
            The initial concentrations of the species.

        form:
            `'s-system'` or `'gma'`.

        alpha:
            The rate constants of the productions of the S-system.

        beta:
            The rate constants of the degradations of the S-system.

        g:
            The kinetic orders of the productions of the S-system, one row per species.

        h:
            The kinetic orders of the degradations of the S-system, one row per species.

        stoichiometry:
            The stoichiometric coefficients of the generalized mass action form, one row per species.

        gamma:
            The rate constants of the fluxes of the generalized mass action form.

        orders:
            The kinetic orders of the fluxes of the generalized mass action form, one row per flux.

    Returns:
        system:
            The model, with the concentrations `X1.x`, `X2.x`, and so on.

    Raises:
        ValidationError:
            The form is unknown.

    Examples:
        >>> from skcomplex.models.biology.systems_biology import biochemical_systems_theory
        >>> biochemical_systems_theory(form='gma').interactions[0].gamma
        (2.0, 1.0, 1.0, 1.0)
    """
    species = [SpeciesElement(f'X{index}', x=float(value)) for index, value in enumerate(x, start=1)]
    paths = [item.label for item in species]
    if form == 's-system':
        rates: BaseInteraction = SSystemInteraction(
            'rates',
            species=paths,
            alpha=[float(value) for value in alpha],
            beta=[float(value) for value in beta],
            g=tuple(tuple(row) for row in g),
            h=tuple(tuple(row) for row in h),
        )
    elif form == 'gma':
        rates = GeneralizedMassActionInteraction(
            'rates',
            species=paths,
            stoichiometry=tuple(tuple(row) for row in stoichiometry),
            gamma=tuple(gamma),
            orders=tuple(tuple(row) for row in orders),
        )
    else:
        error_msg = f'Unknown form {form!r}. Use s-system or gma.'
        raise ValidationError(error_msg)
    return System(elements=species, interactions=[rates])

Components

class SpeciesElement(Element):
    """Molecular species, an mRNA, an enzyme, a messenger, or a metabolite, with a nonnegative concentration `x`.

    A species has no laws of its own, and reactions synthesize, convert, and degrade it.

    Args:
        label:
            The label of the species.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        x:
            The initial concentration.

    Examples:
        >>> from skcomplex.models.biology.systems_biology import SpeciesElement
        >>> SpeciesElement('camp', x=2.0).x
        2.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        x: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.x = x

class SSystemInteraction(GeneralizedMassActionInteraction):
    """Rates of a biochemical system in the S-system form of biochemical systems theory.

    Every species has one aggregated production and one aggregated degradation, both power laws:
    `dx_i/dt = alpha_i prod_j x_j^g_ij - beta_i prod_j x_j^h_ij`. It is the generalized mass action form with two
    fluxes per species.

    Args:
        label:
            The label of the interaction.

        species:
            The paths of the species.

        alpha:
            The rate constant of the production of every species.

        beta:
            The rate constant of the degradation of every species.

        g:
            The kinetic orders of the productions, one row per species.

        h:
            The kinetic orders of the degradations, one row per species.

    Raises:
        ValidationError:
            The kinetic orders do not have one row and one column per species.

    Examples:
        >>> from skcomplex.models.biology.systems_biology import SSystemInteraction
        >>> SSystemInteraction('rates', species=['X'], alpha=[1.0], beta=[1.0], g=((0.0,),), h=((1.0,),)).h
        ((1.0,),)
    """

    def __init__(
        self,
        label: str,
        *,
        species: Annotated[list[str], RoleSpec(variadic=True)],
        alpha: Annotated[Sequence[float], ParameterSpec(per='species', domain=RealSet.closed_open(0, oo))] = (),
        beta: Annotated[Sequence[float], ParameterSpec(per='species', domain=RealSet.closed_open(0, oo))] = (),
        g: Sequence[Sequence[float]] = ((0.0,),),
        h: Sequence[Sequence[float]] = ((0.0,),),
    ) -> None:
        super().__init__(label, species=species)
        self.alpha = alpha
        self.beta = beta
        self.g = g
        self.h = h

    def _check_matrices(self) -> None:
        """Raise an error for kinetic orders without one row and one column per species."""
        self._check_shape('g', len(self.species), len(self.species))
        self._check_shape('h', len(self.species), len(self.species))

    def _get_fluxes(self, s: SimpleNamespace, values: Sequence[Expression]) -> list[Expression]:
        """Return the production and the degradation flux of every species, in turn."""
        return [
            flux
            for alpha, beta, g, h in zip(s.alpha, s.beta, self.g, self.h, strict=True)
            for flux in (alpha * self._get_power_product(values, g), beta * self._get_power_product(values, h))
        ]

    def _get_stoichiometry(self) -> Sequence[Sequence[float]]:
        """Return the coefficient 1 of every species in its production and -1 in its degradation."""
        size = len(self.species)
        return [
            [1.0 if flux == 2 * row else -1.0 if flux == 2 * row + 1 else 0.0 for flux in range(2 * size)]
            for row in range(size)
        ]

Equations

X1.x: diff(x_X1(t), t) == -1.0*beta_rates_0*sqrt(x_X1(t)) + 1.0*alpha_rates_0/sqrt(x_X3(t))
X2.x: diff(x_X2(t), t) == 1.0*alpha_rates_1*sqrt(x_X1(t)) - 1.0*beta_rates_1*sqrt(x_X2(t))
X3.x: diff(x_X3(t), t) == 1.0*alpha_rates_2*sqrt(x_X2(t)) - 1.0*beta_rates_2*sqrt(x_X3(t))