fix: split inner-loop yi_max clamp to fix HOLD-state windup overshoot
Renames pid_hold/pid_heat/pid_cool to pid_outer/pid_inner/pid_inner_cool
to match what actually runs when, and splits inner-loop config into
Inner.Heat/Inner.Hold/Inner.Cool so the same PID instance gets a tight
yi_max ceiling only while HOLD drives it, without capping legitimate
1.5 K/min ramps. Fixes the overshoot from docs/overshoot_hold_windup.md
where a cold-water disturbance during HOLD wound up pid_heat's integral
term with no anti-windup engagement, taking ~35s+ to unwind naturally.
Breaking config change: Hold/Heat/Cool -> Outer/Inner.{Heat,Hold,Cool}
in config.json, both .tpl templates, the pid/sud demo scripts, and
replay_sim.py's CLI flags. Adds tests/components/pid/ (stdlib unittest)
covering the Pid clamp/recovery behavior and closed-loop disturbance,
ramp, and HOLD<->HEAT transition cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGQhVQ2Y3yXAQTXhrxVd5u
This commit is contained in:
+22
-10
@@ -45,13 +45,21 @@ from components.pid import PidFactory
|
||||
from components.plant.pot import Pot
|
||||
|
||||
|
||||
GAIN_SECTIONS = (('Outer', 'outer'), ('Inner.Heat', 'inner-heat'),
|
||||
('Inner.Hold', 'inner-hold'), ('Inner.Cool', 'inner-cool'))
|
||||
|
||||
|
||||
def _apply_gain_overrides(params, args):
|
||||
p = copy.deepcopy(params)
|
||||
for section, prefix in (('Hold', 'hold'), ('Heat', 'heat'), ('Cool', 'cool')):
|
||||
for section, prefix in GAIN_SECTIONS:
|
||||
for gain in ('kp', 'ki', 'kd', 'kt'):
|
||||
val = getattr(args, '{}_{}'.format(prefix, gain))
|
||||
val = getattr(args, '{}_{}'.format(prefix.replace('-', '_'), gain))
|
||||
if val is not None:
|
||||
p[section][gain] = val
|
||||
if '.' in section:
|
||||
outer, inner = section.split('.')
|
||||
p[outer][inner][gain] = val
|
||||
else:
|
||||
p[section][gain] = val
|
||||
return p
|
||||
|
||||
|
||||
@@ -63,7 +71,7 @@ def _infer_max_power(samples):
|
||||
def _infer_heatrate_soll_set(samples):
|
||||
"""Estimate heatrate_soll_set from the log.
|
||||
|
||||
rate_soll = heatrate_soll_set * pid_hold.get_y(); when the hold PID is
|
||||
rate_soll = heatrate_soll_set * pid_outer.get_y(); when the outer PID is
|
||||
saturated at 1.0 during active heating the two are equal, so the max
|
||||
rate_soll seen while the heater is on is a good upper bound."""
|
||||
candidates = [s['rate_soll'] for s in samples if s['power_set'] > 0]
|
||||
@@ -211,12 +219,12 @@ def main():
|
||||
parser.add_argument('--plant-L', type=float, default=None, metavar='W/kgK', help='Heat loss coeff [W/(kg·K)]')
|
||||
parser.add_argument('--plant-Td', type=float, default=None, metavar='s', help='Transport delay [s]')
|
||||
|
||||
for section, prefix in (('Hold', 'hold'), ('Heat', 'heat'), ('Cool', 'cool')):
|
||||
for section, prefix in GAIN_SECTIONS:
|
||||
for gain in ('kp', 'ki', 'kd', 'kt'):
|
||||
parser.add_argument(
|
||||
'--{}-{}'.format(prefix, gain),
|
||||
type=float, default=None,
|
||||
dest='{}_{}'.format(prefix, gain),
|
||||
dest='{}_{}'.format(prefix.replace('-', '_'), gain),
|
||||
metavar='VAL',
|
||||
help='Override TempCtrl.{}.{}'.format(section, gain),
|
||||
)
|
||||
@@ -265,8 +273,8 @@ def main():
|
||||
ambient = args.ambient if args.ambient is not None else config.get('ambient_temperature', 20.0)
|
||||
|
||||
any_override = any(
|
||||
getattr(args, '{}_{}'.format(p, g)) is not None
|
||||
for p in ('hold', 'heat', 'cool')
|
||||
getattr(args, '{}_{}'.format(prefix.replace('-', '_'), g)) is not None
|
||||
for _, prefix in GAIN_SECTIONS
|
||||
for g in ('kp', 'ki', 'kd', 'kt')
|
||||
)
|
||||
config_source = 'log' if log.get('Config') else 'file'
|
||||
@@ -281,8 +289,12 @@ def main():
|
||||
print('Params: {} ({})'.format('overridden' if any_override else 'from {}'.format(config_source),
|
||||
'log' if log_plant else 'defaults'))
|
||||
print()
|
||||
for section in ('Hold', 'Heat', 'Cool'):
|
||||
p = pid_params[section]
|
||||
for section, _ in GAIN_SECTIONS:
|
||||
if '.' in section:
|
||||
outer, inner = section.split('.')
|
||||
p = pid_params[outer][inner]
|
||||
else:
|
||||
p = pid_params[section]
|
||||
print(' {}: kp={kp} ki={ki} kd={kd} kt={kt}'.format(section, **p))
|
||||
|
||||
replay = run_replay(samples, pid_type, pid_params, rate_soll, plant_params, param_events,
|
||||
|
||||
Reference in New Issue
Block a user