"""Landing motion charts (se-charts.js) must mirror the assumptions registry. Parses the hard-coded ``var D = { ... }`` block and key chart call sites so winter / peak / reliability / cost figures cannot drift from assumptions.yaml. """ from __future__ import annotations import math import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] REPO = ROOT.parent sys.path.insert(0, str(ROOT)) from regulator_paper_assumptions import v # noqa: E402 from system_b_ledger import RETIRED_COMPOSITE_MW, build_ledger # noqa: E402 CHARTS = REPO / "assets" / "js" / "se-charts.js" FIGURES = ROOT / "scripts" / "generate_report_v2_figures.py" def _charts_src() -> str: return CHARTS.read_text(encoding="utf-8") def _parse_d_block(src: str) -> dict[str, float | list[float | None]]: m = re.search(r"var D = \{([^}]+)\};", src, re.S) assert m, "se-charts.js missing var D = { ... } block" body = m.group(1) out: dict[str, float | list[float | None]] = {} for key, val in re.findall(r"(\w+):\s*(\[[^\]]+\]|[-+]?\d+(?:\.\d+)?)", body): if val.startswith("["): items = [] for tok in val.strip("[]").split(","): tok = tok.strip() items.append(None if tok == "null" else float(tok)) out[key] = items else: out[key] = float(val) return out def test_se_charts_d_block_matches_registry_and_ledger(): d = _parse_d_block(_charts_src()) ledger = build_ledger() assert d["peak"] == float(v("peak_record_mw")) assert d["shortfall"] == float(v("shortfall_2030_mw")) assert d["cons"] == float(v("scenario_conservative_mw")) assert d["ref"] == float(v("scenario_reference_mw")) assert d["full"] == float(v("scenario_full_mw")) assert d["gasName"] == float(v("gas_nameplate_mw")) assert d["gasDerate"] == float(v("gas_derate_scenario_mw")) assert d["gasCapex"] == float(v("gas_capex_b")) assert d["hybridGross"] == float(v("hybrid_capex_gross_b")) assert d["hybridNet"] == float(v("hybrid_capex_net_b")) assert d["gasLo"] == float(v("gas_20yr_low_b")) assert d["gasHi"] == float(v("gas_20yr_high_b")) assert d["hybLo"] == float(v("hybrid_20yr_low_b")) assert d["hybHi"] == float(v("hybrid_20yr_high_b")) assert d["hubTh"] == float(v("hub_thermal_mwth")) assert d["hubNet"] == float(v("hub_net_electric_mw")) assert d["hubSource"] == float(v("hub_source_thermal_mwth")) assert d["hubComp"] == float(v("hub_compressor_mw")) assert d["hubDelivered"] == float(v("hub_delivered_thermal_exact_mwth")) assert d["bess"] == float(v("bess_mw")) assert d["dsm"] == float(v("dsm_mw")) assert d["intertie"] == float(v("intertie_mw")) assert d["pdrc"] == float(v("pdrc_summer_shave_mw")) assert d["btes"] == float(v("btes_summer_chiller_shave_mw")) assert d["summerAnchor"] == float(v("summer_peak_anchor_mw")) assert d["winterBase"] == float(v("winter_load_base_mw")) assert d["winterThresh"] == float(v("winter_heat_threshold_c")) assert abs(float(d["winterBeta"]) - float(v("winter_heat_beta_mw_per_c"))) < 1e-6 assert d["winterDesign"] == float(v("winter_design_temp_c")) assert d["peakRecordTemp"] == float(v("peak_record_temp_c")) assert d["summerBase"] == float(v("summer_load_base_mw")) assert d["summerThresh"] == float(v("summer_heat_threshold_c")) assert d["summerBeta"] == float(v("summer_heat_beta_mw_per_c")) assert d["summerPdrcOnset"] == float(v("summer_pdrc_onset_c")) assert d["summerPdrcBeta"] == float(v("summer_pdrc_beta_mw_per_c")) assert d["savLo"] == float(ledger.savings_band_b[0]) assert d["savHi"] == float(ledger.savings_band_b[1]) gas_events = v("gas_cold_events_outage") assert d["gasFail"] == float(gas_events[0]) assert d["gasEvents"] == float(gas_events[1]) # Effluent stage is what counts today; compute reject stays contingent assert d["effluent"] == d["hubTh"] assert d["reject"] == float(v("hub_contingent_thermal_mwth")) assert d["eligibleMw"] == float(v("eligible_mw")) assert d["btesWinterPlan"] == float(v("btes_winter_planning_mw")) assert d["btesWinterElig"] == float(v("btes_winter_eligible_mw")) mixed = d["bess"] + d["hubTh"] + d["reject"] + d["dsm"] + d["intertie"] assert mixed == float(RETIRED_COMPOSITE_MW) assert d["full"] != float(RETIRED_COMPOSITE_MW) assert d["full"] == d["bess"] + d["dsm"] + d["intertie"] + d["hubNet"] # Delivered $/kW arrays must match ledger rounding expected_gross = [ round(ledger.cost_per_kw_hybrid_gross("conservative")), round(ledger.cost_per_kw_hybrid_gross("reference")), round(ledger.cost_per_kw_hybrid_gross("full")), round(ledger.cost_per_kw_gas_nameplate()), round(ledger.cost_per_kw_gas_derate()), ] expected_net = [ round(ledger.cost_per_kw_hybrid_net("conservative")), round(ledger.cost_per_kw_hybrid_net("reference")), round(ledger.cost_per_kw_hybrid_net("full")), None, None, ] assert d["costGross"] == [float(x) for x in expected_gross] assert d["costNet"] == [float(x) if x is not None else None for x in expected_net] assert d["yearTmean"] == [float(x) for x in v("winnipeg_normals_mean_c")] assert d["yearTmin"] == [float(x) for x in v("winnipeg_normals_min_c")] assert d["yearTmax"] == [float(x) for x in v("winnipeg_normals_max_c")] assert d["yearLoadOff"] == [float(x) for x in v("year_load_unmanaged_mw")] assert d["yearLoadOn"] == [float(x) for x in v("year_load_managed_mw")] from regulator_paper_assumptions import ( year_round_typical_peak_managed_mw, year_round_typical_peak_unmanaged_mw, ) assert v("year_load_unmanaged_mw") == year_round_typical_peak_unmanaged_mw() assert v("year_load_managed_mw") == year_round_typical_peak_managed_mw() assert all(on <= off for on, off in zip(d["yearLoadOn"], d["yearLoadOff"])) assert d["yearLoadOn"][1] < d["yearLoadOff"][1] assert d["yearLoadOn"][6] < d["yearLoadOff"][6] sav_lo, sav_hi = ledger.savings_band_b assert abs(sav_lo - 2.7) < 1e-9 and abs(sav_hi - 5.4) < 1e-9 def test_se_charts_capacity_values_are_side_by_side_not_additive_stack(): """Hybrid and gas bars are separate values[]; never a stacked hybrid+gas sum.""" src = _charts_src() m = re.search( r'capacity:\s*function[\s\S]*?values:\s*\[([^\]]+)\]', src, ) assert m, "capacity chart values array missing" d = _parse_d_block(src) raw = [tok.strip() for tok in m.group(1).split(",")] vals = [] for tok in raw: if tok.startswith("D."): vals.append(float(d[tok[2:]])) elif tok == "elccRefMw()": vals.append( round( d["bess"] * d["elccBess"] + d["dsm"] * d["elccDsm"] + (d["ref"] - d["bess"] - d["dsm"] - d["hubNet"]) + d["hubNet"] ) ) else: vals.append(float(tok)) assert vals == [ d["cons"], d["ref"], round( d["bess"] * d["elccBess"] + d["dsm"] * d["elccDsm"] + (d["ref"] - d["bess"] - d["dsm"] - d["hubNet"]) + d["hubNet"] ), d["full"], d["gasName"], d["gasDerate"], ] assert sum(vals) != d["full"] + d["gasName"] assert "Winter resource stack" in src or "Firm winter electric" in src assert "not additive" in src.lower() or "Bars are not additive" in src cap = src.split("capacity: function", 1)[1].split("delivered-cost", 1)[0] assert "Accreditation scenario" not in cap assert "Planning cases vs the" in cap assert "elccRefMw()" in cap assert "illustrative" in cap.lower() def test_se_charts_rejects_magic_winter_shave_and_wrong_summer_anchor(): src = _charts_src() # Cold-snap must use ledger hub + BESS + DSM, not an invented 220 MW peak shave assert "day24(D.peak, 220)" not in src assert "coldSnapDay" in src assert "D.hubNet" in src and "D.bess" in src and "D.dsm" in src # Summer load vs temp must use registry summer slope keys, not a bare 4100@+35 assert "3200 + (t - 15) * 45" not in src assert "summerAnchor" in src assert "summerBase" in src and "summerBeta" in src assert "summerPdrcOnset" in src # Savings band in D; §2 decision-compare must show dollar band + gap highlight assert "savLo" in src and "savHi" in src assert "Ratepayer savings" in src # legend swatch assert "Ratepayer savings $" not in src # in-chart band retired; callout states it assert "D.savLo.toFixed(1)" in src and "D.savHi.toFixed(1)" in src assert "se-savings-gap" in src assert "Savings band $" in src # lifecycle footer still carries the band def test_winter_load_formula_anchors_at_peak(): """Research heating model must hit peak_record_mw at design temp and base at threshold.""" from regulator_paper_assumptions import winter_system_load_mw peak = float(v("peak_record_mw")) base = float(v("winter_load_base_mw")) thresh = float(v("winter_heat_threshold_c")) design = float(v("winter_design_temp_c")) hub = float(v("hub_net_electric_mw")) assert math.isclose(winter_system_load_mw(design), peak, abs_tol=1e-6) assert math.isclose(winter_system_load_mw(thresh), base, abs_tol=1e-6) for t in (4, 0, -20, -35, -40): load = winter_system_load_mw(t) assert load - hub == max(load - hub, 0) def test_summer_load_formula_anchors_at_registry_peak(): """Landing summer curve must match fig13-style anchor at +35 °C.""" from regulator_paper_assumptions import summer_system_load_mw, summer_load_with_pdrc_mw anchor = float(v("summer_peak_anchor_mw")) load_35 = summer_system_load_mw(35) assert math.isclose(load_35, anchor, abs_tol=0.5) assert math.isclose( summer_system_load_mw(35) - summer_load_with_pdrc_mw(35), float(v("pdrc_summer_shave_mw")), abs_tol=0.5, ) def test_se_charts_uses_research_winter_heating_model(): """JS must call the registry heating params, not ad-hoc peak-(t+35)*28.""" src = _charts_src() d = _parse_d_block(src) assert d["winterBase"] == float(v("winter_load_base_mw")) assert d["winterThresh"] == float(v("winter_heat_threshold_c")) assert abs(float(d["winterBeta"]) - float(v("winter_heat_beta_mw_per_c"))) < 1e-6 assert d["winterDesign"] == float(v("winter_design_temp_c")) assert d["peakRecordTemp"] == float(v("peak_record_temp_c")) assert "peak - (t + 35) * 28" not in src assert "winterBase" in src and "winterBeta" in src cold_fn = src.split("function coldSnapDay", 1)[1].split("function diagramCards", 1)[0] assert "4200" not in cold_fn assert "D.peak" in cold_fn # Summer uses registry slope keys assert d["summerBase"] == float(v("summer_load_base_mw")) assert d["summerThresh"] == float(v("summer_heat_threshold_c")) assert d["summerBeta"] == float(v("summer_heat_beta_mw_per_c")) assert d["summerPdrcOnset"] == float(v("summer_pdrc_onset_c")) assert d["summerPdrcBeta"] == float(v("summer_pdrc_beta_mw_per_c")) def test_figure3_heat_path_shares_electric_scale(): """50 MWth must not fill a 160-unit axis while 688 MW fills 800.""" lanes = _charts_src().split("function lanes", 1)[1].split("var RENDERERS", 1)[0] assert "tMax = 160" not in lanes assert "var scaleMax = 800, px = barW / scaleMax" in lanes assert "s.v * px" in lanes assert "same scale as MW" in lanes def test_figure_generator_uses_peak_2024_key_not_alias(): src = FIGURES.read_text(encoding="utf-8") assert "peak_load_2024_mw" not in src assert 'v("peak_record_mw")' in src or "v('peak_record_mw')" in src assert "shortfall_2030_mw" in src assert "winter_system_load_mw" in src assert "summer_system_load_mw" in src assert re.search( r"(shortfall_2030_mw|int\(v\([\"']shortfall_2030_mw[\"']\)\))", src, ) def test_cold_snap_shave_matches_ledger_profile_semantics(): """Off-peak = hub net; spike hours add BESS+DSM — same contract as ledger.""" ledger = build_ledger() profile = ledger.peak_shave_mw_by_hour() assert profile[3] == ledger.hub_net_electric_mw assert profile[8] == ledger.hub_net_electric_mw + ledger.bess_mw + ledger.dsm_mw src = _charts_src() cold = src.split('"cold-snap":', 1)[1].split("reliability:", 1)[0] assert "hubNet" in cold or "D.hubNet" in cold assert "bess" in cold.lower() assert "dsm" in cold.lower() assert "220" not in cold