SAFETY-26: pin the negative-price refusal from below, not only from above #23

Merged
admin merged 1 commits from SAFETY-26 into release/1.0 2026-08-31 07:37:47 +02:00
+118 -1
View File
@@ -378,7 +378,11 @@ check("BUYING in a negative-price hour is still permitted - §17 forbids "
"refusing to be paid to charge",
act(NEG_PLAN, NEG.index(min(NEG)), thresh=_shipped,
unc=NO_WEATHER).action == CHARGE)
# A zero price is the boundary and it is NOT negative.
# A zero price is the boundary and it is NOT negative. ⚠️ KEEP THIS PROBE. It
# pins the boundary from ABOVE - it is what catches a guard written `<= 0` - and
# the SAFETY-26 block below pins it from BELOW. Neither replaces the other, and
# the whole defect that card exists to close is that only one of the two sides
# was ever stated.
ZERO = list(POS)
ZERO[DEAR_H] = 0.0
check("a sell hour priced at exactly ZERO is not a negative one, so the guard "
@@ -387,6 +391,119 @@ check("a sell hour priced at exactly ZERO is not a negative one, so the guard "
act(plan_of(ZERO), DEAR_H, thresh=_shipped, unc=NO_WEATHER).action
== DISCHARGE)
# --------------------------------------------------------------------------- #
# SAFETY-26. THE OTHER SIDE OF THE SAME BOUNDARY, AND WHY IT HAD TO BE ADDED.
#
# OPT-04's reviewer mutated `if price < 0` to `if price < -0.001` and EVERY
# SUITE STAYED GREEN, with a -0.0005 EUR/kWh sell hour discharging. The shipped
# code was right the whole time; the CHECKS could not tell correct from
# nearly-correct, because the only probe near the boundary sat at 0.0000. A
# probe above the line catches `<=`. It says nothing at all about a tolerance
# invented below the line, and a tolerance below the line IS §17's regression
# case walking through with the suite passing.
#
# ⚠️ AND -0.0005 IS NOT AN EXOTIC INPUT. Belgian day-ahead prints small negative
# hours regularly; an all-in price a twentieth of a cent below zero is an
# ordinary day, not a fuzz value. That is what makes this a live regression
# rather than a theoretical one.
#
# ⚠️ SO THE PROBE IS A SWEEP, NOT A POINT, and the sweep is the discriminating
# part. One probe at -0.0005 closes `-0.001` and nothing else; a mutation
# reaching for `-0.01` would survive it, which is the identical defect one
# decimal place further out. The sweep spans four orders of magnitude down to
# the last place the canonical six-decimal format can even express, so there is
# no tolerance a guard can invent that some member of it does not straddle.
# `_ROUNDING` asserts the smallest member SURVIVES the format rather than being
# rounded to zero on the way in - a probe that silently became 0.0 would be the
# `0.0000` check again, wearing a different name.
TINY_NEG = (-0.05, -0.005, -0.0005, -0.00005, -0.000001)
_ROUNDING = [round(p, 6) for p in TINY_NEG]
check(f"every probe price survives the canonical 6-decimal rounding as a "
f"STRICTLY negative number {_ROUNDING} - one that rounded to 0.0 would "
f"be the check above again under another name",
all(r < 0 for r in _ROUNDING) and len(set(_ROUNDING)) == len(TINY_NEG))
check("...and they span four orders of magnitude, so no single invented "
"tolerance can sit below all of them and above none",
abs(min(TINY_NEG)) / abs(max(TINY_NEG)) > 10000)
def tiny_neg_plan(price):
"""The POS day with its DEAREST hour set to a small negative print."""
day = list(POS)
day[DEAR_H] = price
return plan_of(day), day
# The stimulus has to survive the rungs ABOVE §17 or it grades nothing: the
# hour must still be the dearest, and the spread must still clear the widened
# threshold, at every probe. Asserted first, for all of them, as its own check.
_wellformed = [
(p, day.index(max(day)) == DEAR_H,
round(max(day) - min(day), 6) > required_spread(_shipped, NO_WEATHER))
for p, (_pl, day) in ((p, tiny_neg_plan(p)) for p in TINY_NEG)]
check(f"at every probe the negative hour is still the DEAREST one and the "
f"spread still clears the threshold, so §17 is the only rung that can "
f"refuse {_wellformed}",
all(dearest and clears for _p, dearest, clears in _wellformed))
_swept = [(p, act(tiny_neg_plan(p)[0], DEAR_H, thresh=_shipped,
unc=NO_WEATHER).action) for p in TINY_NEG]
check(f"SAFETY-26: a sell hour that is negative BY ANY MARGIN AT ALL refuses - "
f"down to -0.000001 EUR/kWh, so the guard is `< 0` and not `< -epsilon` "
f"for any epsilon {_swept}",
all(a is None for _p, a in _swept))
check("...and each refusal is §17's, not some other rung that happens to fire "
"at small numbers",
all("§17" in act(tiny_neg_plan(p)[0], DEAR_H, thresh=_shipped,
unc=NO_WEATHER).reason for p in TINY_NEG))
# The mirror image, one place above zero. Without this the sweep above would
# pass for a guard that refused EVERYTHING, which grades §17 by nothing.
_tiny_pos = [(-p, act(tiny_neg_plan(-p)[0], DEAR_H, thresh=_shipped,
unc=NO_WEATHER).action) for p in TINY_NEG]
check(f"...while THE SAME MAGNITUDES with the sign flipped all DISCHARGE, so "
f"the sweep is a boundary at exactly zero rather than a refusal of small "
f"numbers - which a guard that refused everything would also have passed "
f"{_tiny_pos}",
all(a == DISCHARGE for _p, a in _tiny_pos))
# SAFETY-26 AC4 - THE LOOK AT THE OTHER BOUNDARIES, NAMED RATHER THAN FIXED.
#
# The card asks whether the same one-sided pinning exists elsewhere. Every
# price/quantity boundary in arbitrage.py and static_tariff.py was given the
# tolerance mutation that survived on `price < 0`, in both directions where
# both exist, under `-B` with a recursive purge. Two survived, BOTH in
# static_tariff.py - TARIFF-03's file, and deliberately NOT touched here:
#
# D `spread == 0.0` (the flat-tariff rung) -> `spread <= 0.001` SURVIVES.
# Measured, not reasoned: with it loosened, a series priced 0.20000 /
# 0.20050 - a REAL 0.0005 spread - flips from ALLOWED to REFUSED at
# min_spread_eur_kwh=0.0, which `arbitrage()`'s own docstring documents as
# a supported value ("pass 0.0 to mean any positive spread, explicitly").
# At a shipped threshold the OUTCOME is unchanged, but the operator's
# reason becomes the false sentence "the tariff is flat - every one of the
# 2 firm hours costs 0.2000 EUR/kWh" about a tariff that is not flat.
# Same shape as §17's: the rung is only ever probed with an EXACTLY flat
# series, so nothing pins the tiny-but-real side.
#
# F `len(firm) < 2` (§8.3's firm-pair rung) -> `< 1` SURVIVES, at +0 BYTES,
# which is the delta TEST-15 names as the risky one. It is not live on its
# own: a single firm hour has a spread of 0.0, so the flat rung refuses it
# anyway - but the reason degrades from §8.3's honest "only 1 of 3 hours
# carry a published price" to "the tariff is flat". THE FINDING THAT
# MATTERS IS THE INTERACTION: D and F MASK EACH OTHER. Neither is
# individually live because the other catches it, which is exactly the
# arrangement in which a later edit to either one opens a hole nothing
# reports.
#
# Everything else in arbitrage.py is pinned on BOTH sides and was verified so:
# `price < 0` (this card), `soc <= floor_pct` and `soc >= ceiling_pct` are each
# red under a +5 and a -5 tolerance; static_tariff's `spread < need` is red
# under +/-0.001 and additionally bisected to 1e-6 in section 1; `cheap_now`'s
# `price < mean` is red under a +0.001 tolerance in test_static_tariff.py.
#
# Not fixed here on purpose. D and F are TARIFF-03's rungs, they need probes in
# test_static_tariff.py rather than in this file, and the card is explicit that
# its fourth criterion is a look and not a refactor.
# --------------------------------------------------------------------------- #
print()