Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions contrib/settle_amp_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@
current. Not rate-limited, and judged on the higher of the fused theta
and the present stretch's own reading: the cable soaks through a hot
session, so earlier readings are of a cooler cable.
- **Up** only when I* (at ``k_up``) is at least 1 A over, the present
stretch is at least a time constant old, the handle has margin, and a
time constant has passed since the last move. A step-up undone within a
time constant widens ``k_up`` for the rest of the session — it never
gives up and pins the session low.
- **Up** only when I* (at ``k_up``, on that same higher reading, so a
step-up is never to a current the down rule would refuse) is at least
1 A over, the present stretch is at least a time constant old, the handle
has margin, and a time constant has passed since the last move. A step-up
undone within a time constant widens ``k_up`` for the rest of the session
— it never gives up and pins the session low.
- **Emergency:** handle at ``emergency_c`` and still rising (against itself
two minutes ago, not the last tick) — cut 2 A below the present current,
or below I* if that is lower, whatever the estimate says.
Expand Down Expand Up @@ -187,14 +188,19 @@ def decide(thermal: dict, state: State, cfg: Config) -> tuple[Action, State, str
# present stretch's own reading once it is half a time constant old.
# (Whole-history backtest: without this, hot days landed 3-4 A high and
# walked down 1 A at a time into the trip — seven sessions.)
# Stepping *up* is judged on the same number. On the fused estimate
# alone, a prior well under the cable's real temperature offered an amp
# that the live reading took back on the next tick (a 1 A step does not
# start a new stretch): with k_up >= k_down, I* up can now never exceed
# I* down.
theta_down = theta
if st.live is not None and age_s >= cfg.live_down_tau * tau_s:
theta_down = max(theta, st.live[0])

cur = st.cap if st.cap is not None else cfg.normal_amps
k_up = cfg.k_up + st.k_extra
i_down = _amps_for(theta_down, cfg.k_down * sd, model, cfg)
i_up = _amps_for(theta, k_up * sd, model, cfg)
i_up = _amps_for(theta_down, k_up * sd, model, cfg)
rising = False
if isinstance(handle, (int, float)):
log = [(ts, value) for ts, value in st.handle_log if now - ts <= cfg.rising_window_s + 45.0] + [(now, handle)]
Expand Down
10 changes: 10 additions & 0 deletions tests/test_settle_amp_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ def test_front_load_drops_once_to_the_target_when_the_handle_nears_the_plateau()
assert peak < MODEL["trip_c"] - sac.Config().soak_margin_c


def test_a_cool_prior_does_not_buy_an_amp_the_live_reading_takes_back():
# The same cable, but the ambient prior reads 8 C under it. The fused
# estimate sits between the two; judged on it alone, the step-up rule
# offered 43 A at minute 24 and the step-down rule, on the stretch's own
# reading, took it back 30 s later.
moves, _peak, state = _closed_loop(theta=33.0, prior=25.0, minutes=120)
assert [(kind, amps) for _minute, kind, amps in moves] == [("cap", 42.0)]
assert state.k_extra == 0.0 # no reversal to back off from


def test_a_long_stretch_is_one_reading_not_one_per_tick():
# 40 minutes at one current: the stretch must never be banked as finished.
ticks = [(30.0 * i, 50.0, 48.7, 60.0) for i in range(1, 81)]
Expand Down
Loading