PETSc Time-stepping Solver – PID Control and Steam Example
Contents
###############################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
###############################################################################
PETSc Time-stepping Solver – PID Control and Steam Example#
This example provides an overview of the PETSc time-stepping solver utilities in IDAES, which can be used to solve systems of differential algebraic equations (DAEs). PETSc is a solver suite developed primarily by Argonne National Lab (https://petsc.org/release/). IDAES provides a wrapper for PETSc (https://github.com/IDAES/idaes-ext/tree/main/petsc) that uses the AMPL solver interface (https://ampl.com/resources/learn-more/hooking-your-solver-to-ampl/) and utility functions that allow Pyomo and Pyomo.DAE (https://pyomo.readthedocs.io/en/stable/modeling_extensions/dae.html) problems to be solved using PETSc.
This demonstration uses the IDAES PID controller model and a flowsheet arranged like so:
->--|><|------[]------|><|-->-
valve_1 tank valve_2
where the tank pressure is controlled by the opening of valve_1.
Prerequisites#
The PETSc solver is an extra download for IDAES, which can be downloaded using the command idaes get-extensions --extra petsc, if it is not installed already. See the IDAES solver documentation for more information (https://idaes-pse.readthedocs.io/en/stable/reference_guides/core/solvers.html).
You may want to review the “PETSc Time-stepping Solver – Chemical Akzo Nobel Example” notebook first.
Imports#
import numpy as np
import matplotlib.pyplot as plt
import pyomo.environ as pyo
import pyomo.dae as pyodae
import idaes.core.solvers.petsc as petsc # petsc utilities module
import idaes_examples.mod.dae.petsc.pid_steam_tank as pid
from idaes.core.util.math import smooth_max, smooth_min
Model Set Up#
# to see the model code uncomment the line below
# ??pid
m = pid.create_model(
time_set=[0, 12],
nfe=1,
calc_integ=True,
)
Solve#
result = petsc.petsc_dae_by_time_element(
m,
time=m.fs.time,
ts_options={
"--ts_type": "beuler",
"--ts_dt": 0.1,
"--ts_monitor": "", # set initial step to 0.1
"--ts_save_trajectory": 1,
},
)
tj = result.trajectory # trajectroy data
res = result.results # solver status list
Plot Trajectory#
At the initial conditions the valve is fully open. At t=0, the controller is activated and the controller adjusts the opening of valve 1 to keep the tank pressure at the setpoint of 300 kPa.
a = plt.plot(tj.time, tj.get_vec(m.fs.valve_1.valve_opening[12]))
a = plt.ylabel("valve 1 fraction open")
a = plt.xlabel("time (s)")
a = plt.plot(tj.time, tj.get_vec(m.fs.tank.control_volume.properties_out[12].pressure))
a = plt.ylabel("tank pressure (Pa)")
a = plt.xlabel("time (s)")
Model a ramp in inlet pressure#
Next we show how to add an explicit time variable and ramp the inlet pressure from 500 kPa to 600 kPa between 10 and 12 seconds.
# Create a new copy of the model that runs to 24 seconds, and add a constraint.
m = pid.create_model(
time_set=[0, 24],
nfe=1,
calc_integ=True,
)
# time_var will be an explicit time variable we can use in constraints.
m.fs.time_var = pyo.Var(m.fs.time)
# We'll add a constraint to calculate the inlet pressure based on time,
# so we need to unfix pressure.
m.fs.valve_1.control_volume.properties_in[0].pressure.unfix()
m.fs.valve_1.control_volume.properties_in[24].pressure.unfix()
# The solver will directly set the time variable for the DAE solve, but
# solving the initial conditions is just a system of nonlinear equations,
# so we need to fix the initial time.
m.fs.time_var[0].fix(m.fs.time.first())
# We could break up the time domain and solve this in pieces, but creative use
# of min and max will let us create the ramping function we want.
# From 10s to 12s ramp inlet pressure from 500,000 Pa to 600,000 Pa
@m.fs.Constraint(m.fs.time)
def inlet_pressure_eqn(b, t):
return b.valve_1.control_volume.properties_in[t].pressure == smooth_min(
600000, smooth_max(500000, 50000 * (b.time_var[t] - 10) + 500000)
)
# Solve the new problem. Notice the new argument specifying the explicit time variable.
result = petsc.petsc_dae_by_time_element(
m,
time=m.fs.time,
timevar=m.fs.time_var,
ts_options={
"--ts_type": "beuler",
"--ts_dt": 0.1,
"--ts_monitor": "", # set initial step to 0.1
"--ts_save_trajectory": 1,
},
)
tj = result.trajectory # trajectroy data
res = result.results # solver status list
a = plt.plot(
tj.time, tj.get_vec(m.fs.valve_1.control_volume.properties_in[24].pressure)
)
a = plt.ylabel("inlet pressure (Pa)")
a = plt.xlabel("time (s)")
a = plt.plot(tj.time, tj.get_vec(m.fs.valve_1.valve_opening[24]))
a = plt.ylabel("valve 1 fraction open")
a = plt.xlabel("time (s)")
a = plt.plot(tj.time, tj.get_vec(m.fs.tank.control_volume.properties_out[24].pressure))
a = plt.ylabel("tank pressure (Pa)")
a = plt.xlabel("time (s)")
Model a ramp in inlet pressure (again)#
Here we repeat the ramp from the previous simulation in a different way. In this case we do the integration in three parts. 1) Constant pressure at 500 kPa to 10 s 2) ramp from 500 to 600 kPa from 10 to 12 s. 3) Constant pressure at 600 kPa from 12 to 24 s.
# Create a new copy of the model that runs to 24 seconds, and add a constraint.
m = pid.create_model(
time_set=[0, 10, 12, 24],
nfe=3,
calc_integ=True,
)
# time_var will be an explicit time variable we can use in constraints.
m.fs.time_var = pyo.Var(m.fs.time)
# We'll add a constraint to calculate the inlet pressure from 10 to 12s. The rest of the
# time pressure will be fixed. For the time section from 10 to 12s, the constraints are
# defined by time 12; this means the pressure at time 12 should be unfixed and the
# pressure constraint should be active. At all other times, pressure should be fixed and
# the pressure constraint should be deactivated.
m.fs.valve_1.control_volume.properties_in[0].pressure.fix(500000)
m.fs.valve_1.control_volume.properties_in[10].pressure.fix(500000)
m.fs.valve_1.control_volume.properties_in[12].pressure.set_value(600000)
m.fs.valve_1.control_volume.properties_in[12].pressure.unfix()
m.fs.valve_1.control_volume.properties_in[24].pressure.fix(600000)
@m.fs.Constraint(m.fs.time)
def inlet_pressure_eqn(b, t):
return (
b.valve_1.control_volume.properties_in[t].pressure
== 50000 * (b.time_var[t] - 10) + 500000
)
m.fs.inlet_pressure_eqn.deactivate()
m.fs.inlet_pressure_eqn[12].activate()
# Solve the new problem. Notice the argument specifying the explicit time variable.
result = petsc.petsc_dae_by_time_element(
m,
time=m.fs.time,
timevar=m.fs.time_var,
ts_options={
"--ts_type": "beuler",
"--ts_dt": 0.1,
"--ts_monitor": "", # set initial step to 0.1
"--ts_save_trajectory": 1,
},
)
tj = result.trajectory # trajectroy data
res = result.results # solver status list
a = plt.plot(
tj.time, tj.get_vec(m.fs.valve_1.control_volume.properties_in[24].pressure)
)
a = plt.ylabel("inlet pressure (Pa)")
a = plt.xlabel("time (s)")
a = plt.plot(tj.time, tj.get_vec(m.fs.valve_1.valve_opening[24]))
a = plt.ylabel("valve 1 fraction open")
a = plt.xlabel("time (s)")
a = plt.plot(tj.time, tj.get_vec(m.fs.tank.control_volume.properties_out[24].pressure))
a = plt.ylabel("tank pressure (Pa)")
a = plt.xlabel("time (s)")