Add a simulation-based Sud forecast, computed server-side
New components/sud_forecast.py: SudForecastEstimator simulates a whole
schedule with the same kind of plant/controller (and the same
configured params - ambient, plant params, pid_type, TempCtrl gains,
heater max power) the server uses for real, by driving a throwaway
Sud/Pot/controller trio through it exactly as tasks/sud.py's SudTask
would. It's pure CPU-bound iteration (no real time/IO), so a multi-
hour brew simulates in well under a second.
tasks/sud.py: SudTask now takes an optional forecast_estimator and
sends its result ({'Forecast': {'T': ..., 'Theta': ...}}) on the Sud
channel after every successful Save/Load, run in a worker thread via
run_in_executor so the ~100-500ms simulation doesn't stall the other
tasks. server/brewpi.py constructs one with the real server's config
and wires it in; AmbientTemp changes update it too.
client/brewpi_gui.py: the quick naive show_schedule() estimate (drawn
immediately on Load, before the server's simulation finishes) is now
superseded by show_precomputed_course() once the Forecast message
arrives - only while not actively running, so it doesn't fight the
dynamic re-anchored view.
Verified: matches a standalone run of the estimator (177 min for
sude/sud_0010.json) and replaces the old naive 164 min estimate live
within a couple seconds of loading.
This commit is contained in:
+23
-2
@@ -11,7 +11,8 @@ TEMP_REACHED_TOLERANCE = 0.2
|
||||
|
||||
|
||||
class SudTask(ATask):
|
||||
def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, dt, interval, msg_handler: MsgIo):
|
||||
def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, dt, interval, msg_handler: MsgIo,
|
||||
forecast_estimator=None):
|
||||
ATask.__init__(self, interval)
|
||||
self.sud = sud
|
||||
self.tc = tc
|
||||
@@ -25,6 +26,12 @@ class SudTask(ATask):
|
||||
# the same speedup instead of running in real time.
|
||||
self.dt = dt
|
||||
self.msg_handler = msg_handler
|
||||
# Predicts a schedule's actual duration by simulating it with the
|
||||
# same plant/controller machinery this server uses for real - see
|
||||
# components/sud_forecast.py. Optional only so tests/demos that
|
||||
# build a SudTask without one still work; the server always passes
|
||||
# one.
|
||||
self.forecast_estimator = forecast_estimator
|
||||
msg_handler.set_recv_handler(self.recv)
|
||||
|
||||
def apply_plant_params(self, step):
|
||||
@@ -99,6 +106,17 @@ class SudTask(ATask):
|
||||
def on_hold_remaining_changed(self, value):
|
||||
asyncio.create_task(self.send({'HoldRemaining': value}))
|
||||
|
||||
async def send_forecast(self, doc):
|
||||
if self.forecast_estimator is None:
|
||||
return
|
||||
# Runs the simulation in a worker thread - it's CPU-bound and can
|
||||
# take a couple hundred ms for a long schedule, which would
|
||||
# otherwise stall every other task (heater, sensor, ...) for that
|
||||
# whole window.
|
||||
loop = asyncio.get_event_loop()
|
||||
t, theta = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc)
|
||||
await self.send({'Forecast': {'T': t, 'Theta': theta}})
|
||||
|
||||
async def recv(self, data):
|
||||
for pair in data.items():
|
||||
if 'Start' in pair[0]:
|
||||
@@ -110,11 +128,14 @@ class SudTask(ATask):
|
||||
elif 'Stop' in pair[0]:
|
||||
self.sud.stop()
|
||||
elif 'Save' in pair[0]:
|
||||
await self.send({'Json': self.sud.save()})
|
||||
doc = self.sud.save()
|
||||
await self.send({'Json': doc})
|
||||
await self.send_forecast(doc)
|
||||
elif 'Load' in pair[0]:
|
||||
if self.sud.load(pair[1]):
|
||||
await self.send({'Name': self.sud.name, 'Description': self.sud.description})
|
||||
await self.send({'Json': pair[1]})
|
||||
await self.send_forecast(pair[1])
|
||||
|
||||
async def send(self, data):
|
||||
await self.msg_handler.send(data)
|
||||
|
||||
Reference in New Issue
Block a user