Process Modeling 101

Click the badge below to try this tutorial interactively in your browser:

Launch Binder

You can also run this tutorial inGoogle Colab. It takes a one-time setup per session: follow theColab instructions.

  • Prepared by:

  • Learning objectives. After this tutorial, you will be able to:

    • Assemble a multi-unit dynamic flowsheet from CSTR, FlatBottomCircularClarifier, and internal plus external recycle streams

    • Attach a biokinetic model (ASM2d) and a per-zone aeration model to dynamic reactors

    • Initialize reactor states from a file with batch_init, run the simulation, and interpret the resulting state-variable trajectories

  • Prerequisites:

  • Covered topics:

      1. Introduction

      1. System setup

      1. System simulation

Setup

Import QSDsan and confirm the installed version.

[1]:
import qsdsan as qs
print(f'This tutorial was made with qsdsan v{qs.__version__}.')
This tutorial was made with qsdsan v1.5.3.

1. Introduction

In Tutorials 10-12, we covered the building blocks of process modeling: how a kinetic model is represented (Tutorial 10 introduced the Process / CompiledProcesses classes and the Petersen / Gujer matrix), how a single dynamic unit is set up and integrated through time (Tutorial 11), and how a full biokinetic model lives inside one reactor (Tutorial 12 uses ADM1 inside an AnaerobicCSTR).

This tutorial serves both as a review and a practical application example. It puts those pieces together at the flowsheet scale: a complete A2O (anaerobic-anoxic-oxic) biological nutrient removal plant, built from five coupled dynamic CSTRs with per-zone aeration, a layered final clarifier, and internal plus external recycle streams. The biokinetic model is Activated Sludge Model No. 2d (ASM2d), the IWA-validated activated-sludge model that resolves carbon, nitrogen, and phosphorus dynamics simultaneously.

If you want to revisit the conceptual basics (components, stoichiometry, kinetics, the Petersen / Gujer matrix), Tutorial 10 and Tutorial 12 §1 cover them in depth.

example_wastewater_treatment_process.png

2. System setup

2.1. Component

Components were defined conceptually in Tutorial 2. Here, instead of constructing each one by one, we use the precompiled ASM2d component set shipped with QSDsan.

component.png

[2]:
# Import packages
import numpy as np, pandas as pd
from qsdsan import unit_operations as su, process_models as pc, WasteStream, System
from qsdsan.utils import time_printer, load_data, get_SRT
[3]:
# Components
cmps = pc.create_asm2d_cmps()           # create components of ASM2d
                                        # you don't need to define each component one by one.
                                        # compiled components for ASM2d are already available.

cmps.show()                             # 18 components of ASM2d + water (X_TSS was excluded due to redundancy.)
CompiledComponents([
    S_O2,   S_N2,  S_NH4, S_NO3,
    S_PO4,  S_F,   S_A,   S_I,
    S_ALK,  X_I,   X_S,   X_H,
    X_PAO,  X_PP,  X_PHA, X_AUT,
    X_MeOH, X_MeP, H2O,
])
  • S_O2: Dissolved oxygen

  • S_N2: Dinitrogen

  • S_NH4: Ammonium plus ammonia nitrogen

  • S_NO3: Nitrate plus nitrite nitrogen (NO3-N + NO2-N)

  • S_PO4: Inorganic soluble phosphorus, primarily orthophosphates

  • S_F: Fermentable, readily biodegradable organic substrates

  • S_A: Fermentation products, considered to be acetate

  • S_I: Inert soluble organic material

  • S_ALK: Alkalinity of the wastewater

  • X_I: Inert particulate organic material

  • X_S: Slowly biodegradable substrates

  • X_H: Heterotrophic organisms

  • X_PAO: Phosphate-accumulating organisms (PAO)

  • X_PP: Poly-phosphate

  • X_PHA: A cell-internal storage product of PAO

  • X_AUT: Nitrifying organisms

  • X_MeOH: Metal-hydroxides

  • X_MeP: Metal-phosphate (MePO4)

[4]:
cmps.S_A.show(chemical_info=True)              # each component stores thermodynamic properties.
Component: S_A (phase_ref='l')
[Names]  CAS: 71-50-1
         InChI: C2H4O2/c1-2(3)4/h1H3...
         InChI_key: QTBSBXVTEAMEQO-U...
         common_name: acetate ion
         iupac_name: ('acetate',)
         pubchemid: 175
         smiles: CC(=O)[O-]
         formula: C2H3O2-
