Loading src/lib.rs +1 −3 Original line number Diff line number Diff line Loading @@ -308,7 +308,6 @@ fn simulate_timestep( let mut head; let mut release; let mut target_release; let mut effective_power; let mut spill; for _ in 0..MAX_ITER { Loading @@ -322,7 +321,6 @@ fn simulate_timestep( &reservoir.hpf_q, ); target_release = release_result.release; effective_power = release_result.effective_power; release = if below_power_pool { 0.0 Loading Loading @@ -367,7 +365,7 @@ fn simulate_timestep( &reservoir.hpf_q, ); target_release = release_result.release; effective_power = release_result.effective_power; let effective_power = release_result.effective_power; release = if below_power_pool { 0.0 Loading tests/test_dynamic_tailwater.py +66 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,30 @@ import pandas as pd import pytest import powersheds from helpers import make_reservoir, make_cascade, run_single_reservoir from helpers import ( DEFAULT_HPF_H, DEFAULT_HPF_P, DEFAULT_HPF_Q, make_reservoir, make_cascade, run_single_reservoir, ) def _interp_linear(x, xs, ys): """Simple clamped linear interpolation mirroring the Rust helper.""" if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1] for i in range(len(xs) - 1): if xs[i] <= x <= xs[i + 1]: x1, x2 = xs[i], xs[i + 1] y1, y2 = ys[i], ys[i + 1] return y1 + (x - x1) * (y2 - y1) / (x2 - x1) return ys[0] # ============================================================================= Loading Loading @@ -140,6 +163,48 @@ class TestConvergence: f"NaN in tailwater_elevation at t={t}" ) def test_tailwater_uses_converged_outflow_not_first_pass_guess(self): """Tailwater should reflect the converged fixed point, not a single pass. With a steep TW curve and high target power, the first-pass TW estimate from the initial head differs materially from the converged TW. This test fails if the model stops after the initial guess instead of iterating release/head/TW to consistency. """ tw_outflow = [0.0, 100.0, 200.0] tw_elev = [140.0, 155.0, 170.0] target_power = 100.0 result = run_single_reservoir( n_hours=1, capacity=1000.0, # avoid spill so TW depends only on release catchment_inflow=[10.0], target_power=[target_power], max_release=[100.0], set_tw_outflow=tw_outflow, set_tw_elevation=tw_elev, ) initial_tw = _interp_linear(0.0, tw_outflow, tw_elev) initial_head = 200.0 - initial_tw first_pass_release = powersheds.compute_release( target_power, initial_head, DEFAULT_HPF_H, DEFAULT_HPF_P, DEFAULT_HPF_Q, ) first_pass_outflow_cumecs = first_pass_release * 1_000_000.0 / 3600.0 first_pass_tw = _interp_linear(first_pass_outflow_cumecs, tw_outflow, tw_elev) final_release = result["release"][0] final_tw = result["tailwater_elevation"][0] final_outflow_cumecs = final_release * 1_000_000.0 / 3600.0 fixed_point_tw = _interp_linear(final_outflow_cumecs, tw_outflow, tw_elev) assert final_tw == pytest.approx(fixed_point_tw, abs=0.2) assert abs(final_tw - first_pass_tw) > 1.0 # ============================================================================= # Mass balance preserved Loading Loading
src/lib.rs +1 −3 Original line number Diff line number Diff line Loading @@ -308,7 +308,6 @@ fn simulate_timestep( let mut head; let mut release; let mut target_release; let mut effective_power; let mut spill; for _ in 0..MAX_ITER { Loading @@ -322,7 +321,6 @@ fn simulate_timestep( &reservoir.hpf_q, ); target_release = release_result.release; effective_power = release_result.effective_power; release = if below_power_pool { 0.0 Loading Loading @@ -367,7 +365,7 @@ fn simulate_timestep( &reservoir.hpf_q, ); target_release = release_result.release; effective_power = release_result.effective_power; let effective_power = release_result.effective_power; release = if below_power_pool { 0.0 Loading
tests/test_dynamic_tailwater.py +66 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,30 @@ import pandas as pd import pytest import powersheds from helpers import make_reservoir, make_cascade, run_single_reservoir from helpers import ( DEFAULT_HPF_H, DEFAULT_HPF_P, DEFAULT_HPF_Q, make_reservoir, make_cascade, run_single_reservoir, ) def _interp_linear(x, xs, ys): """Simple clamped linear interpolation mirroring the Rust helper.""" if x <= xs[0]: return ys[0] if x >= xs[-1]: return ys[-1] for i in range(len(xs) - 1): if xs[i] <= x <= xs[i + 1]: x1, x2 = xs[i], xs[i + 1] y1, y2 = ys[i], ys[i + 1] return y1 + (x - x1) * (y2 - y1) / (x2 - x1) return ys[0] # ============================================================================= Loading Loading @@ -140,6 +163,48 @@ class TestConvergence: f"NaN in tailwater_elevation at t={t}" ) def test_tailwater_uses_converged_outflow_not_first_pass_guess(self): """Tailwater should reflect the converged fixed point, not a single pass. With a steep TW curve and high target power, the first-pass TW estimate from the initial head differs materially from the converged TW. This test fails if the model stops after the initial guess instead of iterating release/head/TW to consistency. """ tw_outflow = [0.0, 100.0, 200.0] tw_elev = [140.0, 155.0, 170.0] target_power = 100.0 result = run_single_reservoir( n_hours=1, capacity=1000.0, # avoid spill so TW depends only on release catchment_inflow=[10.0], target_power=[target_power], max_release=[100.0], set_tw_outflow=tw_outflow, set_tw_elevation=tw_elev, ) initial_tw = _interp_linear(0.0, tw_outflow, tw_elev) initial_head = 200.0 - initial_tw first_pass_release = powersheds.compute_release( target_power, initial_head, DEFAULT_HPF_H, DEFAULT_HPF_P, DEFAULT_HPF_Q, ) first_pass_outflow_cumecs = first_pass_release * 1_000_000.0 / 3600.0 first_pass_tw = _interp_linear(first_pass_outflow_cumecs, tw_outflow, tw_elev) final_release = result["release"][0] final_tw = result["tailwater_elevation"][0] final_outflow_cumecs = final_release * 1_000_000.0 / 3600.0 fixed_point_tw = _interp_linear(final_outflow_cumecs, tw_outflow, tw_elev) assert final_tw == pytest.approx(fixed_point_tw, abs=0.2) assert abs(final_tw - first_pass_tw) > 1.0 # ============================================================================= # Mass balance preserved Loading