You can impose physical constraints on a Neural ODE by formulating it as a Differential-Algebraic Equation (DAE). This is achieved by using a singular mass matrix M (where some rows are zero) and providing a constraint function to NeuralODEMM.
In this pattern, the mass matrix M defines which equations are differential (non-zero rows) and which are algebraic constraints (zero rows). When using NeuralODEMM, you must provide a constraint function that represents the algebraic part of the system (e.g., (u, p, t) -> [u[1] + u[2] + u[3] - 1] to enforce that the sum of components equals 1).
using DiffEqFlux
using Lux, ComponentArrays, Optimization, OptimizationOptimJL, OrdinaryDiffEq
using OrdinaryDiffEqRosenbrock: Rodas5
# 1. Define the mass matrix (singular matrix for DAE)
M = [1.0 0 0;
0 1.0 0;
0 0 0]
# 2. Define the constraint function (e.g., u[1]+u[2]+u[3]-1 = 0)
constraint_func = (u, p, t) -> [u[1] + u[2] + u[3] - 1]
# 3. Define the neural network architecture
nn_dudt = Lux.Chain(Lux.Dense(3, 64, tanh), Lux.Dense(64, 2))
pinit, st = Lux.setup(Random.default_rng(), nn_dudt)
# 4. Initialize NeuralODEMM with the constraint
model_stiff_ndae = NeuralODEMM(
nn_dudt,
constraint_func,
tspan,
M,
Rodas5(; autodiff = AutoFiniteDiff());
saveat = 0.1
)