opinf.operators
#
Operator classes for the individual terms of a model.
Nonparametric Operators
Template for general operators \(\Ophat_{\ell}(\qhat,\u).\) |
|
Mixin for operators whose |
|
Template for nonparametric operators that can be calibrated through Operator Inference, i.e., \(\Ophat_{\ell}(\qhat, \u) = \Ohat_{\ell}\d(\qhat, \u)\). |
|
Constant operator \(\Ophat_{\ell}(\qhat,\u) = \chat \in \RR^{r}\). |
|
Linear state operator \(\Ophat_{\ell}(\qhat,\u) = \Ahat\qhat\) where \(\Ahat \in \RR^{r \times r}\). |
|
Quadratic state operator \(\Ophat_{\ell}(\qhat,\u) = \Hhat[\qhat\otimes\qhat]\) where \(\Hhat\in\RR^{r \times r^{2}}\). |
|
Cubic state operator \(\Ophat_{\ell}(\qhat,\u) = \Ghat[\qhat\otimes\qhat\otimes\qhat]\) where \(\Ghat\in\RR^{r \times r^{3}}\). |
|
Linear input operator \(\Ophat_{\ell}(\qhat,\u) = \Bhat\u\) where \(\Bhat \in \RR^{r \times m}\). |
|
Linear state / input interaction operator \(\Ophat_{\ell}(\qhat,\u) = \Nhat[\u\otimes\qhat]\) where \(\Nhat \in \RR^{r \times rm}\). |
Parametric Operators
Template for operators that depend on external parameters, \(\Ophat_{\ell}(\qhat,\u;\bfmu).\) |
|
Template for operators that depend on external parameters, and which can be calibrated through operator inference, i.e., \(\Ophat_\ell(\qhat,\u;\bfmu) = \Ohat_\ell(\bfmu)\d_\ell(\qhat,\u)\). |
|
Affine-parametric constant operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \chat_{\ell}(\bfmu) = \sum_{a=0}^{A_{\ell}-1}\theta_\ell^{(a)}\!(\bfmu)\,\chat_{\ell}^{(a)}.\) |
|
Affine-parametric linear operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ahat_{\ell}(\bfmu)\qhat = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Ahat_{\ell}^{(a)} \right)\qhat.\) |
|
Affine-parametric quadratic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Hhat_{\ell}(\bfmu)[\qhat\otimes\qhat] = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Hhat_{\ell}^{(a)} \right)[\qhat\otimes\qhat].\) |
|
Affine-parametric cubic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ghat_{\ell}(\bfmu)[\qhat\otimes\qhat\otimes\qhat] = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Ghat_{\ell}^{(a)} \right)[\qhat\otimes\qhat\otimes\qhat].\) |
|
Affine-parametric input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Bhat_{\ell}(\bfmu)\u = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Bhat_{\ell}^{(a)} \right)\u.\) |
|
Affine-parametric state-input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Nhat_{\ell}(\bfmu)\qhat = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Nhat_{\ell}^{(a)} \right)[\u\otimes\qhat].\) |
|
Parametric constant operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \chat(\bfmu) \in \RR^r\) where the parametric dependence is handled with elementwise interpolation. |
|
Parametric linear operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ahat(\bfmu)\qhat\) where \(\Ahat(\bfmu) \in \RR^{r \times r}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric quadratic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Hhat(\bfmu)[\qhat\otimes\qhat]\) where \(\Ahat(\bfmu) \in \RR^{r \times r^2}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric cubic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ghat(\bfmu)[\qhat\otimes\qhat\otimes\qhat]\) where \(\Ghat(\bfmu) \in \RR^{r \times r^3}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Bhat(\bfmu)\u\) where \(\Bhat(\bfmu) \in \RR^{r \times m}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric state-input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Nhat(\bfmu)[\u\otimes\qhat]\) where \(\Nhat(\bfmu) \in \RR^{r \times rm}\) and the parametric dependence is handled with elementwise interpolation. |
Overview
opinf.operators
classes represent the individual terms in a model. Nonparametric operators are functions of the state and input, while parametric operators are also dependent on one or more external parameters.Operators that can be written as the product of a matrix and a known vector-valued function can be calibrated through solving a regression problem.
opinf.models
objects are constructed with a list of operator objects. A model’sfit()
method constructs and solves a regression problem to learn the operator matrices.
import numpy as np
import scipy.linalg as la
import opinf
Operators#
Models based on Operator Inference are systems of ordinary differential equations (or discrete-time difference equations) that can be written as a sum of terms,
where each \(\Ophat_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{r}\) is a vector-valued function of the reduced state \(\qhat\in\RR^{r}\) and the input \(\u\in\RR^{m}\). We call these functions operators on this page.
Operator Inference calibrates operators that can be written as the product of a matrix \(\Ohat_{\ell}\in\RR^{r \times d_\ell}\) and a known (possibly nonlinear) vector-valued function \(\d_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{d_ell}\),
Operators with this structure are called OpInf operators on this page. The goal of Operator Inference is to learn the operator matrix \(\Ohat_\ell\) for each OpInf operator in the model.
This module defines classes representing various operators.
Example
To represent a linear time-invariant (LTI) system
we use the following operator classes.
Class |
Definition |
Operator matrix |
data vector |
---|---|---|---|
\(\Ophat_{1}(\qhat,\u) = \Ahat\qhat\) |
\(\Ohat_{1} = \Ahat \in \RR^{r\times r}\) |
\(\d_{1}(\qhat,\u) = \qhat\in\RR^{r}\) |
|
\(\Ophat_{2}(\qhat,\u) = \Bhat\u\) |
\(\Ohat_{2} = \Bhat \in \RR^{r\times m}\) |
\(\d_{2}(\qhat,\u) = \u\in\RR^{m}\) |
An opinf.models.ContinuousModel
object can be instantiated with a list of operators objects to represent (12):
LTI_model = opinf.models.ContinuousModel(
operators=[
opinf.operators.LinearOperator(),
opinf.operators.InputOperator(),
]
)
The operator matrices \(\Ahat\) and \(\Bhat\) are calibrated by calling LTI_model.fit()
.
Nonparametric Operators#
A nonparametric operator is a function of the state and input only. For OpInf operators, this means the operator matrix \(\Ohat_\ell\) is constant. See Parametric Operators for operators that also depend on one or more external parameters.
Available nonparametric OpInf operators are listed below.
Constant operator \(\Ophat_{\ell}(\qhat,\u) = \chat \in \RR^{r}\). |
|
Linear state operator \(\Ophat_{\ell}(\qhat,\u) = \Ahat\qhat\) where \(\Ahat \in \RR^{r \times r}\). |
|
Quadratic state operator \(\Ophat_{\ell}(\qhat,\u) = \Hhat[\qhat\otimes\qhat]\) where \(\Hhat\in\RR^{r \times r^{2}}\). |
|
Cubic state operator \(\Ophat_{\ell}(\qhat,\u) = \Ghat[\qhat\otimes\qhat\otimes\qhat]\) where \(\Ghat\in\RR^{r \times r^{3}}\). |
|
Linear input operator \(\Ophat_{\ell}(\qhat,\u) = \Bhat\u\) where \(\Bhat \in \RR^{r \times m}\). |
|
Linear state / input interaction operator \(\Ophat_{\ell}(\qhat,\u) = \Nhat[\u\otimes\qhat]\) where \(\Nhat \in \RR^{r \times rm}\). |
Nonparametric OpInf operators can be instantiated without arguments.
If the operator matrix is known, it can be passed into the constructor or set later with set_entries()
.
The operator matrix is stored as the entries
attribute and can be accessed with slicing operations [:]
.
N = opinf.operators.StateInputOperator()
print(N)
StateInputOperator
state_dimension: None
input_dimension: None
entries.shape: None
r = 4
m = 2
Ohat = np.arange(r * r * m).reshape((r, r * m))
N.set_entries(Ohat)
print(N)
StateInputOperator
state_dimension: 4
input_dimension: 2
entries.shape: (4, 8)
N.entries # or N[:]
array([[ 0, 1, 2, 3, 4, 5, 6, 7],
[ 8, 9, 10, 11, 12, 13, 14, 15],
[16, 17, 18, 19, 20, 21, 22, 23],
[24, 25, 26, 27, 28, 29, 30, 31]])
There are two ways to determine operator matrices in the context of model reduction:
Non-intrusive Operator Inference: Learn operator matrices from data.
Intrusive (Petrov–)Galerkin Projection: Compress an existing high-dimensional operator.
Once the entries
are set, the following methods are used to compute the action
of the operator or its derivatives.
apply()
: compute the operator action \(\Ophat_\ell(\qhat, \u)\).jacobian()
: construct the state Jacobian \(\ddqhat\Ophat_\ell(\qhat, \u)\).
qhat = np.zeros(r)
u = np.ones(m)
N.apply(qhat, u)
array([0., 0., 0., 0.])
N.jacobian(qhat, u)
array([[ 4., 6., 8., 10.],
[20., 22., 24., 26.],
[36., 38., 40., 42.],
[52., 54., 56., 58.]])
Learning Operators from Data#
Attention
This section describes the crux of Operator Inference.
Operator Inference requires state, input, and “left-hand side” data \(\{(\qhat_j,\u_j,\z_j)\}_{j=0}^{k-1}\) that approximately satisfy the desired model dynamics. For time-continuous models, \(\z_j\) is the time derivative of the state data; for fully discrete models, \(\z_j\) is the ``next state,’’ usually \(\qhat_{j+1}\). For (11), and assuming each operator is an OpInf operator, the data should approximately satisfy
Operator Inference determines the operator matrices \(\Ohat_1,\ldots,\Ohat_{n_\textrm{terms}}\) through a regression problem, written generally as
where \(\D\) is the \(k \times d\) data matrix formed from state and input snapshots and \(\Z\) is the \(r \times k\) matrix of left-hand side data. To arrive at this problem, we write
Collecting this matrix-vector product for each \(j=0,\ldots,k-1\) results in the matrix-matrix product system
which is \(\Z \approx \Ohat\D\trp\) with
where \(d = \sum_{i=1}^{n_\textrm{terms}}d_\ell\).
Nonparametric OpInf operator classes have two static methods that facilitate constructing the operator regression problem.
operator_dimension()
: given the state dimension \(r\) and the input dimension \(r\), return the data dimension \(d_\ell\).datablock()
: given the state-input data pairs \(\{(\qhat_j,\u_j)\}_{j=0}^{k-1}\), form the matrix
The complete data matrix \(\D\) is the concatenation of the data matrices from each operator:
Model Classes Do the Work
Model classes from opinf.models
are instantiated with a list of operators.
The model’s fit()
method calls the datablock()
method of each OpInf operator to assemble the full data matrix \(\D\), solves the regression problem for the full data matrix \(\Ohat\) (see opinf.lstsq
), and extracts from \(\Ohat\) the individual operator matrix \(\Ohat_{\ell}\) for each \(\ell = 1, \ldots, n_{\textrm{terms}}\) using the operator_dimension()
.
Example
For the LTI system (12), the Operator Inference regression \(\Z \approx \Ohat\D\trp\) has the following matrices.
In this setting, \(\z_j = \dot{\qhat}_j\), the time derivative of \(\qhat_j\). Collecting the state snapshots in the matrix \(\Qhat = [~\qhat_0~~\cdots~~\qhat_{k-1}~]\in\RR^{r\times k}\) and the inputs in the matrix \(\U = [~\u_0~~\cdots~~\u_{k-1}~]\), the full data matrix can be abbreviated as \(\D = [~\Qhat\trp~~\U\trp~]\).
If the regression \(\Z \approx \Ohat\D\trp\) is treated as an ordinary least-squares problem, the optimization problem to solve is given by
That is, the ordinary least-squares Operator Inference regression minimizes a sum of residuals of the model equation (12) with respect to available data.
Operators with Entries are Not Recalibrated
Only operators whose entries are not initialized (set to None
) when a model is constructed are learned with Operator Inference when fit()
is called.
For example, suppose for the LTI system (12) an appropriate input matrix \(\Bhat\) is known and stored as the variable B_
.
LTI_model = opinf.models.ContinuousModel(
operators=[
opinf.operators.LinearOperator(), # No entries specified.
opinf.operators.InputOperator(B_), # Entries set to B_.
]
)
In this case, LIT_model.fit()
only determines the entries of the LinearOperator
object using Operator Inference.
The known information for \(\Bhat\) is absorbed into the matrix \(\Z\), so the Operator Inference regression \(\Z \approx \Ohat\D\trp\) is now defined with the matrices
Using ordinary least-squares regression, the optimization problem is given by
A more likely scenario is that \(\Bhat\in\RR^{r \times m}\) is derived from a known \(\B\in\RR^{n \times m}\), which is the subject of the next section.
Learning Operators via Projection#
The goal of Operator Inference is to learn operator entries from data because the operators of a high-fidelity / full-order model are unknown or computationally inaccessible. However, in some scenarios a subset of the full-order model operators are known, in which case the corresponding reduced-order model operators can be determined through intrusive projection. Consider a full-order operator \(\Op:\RR^{n}\times\RR^{m}\to\RR^{n}\), written \(\Op_{\ell}(\q,\u)\), where
\(\q\in\RR^n\) is the full-order state, and
\(\u\in\RR^m\) is the input.
Given a trial basis \(\Vr\in\RR^{n\times r}\) and a test basis \(\Wr\in\RR^{n\times r}\), the corresponding intrusive projection of \(\Op_{\ell}\) is the operator \(\Ophat_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{r}\) defined by
where
\(\qhat\in\RR^{r}\) is the reduced-order state, and
\(\u\in\RR^{m}\) is the input (the same as before).
This approach uses the low-dimensional state approximation \(\q \approx \Vr\qhat\). If \(\Wr = \Vr\), the result is called a Galerkin projection. Note that if \(\Vr\) has orthonormal columns, we have in this case the simplification
If \(\Wr \neq \Vr\), the result is called a Petrov–Galerkin projection.
Example
Consider the bilinear operator
\(\Op(\q,\u) = \N[\u\otimes\q]\) where \(\N\in\RR^{n \times nm}\).
This type of operator can represented as a StateInputOperator
.
The intrusive Petrov–Galerkin projection of \(\Op\) is the bilinear operator
where \(\Nhat = (\Wr\trp\Vr)^{-1}\Wr\trp\N(\I_m\otimes\Vr) \in \RR^{r\times rm}\).
Hence, \(\Ophat\) can also be represented as a StateInputOperator
.
Using Galerkin projection (\(\Wr = \Vr\)), \(\Nhat\) simplifies to \(\Nhat = \Vr\trp\N(\I_m\otimes\Vr)\).
Every operator class has a galerkin()
method that performs intrusive projection.
Custom Nonparametric Operators#
New nonparametric operators can be defined by inheriting from OperatorTemplate
or, for operators that can be calibrated through Operator Inference, OpInfOperator
.
Once implemented, the verify()
method may be used to test for consistency between apply()
and the other methods outlined below.
For an arbitrary operator (not calibrated through Operator Inference), use the following inheritance template.
Show code cell source
class MyOperator(opinf.operators.OperatorTemplate):
"""Custom non-OpInf nonparametric operator."""
# Constructor -------------------------------------------------------------
def __init__(self, args_and_kwargs):
"""Construct the operator and set the state_dimension."""
raise NotImplementedError
# Required properties and methods -----------------------------------------
@property
def state_dimension(self):
"""Dimension of the state that the operator acts on."""
return NotImplemented
def apply(self, state, input_=None):
"""Apply the operator to the given state / input."""
raise NotImplementedError
# Optional methods --------------------------------------------------------
def jacobian(self, state, input_=None):
"""Construct the state Jacobian of the operator."""
raise NotImplementedError
def galerkin(self, Vr: np.ndarray, Wr=None):
"""Get the (Petrov-)Galerkin projection of this operator."""
raise NotImplementedError
def save(self, savefile, overwrite=False):
"""Save the operator to an HDF5 file."""
raise NotImplementedError
@classmethod
def load(cls, loadfile):
"""Load an operator from an HDF5 file."""
raise NotImplementedError
def copy(self):
"""Make a copy of the operator.
If not implemented, copy.deepcopy() is used.
"""
raise NotImplementedError
See OperatorTemplate
for details on the arguments for each method.
For a new Operator Inference operator, use the following inheritance template.
Show code cell source
class MyOpInfOperator(opinf.operators.OpInfOperator):
"""Custom nonparametric OpInf operator."""
# Required methods --------------------------------------------------------
@opinf.utils.requires("entries")
def apply(self, state=None, input_=None):
"""Apply the operator to the given state / input."""
raise NotImplementedError
@staticmethod
def datablock(states, inputs=None):
"""Return the data matrix block corresponding to the operator."""
raise NotImplementedError
@staticmethod
def operator_dimension(r=None, m=None):
"""Column dimension of the operator entries matrix."""
raise NotImplementedError
# Optional methods --------------------------------------------------------
@opinf.utils.requires("entries")
def jacobian(self, state, input_=None):
"""Construct the state Jacobian of the operator.
NOTE: If this method is omitted it is assumed that the Jacobian is
zero, implying that the operator does not depend on the state.
"""
raise NotImplementedError
def galerkin(self, Vr, Wr=None):
"""Get the (Petrov-)Galerkin projection of this operator."""
raise NotImplementedError
See OpInfOperator
for details on the arguments for each method.
Developer Notes
If the operator depends on the input \(\u\), the class should also inherit from
InputMixin
and set theinput_dimension
attribute.The
jacobian()
method is optional, butopinf.models
objects have ajacobian()
method that callsjacobian()
for each of its operators. In anopinf.models.ContinuousModel
, the Jacobian is required for some time integration strategies used inpredict()
.The
galerkin()
method is optional, butopinf.models
objects have agalerkin()
method that callsgalerkin()
for each of its operators.The
save()
andload()
methods should be implemented usingopinf.utils.hdf5_savehandle()
andopinf.utils.hdf5_loadhandle()
, respectively.
Example: Hadamard Product with a Fixed Vector#
Consider the operator \(\Ophat_{\ell}(\qhat, \u) = \qhat \ast \hat{\s}\), where \(\hat{\s}\in\RR^{r}\) is a constant vector and \(\ast\) denotes the Hadamard (elementwise) product (*
in NumPy).
To implement an operator for this class, we first calculate its state Jacobian and determine the operator produced by (Petrov–)Galerkin projection.
Let \(\qhat = [~\hat{q}_1~~\cdots~~\hat{q}_r~]\trp\) and \(\hat{\s} = [~\hat{s}_1~~\cdots~~\hat{s}_r~]\trp\), i.e., the \(i\)-th entry of \(\Ophat_{\ell}(\qhat, \u)\) is \(\hat{q}_i\hat{s}_i\). Then the \((i,j)\)-th entry of the Jacobian is
That is, \(\ddqhat\Ophat_{\ell}(\qhat, \u) = \operatorname{diag}(\hat{\s}).\)
Now consider a version of this operator with a large state dimension, \(\Op_{\ell}(\q, \u) = \q \ast \s\) for \(\q,\s\in\mathbb{R}^{n}\). For basis matrices \(\Vr,\Wr\in\mathbb{R}^{n \times r}\), the Petrov–Galerkin projection of \(\Op_{\ell}\) is given by
It turns out that this product can be written as a matrix-vector product \(\Ahat\qhat\) where \(\Ahat\) depends on \(\Vr\) and \(\s\).
Therefore, galerkin()
should return a LinearOperator
with entries matrix \(\Ahat\).
The following class inherits from OperatorTemplate
, stores \(\hat{\s}\) and sets the state dimension \(r\) in the constructor, and implements the methods outlined the inheritance template.
class HadamardOperator(opinf.operators.OperatorTemplate):
"""Custom non-OpInf nonparametric operator for the Hadamard product."""
# Constructor -------------------------------------------------------------
def __init__(self, s):
"""Construct the operator and set the state_dimension."""
self.svector = np.array(s)
self._jac = np.diag(self.svector)
# Required properties and methods -----------------------------------------
@property
def state_dimension(self):
"""Dimension of the state that the operator acts on."""
return self.svector.shape[0]
def apply(self, state, input_=None):
"""Apply the operator to the given state / input."""
svec = self.svector
if state.ndim == 2:
svec = svec.reshape((self.state_dimension, 1))
return state * svec
# Optional methods --------------------------------------------------------
def jacobian(self, state, input_=None):
"""Construct the state Jacobian of the operator."""
return self._jac
def galerkin(self, Vr, Wr=None):
"""Get the (Petrov-)Galerkin projection of this operator."""
if Wr is None:
Wr = Vr
n = self.state_dimension
r = Vr.shape[1]
M = la.khatri_rao(Vr.T, np.eye(n)).T
Ahat = Wr.T @ (M.reshape((n, r, n)) @ self.svector)
if not np.allclose((WrTVr := Wr.T @ Vr), np.eye(r)):
Ahat = la.solve(WrTVr, Ahat)
return opinf.operators.LinearOperator(Ahat)
def save(self, savefile, overwrite=False):
"""Save the operator to an HDF5 file."""
with opinf.utils.hdf5_savehandle(savefile, overwrite) as hf:
hf.create_dataset("svector", data=self.svector)
@classmethod
def load(cls, loadfile):
"""Load an operator from an HDF5 file."""
with opinf.utils.hdf5_loadhandle(loadfile) as hf:
return cls(hf["svector"][:])
def copy(self):
"""Make a copy of the operator."""
return self.__class__(self.svector)
r = 10
svec = np.random.standard_normal(r)
hadamard = HadamardOperator(svec)
print(hadamard)
HadamardOperator
state_dimension: 10
hadamard.verify()
apply() is consistent with state_dimension
jacobian() is consistent with state_dimension
jacobian() finite difference relative errors
------------------------------------------
h = 1.00e-01 error = 5.7463e-16
h = 1.00e-02 error = 1.3123e-14
h = 1.00e-03 error = 7.3735e-14
h = 1.00e-04 error = 7.7231e-13
h = 1.00e-05 error = 3.0133e-12
h = 1.00e-06 error = 9.6158e-11
h = 1.00e-07 error = 7.1203e-10
h = 1.00e-08 error = 5.0540e-09
h = 1.00e-09 error = 9.4283e-08
h = 1.00e-10 error = 3.7839e-07
galerkin() is consistent with apply()
copy() is consistent with state_dimension
copy() preserves the results of apply()
save()/load() is consistent with state_dimension
save()/load() preserves the results of apply()
Example: Weighted Hadamard Input Operator#
Consider an operator
where \(\Ohat_{\ell}\in\RR^{r \times m}\) and \(\ast\) is the Hadamard (elementwise) product. The matrix \(\Ohat_{\ell}\) can be calibrated with Operator Inference. Since \(\Ophat_{\ell}\) does not depend on the state \(\qhat\), the state Jacobian is zero.
class HadamardInputOperator(
opinf.operators.OpInfOperator,
opinf.operators.InputMixin,
):
"""Custom nonparametric OpInf operator: Hadamard of inputs."""
# Required methods --------------------------------------------------------
@property
def input_dimension(self):
"""Dimension of the input that the operator acts on."""
return self.entries.shape[1] if self.entries is not None else None
@opinf.utils.requires("entries")
def apply(self, state=None, input_=None):
"""Apply the operator to the given state / input."""
return self.entries @ (input_**2)
@staticmethod
def datablock(states, inputs=None):
"""Return the data matrix block corresponding to the operator."""
return inputs**2
@staticmethod
def operator_dimension(r=None, m=None):
"""Column dimension of the operator entries matrix."""
return m
# Optional methods --------------------------------------------------------
def galerkin(self, Vr, Wr=None):
"""Get the (Petrov-)Galerkin projection of this operator."""
if Wr is None:
Wr = Vr
r = Vr.shape[1]
Ohat = Wr.T @ self.entries
if not np.allclose((WrTVr := Wr.T @ Vr), np.eye(r)):
Ohat = la.solve(WrTVr, Ohat)
return self.__class__(Ohat)
# Instantiate an operator without entries.
hadamard_input = HadamardInputOperator()
print(hadamard_input)
HadamardInputOperator
state_dimension: None
input_dimension: None
entries.shape: None
hadamard_input.verify()
operator_dimension() is consistent with datablock()
cannot verify apply() when entries=None
m = 4
Ohat = np.random.random((r, m))
hadamard_input.set_entries(Ohat)
print(hadamard_input)
HadamardInputOperator
state_dimension: 10
input_dimension: 4
entries.shape: (10, 4)
hadamard_input.verify()
operator_dimension() is consistent with datablock()
apply() is consistent with state_dimension and input_dimension
jacobian() = 0
galerkin() is consistent with apply()
copy() is consistent with state_dimension and input_dimension
copy() preserves the results of apply()
save()/load() is consistent with state_dimension and input_dimension
save()/load() preserves the results of apply()
Parametric Operators#
An operator is called parametric if it depends on an independent parameter vector
\(\bfmu\in\RR^{p}\), i.e., \(\Ophat_{\ell} = \Ophat_{\ell}(\qhat,\u;\bfmu)\)
When the parameter vector is fixed, a parametric operator becomes nonparametric.
In particular, a parametric operator’s evaluate()
method accepts a parameter vector \(\bfmu\) and returns an instance of a nonparametric operator.
Parametric OpInf operators have the form \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ohat_{\ell}(\bfmu)\d_{\ell}(\qhat,\u)\) defined by the matrix-valued function \(\Ohat_{\ell}:\RR^{p}\to\RR^{r\times d_\ell}\) and (as in the nonparametric case) the data vector \(\d_{\ell}:\RR^{r}\times\RR^{m}\to\RR^{d_\ell}\). This module provides two options for the parameterization of \(\Ohat_{\ell}(\bfmu)\): affine expansion and elementwise interpolation. In each case, Operator Inference begins with \(s\) training parameter values \(\bfmu_{0},\ldots,\bfmu_{s-1}\) and corresponding state, input, and left-hand side data \(\{(\qhat_{i,j},\u_{i,j},\z_{i,j})\}_{j=0}^{k_{i}-1}\) for each training parameter value \(\bfmu_{i}\). A regression of the form \(\Z \approx \Ohat\D\trp\) is formed as in the nonparametric case, with the structure of the matrices \(\Ohat\) and \(\D\) depending on the choice of parameterization for each \(\Ohat_{\ell}(\bfmu)\).
Example
Let \(\bfmu = [~\mu_{0}~~\mu_{1}~]\trp\). The operator
is a parametric OpInf operator because it can be written as \(\Ophat_1(\qhat,\u;\bfmu) = \Ohat_1(\bfmu)\d_1(\qhat,\u)\) with \(\Ohat_1(\bfmu) = \mu_{0}\Ahat_{0} + \mu_{1}^{2}\Ahat_{1}\) and \(\d_1(\qhat,\u) = \qhat\).
This operator can be represented with an AffineLinearOperator
.
For a given parameter value, the evaluate()
method returns a LinearOperator
instance.
Affine Operators#
Affine parametric OpInf operators \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ohat_{\ell}(\bfmu)\d_{\ell}(\qhat,\u)\) parameterize the operator matrix \(\Ohat_{\ell}(\bfmu)\) as a sum of constant matrices with parameter-dependent scalar coefficients,
where each \(\theta_{\ell}^{(a)}:\RR^{p}\to\RR\) is a scalar-valued function and each \(\Ohat_{\ell}^{(a)}\in\RR^{r\times d_\ell}\) is constant. Affine expansions are grouped such that the coefficient functions \(\theta_{\ell}^{(0)},\ldots,\theta_{\ell}^{(A_{\ell}-1)}\) are linearly independent.
Affine parametric operators arise in Operator Inference settings because linear projection preserves affine structure.
Preservation of Affine Structure
Consider a full-order affine parametric OpInf operator
Given a trial basis \(\Vr\in\RR^{n\times r}\) and a test basis \(\Wr\in\RR^{n\times r}\), the intrusive projection of \(\Op_{\ell}\) is the operator
where \(\Ophat_{\ell}^{(a)}\!(\qhat, \u) = (\Wr\trp\Vr)^{-1}\Wr\trp\Op_{\ell}^{(a)}\!(\V\qhat, \u)\) is the intrusive projection of \(\Op_{\ell}^{(a)}\). That is, the intrusive projection of an affine expansion is an affine expansion of intrusive projections, and both expansions feature the same coefficient functions.
Available affine parametric operators are listed below.
Affine-parametric constant operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \chat_{\ell}(\bfmu) = \sum_{a=0}^{A_{\ell}-1}\theta_\ell^{(a)}\!(\bfmu)\,\chat_{\ell}^{(a)}.\) |
|
Affine-parametric linear operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ahat_{\ell}(\bfmu)\qhat = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Ahat_{\ell}^{(a)} \right)\qhat.\) |
|
Affine-parametric quadratic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Hhat_{\ell}(\bfmu)[\qhat\otimes\qhat] = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Hhat_{\ell}^{(a)} \right)[\qhat\otimes\qhat].\) |
|
Affine-parametric cubic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ghat_{\ell}(\bfmu)[\qhat\otimes\qhat\otimes\qhat] = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Ghat_{\ell}^{(a)} \right)[\qhat\otimes\qhat\otimes\qhat].\) |
|
Affine-parametric input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Bhat_{\ell}(\bfmu)\u = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Bhat_{\ell}^{(a)} \right)\u.\) |
|
Affine-parametric state-input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Nhat_{\ell}(\bfmu)\qhat = \left( \sum_{a=0}^{A_{\ell}-1}\theta_{\ell}^{(a)}\!(\bfmu)\,\Nhat_{\ell}^{(a)} \right)[\u\otimes\qhat].\) |
Affine parametric operators are instantiated with a function \(\boldsymbol{\theta}_{\ell}(\mu) = [~\theta_{\ell}^{(0)}(\bfmu)~~\cdots~~\theta_{\ell}^{(A_{\ell}-1)}(\bfmu)~]\trp\) for the affine expansion coefficients, the number of terms \(A_{\ell}\) in the expansion, and with or without the operator matrices \(\Ohat_{\ell}^{(1)},\ldots,\Ohat_{\ell}^{(A_{\ell})}\).
thetas = lambda mu: np.array([mu[0], mu[1] ** 2])
A = opinf.operators.AffineLinearOperator(thetas, nterms=2)
print(A)
AffineLinearOperator
state_dimension: None
parameter_dimension: None
expansion terms: 2
evaluate(parameter) -> LinearOperator
# Set the constant operator matrices in the affine expansion.
r = 5
Ahats = [np.ones((r, r)), np.eye(r)]
A.set_entries(Ahats, fromblock=False)
print(A)
AffineLinearOperator
state_dimension: 5
parameter_dimension: None
expansion terms: 2
evaluate(parameter) -> LinearOperator
A.entries
[array([[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.]]),
array([[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
[0., 0., 1., 0., 0.],
[0., 0., 0., 1., 0.],
[0., 0., 0., 0., 1.]])]
# Evaluate the parametric operator at a fixed parameter value,
# resulting in a nonparametric operator.
A_nonparametric = A.evaluate([2, 4])
print(A_nonparametric)
LinearOperator
state_dimension: 5
entries.shape: (5, 5)
A_nonparametric.entries
array([[18., 2., 2., 2., 2.],
[ 2., 18., 2., 2., 2.],
[ 2., 2., 18., 2., 2.],
[ 2., 2., 2., 18., 2.],
[ 2., 2., 2., 2., 18.]])
# The parameter dimension p = 2 was recorded when A was evaluated.
print(A)
AffineLinearOperator
state_dimension: 5
parameter_dimension: 2
expansion terms: 2
evaluate(parameter) -> LinearOperator
Operator Inference for Affine Operators
Consider a model with a single affine operator,
For the moment we will neglect the \(\ell = 1\) subscript. With data \(\{(\qhat_{i,j},\u_{i,j},\z_{i,j})\}_{j=0}^{k_{i}-1}\) corresponding to \(s\) training parameter values \(\bfmu_0,\ldots,\bfmu_{s-1}\), we seek the \(A\) operator matrices \(\Ohat^{(0)},\ldots,\Ohat^{(A-1)}\) such that for each parameter index \(i\) and time index \(j\), we have
where \(d\) is the output dimension of \(\d\). Collecting these expressions for each time index \(j = 0, \ldots, k_i - 1\) (but keeping the parameter index \(i\) fixed for the moment) results in
Finally, we concatenate each of these expressions for each parameter index \(i = 0,\ldots, s-1\) to arrive at
where \(K = \sum_{i=0}^{s-1}k_i\), the total number of available data instances.
This is the familiar \(\Z \approx \Ohat\D\trp\) where \(\Ohat = [~\Ohat^{(1)}~~\cdots~~\Ohat^{(A)}~]\), which can be solved for \(\Ohat\) using opinf.lstsq
.
The construction of \(\D\) is taken care of through the datablock()
method of the affine
For models with multiple affine operators, the operator matrix \(\Ohat\) is further concatenated horizontally to accommodate the operator matrices from each affine expansion, and the data matrix \(\D\trp\) gains additional block rows.
Interpolatory Operators#
Interpolatory parametric OpInf operators define the parametric dependence of the operator matrix on \(\bfmu\) through elementwise interpolation. That is,
where \(\Ohat_{\ell}(\bfmu)\) is determined by interpolating \(s\) matrices \(\Ohat_{\ell}^{(0)},\ldots,\Ohat_{\ell}^{(s-1)}\). In the context of Operator Inference, \(s\) is the number of training parameter values.
Available interpolatory operators are listed below.
Parametric constant operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \chat(\bfmu) \in \RR^r\) where the parametric dependence is handled with elementwise interpolation. |
|
Parametric linear operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ahat(\bfmu)\qhat\) where \(\Ahat(\bfmu) \in \RR^{r \times r}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric quadratic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Hhat(\bfmu)[\qhat\otimes\qhat]\) where \(\Ahat(\bfmu) \in \RR^{r \times r^2}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric cubic operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Ghat(\bfmu)[\qhat\otimes\qhat\otimes\qhat]\) where \(\Ghat(\bfmu) \in \RR^{r \times r^3}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Bhat(\bfmu)\u\) where \(\Bhat(\bfmu) \in \RR^{r \times m}\) and the parametric dependence is handled with elementwise interpolation. |
|
Parametric state-input operator \(\Ophat_{\ell}(\qhat,\u;\bfmu) = \Nhat(\bfmu)[\u\otimes\qhat]\) where \(\Nhat(\bfmu) \in \RR^{r \times rm}\) and the parametric dependence is handled with elementwise interpolation. |
Interpolatory operators can be instantiated with no arguments.
B = opinf.operators.InterpInputOperator()
print(B)
InterpInputOperator
state_dimension: None
input_dimension: None
parameter_dimension: None
training parameters: None
type(interpolator): None
evaluate(parameter) -> InputOperator
s = 9 # Number of training parameters
p = 1 # Dimension of the training parameters.
r = 4 # Dimension of the states.
m = 2 # Dimension of the inputs.
training_parameters = np.random.standard_normal((s, p))
operator_matrices = [np.random.random((r, m)) for _ in range(s)]
B.set_training_parameters(training_parameters)
print(B)
InterpInputOperator
state_dimension: None
input_dimension: None
parameter_dimension: 1
training parameters: 9
type(interpolator): None
evaluate(parameter) -> InputOperator
B.set_entries(operator_matrices, fromblock=False)
print(B)
InterpInputOperator
state_dimension: 4
input_dimension: 2
parameter_dimension: 1
training parameters: 9
type(interpolator): CubicSpline
evaluate(parameter) -> InputOperator
B_nonparametric = B.evaluate(np.random.standard_normal(p))
print(B_nonparametric)
InputOperator
state_dimension: 4
input_dimension: 2
entries.shape: (4, 2)
Operator Inference for Interpolatory Operators
Consider a model with a single affine operator,
For the moment we will neglect the \(\ell = 1\) subscript. With data \(\{(\qhat_{i,j},\u_{i,j},\z_{i,j})\}_{j=0}^{k_{i}-1}\) corresponding to \(s\) training parameter values \(\bfmu_0,\ldots,\bfmu_{s-1}\), we seek \(s\) operator matrices \(\Ohat^{(0)},\ldots,\Ohat^{(s-1)}\in\RR^{r\times d}\) such that for each parameter index \(i\) and time index \(j\), we have
which comes from the interpolation condition \(\Ohat^{(i)} = \Ohat_{1}(\bfmu_{i})\) for \(i = 0,\ldots,s-1\). Because only one operator matrix \(\Ohat^{(i)}\) defines the operator action at each parameter value for which we have data, we have \(s\) independent nonparametric Operator Inference problems:
The InterpolatedModel classes represent models comprised solely of interpolatory operators. If interpolatory operators are mixed with other operators (nonparametric or affine parametric), the \(\Ohat\D\trp\) block of the problem for the interpolatory operator is included as follows:
Mixing Nonparametric and Parametric Operators
Consider a system of ODEs with a mix of parametric and nonparametric operators,
This model can be written in the general form (11) with three operators:
\(\Ophat_1(\qhat,\u;\bfmu) = \left(\theta^{(0)}\!(\bfmu)\,\Ahat^{(0)} + \theta^{(1)}\!(\bfmu)\,\Ahat^{(1)}\right)\qhat(t)\), an affine-parametric linear operator where \(\theta^{(0)}\!(\bfmu) = \mu_{0}\) and \(\theta^{(1)}\!(\bfmu) = \cos(\mu_{1})\);
\(\Ophat_2(\qhat,\u) = \Hhat[\qhat(t) \otimes \qhat(t)]\), a nonparametric quadratic operator; and
\(\Ophat_3(\qhat,\u;\bfmu) = \Bhat(\bfmu)\u(t)\), a parametric input operator without a specified parametric structure.
If \(\Bhat(\bfmu)\) is parameterized with interpolation, the Operator Inference problem to learn the operator matrices can be written as \(\Z \approx \Ohat\D\trp\) in the following way. Let \(\Qhat_i\in\RR^{r\times k_i}\) and \(\U_i\in\RR^{m \times k_i}\) collect the state and input data for training parameter value \(\bfmu_i\), with corresponding state time derivative data \(\Z_i = \dot{\Qhat}_i\in\RR^{r\times k_i}\) for \(i = 0,\ldots, s-1\). We then have
where \(K = \sum_{i=0}^{s-1}k_i\) is the total number of data snapshots and \(d = 2r + r(r+1)/2 + sm\) is the total operator dimension. Note that the operator and data matrices have blocks corresponding to each of the three operators in the model.