Add a comprehensive mash-schedule demo
Drives a real Smith-predictor TempController, Pot, HeaterSim, and StirrerSim through sude/sud_0010.json end-to-end (mirroring brewpi.py's wiring, including the model/plant power-discretization mismatch and tasks/sud.py's stirrer-switching rules), auto-confirming waitForUser pauses so it completes unattended. Plots theta/heatrate (actual vs. target), heater power_set/power_eff, Sud state, rest index, and stirrer speed/duty cycle over time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import numpy as np
|
||||
from matplotlib.pyplot import plot, step, figure, subplot, grid, show, legend, yticks
|
||||
from components.sud import Sud, SudState
|
||||
from components.pid.temp_controller_smith import TempController
|
||||
from components.plant.pot import Pot
|
||||
from components.actor.stirrersim import StirrerSim
|
||||
from components.actor.heater_sim import HeaterSim
|
||||
|
||||
# Mirrors tasks/sud.py's SudTask, but driven synchronously for a demo run.
|
||||
TEMP_REACHED_TOLERANCE = 0.2
|
||||
|
||||
# A real user would click "Confirm" in the GUI; auto-confirm after this many
|
||||
# simulated seconds so the demo can run to completion unattended.
|
||||
WAIT_USER_AUTO_CONFIRM_S = 30.0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
dt = 1.0
|
||||
theta_amb = 20
|
||||
|
||||
ctrl_params = {
|
||||
"Hold": {
|
||||
"kp": 0.4,
|
||||
"ki": 0.0,
|
||||
"kd": 0.0,
|
||||
"kt": 0.0
|
||||
},
|
||||
"Heat": {
|
||||
"kp": 0.08,
|
||||
"ki": 0.008,
|
||||
"kd": 0.0,
|
||||
"kt": 1.5
|
||||
}
|
||||
}
|
||||
|
||||
plant_params = {
|
||||
"theta" : 20,
|
||||
"C" : 4190,
|
||||
"M" : 20,
|
||||
"L" : 0.2,
|
||||
"Td" : 30,
|
||||
"gain" : 1.0
|
||||
}
|
||||
|
||||
sud = Sud("sude/sud_0010.json")
|
||||
ctrl = TempController(dt, ctrl_params, plant_params, theta_amb)
|
||||
plant = Pot(dt, plant_params, theta_amb)
|
||||
stirrer = StirrerSim(dt)
|
||||
heater = HeaterSim({})
|
||||
|
||||
# Mirror brewpi.py's data flow: the controller's internal Smith-predictor
|
||||
# model sees the (continuous) requested power, the real plant sees the
|
||||
# heater's actual (discretized) output power.
|
||||
heater.set_on_changed('power_set', ctrl.set_model_power)
|
||||
heater.set_on_changed('power_eff', plant.set_power)
|
||||
|
||||
def on_rast_changed(rast):
|
||||
if rast is not None:
|
||||
ctrl.set_theta_soll(rast['temp'])
|
||||
ctrl.set_heatrate_soll(rast['heatRate'])
|
||||
|
||||
def on_state_changed(state):
|
||||
if state == SudState.RAMPING:
|
||||
stirrer.set_duty_cycle(1.0)
|
||||
stirrer.set_speed(sud.stirr_speed_heat)
|
||||
elif state == SudState.HOLDING:
|
||||
stirrer.set_cycle_time(sud.stirr_cycle_time)
|
||||
stirrer.set_duty_cycle(sud.stirr_duty_rast)
|
||||
stirrer.set_speed(sud.stirr_speed_rast)
|
||||
elif state in (SudState.WAIT_USER, SudState.DONE):
|
||||
stirrer.set_duty_cycle(1.0)
|
||||
stirrer.set_speed(0)
|
||||
|
||||
sud.set_on_changed('rast', on_rast_changed)
|
||||
sud.set_on_changed('state', on_state_changed)
|
||||
|
||||
sud.start()
|
||||
|
||||
_t = []
|
||||
_temp_ist = []
|
||||
_temp_soll = []
|
||||
_heatrate_ist = []
|
||||
_heatrate_soll = []
|
||||
_sud_state = []
|
||||
_rast_index = []
|
||||
_stirrer_speed = []
|
||||
_stirrer_duty = []
|
||||
_heater_power_set = []
|
||||
_heater_power_eff = []
|
||||
|
||||
wait_user_elapsed = 0.0
|
||||
t = 0.0
|
||||
steps = 0
|
||||
max_steps = 30000
|
||||
while sud.state != SudState.DONE and steps < max_steps:
|
||||
plant.process()
|
||||
ctrl.set_theta_ist(plant.get_temperature())
|
||||
ctrl.process()
|
||||
|
||||
y = ctrl.get_power()
|
||||
heater.set_power(max(0, 3500*y))
|
||||
heater.process()
|
||||
|
||||
stirrer.process()
|
||||
|
||||
if sud.state == SudState.RAMPING:
|
||||
if abs(ctrl.get_theta_ist() - ctrl.get_theta_soll_set()) < TEMP_REACHED_TOLERANCE:
|
||||
sud.temp_reached()
|
||||
|
||||
if sud.state == SudState.WAIT_USER:
|
||||
wait_user_elapsed += dt
|
||||
if wait_user_elapsed >= WAIT_USER_AUTO_CONFIRM_S:
|
||||
sud.confirm()
|
||||
wait_user_elapsed = 0.0
|
||||
else:
|
||||
wait_user_elapsed = 0.0
|
||||
|
||||
sud.tick(dt)
|
||||
|
||||
_t.append(t)
|
||||
_temp_ist.append(ctrl.get_theta_ist())
|
||||
_temp_soll.append(ctrl.get_theta_soll_set())
|
||||
_heatrate_ist.append(ctrl.get_heatrate_ist())
|
||||
_heatrate_soll.append(ctrl.get_heatrate_soll())
|
||||
_sud_state.append(sud.state.value)
|
||||
_rast_index.append(sud.index)
|
||||
_stirrer_speed.append(stirrer.speed * stirrer.isOn)
|
||||
_stirrer_duty.append(stirrer.dutyCycle * 100)
|
||||
_heater_power_set.append(heater.power_set)
|
||||
_heater_power_eff.append(heater.power_eff)
|
||||
|
||||
t += dt
|
||||
steps += 1
|
||||
|
||||
if sud.state != SudState.DONE:
|
||||
print("Sud did not finish within {} steps (stuck in {})".format(max_steps, sud.state))
|
||||
print("Sud finished after {:.0f} s ({:.1f} min), final state = {}".format(t, t/60.0, sud.state))
|
||||
|
||||
_t_min = np.array(_t) / 60.0
|
||||
|
||||
figure(1)
|
||||
subplot(3, 1, 1)
|
||||
plot(_t_min, _temp_ist, '-b', _t_min, _temp_soll, '-r', linewidth=1)
|
||||
legend(["theta_ist", "theta_soll"])
|
||||
grid(True)
|
||||
subplot(3, 1, 2)
|
||||
plot(_t_min, _heatrate_ist, '-b', _t_min, _heatrate_soll, '-r', linewidth=1)
|
||||
legend(["heatrate_ist", "heatrate_soll"])
|
||||
grid(True)
|
||||
subplot(3, 1, 3)
|
||||
plot(_t_min, _heater_power_set, '-r', _t_min, _heater_power_eff, '-b', linewidth=1)
|
||||
legend(["heater power_set", "heater power_eff"])
|
||||
grid(True)
|
||||
|
||||
figure(2)
|
||||
subplot(3, 1, 1)
|
||||
step(_t_min, _sud_state, where='post', color='m')
|
||||
yticks([0, 1, 2, 3, 4], ["IDLE", "RAMPING", "HOLDING", "WAIT_USER", "DONE"])
|
||||
legend(["Sud state"])
|
||||
grid(True)
|
||||
subplot(3, 1, 2)
|
||||
step(_t_min, _rast_index, where='post', color='k')
|
||||
legend(["Rast index"])
|
||||
grid(True)
|
||||
subplot(3, 1, 3)
|
||||
plot(_t_min, _stirrer_speed, '-g', _t_min, _stirrer_duty, '-c', linewidth=1)
|
||||
legend(["stirrer speed (effective)", "stirrer duty cycle [%]"])
|
||||
grid(True)
|
||||
|
||||
show()
|
||||
|
||||
print("End of program")
|
||||
Reference in New Issue
Block a user