feat: embed config in server log; replay_sim takes a single log path
ServerLogTask now embeds the full config in its JSON output, matching SudLogTask — so replay_sim and analyze_log work on server logs without needing a separate config file. replay_sim.py switches from two positional args (date_time, sud_name) to a single log file path, with config and plant params resolved from the log's embedded "Config"/"PlantParams" sections when present and falling back to config.json / hardcoded defaults for older logs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+54
-46
@@ -13,18 +13,19 @@ plant reaches setpoint faster, overshoots less, or oscillates less than
|
||||
the original logged run, all without touching real hardware.
|
||||
|
||||
Usage:
|
||||
python utils/replay_sim.py <date_time> <sud_name>
|
||||
python utils/replay_sim.py <date_time> <sud_name> \\
|
||||
python utils/replay_sim.py logs/log_<date_time>_<name>.json
|
||||
python utils/replay_sim.py logs/log_<date_time>_<name>.json \\
|
||||
--heat-kp 0.1 --heat-ki 0.03 --heat-kt 1.0
|
||||
python utils/replay_sim.py <date_time> <sud_name> \\
|
||||
python utils/replay_sim.py logs/log_<date_time>_<name>.json \\
|
||||
--plant-M 25 --plant-L 0.15 --ambient 18
|
||||
|
||||
Plant params default to water (C=4190 J/kg·K), M=20 kg, L=0.2 W/kg·K,
|
||||
Td=30 s — pass --plant-* to match the actual batch. Max heater power is
|
||||
inferred from the highest power_eff step in the log.
|
||||
PID params and plant params are read from the log's embedded "Config" and
|
||||
"PlantParams" sections when present (logs recorded since the config-in-log
|
||||
feature was added). Pass --config to supply a config file for older logs
|
||||
that predate that feature. Any --<section>-<gain> or --plant-* flag
|
||||
overrides just that one value regardless of source.
|
||||
|
||||
By default reads PID params from config.json in the repo root. Any
|
||||
--<section>-<gain> flag overrides just that one value.
|
||||
Max heater power is inferred from the highest power_eff step in the log.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -158,34 +159,27 @@ def plot_replay(samples, replay, name, max_power, params_label):
|
||||
|
||||
|
||||
def main():
|
||||
repo_root = Path(__file__).parent.parent
|
||||
default_config = repo_root / 'config.json'
|
||||
default_log_dir = repo_root / 'logs'
|
||||
repo_root = Path(__file__).parent.parent
|
||||
default_config = repo_root / 'config.json'
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument('date_time', help='Timestamp part of log filename, e.g. 20260628T184903')
|
||||
parser.add_argument('sud_name', help='Sud name part of log filename, e.g. Sud-0010')
|
||||
parser.add_argument('--log-dir', default=str(default_log_dir),
|
||||
help='Log directory (default: %(default)s)')
|
||||
parser.add_argument('--config', default=str(default_config),
|
||||
help='Config file for base PID params (default: %(default)s)')
|
||||
parser.add_argument('log', help='Path to log_*.json file')
|
||||
parser.add_argument('--config', default=None,
|
||||
help='Config file for PID params (default: embedded in log, or config.json)')
|
||||
parser.add_argument('--rate-soll', type=float, default=None,
|
||||
help='Heat-rate setpoint [K/min] (default: inferred from log)')
|
||||
parser.add_argument('--ambient', type=float, default=None,
|
||||
help='Ambient temperature [°C] (default: from config ambient_temperature)')
|
||||
help='Ambient temperature [°C] (default: from config)')
|
||||
|
||||
# Plant model params
|
||||
parser.add_argument('--plant-M', type=float, default=20.0, metavar='kg',
|
||||
help='Plant mass [kg] (default: %(default)s)')
|
||||
parser.add_argument('--plant-C', type=float, default=4190.0, metavar='J/kgK',
|
||||
help='Specific heat capacity [J/(kg·K)] (default: %(default)s — water)')
|
||||
parser.add_argument('--plant-L', type=float, default=0.2, metavar='W/kgK',
|
||||
help='Heat loss coefficient [W/(kg·K)] (default: %(default)s)')
|
||||
parser.add_argument('--plant-Td', type=float, default=30.0, metavar='s',
|
||||
help='Transport delay [s] (default: %(default)s)')
|
||||
# Plant model params — all default to None so we can tell "not specified"
|
||||
# and fall back to the log's embedded PlantParams when available.
|
||||
parser.add_argument('--plant-M', type=float, default=None, metavar='kg', help='Plant mass [kg]')
|
||||
parser.add_argument('--plant-C', type=float, default=None, metavar='J/kgK', help='Specific heat [J/(kg·K)]')
|
||||
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]')
|
||||
parser.add_argument('--heater-efficiency', type=float, default=1.0, metavar='0-1',
|
||||
help='Fraction of rated heater power delivered to the plant (default: %(default)s)')
|
||||
|
||||
@@ -201,44 +195,58 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
log_path = Path(args.log_dir) / 'log_{}_{}.json'.format(args.date_time, args.sud_name)
|
||||
log_path = Path(args.log)
|
||||
if not log_path.exists():
|
||||
print('Error: log file not found: {}'.format(log_path), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config_path = Path(args.config)
|
||||
if not config_path.exists():
|
||||
print('Error: config file not found: {}'.format(config_path), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(log_path) as f:
|
||||
log = json.load(f)
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
|
||||
samples = log['Samples']
|
||||
name = log.get('Name', '{}_{}'.format(args.date_time, args.sud_name))
|
||||
pid_params = _apply_gain_overrides(config['TempCtrl'], args)
|
||||
dt = (samples[1]['t'] - samples[0]['t']) if len(samples) > 1 else 1.0
|
||||
max_power = _infer_max_power(samples)
|
||||
rate_soll = args.rate_soll if args.rate_soll is not None else _infer_heatrate_soll_set(samples)
|
||||
ambient = args.ambient if args.ambient is not None else config.get('ambient_temperature', 20.0)
|
||||
# Config: prefer embedded in log, then --config arg, then default config.json.
|
||||
config = log.get('Config')
|
||||
if config is None:
|
||||
config_path = Path(args.config) if args.config else default_config
|
||||
if config_path.exists():
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
else:
|
||||
print('Error: no config embedded in log and config file not found: {}'.format(config_path),
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
plant_params = {'M': args.plant_M, 'C': args.plant_C, 'L': args.plant_L, 'Td': args.plant_Td}
|
||||
# Plant params: prefer log's PlantParams[0], then --plant-* flags, then hardcoded defaults.
|
||||
log_plant = log['PlantParams'][0]['params'] if log.get('PlantParams') else None
|
||||
plant_params = {
|
||||
'M': args.plant_M if args.plant_M is not None else (log_plant['M'] if log_plant else 20.0),
|
||||
'C': args.plant_C if args.plant_C is not None else (log_plant['C'] if log_plant else 4190.0),
|
||||
'L': args.plant_L if args.plant_L is not None else (log_plant['L'] if log_plant else 0.2),
|
||||
'Td': args.plant_Td if args.plant_Td is not None else (log_plant['Td'] if log_plant else 30.0),
|
||||
}
|
||||
|
||||
samples = log['Samples']
|
||||
name = log.get('Name', log_path.stem)
|
||||
pid_params = _apply_gain_overrides(config['TempCtrl'], args)
|
||||
dt = (samples[1]['t'] - samples[0]['t']) if len(samples) > 1 else 1.0
|
||||
max_power = _infer_max_power(samples)
|
||||
rate_soll = args.rate_soll if args.rate_soll is not None else _infer_heatrate_soll_set(samples)
|
||||
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')
|
||||
for g in ('kp', 'ki', 'kd', 'kt')
|
||||
)
|
||||
params_label = 'candidate' if any_override else 'config'
|
||||
config_source = 'log' if log.get('Config') else 'file'
|
||||
params_label = 'candidate' if any_override else config_source
|
||||
|
||||
print('Log: {} ({} samples, dt={:.1f} s)'.format(name, len(samples), dt))
|
||||
print('Power: max={:.0f} W'.format(max_power))
|
||||
print('Rate: heatrate_soll_set={:.2f} K/min'.format(rate_soll))
|
||||
print('Plant: M={M} kg C={C} J/kgK L={L} W/kgK Td={Td} s ambient={amb} °C heater_efficiency={eff}'.format(
|
||||
print('Plant: M={M} kg C={C:.0f} J/kgK L={L} W/kgK Td={Td} s ambient={amb} °C heater_efficiency={eff}'.format(
|
||||
amb=ambient, eff=args.heater_efficiency, **plant_params))
|
||||
print('Params: {}'.format('overridden' if any_override else 'from config'))
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user