Claude C code:
#include <stdio.h>
#include <stdbool.h>
#include <time.h>
// --- Configuration ---
#define TARGET_TEMP 22.0f // Desired room temperature (°C)
#define HYST_ON_OFFSET (-0.5f) // Turn ON when temp <= target + this → 21.5°C
#define HYST_OFF_BASE 0.5f // Base turn-OFF offset above target → 22.5°C
#define HYST_OFF_MIN 0.05f // Minimum turn-off offset (floor)
#define DECAY_RATE 0.02f // How fast off-offset shrinks per second while ON
#define RECOVERY_RATE 0.005f // How fast off-offset recovers per second while OFF
#define SAMPLE_INTERVAL_S 1.0f // Control loop period (seconds)
// --- Controller State ---
typedef struct {
bool heater_on;
float hyst_off_offset; // Dynamic upper hysteresis threshold offset
time_t last_update;
} HeaterController;
// Initialize controller
void controller_init(HeaterController *ctrl) {
ctrl->heater_on = false;
ctrl->hyst_off_offset = HYST_OFF_BASE;
ctrl->last_update = time(NULL);
}
// Update controller given current temperature; returns heater state
bool controller_update(HeaterController *ctrl, float current_temp) {
time_t now = time(NULL);
float dt = (float)difftime(now, ctrl->last_update);
ctrl->last_update = now;
float turn_on_temp = TARGET_TEMP + HYST_ON_OFFSET; // e.g. 21.5°C
float turn_off_temp = TARGET_TEMP + ctrl->hyst_off_offset; // dynamic
if (ctrl->heater_on) {
// --- Heater is ON ---
// Decay the off-threshold toward the target to preempt overshoot
ctrl->hyst_off_offset -= DECAY_RATE * dt;
if (ctrl->hyst_off_offset < HYST_OFF_MIN)
ctrl->hyst_off_offset = HYST_OFF_MIN;
// Re-evaluate turn-off with updated threshold
turn_off_temp = TARGET_TEMP + ctrl->hyst_off_offset;
if (current_temp >= turn_off_temp) {
ctrl->heater_on = false;
printf(" → Heater OFF (%.2f°C >= turn-off %.2f°C)\n",
current_temp, turn_off_temp);
}
} else {
// --- Heater is OFF ---
// Let the offset recover slowly back toward base
ctrl->hyst_off_offset += RECOVERY_RATE * dt;
if (ctrl->hyst_off_offset > HYST_OFF_BASE)
ctrl->hyst_off_offset = HYST_OFF_BASE;
if (current_temp <= turn_on_temp) {
ctrl->heater_on = true;
printf(" → Heater ON (%.2f°C <= turn-on %.2f°C)\n",
current_temp, turn_on_temp);
}
}
return ctrl->heater_on;
}
// Print current controller diagnostics
void controller_print_status(const HeaterController *ctrl, float temp) {
printf(" Temp: %6.2f°C | Heater: %-3s | Off-threshold: %.2f°C | Offset: %.3f\n",
temp,
ctrl->heater_on ? "ON" : "OFF",
TARGET_TEMP + ctrl->hyst_off_offset,
ctrl->hyst_off_offset);
}
// --- Demo: simulated temperature trace ---
int main(void) {
HeaterController ctrl;
controller_init(&ctrl);
// Simulated temperature readings (°C) — a realistic warm-up/cool-down trace
float sim_temps[] = {
20.0f, 20.2f, 20.5f, 20.9f, 21.3f, // Heating up
21.5f, 21.8f, 22.0f, 22.3f, 22.6f, // Crosses thresholds
22.4f, 22.1f, 21.9f, 21.6f, 21.4f, // Cooling
21.2f, 21.0f, 20.8f, 21.1f, 21.5f, // Crosses turn-on again
21.8f, 22.0f, 22.2f, 22.5f, 22.3f, // Second cycle
22.0f, 21.8f, 21.5f, 21.3f, 21.1f
};
int n = sizeof(sim_temps) / sizeof(sim_temps[0]);
printf("=== Heater Controller Demo ===\n");
printf("Target: %.1f°C | Turn-on: %.2f°C | Base turn-off: %.2f°C\n\n",
TARGET_TEMP,
TARGET_TEMP + HYST_ON_OFFSET,
TARGET_TEMP + HYST_OFF_BASE);
for (int i = 0; i < n; i++) {
printf("Step %2d: ", i + 1);
controller_update(&ctrl, sim_temps[i]);
controller_print_status(&ctrl, sim_temps[i]);
// In real code: sleep(SAMPLE_INTERVAL_S);
}
return 0;
}
Plotted behavior:
Microsoft Copilot produced a python program that works quite a bit better:
import matplotlib.pyplot as plt
# --- Adaptive controller (C-like logic) ---
class AdaptiveHeaterController:
def __init__(self, setpoint=22.0, hysteresis=0.5, adapt_rate=0.2, overshoot_margin=0.1):
self.setpoint = setpoint
self.upper_limit = setpoint + hysteresis
self.lower_limit = setpoint - hysteresis
self.adapt_rate = adapt_rate
self.overshoot_margin = overshoot_margin
self.heater_on = False
self.in_cycle = False
self.peak_temp = None
def update(self, T):
# Track peak temperature during heating cycle
if self.heater_on:
if self.peak_temp is None or T > self.peak_temp:
self.peak_temp = T
# Hysteresis control
if not self.heater_on and T < self.lower_limit:
self.heater_on = True
self.in_cycle = True
self.peak_temp = T
elif self.heater_on and T > self.upper_limit:
self.heater_on = False
self._end_cycle()
return self.heater_on
def _end_cycle(self):
if not self.in_cycle:
return
# Check overshoot
if self.peak_temp is not None and self.peak_temp > self.setpoint + self.overshoot_margin:
overshoot = self.peak_temp - self.setpoint
self.upper_limit -= self.adapt_rate * overshoot
# Keep hysteresis sane
min_hyst = 0.2
if self.upper_limit < self.lower_limit + min_hyst:
self.upper_limit = self.lower_limit + min_hyst
self.in_cycle = False
self.peak_temp = None
# --- Simulation parameters ---
ambient = 15.0
setpoint = 22.0
hysteresis = 0.5
adapt_rate = 0.2
overshoot_margin = 0.1
controller = AdaptiveHeaterController(setpoint, hysteresis, adapt_rate, overshoot_margin)
T = 18.0 # initial room temperature
dt = 1.0 # time step (arbitrary units)
times = []
temps = []
upper_limits = []
lower_limits = []
heater_states = []
time = 0.0
cycles_completed = 0
prev_heater_on = controller.heater_on
# --- Run until N OFF transitions (N heating cycles) ---
N = 20
while cycles_completed < N:
heater_on = controller.update(T)
# Thermal model:
# T_next = T + 0.1*(heater_on) - 0.02*(T - ambient)
T = T + 0.25*heater_on - 0.015 * (T - ambient)
times.append(time)
temps.append(T)
upper_limits.append(controller.upper_limit)
lower_limits.append(controller.lower_limit)
heater_states.append(1 if heater_on else 0)
# Detect OFF transition (end of a heating cycle)
if prev_heater_on and not heater_on:
cycles_completed += 1
prev_heater_on = heater_on
time += dt
# --- Plot ---
fig, ax1 = plt.subplots(figsize=(10, 5))
ax1.plot(times, temps, label="Temperature (°C)", color="tab:blue")
ax1.plot(times, upper_limits, "--", label="Upper limit (°C)", color="tab:red")
ax1.plot(times, lower_limits, "--", label="Lower limit (°C)", color="tab:green")
ax1.axhline(setpoint, color="gray", linestyle=":", label="Setpoint")
ax1.set_xlabel("Time (steps)")
ax1.set_ylabel("Temperature (°C)")
ax1.grid(True)
# Heater state as secondary axis (0/1)
ax2 = ax1.twinx()
ax2.step(times, heater_states, where="post", color="black", alpha=0.3, label="Heater ON")
ax2.set_ylabel("Heater state")
# Combine legends
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="best")
plt.tight_layout()
plt.show()
Resulting plot, for this simple thermal model of the room:
T_next = T + 0.25*(heater_on) - 0.015*(T - ambient)