Source code for fluidsolve.comp_valve

'''
Hydraulic valve component models.

This module provides valve-related resistance components used to model
controllable pressure losses in hydraulic systems.

Main ideas:

* valves are modeled as resistance components,
* valve behavior is state-dependent (opening/position logic defined by each
  subclass),
* loss is evaluated through valve-specific coefficients and converted to head.

Design conventions:

* mounting direction is port 1 -> port 2,
* runtime flow direction is represented by ``sense``,
* valve classes inherit from ``Comp_Valve`` and specialize ``calcK`` and/or
  related helper methods,
* resulting head loss is returned through ``calcH`` in the same unit
  conventions as other components.

Typical extension pattern:

1. Subclass ``Comp_Valve``.
2. Define how ``state`` maps to hydraulic behavior.
3. Implement ``calcK`` (or equivalent) for that state.
4. Reuse the base ``calcH`` conversion pipeline.

Example::

  valve = Comp_Valve(D=50)
  valve.state = 1.0
  H = valve.calcH(2.0 * u.m**3 / u.h, sense=1)

Valve subclasses in this module are intended to be combined with pumps,
pipes, and fittings inside paths or full network solves.
'''
from typing import Any
# =============================================================================
# IMPORTS
# =============================================================================
import fluids.units as fu
# module own
import fluidsolve.aux_tools as flsa
import fluidsolve.util      as flsu
import fluidsolve.medium    as flsme
import fluidsolve.comp_base as flsb
# units
u        = flsme.unitRegistry
Quantity = flsme.Quantity  # type: ignore[misc]

