Prepared by: Prof. Alexander Dowling, Myia Dickens (mdicken2@nd.edu, 2023), Molly Dougher (mdoughe6@nd.edu, 2023)
import sys
if "google.colab" in sys.modules:
!wget "https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py"
!pip install casadi
import helper
helper.easy_install()
else:
sys.path.insert(0, "../")
import helper
helper.set_plotting_style()External Documentation for libraries used¶
Pyomo.dae documentation:
CasADi (need to integrate DAEs):
For local installation:
pip install casadi. Warning: installingCasADiwithcondawill install an “okay” version of Ipopt. If you really want to installCasADiwithconda, you’ll likely need to addimport idaesto your notebook to load the “good” version of Ipopt (Linux and Windows users).
Dynamic Optimization Overview¶
Introduction to Dynamic Optimization¶
Dynamic systems can be found in a wide range of engineering fields and subfields. This section is meant to be an introduction to solve differentiable-algebraic equations (DAEs) with respect to optimizing dynamic systems. To allow for uniform understanding, the examples and explanations in this notebook will focus on basic physics examples (i.e. the pendulum example) and explanations.
DAEs are typically specified as initial value problems with initial conditions at zero. These equations are expressed with respect to an independent variable, , and are typically autonomous, or does not explicitly appear in the equation.
DAEs are typically of the form:
: state variables, functions of time
: control variables, functions of time
: variables, independent of
With respect to optimizing DAEs, the equations are structured to the simpler form:
where the state variables is partitioned into differentiable variables, , and algebraic variables . It is assumed that can be solved independently from , , and , which allows for the DAE to be treated as an ODE of the form:
The section, Handling Path Constraints, will explain DAE indexing and how to reduce the index of a DAE to allow it to be treated as an ODE.
General Breakdown of DAEs¶
With respect to constructing DAEs:
: Differential equations are typically derived from conservation laws.
: Algebraic equations are typically derived from constitutive and equilibrium laws.
The decision variables of the system fall under the two following variables:
: the control variables include manipulated variables that change over time.
: time-independent variables that correspond to parameters, initial conditions, and other steady state conditions.
DAEs as Boundary Value Problems¶
Because DAEs are typically initial value problems, they can also be boundary value problems (BVP), where the initial condition is replaced by boundary conditions. With respect to solving DAE optimization problems, BVP DAEs are of the form:
In this form the solutions may be nonunique or not exist over a specified reason; therefore, a key property is finding a locally unique solution, expressed by the theorem:
Theorem 8.2: Consider the BVP DAE with solution . Also let be Lipschitz continuous for all z(t) with for some and . Then the solution is locally unique if and only if the matrix:
is nonsingular, where the fundamental solution matrix is evaluated at the solution .
Theorem 8.2 is important when designing DAE constrained optimization problems.
DAE Optimization¶
Typically dynamic systems needed to be optimized over multiple periods, , in a time range, . The model, states, and decisions can change during each period, , or over multiple periods. The multiperiod, dynamic optimization problem is of the form:
In this form, it is assumed that the state variables are not continuous across periods; therefore, the last line is meant to connect the states of each period. The initial conditions and inequality constraints are represented as simple bounds in this form.
Key applications of DAEs and Dynamic Optimization are as follows:
Chemical Reactor Design
Parameter Estimation of a Dynamic System
Batch Process Estimation
Dynamic Real-Time Optimization
A description and further explanation of these applications can be found in Chapter 8 of the Biegler (2010) textbook.
Handling Path Constraints¶
Information taken from Section 8.4 of Biegler (2010).
The motivation for defining the index of a DAE system starts with considering the general algebraic equality constraint . After the algebraic and control variables are established, deriving the Euler-Lagrange equations requires variable and equation nesting. In order to be nested, the algebraic variables must be able to be implicitly eliminated from their paired algebraic equation. If cannot be implicitly eliminated from this algebraic equality, reformulation is needed. This reformulation begins with establishing an index of a DAE system.
DAE Index and Index Reduction¶
Definition 8.6: Consider the DAE systems of the form:
OR
with decisions ) and fixed. The index is the integer that represents the minimum number of differentiations of the DAE system (with respect to time) required to determine an ODE for the variables and .
Generally, for semiexplicit systems, the index of a DAE system can be determined by first differentiating the algebraic equations and then substituting resulting differential terms into the respective differential equation. Recall the number of differentiations required to isolate an ODE system corresponds to the index.
As the goal is usually to isolate a DAE of index 1, the following algorithm describes the typical procedure to reduce the index of a DAE system.
Algorithm 8.1: (Reduction of High Index DAEs) Start with a DAE of the form:
Check if the DAE system is index 1. If yes, stop.
Identify a subset of algebraic equations that can be solved for a subset of algebraic variables.
Consider the remaining algebraic equations that contain differential variables . Differentiating these remaining algebraic equations with respect to time leads to terms in the differentiated equations.
For the differential terms , substitute the right-hand sides of the corresponding differential equations into the differentiated algebraic equations, and eliminate (some of) these differential equations. This leads to new algebraic equations that replace the same number of existing differential equations.
With this new DAE system, go to step 1.
An example of this algorithm in use can be found within the following pendulum example.
Pendulum model and index reduction¶
Pendulum example:
Python version of example: http://
apmonitor .com /wiki /index .php /Apps /PendulumMotion More details on index reduction for example: https://
www .lehigh .edu / ~wes1 /apci /11may00 .pdf

