ASM1: true and false positive ramps#

# Copyright (C) 2024 Juan Pablo Carbajal
# Copyright (C) 2024 Mariane Yvonne Schneider
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

# Author: Juan Pablo Carbajal <ajuanpi@gmail.com>
# Author: Mariane Yvonne Schneider <myschneider@meiru.ch>
import matplotlib.pyplot as plt
import numpy as np
from numpy import ma
from sympy import *

try:
    import dsafeatures
except ModuleNotFoundError:
    import sys
    import os

    sys.path.insert(0, os.path.abspath("../.."))

import dsafeatures.odemodel as m
import dsafeatures.symtools as st
import dsafeatures.signal_processing as sp

from dsafeatures.printing import *

init_printing()

Load the model#

asm = m.ODEModel.from_csv(name="ASM1")

# States
X = asm.states
# Process rates
r_sym, r = asm.rates, asm.rates_expr
Jr = r.jacobian(X)
# Matrix
M = asm.coef_matrix
# State derivative
dotX = st.dXdt(M, r)
# add areation
O2, idxO2 = asm.state_by_name("S_O2")
oxyflow = 500.0  # oxygen intake flow
aer = m.oxygen_PC(O2, max_inflow=oxyflow)
dotX[idxO2] += aer  #
# Inflection point curve
ipc = st.ddXdt(M[idxO2, :], Jr, dotX)[0] + aer.diff(O2) * dotX[idxO2]

# Parameter values
param_values = dict(asm.parameters(and_values=True).set_index("sympy").value)
param_values = {k: float(v.subs(param_values)) for k, v in param_values.items()}

replace parameter symbols with values

ipc_pv = ipc.subs(param_values)
ipc_states = sorted(tuple(x for x in X if ipc_pv.has(x)), key=str)

# same for ODE system
dotX_pv = dotX.subs(param_values)

Numerical simulation#

State values initial values

states_iv = asm.component["states"][["sympy", "initial_value"]].set_index("sympy").loc[list(X), "initial_value"]
# ignore all states not affecting the ODE,
states_iv[asm.state_by_symbol_name("S_b")[0]] = 128.94744
states_iv[asm.state_by_symbol_name("X_cb")[0]] = 11.48889
states_iv[asm.state_by_symbol_name("X_h")[0]] = 1585.25958
states_iv[asm.state_by_symbol_name("X_a")[0]] = 0.96299
states_iv[O2] = 8.76297
states_iv[asm.state_by_symbol_name("S_NOx")[0]] = 0.00235
NHx, idxNHx = asm.state_by_name("S_NHx")
states_iv[NHx] = 0.0065
states_iv[asm.state_by_symbol_name("S_bN")[0]] = 0.62963
cbN = asm.state_by_symbol_name("X_cbN")[0]
states_iv[cbN] = 1.01402

Integrate numerically#

Tend = 7
dotO2_min = 4e-2

ipc_np = lambdify([tuple(X)], ipc_pv, cse=True)

def ev(t, y):
    return ipc_np(y)
ev.terminal = 0
ev.direction = -1

sol, dX_np, J_np = sp.integrate_numer(dotX_pv, X, X_iv=states_iv, t_span=(0, Tend), max_step=1e-3,
                                      events=[ev])
t = sol.t
y = sol.sol(t)
ipc_val = ipc_np(y)
dx_val = np.zeros_like(t)
for k, x_ in enumerate(y.T):
    dx_val[k] = dX_np(x_).flatten()[idxO2]
other_states = [x for x in ipc_states if x not in (O2, NHx)] + [cbN]
fig = plt.figure(layout="constrained")
axd = fig.subplot_mosaic(
    [
        [O2] * 4,
        other_states[:4],
        other_states[4:] + ['.'],
        ['ipc'] * 4,
    ])

ax = axd[O2]
# add shared axis for NHx
ax.plot(t, y[idxO2])
clc1 = ax.get_lines()[-1].get_color()
ax.set_ylabel(latex(O2, mode="inline")+' [g$\mathrm{m}^{-3}$]', color=clc1, fontsize=12)
ax.set_xlabel("time [d]", fontsize=12)

axNHx = ax.twinx()
axNHx.plot(t, y[idxNHx], color='C1')
clc = axNHx.get_lines()[-1].get_color()
axNHx.set_ylabel(latex(NHx, mode="inline")+' [g$\mathrm{m}^{-3}$]', color=clc, fontsize=12)
axNHx.spines['right'].set_color(clc)
axNHx.tick_params(axis='y', colors=clc)

Xl_ = list(X)
for x in other_states:
    ax = axd[x]
    ax.plot(t, y[Xl_.index(x)], label=latex(x, mode="inline"))
    ax.legend()
    ax.set_xticklabels([])

trans = lambda x, n=0.3: np.sign(x) * np.minimum(np.abs(x), 10)
ax = axd['ipc']
ax.axhline(0.0, color='k', alpha=0.3)
ax.plot(t, trans(ipc_val))
ipc_ = ma.masked_where(dx_val <= 4e-2, ipc_val)
ax.plot(t, trans(ipc_), c='r', lw=3, alpha=0.4)

ax.set_xlabel("time [d]", fontsize=12)
ax.set_ylabel("ramp condition", fontsize=12)

# Add lines at ramps
for t_, y_ in zip(sol.t_events[0], sol.y_events[0]):
    if dX_np(y_)[idxO2] > dotO2_min:
        for ax in axd.values():
            ax.axvline(t_, color='k', alpha=0.3)

if False:  # DEBUG
    # Spline representation
    dx_s = sp.make_interp_spline(t, dx_val, k=3)
    ipc_s = dx_s.derivative()

    ax.plot(t, ipc_s(t), ':')
    ax_ = ax.twinx()
    ax_.plot(t, dx_val, label="symbolic")
    ax_.plot(t, dx_s(t), ':', label="numerical")
example asm1 SO ramps
/builds/sbrml/dsa-signal-features/doc/examples/example_asm1_SO_ramps.py:137: SyntaxWarning: invalid escape sequence '\m'
  ax.set_ylabel(latex(O2, mode="inline")+' [g$\mathrm{m}^{-3}$]', color=clc1, fontsize=12)
/builds/sbrml/dsa-signal-features/doc/examples/example_asm1_SO_ramps.py:143: SyntaxWarning: invalid escape sequence '\m'
  axNHx.set_ylabel(latex(NHx, mode="inline")+' [g$\mathrm{m}^{-3}$]', color=clc, fontsize=12)

Total running time of the script: (0 minutes 6.281 seconds)

Estimated memory usage: 108 MB

Gallery generated by Sphinx-Gallery