# =============================================================================
# GENERIC VALVE
# =============================================================================
[docs] class Comp_Valve(flsb.Comp_Base): # pylint: disable=invalid-name ''' Generic valve base class. State meaning is defined by subclasses. Args: D (int | float | Quantity, optional): Valve bore diameter (default in mm). ''' # -------------------------------------------------------------------------- # FIXED PROPERTIES _group : str = 'Resistance' _part : str = 'Valve' _prefix : str = 'V' _conn : dict = {1: [[1,2]]} # -------------------------------------------------------------------------- # INITIALIZE
[docs] def __init__(self, **kwargs: Any) -> Any: args_in = flsa.GetArgs(kwargs) self._D = args_in.getArg( 'D', [ flsa.vFun.default(None), flsa.vFun.istype(int, float, Quantity, need=False), flsa.vFun.tounits(u.mm, need=False), ] ) # base class init super().__init__(**args_in.restArgs())
# -------------------------------------------------------------------------- # PROPERTIES @property def state(self) -> float: ''' Valve state. Returns: int | float: State value. ''' return self._state @state.setter def state(self, value: int | float) -> Any: ''' Set valve state. Args: value (int | float): State value. ''' self._state = value
[docs] def connections(self, state: Any=None) -> Any: '''Return open port connections for the given valve state. Args: state (Any, optional): Valve state override. Uses current state when omitted. Returns: Any: List of open port-pair tuples. ''' if state is None: state = self._state return [tuple(conn) for conn in self._conn.get(state, [])]
# -------------------------------------------------------------------------- # PHYSICS
[docs] def calcK(self, Q: Quantity, sense: int, pin: int=1, pout: int=2) -> float: # pylint: disable=unused-argument ''' Default valve behavior: open. ''' return 0.0
[docs] def calcH(self, Q: Quantity, sense: int=1, pin: int=1, pout: int=2) -> Quantity: lQ = flsa.toUnits(Q, u.m**3/u.h) return flsu.KtoH(self.calcK(lQ, sense, pin, pout), flsu.Qtov(lQ, self._D)) * self._sign
# -------------------------------------------------------------------------- # REPRESENTATION
[docs] def toString(self, detail: int=0) -> str: ''' Return string representation. ''' sdetail = detail // 10 txt = super().toString(sdetail) + '\n' txt += f' State:{self._state:.2f} ' txt += f' D:{self._D:.2f~P} ' return txt
# ============================================================================= # NON-RETURN (CHECK) VALVE # =============================================================================
[docs] class Comp_Valve_NR(Comp_Valve): # pylint: disable=invalid-name ''' Non-return (check) valve. Mounted flow direction: port 1 -> port 2 = allowed flow ''' # -------------------------------------------------------------------------- # FIXED PROPERTIES _part : str = 'Valve_NR' # -------------------------------------------------------------------------- # PHYSICS
[docs] def calcK(self, Q: Quantity, sense: int=1, pin: int=1, pout: int=2) -> float: if (sense > 0 and pin < pout) or (sense < 0 and pin > pout): return 0.0 else: return 1e6
# ============================================================================= # ON / OFF VALVE # =============================================================================
[docs] class Comp_Valve_01(Comp_Valve): # pylint: disable=invalid-name ''' On/off valve. state: 0.0 = closed 1.0 = fully open ''' # -------------------------------------------------------------------------- # FIXED PROPERTIES _part : str = 'Valve_01' # -------------------------------------------------------------------------- # PHYSICS
[docs] def calcK(self, Q: Quantity, sense: int=1, pin: int=1, pout: int=2) -> float: # pylint: disable=unused-argument if self._state <= 0.0: return 1e6 else: return 0.0
# ============================================================================= # THROTLING VALVE # =============================================================================
[docs] class Comp_Valve_Kv(Comp_Valve): # pylint: disable=invalid-name ''' Throttling valve with equal percentage characteristic. state: valve position (0.0 = closed, 1.0 = fully open) Kv at position s = Kvs * (R^s - 1) / (R - 1) ''' # -------------------------------------------------------------------------- # FIXED PROPERTIES _part : str = 'Valve_KV' # -------------------------------------------------------------------------- # INITIALIZE
[docs] def __init__(self, **kwargs: Any) -> Any: # arguments args_in = flsa.GetArgs(kwargs) self._Kvs = args_in.getArg( 'Kvs', [flsa.vFun.istype(int, float)] ) self._R = args_in.getArg( 'R', [flsa.vFun.default(3.0), flsa.vFun.istype(int, float)] ) # base class init super().__init__(**args_in.restArgs())
# -------------------------------------------------------------------------- # PROPERTIES @property def Kvs(self) -> float: ''' Fully-open flow coefficient Kvs. Returns: float: Kvs value (m³/h at 1 bar). ''' return self._Kvs @Kvs.setter def Kvs(self, value: int | float) -> None: ''' Set fully-open flow coefficient. Args: value (int | float): Kvs value. ''' self._Kvs = value @property def R(self) -> float: ''' Valve rangeability (authority ratio). Returns: float: R value (Kvs / Kvmin), default 3.0. ''' return self._R @R.setter def R(self, value: int | float) -> None: ''' Set valve rangeability. Args: value (int | float): R value. ''' self._R = value
[docs] def connections(self, state: Any=None) -> Any: # pylint: disable=unused-argument '''Return always-open hydraulic connectivity for throttling valves.''' return [(1, 2)]
# -------------------------------------------------------------------------- # PHYSICS
[docs] def calcK(self, Q: Quantity, sense: int=1, pin: int=1, pout: int=2) -> float: # pylint: disable=unused-argument state = min(max(0.0, self._state), 1.0) if state <= 0.0: return 1e6 if self._R == 1.0: kv = self._Kvs * state else: kv = self._Kvs * (self._R ** state - 1.0) / (self._R - 1.0) return min(1e6, flsu.KvtoK(kv, self._D))
# ============================================================================= # 3-WAY VALVE # =============================================================================
[docs] class Comp_Valve_3W(Comp_Valve): # pylint: disable=invalid-name ''' 3-way valve. Ports: 1 = common 2 = branch A 3 = branch B state: 1 -> connect 1-2 2 -> connect 1-3 ''' # -------------------------------------------------------------------------- # FIXED PROPERTIES _part : str = 'Valve_3W' _nports : int = 3 _ports : list = [[1,2], [1,3], [2,3]] _conn : dict = {1: [[1,2]], 2: [[1,3]]} # -------------------------------------------------------------------------- # PHYSICS
[docs] def calcK(self, Q: Quantity, sense: int=1, pin: int=1, pout: int=2) -> float: ''' Fixed valve loss, independent of direction. ''' k_values : dict = { 1: {(1,2): 0.7}, 2: {(1,3): 0.7}, } if self._state not in self._conn: raise ValueError(f'Invalid state for 3-way valve: {self._state}') if not (1 <= pin <= self._nports and 1 <= pout <= self._nports): raise ValueError(f'Invalid ports for 3-way valve: pin={pin}, pout={pout}') if pin == pout: raise ValueError(f'pin and pout must be different: pin={pin}, pout={pout}') k_state = k_values[self._state] if (pin, pout) in k_state: return k_state[(pin, pout)] elif (pout, pin) in k_state: return k_state[(pout, pin)] else: return 1e6
# ============================================================================= # DOUBLE SEAT VALVE # =============================================================================
[docs] class Comp_Valve_DS(Comp_Valve): # pylint: disable=invalid-name ''' Double seat valve. Ports: state 1: 1-2 and 3-4 open state 2: 1-2 and 3-4 remain open, plus 1-3 opens ''' # -------------------------------------------------------------------------- # FIXED PROPERTIES _part : str = 'Valve_DS' _nports : int = 4 _ports : list = [[1,2], [1,3], [3,4]] _conn : dict = {1: [[1,2], [3,4]], 2: [[1,2], [1,3], [3,4]]} # -------------------------------------------------------------------------- # PHYSICS
[docs] def calcK(self, Q: Quantity, sense: int=1, pin: int=1, pout: int=2) -> float: ''' Fixed valve loss coefficient. ''' k_values : dict = { 1: {(1,2): 0.7, (3,4): 0.7}, 2: {(1,2): 0.7, (1,3): 0.7, (1,4): 0.7, (2,3): 0.7, (2,4): 0.7, (3,4): 0.7}, } if self._state not in self._conn: raise ValueError(f'Invalid state for 3-way valve: {self._state}') if not (1 <= pin <= self._nports and 1 <= pout <= self._nports): raise ValueError(f'Invalid ports for 3-way valve: pin={pin}, pout={pout}') if pin == pout: raise ValueError(f'pin and pout must be different: pin={pin}, pout={pout}') k_state = k_values[self._state] if (pin, pout) in k_state: return k_state[(pin, pout)] elif (pout, pin) in k_state: return k_state[(pout, pin)] else: return 1e6