The following DAE system describes the pendulum system:
To find the index of the DAE system, first take the derivative of the algebraic equation and substitute in for the differential variables:
Next, differentiate the obtained equation using the product rule, make similar substitutions, and rearrange:
Finally, implicitly differentiate the obtained equation, make similar substitutions, and rearrange to isolate the term:
Now the equations are a system of ODEs. Three differentiations were completed, indicating that this is an Index 3 DAE.
To reduce this DAE to index 1, follow Algorithm 8.1. The above analysis shows that this is not an index 1 DAE system. Therefore, the following algebraic equation is identified: . Differentiate the algebraic equation to yield:
which can be solved for . Substitute for and to yield:
The new DAE system is now:
where is now an algebraic variable and the final equation must be solved for . Differentiating the algebraic (last) equation leads to:
The equations can replace , leading to the following DAE system:
which is an Index 1 DAE.
## Load libraries
import pyomo.environ as pyo
import pyomo.dae as dae
from pyomo.dae.simulator import Simulator
import matplotlib.pyplot as plt
import numpy as np
## Define function for plotting results
def plot_results(sim, tsim, profiles):
"""
inputs:
sim: pyomo.DAE simulator results from a simulation of ODE or DAE
tsim: 1D array of time samples of the DAE/ODE simulatiion
profiles: 2D array of simulated differential and algebraic equations
outputs:
Plot 1: A plot of the curated profiles
Plot 2: A plot of the simulated results
time = list(m.t)
x = [value(m.x[t]) for t in m.t]
y = [value(m.y[t]) for t in m.t]
plt.plot(time, x, '-b', label='x')
plt.plot(time, y, '-r', label='y')
plt.xlabel('Time')
plt.ylabel('Position')
plt.legend(loc='best')
plt.show()
"""
plt.figure(1, figsize=(4, 4))
# Every series also carries a linestyle, so the figure survives a
# black-and-white printout (figures/README.md).
linestyles = ["-", "--", "-.", ":", (0, (3, 1, 1, 1, 1, 1))]
varorder = sim.get_variable_order()
algorder = sim.get_variable_order(vartype="algebraic")
# Create empty dictionary
results = {}
# Collect Different Profiles
for idx1, v in enumerate(varorder):
i = idx1
v_ = str(v)
results[v_] = profiles[:, i]
plt.plot(tsim, results[v_], label=v, linestyle=linestyles[i % len(linestyles)])
# Collect Algebraic Profiles
for idx2, v in enumerate(algorder):
i = len(varorder) + idx2
v_ = str(v)
results[v_] = profiles[:, i]
plt.plot(tsim, results[v_], label=v, linestyle=linestyles[i % len(linestyles)])
# Plot the simulated algebraic and differential profiles as they change in time
plt.xlabel("t", fontsize=16, fontweight="bold")
plt.tick_params(direction="in", labelsize=15)
plt.legend(loc="best")
plt.show()
# Plot the results of the simulation
plt.figure(2, figsize=(4, 4))
x_ = results["x[{t}]"]
y_ = results["y[{t}]"]
plt.plot(tsim, np.sqrt(x_**2 + y_**2), "-b", label="length")
plt.xlabel("t", fontsize=16, fontweight="bold")
plt.tick_params(direction="in", labelsize=15)
plt.ylabel(r"$\sqrt{x^2 + y^2}$")
plt.show()
# return resultsdef create_model_index3():
"""
Creates a model to define the Index 3 DAE system.
Output: the model, m
"""
m = pyo.ConcreteModel()
# Declare time
m.t = dae.ContinuousSet(bounds=(0.0, 1))
# Declare parameter - acceleration due to gravity
m.g = pyo.Param(initialize=9.81) # [m/s^2]
# Declare variables indexed over time
m.x = pyo.Var(m.t) # horizontal position
m.y = pyo.Var(m.t) # vertical position
m.u = pyo.Var(m.t) # horizontal velocity
m.v = pyo.Var(m.t) # vertical velocity
m.T = pyo.Var(m.t) # tension
# Declare derivative variables
m.dx = dae.DerivativeVar(m.x) # with respect to t is implied
m.dy = dae.DerivativeVar(m.y)
m.du = dae.DerivativeVar(m.u)
m.dv = dae.DerivativeVar(m.v)
# Declare differential equations
def _dx_eqn(m, t):
return m.dx[t] == m.u[t]
m.dx_eqn = pyo.Constraint(m.t, rule=_dx_eqn)
def _dy_eqn(m, t):
return m.dy[t] == m.v[t]
m.dy_eqn = pyo.Constraint(m.t, rule=_dy_eqn)
def _du_eqn(m, t):
return m.du[t] == -m.T[t] * m.x[t]
m.du_eqn = pyo.Constraint(m.t, rule=_du_eqn)
def _dv_eqn(m, t):
return m.dv[t] == m.g - m.T[t] * m.y[t]
m.dv_eqn = pyo.Constraint(m.t, rule=_dv_eqn)
# Declare algebraic equation
def _alg_eqn(m, t):
return m.x[t] ** 2 + m.y[t] ** 2 == 1
m.alg_eqn = pyo.Constraint(m.t, rule=_alg_eqn)
# Specify initial conditions
m.x[0] = 0
m.y[0] = 1
m.u[0] = 1
m.v[0] = 0
m.T[0] = 1 + m.g
return mindex3 = create_model_index3()
# Specify integrator options
int_ops = {"print_stats": True, "abstol": 1e-8, "reltol": 1e-6}
# Solve DAEs
sim = Simulator(index3, package="casadi")
try:
tsim, profiles = sim.simulate(
numpoints=100, integrator="idas", integrator_options=int_ops
)
except RuntimeError as err:
# Expected: IDAS supports index-0 and index-1 DAEs, not this index-3 model.
print(f"Expected IDAS failure: {err}")
else:
plot_results(sim, tsim, profiles)CasADi - 2026-08-21 12:57:46 WARNING("The options 't0', 'tf', 'grid' and 'output_t0' have been deprecated.
The same functionality is provided by providing additional input arguments to the 'integrator' function, in particular:
* Call integrator(..., t0, tf, options) for a single output time, or
* Call integrator(..., t0, grid, options) for multiple grid points.
The legacy 'output_t0' option can be emulated by including or excluding 't0' in 'grid'.
Backwards compatibility is provided in this release only.") [.../casadi/core/integrator.cpp:698]
At t = 0 and h = 1.06624e-14, the corrector convergence failed repeatedly or with |h| = hmin.
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[3], line 73
69 int_ops = {"print_stats": True, "abstol": 1e-8, "reltol": 1e-6}
70
71 # Solve DAEs
72 sim = Simulator(index3, package="casadi")
---> 73 tsim, profiles = sim.simulate(
74 numpoints=100, integrator="idas", integrator_options=int_ops
75 )
76
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:935, in Simulator.simulate(self, numpoints, tstep, integrator, varying_inputs, initcon, integrator_options)
931 tsim, profile = self._simulate_with_casadi_with_inputs(
932 initcon, tsim, varying_inputs, integrator, integrator_options
933 )
934 else:
--> 935 tsim, profile = self._simulate_with_casadi_no_inputs(
936 initcon, tsim, integrator, integrator_options
937 )
939 self._tsim = tsim
940 self._simsolution = profile
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:997, in Simulator._simulate_with_casadi_no_inputs(self, initcon, tsim, integrator, integrator_options)
995 integrator_options['output_t0'] = True
996 F = casadi.integrator('F', integrator, dae, integrator_options)
--> 997 sol = F(x0=initcon)
998 profile = sol['xf'].full().T
1000 if len(self._algvars) != 0:
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:24049, in Function.__call__(self, *args, **kwargs)
24045 else:
24046 return tuple(ret)
24047 else:
24048 # Named inputs -> return dictionary
> 24049 return self.call(kwargs)
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:20698, in Function.call(self, *args)
20694
20695
20696
20697 """
> 20698 return _casadi.Function_call(self, *args)
RuntimeError: Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:1466:
Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:362:
.../casadi/interfaces/sundials/idas_interface.cpp:599: IDASolve returned "IDA_CONV_FAIL". Consult IDAS documentation.Warning: If you run this notebook in Colab, you may get the following runtime error and your kernel may crash:


Why did the IDAS integrator in SUNDIALS fail? It is only meant for index 0 or 1 DAEs! Integrating high index DAEs is really hard!
def create_model_ode():
"""
Creates a model to define the Index 0 DAE system.
Output: the model, m
"""
m = pyo.ConcreteModel()
# Declare time
m.t = dae.ContinuousSet(bounds=(0.0, 5.0))
# Declare parameter - acceleration due to gravit
m.g = pyo.Param(initialize=9.81) # [m/s^2]
# Declare variables indexed over time
m.x = pyo.Var(m.t) # horizontal position
m.y = pyo.Var(m.t) # vertical position
m.u = pyo.Var(m.t) # horizontal velocity
m.v = pyo.Var(m.t) # vertical velocity
m.T = pyo.Var(m.t) # tension
# Declare derivative variables
m.dx = dae.DerivativeVar(m.x) # with respect to t is implied
m.dy = dae.DerivativeVar(m.y)
m.du = dae.DerivativeVar(m.u)
m.dv = dae.DerivativeVar(m.v)
m.dT = dae.DerivativeVar(m.T)
# Declare differential equations
def _dx_eqn(m, t):
return m.dx[t] == m.u[t]
m.dx_eqn = pyo.Constraint(m.t, rule=_dx_eqn)
def _dy_eqn(m, t):
return m.dy[t] == m.v[t]
m.dy_eqn = pyo.Constraint(m.t, rule=_dy_eqn)
def _du_eqn(m, t):
return m.du[t] == -m.T[t] * m.x[t]
m.du_eqn = pyo.Constraint(m.t, rule=_du_eqn)
def _dv_eqn(m, t):
return m.dv[t] == m.g - m.T[t] * m.y[t]
m.dv_eqn = pyo.Constraint(m.t, rule=_dv_eqn)
def _dT_eqn(m, t):
return (
m.dT[t]
== (
-4 * m.T[t] * (m.x[t] * m.u[t] + m.y[t] * m.v[t])
+ 3 * m.g * m.v[t]
) / (m.x[t] ** 2 + m.y[t] ** 2)
)
m.dT_eqn = pyo.Constraint(m.t, rule=_dT_eqn)
# Specify initial conditions
m.x[0] = 0
m.y[0] = 1
m.u[0] = 1
m.v[0] = 0
m.T[0] = 1 + m.g
return m
ode = create_model_ode()
# Specify integrator options
int_ops = {"print_stats": True, "abstol": 1e-6, "reltol": 1e-4}
# Solve DAEs
sim = Simulator(ode, package="casadi")
tsim, profiles = sim.simulate(
numpoints=100, integrator="idas", integrator_options=int_ops
)
# Plot solution
results = plot_results(sim, tsim, profiles)FORWARD INTEGRATION:
Number of steps taken by SUNDIALS: 172
Number of calls to the user's f function: 236
Number of calls made to the linear solver setup function: 25
Number of error test failures: 6
Method order used on the last internal step: 5
Method order to be used on the next internal step: 5
Actual value of initial step size: 7.90569e-07
Step size taken on the last internal step: 0.0158106
Step size to be attempted on the next internal step: 0.0316212
Current internal time reached: 5
Number of nonlinear iterations performed: 234
Number of nonlinear convergence failures: 0


The drift, over a longer horizon¶
The plot above is the same quantity as the figure below, run for with IDAS.
The figure below is the canonical version used in the lecture handout: the same index-reduced
ODE, integrated with scipy.integrate.solve_ivp (RK45) out to , at two tolerances, and
with a third curve that adds the stabilization term of Ascher & Petzold
(1998), eq. (9.40), p. 251.

The takeaway: drift is not a tolerance bug. Tightening rtol buys decades, not a fix, because
nothing in the reduced ODE refers to — only to its derivatives.
Source: figures/plots/pendulum-drift.py. The figure and Pyomo model both use
, obtained by differentiating the
acceleration constraint.
Dimensional check¶
Restore units: in m; in m/s; and in .
| Term | Units |
|---|---|
| quotient, |
The denominator is required dimensionally. Normalizing the rod length to one can hide this factor algebraically, but not physically.
Discussion
Are all of the algebraic constraints in the original formulation satisfied?
Formulation 3: Index-1 DAE Model¶
(This reformulation is NOT unique... could have written and instead.)
Consistent initial conditions:
Specify and .
Solve for , , and
def create_model_index1():
"""
Creates a model to define the Index 1 DAE system.
Output: the model, m
"""
m = pyo.ConcreteModel()
# Declare time
m.t = dae.ContinuousSet(bounds=(0, 5))
# Declare parameter - acceleration due to gravity
m.g = pyo.Param(initialize=9.81) # [m/s^2]
# Declare variables indexed over time
m.x = pyo.Var(m.t) # horizontal position
m.y = pyo.Var(m.t) # vertical position
m.u = pyo.Var(m.t) # horizontal velocity
m.v = pyo.Var(m.t) # vertical velocity
m.T = pyo.Var(m.t) # tension
# Declare derivative variables
m.dy = dae.DerivativeVar(m.y)
m.dv = dae.DerivativeVar(m.v)
# Declare differential equations
def _dy_eqn(m, t):
return m.dy[t] == m.v[t]
m.dy_eqn = pyo.Constraint(m.t, rule=_dy_eqn)
def _dv_eqn(m, t):
return m.dv[t] == m.g - m.T[t] * m.y[t]
m.dv_eqn = pyo.Constraint(m.t, rule=_dv_eqn)
# Declare algebraic equations
def _alg_eqn1(m, t):
return m.x[t] ** 2 + m.y[t] ** 2 == 1
m.alg_eqn1 = pyo.Constraint(m.t, rule=_alg_eqn1)
def _alg_eqn2(m, t):
return m.x[t] * m.u[t] + m.y[t] * m.v[t] == 0
m.alg_eqn2 = pyo.Constraint(m.t, rule=_alg_eqn2)
def _alg_eqn3(m, t):
return (
m.u[t] ** 2
+ m.v[t] ** 2
- m.T[t] * (m.x[t] ** 2 + m.y[t] ** 2)
+ m.g * m.y[t]
== 0
)
m.alg_eqn3 = pyo.Constraint(m.t, rule=_alg_eqn3)
# Specify initial conditions
m.x[0] = 0
m.y[0] = 1
m.u[0] = 1
m.v[0] = 0
m.T[0] = 1 + m.g
return mdef index1_check_constraints(m):
"""Check if the three constraints are feasible."""
print("Constraint 1:")
r1 = m.x[0]() ** 2 + m.y[0]() ** 2 - 1
print(r1)
print("\nConstraint 2:")
r2 = m.x[0]() * m.u[0]() + m.y[0]() * m.v[0]()
print(r2)
print("\nConstraint 3:")
r3 = m.u[0]() ** 2 + m.v[0]() ** 2 - m.T[0]() + m.g * m.y[0]()
print(r3)
index1 = create_model_index1()
# Check initial condition
index1_check_constraints(index1)
# Specify integrator options
int_ops = {"print_stats": True, "abstol": 1e-6, "reltol": 1e-4}
# Solve DAEs
sim = Simulator(index1, package="casadi")
try:
tsim, profiles = sim.simulate(
numpoints=100, integrator="idas", integrator_options=int_ops
)
except RuntimeError as err:
# Expected here: the reformulation is singular at x = 0.
print(f"Expected point-singularity failure: {err}")
# tsim, profiles = sim.simulate(numpoints=100, integrator='collocation')Constraint 1:
0
Constraint 2:
0
Constraint 3:
0.0
The residual routine or the linear setup or solve routine had a recoverable error, but IDACalcIC was unable to recover.
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[5], line 96
92 int_ops = {"print_stats": True, "abstol": 1e-6, "reltol": 1e-4}
93
94 # Solve DAEs
95 sim = Simulator(index1, package="casadi")
---> 96 tsim, profiles = sim.simulate(
97 numpoints=100, integrator="idas", integrator_options=int_ops
98 )
99 # tsim, profiles = sim.simulate(numpoints=100, integrator='collocation')
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:935, in Simulator.simulate(self, numpoints, tstep, integrator, varying_inputs, initcon, integrator_options)
931 tsim, profile = self._simulate_with_casadi_with_inputs(
932 initcon, tsim, varying_inputs, integrator, integrator_options
933 )
934 else:
--> 935 tsim, profile = self._simulate_with_casadi_no_inputs(
936 initcon, tsim, integrator, integrator_options
937 )
939 self._tsim = tsim
940 self._simsolution = profile
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:997, in Simulator._simulate_with_casadi_no_inputs(self, initcon, tsim, integrator, integrator_options)
995 integrator_options['output_t0'] = True
996 F = casadi.integrator('F', integrator, dae, integrator_options)
--> 997 sol = F(x0=initcon)
998 profile = sol['xf'].full().T
1000 if len(self._algvars) != 0:
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:24049, in Function.__call__(self, *args, **kwargs)
24045 else:
24046 return tuple(ret)
24047 else:
24048 # Named inputs -> return dictionary
> 24049 return self.call(kwargs)
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:20698, in Function.call(self, *args)
20694
20695
20696
20697 """
> 20698 return _casadi.Function_call(self, *args)
RuntimeError: Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:1466:
Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:362:
.../casadi/interfaces/sundials/idas_interface.cpp:599: IDACalcIC returned "IDA_NO_RECOVERY". Consult IDAS documentation.What happened? Point singularity at .
Let’s try as the initial point.
index1_again = create_model_index1()
# Specify alternative initial conditions
small_number = 0.1
index1_again.x[0] = small_number
index1_again.y[0] = 1
index1_again.u[0] = 1
index1_again.v[0] = 0
index1_again.T[0] = 1 + index1_again.g
# Check initial condition
index1_check_constraints(index1_again)
# Solve DAEs
sim = Simulator(index1_again, package="casadi")
# Simulator
try:
tsim, profiles = sim.simulate(
numpoints=100, integrator="idas", integrator_options=int_ops
)
except RuntimeError as err:
# Expected here: these trial initial values violate the invariants.
print(f"Expected inconsistent-initialization failure: {err}")Constraint 1:
0.010000000000000009
Constraint 2:
0.1
Constraint 3:
0.0
The residual routine or the linear setup or solve routine had a recoverable error, but IDACalcIC was unable to recover.
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[6], line 19
15 # Solve DAEs
16 sim = Simulator(index1_again, package="casadi")
17
18 # Simulator
---> 19 tsim, profiles = sim.simulate(
20 numpoints=100, integrator="idas", integrator_options=int_ops
21 )
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:935, in Simulator.simulate(self, numpoints, tstep, integrator, varying_inputs, initcon, integrator_options)
931 tsim, profile = self._simulate_with_casadi_with_inputs(
932 initcon, tsim, varying_inputs, integrator, integrator_options
933 )
934 else:
--> 935 tsim, profile = self._simulate_with_casadi_no_inputs(
936 initcon, tsim, integrator, integrator_options
937 )
939 self._tsim = tsim
940 self._simsolution = profile
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:997, in Simulator._simulate_with_casadi_no_inputs(self, initcon, tsim, integrator, integrator_options)
995 integrator_options['output_t0'] = True
996 F = casadi.integrator('F', integrator, dae, integrator_options)
--> 997 sol = F(x0=initcon)
998 profile = sol['xf'].full().T
1000 if len(self._algvars) != 0:
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:24049, in Function.__call__(self, *args, **kwargs)
24045 else:
24046 return tuple(ret)
24047 else:
24048 # Named inputs -> return dictionary
> 24049 return self.call(kwargs)
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:20698, in Function.call(self, *args)
20694
20695
20696
20697 """
> 20698 return _casadi.Function_call(self, *args)
RuntimeError: Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:1466:
Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:362:
.../casadi/interfaces/sundials/idas_interface.cpp:599: IDACalcIC returned "IDA_NO_RECOVERY". Consult IDAS documentation.Our initial point does not satisfy the algebraic constraints! We need a consistent initial point.
index1_take_two = create_model_index1()
# Specify alternative initial conditions
small_number = 0.1
index1_take_two.x[0] = small_number
index1_take_two.y[0] = np.sqrt(1 - small_number**2)
index1_take_two.u[0] = 1
index1_take_two.v[0] = (
-index1_take_two.x[0]() * index1_take_two.u[0]() / index1_take_two.y[0]()
)
index1_take_two.T[0] = (
index1_take_two.u[0]() ** 2
+ index1_take_two.v[0]() ** 2
+ index1_take_two.g * index1_take_two.y[0]()
)
# Check initial condition
index1_check_constraints(index1_take_two)
# Solve DAEs
sim = Simulator(index1_take_two, package="casadi")
# Specify integrator options
int_ops2 = {
"print_stats": True,
"abstol": 1e-6,
"reltol": 1e-4,
"verbose": False,
"calc_ic": True,
}
# Simulator
try:
tsim, profiles = sim.simulate(
numpoints=20, integrator="idas", integrator_options=int_ops2
)
except RuntimeError as err:
# This documented failure motivates discretizing the model next.
print(f"Expected formulation failure: {err}")Constraint 1:
0.0
Constraint 2:
0.0
Constraint 3:
0.0
The residual routine or the linear setup or solve routine had a recoverable error, but IDACalcIC was unable to recover.
---------------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
Cell In[7], line 34
30 "calc_ic": True,
31 }
32
33 # Simulator
---> 34 tsim, profiles = sim.simulate(
35 numpoints=20, integrator="idas", integrator_options=int_ops2
36 )
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:935, in Simulator.simulate(self, numpoints, tstep, integrator, varying_inputs, initcon, integrator_options)
931 tsim, profile = self._simulate_with_casadi_with_inputs(
932 initcon, tsim, varying_inputs, integrator, integrator_options
933 )
934 else:
--> 935 tsim, profile = self._simulate_with_casadi_no_inputs(
936 initcon, tsim, integrator, integrator_options
937 )
939 self._tsim = tsim
940 self._simsolution = profile
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/pyomo/dae/simulator.py:997, in Simulator._simulate_with_casadi_no_inputs(self, initcon, tsim, integrator, integrator_options)
995 integrator_options['output_t0'] = True
996 F = casadi.integrator('F', integrator, dae, integrator_options)
--> 997 sol = F(x0=initcon)
998 profile = sol['xf'].full().T
1000 if len(self._algvars) != 0:
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:24049, in Function.__call__(self, *args, **kwargs)
24045 else:
24046 return tuple(ret)
24047 else:
24048 # Named inputs -> return dictionary
> 24049 return self.call(kwargs)
File ~/opt/anaconda3/envs/optimization_fall2026/lib/python3.13/site-packages/casadi/casadi.py:20698, in Function.call(self, *args)
20694
20695
20696
20697 """
> 20698 return _casadi.Function_call(self, *args)
RuntimeError: Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:1466:
Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:362:
.../casadi/interfaces/sundials/idas_interface.cpp:599: IDACalcIC returned "IDA_NO_RECOVERY". Consult IDAS documentation.Hmm, this does not make sense. Perhaps there is something subtle wrong with the model.
Let’s try solving the model with Ipopt after discretizing with collocation.
# discretize the model
index1_take_two.Obj = pyo.Objective(expr=1) # Add a dummy objective
discretizer = pyo.TransformationFactory("dae.collocation")
discretizer.apply_to(index1_take_two, nfe=15, scheme="LAGRANGE-RADAU", ncp=3)
# initialize
for t in index1_take_two.t:
index1_take_two.x[t] = small_number
index1_take_two.y[t] = np.sqrt(1 - small_number**2)
index1_take_two.u[t] = 1
index1_take_two.v[t] = (
-index1_take_two.x[t]() * index1_take_two.u[t]() / index1_take_two.y[t]()
)
index1_take_two.T[t] = (
index1_take_two.u[t]() ** 2
+ index1_take_two.v[t]() ** 2
+ index1_take_two.g * index1_take_two.y[t]()
)# solve the discretized model
solver = pyo.SolverFactory("ipopt")
solver.options["max_iter"] = 300
results = solver.solve(index1_take_two, tee=True)
assert pyo.check_optimal_termination(results), (
f"Solve failed: status={results.solver.status}, "
f"termination={results.solver.termination_condition}"
)Ipopt 3.14.19: max_iter=300
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit https://github.com/coin-or/Ipopt
******************************************************************************
This is Ipopt version 3.14.19, running with linear solver MUMPS 5.8.2.
Number of nonzeros in equality constraint Jacobian...: 1186
Number of nonzeros in inequality constraint Jacobian.: 0
Number of nonzeros in Lagrangian Hessian.............: 368
Total number of variables............................: 322
variables with only lower bounds: 0
variables with lower and upper bounds: 0
variables with only upper bounds: 0
Total number of equality constraints.................: 320
Total number of inequality constraints...............: 0
inequality constraints with only lower bounds: 0
inequality constraints with lower and upper bounds: 0
inequality constraints with only upper bounds: 0
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
0 1.0000000e+00 9.07e-01 0.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
1 1.0000000e+00 2.58e+00 1.51e-21 -1.7 4.10e+00 - 1.00e+00 1.00e+00h 1
2 1.0000000e+00 3.54e+00 9.17e-03 -1.7 2.11e+00 -4.0 1.00e+00 1.00e+00h 1
3 1.0000000e+00 3.17e+00 2.87e-01 -1.7 1.78e+00 -3.6 1.00e+00 1.00e+00h 1
4 1.0000000e+00 6.54e-01 7.16e-02 -1.7 2.49e+00 - 1.00e+00 1.00e+00h 1
5 1.0000000e+00 1.56e-01 1.79e-02 -1.7 3.95e-01 -3.1 1.00e+00 1.00e+00h 1
6 1.0000000e+00 3.66e-02 4.48e-03 -2.5 1.92e-01 -2.7 1.00e+00 1.00e+00h 1
7 1.0000000e+00 1.87e-02 5.74e-02 -2.5 2.25e-01 -2.3 1.00e+00 5.00e-01h 2
8 1.0000000e+00 1.84e-02 1.44e-02 -2.5 1.39e-01 - 1.00e+00 1.00e+00h 1
9 1.0000000e+00 5.18e-03 3.59e-03 -3.8 7.39e-02 - 1.00e+00 1.00e+00h 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
10 1.0000000e+00 1.30e-03 8.97e-04 -3.8 3.69e-02 - 1.00e+00 1.00e+00h 1
11 1.0000000e+00 3.24e-04 2.24e-04 -5.7 1.85e-02 - 1.00e+00 1.00e+00h 1
12 1.0000000e+00 8.10e-05 5.61e-05 -5.7 9.23e-03 - 1.00e+00 1.00e+00h 1
13 1.0000000e+00 2.02e-05 1.40e-05 -5.7 4.62e-03 - 1.00e+00 1.00e+00h 1
14 1.0000000e+00 5.06e-06 3.50e-06 -5.7 2.31e-03 - 1.00e+00 1.00e+00h 1
15 1.0000000e+00 1.26e-06 8.76e-07 -8.6 1.15e-03 - 1.00e+00 1.00e+00h 1
16 1.0000000e+00 3.16e-07 2.19e-07 -8.6 5.77e-04 - 1.00e+00 1.00e+00h 1
17 1.0000000e+00 7.91e-08 5.48e-08 -8.6 2.88e-04 - 1.00e+00 1.00e+00h 1
18 1.0000000e+00 1.98e-08 1.37e-08 -8.6 1.44e-04 - 1.00e+00 1.00e+00h 1
19 1.0000000e+00 4.94e-09 3.42e-09 -9.0 7.21e-05 - 1.00e+00 1.00e+00h 1
Number of Iterations....: 19
(scaled) (unscaled)
Objective...............: 1.0000000000000000e+00 1.0000000000000000e+00
Dual infeasibility......: 3.4227969388363734e-09 3.4227969388363734e-09
Constraint violation....: 4.9410901192814551e-09 4.9410901192814551e-09
Variable bound violation: 0.0000000000000000e+00 0.0000000000000000e+00
Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00
Overall NLP error.......: 4.9410901192814551e-09 4.9410901192814551e-09
Number of objective function evaluations = 22
Number of objective gradient evaluations = 20
Number of equality constraint evaluations = 22
Number of inequality constraint evaluations = 0
Number of equality constraint Jacobian evaluations = 20
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations = 19
Total seconds in IPOPT = 0.330
EXIT: Optimal Solution Found.
Take away: There is something strange with formulation 3 that is causing the numeric integrator to fail. We can still solve this problem after discretizing.
def create_model_index1_b():
"""
Creates a model to define the Index 1 DAE system.
Output: the model, m
"""
m = pyo.ConcreteModel()
# Declare time
m.t = dae.ContinuousSet(bounds=(0.0, 5))
# Declare parameter - acceleration due to gravity
m.g = pyo.Param(initialize=9.81) # [m/s^2]
# Declare variables indexed over time
m.x = pyo.Var(m.t) # horizontal position
m.y = pyo.Var(m.t) # vertical position
m.u = pyo.Var(m.t) # horizontal velocity
m.v = pyo.Var(m.t) # vertical velocity
m.T = pyo.Var(m.t) # tension
# Declare derivative variables
m.dx = dae.DerivativeVar(m.x) # with respect to t is implied
m.dy = dae.DerivativeVar(m.y)
m.du = dae.DerivativeVar(m.u)
m.dv = dae.DerivativeVar(m.v)
# Declare differential equations
def _dx_eqn(m, t):
return m.dx[t] == m.u[t]
m.dx_eqn = pyo.Constraint(m.t, rule=_dx_eqn)
def _dy_eqn(m, t):
return m.dy[t] == m.v[t]
m.dy_eqn = pyo.Constraint(m.t, rule=_dy_eqn)
def _du_eqn(m, t):
return m.du[t] == -m.T[t] * m.x[t]
m.du_eqn = pyo.Constraint(m.t, rule=_du_eqn)
def _dv_eqn(m, t):
return m.dv[t] == m.g - m.T[t] * m.y[t]
m.dv_eqn = pyo.Constraint(m.t, rule=_dv_eqn)
def _alg_eqn3(m, t):
return (
m.u[t] ** 2
+ m.v[t] ** 2
- m.T[t] * (m.x[t] ** 2 + m.y[t] ** 2)
+ m.g * m.y[t]
== 0
)
m.alg_eqn3 = pyo.Constraint(m.t, rule=_alg_eqn3)
# Specify initial conditions
m.x[0] = 0
m.y[0] = 1
m.u[0] = 1
m.v[0] = 0
m.T[0] = 1 + m.g
return mindex1_b = create_model_index1_b()
# Specify integrator options
int_ops = {"print_stats": True, "abstol": 1e-8, "reltol": 1e-6}
# Solve DAEs
sim = Simulator(index1_b, package="casadi")
tsim, profiles = sim.simulate(
numpoints=100, integrator="idas", integrator_options=int_ops
)
# Plot solution
plot_results(sim, tsim, profiles)FORWARD INTEGRATION:
Number of steps taken by SUNDIALS: 379
Number of calls to the user's f function: 485
Number of calls made to the linear solver setup function: 31
Number of error test failures: 3
Method order used on the last internal step: 5
Method order to be used on the next internal step: 5
Actual value of initial step size: 7.90569e-09
Step size taken on the last internal step: 0.0120796
Step size to be attempted on the next internal step: 0.0120796
Current internal time reached: 5
Number of nonlinear iterations performed: 483
Number of nonlinear convergence failures: 0


Take Away Messages¶
Differential algebraic equations (DAEs) are really powerful modeling tools.
Integrating DAEs requires special care. Make sure your model is index 1.
Often there are many ways to reformulate the DAE model. But the numeric integrator only enforces error tolerances on the equations that are explicitly modeled. If an algebraic constraint must be satisfied to a specific tolerance, include it in the DAE model (as long as it is not high index!).
Important Topics from the Textbook¶
Chapter 8: Dynamic Optimization Introduction (Biegler, 2010)
Chemical engineering examples
Classical (variational) approaches including Hamiltonian and Euler-Lagrange equations

Re-authored as TikZ from Biegler (2010), Figure 8.10, p. 245; source at figures/tikz/dae-optimization-strategies.tex.
Chapter 9: Sequential Methods (Biegler, 2010)
DAE integration
Single Shooting
Multiple Shooting

Chapter 10: Simultaneous Methods (Biegler, 2010)
Gauss quadrature
Orthogonal collocation on finite elements
Examples, benchmarks, and large-scale extensions