Profiling (ODE parameters)¶
fdatools.profiling: generalized profiling estimation of ordinary differential
equation parameters (Ramsay, Hooker, Campbell & Cao 2007). ODEModel wraps a
right-hand side, ProfiledODE / profile_ode fit states and parameters, and
cstr_model / fitzhugh_nagumo_model are built-in examples.
Profiling: parameter estimation for nonlinear ODEs by generalized profiling.
A system of d ordinary differential equations
.. math::
\dot x_i(t) = f_i\bigl(x(t), t, \theta\bigr), \qquad i = 1, \dots, d,
is fitted to noisy observations of some (not necessarily all) of its states by
the generalized profiling method of Ramsay, Hooker, Campbell & Cao (2007,
JRSS-B 69: 741-796; Ramsay, Hooker & Graves 2009, chapter 11). Each state is a
basis expansion :math:x_i(t) = \phi_i(t)^T c_i, and the fit has two levels.
Inner problem. For a fixed parameter vector :math:\theta the
coefficients minimise the penalised criterion
.. math::
J(c \mid \theta) = \sum_{i} w_i \Bigl[ \sum_j \bigl(y_{ij} - x_i(t_{ij})\bigr)^2
+ \lambda_i \int \bigl(\dot x_i(t) - f_i(x(t), t, \theta)\bigr)^2 dt \Bigr],
where :math:w_i scales state i (R's Cwt/Twt are :math:1/w_i)
and the integral is computed by a quadrature rule (composite Simpson on the
B-spline break points by default, as R's quadset). J is a sum of
squared residuals and is minimised by damped Gauss-Newton. An unobserved
state has no data term; it is identified through the equations only.
Outer problem. The fitted coefficients :math:\hat c(\theta) are an
implicit function of :math:\theta, and :math:\theta minimises the profiled
data criterion
.. math::
H(\theta) = \sum_i w_i \sum_j \bigl(y_{ij} - \phi_i(t_{ij})^T \hat c_i(\theta)\bigr)^2 .
Its Jacobian comes from the implicit function theorem: at the inner optimum
:math:\partial J / \partial c = 0, so
.. math::
\frac{d\hat c}{d\theta} = -\Bigl(\frac{\partial^2 J}{\partial c\,\partial c^T}\Bigr)^{-1}
\frac{\partial^2 J}{\partial c\,\partial \theta^T},
with both second-derivative matrices exact (they include the second derivatives
of f). Gauss-Newton on :math:H then gives :math:\hat\theta and its
approximate covariance :math:\hat\sigma^2 (A^T A)^{-1},
:math:A = d e / d\theta, :math:\hat\sigma^2 = H(\hat\theta) / (N - p).
This replaces R's CSTR family (CSTR2in, CSTR2, CSTRfitLS,
CSTRfn, CSTRres, CSTRsse) with one general interface: the user
supplies the right-hand side as an :class:ODEModel. The continuously stirred
tank reactor is provided as :func:cstr_model (with its input scenarios in
:func:cstr_inputs), and the FitzHugh-Nagumo neuron as
:func:fitzhugh_nagumo_model.
All arithmetic is done in float64 NumPy (through the array-API namespace); the
estimation is an iterative optimisation, not a differentiable map, so PyTorch
tensors are accepted as data but results are NumPy arrays. A right-hand side
written in PyTorch is supported through :meth:ODEModel.from_torch, which
obtains every derivative by automatic differentiation.
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ODEModel, profile_ode
>>> decay = ODEModel(
... rhs=lambda x, t, theta: -theta[0] * x,
... n_states=1,
... n_params=1,
... jac_x=lambda x, t, theta: np.full((len(t), 1, 1), -theta[0]),
... jac_theta=lambda x, t, theta: -x[:, :, None],
... )
>>> t = np.linspace(0.0, 2.0, 41)
>>> y = 3.0 * np.exp(-1.5 * t)
>>> basis = fdt.BSpline(domain=(0.0, 2.0), breaks=np.linspace(0.0, 2.0, 21))
>>> fit = profile_ode(decay, t, y[:, None], basis, lam=1e4, theta0=[1.0])
>>> round(float(fit.theta[0]), 4)
1.5
ODEModel
dataclass
¶
ODEModel(
rhs: Function,
n_states: int,
n_params: int,
jac_x: Function | None = None,
jac_theta: Function | None = None,
hess_xx: Function | None = None,
hess_xtheta: Function | None = None,
state_names: tuple[str, ...] = (),
param_names: tuple[str, ...] = (),
)
The right-hand side of an ODE system :math:\dot x = f(x, t, \theta).
Every callable receives the states x as an (n, d) array, the times
t as an (n,) array and the parameters theta as a (p,)
array, and is evaluated at n points at once. The derivatives are
optional: any that is omitted is computed by central finite differences
(of rhs for the Jacobians, of the Jacobians for the second
derivatives), which is accurate to about 1e-10 relative for the Jacobians
and 1e-6 for the second derivatives. Supplying them analytically, or
building the model with :meth:from_torch, makes the fit exact.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs
|
callable
|
|
required |
n_states
|
int
|
Number of states |
required |
n_params
|
int
|
Number of parameters |
required |
jac_x
|
callable
|
|
None
|
jac_theta
|
callable
|
|
None
|
hess_xx
|
callable
|
|
None
|
hess_xtheta
|
callable
|
|
None
|
state_names
|
sequence of str
|
Labels; default |
()
|
param_names
|
sequence of str
|
Labels; default |
()
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If a count is not positive or a name list has the wrong length. |
Examples:
>>> import numpy as np
>>> from fdatools.profiling import ODEModel
>>> growth = ODEModel(lambda x, t, th: th[0] * x, n_states=1, n_params=1)
>>> growth(np.array([[2.0]]), np.array([0.0]), np.array([0.5])).tolist()
[[1.0]]
>>> round(
... float(
... growth.jacobians(np.array([[2.0]]), np.array([0.0]), np.array([0.5]))[1][0, 0, 0]
... ),
... 6,
... )
2.0
__call__ ¶
__call__(x: Any, t: Any, theta: Any) -> Array
Evaluate :math:f(x, t, \theta) at n points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
States, |
required |
t
|
array_like
|
Times, |
required |
theta
|
array_like
|
Parameters, |
required |
Returns:
| Type | Description |
|---|---|
array
|
|
Examples:
>>> import numpy as np
>>> from fdatools.profiling import fitzhugh_nagumo_model
>>> fhn = fitzhugh_nagumo_model()
>>> fhn(np.array([[0.0, 0.0]]), np.array([0.0]), np.array([0.2, 0.2, 3.0])).round(
... 4
... ).tolist()
[[0.0, 0.0667]]
jacobians ¶
jacobians(
x: Any, t: Any, theta: Any
) -> tuple[Array, Array]
Return :math:\partial f / \partial x and :math:\partial f / \partial \theta.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
States, |
required |
t
|
array_like
|
Times, |
required |
theta
|
array_like
|
Parameters, |
required |
Returns:
| Name | Type | Description |
|---|---|---|
jac_x |
array
|
|
jac_theta |
array
|
|
Examples:
>>> import numpy as np
>>> from fdatools.profiling import fitzhugh_nagumo_model
>>> fx, ft = fitzhugh_nagumo_model().jacobians(
... np.array([[1.0, 0.5]]), np.array([0.0]), np.array([0.2, 0.2, 3.0])
... )
>>> fx.shape, ft.shape
((1, 2, 2), (1, 2, 3))
hessians ¶
hessians(x: Any, t: Any, theta: Any) -> tuple[Array, Array]
Return the second derivatives of f used by the profiling fit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
States, |
required |
t
|
array_like
|
Times, |
required |
theta
|
array_like
|
Parameters, |
required |
Returns:
| Name | Type | Description |
|---|---|---|
hess_xx |
array
|
|
hess_xtheta |
array
|
|
Examples:
>>> import numpy as np
>>> from fdatools.profiling import fitzhugh_nagumo_model
>>> fxx, fxt = fitzhugh_nagumo_model().hessians(
... np.array([[1.0, 0.5]]), np.array([0.0]), np.array([0.2, 0.2, 3.0])
... )
>>> float(fxx[0, 0, 0, 0])
-6.0
simulate ¶
simulate(
t: Any,
x0: Any,
theta: Any,
*,
breaks: Sequence[float] = (),
rtol: float = _ODE_RTOL,
atol: float = _ODE_ATOL,
method: OdeMethod = "LSODA",
) -> Array
Solve the initial-value problem with :func:scipy.integrate.solve_ivp.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
array_like
|
Increasing output times; the integration starts at |
required |
x0
|
array_like
|
|
required |
theta
|
array_like
|
|
required |
breaks
|
sequence of float
|
Times where the right-hand side jumps (for example step inputs). The integration is restarted at each, so the solver never steps across a discontinuity. |
()
|
rtol
|
float
|
Solver tolerances. |
_ODE_RTOL
|
atol
|
float
|
Solver tolerances. |
_ODE_RTOL
|
method
|
str
|
A :func: |
'LSODA'
|
Returns:
| Type | Description |
|---|---|
array
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from fdatools.profiling import ODEModel
>>> decay = ODEModel(lambda x, t, th: -th[0] * x, n_states=1, n_params=1)
>>> x = decay.simulate([0.0, 1.0], [1.0], [2.0])
>>> round(float(x[1, 0]), 8) == round(float(np.exp(-2.0)), 8)
True
from_torch
classmethod
¶
from_torch(
rhs: Callable[[Any, Any, Any], Any],
n_states: int,
n_params: int,
*,
state_names: Sequence[str] = (),
param_names: Sequence[str] = (),
) -> ODEModel
Build a model from a PyTorch right-hand side, with autodiff derivatives.
rhs(x, t, theta) must use PyTorch operations only and treat the
n points independently (it is differentiated one point at a time
with :func:torch.func.vmap). Every Jacobian and second derivative is
then exact. Requires the fdatools[torch] extra; PyTorch is imported
only when this method is called.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rhs
|
callable
|
PyTorch function of |
required |
n_states
|
int
|
Dimensions of |
required |
n_params
|
int
|
Dimensions of |
required |
state_names
|
sequence of str
|
Labels. |
()
|
param_names
|
sequence of str
|
Labels. |
()
|
Returns:
| Type | Description |
|---|---|
ODEModel
|
A model whose callables accept and return NumPy arrays. |
Examples:
>>> import numpy as np
>>> import torch
>>> from fdatools.profiling import ODEModel
>>> model = ODEModel.from_torch(lambda x, t, th: -th[0] * x**2, 1, 1)
>>> float(
... model.hessians(np.array([[3.0]]), np.array([0.0]), np.array([2.0]))[0][0, 0, 0, 0]
... )
-4.0
Residuals ¶
Bases: NamedTuple
Residuals of the inner criterion and their Jacobians at (c, theta).
J(c | theta) is data @ data + equation @ equation. Rows of the
Jacobians are the data residuals (observed states in order) followed by the
equation residuals (state by state, quadrature node by node); columns of
jac_coefs are the stacked coefficients of all states.
Attributes:
| Name | Type | Description |
|---|---|---|
data |
array
|
|
equation |
array
|
|
jac_coefs |
array
|
Derivative of |
jac_theta |
array
|
Derivative of |
InnerFit
dataclass
¶
InnerFit(
coefs: tuple[Array, ...],
criterion: float,
sse: float,
df: float,
gcv: float,
dcoefs_dtheta: Array,
n_iter: int,
converged: bool,
)
The state fit for one fixed parameter vector (the inner problem).
Attributes:
| Name | Type | Description |
|---|---|---|
coefs |
tuple of array
|
Fitted basis coefficients, one |
criterion |
float
|
The minimised |
sse |
float
|
Its weighted data part |
df |
float
|
Effective degrees of freedom, the trace of the (linearised) map from the data to the fitted values. |
gcv |
float
|
|
dcoefs_dtheta |
array
|
|
n_iter |
int
|
Gauss-Newton iterations used. |
converged |
bool
|
Whether the step tolerance was met. |
ProfileResult
dataclass
¶
ProfileResult(
theta: Array,
cov: Array,
sigma2: float,
states: tuple[FData, ...],
inner: InnerFit,
theta_path: Array,
sse_path: Array,
n_iter: int,
converged: bool,
param_names: tuple[str, ...] = (),
state_names: tuple[str, ...] = (),
)
The result of a generalized-profiling fit.
Attributes:
| Name | Type | Description |
|---|---|---|
theta |
array
|
|
cov |
array
|
|
sigma2 |
float
|
Residual variance |
states |
tuple of FData
|
The fitted state functions, one single-curve :class: |
inner |
InnerFit
|
The inner fit at |
theta_path |
array
|
|
sse_path |
array
|
|
n_iter |
int
|
Outer Gauss-Newton iterations. |
converged |
bool
|
Whether the outer tolerance was met. |
param_names, state_names |
tuple of str
|
Labels from the model. |
stderr
property
¶
stderr: Array
Standard errors, the square roots of the diagonal of :attr:cov.
Returns:
| Type | Description |
|---|---|
array
|
|
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ODEModel, profile_ode
>>> m = ODEModel(lambda x, t, th: -th[0] * x, 1, 1)
>>> t = np.linspace(0.0, 1.0, 21)
>>> y = np.exp(-t) + 0.01 * np.cos(40 * t)
>>> fit = profile_ode(m, t, y[:, None], fdt.BSpline((0.0, 1.0), 12), 1e3, [0.5])
>>> fit.stderr.shape
(1,)
__call__ ¶
__call__(t: Any, deriv: int = 0) -> Array
Evaluate the fitted states at t.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
array_like
|
Evaluation times. |
required |
deriv
|
int
|
Derivative order. |
0
|
Returns:
| Type | Description |
|---|---|
array
|
|
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ODEModel, profile_ode
>>> m = ODEModel(lambda x, t, th: -th[0] * x, 1, 1)
>>> t = np.linspace(0.0, 1.0, 21)
>>> fit = profile_ode(m, t, np.exp(-t)[:, None], fdt.BSpline((0.0, 1.0), 12), 1e3, [0.5])
>>> fit(np.array([0.0, 1.0])).shape
(2, 1)
ProfiledODE ¶
ProfiledODE(
model: ODEModel,
t: Any,
y: Any,
bases: Basis | Sequence[Basis],
lam: float | Sequence[float] = 1.0,
*,
state_weights: float | Sequence[float] = 1.0,
quadrature: tuple[Any, Any] | None = None,
n_quad: int = 5,
)
A generalized-profiling problem: model, data, bases and smoothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
ODEModel
|
The right-hand side. |
required |
t
|
array_like or sequence
|
Observation times: one |
required |
y
|
array_like or sequence
|
Observations: an |
required |
bases
|
Basis or sequence of Basis
|
One basis for every state, or one per state, all on the same domain. |
required |
lam
|
float or sequence of float
|
Weight :math: |
1.0
|
state_weights
|
float or sequence of float
|
Weights :math: |
1.0
|
quadrature
|
tuple of array_like
|
|
None
|
n_quad
|
int
|
Nodes per panel of the default rule. |
5
|
Attributes:
| Name | Type | Description |
|---|---|---|
model |
ODEModel
|
|
bases |
tuple of Basis
|
|
lam, state_weights |
tuple of float
|
|
nodes, weights |
array
|
The quadrature rule. |
n_obs |
int
|
Total number of observations |
Raises:
| Type | Description |
|---|---|
ValueError
|
If shapes, domains or weights are inconsistent, or no state is observed. |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ProfiledODE, fitzhugh_nagumo_model
>>> fhn = fitzhugh_nagumo_model()
>>> t = np.linspace(0.0, 20.0, 201)
>>> x = fhn.simulate(t, [-1.0, 1.0], [0.2, 0.2, 3.0])
>>> basis = fdt.BSpline(domain=(0.0, 20.0), breaks=np.linspace(0.0, 20.0, 201))
>>> problem = ProfiledODE(fhn, t, x, basis, lam=1e3)
>>> fit = problem.fit([0.3, 0.3, 2.5])
>>> np.round(fit.theta, 3).tolist()
[0.2, 0.2, 3.0]
n_coefs
property
¶
n_coefs: int
Total number of basis coefficients K over all states.
Returns:
| Type | Description |
|---|---|
int
|
|
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ProfiledODE, fitzhugh_nagumo_model
>>> t = np.linspace(0.0, 1.0, 5)
>>> p = ProfiledODE(fitzhugh_nagumo_model(), t, np.ones((5, 2)), fdt.BSpline(n_basis=6))
>>> p.n_coefs
12
residuals ¶
residuals(coefs: Any, theta: Any) -> Residuals
Residuals and Jacobians of the inner criterion (R's CSTRfitLS).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coefs
|
array_like or sequence of array_like
|
The stacked coefficients, or one array per state. |
required |
theta
|
array_like
|
|
required |
Returns:
| Type | Description |
|---|---|
Residuals
|
|
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ProfiledODE, ODEModel
>>> m = ODEModel(lambda x, t, th: -th[0] * x, 1, 1)
>>> t = np.linspace(0.0, 1.0, 5)
>>> p = ProfiledODE(m, t, np.ones((5, 1)), fdt.BSpline(n_basis=4))
>>> r = p.residuals(np.ones(4), [0.0])
>>> float(abs(r.data).max()), float(abs(r.equation).max())
(0.0, 0.0)
criterion ¶
criterion(coefs: Any, theta: Any) -> float
Evaluate the inner criterion J(c | theta).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coefs
|
array_like or sequence of array_like
|
Coefficients. |
required |
theta
|
array_like
|
Parameters. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Sum of squared data and equation residuals. |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ProfiledODE, ODEModel
>>> m = ODEModel(lambda x, t, th: -th[0] * x, 1, 1)
>>> t = np.linspace(0.0, 1.0, 5)
>>> p = ProfiledODE(m, t, np.zeros((5, 1)), fdt.BSpline(n_basis=4))
>>> p.criterion(np.ones(4), [0.0])
5.0
fit_states ¶
fit_states(
theta: Any,
coef0: Any = None,
*,
tol: float = 1e-14,
max_iter: int = 200,
) -> InnerFit
Fit the states for fixed parameters (the inner problem, R's CSTRfn).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta
|
array_like
|
|
required |
coef0
|
array_like or sequence
|
Starting coefficients; by default a least-squares fit of each observed state and zeros for unobserved ones. |
None
|
tol
|
float
|
Step size, relative to each state's largest coefficient, at which Gauss-Newton stops. |
1e-14
|
max_iter
|
int
|
Iteration cap. |
200
|
Returns:
| Type | Description |
|---|---|
InnerFit
|
Coefficients, criterion, degrees of freedom and |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ProfiledODE, ODEModel
>>> m = ODEModel(lambda x, t, th: -th[0] * x, 1, 1)
>>> t = np.linspace(0.0, 1.0, 11)
>>> p = ProfiledODE(m, t, np.exp(-t)[:, None], fdt.BSpline(n_basis=8), lam=1e2)
>>> inner = p.fit_states([1.0])
>>> inner.converged, inner.sse < 1e-8
(True, True)
fit ¶
fit(
theta0: Any,
coef0: Any = None,
*,
tol: float = 1e-08,
max_iter: int = 200,
inner_tol: float = 1e-14,
inner_max_iter: int = 200,
) -> ProfileResult
Estimate the parameters by Gauss-Newton on the profiled criterion.
Each outer step re-fits the states (warm-started from the previous
fit), forms the data-residual Jacobian A = -Phi dc/dtheta by the
implicit function theorem, and takes a Gauss-Newton step on
theta with Marquardt damping when the criterion would increase.
Iteration stops when the relative-offset criterion of Bates and Watts
(as R's nls), |Q'e| / sqrt(p) / (|e| / sqrt(N - p)), falls
below tol, or the step is at rounding level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
theta0
|
array_like
|
|
required |
coef0
|
array_like or sequence
|
Starting coefficients for the first inner fit. |
None
|
tol
|
float
|
Relative-offset tolerance. |
1e-08
|
max_iter
|
int
|
Outer iteration cap. |
200
|
inner_tol
|
optional
|
Passed to the inner fits (see :meth: |
1e-14
|
inner_max_iter
|
optional
|
Passed to the inner fits (see :meth: |
1e-14
|
Returns:
| Type | Description |
|---|---|
ProfileResult
|
Estimates, covariance, fitted states and the iteration path. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If there are no more observations than parameters. |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import ProfiledODE, ODEModel
>>> m = ODEModel(lambda x, t, th: -th[0] * x, 1, 1)
>>> t = np.linspace(0.0, 1.0, 21)
>>> basis = fdt.BSpline(domain=(0.0, 1.0), breaks=np.linspace(0.0, 1.0, 11))
>>> fit = ProfiledODE(m, t, 2 * np.exp(-0.7 * t)[:, None], basis, lam=1e4).fit([0.2])
>>> round(float(fit.theta[0]), 5)
0.7
fitzhugh_nagumo_model ¶
fitzhugh_nagumo_model() -> ODEModel
Build the FitzHugh-Nagumo neuron model, with analytic derivatives.
.. math::
\dot V = c\,(V - V^3/3 + R), \qquad \dot R = -(V - a + b R) / c,
with parameters :math:\theta = (a, b, c); the classic values are
(0.2, 0.2, 3) (Ramsay et al. 2007, section 2.1).
Returns:
| Type | Description |
|---|---|
ODEModel
|
States |
Examples:
>>> import numpy as np
>>> from fdatools.profiling import fitzhugh_nagumo_model
>>> fhn = fitzhugh_nagumo_model()
>>> x = fhn.simulate(np.linspace(0.0, 20.0, 5), [-1.0, 1.0], [0.2, 0.2, 3.0])
>>> x.shape
(5, 2)
cstr_inputs ¶
cstr_inputs(
t: Any, condition: str = "all.cool.step"
) -> Array
Input series of the CSTR experiments (R's CSTR2in).
The reactor is driven by five inputs: the flow F, the inlet
concentration CA0, the inlet temperature T0, the coolant inlet
temperature Tcin and the coolant flow Fc. The all.*.step
scenarios hold them at nominal values (F = 1, CA0 = 2,
T0 = 323, Fc = 15, Tcin = 335 cool or 365 hot) and step one
input at a time on the 4-minute intervals [4k, 4k + 4) of [0, 64):
F to 1.5 and 0.5, CA0 to 2.2 and 1.8, T0 to 343 and 303,
Tcin up and down by 5, Fc to 20 and 10. The Tc.*.step
scenarios lower Tcin by 15 on [2, 12) only, with F = 1 (hot)
or 0.05 (cool).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
array_like
|
Times (minutes). |
required |
condition
|
str
|
One of :data: |
'all.cool.step'
|
Returns:
| Type | Description |
|---|---|
array
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from fdatools.profiling import cstr_inputs
>>> cstr_inputs([0.0, 4.0, 8.0], "all.cool.step")[:, 0].tolist()
[1.0, 1.5, 0.5]
cstr_model ¶
cstr_model(
condition: str
| Callable[[Array], Any] = "all.cool.step",
*,
estimate: Sequence[str] = ("kref", "EoverR", "a", "b"),
**values: float,
) -> ODEModel
Build the two-state continuously stirred tank reactor (Ramsay et al. 2007).
States are the concentration C and temperature T:
.. math::
\dot C &= -\bigl(k(T) + F/V\bigr) C + (F/V)\, C_{A0}, \\
\dot T &= -\bigl(\alpha + F/V\bigr) T - \frac{\Delta H}{V \rho C_p} k(T)\, C
+ (F/V)\, T_0 + \alpha\, T_{cin},
with :math:k(T) = k_{ref} \exp\{-10^4 (E/R) (1/T - 1/T_{ref})\} and
:math:\alpha = a F_c^{b+1} / \{V \rho C_p (F_c + a F_c^b / (2 \rho_c C_{pc}))\}
-- the same right-hand side as R's CSTR2. The parameters are
kref, EoverR (in units of 1e4 K), a and b; those named in
estimate form :math:\theta (in that order) and the others are held at
their values. All derivatives are analytic.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
str or callable
|
A scenario of :func: |
'all.cool.step'
|
estimate
|
sequence of str
|
The parameters to estimate. |
('kref', 'EoverR', 'a', 'b')
|
**values
|
float
|
Overrides of the parameter values (:data: |
{}
|
Returns:
| Type | Description |
|---|---|
ODEModel
|
States |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a name in |
Examples:
>>> import numpy as np
>>> from fdatools.profiling import cstr_model
>>> model = cstr_model("all.cool.step", estimate=("kref", "EoverR"))
>>> dx = model(np.array([[1.5965, 341.3754]]), np.array([1.0]), np.array([0.461, 0.83301]))
>>> dx.round(6).tolist()
[[0.000106, 0.010411]]
simpson_rule ¶
simpson_rule(
breaks: Sequence[float], n_quad: int = 5
) -> tuple[Array, Array]
Composite Simpson rule on the panels between consecutive breaks.
Each panel gets n_quad equally spaced nodes including both ends, with
weights h/3 * (1, 4, 2, 4, ..., 2, 4, 1); a node on an interior break
appears twice, once per panel -- the rule of R's quadset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
breaks
|
sequence of float
|
Strictly increasing panel boundaries. |
required |
n_quad
|
int
|
Odd number of nodes per panel, at least 3. |
5
|
Returns:
| Type | Description |
|---|---|
nodes, weights : array
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from fdatools.profiling import simpson_rule
>>> nodes, weights = simpson_rule([0.0, 1.0], 5)
>>> nodes.tolist()
[0.0, 0.25, 0.5, 0.75, 1.0]
>>> [round(float(w) * 12, 12) for w in weights]
[1.0, 4.0, 2.0, 4.0, 1.0]
profile_ode ¶
profile_ode(
model: ODEModel,
t: Any,
y: Any,
bases: Basis | Sequence[Basis],
lam: float | Sequence[float],
theta0: Any,
*,
state_weights: float | Sequence[float] = 1.0,
quadrature: tuple[Any, Any] | None = None,
n_quad: int = 5,
coef0: Any = None,
tol: float = 1e-08,
max_iter: int = 200,
) -> ProfileResult
Estimate ODE parameters from noisy data by generalized profiling.
A one-call front end to :class:ProfiledODE; see there and the module
documentation for the method and the arguments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
ODEModel
|
The right-hand side |
required |
t
|
array_like or sequence
|
Observations (see :class: |
required |
y
|
array_like or sequence
|
Observations (see :class: |
required |
bases
|
Basis or sequence of Basis
|
State bases. |
required |
lam
|
float or sequence of float
|
Equation weights. |
required |
theta0
|
array_like
|
Starting parameters. |
required |
state_weights
|
float or sequence of float
|
Per-state weights. |
1.0
|
quadrature
|
tuple of array_like
|
|
None
|
n_quad
|
int
|
Simpson nodes per panel. |
5
|
coef0
|
array_like or sequence
|
Starting coefficients. |
None
|
tol
|
float
|
Outer relative-offset tolerance. |
1e-08
|
max_iter
|
int
|
Outer iteration cap. |
200
|
Returns:
| Type | Description |
|---|---|
ProfileResult
|
Estimates, covariance, fitted states and the iteration path. |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.profiling import fitzhugh_nagumo_model, profile_ode
>>> fhn = fitzhugh_nagumo_model()
>>> t = np.linspace(0.0, 20.0, 201)
>>> x = fhn.simulate(t, [-1.0, 1.0], [0.2, 0.2, 3.0])
>>> y = np.column_stack([x[:, 0], np.full(201, np.nan)]) # R is never observed
>>> basis = fdt.BSpline(domain=(0.0, 20.0), breaks=np.linspace(0.0, 20.0, 201))
>>> fit = profile_ode(fhn, t, y, basis, lam=1e3, theta0=[0.25, 0.25, 2.8])
>>> np.round(fit.theta, 2).tolist()
[0.2, 0.2, 3.0]