Source code for sproclib.unit.tank.interacting.interacting_tanks

"""
Interacting Tanks Model for SPROCLIB

This module provides a model for two interacting tanks in series,
commonly used for studying process dynamics and control.

Author: Thorsten Gressling <gressling@paramus.ai>
License: MIT License
"""

import numpy as np
import logging
from sproclib.unit.base import ProcessModel

logger = logging.getLogger(__name__)


[docs] class InteractingTanks(ProcessModel): """Two interacting tanks in series."""
[docs] def __init__( self, A1: float = 1.0, A2: float = 1.0, C1: float = 1.0, C2: float = 1.0, name: str = "InteractingTanks" ): """ Initialize interacting tanks model. Args: A1, A2: Cross-sectional areas [m²] C1, C2: Discharge coefficients [m²/min] name: Model name """ super().__init__(name) self.A1 = A1 self.A2 = A2 self.C1 = C1 self.C2 = C2 self.parameters = {'A1': A1, 'A2': A2, 'C1': C1, 'C2': C2}
[docs] def dynamics(self, t: float, x: np.ndarray, u: np.ndarray) -> np.ndarray: """ Interacting tanks dynamics: dh1/dt = (q_in - C1*sqrt(h1))/A1 dh2/dt = (C1*sqrt(h1) - C2*sqrt(h2))/A2 Args: t: Time x: [h1, h2] - tank heights u: [q_in] - inlet flow rate Returns: [dh1/dt, dh2/dt] """ h1, h2 = x q_in = u[0] # Ensure heights are non-negative h1 = max(h1, 0.0) h2 = max(h2, 0.0) q12 = self.C1 * np.sqrt(h1) # Flow from tank 1 to tank 2 q_out = self.C2 * np.sqrt(h2) # Outflow from tank 2 dh1dt = (q_in - q12) / self.A1 dh2dt = (q12 - q_out) / self.A2 return np.array([dh1dt, dh2dt])
[docs] def describe(self) -> dict: """ Introspect metadata for documentation and algorithm querying. Returns: dict: Metadata about the InteractingTanks model including algorithms, parameters, equations, and usage information. """ return { 'name': 'InteractingTanks', 'class_name': 'InteractingTanks', 'description': 'Two interacting tanks in series for process dynamics studies', 'algorithm': 'Coupled nonlinear ODEs based on material balance for each tank', 'equations': { 'tank1_dynamics': 'dh1/dt = (q_in - C1*sqrt(h1))/A1', 'tank2_dynamics': 'dh2/dt = (C1*sqrt(h1) - C2*sqrt(h2))/A2', 'flow_12': 'q12 = C1*sqrt(h1)', 'outlet_flow': 'q_out = C2*sqrt(h2)', 'steady_state': 'h1_ss = (q_in/C1)², h2_ss = (q_in/C2)²' }, 'parameters': { 'A1': {'description': 'Tank 1 cross-sectional area', 'units': 'm²', 'typical_range': '[0.1, 10]'}, 'A2': {'description': 'Tank 2 cross-sectional area', 'units': 'm²', 'typical_range': '[0.1, 10]'}, 'C1': {'description': 'Tank 1 discharge coefficient', 'units': 'm²/min', 'typical_range': '[0.01, 1]'}, 'C2': {'description': 'Tank 2 discharge coefficient', 'units': 'm²/min', 'typical_range': '[0.01, 1]'} }, 'state_variables': { 'h1': 'Tank 1 height [m]', 'h2': 'Tank 2 height [m]' }, 'inputs': { 'q_in': 'Inlet flow rate to tank 1 [m³/min]' }, 'outputs': { 'h1': 'Tank 1 height [m]', 'h2': 'Tank 2 height [m]', 'q12': 'Flow from tank 1 to tank 2 [m³/min]', 'q_out': 'Outlet flow from tank 2 [m³/min]' }, 'typical_applications': [ 'Process dynamics studies', 'Control system design', 'Educational demonstrations', 'Multi-tank level control systems' ], 'working_ranges': { 'height': {'min': 0.0, 'max': 10.0, 'units': 'm'}, 'flow_rate': {'min': 0.0, 'max': 5.0, 'units': 'm³/min'}, 'time_constants': {'typical': [2, 200], 'units': 'min'} }, 'assumptions': [ 'Incompressible fluid', 'Constant cross-sectional areas', 'Gravity-driven discharge', 'Turbulent flow through outlets', 'Perfect mixing in each tank' ], 'limitations': [ 'Cannot handle negative heights', 'Assumes steady discharge coefficients', 'Neglects pipe dynamics between tanks' ] }
[docs] def steady_state(self, u: np.ndarray) -> np.ndarray: """ Steady-state heights for interacting tanks. Args: u: [q_in] - inlet flow rate Returns: [h1_ss, h2_ss] - steady-state heights """ q_in = u[0] # At steady state: q_in = C1*sqrt(h1) = C2*sqrt(h2) h2_ss = (q_in / self.C2) ** 2 h1_ss = (q_in / self.C1) ** 2 return np.array([h1_ss, h2_ss])