opinf.lift
#
Variable transformations for inducing polynomial structure.
Template class for lifting transformations. |
|
Quadratic lifting map \(q \to (q, q^2)\). |
|
Polynomial lifting map \(q \to (q, q^2, q^3, ...)\). |
Overview
Operator Inference models have polynomial structure. To calibrate a model with data, it can be advantageous to transform and/or augment the state variables to induce a desired structure.
LifterTemplate
provides an API for variable transformation / augmentation operations.
import numpy as np
import opinf
Lifting Maps#
Operator Inference learns models with polynomial terms, for example,
If training data do not exhibit this kind of polynomial structure, an Operator Inference model is not likely to perform well. In some systems with nonpolynomial nonlinearities, a change of variables can induce a polynomial structure, which can greatly improve the effectiveness of Operator Inference. Such variable transformations are often called lifting maps, especially if the transformation augments the state by introducing additional variables.
This module defines a template class for implementing lifting maps that can interface with opinf.roms
classes and provides a few examples of lifting maps.
Example
This example originates from [Qia21]. Consider a nonlinear diffusion-reaction equation with a cubic reaction term:
By introducing an auxiliary variable \(w = q^{2}\), we have \(\frac{\partial}{\partial t}w = 2q\frac{\partial q}{\partial t} = 2q\Delta q - 2q^4\), hence the previous equation can be expressed as the system
This system is quadratic in the lifted variables \((q, w)\), motivating a quadratic model structure instead of a cubic one.
The QuadraticLifter
class implements this transformation.
Custom Lifting Maps#
New transformers can be defined by inheriting from the LifterTemplate
.
Once implemented, the verify()
method may be used to test the consistency of these three methods.
class MyLifter(opinf.lift.LifterTemplate):
"""Custom lifting map."""
# Required methods --------------------------------------------------------
@staticmethod
def lift(state):
"""Lift the native state variables to the learning variables."""
raise NotImplementedError
@staticmethod
def unlift(lifted_state):
"""Recover the native state variables from the learning variables."""
raise NotImplementedError
# Optional methods --------------------------------------------------------
@staticmethod
def lift_ddts(state, ddts):
"""Lift the native state time derivatives to the time derivatives
of the learning variables.
"""
raise NotImplementedError
A more detailed version of this class is included in the package as opinf.lift.QuadraticLifter
.
Example: Specific Volume Variables#
This example was used in [GMW22, QKMW19, QKPW20, Qia21]. The compressible Euler equations for an ideal gas can be written in conservative form as
These equations are nonpolynomially nonlinear in the conservative variables \(\vec{q}_{c} = (\rho, \rho u, \rho e)\). However, by changing to the specific-volume variables \(\vec{q} = (u, p, \zeta)\) and using the ideal gas law
we arrive at a quadratic system
Hence, a quadratic reduced-order model of the form
can be learned for this system using data in the variables \(\vec{q}\). See [QKPW20] for details.
The following class defines this the variable transformation.
class EulerLifter(opinf.lift.LifterTemplate):
"""Lifting map for the Euler equations transforming conservative
variables to specific volume variables.
"""
def __init__(self, gamma=1.4):
"""Store the heat capacity ratio, gamma."""
self.gamma = gamma
def lift(self, state):
"""Map the conservative variables to the learning variables,
[rho, rho*u, rho*e] -> [u, p, 1/rho].
"""
rho, rho_u, rho_e = np.split(state, 3)
u = rho_u / rho
p = (self.gamma - 1) * (rho_e - 0.5 * rho * u**2)
zeta = 1 / rho
return np.concatenate((u, p, zeta))
def unlift(self, upzeta):
"""Map the learning variables to the conservative variables,
[u, p, 1/rho] -> [rho, rho*u, rho*e].
"""
u, p, zeta = np.split(upzeta, 3)
rho = 1 / zeta
rho_u = rho * u
rho_e = p / (self.gamma - 1) + 0.5 * rho * u**2
return np.concatenate((rho, rho_u, rho_e))
# Get test state data.
n = 100
Q = np.random.random((3 * n, 200))
# Verify the implementation.
EulerLifter().verify(Q)
lift() and unlift() are consistent