From 46e76ddc7df7a9331fc03344d6a3cc8d44e08134 Mon Sep 17 00:00:00 2001 From: andr1976 Date: Sun, 22 Feb 2026 21:45:28 +0100 Subject: [PATCH 1/5] Adding exception handling and safety checks for various variables and bounds --- .gitignore | 6 + src/hyddown/__init__.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 201 -> 274 bytes .../__pycache__/exceptions.cpython-312.pyc | Bin 0 -> 12772 bytes .../__pycache__/hdclass.cpython-312.pyc | Bin 99063 -> 100429 bytes .../__pycache__/safety_checks.cpython-312.pyc | Bin 0 -> 13729 bytes src/hyddown/hdclass.py | 75 ++- src/hyddown/safety_checks.py | 464 ++++++++++++++++++ 8 files changed, 540 insertions(+), 7 deletions(-) create mode 100644 src/hyddown/__pycache__/exceptions.cpython-312.pyc create mode 100644 src/hyddown/__pycache__/safety_checks.cpython-312.pyc create mode 100644 src/hyddown/safety_checks.py diff --git a/.gitignore b/.gitignore index 03dd8bc..ad5b350 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,9 @@ BUG_FIX_VALIDATION_SUMMARY.md src/hyddown/__pycache__/hdclass.cpython-312.pyc VALIDATION_REPORT.txt src/hyddown/__pycache__/hdclass.cpython-312.pyc +src/hyddown/exceptions.py +PHASE1_PROGRESS.md +src/hyddown/__pycache__/__init__.cpython-312.pyc +src/hyddown/__pycache__/__init__.cpython-312.pyc +PHASE1_INTEGRATION_GUIDE.md +PHASE1_COMPLETE.md diff --git a/src/hyddown/__init__.py b/src/hyddown/__init__.py index 40bbce3..8dfe927 100644 --- a/src/hyddown/__init__.py +++ b/src/hyddown/__init__.py @@ -4,3 +4,5 @@ from .hdclass import * from .transport import * +from . import exceptions +from . import safety_checks diff --git a/src/hyddown/__pycache__/__init__.cpython-312.pyc b/src/hyddown/__pycache__/__init__.cpython-312.pyc index 00fbbb9041007728df9853d7557092e808a0291d..988b41e2d5cff4c55d058b3ce2c90ed282e49ab0 100644 GIT binary patch delta 177 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 OXWu_Q{a-|9r}DpXlgW|* literal 0 HcmV?d00001 diff --git a/src/hyddown/__pycache__/hdclass.cpython-312.pyc b/src/hyddown/__pycache__/hdclass.cpython-312.pyc index 519b9b2a148bba1fe869e60f40cacda864726eec..367375af0258434b2d6f8f41ca092fcadebd16e4 100644 GIT binary patch delta 11509 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 delta 10047 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 aFj4w;0~ulZvnuj??W{q>a!G~AclX~{Z~sOB diff --git a/src/hyddown/__pycache__/safety_checks.cpython-312.pyc b/src/hyddown/__pycache__/safety_checks.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..95b01fafae9f445082e377f7166ae0b1fd6897f1 GIT binary patch literal 13729 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 0: + sc.check_cfl_stability( + mass_flow_rate=self.mass_rate[i - 1], + vessel_mass=self.mass_fluid[i], + time_step=self.tstep, + characteristic_fraction=0.1 + ) + # ------------------------------------------------------------------------ # THERMODYNAMIC STATE UPDATE # ------------------------------------------------------------------------ @@ -1117,14 +1152,24 @@ def run(self, disable_pbar=True): domain2_w, solver2_w ) else: + # Boundary conditions for unwetted (gas) section + # Use safe_divide to handle fully liquid case (unwetted_area = 0) bc = [ { - "q": -self.Q_inner[i] - / (self.surf_area_inner - wetted_area) + "q": sc.safe_divide( + -self.Q_inner[i], + self.surf_area_inner - wetted_area, + name="q_inner_unwetted", + default=0.0 + ) }, { - "q": self.Q_outer[i] - / (self.surf_area_outer - wetted_area) + "q": sc.safe_divide( + self.Q_outer[i], + self.surf_area_outer - wetted_area, + name="q_outer_unwetted", + default=0.0 + ) }, ] domain2 = tm.Domain(mesh2, [liner, shell], bc) @@ -1135,9 +1180,25 @@ def run(self, disable_pbar=True): "theta": theta, } t_bonded, T_profile2 = tm.solve_ht(domain2, solver2) + # Boundary conditions for wetted (liquid) section + # Use safe_divide to handle fully vapor case (wetted_area = 0) bc_w = [ - {"q": -self.Q_inner_wetted[i] / (wetted_area)}, - {"q": self.Q_outer_wetted[i] / wetted_area}, + { + "q": sc.safe_divide( + -self.Q_inner_wetted[i], + wetted_area, + name="q_inner_wetted", + default=0.0 + ) + }, + { + "q": sc.safe_divide( + self.Q_outer_wetted[i], + wetted_area, + name="q_outer_wetted", + default=0.0 + ) + }, ] domain2_w = tm.Domain(mesh2_w, [liner_w, shell_w], bc_w) domain2_w.set_T(T_profile2_w[-1, :]) @@ -1603,7 +1664,7 @@ def run(self, disable_pbar=True): if input["valve"]["type"] == "relief": idx_max = self.mass_rate.argmax() # Smooth peak value by averaging with neighbors (avoid array index out of bounds) - if 0 < idx_max < len(self.mass_rate) - 1: + if sc.check_bounds_for_smoothing(idx_max, len(self.mass_rate), "relief valve smoothing"): self.mass_rate[idx_max] = ( self.mass_rate[idx_max - 1] + self.mass_rate[idx_max + 1] ) / 2 diff --git a/src/hyddown/safety_checks.py b/src/hyddown/safety_checks.py new file mode 100644 index 0000000..1d993e8 --- /dev/null +++ b/src/hyddown/safety_checks.py @@ -0,0 +1,464 @@ +# HydDown hydrogen/other gas depressurisation +# Copyright (c) 2021-2025 Anders Andreasen +# Published under an MIT license + +""" +Runtime safety checks for HydDown simulations. + +This module provides defensive checks for issues that can ONLY be detected +during simulation execution, not from static input validation. Cerberus +validator handles all input file validation - this module handles runtime +numerical and physical errors. + +Runtime-only checks: +- Negative values from numerical errors (mass, pressure, temperature) +- NaN/Inf detection during calculation +- Array bounds checking +- CFL stability condition (depends on computed flow rates) +- Triple point violations during discharge +- Thermodynamic solver convergence +- Division by zero protection +""" + +import warnings +import numpy as np +from CoolProp.CoolProp import PropsSI +from hyddown.exceptions import ( + NegativeMassError, + TriplePointViolation, + InvalidStateError, + NumericalInstabilityError, + NumericalStabilityWarning, + ThermodynamicConvergenceError, + ConvergenceWarning, +) + + +def check_positive_runtime(value, name, time=None, step=None): + """ + Check that a computed value is positive during simulation. + + This checks for negative values that arise from numerical errors during + time integration, NOT from invalid input (Cerberus handles input validation). + + Parameters + ---------- + value : float + Computed value to check + name : str + Name of the variable (for error messages) + time : float, optional + Simulation time when check occurs [s] + step : int, optional + Time step number when check occurs + + Raises + ------ + NegativeMassError + If computed value is negative or zero + + Examples + -------- + >>> check_positive_runtime(100000, "pressure", time=5.0, step=100) + >>> check_positive_runtime(-50, "mass", time=10.0) # Raises error + """ + if value <= 0: + msg = f"{name} became non-positive during simulation: {value:.3e}" + if time is not None: + msg += f" at t={time:.3f}s" + if step is not None: + msg += f" (step {step})" + msg += ". This indicates numerical instability - try reducing time step." + + raise NegativeMassError( + msg, + variable=name, + value=value, + time=time + ) + + +def check_array_bounds(index, array_length, context=""): + """ + Check array index is within bounds during simulation. + + This prevents IndexError crashes from array access bugs. + Particularly important for relief valve smoothing and similar operations. + + Parameters + ---------- + index : int + Index to check + array_length : int + Length of the array + context : str, optional + Description of operation (for error messages) + + Raises + ------ + IndexError + If index is out of bounds + + Examples + -------- + >>> check_array_bounds(5, 10, "mass_rate smoothing") + >>> check_array_bounds(10, 10) # Raises IndexError + """ + if index < 0 or index >= array_length: + msg = f"Index {index} out of bounds for array of length {array_length}" + if context: + msg += f" in {context}" + msg += f". Valid range is [0, {array_length-1}]" + raise IndexError(msg) + + +def check_nan_inf(value, name, time=None, step=None): + """ + Check for NaN or Inf in computed values. + + NaN/Inf indicate serious numerical problems (division by zero, + overflow, etc.) and must be caught immediately. + + Parameters + ---------- + value : float or np.ndarray + Computed value(s) to check + name : str + Name of the variable + time : float, optional + Simulation time [s] + step : int, optional + Time step number + + Raises + ------ + ValueError + If value contains NaN or Inf + + Examples + -------- + >>> check_nan_inf(5.0, "temperature") + >>> check_nan_inf(np.nan, "pressure", time=10.0) # Raises ValueError + """ + has_nan = np.any(np.isnan(value)) + has_inf = np.any(np.isinf(value)) + + if has_nan or has_inf: + problem = "NaN" if has_nan else "Inf" + msg = f"{name} contains {problem}: {value}" + if time is not None: + msg += f" at t={time:.3f}s" + if step is not None: + msg += f" (step {step})" + msg += ". This indicates numerical instability." + raise ValueError(msg) + + +def check_triple_point_runtime(temperature, pressure, fluid_name, time=None): + """ + Check if state is above triple point during simulation. + + Triple point violations can occur during discharge even if initial + conditions are valid. This is a physical constraint violation. + + Parameters + ---------- + temperature : float + Current temperature [K] + pressure : float + Current pressure [Pa] + fluid_name : str + CoolProp fluid name (e.g., "CO2", "N2") + time : float, optional + Simulation time [s] + + Raises + ------ + TriplePointViolation + If temperature or pressure is below triple point + + Warnings + -------- + Issues warning if within 5% of triple point + + Examples + -------- + >>> check_triple_point_runtime(300, 101325, "N2") # OK + >>> check_triple_point_runtime(200, 101325, "CO2") # Raises error + """ + try: + # Get triple point properties (not all fluids have this data) + T_triple = PropsSI("Ttriple", fluid_name) + P_triple = PropsSI("ptriple", fluid_name) + + # Check temperature + if temperature < T_triple: + msg = (f"{fluid_name} temperature {temperature:.2f}K dropped below " + f"triple point {T_triple:.2f}K during simulation") + if time is not None: + msg += f" at t={time:.3f}s" + msg += ". Solid phase would form - EOS invalid." + + raise TriplePointViolation( + msg, + fluid=fluid_name, + temperature=temperature, + pressure=pressure, + T_triple=T_triple, + P_triple=P_triple + ) + + # Check pressure + if pressure < P_triple: + msg = (f"{fluid_name} pressure {pressure:.2e}Pa dropped below " + f"triple point {P_triple:.2e}Pa during simulation") + if time is not None: + msg += f" at t={time:.3f}s" + msg += ". Solid phase would form - EOS invalid." + + raise TriplePointViolation( + msg, + fluid=fluid_name, + temperature=temperature, + pressure=pressure, + T_triple=T_triple, + P_triple=P_triple + ) + + # Warning if close to triple point (within 5%) + if temperature < T_triple * 1.05: + msg = (f"{fluid_name} temperature {temperature:.2f}K is within 5% " + f"of triple point {T_triple:.2f}K") + if time is not None: + msg += f" at t={time:.3f}s" + msg += ". Results may be inaccurate near phase boundary." + warnings.warn(msg, NumericalStabilityWarning) + + except ValueError: + # Fluid doesn't have triple point data in CoolProp, skip check + pass + + +def check_cfl_stability(mass_flow_rate, vessel_mass, time_step, + characteristic_fraction=0.1): + """ + Check CFL (Courant-Friedrichs-Lewy) stability condition. + + For explicit Euler integration, the time step must be small compared + to the characteristic time scale (mass inventory / mass flow rate). + This check can only be done at runtime with actual computed flow rates. + + Parameters + ---------- + mass_flow_rate : float + Current mass flow rate [kg/s] (absolute value) + vessel_mass : float + Current mass in vessel [kg] + time_step : float + Integration time step [s] + characteristic_fraction : float, default 0.1 + Fraction of characteristic time for stability (typically 0.05 to 0.2) + + Warnings + -------- + Issues NumericalStabilityWarning if time step is too large + + Returns + ------- + float + Recommended maximum time step [s], or None if no limit + + Examples + -------- + >>> check_cfl_stability(0.1, 50.0, 1.0) # May warn + >>> check_cfl_stability(0.001, 50.0, 0.01) # OK + """ + # Avoid division by zero for very small flow rates + if abs(mass_flow_rate) < 1e-10: + return None # No significant flow, any time step is OK + + # Characteristic time: how long to change vessel mass significantly + # tau = mass / dmdt + characteristic_time = vessel_mass / abs(mass_flow_rate) + + # Recommended maximum time step + dt_max_recommended = characteristic_fraction * characteristic_time + + # Warn if current time step is too large + if time_step > dt_max_recommended: + warnings.warn( + f"Time step dt={time_step:.3f}s may be too large for stability. " + f"Recommended: dt < {dt_max_recommended:.3f}s " + f"(based on characteristic time {characteristic_time:.3f}s). " + f"Current mass flow rate = {abs(mass_flow_rate):.3e} kg/s, " + f"vessel mass = {vessel_mass:.3e} kg. " + f"Consider reducing time step if results show oscillations.", + NumericalStabilityWarning, + stacklevel=2 + ) + + return dt_max_recommended + + +def check_optimization_convergence(result, solver_name, state_vars=None, + tolerance=1e-4): + """ + Check if scipy optimization converged successfully. + + This checks convergence of thermodynamic solvers (PHproblem, UDproblem) + which use numerical optimization for multicomponent fluids. Convergence + can only be checked at runtime. + + Parameters + ---------- + result : scipy.optimize.OptimizeResult + Result object from scipy.optimize.minimize or root_scalar + solver_name : str + Name of the solver (for error messages) + state_vars : dict, optional + Input state variables being solved for + tolerance : float, default 1e-4 + Acceptable residual tolerance + + Raises + ------ + ThermodynamicConvergenceError + If optimization did not converge + + Warnings + -------- + Issues ConvergenceWarning if convergence was marginal + + Examples + -------- + >>> from scipy.optimize import minimize + >>> result = minimize(lambda x: x**2, x0=1.0) + >>> check_optimization_convergence(result, "test_solver") + """ + # Check if optimization succeeded + if hasattr(result, 'success'): + if not result.success: + msg = f"{solver_name} failed to converge: {result.message}" + if state_vars: + msg += f"\nInput state: {state_vars}" + raise ThermodynamicConvergenceError( + msg, + solver=solver_name, + state_vars=state_vars, + iterations=getattr(result, 'nit', None) + ) + + # Check converged flag (for root_scalar) + if hasattr(result, 'converged'): + if not result.converged: + msg = f"{solver_name} did not converge" + if state_vars: + msg += f" for state: {state_vars}" + raise ThermodynamicConvergenceError( + msg, + solver=solver_name, + state_vars=state_vars, + iterations=getattr(result, 'iterations', None) + ) + + # Warn if residual is large (marginal convergence) + if hasattr(result, 'fun'): + if isinstance(result.fun, (list, np.ndarray)): + residual = np.max(np.abs(result.fun)) + else: + residual = abs(result.fun) + + if residual > tolerance: + warnings.warn( + f"{solver_name} converged but residual {residual:.2e} " + f"exceeds tolerance {tolerance:.2e}. Results may be inaccurate.", + ConvergenceWarning, + stacklevel=2 + ) + + # Warn if required many iterations + nit = getattr(result, 'nit', getattr(result, 'iterations', None)) + if nit is not None and nit > 100: + warnings.warn( + f"{solver_name} required {nit} iterations. " + "Consider improving initial guess for better performance.", + ConvergenceWarning, + stacklevel=2 + ) + + +def safe_divide(numerator, denominator, name="division", default=0.0): + """ + Safely divide two numbers, returning default if denominator is zero. + + This prevents division by zero errors that occur at runtime (e.g., + when wetted_area = 0 in two-phase calculations). + + Parameters + ---------- + numerator : float + Numerator value + denominator : float + Denominator value + name : str, optional + Description of the division operation (for warnings) + default : float, default 0.0 + Value to return if denominator is zero or very small + + Returns + ------- + float + numerator / denominator, or default if division would fail + + Examples + -------- + >>> safe_divide(10.0, 2.0) + 5.0 + >>> safe_divide(10.0, 0.0, default=0.0) + 0.0 + >>> safe_divide(10.0, 1e-20, name="heat flux") + 0.0 + """ + # Check for zero or near-zero denominator + if abs(denominator) < 1e-15: + # Silently return default - this is expected behavior + # (e.g., wetted_area = 0 is normal for fully vapor state) + return default + + return numerator / denominator + + +def check_bounds_for_smoothing(idx, array_length, context=""): + """ + Check that index and neighbors are within bounds for smoothing operations. + + Relief valve smoothing and similar operations need idx-1, idx, idx+1. + This checks all three indices are valid. + + Parameters + ---------- + idx : int + Center index to smooth + array_length : int + Array length + context : str, optional + Description of operation + + Returns + ------- + bool + True if smoothing is safe (idx-1, idx, idx+1 all valid) + + Examples + -------- + >>> check_bounds_for_smoothing(5, 10) + True + >>> check_bounds_for_smoothing(0, 10) # Can't access idx-1 + False + >>> check_bounds_for_smoothing(9, 10) # Can't access idx+1 + False + """ + # Need idx-1, idx, and idx+1 to be valid + if idx < 1 or idx >= array_length - 1: + return False + return True From 566913272b4e11e3aaa019e9ed911e97f4eb8cf7 Mon Sep 17 00:00:00 2001 From: andr1976 Date: Mon, 23 Feb 2026 08:32:53 +0100 Subject: [PATCH 2/5] Adding safety checks to validation --- .gitignore | 1 + .../__pycache__/validator.cpython-312.pyc | Bin 12607 -> 12905 bytes src/hyddown/validator.py | 136 +++++++++++++++--- 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index ad5b350..2a71cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ src/hyddown/__pycache__/__init__.cpython-312.pyc src/hyddown/__pycache__/__init__.cpython-312.pyc PHASE1_INTEGRATION_GUIDE.md PHASE1_COMPLETE.md +PHASE1_CERBERUS_ENHANCEMENTS.md diff --git a/src/hyddown/__pycache__/validator.cpython-312.pyc b/src/hyddown/__pycache__/validator.cpython-312.pyc index aadc4666c713f659a8c3be0504ad55abadce1cd9..7a2a068b2e1c61dd7490b9ce85adc798173be147 100644 GIT binary patch delta 4352 zcmcgvYit}>6`t9*_m%b7-j{d1Z`)(99Vd;`rtu@RX%Z48G@(tYc{pp|jlI-fyLUHF z60Oy=sVadKai#Ku1)=f>3nU7r{NP977ZL(QTKHits$}>ROsUvSi$JJ6&N(ytvJ0qE z#Yq14%(>@$=iEDUzq$VL+%|Ln+~u-MaNU*4IqR3d=YHQ*nlnk#S0pAg12Y1en3-A1 z?Ut3<$_CZ}UVGWph_o(9%<+oMoO-m2x!E2XuXA{IWYqo5Dn?c^u}&&zgwhJ@A_}xS zS$D%$SWm;rnlm!%qQnB<)LO;(AVu|-UCdsBj*a@_3 z8{1np>+3Vd_CW&D?`IEyWn?L6)&$KSpvTa9Hx8@^p;d?i<_Ccp*LXjBXjfjTo#qfE zKCI92gTg=Z=+b|j!~dt1(g!cgM={UC@c1TREhk}3@i9)7tvBVOjQbz28}hfo7Jsy@ z$kSq`|l;c@eF%_?^W-G?!T)tu{6nQScux{WeBHRHW zj@uI!?M|%PFO(Vs6>DZ`Y2`|mEqv*n*S_)BpT0Vk^SC8x-PFwGP9Om0%vIG$WR;F$ zZaF(!C}vkHwrrlw;_bbWM?-J_{$Ox=8#~|j3`kP$Juq|M%1F{ZaM4w%tvO^@DcT(6V)zYJ@FhQzEP9DZ#hq&vE`YEy(?p2r8km(}Kfv1Jdby|Kz zoOeBKp^!=Onrm-o024n4u>A)kz+J0&+jY1^0Z-%548ms-K7;USgh_;1fOT`rLUf@Q z>+)^{55gXV^8gjoD$f>Ri5hE5QL2G|z8<2QwD~}UORCuBSMtS7E??l?;`{FT#bQfX zlj9lmVmZ~upGBJ^6cCmW@(9ZaD+sFqEoEBTg+{c7#mzl`D!l-4G59NNm?ddVsz#+i zc(XgW8ID&iQY^I@N^bT>YgSWKsXANl!%EdI4Gh0};*G#$&FA#FYaN~4_Gu|uGp(5#G=wau@%)4aXL-!xwXB_a?~GHt)N@h}iQn`@egC*2F*Cl{%t&Z?W)L3!^TOf% z<*6X+Wg*ZLnc3lc9B$Yi5OoJ=jE(#*_!#to+C<X!j$)bCNiSqeSw@ zhtN_0R5K_~32sP)NRCLNP=xpy1RKIULI@!a07(?IL%k>`hvQa$31ewmUqI_B!Zm~| z2sFV9V1c>}OHiMu(LI7NiZF&SjzEPDqJ^pnYC;aQIlh7*d?cjJe&#eUIcNyG+}#m1 zLv3A=AVO;ikpdi< zr6^qdd5rUm(y3CK-kL8IS8A8!r{skDO=qOa4I&*G-pKa7EFan$)KYb}cne#jK&{gaB#v$% zIVC@waK8Z@S2t48y>jnuXl@>wYh3sm!dFXr_ZMO%HgmRqc!$F-bI_?7e?9Of!=WFn zPC7yE9#vqpi_WcZc88;2ciF*uU{8C6KRzz|#F=>Fzi?}OAD;UsySVG!w*Gc9KkUX2 z(K9H3QCyEj#ES`qdRN1BJ~Qm^0POD|tVnO!LURm>t%Nqmz#&)zhW{!w*P~e%)sWT% z_|2d%5qzzgLKqc`$y=^gPfYwR`C^+VF20y*_asELzul7*Kj?qKn7aO!_;a8~oE`WX zoy4Bdm4&op3aXN9@S|uwhCq^o$0bc|NC}SPEh)hHEr5{J@ zEW$a2Pa-@)JN`*DA3->eKw`8@H%KjLmy%@QriD#7jR{8(K88RfC(t^HfU2chS{i|R z5~p%cq4g<%Qe6;;yQx!6bs~L0xvh&G&8gWhky6m3NO}Z;`f9dBl3^pssJL{s;Mt{I z_F_eRU7enO4s%dT3b?YU7uu73m%ufYX9g8r?6%&{1Vy08a{iioLd{zsIt6x_VB8M&`t+;SR`3YR4_@gi_DS z47X<~mbH`rBa2QZ(`~iz=ukbkv6jARkZ*7NbM&Y@j7LA31WBPP)4CK;bhIgR`cNRe zE|$j*$hXBSW8;gIh1#j5;_$k}QLkv7{%)ICUSXG(vQzwJ@F80P-@B?&mgSn=E1Ui@ iCdrYvq~ZIP8H=pkzwVqd;g9WtQC8kPX6=$K#P)C5uk=a) delta 3994 zcmb_eZ%kX)6@Sluei$1YV}r2`#{V$30S8Dz7M7Hz4MmiU1ZdiltZm|P9q2_da1i{hIZc7K=%O-|L^epL+hAKeql`$xbSg^q$0Irek_Q zg&A_XnyX6p4LLnC=9G$+x*)N}`!X|u9(~NLsVrGYvewxjX4Qj7Gui1`R$;cXsIZob zRMj0Aezu*;?98#CqlENKU6fes_qFsen3hyq&O$}#%cWuw&TP2~aH2}vq5AqtU6o2z z=4PI97^Lk0>(9E{SSLJ*dJ8XTx)yXPnR!{Z3oZu+n2`^H`GGc2n=R;rpvU!4c^zr- zRaNWzG)aYoHB0pGE*Dd_&O(0SoRRh93_yi+5f%jplwzzGBt7ebJQYZ@4{Q(L^+N)2 z_%6T-sE+|Pa>UrcfAJdxzaj90_y76Kzi{g;tLE~Blm8FgFVsHV|1Xu}@cK@`!#oKO z6<_19_?vN4J|%eL?>rB5MI%qH-cIo(D=KSm+)5VJ%o7>y?=bTJ^Db;tE>9uRGRC+E~W?1vu3vwWcodk;qs^W`OZK8 z;gsiT@uvB__?h`fvPrBoZFS<<;T`}WtH?$Da^fzqbX)u`Qo6t(W+8*3as#s9QSpjp zN*)ytEmu@B84-=vfd&l9uS(*1>zOR6Uqr=A2v-p15MD+YLHH`b14Fe6ZbL0b<1Gjl zgeHVpfTD7XCo@oArE0Q@={P$ui78qYU}2Gp#`%@>S|XLs@D^cfo?lGYm{mEFsKszG zBfp9A62daV4TMF61%wpB*8yt6)T9fEXbFqoH~%G@1-k(JGtU|%X+tXcCAYWG>M3}G zkNXFAqj}%?LM*<0H{W@#(9^p;k#|3}-=MTx_Zu5qO(kc=xTH#faG|sNBc1AZ?l+j+ zc3A8i*fs9C^4?jZ*pu^}llvW3bauE*rCxL%*Fr`5w&(Jm(MqV^fnC>LDDS;kICf%p zZZDA!Oh5J`f1x7)@niOqNs1lYz6+cklZC-yQD{jtoe3g!#9=DU%3|EUU@r8X*nK7+ z1ll3-$UbfEbE83*-Bda!IvhQLLJx#}l*qd$3i0E+6MHjx|D{3{s?}(tak1`*m}70= zKAd>*o7`=?snRF8-d9Ob5;i%rguSR?lL#Vzzw6qoc?tkZ-97T1>RxgoH@i<-db!d zP{Y|&S5FA4L;h~eJe{oL4~uu)*)JVpWj&Os47rAZ*FjIm2C*WrN8nwwoN>1}=h>8F z;^&?x*<0}7wYIXOw1)610`|~2S5a(0Fd}#n@SX531RsJK!H<9)!P^0#g~BKY5xNmV z2spub4?;6Q%mBjz8cCZQS5Qq;1?L8z)C@>LOV!~*>+`tYz*o?gO1O;@N5~+oB2Y1} zfCSASK?Qt35|1JDBJ@iTpAVr(fsUhuLk*gp6g1#)?7>{3IakRC2QgY#rr*(IC=bf0 zn#@)789{grS#^AjSV(gRH41+dSJwfGj?DaGayfBrF_~Cfy0_(PEmUcICpR^hBzKDOf30-vLvG*;G32b%g-8?3g_q0Pox z;4oy*nc+iLSgYs_4a;p}yE`^@lvC4uZcc17aV#`Myz%ESyMrw8YGqDnp`+10HAmpn ze>dDAn!_dwo>d`SGTZ_FNP z#m(JqH|;kE+SHAy(u9!vZ-SwAfCx^b;oJR(4ejd2 0 + }, + "pressure": { + "required": True, + "type": "number", + "min": 0.001, # Pressure must be > 0 (Pa) + }, "fluid": {"required": True, "type": "string"}, }, }, @@ -71,8 +79,17 @@ def validate_mandatory_ruleset(input): "specified_U", ], }, - "time_step": {"required": True, "type": "number", "min": 0.000001}, - "end_time": {"required": True, "type": "number", "min": 0}, + "time_step": { + "required": True, + "type": "number", + "min": 0.000001, + "max": 3600, # Sanity check: max 1 hour per step + }, + "end_time": { + "required": True, + "type": "number", + "min": 0.001, # Must have positive duration + }, }, }, "vessel": { @@ -80,16 +97,39 @@ def validate_mandatory_ruleset(input): "type": "dict", "allow_unknown": False, "schema": { - "length": {"required": True, "type": "number"}, - "diameter": {"required": True, "type": "number"}, - "thickness": {"required": False, "type": "number", "min": 0.0}, - "heat_capacity": {"required": False, "type": "number", "min": 1}, + "length": { + "required": True, + "type": "number", + "min": 0.001, # Length must be > 0 (m) + }, + "diameter": { + "required": True, + "type": "number", + "min": 0.001, # Diameter must be > 0 (m) + }, + "thickness": { + "required": False, + "type": "number", + "min": 0.0001, # Wall thickness must be > 0 when specified + }, + "heat_capacity": { + "required": False, + "type": "number", + "min": 1, + "max": 10000, # Sanity check [J/(kg·K)] + }, "thermal_conductivity": { "required": False, "type": "number", "min": 0.0001, + "max": 500, # Max for metals ~400 W/(m·K) + }, + "density": { + "required": False, + "type": "number", + "min": 1, + "max": 25000, # Max: osmium ~22,000 kg/m³ }, - "density": {"required": False, "type": "number", "min": 1}, "liner_thickness": {"required": False, "type": "number", "min": 0.0}, "liner_heat_capacity": {"required": False, "type": "number", "min": 1}, "liner_thermal_conductivity": { @@ -143,8 +183,12 @@ def validate_mandatory_ruleset(input): "type": "string", "allowed": ["discharge", "filling"], }, - "diameter": {"type": "number", "min": 0}, - "discharge_coef": {"type": "number", "min": 0}, + "diameter": {"type": "number", "min": 0}, # Allow 0 for no-flow + "discharge_coef": { + "type": "number", + "min": 0.001, # > 0 + "max": 1.0, # Physical limit for ideal orifice + }, "set_pressure": {"type": "number", "min": 0}, "end_pressure": {"type": "number", "min": 0}, "blowdown": {"type": "number", "min": 0, "max": 1}, @@ -180,9 +224,24 @@ def validate_mandatory_ruleset(input): "allowed": ["specified_Q", "specified_h", "specified_U", "s-b"], }, "Q_fix": {"required": False, "type": "number"}, - "U_fix": {"required": False, "type": "number", "min": 0}, - "temp_ambient": {"required": False, "type": "number", "min": 0}, - "h_outer": {"required": False, "type": "number", "min": 0}, + "U_fix": { + "required": False, + "type": "number", + "min": 0.001, # > 0 + "max": 1000, # Sanity check [W/(m²·K)] + }, + "temp_ambient": { + "required": False, + "type": "number", + "min": 0.001, # Kelvin scale > 0 + "max": 2000, # Max for fire scenarios + }, + "h_outer": { + "required": False, + "type": "number", + "min": 0, + "max": 10000, # Sanity check [W/(m²·K)] + }, "h_inner": {"required": False, "type": ["number", "string"]}, "fire": { "required": False, @@ -474,8 +533,18 @@ def heat_transfer_validation(input): ], "schema": { "type": {"type": "string", "allowed": ["specified_h"]}, - "temp_ambient": {"required": True, "type": "number", "min": 0}, - "h_outer": {"required": True, "type": "number", "min": 0}, + "temp_ambient": { + "required": True, + "type": "number", + "min": 0.001, # Kelvin > 0 + "max": 2000, # Max for fire scenarios + }, + "h_outer": { + "required": True, + "type": "number", + "min": 0, + "max": 10000, # Sanity check [W/(m²·K)] + }, "h_inner": {"required": True, "type": ["number", "string"]}, "D_throat": {"required": False, "type": "number", "min": 0}, }, @@ -577,8 +646,18 @@ def heat_transfer_validation(input): "allowed": ["U_fix", "type", "temp_ambient"], "schema": { "type": {"type": "string", "allowed": ["specified_U"]}, - "U_fix": {"required": False, "type": "number", "min": 0.0}, - "temp_ambient": {"required": True, "type": "number", "min": 0}, + "U_fix": { + "required": False, + "type": "number", + "min": 0.001, # > 0 + "max": 1000, # Sanity check [W/(m²·K)] + }, + "temp_ambient": { + "required": True, + "type": "number", + "min": 0.001, # Kelvin > 0 + "max": 2000, # Max for fire scenarios + }, }, }, } @@ -687,7 +766,12 @@ def valve_validation(input): "allowed": ["discharge", "filling"], }, "diameter": {"required": False, "type": "number", "min": 0}, - "discharge_coef": {"required": False, "type": "number", "min": 0}, + "discharge_coef": { + "required": False, + "type": "number", + "min": 0.001, + "max": 1.0, # Physical limit + }, "set_pressure": {"required": True, "type": "number", "min": 0}, "end_pressure": {"type": "number", "min": 0}, "blowdown": {"required": False, "type": "number", "min": 0, "max": 1}, @@ -722,7 +806,12 @@ def valve_validation(input): "allowed": ["discharge", "filling"], }, "diameter": {"required": True, "type": "number", "min": 0}, - "discharge_coef": {"required": True, "type": "number", "min": 0}, + "discharge_coef": { + "required": True, + "type": "number", + "min": 0.001, + "max": 1.0, # Physical limit + }, "set_pressure": {"required": True, "type": "number", "min": 0}, "end_pressure": {"type": "number", "min": 0}, "blowdown": {"required": True, "type": "number", "min": 0, "max": 1}, @@ -757,7 +846,12 @@ def valve_validation(input): "allowed": ["discharge", "filling"], }, "diameter": {"required": True, "type": "number", "min": 0}, - "discharge_coef": {"required": True, "type": "number", "min": 0}, + "discharge_coef": { + "required": True, + "type": "number", + "min": 0.001, + "max": 1.0, # Physical limit + }, "set_pressure": {"type": "number", "min": 0}, "end_pressure": {"type": "number", "min": 0}, "blowdown": {"type": "number", "min": 0, "max": 1}, From 35ca0aae8f229ef74245bd859925bc3d8f6e4974 Mon Sep 17 00:00:00 2001 From: andr1976 Date: Mon, 23 Feb 2026 09:30:16 +0100 Subject: [PATCH 3/5] Improved/refactored validation schema --- .gitignore | 2 + .../__pycache__/validator.cpython-312.pyc | Bin 12905 -> 18329 bytes src/hyddown/validator.py | 1342 ++++++++--------- 3 files changed, 654 insertions(+), 690 deletions(-) diff --git a/.gitignore b/.gitignore index 2a71cc2..cba009f 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,5 @@ src/hyddown/__pycache__/__init__.cpython-312.pyc PHASE1_INTEGRATION_GUIDE.md PHASE1_COMPLETE.md PHASE1_CERBERUS_ENHANCEMENTS.md +VALIDATOR_ANALYSIS.md +PHASE1.5_COMPLETE.md diff --git a/src/hyddown/__pycache__/validator.cpython-312.pyc b/src/hyddown/__pycache__/validator.cpython-312.pyc index 7a2a068b2e1c61dd7490b9ce85adc798173be147..918234e0aaec7599ff200f4fce4fa71dd1b6709d 100644 GIT binary patch literal 18329 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

8d^TPxrW{} z5L~gXwx=FHi40v8%RPgc7RL_!rCdk%itW?lbj``FV|+7+s(-A?u^G(~MOh{Tsq)=- z{d?B^d$*jHa>v$H(2&a5s_D?`{yVNycw5Tn*{tL|WsCfKws&l4w?M$h zmd<>1;r4~)(R9~Ssivn_%kQ{OZ2DE;GR{+*z58!qJKD~VAlM5eTSc#|i4yKw; zr_Ox;jw`h3Q=yAFPx)fQd+qPEr`?SNePZdAkEU-=uh`Swhf_^QQhhJpab3jbVuZLk zPxWHxy|H)3((YD*9$4=EWdFzeR|e91o=G*G{Pg8Ju9vVS2>2^A=Wa^&ez5=j{Yx+0 zaqZo37VD`=;H~UMvYoj4K91zffMyvlnq!)jwrHZr;{q>z0SC32^%$wVX2_YsB&g^@ zQm_S%DSi$nFUgB+%T~}bQ)>>UIE!hXg~{kZN-|tbmx+>XR~EM!W;HT>@{B{%Puw4B z??Rth&6%~QGB{u@f+6Y^?W1Zo4=?)IM^|Ocxc7;E7~Fc@3j?+S2CoY+ez6iJYZc7Z z>RUL*i;UWs0(#Z#nS(IA>qZT~ZhFOf(FiKMVvR;G(Lyu{_~ldbyP%|j&D`y6>|zIV-0sSc4zpa|&e{LQOw#juaE4jQFSQkf*xO#lhgtY& zP4h|WLDAooi|L_O_pfMdGkb{HjqhQ}R8xo<**R0_pW_DXoUBEwx_2qOFO^YooC*O& zzw`%GeHR7%H_S+5<{C5H*fYpnlMYgGgo>Pv|1YRX(pEKY$!wHCpkg?GWPtE{j z$o`ao{uvcH`p9I2E0~R8=Hu^Ehf%d6bC~`_?PVP~1CtC`)wcW|)jlC04HpU9*`k8g z7uvt(1F$VA(^E|c9;6ycYUy86kuw6Vsy})ay^3`VU!WmLPMV;WKbtzN`kuAKZly%C z7wL(5AX)m?7?BIi>+Y&{HWuBCG+fs&(+r){+vp=0T2s=gP^V`*DW zrhWJF(Nx=a(4^d|4X1Bme9hT(&tz@&q+E^A5u5_nsp4n(_&j{_A`sPf169%;S z0oW2?T>^U1CHP@5b>Sx9wb4- zXmPK(TQ*=gI#Mm)1+5#6UB4QonVP2L`BZghj@%4Pvx;=>VM)&!0tV-I|^HmS$_9Gj5-PLiO&lLM^R=r)d*tcHXc-``OQQ1aOU9#bW z_V?SEaf^ zFWHtGK575BePw@oH#ThF>hvA&vsz9%tij^8K@X%#V$gFwByoE6R@SPen@AQ;68+i^{KQjqqRyMp0MQ^eYuz>{C(fz5ky=@?nC;r@$$EO1RnyW=43#T^LZ-Qbe7BWT8X?Y8I1Z7W&vsO~I1o>T|4yq~)z!e14e8f&s;_}%LcE4{KhvO0cg|Yb+Y0T{ zzrhV?UZz}LL48NPVpjD^obHo4VH!eOMQwZ9fS2LTR}TLUQ0K9e)xnN71D#71+mTjK=UxWNdvoF%Lw(+aghS!J0NzeC4Gx=&p5J-HvBa-$Ll-*xO@iXWc`OmFhPo`KP(7 z%d^YT)rM5p(L1ibhwv|X&>8>!dsJ?6IeyKVxzoHmrZ!Z9VhPOyztpOlxZOI zGrXP4nbtXs7(@0By9#>A*m7HJp8ElJm76xc$X(?F z#@YYDxaZI)!hWO356Mqso1{pdpxdYHe_UYS#dNVxlmxh@hOhj_5{rJlW^4bsfL;12 zCPJbO%uF1X^umJnEPlt5>T-#8u;9pBhfICd0|!Qkh%)(6H3Ld@|B(;y9}+uEp)${X zC2;np7pqfJ{nSey`XURyyXPMuLPj`zf_;l9~T)um)Pw|%fhYfpq-@OUm%3JQ0 z9^?7)`{xYZe8;^Q-^@Gi4e}Md=ia_X6zBLpBOsr(fNOpCE}Oe}-~I3NExeZ{di;OJ CLEGd2 literal 12905 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 0 when specified + }, +} + + +def create_liner_properties(): + """Create liner property schema (same constraints as base materials).""" + return { + "liner_thickness": {"required": False, "type": "number", "min": 0.0}, + "liner_heat_capacity": {"required": False, "type": "number", "min": 1}, + "liner_thermal_conductivity": { + "required": False, + "type": "number", + "min": 0.0001, + }, + "liner_density": {"required": False, "type": "number", "min": 1}, + } + + +# Time series validation schema (for validation data) +TIME_SERIES_SCHEMA = { + "required": False, + "type": "dict", + "contains": ["time", "data"], + "schema": { + "data": { + "required": False, + "type": "list", + "schema": {"type": "number"}, + }, + "time": { + "required": False, + "type": "list", + "schema": {"type": "number"}, + }, + }, +} + + +def create_validation_data_schema(): + """Create schema for validation data section (eliminates massive duplication).""" + # Pressure validation data + pressure_schema = { + "type": "dict", + "required": False, + "contains": ["time", "pres"], + "schema": { + "pres": { + "required": False, + "type": "list", + "schema": {"type": "number"}, + }, + "time": { + "required": False, + "type": "list", + "schema": {"type": "number"}, + }, + }, + } + + # Temperature data schema (same for all temperature types) + temp_schema = { + "required": False, + "type": "dict", + "contains": ["time", "temp"], + "schema": { + "temp": { + "required": False, + "type": "list", + "schema": {"type": "number"}, + }, + "time": { + "required": False, + "type": "list", + "schema": {"type": "number"}, + }, + }, + } + + # All temperature types use the same schema + temp_types = [ + "gas_high", + "gas_low", + "gas_mean", + "wall_high", + "wall_mean", + "wall_low", + "wall_inner", + "wall_outer", + ] + + temperature_schema = { + "type": "dict", + "required": False, + "allowed": temp_types, + "schema": {temp_type: temp_schema.copy() for temp_type in temp_types}, + } + + return { + "pressure": pressure_schema, + "temperature": temperature_schema, + } + + +def create_vessel_schema(required_fields=None): + """ + Create vessel schema with specified required fields. + + Parameters + ---------- + required_fields : list, optional + Fields that should be required. If None, all are optional. + + Returns + ------- + dict + Vessel schema with specified requirements + """ + required_fields = required_fields or [] + + schema = { + "length": { + "type": "number", + "min": 0.001, # Length must be > 0 (m) + }, + "diameter": { + "type": "number", + "min": 0.001, # Diameter must be > 0 (m) + }, + "orientation": { + "type": "string", + "allowed": ["vertical", "horizontal"], + }, + "type": { + "type": "string", + "allowed": ["Flat-end", "ASME F&D", "DIN", "Hemispherical"], + }, + "liquid_level": { + "type": "number", + "min": 0, + }, + } + + # Add material properties + schema.update(MATERIAL_PROPERTIES.copy()) + schema.update(create_liner_properties()) + + # Mark specified fields as required + for field in required_fields: + if field in schema: + schema[field]["required"] = True + + # Set all non-required fields + for field in schema: + if "required" not in schema[field]: + schema[field]["required"] = False + + return schema + + +def create_top_level_schema(): + """Create the common top-level section schema used across all validations.""" + return { + "initial": {"required": True}, + "calculation": {"required": True}, + "validation": {"required": False}, + "rupture": {"required": False}, + } + + +def create_valve_schema(valve_type): + """ + Create valve schema for specific valve type. + + Parameters + ---------- + valve_type : str + Valve type: 'relief', 'psv', 'orifice', 'controlvalve', 'mdot' + + Returns + ------- + dict + Valve schema with type-specific requirements + """ + # Base valve schema (all possible fields) + base_schema = { + "type": { + "required": True, + "type": "string", + "allowed": ["orifice", "psv", "controlvalve", "mdot", "relief"], + }, + "flow": { + "required": True, + "type": "string", + "allowed": ["discharge", "filling"], + }, + "diameter": {"type": "number", "min": 0}, + "discharge_coef": { + "type": "number", + "min": 0.001, + "max": 1.0, # Physical limit + }, + "set_pressure": {"type": "number", "min": 0}, + "end_pressure": {"type": "number", "min": 0}, + "blowdown": {"type": "number", "min": 0, "max": 1}, + "back_pressure": {"type": "number", "min": 0}, + "Cv": {"type": "number", "min": 0}, + "mdot": {"type": ["number", "list"]}, + "time": {"type": ["number", "list"]}, + "time_constant": {"type": "number", "min": 0}, + "characteristic": { + "type": "string", + "allowed": ["linear", "eq", "fast"], + }, + } + + # Type-specific requirements + if valve_type == "relief": + base_schema["set_pressure"]["required"] = True + base_schema["back_pressure"]["required"] = True + + elif valve_type == "psv": + base_schema["diameter"]["required"] = True + base_schema["discharge_coef"]["required"] = True + base_schema["set_pressure"]["required"] = True + base_schema["blowdown"]["required"] = True + base_schema["back_pressure"]["required"] = True + + elif valve_type == "orifice": + base_schema["diameter"]["required"] = True + base_schema["discharge_coef"]["required"] = True + base_schema["back_pressure"]["required"] = True + + elif valve_type == "controlvalve": + base_schema["Cv"]["required"] = True + base_schema["back_pressure"]["required"] = True + + elif valve_type == "mdot": + base_schema["mdot"]["required"] = True + base_schema["back_pressure"]["required"] = True + + return base_schema + + +def format_cerberus_errors(errors, section="input"): + """ + Convert Cerberus error dict to user-friendly ConfigurationError. + + Parameters + ---------- + errors : dict + Cerberus validation errors + section : str + Section name (vessel, valve, heat_transfer, etc.) + + Raises + ------ + ConfigurationError + With formatted, actionable error message + """ + error_messages = [] + + def flatten_errors(err_dict, prefix=""): + """Recursively flatten nested error dictionaries.""" + for field, error_list in err_dict.items(): + if isinstance(error_list, list): + for error in error_list: + if isinstance(error, dict): + # Nested errors - recurse + flatten_errors(error, f"{prefix}{field}.") + else: + # Leaf error - format it + error_messages.append(f" {prefix}{field}: {error}") + else: + error_messages.append(f" {prefix}{field}: {error_list}") + + flatten_errors(errors) + + if not error_messages: + error_messages = [f" Unknown validation error: {errors}"] + + message = f"Invalid {section} configuration:\n" + "\n".join(error_messages) + + # Raise specific exception based on section + if section == "vessel": + raise VesselConfigurationError(message) + elif section == "valve": + raise ValveConfigurationError(message) + elif section == "heat_transfer": + raise HeatTransferConfigurationError(message) + else: + raise ConfigurationError(message) + + +# ============================================================================ +# CROSS-FIELD VALIDATION (Physical Constraints) +# ============================================================================ + + +def validate_relief_valve_physics(input): + """ + Validate physical constraints for relief valves. + + Parameters + ---------- + input : dict + Structure holding input + + Raises + ------ + ValveConfigurationError + If physical constraints violated + """ + if input["valve"]["type"] not in ["relief", "psv"]: + return # Not a relief valve + + valve = input["valve"] + set_p = valve.get("set_pressure") + back_p = valve.get("back_pressure") + + # Check set pressure > back pressure + if set_p is not None and back_p is not None: + if set_p <= back_p: + raise ValveConfigurationError( + f"Relief valve set_pressure ({set_p} Pa) must be greater than " + f"back_pressure ({back_p} Pa).\n" + f"The valve cannot open if set pressure is at or below back pressure." + ) + + # Check blowdown is reasonable + blowdown = valve.get("blowdown") + if blowdown is not None and blowdown >= 1.0: + raise ValveConfigurationError( + f"Relief valve blowdown ({blowdown}) must be < 1.0.\n" + f"Typical values are 0.1-0.2 (10%-20%)." + ) + + +def validate_composite_vessel(input): + """ + Validate composite vessel liner constraints. + + Parameters + ---------- + input : dict + Structure holding input + + Raises + ------ + VesselConfigurationError + If liner thickness >= wall thickness + """ + vessel = input.get("vessel", {}) + thickness = vessel.get("thickness") + liner_thickness = vessel.get("liner_thickness") + + if thickness and liner_thickness: + if liner_thickness >= thickness: + raise VesselConfigurationError( + f"liner_thickness ({liner_thickness} m) must be less than " + f"thickness ({thickness} m).\n" + f"The liner cannot be thicker than the entire wall." + ) + + +def validate_time_step_sanity(input): + """ + Validate time step is reasonable for simulation duration. + + Parameters + ---------- + input : dict + Structure holding input + + Raises + ------ + ConfigurationError + If time step is too large relative to end time + """ + calc = input.get("calculation", {}) + time_step = calc.get("time_step") + end_time = calc.get("end_time") + + if time_step and end_time: + if time_step > end_time: + raise ConfigurationError( + f"time_step ({time_step} s) is greater than end_time ({end_time} s).\n" + f"The simulation would complete in a single step." + ) + + # Warn if very few steps + num_steps = end_time / time_step + if num_steps < 10: + import warnings + + warnings.warn( + f"Only {num_steps:.1f} time steps will be simulated. " + f"Consider reducing time_step for better resolution.", + UserWarning, + ) + + +# ============================================================================ +# VALIDATION FUNCTIONS +# ============================================================================ def validate_mandatory_ruleset(input): @@ -96,64 +536,7 @@ def validate_mandatory_ruleset(input): "required": True, "type": "dict", "allow_unknown": False, - "schema": { - "length": { - "required": True, - "type": "number", - "min": 0.001, # Length must be > 0 (m) - }, - "diameter": { - "required": True, - "type": "number", - "min": 0.001, # Diameter must be > 0 (m) - }, - "thickness": { - "required": False, - "type": "number", - "min": 0.0001, # Wall thickness must be > 0 when specified - }, - "heat_capacity": { - "required": False, - "type": "number", - "min": 1, - "max": 10000, # Sanity check [J/(kg·K)] - }, - "thermal_conductivity": { - "required": False, - "type": "number", - "min": 0.0001, - "max": 500, # Max for metals ~400 W/(m·K) - }, - "density": { - "required": False, - "type": "number", - "min": 1, - "max": 25000, # Max: osmium ~22,000 kg/m³ - }, - "liner_thickness": {"required": False, "type": "number", "min": 0.0}, - "liner_heat_capacity": {"required": False, "type": "number", "min": 1}, - "liner_thermal_conductivity": { - "required": False, - "type": "number", - "min": 0.0001, - }, - "liner_density": {"required": False, "type": "number", "min": 1}, - "orientation": { - "required": False, - "type": "string", - "allowed": ["vertical", "horizontal"], - }, - "type": { - "required": False, - "type": "string", - "allowed": {"Flat-end", "ASME F&D", "DIN", "Hemispherical"}, - }, - "liquid_level": { - "required": False, - "type": "number", - "min": 0, - }, - }, + "schema": create_vessel_schema(required_fields=["length", "diameter"]), }, "rupture": { "required": False, @@ -261,717 +644,296 @@ def validate_mandatory_ruleset(input): "type": "dict", "allow_unknown": False, "allowed": ["pressure", "temperature"], - "schema": { - "pressure": { - "type": "dict", - "required": False, - "contains": ["time", "pres"], - "schema": { - "pres": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "temperature": { - "type": "dict", - "required": False, - "allowed": [ - "wall_high", - "wall_low", - "wall_mean", - "wall_inner", - "wall_outer", - "gas_high", - "gas_low", - "gas_mean", - ], - "schema": { - "gas_high": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "gas_low": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "gas_mean": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "wall_high": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "wall_mean": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "wall_low": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "wall_inner": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - "wall_outer": { - "required": False, - "type": "dict", - "contains": ["time", "temp"], - "schema": { - "temp": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - "time": { - "required": False, - "type": "list", - "schema": {"type": "number"}, - }, - }, - }, - }, - }, - }, + "schema": create_validation_data_schema(), }, } v = Validator(schema_general) retval = v.validate(input) - if v.errors: - print(v.errors) + + if not retval: + # Raise specific exceptions instead of printing + if "vessel" in v.errors: + format_cerberus_errors({"vessel": v.errors["vessel"]}, "vessel") + elif "valve" in v.errors: + format_cerberus_errors({"valve": v.errors["valve"]}, "valve") + elif "heat_transfer" in v.errors: + format_cerberus_errors({"heat_transfer": v.errors["heat_transfer"]}, "heat_transfer") + elif "initial" in v.errors: + format_cerberus_errors({"initial": v.errors["initial"]}, "initial conditions") + elif "calculation" in v.errors: + format_cerberus_errors({"calculation": v.errors["calculation"]}, "calculation") + else: + # Generic configuration error for other cases + format_cerberus_errors(v.errors, "input") return retval def heat_transfer_validation(input): """ - Validate input['heat_transfer'] deeper than cerberus + Validate input['heat_transfer'] deeper than cerberus. Parameters ---------- input : dict Structure holding input - Return ---------- - : bool + bool True for success, False for failure """ retval = True if input["calculation"]["type"] == "energybalance": - if input["heat_transfer"]["type"] == "specified_h": - schema_heattransfer = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "rupture": {"required": False}, - "valve": {"required": True}, - "vessel": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "length": {"required": True, "type": "number"}, - "diameter": {"required": True, "type": "number"}, - "thickness": {"required": True, "type": "number", "min": 0.0}, - "heat_capacity": {"required": True, "type": "number", "min": 1}, - "thermal_conductivity": { - "required": False, - "type": "number", - "min": 0.0001, - }, - "density": {"required": True, "type": "number", "min": 1}, - "liner_thickness": { - "required": False, - "type": "number", - "min": 0.0, - }, - "liner_heat_capacity": { - "required": False, - "type": "number", - "min": 1, - }, - "liner_thermal_conductivity": { - "required": False, - "type": "number", - "min": 0.0001, - }, - "liner_density": { - "required": False, - "type": "number", - "min": 1, - }, - "orientation": { - "required": True, - "type": "string", - "allowed": ["vertical", "horizontal"], - }, - "type": { - "required": False, - "type": "string", - "allowed": ["Flat-end", "DIN", "ASME F&D", "Hemispherical"], - }, - "liquid_level": { - "required": False, - "type": "number", - "min": 0, - }, + ht_type = input["heat_transfer"]["type"] + + # Build schema using factory functions instead of massive duplication + base_schema = create_top_level_schema() + base_schema["vessel"] = {"required": True} + base_schema["valve"] = {"required": True} + base_schema["heat_transfer"] = {"required": True} + + if ht_type == "specified_h": + # Vessel requirements for specified_h + required_vessel_fields = [ + "length", + "diameter", + "thickness", + "heat_capacity", + "density", + "orientation", + ] + + schema_heattransfer = base_schema.copy() + schema_heattransfer["vessel"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "schema": create_vessel_schema(required_fields=required_vessel_fields), + } + schema_heattransfer["heat_transfer"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "allowed": ["h_inner", "h_outer", "temp_ambient", "type", "D_throat"], + "schema": { + "type": {"type": "string", "allowed": ["specified_h"]}, + "temp_ambient": { + "required": True, + "type": "number", + "min": 0.001, + "max": 2000, }, - }, - "heat_transfer": { - "required": True, - "type": "dict", - "allow_unknown": False, - "allowed": [ - "h_inner", - "h_outer", - "temp_ambient", - "type", - "D_throat", - ], - "schema": { - "type": {"type": "string", "allowed": ["specified_h"]}, - "temp_ambient": { - "required": True, - "type": "number", - "min": 0.001, # Kelvin > 0 - "max": 2000, # Max for fire scenarios - }, - "h_outer": { - "required": True, - "type": "number", - "min": 0, - "max": 10000, # Sanity check [W/(m²·K)] - }, - "h_inner": {"required": True, "type": ["number", "string"]}, - "D_throat": {"required": False, "type": "number", "min": 0}, + "h_outer": { + "required": True, + "type": "number", + "min": 0, + "max": 10000, }, + "h_inner": {"required": True, "type": ["number", "string"]}, + "D_throat": {"required": False, "type": "number", "min": 0}, }, } - elif input["heat_transfer"]["type"] == "specified_Q": - schema_heattransfer = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "rupture": {"required": False}, - "valve": {"required": True}, - "vessel": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "length": {"required": True, "type": "number"}, - "diameter": {"required": True, "type": "number"}, - "thickness": {"required": False, "type": "number", "min": 0.0}, - "heat_capacity": { - "required": False, - "type": "number", - "min": 1, - }, - "density": {"required": False, "type": "number", "min": 1}, - "orientation": { - "required": False, - "type": "string", - "allowed": ["vertical", "horizontal"], - }, - "type": { - "required": False, - "type": "string", - "allowed": ["Flat-end", "DIN", "ASME F&D", "Hemispherical"], - }, - "liquid_level": { - "required": False, - "type": "number", - "min": 0, - }, - }, - }, - "heat_transfer": { - "required": True, - "type": "dict", - "allow_unknown": False, - "allowed": ["Q_fix", "type"], - "schema": { - "type": {"type": "string", "allowed": ["specified_Q"]}, - "Q_fix": {"required": False, "type": "number"}, - }, + elif ht_type == "specified_Q": + # Vessel requirements for specified_Q (minimal) + required_vessel_fields = ["length", "diameter"] + + schema_heattransfer = base_schema.copy() + schema_heattransfer["vessel"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "schema": create_vessel_schema(required_fields=required_vessel_fields), + } + schema_heattransfer["heat_transfer"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "allowed": ["Q_fix", "type"], + "schema": { + "type": {"type": "string", "allowed": ["specified_Q"]}, + "Q_fix": {"required": False, "type": "number"}, }, } - elif input["heat_transfer"]["type"] == "specified_U": - schema_heattransfer = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "valve": {"required": True}, - "rupture": {"required": False}, - "vessel": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "length": {"required": True, "type": "number"}, - "diameter": {"required": True, "type": "number"}, - "thickness": {"required": False, "type": "number", "min": 0.0}, - "heat_capacity": { - "required": False, - "type": "number", - "min": 1, - }, - "density": {"required": False, "type": "number", "min": 1}, - "orientation": { - "required": False, - "type": "string", - "allowed": ["vertical", "horizontal"], - }, - "type": { - "required": False, - "type": "string", - "allowed": ["Flat-end", "DIN", "ASME F&D", "Hemispherical"], - }, - "liquid_level": { - "required": False, - "type": "number", - "min": 0, - }, + elif ht_type == "specified_U": + # Vessel requirements for specified_U (minimal) + required_vessel_fields = ["length", "diameter"] + + schema_heattransfer = base_schema.copy() + schema_heattransfer["vessel"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "schema": create_vessel_schema(required_fields=required_vessel_fields), + } + schema_heattransfer["heat_transfer"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "allowed": ["U_fix", "type", "temp_ambient"], + "schema": { + "type": {"type": "string", "allowed": ["specified_U"]}, + "U_fix": { + "required": False, + "type": "number", + "min": 0.001, + "max": 1000, }, - }, - "heat_transfer": { - "required": True, - "type": "dict", - "allow_unknown": False, - "allowed": ["U_fix", "type", "temp_ambient"], - "schema": { - "type": {"type": "string", "allowed": ["specified_U"]}, - "U_fix": { - "required": False, - "type": "number", - "min": 0.001, # > 0 - "max": 1000, # Sanity check [W/(m²·K)] - }, - "temp_ambient": { - "required": True, - "type": "number", - "min": 0.001, # Kelvin > 0 - "max": 2000, # Max for fire scenarios - }, + "temp_ambient": { + "required": True, + "type": "number", + "min": 0.001, + "max": 2000, }, }, } - elif input["heat_transfer"]["type"] == "s-b": - schema_heattransfer = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "valve": {"required": True}, - "rupture": {"required": False}, - "vessel": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "length": {"required": True, "type": "number"}, - "diameter": {"required": True, "type": "number"}, - "thickness": {"required": True, "type": "number", "min": 0.0}, - "heat_capacity": {"required": True, "type": "number", "min": 1}, - "density": {"required": True, "type": "number", "min": 1}, - "orientation": { - "required": True, - "type": "string", - "allowed": ["vertical", "horizontal"], - }, - "type": { - "required": False, - "type": "string", - "allowed": ["Flat-end", "DIN", "ASME F&D", "Hemispherical"], - }, - "liquid_level": { - "required": False, - "type": "number", - "min": 0, - }, + elif ht_type == "s-b": + # Vessel requirements for s-b (Stefan-Boltzmann fire) + required_vessel_fields = [ + "length", + "diameter", + "thickness", + "heat_capacity", + "density", + "orientation", + ] + + schema_heattransfer = base_schema.copy() + schema_heattransfer["vessel"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "schema": create_vessel_schema(required_fields=required_vessel_fields), + } + schema_heattransfer["heat_transfer"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "allowed": ["fire", "type"], + "schema": { + "type": { + "required": True, + "type": "string", + "allowed": ["s-b"], }, - }, - "heat_transfer": { - "required": True, - "type": "dict", - "allow_unknown": False, - "allowed": ["fire", "type"], - "schema": { - "type": { - "required": True, - "type": "string", - "allowed": ["s-b"], - }, - "fire": { - "required": True, - "type": "string", - "allowed": [ - "api_pool", - "api_jet", - "scandpower_pool", - "scandpower_jet", - ], - }, + "fire": { + "required": True, + "type": "string", + "allowed": [ + "api_pool", + "api_jet", + "scandpower_pool", + "scandpower_jet", + ], }, }, } + # Validate with the schema v = Validator(schema_heattransfer) retval = v.validate(input) - if v.errors: - print(v.errors) + + if not retval: + # Raise specific exceptions for heat transfer errors + if "heat_transfer" in v.errors: + format_cerberus_errors( + {"heat_transfer": v.errors["heat_transfer"]}, "heat_transfer" + ) + elif "vessel" in v.errors: + format_cerberus_errors({"vessel": v.errors["vessel"]}, "vessel") + else: + format_cerberus_errors(v.errors, f"heat_transfer ({ht_type})") return retval def valve_validation(input): """ - Validate input['valve'] deeper than cerberus + Validate input['valve'] deeper than cerberus. Parameters ---------- input : dict Structure holding input - Return ---------- - : bool + bool True for success, False for failure """ - schema_relief = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "vessel": {"required": True}, - "rupture": {"required": False}, - "heat_transfer": {"required": True}, - "valve": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "type": { - "required": True, - "type": "string", - "allowed": ["orifice", "psv", "controlvalve", "mdot", "relief"], - }, - "flow": { - "required": True, - "type": "string", - "allowed": ["discharge", "filling"], - }, - "diameter": {"required": False, "type": "number", "min": 0}, - "discharge_coef": { - "required": False, - "type": "number", - "min": 0.001, - "max": 1.0, # Physical limit - }, - "set_pressure": {"required": True, "type": "number", "min": 0}, - "end_pressure": {"type": "number", "min": 0}, - "blowdown": {"required": False, "type": "number", "min": 0, "max": 1}, - "back_pressure": {"required": True, "type": "number", "min": 0}, - "Cv": {"type": "number", "min": 0}, - "mdot": {"type": ["number", "list"]}, - "time": {"type": "list"}, - }, - }, - } - - schema_psv = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "vessel": {"required": True}, - "rupture": {"required": False}, - "heat_transfer": {"required": False}, - "valve": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "type": { - "required": True, - "type": "string", - "allowed": ["orifice", "psv", "controlvalve", "mdot"], - }, - "flow": { - "required": True, - "type": "string", - "allowed": ["discharge", "filling"], - }, - "diameter": {"required": True, "type": "number", "min": 0}, - "discharge_coef": { - "required": True, - "type": "number", - "min": 0.001, - "max": 1.0, # Physical limit - }, - "set_pressure": {"required": True, "type": "number", "min": 0}, - "end_pressure": {"type": "number", "min": 0}, - "blowdown": {"required": True, "type": "number", "min": 0, "max": 1}, - "back_pressure": {"required": True, "type": "number", "min": 0}, - "Cv": {"type": "number", "min": 0}, - "mdot": {"type": ["number", "list"]}, - "time": {"type": "list"}, - }, - }, - } - - schema_orifice = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "vessel": {"required": True}, - "rupture": {"required": False}, - "heat_transfer": {"required": False}, - "valve": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "type": { - "required": True, - "type": "string", - "allowed": ["orifice", "psv", "controlvalve", "mdot"], - }, - "flow": { - "required": True, - "type": "string", - "allowed": ["discharge", "filling"], - }, - "diameter": {"required": True, "type": "number", "min": 0}, - "discharge_coef": { - "required": True, - "type": "number", - "min": 0.001, - "max": 1.0, # Physical limit - }, - "set_pressure": {"type": "number", "min": 0}, - "end_pressure": {"type": "number", "min": 0}, - "blowdown": {"type": "number", "min": 0, "max": 1}, - "back_pressure": {"required": True, "type": "number", "min": 0}, - "Cv": {"type": "number", "min": 0}, - "mdot": {"type": ["number", "list"]}, - "time": {"type": "list"}, - }, - }, - } - - schema_control_valve = { - "initial": {"required": True}, - "rupture": {"required": False}, - "calculation": {"required": True}, - "validation": {"required": False}, - "vessel": {"required": True}, - "heat_transfer": {"required": False}, - "valve": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "type": { - "required": True, - "type": "string", - "allowed": ["orifice", "psv", "controlvalve", "mdot"], - }, - "flow": { - "required": True, - "type": "string", - "allowed": ["discharge", "filling"], - }, - "back_pressure": {"required": True, "type": "number", "min": 0}, - "Cv": {"required": True, "type": "number", "min": 0}, - "time_constant": {"type": "number", "min": 0}, - "characteristic": { - "type": "string", - "allowed": ["linear", "eq", "fast"], - }, - }, - }, + valve_type = input["valve"]["type"] + + # Build schema using factory function + base_schema = create_top_level_schema() + base_schema["vessel"] = {"required": True} + + # Heat transfer requirements vary by valve type + if valve_type == "relief": + base_schema["heat_transfer"] = {"required": True} + else: + base_schema["heat_transfer"] = {"required": False} + + # Create valve schema with type-specific requirements + base_schema["valve"] = { + "required": True, + "type": "dict", + "allow_unknown": False, + "schema": create_valve_schema(valve_type), } - schema_mdot = { - "initial": {"required": True}, - "calculation": {"required": True}, - "validation": {"required": False}, - "vessel": {"required": True}, - "rupture": {"required": False}, - "heat_transfer": {"required": False}, - "valve": { - "required": True, - "type": "dict", - "allow_unknown": False, - "schema": { - "type": { - "required": True, - "type": "string", - "allowed": ["orifice", "psv", "controlvalve", "mdot"], - }, - "flow": { - "required": True, - "type": "string", - "allowed": ["discharge", "filling"], - }, - "mdot": {"required": True, "type": ["number", "list"]}, - "time": {"type": ["number", "list"]}, - "back_pressure": {"required": True, "type": "number", "min": 0}, - }, - }, - } - - if input["valve"]["type"] == "relief": - v = Validator(schema_relief) - retval = v.validate(input) - if v.errors: - print(v.errors) + # Validate + v = Validator(base_schema) + retval = v.validate(input) - if input["valve"]["type"] == "psv": - v = Validator(schema_psv) - retval = v.validate(input) - if v.errors: - print(v.errors) - elif input["valve"]["type"] == "orifice": - v = Validator(schema_orifice) - retval = v.validate(input) - if v.errors: - print(v.errors) - elif input["valve"]["type"] == "controlvalve": - v = Validator(schema_control_valve) - retval = v.validate(input) - if v.errors: - print(v.errors) - elif input["valve"]["type"] == "mdot": - v = Validator(schema_mdot) - retval = v.validate(input) - if v.errors: - print(v.errors) + if not retval: + # Raise specific exceptions for valve errors + if "valve" in v.errors: + format_cerberus_errors({"valve": v.errors["valve"]}, "valve") + else: + format_cerberus_errors(v.errors, f"valve ({valve_type})") return retval def validation(input): """ - Aggregate validation using cerberus + Aggregate validation using cerberus and cross-field checks. Parameters ---------- input : dict Structure holding input - Return ---------- - : bool + bool True for success, False for failure """ - return ( + # Schema-based validation + schema_valid = ( validate_mandatory_ruleset(input) and valve_validation(input) and heat_transfer_validation(input) ) + + if not schema_valid: + return False + + # Cross-field validation (physical constraints) + try: + validate_relief_valve_physics(input) + validate_composite_vessel(input) + validate_time_step_sanity(input) + except ( + ConfigurationError, + VesselConfigurationError, + ValveConfigurationError, + ): + # Re-raise configuration errors + raise + + return True From 7917fe8f3d8728bdf75b8966694a7df315d15a85 Mon Sep 17 00:00:00 2001 From: andr1976 Date: Mon, 23 Feb 2026 10:27:37 +0100 Subject: [PATCH 4/5] Adding exception handling and initial code refactoring --- src/hyddown/exceptions.py | 369 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 src/hyddown/exceptions.py diff --git a/src/hyddown/exceptions.py b/src/hyddown/exceptions.py new file mode 100644 index 0000000..620e987 --- /dev/null +++ b/src/hyddown/exceptions.py @@ -0,0 +1,369 @@ +# HydDown hydrogen/other gas depressurisation +# Copyright (c) 2021-2025 Anders Andreasen +# Published under an MIT license + +""" +Custom exceptions for HydDown package. + +This module defines a hierarchy of exceptions for different error conditions +that can occur during pressure vessel simulations. Using specific exception +types allows calling code to handle different error cases appropriately. + +Exception Hierarchy: + HydDownError (base) + ├── ThermodynamicError + │ ├── ThermodynamicConvergenceError + │ ├── InvalidStateError + │ └── PhaseEquilibriumError + ├── NumericalError + │ ├── NumericalInstabilityError + │ ├── IntegrationError + │ └── ConvergenceError + ├── ConfigurationError + │ ├── ValveConfigurationError + │ ├── VesselConfigurationError + │ └── HeatTransferConfigurationError + └── PhysicalConstraintError + ├── TriplePointViolation + ├── CriticalPointViolation + └── NegativeMassError + +Usage: + from hyddown.exceptions import ThermodynamicConvergenceError + + if not result.success: + raise ThermodynamicConvergenceError( + f"Failed to converge: {result.message}" + ) +""" + + +class HydDownError(Exception): + """ + Base exception for all HydDown errors. + + All custom exceptions in HydDown inherit from this base class, + allowing calling code to catch all HydDown-specific errors with + a single except clause if desired. + """ + pass + + +# ============================================================================ +# Thermodynamic Errors +# ============================================================================ + +class ThermodynamicError(HydDownError): + """ + Base class for thermodynamic calculation errors. + + Raised when CoolProp property calculations fail or produce + non-physical results. + """ + pass + + +class ThermodynamicConvergenceError(ThermodynamicError): + """ + Failed to converge in thermodynamic property solver. + + Raised when numerical optimization fails to find a valid thermodynamic + state (e.g., during PHproblem or UDproblem calculations). This typically + occurs with multicomponent mixtures when scipy.optimize cannot find + a solution. + + Attributes + ---------- + solver : str + Name of the solver that failed ('PHproblem', 'UDproblem', etc.) + state_vars : dict + Input state variables that caused failure + iterations : int, optional + Number of iterations attempted + """ + + def __init__(self, message, solver=None, state_vars=None, iterations=None): + super().__init__(message) + self.solver = solver + self.state_vars = state_vars + self.iterations = iterations + + +class InvalidStateError(ThermodynamicError): + """ + Thermodynamic state is outside valid range for equation of state. + + Raised when requested state conditions are outside the valid range + of the CoolProp equation of state (e.g., below triple point, above + maximum temperature, or at unphysical densities). + + Attributes + ---------- + variable : str + The state variable that is out of range ('temperature', 'pressure', etc.) + value : float + The invalid value + valid_range : tuple + (min, max) valid range for the variable + """ + + def __init__(self, message, variable=None, value=None, valid_range=None): + super().__init__(message) + self.variable = variable + self.value = value + self.valid_range = valid_range + + +class PhaseEquilibriumError(ThermodynamicError): + """ + Error in phase equilibrium calculations for two-phase systems. + + Raised when calculations involving vapor-liquid equilibrium fail, + such as when trying to determine liquid level or saturation properties. + """ + pass + + +# ============================================================================ +# Numerical Errors +# ============================================================================ + +class NumericalError(HydDownError): + """ + Base class for numerical integration and solver errors. + """ + pass + + +class NumericalInstabilityError(NumericalError): + """ + Time step too large for numerical stability. + + Raised when the explicit Euler integration scheme becomes unstable, + typically because the time step is too large relative to the + characteristic time scales of the problem. This can manifest as + negative masses, temperatures, or pressures. + + Attributes + ---------- + time_step : float + The time step that caused instability [s] + recommended_dt : float, optional + Recommended smaller time step [s] + characteristic_time : float, optional + Characteristic time scale of the problem [s] + """ + + def __init__(self, message, time_step=None, recommended_dt=None, + characteristic_time=None): + super().__init__(message) + self.time_step = time_step + self.recommended_dt = recommended_dt + self.characteristic_time = characteristic_time + + +class IntegrationError(NumericalError): + """ + Error during time integration of mass/energy balances. + + Raised when the numerical integration produces invalid results, + such as NaN or Inf values in state variables. + """ + pass + + +class ConvergenceError(NumericalError): + """ + General convergence failure in iterative solvers. + + Raised when iterative algorithms (not thermodynamic solvers) + fail to converge within specified tolerance or iteration limit. + """ + pass + + +# ============================================================================ +# Configuration Errors +# ============================================================================ + +class ConfigurationError(HydDownError): + """ + Base class for invalid input configuration errors. + + These errors indicate problems with the input YAML file beyond + what the Cerberus schema validator can catch (e.g., physically + impossible values or inconsistent parameters). + """ + pass + + +class ValveConfigurationError(ConfigurationError): + """ + Invalid valve parameters. + + Raised when valve configuration is physically impossible or + inconsistent (e.g., negative diameter, discharge coefficient > 1, + relief valve set pressure below operating pressure). + + Attributes + ---------- + valve_type : str + Type of valve ('orifice', 'relief_valve', 'control_valve') + parameter : str + The problematic parameter name + value : float + The invalid value + """ + + def __init__(self, message, valve_type=None, parameter=None, value=None): + super().__init__(message) + self.valve_type = valve_type + self.parameter = parameter + self.value = value + + +class VesselConfigurationError(ConfigurationError): + """ + Invalid vessel geometry or material parameters. + + Raised when vessel configuration is physically impossible + (e.g., negative dimensions, wall thickness greater than diameter, + invalid material properties). + """ + pass + + +class HeatTransferConfigurationError(ConfigurationError): + """ + Invalid heat transfer parameters. + + Raised when heat transfer configuration is inconsistent or + physically impossible (e.g., negative heat transfer coefficient, + invalid fire scenario). + """ + pass + + +# ============================================================================ +# Physical Constraint Errors +# ============================================================================ + +class PhysicalConstraintError(HydDownError): + """ + Base class for violations of physical constraints. + + These errors indicate that the simulation has reached a state + that violates fundamental physical constraints. + """ + pass + + +class TriplePointViolation(PhysicalConstraintError): + """ + Fluid state below triple point temperature or pressure. + + Raised when calculations result in conditions below the fluid's + triple point, where the equation of state is not valid and + solid phase would form. + + Attributes + ---------- + fluid : str + Name of the fluid + temperature : float + Current temperature [K] + pressure : float + Current pressure [Pa] + T_triple : float + Triple point temperature [K] + P_triple : float + Triple point pressure [Pa] + """ + + def __init__(self, message, fluid=None, temperature=None, pressure=None, + T_triple=None, P_triple=None): + super().__init__(message) + self.fluid = fluid + self.temperature = temperature + self.pressure = pressure + self.T_triple = T_triple + self.P_triple = P_triple + + +class CriticalPointViolation(PhysicalConstraintError): + """ + Fluid state exceeds equation of state validity limits. + + Raised when calculations produce conditions far beyond the + critical point or other EOS validity limits. + """ + pass + + +class NegativeMassError(PhysicalConstraintError): + """ + Mass, pressure, or temperature became negative. + + Raised when numerical integration produces unphysical negative + values for extensive properties. This usually indicates numerical + instability or time step too large. + + Attributes + ---------- + variable : str + The variable that became negative ('mass', 'pressure', 'temperature') + value : float + The negative value + time : float + Simulation time when error occurred [s] + """ + + def __init__(self, message, variable=None, value=None, time=None): + super().__init__(message) + self.variable = variable + self.value = value + self.time = time + + +# ============================================================================ +# Warning Classes (for non-fatal issues) +# ============================================================================ + +class HydDownWarning(UserWarning): + """ + Base warning class for HydDown. + + Warnings are issued for conditions that may affect accuracy but + don't prevent calculation from completing. + """ + pass + + +class NumericalStabilityWarning(HydDownWarning): + """ + Warning that time step may be too large for optimal stability. + + Issued when CFL condition or other stability criteria suggest + the time step should be reduced, but calculation can continue. + """ + pass + + +class AccuracyWarning(HydDownWarning): + """ + Warning that results may have reduced accuracy. + + Issued when approximations or numerical issues may affect + accuracy but not prevent calculation completion. + """ + pass + + +class ConvergenceWarning(HydDownWarning): + """ + Warning about slow or marginal convergence. + + Issued when iterative solvers converge but required many + iterations or achieved marginal tolerance. + """ + pass From 7972524e5a3f59f575daca71316e4977597c859a Mon Sep 17 00:00:00 2001 From: andr1976 Date: Mon, 23 Feb 2026 13:37:26 +0100 Subject: [PATCH 5/5] Fixed cerberus schema for failing tests --- .gitignore | 5 +- src/hyddown/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 274 -> 332 bytes .../__pycache__/hdclass.cpython-312.pyc | Bin 100429 -> 93798 bytes .../__pycache__/thermo_solver.cpython-312.pyc | Bin 0 -> 15976 bytes .../__pycache__/validator.cpython-312.pyc | Bin 18329 -> 18458 bytes src/hyddown/hdclass.py | 243 +++-------- src/hyddown/thermo_solver.py | 397 ++++++++++++++++++ src/hyddown/validator.py | 16 +- 9 files changed, 467 insertions(+), 195 deletions(-) create mode 100644 src/hyddown/__pycache__/thermo_solver.cpython-312.pyc create mode 100644 src/hyddown/thermo_solver.py diff --git a/.gitignore b/.gitignore index cba009f..cee17ec 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,6 @@ BUG_FIX_VALIDATION_SUMMARY.md src/hyddown/__pycache__/hdclass.cpython-312.pyc VALIDATION_REPORT.txt src/hyddown/__pycache__/hdclass.cpython-312.pyc -src/hyddown/exceptions.py PHASE1_PROGRESS.md src/hyddown/__pycache__/__init__.cpython-312.pyc src/hyddown/__pycache__/__init__.cpython-312.pyc @@ -93,3 +92,7 @@ PHASE1_COMPLETE.md PHASE1_CERBERUS_ENHANCEMENTS.md VALIDATOR_ANALYSIS.md PHASE1.5_COMPLETE.md +PHASE2_EXTRACTION_PLAN.md +.gitignore +PHASE2.1_COMPLETE.md +VALIDATOR_TEST_FIX.md diff --git a/src/hyddown/__init__.py b/src/hyddown/__init__.py index 8dfe927..a6f8419 100644 --- a/src/hyddown/__init__.py +++ b/src/hyddown/__init__.py @@ -6,3 +6,4 @@ from .transport import * from . import exceptions from . import safety_checks +from .thermo_solver import ThermodynamicSolver diff --git a/src/hyddown/__pycache__/__init__.cpython-312.pyc b/src/hyddown/__pycache__/__init__.cpython-312.pyc index 988b41e2d5cff4c55d058b3ce2c90ed282e49ab0..e009e681d661ab5fd34fa41a2c981926e2609e9c 100644 GIT binary patch delta 139 zcmbQlbcTuVG%qg~0}$*Lo0G}KIFV0+(PpB$p+E{l3QG=aE?X2EBSR&tCfmdqO({*r zTf!k3sYSW@DV2GNxtYnq`8j2&MShy>6FU?HZ}FCZ<>HIMqD5>ImwIUM0ZnEE;$kr% d@qw9M7tG%qg~0}wE$&Ca~dFp*D!(O{ywAxkBTChNouO=dqJwOV1~eOFZ;pdv;f fE*1t7AD9^#8NYBbFpA${khseragRZ|2xJTZ9+MIL diff --git a/src/hyddown/__pycache__/hdclass.cpython-312.pyc b/src/hyddown/__pycache__/hdclass.cpython-312.pyc index 367375af0258434b2d6f8f41ca092fcadebd16e4..a599d62341be449b31ccc1863f280ec39d7e7cdd 100644 GIT binary patch delta 8277 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 diff --git a/src/hyddown/__pycache__/thermo_solver.cpython-312.pyc b/src/hyddown/__pycache__/thermo_solver.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e61419cfff8a15d90dea875851ec8d1be79a9731 GIT binary patch literal 15976 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 F{VzgLUJL*L literal 0 HcmV?d00001 diff --git a/src/hyddown/__pycache__/validator.cpython-312.pyc b/src/hyddown/__pycache__/validator.cpython-312.pyc index 918234e0aaec7599ff200f4fce4fa71dd1b6709d..0bc2ff270cae710b2b98a191acad49dcd2aa65fe 100644 GIT binary patch delta 2302 zcmbu9Z){Ul6u^65``WJU)~#FDj;*5`9b1*rjX& zB=s~Lj`i$~9Y|((v~>Hs+FRBIH*~Gv;P2{g_iy4G&292}@SEW0%n|cx*I4;z`E_UI zjAi9@O_h?D*-hkUVE8A6W#wzg7JkB7R{H`NST7%^mJs2uJIA!*f7^LwQJ)o^lX5(q2*#4y9ig87V2t(i%S9{X6ELzzxmEO{L`HbX zHEcNs28PGz*0AF!kMKLL8u=86XO!h7Tq0e3y7U)vj8Bx66`@B`(-VsHu-DL;qn@^B zAR3G(!m&6z!|#=O)->P=7#6cvJDYLNQfrH zc0>q)bt3l8mY}o)u^O?H-|^IuNxsbM(Y}R-%RJ=W;C&ax_YfZ-rV#HVCJ_qYlAO9X zJWzI?-}El3+=yZqVg%8R5QlykrA>&DM{OJV@(TM35rXI9)-uc@R*%3$P3rfDBE2FF zUqKt*CY~~XsiIw;MhE5VihXLSQN!!19E&BEF{q+)9t==5a!5)L_=u-Z!hFw`ALtXa z0_6QbmCv_%mx>xLk|E8*5f6Olzk}+D*%^mq=qJz}5;}gfYQTSMP@)C>Ww1y;bO=&N-l(;w&qbkCA$4Zr68RwV>P?uE ziR=+QVgyx@Q6KS@tDY!&8})Mlx17pOk*6YKMYzI;SJ{WJpqB|DA&iJ|garVR!gWEK zV}`SRJ9GgS{*3hXn)d==n`7nt6?U$k$dZ#GVWPaOG@7}u?n#Ov6M}`x7D%udvGN1#x=k{)P#^f4Y@>f*Q+9?A5?|>V(X9AxI`>> RB62yg+}%z}NG5YYz`vQr_@e*- delta 2201 zcmaKsZETZO6vvD%a7JH|G;j=>5WU3nQBFXLtIO<{~N0Si*7Y<5}IfXAH)xapvEsIM9;a!O+mRIemVEt z|2_BRoO|CmOpcEd+gDbrCP912GT!$>;)ty1%os2V z+Za`1YCw&d0;X6_ASad^$OXSbRcfL+Gk>HZ4nsE7|^JFzZ5Wo zI*(4Hjvkq&6`D_{Q?-xK0ucSX=Ny-*3#M6UAxyK<3R<+E1Z;FB^z5`4m`CT(67Wr< zrOp}XT}GWNdt}n0YdH-D z(#PA(cS$?{#^WST-fr1pmhr~0MHIe1w}@M(J%|Wm3*s3>6cON>vq>2O^Jl%wc}XGb zxudw1r1-YtMWj_9EnY*^VHBqLw5P86^=;2ICp^xlnh1FwP?U>#~K)AsC72isyNmq=m@r z+*Ywv83l7pf3kwhWF!B6?oZ?(zgk&YhKh`_I~46^M^Jne`?&+VW5Gl+98a(@p6B%` z$H99}f5Pih$#%ZoSL&M7vSyrxcXXPtb^M5L^~%lYc^0t)f%#;s5JALFgoePgvvNcT z(TE6hTTMNg;H@=2^<8wF=c$^GnzLxWkN6OA4)FnE0&xLwTFK^31U<>`*A$sMaBL$Y z&HeuTWu3xAj38b_pk|+=Z=f&P7mB*O2L~uvg9C}|JiHvQ%G&v$ztFlL^_V}rS3c%% zQ$B~zE&aBC$k1@lAl@}ekzt9-Ghl!k1daWbl607`JK$HQ{3$s}KqT6P#4j#cZr#)) zQSBX)G7cLe2CRH7rNGBvrk13_XVuN&n`$?>GQ%=0#*l0^Eup17^8d-_a@*2M*XtlJ z6Y|!_%jfV@OY2>&Ag>Ve#>a1Fi_7%)?$gS|gOjAxVU>qwPUl*cnV+rGh{oMjE;~La z(Wo-4Ml}9io$rwmlNec4Ki@j3MvvZKZzr=w3PoPuf;ic{p^T*wBZ!aq$z}7>@1gl7 zz^i1dL)3|=7%?5|gsn!1TFXVN0r3gKf)K@OM(YH^0f1m)wum!Tc_P?nDAEz|4AKw& z3M^K5(_)e+Tr6a-AzQywp+3%fg=4C;&touA)!Wg!h@uCv(7Vsx8xAFdNfsJN>z40)M^FNpS$z|?e;nS|d$SJsh z#1bA^vB0ECt_9bfK7M+|D%(AWRKDP*XW=!^!t0*J{Oy(1ynN-5)JF0n0 z!_$vW_UX48iVW!ySTB1^n9Z(iI>k?jPqUf1aU9dR&$K5L-4k|KvM-@&Owb_g+wz|W82O!2ES ztb9m`_&6Oco@orl&II}@7{+w0_anoEkWfe;^Ff&lLs2qC{{PGbDMMk2F#dCTnrCKJc6 R5v7_qe%nbZNFFNz^ 1 or subcooled liquid). - """ - if self.fluid.Q() >= 0 and self.fluid.Q() <= 1: - rho_liq = self.fluid.saturated_liquid_keyed_output(CP.iDmass) - rho_vap = self.fluid.saturated_vapor_keyed_output(CP.iDmass) - m_liq = self.fluid.rhomass() * self.inner_vol.V_total * (1 - self.fluid.Q()) - V_liq = m_liq / rho_liq - h_liq = self.inner_vol.h_from_V(V_liq) - return h_liq - else: - return 0.0 - - def PHres(self, T, P, H): - """ - Residual enthalpy function to be minimized during PH-problem. - - Used by numerical optimizer (scipy.optimize.minimize) to find temperature - that satisfies constant pressure-enthalpy constraints for multicomponent - fluids. The optimizer adjusts T until residual approaches zero. - - Parameters - ---------- - H : float - Enthalpy at initial/final conditions [J/kg] - P : float - Pressure at final conditions [Pa] - T : float - Updated estimate for the final temperature at (P,H) [K] + This is a convenience method that delegates to the ThermodynamicSolver. + Maintained for backward compatibility with existing code. Returns ------- float - Squared normalized enthalpy residual (dimensionless). - Zero when correct temperature is found. + Liquid level height from vessel bottom [m] """ - # Extract scalar from array (scipy optimizers pass arrays, CoolProp needs scalars) - T_scalar = float(T.item()) if hasattr(T, 'item') else float(T) - self.vent_fluid.update(CP.PT_INPUTS, P, T_scalar) - return ((H - self.vent_fluid.hmass()) / H) ** 2 + return self.solver.calc_liquid_level() - def PHres_relief(self, T, P, H): + def PHproblem(self, H, P, Tguess, relief=False): """ - Residual enthalpy function for PH-problem during relief valve calculations. + Solve constant pressure, constant enthalpy problem. - Used by numerical optimizer (scipy.optimize.root_scalar) to find temperature - for relief valve discharge calculations. Similar to PHres() but uses the - main fluid state instead of vent_fluid state. + This is a convenience method that delegates to the ThermodynamicSolver. + Maintained for backward compatibility with existing code. Parameters ---------- H : float - Enthalpy at initial/final conditions [J/kg] + Enthalpy [J/kg] P : float - Pressure at final conditions [Pa] - T : float - Updated estimate for the final temperature at (P,H) [K] + Pressure [Pa] + Tguess : float + Initial temperature guess [K] + relief : bool, optional + Use relief valve solver if True Returns ------- float - Normalized enthalpy residual (dimensionless). - Zero when correct temperature is found. + Temperature at (P, H) [K] """ - # Extract scalar from array (scipy optimizers pass arrays, CoolProp needs scalars) - T_scalar = float(T.item()) if hasattr(T, 'item') else float(T) - self.fluid.update(CP.PT_INPUTS, P, T_scalar) - return (H - self.fluid.hmass()) / H - - def PHproblem(self, H, P, Tguess, relief=False): - """ - Defining a constant pressure, constant enthalpy problem i.e. typical adiabatic - problem like e.g. valve flow for the vented flow (during discharge). - For multicomponent mixture the final temperature is changed/optimised until the residual - enthalpy is near zero in an optimisation step. For single component fluid the coolprop - built in methods are used for speed. - - Parameters - ---------- - H : float - Enthalpy at initial/final conditions - P : float - Pressure at final conditions. - Tguess : float - Initial guess for the final temperature at P,H - """ - - # Multicomponent case - if "&" in self.species: - x0 = Tguess - if relief == False: - res = minimize( - self.PHres, - x0, - args=(P, H), - method="Nelder-Mead", - options={"xatol": 0.1, "fatol": 0.001}, - ) - # Check convergence - sc.check_optimization_convergence( - res, - solver_name="PHproblem", - state_vars={"P": P, "H": H, "T_guess": x0} - ) - T1 = res.x[0] - else: - res = root_scalar( - self.PHres_relief, - args=(P, H), - x0=x0, - method="newton", - ) - # Check convergence - sc.check_optimization_convergence( - res, - solver_name="PHproblem_relief", - state_vars={"P": P, "H": H} - ) - T1 = res.root - # single component fluid case - else: - T1 = PropsSI("T", "P", P, "H", H, self.species) - return T1 - - def UDres(self, x, U, rho): - """ - Residual U-rho to be minimised during a U-rho/UV-problem - - Parameters - ---------- - U : float - Internal energy at final conditions - rho : float - Density at final conditions - """ - self.fluid.update(CP.PT_INPUTS, x[0], x[1]) - return ((U - self.fluid.umass()) / U) ** 2 + ( - (rho - self.fluid.rhomass()) / rho - ) ** 2 + return self.solver.PHproblem(H, P, Tguess, relief) def UDproblem(self, U, rho, Pguess, Tguess): """ - Defining a constant UV problem i.e. constant internal energy and density/volume - problem relevant for the 1. law of thermodynamics. - For multicomponent mixture the final temperature/pressure is changed/optimised until the - residual U/rho is near zero. For single component fluid the coolprop - built in methods are used for speed. + Solve constant internal energy, constant density problem. + + This is a convenience method that delegates to the ThermodynamicSolver. + Maintained for backward compatibility with existing code. Parameters ---------- U : float - Internal energy at final conditions + Internal energy [J/kg] rho : float - Density at final conditions. + Density [kg/m³] Pguess : float - Initial guess for the final pressure at U, rho + Initial pressure guess [Pa] Tguess : float - Initial guess for the final temperature at U, rho + Initial temperature guess [K] + + Returns + ------- + P1 : float + Pressure [Pa] + T1 : float + Temperature [K] + Ures : float + Internal energy residual [J/kg] """ - if "&" in self.species: - x0 = [Pguess, Tguess] - res = minimize( - self.UDres, - x0, - args=(U, rho), - method="Nelder-Mead", - options={"xatol": 0.1, "fatol": 0.001}, - ) - # Check convergence - sc.check_optimization_convergence( - res, - solver_name="UDproblem", - state_vars={"U": U, "rho": rho, "P_guess": Pguess, "T_guess": Tguess} - ) - P1 = res.x[0] - T1 = res.x[1] - Ures = U - self.fluid.umass() - else: - P1 = PropsSI("P", "D", rho, "U", U, self.species) - T1 = PropsSI("T", "D", rho, "U", U, self.species) - Ures = 0 - return P1, T1, Ures + return self.solver.UDproblem(U, rho, Pguess, Tguess) def run(self, disable_pbar=True): """ @@ -1452,7 +1321,7 @@ def run(self, disable_pbar=True): 1, ) else: - T1 = self.PHproblem( + T1 = self.solver.PHproblem( h_in + self.tstep * self.Q_inner[i] / self.mass_fluid[i], self.Pset, @@ -1498,7 +1367,7 @@ def run(self, disable_pbar=True): else: self.mass_rate[i] = 0 - P1, T1, self.U_res[i] = self.UDproblem( + P1, T1, self.U_res[i] = self.solver.UDproblem( U_end / self.mass_fluid[i], self.rho[i], self.P[i - 1], @@ -1510,7 +1379,7 @@ def run(self, disable_pbar=True): self.fluid.update(CP.PT_INPUTS, self.P[i], self.T_fluid[i]) else: - P1, T1, self.U_res[i] = self.UDproblem( + P1, T1, self.U_res[i] = self.solver.UDproblem( U_end / self.mass_fluid[i], self.rho[i], self.P[i - 1], @@ -1556,12 +1425,12 @@ def run(self, disable_pbar=True): self.S_mass[i] = self.fluid.smass() self.U_mass[i] = self.fluid.umass() - self.liquid_level[i] = self.calc_liquid_level() + self.liquid_level[i] = self.solver.calc_liquid_level() # Calculating vent temperature (adiabatic) only for discharge problem if self.input["valve"]["flow"] == "discharge": if "&" in self.species: - self.T_vent[i] = self.PHproblem( + self.T_vent[i] = self.solver.PHproblem( self.H_mass[i], self.p_back, self.vent_fluid.T() ) else: diff --git a/src/hyddown/thermo_solver.py b/src/hyddown/thermo_solver.py new file mode 100644 index 0000000..767ee66 --- /dev/null +++ b/src/hyddown/thermo_solver.py @@ -0,0 +1,397 @@ +# HydDown hydrogen/other gas depressurisation +# Copyright (c) 2021-2025 Anders Andreasen +# Published under an MIT license + +""" +Thermodynamic solver for CoolProp fluid property calculations. + +This module provides the ThermodynamicSolver class which encapsulates all +thermodynamic state calculations for HydDown simulations. It manages CoolProp +AbstractState objects and provides methods for solving thermodynamic problems +(PH-problems, UD-problems) for both single-component and multicomponent fluids. + +The ThermodynamicSolver handles: +- CoolProp AbstractState initialization and management +- PH-problems: Finding temperature at constant pressure and enthalpy +- UD-problems: Finding pressure and temperature at constant internal energy and density +- Liquid level calculations for two-phase systems +- Single-component (fast, direct CoolProp calls) and multicomponent (slower, numerical optimization) fluids + +Key Methods: +- PHproblem(): Solve for temperature at given P, H (isenthalpic expansion) +- UDproblem(): Solve for P, T at given U, density (constant internal energy) +- calc_liquid_level(): Calculate liquid height in two-phase systems + +Typical usage: + solver = ThermodynamicSolver( + species="HEOS::Hydrogen", + mole_fractions=[1.0], + vessel_geometry=inner_vol + ) + T = solver.PHproblem(H=1e6, P=1e5, Tguess=300) +""" + +import numpy as np +from scipy.optimize import minimize, root_scalar +from CoolProp.CoolProp import PropsSI +import CoolProp.CoolProp as CP +from hyddown import safety_checks as sc + + +class ThermodynamicSolver: + """ + Thermodynamic solver for CoolProp-based fluid property calculations. + + This class encapsulates all thermodynamic calculations required for HydDown + simulations, managing CoolProp AbstractState objects and providing methods + to solve for thermodynamic states given various property pairs. + + Parameters + ---------- + species : str + CoolProp fluid name (e.g., "HEOS::Hydrogen") or mixture + (e.g., "HEOS::Methane[0.9]&Ethane[0.1]") + mole_fractions : list of float + Mole fractions for each component (must sum to 1.0) + vessel_geometry : fluids.TANK, optional + Vessel geometry object for liquid level calculations. + Required if calc_liquid_level() will be used. + species_SRK : str, optional + CoolProp species string using SRK backend for transport properties. + If not provided, defaults to species. + + Attributes + ---------- + fluid : CoolProp.AbstractState + Main fluid state for thermodynamic calculations + vent_fluid : CoolProp.AbstractState + Fluid state for vent/discharge calculations (gas phase) + res_fluid : CoolProp.AbstractState + Fluid state for reservoir/filling calculations + transport_fluid : CoolProp.AbstractState + Fluid state for transport property calculations (gas phase) + transport_fluid_wet : CoolProp.AbstractState + Fluid state for wetted region transport properties (liquid phase) + is_multicomponent : bool + True if fluid is multicomponent mixture + """ + + def __init__(self, species, mole_fractions, vessel_geometry=None, species_SRK=None): + """ + Initialize the thermodynamic solver with fluid composition and geometry. + + Parameters + ---------- + species : str + CoolProp fluid name + mole_fractions : list of float + Mole fractions for each component + vessel_geometry : fluids.TANK, optional + Vessel geometry for liquid level calculations + species_SRK : str, optional + Species string for SRK backend (transport properties) + """ + self.species = species + self.mole_fractions = mole_fractions + self.vessel_geometry = vessel_geometry + self.is_multicomponent = "&" in species + + # Set up SRK species for transport properties + if species_SRK is None: + self.species_SRK = species + else: + self.species_SRK = species_SRK + + # Initialize CoolProp AbstractState objects + self._initialize_fluid_states() + + def _initialize_fluid_states(self): + """ + Create CoolProp AbstractState objects for different calculation needs. + + Creates: + - fluid: Main thermodynamic state + - vent_fluid: Vent/discharge calculations (gas phase) + - res_fluid: Reservoir/filling calculations + - transport_fluid: Transport properties (gas phase, SRK backend) + - transport_fluid_wet: Transport properties (liquid phase, SRK backend) + """ + # Main fluid state + self.fluid = CP.AbstractState("HEOS", self.species) + if self.is_multicomponent: + self.fluid.specify_phase(CP.iphase_gas) + self.fluid.set_mole_fractions(self.mole_fractions) + + # Vent fluid (for discharge calculations, gas phase) + self.vent_fluid = CP.AbstractState("HEOS", self.species) + self.vent_fluid.specify_phase(CP.iphase_gas) + self.vent_fluid.set_mole_fractions(self.mole_fractions) + + # Reservoir fluid (for filling calculations) + self.res_fluid = CP.AbstractState("HEOS", self.species) + self.res_fluid.set_mole_fractions(self.mole_fractions) + + # Transport properties (gas phase, SRK backend for robustness) + self.transport_fluid = CP.AbstractState("HEOS", self.species_SRK) + self.transport_fluid.specify_phase(CP.iphase_gas) + self.transport_fluid.set_mole_fractions(self.mole_fractions) + + # Transport properties (liquid phase for wetted regions) + self.transport_fluid_wet = CP.AbstractState("HEOS", self.species_SRK) + self.transport_fluid_wet.specify_phase(CP.iphase_liquid) + self.transport_fluid_wet.set_mole_fractions(self.mole_fractions) + + def calc_liquid_level(self): + """ + Calculate liquid level height based on current two-phase fluid state. + + For two-phase systems (0 ≤ quality ≤ 1), calculates the height of liquid + phase in the vessel based on vapor quality, phase densities, and vessel geometry. + Uses vessel geometry from fluids.TANK to convert liquid volume to height. + + Returns + ------- + float + Liquid level height from vessel bottom [m]. + Returns 0.0 for single-phase gas (quality > 1) or subcooled liquid (quality < 0). + + Raises + ------ + ValueError + If vessel_geometry was not provided during initialization + """ + if self.vessel_geometry is None: + raise ValueError( + "vessel_geometry must be provided to ThermodynamicSolver " + "for liquid level calculations" + ) + + # Check if in two-phase region + quality = self.fluid.Q() + if quality >= 0 and quality <= 1: + # Get saturated phase densities + rho_liq = self.fluid.saturated_liquid_keyed_output(CP.iDmass) + rho_vap = self.fluid.saturated_vapor_keyed_output(CP.iDmass) + + # Calculate liquid mass and volume + m_liq = self.fluid.rhomass() * self.vessel_geometry.V_total * (1 - quality) + V_liq = m_liq / rho_liq + + # Convert volume to height using vessel geometry + h_liq = self.vessel_geometry.h_from_V(V_liq) + return h_liq + else: + return 0.0 + + def PHres(self, T, P, H): + """ + Residual enthalpy function to be minimized during PH-problem. + + Used by numerical optimizer (scipy.optimize.minimize) to find temperature + that satisfies constant pressure-enthalpy constraints for multicomponent + fluids. The optimizer adjusts T until residual approaches zero. + + Parameters + ---------- + T : float or array-like + Temperature estimate [K]. Optimizer may pass as array. + P : float + Pressure [Pa] + H : float + Target enthalpy [J/kg] + + Returns + ------- + float + Squared normalized enthalpy residual (dimensionless). + Zero when correct temperature is found. + """ + # Extract scalar from array (scipy optimizers pass arrays, CoolProp needs scalars) + T_scalar = float(T.item()) if hasattr(T, "item") else float(T) + self.vent_fluid.update(CP.PT_INPUTS, P, T_scalar) + return ((H - self.vent_fluid.hmass()) / H) ** 2 + + def PHres_relief(self, T, P, H): + """ + Residual enthalpy function for PH-problem during relief valve calculations. + + Used by numerical optimizer (scipy.optimize.root_scalar) to find temperature + for relief valve discharge calculations. Similar to PHres() but uses the + main fluid state instead of vent_fluid state. + + Parameters + ---------- + T : float or array-like + Temperature estimate [K] + P : float + Pressure [Pa] + H : float + Target enthalpy [J/kg] + + Returns + ------- + float + Normalized enthalpy residual (dimensionless). + Zero when correct temperature is found. + """ + # Extract scalar from array (scipy optimizers pass arrays, CoolProp needs scalars) + T_scalar = float(T.item()) if hasattr(T, "item") else float(T) + self.fluid.update(CP.PT_INPUTS, P, T_scalar) + return (H - self.fluid.hmass()) / H + + def PHproblem(self, H, P, Tguess, relief=False): + """ + Solve constant pressure, constant enthalpy problem (isenthalpic expansion). + + Finds temperature at specified pressure and enthalpy. Typical use case is + modeling adiabatic throttling/expansion through valves without work extraction. + For multicomponent mixtures, uses numerical optimization to find temperature. + For single component fluids, uses direct CoolProp methods for speed. + + Parameters + ---------- + H : float + Enthalpy [J/kg] + P : float + Pressure [Pa] + Tguess : float + Initial guess for temperature [K] + relief : bool, optional + If True, use relief valve solver (root_scalar with Newton method). + If False, use general solver (minimize with Nelder-Mead). + Default is False. + + Returns + ------- + float + Temperature at (P, H) [K] + + Raises + ------ + ThermodynamicConvergenceError + If numerical optimization fails to converge for multicomponent fluid + """ + # Multicomponent case: requires numerical optimization + if self.is_multicomponent: + x0 = Tguess + + if not relief: + # Use Nelder-Mead minimization + res = minimize( + self.PHres, + x0, + args=(P, H), + method="Nelder-Mead", + options={"xatol": 0.1, "fatol": 0.001}, + ) + # Check convergence + sc.check_optimization_convergence( + res, solver_name="PHproblem", state_vars={"P": P, "H": H, "T_guess": x0} + ) + T1 = res.x[0] + else: + # Use Newton root finding for relief valve + res = root_scalar( + self.PHres_relief, + args=(P, H), + x0=x0, + method="newton", + ) + # Check convergence + sc.check_optimization_convergence( + res, solver_name="PHproblem_relief", state_vars={"P": P, "H": H} + ) + T1 = res.root + + # Single component case: direct CoolProp calculation + else: + T1 = PropsSI("T", "P", P, "H", H, self.species) + + return T1 + + def UDres(self, x, U, rho): + """ + Residual function for UD-problem (constant internal energy and density). + + Used by numerical optimizer to find pressure and temperature that satisfy + constant internal energy and density constraints. Minimizes sum of squared + normalized residuals for both U and rho. + + Parameters + ---------- + x : array-like of float + [Pressure [Pa], Temperature [K]] + U : float + Target internal energy [J/kg] + rho : float + Target density [kg/m³] + + Returns + ------- + float + Sum of squared normalized residuals (dimensionless) + """ + self.fluid.update(CP.PT_INPUTS, x[0], x[1]) + return ((U - self.fluid.umass()) / U) ** 2 + ((rho - self.fluid.rhomass()) / rho) ** 2 + + def UDproblem(self, U, rho, Pguess, Tguess): + """ + Solve constant internal energy, constant density problem. + + Finds pressure and temperature at specified internal energy and density. + Relevant for 1st law of thermodynamics with constant volume. For multicomponent + mixtures, uses numerical optimization to find P and T. For single component + fluids, uses direct CoolProp methods for speed. + + Parameters + ---------- + U : float + Internal energy [J/kg] + rho : float + Density [kg/m³] + Pguess : float + Initial guess for pressure [Pa] + Tguess : float + Initial guess for temperature [K] + + Returns + ------- + P1 : float + Pressure at (U, rho) [Pa] + T1 : float + Temperature at (U, rho) [K] + Ures : float + Internal energy residual [J/kg]. Zero for single component fluids. + + Raises + ------ + ThermodynamicConvergenceError + If numerical optimization fails to converge for multicomponent fluid + """ + # Multicomponent case: requires numerical optimization + if self.is_multicomponent: + x0 = [Pguess, Tguess] + res = minimize( + self.UDres, + x0, + args=(U, rho), + method="Nelder-Mead", + options={"xatol": 0.1, "fatol": 0.001}, + ) + # Check convergence + sc.check_optimization_convergence( + res, + solver_name="UDproblem", + state_vars={"U": U, "rho": rho, "P_guess": Pguess, "T_guess": Tguess}, + ) + P1 = res.x[0] + T1 = res.x[1] + Ures = U - self.fluid.umass() + + # Single component case: direct CoolProp calculation + else: + P1 = PropsSI("P", "D", rho, "U", U, self.species) + T1 = PropsSI("T", "D", rho, "U", U, self.species) + Ures = 0 + + return P1, T1, Ures diff --git a/src/hyddown/validator.py b/src/hyddown/validator.py index fd7963a..87191f1 100644 --- a/src/hyddown/validator.py +++ b/src/hyddown/validator.py @@ -26,6 +26,7 @@ - valve_validation(): Validates valve parameters based on valve type """ +import copy from cerberus import Validator from cerberus.errors import ValidationError from hyddown.exceptions import ( @@ -206,8 +207,8 @@ def create_vessel_schema(required_fields=None): }, } - # Add material properties - schema.update(MATERIAL_PROPERTIES.copy()) + # Add material properties (deep copy to avoid shared state) + schema.update(copy.deepcopy(MATERIAL_PROPERTIES)) schema.update(create_liner_properties()) # Mark specified fields as required @@ -690,7 +691,8 @@ def heat_transfer_validation(input): ht_type = input["heat_transfer"]["type"] # Build schema using factory functions instead of massive duplication - base_schema = create_top_level_schema() + # Use deepcopy to ensure no schema state is shared between validations + base_schema = copy.deepcopy(create_top_level_schema()) base_schema["vessel"] = {"required": True} base_schema["valve"] = {"required": True} base_schema["heat_transfer"] = {"required": True} @@ -706,7 +708,7 @@ def heat_transfer_validation(input): "orientation", ] - schema_heattransfer = base_schema.copy() + schema_heattransfer = copy.deepcopy(base_schema) schema_heattransfer["vessel"] = { "required": True, "type": "dict", @@ -741,7 +743,7 @@ def heat_transfer_validation(input): # Vessel requirements for specified_Q (minimal) required_vessel_fields = ["length", "diameter"] - schema_heattransfer = base_schema.copy() + schema_heattransfer = copy.deepcopy(base_schema) schema_heattransfer["vessel"] = { "required": True, "type": "dict", @@ -763,7 +765,7 @@ def heat_transfer_validation(input): # Vessel requirements for specified_U (minimal) required_vessel_fields = ["length", "diameter"] - schema_heattransfer = base_schema.copy() + schema_heattransfer = copy.deepcopy(base_schema) schema_heattransfer["vessel"] = { "required": True, "type": "dict", @@ -803,7 +805,7 @@ def heat_transfer_validation(input): "orientation", ] - schema_heattransfer = base_schema.copy() + schema_heattransfer = copy.deepcopy(base_schema) schema_heattransfer["vessel"] = { "required": True, "type": "dict",