[Groups] Dortmund: <Empty>
         UNIFAC: <Empty>
         PSRK: <Empty>
         NIST: <Empty>
[Data]   MW: 63.998 g/mol
         Tm: None
         Tb: 626.15 K
         Tt: None
         Tc: None
         Pt: None
         Pc: None
         Vc: 0.00016858 m^3/mol
         Hf: 0 J/mol
         S0: 0 J/K/mol
         LHV: 0 J/mol
         HHV: 0 J/mol
         Hfus: 0 J/mol
         Sfus: 0
         omega: None
         dipole: None
         similarity_variable: 0.11856
         iscyclic_aliphatic: 0
         combustion: {'CO2': 2, 'O2'...
Component-specific properties:
[Others] measured_as: COD
         description: Acetate
         particle_size: Soluble
         degradability: Readily
         organic: True
         i_C: 0.37535 g C/g COD
         i_N: 0 g N/g COD
         i_P: 0 g P/g COD
         i_K: 0 g K/g COD
         i_Mg: 0 g Mg/g COD
         i_Ca: 0 g Ca/g COD
         i_mass: 0.9226 g mass/g COD
         i_charge: -0.015626 mol +/g COD
         i_COD: 1 g COD/g COD
         i_NOD: 0 g NOD/g COD
         f_BOD5_COD: 0.717
         f_uBOD_COD: 0.8628
         f_Vmass_Totmass: 1
         chem_MW: 59.044

2.2. WasteStream

WasteStream was introduced in Tutorial 3. Here we create the influent, effluent, and recycle streams as empty WasteStream objects, then set the influent composition by concentration.

waste_stream.png

[5]:
# Parameters (flowrates, temperature)
Q_inf = 18446                               # influent flowrate [m3/d]
Q_was = 385                                 # sludge wastage flowrate [m3/d]
Q_ext = 18446                               # external recycle flowrate [m3/d]
                                            # internal recycle flowrate will be defined later using split ratio.
                                            # effluent flowrate will be calculated as the amount remaining after recycling and wastage.

Temp = 273.15+20                            # temperature [K]
[6]:
# Create influent, effluent, recycle stream
influent = WasteStream('influent', T=Temp)                         # create an empty wastestream with specified temperature
effluent = WasteStream('effluent', T=Temp)

int_recycle = WasteStream('internal_recycle', T=Temp)
ext_recycle = WasteStream('external_recycle', T=Temp)
wastage = WasteStream('wastage', T=Temp)                           # streams between the reactors will be
                                                                   # automatically assigned when we define SanUnit.
[7]:
# Set the influent composition
default_inf_kwargs = {                                             # default influent composition
    'concentrations': {                                            # you can set concentration of each component separately.
      'S_I': 14,
      'X_I': 26.5,
      'S_F': 20.1,
      'S_A': 94.3,
      'X_S': 409.75,
      'S_NH4': 31,
      'S_N2': 0,
      'S_NO3': 0.266,
      'S_PO4': 2.8,
      'X_PP': 0.05,
      'X_PHA': 0.5,
      'X_H': 0.15,
      'X_AUT': 0,
      'X_PAO': 0,
      'S_ALK':7*12,
      },
    'units': ('m3/d', 'mg/L'),                                     # ('input total flowrate', 'input concentrations')
    }

influent.set_flow_by_concentration(Q_inf, **default_inf_kwargs)    # set flowrate and composition of empty influent WasteStream
[8]:
# Wastestream stores bulk properties of the stream, as well as concentration of each component.
influent.show()
WasteStream: influent
phase: 'l', T: 293.15 K, P: 101325 Pa
flow (g/hr): S_NH4  2.38e+04
             S_NO3  204
             S_PO4  2.15e+03
             S_F    1.54e+04
             S_A    7.25e+04
             S_I    1.08e+04
             S_ALK  6.46e+04
             X_I    2.04e+04
             X_S    3.15e+05
             X_H    115
             X_PP   38.4
             X_PHA  384
             H2O    7.66e+08
 WasteStream-specific properties:
  pH         : 7.0
  Alkalinity : 2.5 mmol/L
  COD        : 565.3 mg/L
  BOD        : 320.1 mg/L
  TC         : 271.4 mg/L
  TOC        : 187.4 mg/L
  TN         : 48.9 mg/L
  TP         : 7.4 mg/L
  TK         : 0.1 mg/L
  TSS        : 327.8 mg/L
 Component concentrations (mg/L):
  S_NH4   31.0
  S_NO3   0.3
  S_PO4   2.8
  S_F     20.1
  S_A     94.3
  S_I     14.0
  S_ALK   84.0
  X_I     26.5
  X_S     409.8
  X_H     0.2
  X_PP    0.1
  X_PHA   0.5
  H2O     997254.9
[9]:
influent.get_VSS()                     # you can also retrieve other information, such as VSS, TSS, TDS, etc.
[9]:
324.98437505925017

2.3. Process

Process was covered in depth in Tutorial 10. In this tutorial we use the precompiled ASM2d biokinetic processes together with a DiffusedAeration process attached to each aerated zone to model oxygen mass transfer.

2.3.1. Aeration

aeration_process.png

[10]:
# Parameters (volumes)
V_an = 1000                                 # anoxic zone tank volume [m3/d]
V_ae = 1333                                 # aerated zone tank volume [m3/d]
[11]:
# Aeration model
aer1 = pc.DiffusedAeration('aer1', DO_ID='S_O2', KLa=240, DOsat=8.0, V=V_ae)             # aeration model for Tank 3 & Tank 4
aer2 = pc.DiffusedAeration('aer2', DO_ID='S_O2', KLa=84, DOsat=8.0, V=V_ae)              # aeration model for Tank 5
  • DO_ID: The component ID of dissolved oxygen (DO).

  • KLa: Oxygen mass transfer coefficient.

  • DOsat: Surface DO saturation concentration.

  • V: Reactor volume

[12]:
aer1.show()
Process: aer1
[stoichiometry]      S_O2: 1
[reference]          S_O2
[rate equation]      KLa*(DOsat - S_O2)
[parameters]         KLa: 240
                     DOsat: 8
[dynamic parameters]

2.3.2. ASM2d

asm2d_process.png

[13]:
# ASM2d
asm2d = pc.ASM2d()                       # create ASM2d processes
asm2d.show()                             # 21 processes in ASM2d
ASM2d([aero_hydrolysis, anox_hydrolysis, anae_hydrolysis, hetero_growth_S_F, hetero_growth_S_A, denitri_S_F, denitri_S_A, ferment, hetero_lysis, PAO_storage_PHA, aero_storage_PP, anox_storage_PP, PAO_aero_growth_PHA, PAO_anox_growth, PAO_lysis, PP_lysis, PHA_lysis, auto_aero_growth, auto_lysis, precipitation, redissolution])
[14]:
asm2d.aero_hydrolysis                    # Each process includes stoichiometry, rate equation, and parameters.
Process: aero_hydrolysis
[stoichiometry]      S_NH4: 0.02*f_SI + 0.01
                     S_PO4: 0.01*f_SI
                     S_F: 1.0 - 1.0*f_SI
                     S_I: 1.0*f_SI
                     S_ALK: 0.0113*f_SI + 0.00858
                     X_S: -1.00
[reference]          X_S
[rate equation]      K_h*S_O2*X_S/((K_O2 + S_O2)*...
[parameters]         f_SI: 0
                     Y_H: 0.625
                     f_XI_H: 0.1
                     Y_PAO: 0.625
                     Y_PO4: 0.4
                     Y_PHA: 0.2
                     f_XI_PAO: 0.1
                     Y_A: 0.24
                     f_XI_AUT: 0.1
                     K_h: 3
                     eta_NO3: 0.6
                     eta_fe: 0.4
                     K_O2: 0.2
                     K_NO3: 0.5
                     K_X: 0.1
                     mu_H: 6
                     q_fe: 3
                     eta_NO3_H: 0.8
                     b_H: 0.4
                     K_O2_H: 0.2
                     K_F: 4
                     K_fe: 4
                     K_A_H: 4
                     K_NO3_H: 0.5
                     K_NH4_H: 0.05
                     K_P_H: 0.01
                     K_ALK_H: 1.2
                     q_PHA: 3
                     q_PP: 1.5
                     mu_PAO: 1
                     eta_NO3_PAO: 0.6
                     b_PAO: 0.2
                     b_PP: 0.2
                     b_PHA: 0.2
                     K_O2_PAO: 0.2
                     K_NO3_PAO: 0.5
                     K_A_PAO: 4
                     K_NH4_PAO: 0.05
                     K_PS: 0.2
                     K_P_PAO: 0.01
                     K_ALK_PAO: 1.2
                     K_PP: 0.01
                     K_MAX: 0.34
                     K_IPP: 0.02
                     K_PHA: 0.01
                     mu_AUT: 1
                     b_AUT: 0.15
                     K_O2_AUT: 0.5
                     K_NH4_AUT: 1
                     K_ALK_AUT: 6
                     K_P_AUT: 0.01
                     k_PRE: 1
                     k_RED: 0.6
                     K_ALK_PRE: 6
                     COD_deN: 2.86
[dynamic parameters]
[15]:
# Petersen stoichiometric matrix of ASM2d
pd.set_option('display.max_columns', None)                  # to display all columns

asm2d.stoichiometry
[15]:
S_O2 S_N2 S_NH4 S_NO3 S_PO4 S_F S_A S_I S_ALK X_I X_S X_H X_PAO X_PP X_PHA X_AUT X_MeOH X_MeP H2O
aero_hydrolysis 0 0 0.01 0 0 1 0 0 0.00857 0 -1 0 0 0 0 0 0 0 0
anox_hydrolysis 0 0 0.01 0 0 1 0 0 0.00857 0 -1 0 0 0 0 0 0 0 0
anae_hydrolysis 0 0 0.01 0 0 1 0 0 0.00857 0 -1 0 0 0 0 0 0 0 0
hetero_growth_S_F -0.6 0 -0.022 0 -0.004 -1.6 0 0 -0.0165 0 0 1 0 0 0 0 0 0 0
hetero_growth_S_A -0.6 0 -0.07 0 -0.02 0 -1.6 0 0.252 0 0 1 0 0 0 0 0 0 0
denitri_S_F 0 0.21 -0.022 -0.21 -0.004 -1.6 0 0 0.164 0 0 1 0 0 0 0 0 0 0
denitri_S_A 0 0.21 -0.07 -0.21 -0.02 0 -1.6 0 0.432 0 0 1 0 0 0 0 0 0 0
ferment 0 0 0.03 0 0.01 -1 1 0 -0.168 0 0 0 0 0 0 0 0 0 0
hetero_lysis 0 0 0.032 0 0.01 0 0 0 0.0216 0.1 0.9 -1 0 0 0 0 0 0 0
PAO_storage_PHA 0 0 0 0 0.4 0 -1 0 0.11 0 0 0 0 -0.4 1 0 0 0 0
aero_storage_PP -0.2 0 0 0 -1 0 0 0 0.194 0 0 0 0 1 -0.2 0 0 0 0
anox_storage_PP 0 0.07 -7.78e-18 -0.07 -1 0 0 0 0.254 0 0 0 0 1 -0.2 0 0 0 0
PAO_aero_growth_PHA -0.6 0 -0.07 0 -0.02 0 0 0 -0.0484 0 0 0 1 0 -1.6 0 0 0 0
PAO_anox_growth 0 0.21 -0.07 -0.21 -0.02 0 0 0 0.132 0 0 0 1 0 -1.6 0 0 0 0
PAO_lysis 0 0 0.032 0 0.01 0 0 0 0.0216 0.1 0.9 0 -1 0 0 0 0 0 0
PP_lysis 0 0 0 0 1 0 0 0 -0.194 0 0 0 0 -1 0 0 0 0 0
PHA_lysis 0 0 0 0 0 0 1 0 -0.188 0 0 0 0 0 -1 0 0 0 0
auto_aero_growth -18 0 -4.24 4.17 -0.02 0 0 0 -7.19 0 0 0 0 0 0 1 0 0 0
auto_lysis 0 0 0.032 0 0.01 0 0 0 0.0216 0.1 0.9 0 0 0 0 -1 0 0 0
precipitation 0 0 0 0 -1 0 0 0 0.582 0 0 0 0 0 0 0 -3.45 4.87 0
redissolution 0 0 0 0 1 0 0 0 -0.582 0 0 0 0 0 0 0 3.45 -4.87 0
[16]:
# Rate equations of ASM2d
asm2d.rate_equations
[16]:
rate_equation
aero_hydrolysis 3.0*S_O2*X_S/((0.1 + X_S/X_H)*(...
anox_hydrolysis 0.36*S_NO3*X_S/((0.1 + X_S/X_H)...
anae_hydrolysis 0.12*X_S/((0.1 + X_S/X_H)*(S_NO...
hetero_growth_S_F 6.0*S_ALK*S_F**2*S_NH4*S_O2*S_P...
hetero_growth_S_A 6.0*S_A**2*S_ALK*S_NH4*S_O2*S_P...
denitri_S_F 0.96*S_ALK*S_F**2*S_NH4*S_NO3*S...
denitri_S_A 0.96*S_A**2*S_ALK*S_NH4*S_NO3*S...
ferment 0.3*S_ALK*S_F*X_H/((S_ALK + 1.2...
hetero_lysis 0.4*X_H
PAO_storage_PHA 3.0*S_A*S_ALK*X_PP/((0.01 + X_P...
aero_storage_PP 1.5*S_ALK*S_O2*S_PO4*X_PHA*(0.3...
anox_storage_PP 0.18*S_ALK*S_NO3*S_PO4*X_PHA*(0...
PAO_aero_growth_PHA 1.0*S_ALK*S_NH4*S_O2*S_PO4*X_PH...
PAO_anox_growth 0.12*S_ALK*S_NH4*S_NO3*S_PO4*X_...
PAO_lysis 0.2*S_ALK*X_PAO/(S_ALK + 1.2)
PP_lysis 0.2*S_ALK*X_PP/(S_ALK + 1.2)
PHA_lysis 0.2*S_ALK*X_PHA/(S_ALK + 1.2)
auto_aero_growth 1.0*S_ALK*S_NH4*S_O2*S_PO4*X_AU...
auto_lysis 0.15*X_AUT
precipitation 1.0*S_PO4*X_MeOH
redissolution 0.6*S_ALK*X_MeP/(S_ALK + 6.0)

2.4. SanUnit

Static SanUnits were introduced in Tutorial 4 and Tutorial 5; a single dynamic unit was the focus of Tutorial 11. Here we assemble five dynamic CSTRs, each carrying both an aeration and a suspended-growth biokinetic model, plus a 10-layer FlatBottomCircularClarifier for final settling.

san_unit.png

[17]:
# Anoxic reactors (Tank 1 & Tank 2)
A1 = su.CSTR('A1', ins=[influent, int_recycle, ext_recycle], V_max=V_an,      # As CSTR, 3 input streams, tank volume as V_an
             aeration=None, suspended_growth_model=asm2d)                     # No aeration, biokinetic model as asm2d

A2 = su.CSTR('A2', ins=A1-0, V_max=V_an,                 # ins=A1-0: set influent of A2 as effluent of A1 reactor (to connect A1 with A2)
             aeration=None, suspended_growth_model=asm2d)
  • ins: influents to the reactor.

  • outs: treated effluent from the reactor.

  • V_max: designed reactor volume [m^3].

  • aeration: aeration setting: a target dissolved oxygen concentration [mg O2/L], a Process object representing - aeration, or None for no aeration.

  • suspended_growth_model: the suspended-growth biokinetic model.

See the CSTR documentation for the full signature and default values (you can also run ?su.CSTR in IPython).

[18]:
# Before simulation, outs are empty.
A1.show()
CSTR: A1
ins...
[0] influent
phase: 'l', T: 293.15 K, P: 101325 Pa
flow (g/hr): S_NH4  2.38e+04
                S_NO3  204
                S_PO4  2.15e+03
                S_F    1.54e+04
                S_A    7.25e+04
                S_I    1.08e+04
                S_ALK  6.46e+04
                X_I    2.04e+04
                X_S    3.15e+05
                X_H    115
                X_PP   38.4
                X_PHA  384
                H2O    7.66e+08
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
     COD        : 565.3 mg/L
     BOD        : 320.1 mg/L
     TC         : 271.4 mg/L
     TOC        : 187.4 mg/L
     TN         : 48.9 mg/L
     TP         : 7.4 mg/L
     TK         : 0.1 mg/L
     TSS        : 327.8 mg/L
[1] internal_recycle
phase: 'l', T: 293.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams
[2] external_recycle
phase: 'l', T: 293.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams
outs...
[0] ws1  to  CSTR-A2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams
[19]:
# Aerated reactors (Tank 3, Tank 4, Tank 5)
O1 = su.CSTR('O1', ins=A2-0, V_max=V_ae, aeration=aer1,                          # tank volume as V_ae with aeration model1
             DO_ID='S_O2', suspended_growth_model=asm2d)

O2 = su.CSTR('O2', ins=O1-0, V_max=V_ae, aeration=aer1,
             DO_ID='S_O2', suspended_growth_model=asm2d)

O3 = su.CSTR('O3', ins=O2-0, outs=[int_recycle, 'treated'], split=[0.6, 0.4],    # 60% of effluent to internal recycle, 40% to clarifier
             V_max=V_ae, aeration=aer2,
             DO_ID='S_O2', suspended_growth_model=asm2d)
[20]:
O3.show()
CSTR: O3
ins...
[0] ws7  from  CSTR-O2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams
outs...
[0] internal_recycle  to  CSTR-A1
phase: 'l', T: 293.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams
[1] treated
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams
[21]:
# Clarifier
C1 = su.FlatBottomCircularClarifier('C1', ins=O3-1, outs=[effluent, ext_recycle, wastage],  # O3-1: second effluent of O3, three outs
                                    underflow=Q_ext, wastage=Q_was, surface_area=1500,
                                    height=4, N_layer=10, feed_layer=5,                     # modeled as a 10 layer non-reactive unit
                                    X_threshold=3000, v_max=474, v_max_practical=250,
                                    rh=5.76e-4, rp=2.86e-3, fns=2.28e-3)
  • underflow: designed recycling sludge flowrate (RAS) [m^3/d].

  • wastage: designed wasted sludge flowrate (WAS) [m^3/d].

  • surface_area: surface area of the clarifier [m^2].

  • height: height of the clarifier [m].

  • N_layer: number of layers used to model settling.

  • feed_layer: the feed layer, counted from the top.

  • X_threshold: threshold suspended-solids concentration [g/m^3].

  • v_max: maximum theoretical (Vesilind) settling velocity [m/d].

  • v_max_practical: maximum practical settling velocity [m/d].

  • rh: hindered-zone settling parameter in the double-exponential settling-velocity function [m^3/g].

  • rp: flocculant-zone settling parameter in the double-exponential settling-velocity function [m^3/g].

  • fns: non-settleable fraction of the suspended solids (dimensionless, within [0, 1]).

See the FlatBottomCircularClarifier documentation for the full signature and default values (you can also run ?su.FlatBottomCircularClarifier in IPython).

2.5. System

Static System assembly was introduced in Tutorial 6; single-unit dynamic simulation in Tutorial 11. Here we assemble a multi-unit flowsheet with internal plus external recycle streams, set per-reactor initial conditions through batch_init, and let the System organize unit operations for mass-balance convergence, techno-economic analysis (TEA), and life cycle assessment (LCA).

system.png

2.5.1. Create system

[22]:
# Create system
sys = System('example_system', path=(A1, A2, O1, O2, O3, C1), recycle=(int_recycle, ext_recycle))     # path: the order of reactor
[23]:
# System diagram
sys.diagram()
../_images/tutorials_13_Process_Modeling_101_51_0.svg

2.5.2. Set initial conditions of reactors

[24]:
# Import initial condition excel file
df = load_data('assets/tutorial_13/initial_conditions_asm2d.xlsx', sheet='default')
[25]:
df                                                 # Unlike other reactors, C1 has 3 rows for each of soluble, solids, and tss.
[25]:
S_O2 S_NH4 S_NO3 S_PO4 S_F S_A S_I S_ALK X_I X_S X_H X_PAO X_PP X_PHA X_AUT
A1 0.00213 7.23 10.2 4.45 0.211 0.0265 15.9 67 2.28e+03 84.4 3.78e+03 322 37.2 0.0517 218
A2 0.001 22.4 2.4 4.24 6.68 53.8 14.5 79 0 84.1 207 18.2 4.25 3.59 11.9
O2 2 16.5 4.31 5.48 1.9 2.73 13.7 82.6 611 77.3 1.04e+03 86.4 6.45 11 58
O3 2 10.9 9.31 2.62 0.649 0.163 14.1 74.2 662 59.3 1.14e+03 95.7 9.99 7.24 64
O1 2 0.111 26.1 2.32 0.276 0.00407 18.2 46.1 2.24e+03 61.1 3.79e+03 322 38.4 0.00852 218
C1_s 2 0.114 20.9 0.356 0.307 0.00537 20.1 49.6 NaN NaN NaN NaN NaN NaN NaN
C1_x NaN NaN NaN NaN NaN NaN NaN NaN 2.24e+03 61.1 3.79e+03 322 38.4 0.00852 218
C1_tss 17.8 27.9 44.9 90.5 305 304 306 304 304 5.83e+03 NaN NaN NaN NaN NaN
[26]:
# Create a function to set initial conditions of the reactors
def batch_init(sys, df):
    dct = df.to_dict('index')                                         # convert the DataFrame to a dictionary.
    u = sys.flowsheet.unit                                            # unit registry (A1, A2, O1, O2, O3, C1)

    for k in [u.A1, u.A2, u.O1, u.O2, u.O3]:                          # for A1, A2, O1, O2, O3 reactor,
        k.set_init_conc(**dct[k._ID])                                 # A1.set_init_conc(**dct[k_ID])

    c1s = {k:v for k,v in dct['C1_s'].items() if v>0}                # for clarifier, need to use different methods
    c1x = {k:v for k,v in dct['C1_x'].items() if v>0}
    tss = [v for v in dct['C1_tss'].values() if v>0]
    u.C1.set_init_solubles(**c1s)                                     # set solubles
    u.C1.set_init_sludge_solids(**c1x)                                # set sludge solids
    u.C1.set_init_TSS(tss)                                            # set TSS
[27]:
# Set the initial conditions of the system
batch_init(sys, df)

3. System simulation

3.1. Run simulation

The solve_ivp loop from Tutorial 11 now drives the entire flowsheet rather than a single reactor: at each integrator step, every dynamic unit’s state derivative _dstate is evaluated and the global state vector is advanced. Because the flowsheet has two recycle loops (internal between O3 and A1, external between C1 and A1), sys.simulate repeats the full integration in an outer convergence loop until the recycle stream compositions satisfy sys.set_tolerance(rmol=1e-6). The line after 5 loops in the system display below reports how many of those outer passes were needed.

[28]:
# Simulation settings
sys.set_dynamic_tracker(influent, effluent, A1, A2, O1, O2, O3, C1, wastage)           # what you want to track changes in concentration
sys.set_tolerance(rmol=1e-6)

biomass_IDs = ('X_H', 'X_PAO', 'X_AUT')
[29]:
# Simulation settings
t = 50                          # total time for simulation
t_step = 1                      # times at which to store the computed solution

method = 'BDF'                  # integration method to use
# method = 'RK45'
# method = 'RK23'
# method = 'DOP853'
# method = 'Radau'
# method = 'LSODA'

# https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html
[30]:
# Run simulation, this could take several minutes
sys.simulate(state_reset_hook='reset_cache',
             t_span=(0,t),
             t_eval=np.arange(0, t+t_step, t_step),
             method=method,
            # export_state_to=f'sol_{t}d_{method}.xlsx',               # uncomment to export simulation result as excel file
            )
[31]:
srt = get_SRT(sys, biomass_IDs)
print(f'Estimated SRT assuming at steady state is {round(srt, 2)} days')
Estimated SRT assuming at steady state is 10.02 days

The estimated solids retention time (SRT) of about 10 days matches the typical design point for a BNR plant of this size, confirming that the recycle ratios and wastage flowrate set in §2.2 produce the intended sludge inventory at quasi-steady state.

[32]:
sys.show()
System: example_system
Highest convergence error among components in recycle
streams {C1-1, O3-0} after 5 loops:
- flow rate   1.46e-11 kmol/hr (5.4e-14%)
- temperature 0.00e+00 K (0%)
ins...
[0] influent
    phase: 'l', T: 293.15 K, P: 101325 Pa
    flow (kmol/hr): S_NH4  1.7
                    S_NO3  0.0146
                    S_PO4  0.0347
                    S_F    15.4
                    S_A    1.13
                    S_I    10.8
                    S_ALK  5.38
                    ...    4.29e+04
outs...
[0] effluent
    phase: 'l', T: 293.15 K, P: 101325 Pa
    flow (kmol/hr): S_O2   0.000506
                    S_N2   0.00716
                    S_NH4  1.99
                    S_NO3  9.69e-09
                    S_PO4  0.0469
                    S_F    0.973
                    S_A    0.104
                    ...    4.17e+04
[1] wastage
    phase: 'l', T: 293.15 K, P: 101325 Pa
    flow (kmol/hr): S_O2   1.08e-05
                    S_N2   0.000153
                    S_NH4  0.0424
                    S_NO3  2.07e-10
                    S_PO4  0.000999
                    S_F    0.0207
                    S_A    0.00222
                    ...    1.04e+03

3.2. Check simulation results

Note. The influent composition stays constant throughout this simulation (set once in §2.2 via set_flow_by_concentration), so its state-variable plot is flat. For time-varying inputs (diurnal patterns, step changes, file-driven profiles), see the DynamicInfluent walkthrough in Tutorial 11 §3.2.

[33]:
# Influent
influent.scope.plot_time_series(
    ('S_I','X_I','S_F','S_A','X_S','S_NH4','S_N2','S_NO3','S_PO4',
     'X_PP','X_PHA','X_H','X_AUT','X_PAO','S_ALK')
)
[33]:
(<Figure size 2400x1350 with 1 Axes>,
 <Axes: xlabel='Time [d]', ylabel='Concentration [mg/L]'>)
../_images/tutorials_13_Process_Modeling_101_68_1.png
[34]:
# However, the effluent does change over time, especially in the beginning of the simulation, as the system is not at steady state yet.
effluent.scope.plot_time_series((('S_I','X_I','S_F','S_A','X_S','S_NH4','S_N2','S_NO3','S_PO4','X_PP','X_PHA',
                                 'X_H','X_AUT','X_PAO','S_ALK')))                   # you can plot how each state variable changes over time
[34]:
(<Figure size 2400x1350 with 1 Axes>,
 <Axes: xlabel='Time [d]', ylabel='Concentration [mg/L]'>)
../_images/tutorials_13_Process_Modeling_101_69_1.png
[35]:
effluent.scope.plot_time_series(('S_NH4', 'S_NO3'))  # you can plot how each state variable changes over time
[35]:
(<Figure size 2400x1350 with 1 Axes>,
 <Axes: xlabel='Time [d]', ylabel='Concentration [mg/L]'>)
../_images/tutorials_13_Process_Modeling_101_70_1.png
[36]:
effluent.scope.plot_time_series(('S_O2'))  # you can plot how each state variable changes over time
[36]:
(<Figure size 2400x1350 with 1 Axes>,
 <Axes: xlabel='Time [d]', ylabel='Concentration [mg/L]'>)
../_images/tutorials_13_Process_Modeling_101_71_1.png

What to look for. A few features of these state trajectories are characteristic of a working BNR plant:

  • S_NH4 down, S_NO3 up in the effluent. Ammonium drops while nitrate rises in the aerated zones, the signature of nitrification by X_AUT (autotrophic biomass). The remaining nitrate is either recycled internally to the anoxic zones for denitrification, or leaves with the effluent.

  • Dissolved oxygen tracks the KLa setting. The effluent S_O2 trajectory rises to a value set by the balance between aeration supply (KLa * (DOsat - S_O2) in Tank 5, where the effluent is drawn) and biological demand. This is the same exogenous-input pattern covered in Tutorial 11 §3.1, applied per zone.

  • Quasi-steady state by ~50 days. Because all reactors started near a representative operating point (the initial conditions in §2.5.2), the simulation reaches a near-steady state within the 50-day window. A cold-start run would take several SRT (roughly 30 to 40 days here) to wash in.

From here, you can change the influent composition, the KLa values, the recycle split, or the WAS flowrate, and re-run the simulation to see how each design parameter shifts effluent quality and SRT.


↑ Back to top