Spaces:
Sleeping
Sleeping
File size: 13,460 Bytes
b81a86b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | """
tests/test_constrained_scenario.py — Ordering/bound assertions for the
La Nina supply-shock (CONSTRAINED) scenario.
Scenario:
Ngawi (3521), Madiun (3519), Bojonegoro (3522) banjir bersamaan.
surplus_deficit_constrained.csv: 6 SURPLUS rows removed.
Result: surplus=3962t < deficit=5249t (under-supplied by 32.5%).
These tests assert ORDERINGS and BOUNDS, NOT golden numbers.
Golden numbers are in benchmarks/output/equity_comparison_constrained.md.
What these tests defend:
1. Fixture integrity: constrained is genuinely supply-constrained.
2. Uniform Gini sanity DOES NOT apply in constrained because the
algorithm cannot fill all demand equally — some nodes get 0 by
necessity. The formula is still correct; the invariant changes.
3. Greedy vs AgriFlow on Sampang/Bangkalan: the equity boost MUST
protect the two poorest kabs better than greedy under scarcity.
4. AgriFlow Gini <= greedy Gini in constrained (equity mechanism fires).
5. Coverage ordering: greedy >= agriflow (efficiency-equity tradeoff
shows up or is negligible — greedy never below agriflow).
6. Sensitivity ABUNDANT degenerate check (documents known limitation).
"""
from __future__ import annotations
import pytest
from matching_engine import run_matching
from matching_engine.models import LogisticsContext
from sample_data.loader import load_all_sample_data
from benchmarks._metrics import (
gini,
kab_fulfillment,
min_fulfillment,
total_deficit_covered,
)
from benchmarks.equity_comparison import (
_build_demand_tons,
_report_to_matched_tons,
equity_lenient,
equity_strict,
proportional_allocate,
uniform_allocate,
SAMPANG_ID,
BANGKALAN_ID,
)
CONSTRAINED_CSV = "surplus_deficit_constrained.csv"
EXPECTED_SURPLUS_TONS = 3962.0
EXPECTED_DEFICIT_TONS = 5249.0
# =============================================================================
# FIXTURES
# =============================================================================
@pytest.fixture(scope="module")
def constrained_data():
return load_all_sample_data(surplus_deficit_csv=CONSTRAINED_CSV)
@pytest.fixture(scope="module")
def logistics():
return LogisticsContext()
@pytest.fixture(scope="module")
def demand_tons_c(constrained_data):
return _build_demand_tons(constrained_data["deficit"])
@pytest.fixture(scope="module")
def matched_greedy_c(constrained_data, logistics):
report = run_matching(
constrained_data["surplus"], constrained_data["deficit"],
logistics=logistics,
force_strategy="greedy",
equity_fn=lambda _: 1.0,
)
return _report_to_matched_tons(report)
@pytest.fixture(scope="module")
def matched_agriflow_c(constrained_data, logistics):
report = run_matching(
constrained_data["surplus"], constrained_data["deficit"],
logistics=logistics,
)
return _report_to_matched_tons(report)
@pytest.fixture(scope="module")
def matched_uniform_c(constrained_data, logistics):
return uniform_allocate(
constrained_data["surplus"], constrained_data["deficit"], logistics
)
# =============================================================================
# 1. Fixture integrity
# =============================================================================
class TestFixtureIntegrity:
"""Verify the constrained fixture is genuinely supply-constrained."""
def test_surplus_less_than_deficit(self, constrained_data):
"""CONSTRAINED scenario must have deficit > surplus (core fixture assertion)."""
surplus_total = sum(s.volume_tons for s in constrained_data["surplus"])
deficit_total = sum(d.volume_tons for d in constrained_data["deficit"])
assert deficit_total > surplus_total, (
f"CONSTRAINED fixture must have deficit ({deficit_total:.0f}t) > "
f"surplus ({surplus_total:.0f}t). Fixture may be corrupted."
)
def test_surplus_approx_expected(self, constrained_data):
"""Constrained surplus == 3962t (6 rows removed from 3521/3519/3522)."""
surplus_total = sum(s.volume_tons for s in constrained_data["surplus"])
assert abs(surplus_total - EXPECTED_SURPLUS_TONS) < 1.0, (
f"Expected {EXPECTED_SURPLUS_TONS}t surplus, got {surplus_total:.1f}t. "
f"surplus_deficit_constrained.csv may be out of sync."
)
def test_deficit_unchanged_vs_abundant(self, constrained_data):
"""Deficit total must equal ABUNDANT deficit (shock only removes supply)."""
deficit_total = sum(d.volume_tons for d in constrained_data["deficit"])
assert abs(deficit_total - EXPECTED_DEFICIT_TONS) < 1.0, (
f"Expected {EXPECTED_DEFICIT_TONS}t deficit (unchanged vs abundant), "
f"got {deficit_total:.1f}t. Fixture must not modify demand rows."
)
def test_shock_kabs_absent_from_surplus(self, constrained_data):
"""Ngawi (3521), Madiun (3519), Bojonegoro (3522) must have no surplus rows."""
shock_kabs = {"3521", "3519", "3522"}
surplus_kabs = {s.kabupaten.id for s in constrained_data["surplus"]}
intersection = shock_kabs & surplus_kabs
assert not intersection, (
f"Shock kabs {intersection} still have surplus in constrained fixture. "
f"surplus_deficit_constrained.csv is incorrect."
)
def test_sampang_bangkalan_deficit_present(self, constrained_data):
"""Sampang (3527) and Bangkalan (3526) must still have deficit rows."""
deficit_kabs = {d.kabupaten.id for d in constrained_data["deficit"]}
assert SAMPANG_ID in deficit_kabs, (
f"Sampang ({SAMPANG_ID}) has no deficit in constrained scenario."
)
assert BANGKALAN_ID in deficit_kabs, (
f"Bangkalan ({BANGKALAN_ID}) has no deficit in constrained scenario."
)
# =============================================================================
# 2. Coverage is meaningfully below 1 in constrained
# =============================================================================
class TestCoverageConstrained:
"""In constrained scenario, aggregate coverage must be < 1 for all strategies."""
def test_greedy_coverage_below_one(self, matched_greedy_c, demand_tons_c):
"""Under supply shortage, even greedy cannot cover all demand."""
cov = total_deficit_covered(matched_greedy_c, demand_tons_c)
assert cov < 0.99, (
f"Greedy coverage ({cov:.4f}) unexpectedly close to 1.0 in constrained scenario. "
f"Is the fixture actually supply-constrained?"
)
def test_agriflow_coverage_below_one(self, matched_agriflow_c, demand_tons_c):
cov = total_deficit_covered(matched_agriflow_c, demand_tons_c)
assert cov < 0.99, (
f"AgriFlow coverage ({cov:.4f}) unexpectedly close to 1.0 in constrained scenario."
)
def test_both_strategies_above_floor(self, matched_greedy_c, matched_agriflow_c, demand_tons_c):
"""Both strategies must cover at least 30% (engine is not broken)."""
g_cov = total_deficit_covered(matched_greedy_c, demand_tons_c)
a_cov = total_deficit_covered(matched_agriflow_c, demand_tons_c)
assert g_cov > 0.30, f"Greedy coverage ({g_cov:.4f}) below 30% floor."
assert a_cov > 0.30, f"AgriFlow coverage ({a_cov:.4f}) below 30% floor."
# =============================================================================
# 3. Core equity claims under scarcity
# =============================================================================
class TestEquityUnderScarcity:
"""The core pitch claims must hold under supply-constrained conditions."""
def test_greedy_coverage_gte_agriflow(self, matched_greedy_c, matched_agriflow_c, demand_tons_c):
"""Efficiency frontier: greedy >= agriflow on raw volume coverage."""
g = total_deficit_covered(matched_greedy_c, demand_tons_c)
a = total_deficit_covered(matched_agriflow_c, demand_tons_c)
assert g >= a - 1e-9, (
f"Greedy ({g:.4f}) must have >= coverage than AgriFlow ({a:.4f}). "
f"Efficiency-equity tradeoff claim broken."
)
def test_agriflow_gini_lte_greedy(self, matched_agriflow_c, matched_greedy_c, demand_tons_c):
"""AgriFlow must have lower Gini than greedy under scarcity.
This is the key test: in ABUNDANT scenario both are degenerate
(Sampang/Bangkalan fully served regardless). In CONSTRAINED, greedy
abandons the poorest kabs to serve easier nearby nodes; AgriFlow
prioritises them via 1.30x equity boost.
"""
a_gini = gini(matched_agriflow_c, demand_tons_c)
g_gini = gini(matched_greedy_c, demand_tons_c)
assert a_gini <= g_gini + 1e-6, (
f"AgriFlow Gini ({a_gini:.4f}) must be <= greedy Gini ({g_gini:.4f}). "
f"Under scarcity, equity mechanism must improve distributional fairness."
)
def test_sampang_agriflow_gte_greedy(self, matched_agriflow_c, matched_greedy_c, demand_tons_c):
"""Sampang (IPM 66.72) must do >= under AgriFlow vs greedy.
Under CONSTRAINED: greedy routes supply to nearer/higher-scoring nodes,
leaving Sampang (Madura island, relatively remote) unserved. The 1.30x
equity boost in AgriFlow explicitly prioritises Sampang's demand.
"""
a = kab_fulfillment(matched_agriflow_c, demand_tons_c, SAMPANG_ID)
g = kab_fulfillment(matched_greedy_c, demand_tons_c, SAMPANG_ID)
assert a >= g - 1e-9, (
f"Sampang: AgriFlow ({a:.4f}) must be >= greedy ({g:.4f}) under scarcity. "
f"The 1.30x equity boost is specifically calibrated for Sampang (IPM 66.72)."
)
def test_bangkalan_agriflow_gte_greedy(self, matched_agriflow_c, matched_greedy_c, demand_tons_c):
"""Bangkalan (IPM 67.70) must do >= under AgriFlow vs greedy."""
a = kab_fulfillment(matched_agriflow_c, demand_tons_c, BANGKALAN_ID)
g = kab_fulfillment(matched_greedy_c, demand_tons_c, BANGKALAN_ID)
assert a >= g - 1e-9, (
f"Bangkalan: AgriFlow ({a:.4f}) must be >= greedy ({g:.4f}) under scarcity."
)
def test_uniform_gini_lte_agriflow(self, matched_uniform_c, matched_agriflow_c, demand_tons_c):
"""Uniform allocation must have <= Gini than AgriFlow (equity anchor invariant).
Note: in CONSTRAINED, uniform Gini is NOT near-zero because the equal-split
algorithm cannot equalize fulfillment ratios when there is not enough supply.
Nodes connected to more surplus paths get higher ratios. The invariant
'uniform <= agriflow' still holds because uniform ignores score entirely
and distributes supply as evenly as the graph structure allows.
"""
u_gini = gini(matched_uniform_c, demand_tons_c)
a_gini = gini(matched_agriflow_c, demand_tons_c)
assert u_gini <= a_gini + 1e-9, (
f"Uniform gini ({u_gini:.4f}) must be <= AgriFlow gini ({a_gini:.4f}). "
f"Uniform is the equity anchor even under scarcity."
)
# =============================================================================
# 4. Sensitivity ordering under constrained (must not be degenerate on coverage)
# =============================================================================
class TestSensitivityConstrained:
"""Sensitivity check under CONSTRAINED: strict/lenient must differ on at least coverage."""
def test_strict_sampang_gte_lenient(self, constrained_data, logistics, demand_tons_c):
"""Stricter equity gives Sampang >= lenient under scarcity."""
rep_strict = run_matching(
constrained_data["surplus"], constrained_data["deficit"],
logistics=logistics,
equity_fn=equity_strict,
)
rep_lenient = run_matching(
constrained_data["surplus"], constrained_data["deficit"],
logistics=logistics,
equity_fn=equity_lenient,
)
mt_strict = _report_to_matched_tons(rep_strict)
mt_lenient = _report_to_matched_tons(rep_lenient)
s_strict = kab_fulfillment(mt_strict, demand_tons_c, SAMPANG_ID)
s_lenient = kab_fulfillment(mt_lenient, demand_tons_c, SAMPANG_ID)
assert s_strict >= s_lenient - 1e-9, (
f"Strict Sampang ({s_strict:.4f}) must be >= lenient ({s_lenient:.4f}). "
f"Under scarcity, stricter equity weighting should benefit poorest kab."
)
def test_sensitivity_coverage_consistent(self, constrained_data, logistics, demand_tons_c):
"""All three sensitivity variants should have similar aggregate coverage."""
coverages = []
for fn in [equity_strict, equity_lenient]:
rep = run_matching(
constrained_data["surplus"], constrained_data["deficit"],
logistics=logistics,
equity_fn=fn,
)
mt = _report_to_matched_tons(rep)
coverages.append(total_deficit_covered(mt, demand_tons_c))
# Coverage should not diverge by more than 15pp between variants
assert max(coverages) - min(coverages) < 0.15, (
f"Sensitivity coverage spread ({min(coverages):.4f}–{max(coverages):.4f}) "
f"exceeds 15pp. Variants should differ on DISTRIBUTION, not total volume."
)
|