@@ -136,6 +136,62 @@ def test_no_feed(self):
136136 self .assertAlmostEqual (F .outputs [0 ], 0.0 ) # V_rate
137137 self .assertAlmostEqual (F .outputs [1 ], 0.0 ) # L_rate
138138
139+ def test_holdup_dynamics_drives_to_equilibrium (self ):
140+ """At steady state the drum liquid composition must equal the RR
141+ equilibrium liquid composition for the feed."""
142+ F = FlashDrum (holdup = 100.0 , N0 = [80.0 , 20.0 ]) # off-equilibrium init
143+ F .set_solver (EUF , parent = None )
144+
145+ # T=370 K gives two-phase region for benzene/toluene defaults at 1 atm
146+ T , P = 370.0 , 101325.0
147+ u = np .array ([10.0 , 0.5 , T , P ])
148+
149+ # at the RR equilibrium x_eq, dN/dt must vanish
150+ # compute x_eq via direct VLE (binary RR with same Antoine defaults)
151+ Psat = np .exp (F .antoine_A - F .antoine_B / (T + F .antoine_C ))
152+ K = Psat / P
153+ z = np .array ([0.5 , 0.5 ])
154+ d1 , d2 = K [0 ] - 1 , K [1 ] - 1
155+ beta = - (z [0 ]* d1 + z [1 ]* d2 ) / (d1 * d2 )
156+ x_eq = z / (1.0 + beta * (K - 1.0 ))
157+ x_eq = x_eq / x_eq .sum ()
158+
159+ # state at equilibrium with same total holdup
160+ N_eq = 100.0 * x_eq
161+ dN = F .op_dyn (N_eq , u , 0.0 )
162+ self .assertTrue (np .allclose (dN , 0.0 , atol = 1e-10 ))
163+
164+ # state away from equilibrium: dN must be non-zero
165+ dN_off = F .op_dyn (np .array ([80.0 , 20.0 ]), u , 0.0 )
166+ self .assertGreater (np .linalg .norm (dN_off ), 1e-3 )
167+
168+ def test_holdup_total_moles_conserved (self ):
169+ """dM/dt = sum(dN/dt) must be exactly zero (perfect level control)."""
170+ F = FlashDrum (holdup = 100.0 , N0 = [70.0 , 30.0 ])
171+ F .set_solver (EUF , parent = None )
172+
173+ u = np .array ([5.0 , 0.4 , 355.0 , 101325.0 ])
174+ for state in (np .array ([70.0 , 30.0 ]),
175+ np .array ([20.0 , 80.0 ]),
176+ np .array ([1.0 , 99.0 ])):
177+ dN = F .op_dyn (state , u , 0.0 )
178+ self .assertAlmostEqual (dN .sum (), 0.0 , places = 10 ,
179+ msg = f"dM/dt != 0 for state { state } " )
180+
181+ def test_x_output_uses_drum_state (self ):
182+ """x_1 output must reflect drum state, not feed composition."""
183+ F = FlashDrum (holdup = 100.0 , N0 = [90.0 , 10.0 ])
184+ F .set_solver (EUF , parent = None )
185+
186+ F .inputs [0 ] = 10.0
187+ F .inputs [1 ] = 0.3 # different from drum
188+ F .inputs [2 ] = 360.0
189+ F .inputs [3 ] = 101325.0
190+
191+ F .update (None )
192+ # drum state x_1 = 90/100 = 0.9
193+ self .assertAlmostEqual (F .outputs [3 ], 0.9 , places = 8 )
194+
139195
140196# RUN TESTS LOCALLY ====================================================================
141197
0 commit comments