128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
"""Pure logic for the Versatile Thermostat True Tick Cycle Scheduler."""
|
|
|
|
class UnderlyingCycleState:
|
|
"""Per-underlying state tracking for a single cycle."""
|
|
|
|
def __init__(self, underlying, offset: float):
|
|
"""Initialize the state with an underlying reference and a fixed circular offset."""
|
|
self.underlying = underlying
|
|
self.offset = offset
|
|
self.on_t: float = 0.0
|
|
self.off_t: float = 0.0
|
|
self.on_time: float = 0.0
|
|
|
|
|
|
def compute_circular_offsets(cycle_duration_sec: float, n: int) -> list[float]:
|
|
"""Compute evenly-spaced circular offsets for n underlyings.
|
|
|
|
Returns:
|
|
List of start offsets in seconds for each underlying.
|
|
"""
|
|
if n <= 1:
|
|
return [0.0] * n
|
|
|
|
step = cycle_duration_sec / n
|
|
return [round(i * step, 1) for i in range(n)]
|
|
|
|
|
|
def compute_target_state(
|
|
on_t: float, off_t: float, current_t: float, cycle_duration: float
|
|
) -> tuple[bool, float, float]:
|
|
"""Determine the theoretical target state and the next tick timestamp.
|
|
|
|
Returns:
|
|
(target_is_on, next_tick, state_duration)
|
|
"""
|
|
if off_t > on_t: # ON is confined in [on_t, off_t)
|
|
if current_t < on_t:
|
|
target = False
|
|
next_tick = on_t
|
|
elif current_t < off_t:
|
|
target = True
|
|
next_tick = off_t
|
|
else:
|
|
target = False
|
|
next_tick = cycle_duration
|
|
else: # ON wraps around: [0, off_t) U [on_t, cycle_end)
|
|
if current_t < off_t:
|
|
target = True
|
|
next_tick = off_t
|
|
elif current_t < on_t:
|
|
target = False
|
|
next_tick = on_t
|
|
else:
|
|
target = True
|
|
next_tick = cycle_duration
|
|
|
|
# Note (off_t == on_t falls in wrap-around condition):
|
|
# This works safely because upstream guards in cycle_scheduler._start_cycle_switch
|
|
# handle `on_time == 0` and `on_time == cycle_duration` before evaluating ticks.
|
|
state_duration = next_tick - current_t
|
|
return target, next_tick, state_duration
|
|
|
|
|
|
def evaluate_need_on(
|
|
under_dt: float, state_duration: float,
|
|
min_deactivation: float, min_activation: float,
|
|
on_t: float, current_t: float,
|
|
) -> tuple[str, float | None, float]:
|
|
"""Evaluate whether to actually turn ON (need_on) according to constraints.
|
|
|
|
Returns:
|
|
(action, new_on_t_or_none, penalty_delta)
|
|
action is 'turn_on' or 'skip'
|
|
"""
|
|
if under_dt >= min_deactivation and state_duration > min_activation:
|
|
return 'turn_on', None, 0.0
|
|
|
|
# CAS RACOLLAGE (Skip this turn ON)
|
|
new_on_t = max(0.0, on_t - state_duration)
|
|
penalty_delta = state_duration
|
|
|
|
if (new_on_t - current_t) < (min_deactivation - under_dt):
|
|
new_on_t = current_t + (min_deactivation - under_dt)
|
|
delay = new_on_t - current_t
|
|
penalty_delta = delay if delay < state_duration else state_duration
|
|
|
|
return 'skip', new_on_t, penalty_delta
|
|
|
|
|
|
def evaluate_need_off(
|
|
under_dt: float, state_duration: float,
|
|
min_activation: float, min_deactivation: float,
|
|
off_t: float, current_t: float,
|
|
) -> tuple[str, float | None, float]:
|
|
"""Evaluate whether to actually turn OFF (need_off) according to constraints.
|
|
|
|
Returns:
|
|
(action, new_off_t_or_none, penalty_delta)
|
|
action is 'turn_off' or 'skip'
|
|
"""
|
|
if under_dt >= min_activation and state_duration > min_deactivation:
|
|
return 'turn_off', None, 0.0
|
|
|
|
# CAS RACOLLAGE (Skip this turn OFF)
|
|
new_off_t = max(0.0, off_t - state_duration)
|
|
penalty_delta = -state_duration
|
|
|
|
if (new_off_t - current_t) < (min_activation - under_dt):
|
|
new_off_t = current_t + (min_activation - under_dt)
|
|
delay = new_off_t - current_t
|
|
penalty_delta = -delay if delay < state_duration else -state_duration
|
|
|
|
return 'skip', new_off_t, penalty_delta
|
|
|
|
|
|
def compute_e_eff(
|
|
on_percent: float, penalty: float,
|
|
cycle_duration: float, n_underlyings: int,
|
|
) -> float:
|
|
"""Compute effective power ratio (e_eff) at the end of the cycle."""
|
|
if n_underlyings == 0 or cycle_duration <= 0:
|
|
return 0.0
|
|
|
|
full_on_t = cycle_duration * n_underlyings
|
|
e_eff = (full_on_t * on_percent - penalty) / full_on_t
|
|
|
|
return max(0.0, min(1.0, e_eff))
|