Generalize Sud forecast reanchoring to every step boundary
Reanchoring used to only happen when a user confirmed a WAIT_USER step, so anything the schedule advanced through on its own (a ramp reaching target, a hold timing out) left the forecast showing a stale, increasingly wrong prediction once reality diverged from it. _reanchor_forecast() now fires from on_step_changed() on every real transition, splicing in a fresh simulation anchored at the real current temperature/elapsed time instead. Also fixes two bugs surfaced while testing that change: - estimate()'s early-return for an empty/not-yet-loaded schedule still returned the old 4-tuple shape, crashing every connection before a Sud was ever Loaded. - send_forecast() and _reanchor_forecast() can run concurrently (e.g. a fresh Start triggers both at once), and whichever resumed second after its own worker-thread simulation would blindly splice its tail onto whatever the other had already written, producing a spurious connecting line across the plot. Both now carry a generation counter and discard their result if a newer call has since committed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qvu5giu7gvRCyEWzf2Vpx
This commit is contained in:
@@ -129,10 +129,10 @@ class SudForecastPlot(FigureCanvasQTAgg):
|
|||||||
requiring user confirmation - a human's response time genuinely
|
requiring user confirmation - a human's response time genuinely
|
||||||
can't be forecast, so it's modeled as a zero-delay auto-confirm
|
can't be forecast, so it's modeled as a zero-delay auto-confirm
|
||||||
there instead of stopping. That assumption gets corrected once the
|
there instead of stopping. That assumption gets corrected once the
|
||||||
real confirmation actually happens (anchored at the real elapsed
|
schedule actually reaches the next real step boundary (anchored at
|
||||||
time and temperature) - see tasks/sud.py's SudTask.
|
the real elapsed time and temperature) - see tasks/sud.py's
|
||||||
_continue_forecast_after_confirm() - at which point show_forecast()
|
SudTask._reanchor_forecast() - at which point show_forecast() is
|
||||||
is called again with the corrected curve, replacing the optimistic
|
called again with the corrected curve, replacing the optimistic
|
||||||
guess outright rather than the GUI patching it up itself."""
|
guess outright rather than the GUI patching it up itself."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -894,9 +894,9 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
# Computed asynchronously server-side and can arrive slightly
|
# Computed asynchronously server-side and can arrive slightly
|
||||||
# after the schedule itself - applies regardless of whether a
|
# after the schedule itself - applies regardless of whether a
|
||||||
# run is in progress, since this can also be a later piecewise
|
# run is in progress, since this can also be a later piecewise
|
||||||
# segment appended after a real user confirmation (see
|
# segment appended after a real step transition (see tasks/
|
||||||
# tasks/sud.py's SudTask._continue_forecast_after_confirm()), not
|
# sud.py's SudTask._reanchor_forecast()), not just the initial
|
||||||
# just the initial one computed at Load.
|
# one computed at Load.
|
||||||
if self.sud_empty:
|
if self.sud_empty:
|
||||||
return
|
return
|
||||||
t_min = [seconds / 60.0 for seconds in forecast['T']]
|
t_min = [seconds / 60.0 for seconds in forecast['T']]
|
||||||
|
|||||||
+13
-16
@@ -66,23 +66,22 @@ class SudForecastEstimator:
|
|||||||
self.theta_amb = theta_amb
|
self.theta_amb = theta_amb
|
||||||
|
|
||||||
def estimate(self, doc, start_theta=None):
|
def estimate(self, doc, start_theta=None):
|
||||||
"""Returns (t, theta, final_state, confirm_points): t/theta are
|
"""Returns (t, theta, final_state): t/theta are parallel lists of
|
||||||
parallel lists of elapsed simulated seconds and temperature,
|
elapsed simulated seconds and temperature, covering doc['steps']
|
||||||
covering doc['steps'] from the start all the way to the end
|
from the start all the way to the end (final_state is
|
||||||
(final_state is SudState.DONE), or, in the pathological case of
|
SudState.DONE), or, in the pathological case of a step whose
|
||||||
a step whose target can never actually be reached, wherever
|
target can never actually be reached, wherever MAX_TICKS cut the
|
||||||
MAX_TICKS cut the simulation off.
|
simulation off.
|
||||||
|
|
||||||
A step requiring user confirmation doesn't stop the simulation
|
A step requiring user confirmation doesn't stop the simulation
|
||||||
either - a human's response time genuinely can't be forecast,
|
either - a human's response time genuinely can't be forecast,
|
||||||
so it's modeled as zero delay (auto-confirmed the instant that
|
so it's modeled as zero delay (auto-confirmed the instant that
|
||||||
step's hold completes) rather than leaving the estimate stuck
|
step's hold completes) rather than leaving the estimate stuck
|
||||||
there forever. confirm_points records every place that
|
there forever. That optimistic guess gets corrected for real once
|
||||||
assumption was made, as (step_index, t) pairs, so the caller
|
the caller (tasks/sud.py's SudTask) sees the schedule actually
|
||||||
(tasks/sud.py's SudTask) can correct it once a real
|
reach the next step boundary - see SudTask._reanchor_forecast(),
|
||||||
confirmation actually happens: truncate the forecast at that
|
called from on_step_changed() on every real transition, not just
|
||||||
point and splice in a freshly anchored simulation of the
|
confirmations.
|
||||||
remaining steps in place of the optimistic guess.
|
|
||||||
|
|
||||||
start_theta defaults to the configured ambient temperature - i.e.
|
start_theta defaults to the configured ambient temperature - i.e.
|
||||||
a cold start, same as the GUI's static estimate."""
|
a cold start, same as the GUI's static estimate."""
|
||||||
@@ -91,7 +90,7 @@ class SudForecastEstimator:
|
|||||||
|
|
||||||
sud = Sud()
|
sud = Sud()
|
||||||
if not sud.load(doc) or not sud.schedule:
|
if not sud.load(doc) or not sud.schedule:
|
||||||
return [0.0], [start_theta], SudState.DONE, []
|
return [0.0], [start_theta], SudState.DONE
|
||||||
|
|
||||||
# Plant params (M/C/L/Td) are deliberately *not* seeded here from
|
# Plant params (M/C/L/Td) are deliberately *not* seeded here from
|
||||||
# any default - sud.start() below synchronously fires the first
|
# any default - sud.start() below synchronously fires the first
|
||||||
@@ -157,13 +156,11 @@ class SudForecastEstimator:
|
|||||||
|
|
||||||
t = [0.0]
|
t = [0.0]
|
||||||
theta = [pot.get_temperature()]
|
theta = [pot.get_temperature()]
|
||||||
confirm_points = []
|
|
||||||
|
|
||||||
sud.start()
|
sud.start()
|
||||||
ticks = 0
|
ticks = 0
|
||||||
while sud.state != SudState.DONE and ticks < MAX_TICKS:
|
while sud.state != SudState.DONE and ticks < MAX_TICKS:
|
||||||
if sud.state == SudState.WAIT_USER:
|
if sud.state == SudState.WAIT_USER:
|
||||||
confirm_points.append((sud.index, t[-1]))
|
|
||||||
sud.confirm()
|
sud.confirm()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -184,4 +181,4 @@ class SudForecastEstimator:
|
|||||||
theta.append(pot.get_temperature())
|
theta.append(pot.get_temperature())
|
||||||
ticks += 1
|
ticks += 1
|
||||||
|
|
||||||
return t, theta, sud.state, confirm_points
|
return t, theta, sud.state
|
||||||
|
|||||||
+102
-78
@@ -59,13 +59,13 @@ class SudTask(ATask):
|
|||||||
# one.
|
# one.
|
||||||
self.forecast_estimator = forecast_estimator
|
self.forecast_estimator = forecast_estimator
|
||||||
# The forecast as last computed/corrected (see send_forecast()/
|
# The forecast as last computed/corrected (see send_forecast()/
|
||||||
# _continue_forecast_after_confirm()) - T in simulated seconds,
|
# _reanchor_forecast()) - T in simulated seconds, Theta in degrees,
|
||||||
# Theta in degrees, parallel lists, both monotonically growing as
|
# parallel lists, both monotonically growing as corrections get
|
||||||
# corrections get spliced in. Covers the whole schedule from the
|
# spliced in. Covers the whole schedule from the very first Load,
|
||||||
# very first Load, including through steps requiring user
|
# including through steps requiring user confirmation - those are
|
||||||
# confirmation - those are simulated as a zero-delay auto-confirm
|
# simulated as a zero-delay auto-confirm (see components/
|
||||||
# (see components/sud_forecast.py's SudForecastEstimator.
|
# sud_forecast.py's SudForecastEstimator.estimate()) rather than
|
||||||
# estimate()) rather than left unforecast.
|
# left unforecast, until _reanchor_forecast() corrects it for real.
|
||||||
self.forecast_t = []
|
self.forecast_t = []
|
||||||
self.forecast_theta = []
|
self.forecast_theta = []
|
||||||
# Whether the forecast above actually reaches the schedule's real
|
# Whether the forecast above actually reaches the schedule's real
|
||||||
@@ -73,14 +73,13 @@ class SudTask(ATask):
|
|||||||
# of a step whose target can never be reached (see
|
# of a step whose target can never be reached (see
|
||||||
# components/sud_forecast.py's MAX_TICKS).
|
# components/sud_forecast.py's MAX_TICKS).
|
||||||
self.forecast_finished = True
|
self.forecast_finished = True
|
||||||
# Where each user-confirmation step's zero-delay assumption sits
|
# Bumped by every call to send_forecast()/_reanchor_forecast() -
|
||||||
# in the forecast timeline above, keyed by the schedule's
|
# see either's own comment for why: it lets a call that's still
|
||||||
# (absolute) step index - see SudForecastEstimator.estimate()'s
|
# awaiting its worker-thread simulation tell, once it resumes,
|
||||||
# confirm_points. Consulted by _continue_forecast_after_confirm()
|
# whether a newer call has since started and already committed a
|
||||||
# to know where to cut the optimistic guess loose and splice in a
|
# fresher result - if so, it discards its own rather than
|
||||||
# freshly anchored simulation once that confirmation actually
|
# corrupting forecast_t/forecast_theta with stale data.
|
||||||
# happens for real.
|
self._forecast_generation = 0
|
||||||
self.forecast_confirm_marks = {}
|
|
||||||
msg_handler.set_recv_handler(self.recv)
|
msg_handler.set_recv_handler(self.recv)
|
||||||
|
|
||||||
def apply_plant_params(self, grain_mass, water_mass):
|
def apply_plant_params(self, grain_mass, water_mass):
|
||||||
@@ -131,6 +130,14 @@ class SudTask(ATask):
|
|||||||
self.tc.set_theta_soll(step['temperature'])
|
self.tc.set_theta_soll(step['temperature'])
|
||||||
self.tc.set_heatrate_soll(ramp['rate'])
|
self.tc.set_heatrate_soll(ramp['rate'])
|
||||||
self.apply_stirrer(phase)
|
self.apply_stirrer(phase)
|
||||||
|
# Every real step boundary (full step change or ramp->hold
|
||||||
|
# within one) is a trustworthy checkpoint to correct the
|
||||||
|
# forecast against - see _reanchor_forecast(). Catches drift
|
||||||
|
# from anything the original simulation couldn't have known
|
||||||
|
# (a malt fill-in's actual cooldown, a longer/shorter ramp than
|
||||||
|
# modeled, ...) at the next opportunity, not just at the next
|
||||||
|
# user confirmation.
|
||||||
|
asyncio.create_task(self._reanchor_forecast())
|
||||||
|
|
||||||
asyncio.create_task(self.send({'Step': {
|
asyncio.create_task(self.send({'Step': {
|
||||||
'Index': self.sud.index,
|
'Index': self.sud.index,
|
||||||
@@ -184,31 +191,46 @@ class SudTask(ATask):
|
|||||||
actually will, never lining up with the actual trace even at
|
actually will, never lining up with the actual trace even at
|
||||||
t=0. Deliberately the raw sensor reading (theta_ist_set), not
|
t=0. Deliberately the raw sensor reading (theta_ist_set), not
|
||||||
the controller's own get_theta_ist() (theta_ist) -
|
the controller's own get_theta_ist() (theta_ist) -
|
||||||
_continue_forecast_after_confirm() can trust that one because a
|
_reanchor_forecast() can trust that one because a real run has
|
||||||
real run has been actively ticking for a while by the time it
|
been actively ticking for a while by the time it runs, but this
|
||||||
runs, but this is called right after Load, before this
|
is called right after Load, before this controller may have
|
||||||
controller may have processed even a single tick yet (e.g. its
|
processed even a single tick yet (e.g. its plant params/model
|
||||||
plant params/model only just got configured - see SudTask.
|
only just got configured - see SudTask.recv()), so theta_ist
|
||||||
recv()), so theta_ist itself could still be sitting at its
|
itself could still be sitting at its never-updated __init__
|
||||||
never-updated __init__ default.
|
default.
|
||||||
|
|
||||||
Those zero-delay assumptions get corrected piecewise as real
|
Those zero-delay assumptions get corrected piecewise as the
|
||||||
confirmations actually happen - see
|
schedule actually reaches each step boundary - see
|
||||||
_continue_forecast_after_confirm()."""
|
_reanchor_forecast()."""
|
||||||
if self.forecast_estimator is None:
|
if self.forecast_estimator is None:
|
||||||
return
|
return
|
||||||
|
# Both this and _reanchor_forecast() can end up running
|
||||||
|
# concurrently - e.g. a fresh Start triggers this explicitly *and*
|
||||||
|
# (via Sud.start() synchronously firing on_step_changed() for the
|
||||||
|
# first step) a _reanchor_forecast() of its own; a step whose hold
|
||||||
|
# duration is already 0 can likewise advance twice within a single
|
||||||
|
# tick, firing on_step_changed() twice back to back. Each such call
|
||||||
|
# awaits a worker-thread simulation, so without this guard whichever
|
||||||
|
# one resumes second would blindly splice its own tail onto
|
||||||
|
# whatever the other already finished writing, producing a
|
||||||
|
# spurious connecting line across the plot. Bumping/checking this
|
||||||
|
# generation counter across the await ensures only the very latest
|
||||||
|
# call's result is ever committed - any older one discards itself.
|
||||||
|
self._forecast_generation += 1
|
||||||
|
generation = self._forecast_generation
|
||||||
# Runs the simulation in a worker thread - it's CPU-bound and can
|
# Runs the simulation in a worker thread - it's CPU-bound and can
|
||||||
# take a couple hundred ms for a long schedule, which would
|
# take a couple hundred ms for a long schedule, which would
|
||||||
# otherwise stall every other task (heater, sensor, ...) for that
|
# otherwise stall every other task (heater, sensor, ...) for that
|
||||||
# whole window.
|
# whole window.
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
start_theta = self.tc.get_theta_ist_set()
|
start_theta = self.tc.get_theta_ist_set()
|
||||||
t, theta, final_state, confirm_points = await loop.run_in_executor(
|
t, theta, final_state = await loop.run_in_executor(
|
||||||
None, self.forecast_estimator.estimate, doc, start_theta)
|
None, self.forecast_estimator.estimate, doc, start_theta)
|
||||||
|
if generation != self._forecast_generation:
|
||||||
|
return
|
||||||
self.forecast_t = t
|
self.forecast_t = t
|
||||||
self.forecast_theta = theta
|
self.forecast_theta = theta
|
||||||
self.forecast_finished = (final_state == SudState.DONE)
|
self.forecast_finished = (final_state == SudState.DONE)
|
||||||
self.forecast_confirm_marks = dict(confirm_points)
|
|
||||||
await self._send_forecast()
|
await self._send_forecast()
|
||||||
|
|
||||||
async def _send_forecast(self):
|
async def _send_forecast(self):
|
||||||
@@ -219,43 +241,49 @@ class SudTask(ATask):
|
|||||||
'Finished': self.forecast_finished,
|
'Finished': self.forecast_finished,
|
||||||
}})
|
}})
|
||||||
|
|
||||||
async def _continue_forecast_after_confirm(self, confirmed_index, confirmed_target):
|
async def _reanchor_forecast(self):
|
||||||
"""Corrects the optimistic, zero-delay guess send_forecast() (or a
|
"""Corrects the optimistic, zero-delay guesses baked into the
|
||||||
previous call to this method) made at confirmed_index's
|
forecast (see SudForecastEstimator.estimate()'s docstring) now
|
||||||
user-confirmation step, now that the confirmation has actually
|
that the schedule has actually reached a real step boundary -
|
||||||
happened for real - see SudForecastEstimator.estimate()'s
|
called from on_step_changed() on every transition, whether it's
|
||||||
docstring for why that assumption can't just be trusted as-is.
|
a user confirmation or fully automatic (e.g. a ramp reaching its
|
||||||
Truncates the forecast right back to that point and splices in a
|
target, or a hold's duration running out). Truncates the forecast
|
||||||
freshly anchored simulation of the rest of the schedule, anchored
|
back to right now and splices in a freshly anchored simulation of
|
||||||
at the real elapsed time (self.sud.elapsed) - so an actual delay
|
the rest of the schedule, anchored at the real elapsed time
|
||||||
shows up honestly as a gap rather than as the assumed zero - and
|
(self.sud.elapsed) and the real current temperature
|
||||||
the real current temperature (more trustworthy than whatever the
|
(self.tc.get_theta_ist()) - so any divergence the original
|
||||||
now-superseded guess predicted it would be by now).
|
simulation couldn't have predicted (a malt fill-in's actual
|
||||||
|
cooldown, a longer/shorter ramp than modeled, ...) gets corrected
|
||||||
|
at the next opportunity instead of leaving the forecast stuck
|
||||||
|
showing what was once guessed.
|
||||||
|
|
||||||
confirmed_target is the real controller's theta_soll_set at the
|
No-op if the estimator isn't configured.
|
||||||
moment of confirmation (captured by recv() *before* calling
|
|
||||||
Sud.confirm(), since that synchronously pushes the next step's
|
|
||||||
own target onto it) - used to fill the real-world wait itself
|
|
||||||
(see below).
|
|
||||||
|
|
||||||
No-op if confirmed_index was never part of a computed forecast in
|
Guards against the same concurrent-call race send_forecast() does
|
||||||
the first place (e.g. the estimator isn't configured)."""
|
(see its own comment) by working off local copies of the forecast
|
||||||
|
lists throughout, only ever committing them to self.forecast_t/
|
||||||
|
forecast_theta right at the end, and only if this is still the
|
||||||
|
latest call by then - an older call resuming after a newer one
|
||||||
|
has already committed must discard its own (now-stale) result
|
||||||
|
rather than splice it onto what the newer call already wrote."""
|
||||||
if self.forecast_estimator is None:
|
if self.forecast_estimator is None:
|
||||||
return
|
return
|
||||||
mark_t = self.forecast_confirm_marks.pop(confirmed_index, None)
|
self._forecast_generation += 1
|
||||||
if mark_t is None:
|
generation = self._forecast_generation
|
||||||
return
|
real_elapsed = self.sud.elapsed
|
||||||
# Drop the now-stale tail (everything beyond the confirmation
|
# Drop the now-stale tail (everything beyond right now) - it's
|
||||||
# point) - both the curve itself and any confirm marks that fell
|
# about to be replaced by a freshly anchored simulation.
|
||||||
# within it, since they're about to be replaced by fresh ones.
|
cut = bisect.bisect_right(self.forecast_t, real_elapsed)
|
||||||
cut = bisect.bisect_right(self.forecast_t, mark_t)
|
forecast_t = self.forecast_t[:cut]
|
||||||
self.forecast_t = self.forecast_t[:cut]
|
forecast_theta = self.forecast_theta[:cut]
|
||||||
self.forecast_theta = self.forecast_theta[:cut]
|
|
||||||
self.forecast_confirm_marks = {idx: t for idx, t in self.forecast_confirm_marks.items() if t <= mark_t}
|
|
||||||
|
|
||||||
schedule = self.sud.schedule
|
schedule = self.sud.schedule
|
||||||
index = self.sud.index
|
index = self.sud.index
|
||||||
if not (0 <= index < len(schedule)):
|
if not (0 <= index < len(schedule)):
|
||||||
|
if generation != self._forecast_generation:
|
||||||
|
return
|
||||||
|
self.forecast_t = forecast_t
|
||||||
|
self.forecast_theta = forecast_theta
|
||||||
self.forecast_finished = True
|
self.forecast_finished = True
|
||||||
await self._send_forecast()
|
await self._send_forecast()
|
||||||
return
|
return
|
||||||
@@ -271,26 +299,22 @@ class SudTask(ATask):
|
|||||||
'steps': schedule[index:],
|
'steps': schedule[index:],
|
||||||
}
|
}
|
||||||
start_theta = self.tc.get_theta_ist()
|
start_theta = self.tc.get_theta_ist()
|
||||||
real_elapsed = self.sud.elapsed
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
t, theta, final_state, confirm_points = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc, start_theta)
|
t, theta, final_state = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc, start_theta)
|
||||||
# Bridge the gap between the confirmation point and the real
|
if generation != self._forecast_generation:
|
||||||
# elapsed time it actually happened at - the real controller
|
return
|
||||||
# stays enabled and actively holding through WAIT_USER, so
|
# Bridge any gap between the last surviving old point and right
|
||||||
# confirmed_target (its setpoint at the time) is what it was
|
# now (e.g. the old forecast's timeline had already drifted
|
||||||
# actually converging toward during the wait, not whatever
|
# behind real_elapsed) with the same real measurement the fresh
|
||||||
# (possibly still mid-ramp) value the forecast happened to log
|
# simulation below starts from, so the spliced curve doesn't
|
||||||
# the instant WAIT_USER tripped.
|
# visibly jump.
|
||||||
if self.forecast_t and self.forecast_t[-1] < real_elapsed:
|
if forecast_t and forecast_t[-1] < real_elapsed:
|
||||||
self.forecast_t.append(real_elapsed)
|
forecast_t.append(real_elapsed)
|
||||||
self.forecast_theta.append(confirmed_target)
|
forecast_theta.append(start_theta)
|
||||||
self.forecast_t.extend(real_elapsed + seconds for seconds in t)
|
forecast_t.extend(real_elapsed + seconds for seconds in t)
|
||||||
self.forecast_theta.extend(theta)
|
forecast_theta.extend(theta)
|
||||||
# confirm_points' indices/times are relative to this sub-schedule
|
self.forecast_t = forecast_t
|
||||||
# (starting fresh at doc['steps'][0]) - rebase both onto the real
|
self.forecast_theta = forecast_theta
|
||||||
# schedule's absolute indices and the master forecast timeline.
|
|
||||||
self.forecast_confirm_marks.update(
|
|
||||||
(index + local_index, real_elapsed + local_t) for local_index, local_t in confirm_points)
|
|
||||||
self.forecast_finished = (final_state == SudState.DONE)
|
self.forecast_finished = (final_state == SudState.DONE)
|
||||||
await self._send_forecast()
|
await self._send_forecast()
|
||||||
|
|
||||||
@@ -309,10 +333,10 @@ class SudTask(ATask):
|
|||||||
if fresh_start:
|
if fresh_start:
|
||||||
asyncio.create_task(self.send_forecast(self.sud.save()))
|
asyncio.create_task(self.send_forecast(self.sud.save()))
|
||||||
elif 'Confirm' in pair[0]:
|
elif 'Confirm' in pair[0]:
|
||||||
confirmed_index = self.sud.index
|
# Sud.confirm() synchronously fires on_step_changed() for
|
||||||
confirmed_target = self.tc.get_theta_soll_set()
|
# the now-current step, which schedules the forecast
|
||||||
|
# reanchor itself - see _reanchor_forecast().
|
||||||
self.sud.confirm()
|
self.sud.confirm()
|
||||||
asyncio.create_task(self._continue_forecast_after_confirm(confirmed_index, confirmed_target))
|
|
||||||
elif 'Pause' in pair[0]:
|
elif 'Pause' in pair[0]:
|
||||||
self.sud.pause()
|
self.sud.pause()
|
||||||
elif 'Stop' in pair[0]:
|
elif 'Stop' in pair[0]:
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@ class SudLogTask(ATask):
|
|||||||
is of no use before the run is over anyway. The forecast file is
|
is of no use before the run is over anyway. The forecast file is
|
||||||
written from SudTask.forecast_t/forecast_theta as they stand at
|
written from SudTask.forecast_t/forecast_theta as they stand at
|
||||||
that point - the *final*, most-corrected version (see tasks/sud.py's
|
that point - the *final*, most-corrected version (see tasks/sud.py's
|
||||||
SudTask._continue_forecast_after_confirm()), not the optimistic
|
SudTask._reanchor_forecast()), not the optimistic guess computed
|
||||||
guess computed back at Load.
|
back at Load.
|
||||||
|
|
||||||
Samples are taken once per interval regardless of Sud.state
|
Samples are taken once per interval regardless of Sud.state
|
||||||
(RAMPING/HOLDING/WAIT_USER/PAUSED all keep recording, mirroring
|
(RAMPING/HOLDING/WAIT_USER/PAUSED all keep recording, mirroring
|
||||||
|
|||||||
Reference in New Issue
Block a user