|
|
|
@@ -10,6 +10,11 @@ it:
|
|
|
|
|
AC2 Fully-fixed pricing DISABLES ARBITRAGE and leaves self-consumption only.
|
|
|
|
|
AC3 The Belgian scenario suite carries a static-profile regression case.
|
|
|
|
|
|
|
|
|
|
Section 9 is TEST-16 and is not one of those criteria. It pins the two
|
|
|
|
|
`arbitrage()` rungs SAFETY-26's AC4 sweep found pinned from ONE SIDE ONLY -
|
|
|
|
|
`spread == 0.0` and `len(firm) < 2` - and, more to the point, pins them so
|
|
|
|
|
they stop MASKING EACH OTHER. Read its header before editing either rung.
|
|
|
|
|
|
|
|
|
|
THREE RULES THIS FILE HOLDS ITSELF TO, because the reviewer will:
|
|
|
|
|
|
|
|
|
|
* EVERY VERDICT IS DERIVED. No `check("the split is right", True)`. Where a
|
|
|
|
@@ -62,7 +67,7 @@ from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from app.price import (
|
|
|
|
|
Absence, PricePoint, PriceProduct, PriceRead, arbitrage_allowed, brussels,
|
|
|
|
|
plan_is_provisional,
|
|
|
|
|
plan_is_provisional, required_spread, uncertainty_of,
|
|
|
|
|
)
|
|
|
|
|
from app.static_tariff import (
|
|
|
|
|
MEAN_WINDOW_DAYS, ProfileInvalid, StaticProfile, TariffPlan,
|
|
|
|
@@ -860,6 +865,255 @@ check("...and main.py grew no `o.get(\"static_profile\", <a default>)` fallback
|
|
|
|
|
'get("static_profile"' not in _MAIN and "static_status(" in _MAIN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
print()
|
|
|
|
|
print("9. TEST-16 - the two arbitrage rungs, pinned from BOTH sides and "
|
|
|
|
|
"un-masked from each other")
|
|
|
|
|
# SAFETY-26's AC4 gave the same tolerance mutation to every price and quantity
|
|
|
|
|
# boundary in arbitrage.py and static_tariff.py. Everything in arbitrage.py went
|
|
|
|
|
# red. The two survivors are the two rungs graded below, and section 2 above is
|
|
|
|
|
# why they survived: it probes the flat rung with an EXACTLY FLAT series and
|
|
|
|
|
# never probes the firm-pair rung at all.
|
|
|
|
|
#
|
|
|
|
|
# D `spread == 0.0` weakened to `spread <= 0.001` SURVIVED (+2 B). An
|
|
|
|
|
# exactly flat series refuses under both spellings, so a tolerance is
|
|
|
|
|
# invisible to a flat probe. It is NOT cosmetic: a real 0.0005 spread
|
|
|
|
|
# flips ALLOWED -> REFUSED at min_spread_eur_kwh=0.0, which arbitrage()'s
|
|
|
|
|
# own docstring documents as supported ("pass 0.0 to mean any positive
|
|
|
|
|
# spread, explicitly") and which its validator at :861 accepts on purpose.
|
|
|
|
|
#
|
|
|
|
|
# F `len(firm) < 2` weakened to `< 1` SURVIVED, at +0 BYTES - the delta
|
|
|
|
|
# MUTATION.md names as the risky one.
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ AND THE FINDING IS THE INTERACTION, NOT EITHER ROW. D AND F MASK EACH
|
|
|
|
|
# OTHER. A single firm hour has a spread of 0.0, so with F weakened the FLAT
|
|
|
|
|
# rung catches the stimulus and the verdict is still "refused"; with D weakened
|
|
|
|
|
# the tiny-spread stimulus is still refused, just for a lie. Each rung's failure
|
|
|
|
|
# is absorbed by the other, so BOTH can be weakened together and a suite that
|
|
|
|
|
# grades only `kind` reports nothing. That is invisible to single-mutation
|
|
|
|
|
# testing, which is how this pair survived four rounds of review on TARIFF-03
|
|
|
|
|
# and FCST-03.
|
|
|
|
|
#
|
|
|
|
|
# TWO THINGS FOLLOW, and they shape every check below.
|
|
|
|
|
#
|
|
|
|
|
# 1. THE REASON IS GRADED, NOT JUST THE REFUSAL. The masking case REFUSES
|
|
|
|
|
# CORRECTLY WHILE REPORTING THE WRONG SENTENCE - an operator reading "the
|
|
|
|
|
# tariff is flat" about a tariff that is not flat, or reading it about a
|
|
|
|
|
# series whose real problem is that §8.3 has no firm pair to trade on. A
|
|
|
|
|
# check that asserts only "refused" passes on a broken tree. So each
|
|
|
|
|
# stimulus below is graded on WHICH RUNG ANSWERED IT.
|
|
|
|
|
# 2. EACH RUNG NEEDS A STIMULUS THE OTHER CANNOT ABSORB, or the pinning is
|
|
|
|
|
# circular. The flat rung gets a series with THREE firm hours, where the
|
|
|
|
|
# firm-pair rung cannot fire under any weakening down to `< 0`. The
|
|
|
|
|
# firm-pair rung gets a ONE-FIRM series, which the flat rung CAN answer -
|
|
|
|
|
# so its attribution is the whole check, and that is why row 2 of the table
|
|
|
|
|
# is written as a rung name rather than as a verdict.
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ SHIPPED BEHAVIOUR IS UNCHANGED BY THIS SECTION. Both rungs are correct
|
|
|
|
|
# today; nothing in app/ was touched. This is the missing measurement, not a fix.
|
|
|
|
|
#
|
|
|
|
|
# THE LEDGER. Eight rounds through `tools/mutation_run.sh` (TEST-15): recursive
|
|
|
|
|
# `__pycache__` purge before each, `python3 -B`, the tree re-copied FROM DISK
|
|
|
|
|
# between rounds, the mutated line grepped out of the file before the run, and
|
|
|
|
|
# the byte delta printed. `test_static_tariff.py` and `test_arbitrage.py` graded
|
|
|
|
|
# each time. Both suites green on the unmutated tree.
|
|
|
|
|
#
|
|
|
|
|
# round delta test_static_tariff.py
|
|
|
|
|
# --------------------------------------------- ------ ---------------------
|
|
|
|
|
# baseline, shipped +0 B PASS
|
|
|
|
|
# D1 `spread == 0.0` -> `<= 0.001` +2 B RED, 3 checks
|
|
|
|
|
# D2 `spread == 0.0` -> `<= 0.000001` +5 B RED, AC1 alone
|
|
|
|
|
# D3 `spread == 0.0` -> `< 0.0` (rung gone) +0 B RED (section 2 too)
|
|
|
|
|
# F1 `len(firm) < 2` -> `< 1` +0 B RED, 3 checks
|
|
|
|
|
# F2 `len(firm) < 2` -> `< 3` +0 B RED, 6 checks
|
|
|
|
|
# F3 `len(firm) < 2` -> `< 0` (rung gone) +0 B RED (and ValueError)
|
|
|
|
|
# D1+F1 BOTH AT ONCE - THE TICKET +2 B RED, 5 checks
|
|
|
|
|
#
|
|
|
|
|
# Read the rows in pairs. D1 and F1 are SAFETY-26's two survivors and each is
|
|
|
|
|
# now red on its own; D2 is the tolerance bisected to the rung's own resolution,
|
|
|
|
|
# so "pinned at exactly zero" is measured rather than asserted at one epsilon;
|
|
|
|
|
# F2 and F3 pin the firm-pair count from ABOVE and BELOW, which the ticket's
|
|
|
|
|
# criteria do not ask for but which is what stops this section being one-sided
|
|
|
|
|
# in the way it was written to fix. THE LAST ROW IS THE TICKET: with BOTH rungs
|
|
|
|
|
# weakened together - the arrangement that reported nothing before this section
|
|
|
|
|
# existed - five checks go red, including AC1 and AC2, because neither of them
|
|
|
|
|
# is spelled in terms of the other rung's behaviour.
|
|
|
|
|
#
|
|
|
|
|
# ⚠️ FOUR OF THOSE SEVEN MUTATIONS ARE +0 BYTES, which MUTATION.md names as the
|
|
|
|
|
# delta stale bytecode can hide. They are all REDS, the direction the bytecode
|
|
|
|
|
# defect cannot fabricate - but they were run under the purge anyway, because
|
|
|
|
|
# "it went red so the cache cannot have lied" is a conclusion, not a procedure.
|
|
|
|
|
|
|
|
|
|
_A9 = dict(peak_guard_active=False, min_spread_eur_kwh=0.0,
|
|
|
|
|
uncertainty=frozenset())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def plan9(prices, provisional=None, product=PriceProduct.DAY_AHEAD):
|
|
|
|
|
"""A bare hourly series, built here rather than through a profile.
|
|
|
|
|
|
|
|
|
|
A `static_profile` cannot express a 0.0005 spread or a part-provisional day,
|
|
|
|
|
and going through one would grade the profile generator as well. These are
|
|
|
|
|
the smallest series that put exactly one rung in question.
|
|
|
|
|
"""
|
|
|
|
|
flags = provisional if provisional is not None else [False] * len(prices)
|
|
|
|
|
pts = [PricePoint(start=brussels(THU + timedelta(hours=i)),
|
|
|
|
|
eur_per_kwh=p, provisional=v)
|
|
|
|
|
for i, (p, v) in enumerate(zip(prices, flags))]
|
|
|
|
|
return TariffPlan(PriceRead.series(pts, product, "generic_dayahead", THU),
|
|
|
|
|
None, "provider")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rung9(verdict):
|
|
|
|
|
"""WHICH refusal answered, named from the reason the operator would read.
|
|
|
|
|
|
|
|
|
|
Deliberately not a code inspection: the sentence in the log IS the artefact
|
|
|
|
|
that degrades when these two rungs mask each other, so it is the sentence
|
|
|
|
|
that gets graded. An unrecognised refusal is reported as such rather than
|
|
|
|
|
quietly matching nothing - "no rung fired" and "a rung I did not list fired"
|
|
|
|
|
must not grade the same.
|
|
|
|
|
"""
|
|
|
|
|
if verdict.kind == "allowed":
|
|
|
|
|
return "allowed"
|
|
|
|
|
r = verdict.reason
|
|
|
|
|
hit = [n for n, needle in (("flat", "the tariff is flat"),
|
|
|
|
|
("firm-pair", "carry a published price"),
|
|
|
|
|
("threshold", "needs"),
|
|
|
|
|
("peak", "peak guard"),
|
|
|
|
|
("no-series", "No series exists"))
|
|
|
|
|
if needle in r]
|
|
|
|
|
return hit[0] if len(hit) == 1 else f"UNATTRIBUTED<{r[:60]}>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- the sweep D survived: a small but REAL spread, at min_spread 0.0 --------
|
|
|
|
|
# ⚠️ NOT AN EXACTLY FLAT SERIES, which is the whole point - a flat probe cannot
|
|
|
|
|
# see a tolerance. The threshold is 0.0 so the `spread < need` rung below cannot
|
|
|
|
|
# fire either (required_spread(0.0, anything) is 0.0, asserted first), leaving
|
|
|
|
|
# the flat rung as the only thing that can refuse these.
|
|
|
|
|
check(f"the widened threshold at min_spread_eur_kwh=0.0 is still 0.0, so the "
|
|
|
|
|
f"probes below are graded by the FLAT rung and not by the threshold rung "
|
|
|
|
|
f"({required_spread(0.0, uncertainty_of(PriceProduct.DAY_AHEAD)):.6f})",
|
|
|
|
|
near(required_spread(0.0, uncertainty_of(PriceProduct.DAY_AHEAD)), 0.0))
|
|
|
|
|
|
|
|
|
|
# 1e-6 is the floor of the question, not of the check: app/static_tariff.py
|
|
|
|
|
# rounds the spread to 6 decimals before comparing, so a 5e-7 difference IS
|
|
|
|
|
# zero to this rung by construction. Graded as its own row below rather than
|
|
|
|
|
# left as an unstated edge.
|
|
|
|
|
TINY9 = (0.0005, 0.0001, 0.00001, 0.000001)
|
|
|
|
|
_tiny9 = [(d, plan9([0.20000, round(0.20000 + d, 6)])) for d in TINY9]
|
|
|
|
|
check(f"each tiny-spread probe really carries the spread it is named for, and "
|
|
|
|
|
f"two FIRM hours - so the firm-pair rung cannot be what answers them "
|
|
|
|
|
f"{[(f'{d:.6f}', f'{p.spread_eur_kwh():.6f}') for d, p in _tiny9]}",
|
|
|
|
|
all(near(p.spread_eur_kwh(), d, 1e-12)
|
|
|
|
|
and len([q for q in p.read.points if not q.provisional]) == 2
|
|
|
|
|
for d, p in _tiny9))
|
|
|
|
|
_tinyv9 = [(d, rung9(p.arbitrage(**_A9))) for d, p in _tiny9]
|
|
|
|
|
check(f"TEST-16 AC1: a spread that is SMALL BUT REAL is ALLOWED at "
|
|
|
|
|
f"min_spread_eur_kwh=0.0 - down to 0.000001 EUR/kWh, so the flat rung is "
|
|
|
|
|
f"`spread == 0.0` and not `spread <= epsilon` for any epsilon "
|
|
|
|
|
f"{_tinyv9}",
|
|
|
|
|
all(r == "allowed" for _d, r in _tinyv9))
|
|
|
|
|
check("...and the exactly flat series on the OTHER side of that boundary is "
|
|
|
|
|
"still refused, and refused BY THE FLAT RUNG - without this the sweep "
|
|
|
|
|
"above would also pass for a rung that had been deleted outright",
|
|
|
|
|
rung9(plan9([0.20000, 0.20000]).arbitrage(**_A9)) == "flat")
|
|
|
|
|
_q9 = plan9([0.20000, 0.2000005])
|
|
|
|
|
check(f"...and a difference below the rung's own 6-decimal rounding is flat BY "
|
|
|
|
|
f"CONSTRUCTION and says so: the raw difference is "
|
|
|
|
|
f"{_q9.spread_eur_kwh():.7f} and non-zero, `round(..., 6)` makes it "
|
|
|
|
|
f"exactly 0.0, and the rung agrees. That is the QUANTISATION the shipped "
|
|
|
|
|
f"line chooses in the open, not a tolerance smuggled into the comparison "
|
|
|
|
|
f"- and it is why the sweep above stops at 0.000001 rather than pretending "
|
|
|
|
|
f"to bisect further",
|
|
|
|
|
not near(_q9.spread_eur_kwh(), 0.0, 1e-9)
|
|
|
|
|
and near(round(_q9.spread_eur_kwh(), 6), 0.0, 1e-12)
|
|
|
|
|
and rung9(_q9.arbitrage(**_A9)) == "flat")
|
|
|
|
|
|
|
|
|
|
# --- and what D's survival actually costs: the sentence, not the verdict -----
|
|
|
|
|
_lie9 = plan9([0.20000, 0.20050]).arbitrage(**_A9)
|
|
|
|
|
check(f"TEST-16 AC4: the 0.0005 verdict carries the real spread rather than a "
|
|
|
|
|
f"flatness claim about a tariff that is not flat - the false sentence is "
|
|
|
|
|
f"what a loosened rung ships to the operator while the outcome at a "
|
|
|
|
|
f"shipped threshold looks unchanged ({_lie9.kind})",
|
|
|
|
|
_lie9.kind == "allowed" and near(_lie9.spread_eur_kwh, 0.0005)
|
|
|
|
|
and near(_lie9.cheapest.eur_per_kwh, 0.20000)
|
|
|
|
|
and near(_lie9.dearest.eur_per_kwh, 0.20050))
|
|
|
|
|
|
|
|
|
|
# --- the rung F survived: ON ITS OWN, not through the flat rung --------------
|
|
|
|
|
# ⚠️ THIS ROW IS THE ANTI-MASKING ONE. A one-firm series has a spread of 0.0,
|
|
|
|
|
# so with `< 2` weakened to `< 1` the FLAT rung refuses it and `kind` never
|
|
|
|
|
# moves. The only thing that moves is the sentence: §8.3's honest "only 1 of 3
|
|
|
|
|
# hours carry a published price" degrades to "the tariff is flat" about a series
|
|
|
|
|
# whose firm and provisional prices differ by 0.20 EUR/kWh. Grading the rung
|
|
|
|
|
# name is what makes `< 1` red without borrowing anything from the flat rung.
|
|
|
|
|
_one9 = plan9([0.20, 0.40, 0.10], [False, True, True])
|
|
|
|
|
check(f"the one-firm probe is well formed: exactly ONE firm hour out of three, "
|
|
|
|
|
f"and its full series is anything but flat (spread "
|
|
|
|
|
f"{max(p.eur_per_kwh for p in _one9.read.points) - min(p.eur_per_kwh for p in _one9.read.points):.2f} "
|
|
|
|
|
f"EUR/kWh) - so 'flat' would be a false sentence about it",
|
|
|
|
|
len([p for p in _one9.read.points if not p.provisional]) == 1
|
|
|
|
|
and len(_one9.read.points) == 3)
|
|
|
|
|
check("TEST-16 AC2: one firm hour is refused BY §8.3's FIRM-PAIR RUNG, named "
|
|
|
|
|
"in the reason - not by the flat rung that would also have refused it. "
|
|
|
|
|
"This is what takes `len(firm) < 2` -> `< 1` red on its own",
|
|
|
|
|
rung9(_one9.arbitrage(**_A9)) == "firm-pair")
|
|
|
|
|
check("...and the sentence is §8.3's in full, so the operator is told the "
|
|
|
|
|
"series is half-published rather than told it is fixed-price",
|
|
|
|
|
"§8.3" in _one9.arbitrage(**_A9).reason
|
|
|
|
|
and "only 1 of 3" in _one9.arbitrage(**_A9).reason
|
|
|
|
|
and "flat" not in _one9.arbitrage(**_A9).reason)
|
|
|
|
|
# The rung pinned from ABOVE as well. Without this, `len(firm) < 3` - the same
|
|
|
|
|
# +0 B edit in the other direction - would pass everything above.
|
|
|
|
|
_two9 = plan9([0.20, 0.40, 0.10], [False, False, True])
|
|
|
|
|
check("...and TWO firm hours are ALLOWED on the identical series shape, so the "
|
|
|
|
|
"rung is pinned from above too and `< 3` is red as well as `< 1`",
|
|
|
|
|
rung9(_two9.arbitrage(**_A9)) == "allowed"
|
|
|
|
|
and near(_two9.arbitrage(**_A9).spread_eur_kwh, 0.20))
|
|
|
|
|
# ...and from below, where the failure is not a wrong sentence but a crash:
|
|
|
|
|
# `max()` over an empty `firm` raises ValueError, so `< 0` does not degrade the
|
|
|
|
|
# reason, it takes the verdict away entirely.
|
|
|
|
|
_zero9 = plan9([0.20, 0.40, 0.10], [True, True, True])
|
|
|
|
|
check("...and a FULLY provisional series is refused by the same rung and does "
|
|
|
|
|
"not raise - `max()` over an empty firm set is a ValueError, so this row "
|
|
|
|
|
"is what makes `len(firm) < 0` red",
|
|
|
|
|
rung9(derived(lambda: _zero9.arbitrage(**_A9))) == "firm-pair")
|
|
|
|
|
|
|
|
|
|
# --- AC3: the two rungs do not mask each other ------------------------------
|
|
|
|
|
# ⚠️ THE POINT OF THE TICKET, AND IT IS A PROPERTY OF THE TABLE, NOT OF A ROW.
|
|
|
|
|
# Masking is possible exactly when every stimulus that one rung answers could
|
|
|
|
|
# also be answered by the other with an acceptable-looking result. The table
|
|
|
|
|
# below denies that on both sides:
|
|
|
|
|
#
|
|
|
|
|
# * the flat rung is asked a question the firm-pair rung CANNOT reach - three
|
|
|
|
|
# firm hours, where `len(firm) < N` refuses for no N a person would write;
|
|
|
|
|
# * the firm-pair rung is asked a question the flat rung CAN reach - one firm
|
|
|
|
|
# hour - and the answer is graded on WHICH rung gave it, so the flat rung
|
|
|
|
|
# absorbing it is a failure rather than a pass;
|
|
|
|
|
# * and the tiny-spread row is allowed, which neither rung may refuse.
|
|
|
|
|
#
|
|
|
|
|
# Verified by running BOTH mutations at once (`spread <= 0.001` AND
|
|
|
|
|
# `len(firm) < 1`, purged harness, +2 B and +0 B): three rows move. Under
|
|
|
|
|
# single-mutation testing this arrangement reports nothing at all.
|
|
|
|
|
_TABLE9 = (
|
|
|
|
|
("three firm hours, all 0.2000", plan9([0.2, 0.2, 0.2]), "flat"),
|
|
|
|
|
("two firm hours, 0.0005 apart", plan9([0.20000, 0.20050]), "allowed"),
|
|
|
|
|
("one firm hour of three", _one9, "firm-pair"),
|
|
|
|
|
("two firm hours of three", _two9, "allowed"),
|
|
|
|
|
("no firm hours at all", _zero9, "firm-pair"),
|
|
|
|
|
)
|
|
|
|
|
_got9 = [(name, want, rung9(derived(lambda p=pl: p.arbitrage(**_A9))))
|
|
|
|
|
for name, pl, want in _TABLE9]
|
|
|
|
|
check(f"TEST-16 AC3: each stimulus is answered by the rung that OWNS it, and "
|
|
|
|
|
f"by no other - the flat rung is asked a question the firm-pair rung "
|
|
|
|
|
f"cannot reach, and the firm-pair rung is asked one the flat rung could "
|
|
|
|
|
f"reach but must not be allowed to. Weakening EITHER rung moves a row; "
|
|
|
|
|
f"weakening BOTH TOGETHER still moves three "
|
|
|
|
|
f"{[(n, w, g) for n, w, g in _got9]}",
|
|
|
|
|
all(want == got for _n, want, got in _got9))
|
|
|
|
|
check("...and the table really does exercise both rungs and the allowing path, "
|
|
|
|
|
"so 'every row matched' is not a statement about one rung repeated",
|
|
|
|
|
{want for _n, _p, want in _TABLE9} == {"flat", "firm-pair", "allowed"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
print()
|
|
|
|
|
if fails:
|
|
|
|
|