BatterySwapAI2026-MnesisLab / tests /test_competition_model.py
CarlAlbertCode's picture
Submission 006: official scenario interface and causal degradation model
dfa8acb
Raw
History Blame Contribute Delete
4.82 kB
from __future__ import annotations
import pytest
import numpy as np
from batteryswapai.competition_model import FrozenLogisticCalibrator
def test_frozen_logistic_calibrator_is_stable_and_normalized() -> None:
calibrator = FrozenLogisticCalibrator(
coefficients=np.array([1.5, -0.5]),
intercept=-0.25,
)
values = np.array([[0.0, 0.0], [1000.0, -1000.0], [-1000.0, 1000.0]])
probabilities = calibrator.predict_proba(values)
assert probabilities.shape == (3, 2)
assert np.isfinite(probabilities).all()
np.testing.assert_allclose(probabilities.sum(axis=1), 1.0)
assert probabilities[1, 1] == 1.0
assert probabilities[2, 1] == 0.0
def test_blended_risk_keeps_the_calibrated_scale_and_ranks_within_scenario():
import numpy as np
import pandas as pd
from batteryswapai.competition_model import _map_to_scale, _scenario_ranks
groups = np.array(["s_0", "s_0", "s_0", "s_1", "s_1", "s_1"])
reference = np.array([0.10, 0.20, 0.30, 0.01, 0.02, 0.03])
blended = np.array([0.9, 0.1, 0.5, 0.2, 0.9, 0.5])
mapped = _map_to_scale(blended, reference, groups)
# the calibrated values are reused exactly, only the ordering changes
assert sorted(mapped[:3]) == sorted(reference[:3])
assert sorted(mapped[3:]) == sorted(reference[3:])
assert mapped[0] == 0.30 and mapped[1] == 0.10
assert mapped[4] == 0.03 and mapped[3] == 0.01
ranks = _scenario_ranks(blended, groups)
assert ranks[0] == pytest.approx(1.0) and ranks[1] == pytest.approx(1 / 3)
def test_context_columns_describe_one_scenario_and_are_not_model_features():
import pandas as pd
from batteryswapai.competition_features import (
CONTEXT_COLUMNS,
add_context_columns,
numeric_feature_columns,
)
from batteryswapai.competition_model import _context_features
snapshot = add_context_columns(
pd.DataFrame(
{
"battery": ["d_a", "d_b", "d_c"],
"building": ["b_1", "b_1", "b_2"],
"room": ["r_1", "r_1", "r_2"],
"stable_voltage_median": [2.40, 2.60, 2.90],
}
)
)
assert snapshot["room_voltage_mean"].tolist() == [2.5, 2.5, 2.9]
assert snapshot["room_low_count"].tolist() == [1.0, 1.0, 0.0]
assert snapshot["scenario_low_fraction"].tolist() == [pytest.approx(1 / 3)] * 3
# the base ensemble must not train on scenario-level context
assert not set(CONTEXT_COLUMNS) & set(numeric_feature_columns(snapshot))
# the auxiliary model reads the precomputed columns rather than regrouping
assert list(_context_features(snapshot).columns) == list(CONTEXT_COLUMNS)
def test_trajectory_blend_leaves_short_history_batteries_where_they_were():
import numpy as np
from batteryswapai.competition_model import blend_trajectory
groups = np.array(["s_0"] * 4)
primary = np.array([0.40, 0.30, 0.20, 0.10])
residual = np.array([0.10, 0.20, 0.30, 0.40]) # exactly reversed
unchanged = blend_trajectory(primary, residual, np.zeros(4), groups)
assert np.allclose(unchanged, primary)
nudged = blend_trajectory(primary, residual, np.full(4, 0.25), groups)
assert sorted(nudged) == sorted(primary) # calibrated scale is preserved
# a quarter weight is a nudge, not a takeover; a dominant weight does reorder
strong = blend_trajectory(primary, residual, np.full(4, 0.9), groups)
assert sorted(strong) == sorted(primary)
assert not np.allclose(strong, primary)
# a battery with no usable history keeps its own position while the others move
partial = blend_trajectory(primary, residual, np.array([0.0, 0.25, 0.25, 0.0]), groups)
assert sorted(partial) == sorted(primary)
def test_trajectory_blend_is_deterministic_and_scenario_local():
import numpy as np
from batteryswapai.competition_model import blend_trajectory
groups = np.array(["s_0", "s_0", "s_1", "s_1"])
primary = np.array([0.9, 0.1, 0.02, 0.01])
residual = np.array([0.1, 0.9, 0.01, 0.02])
first = blend_trajectory(primary, residual, np.full(4, 0.25), groups)
second = blend_trajectory(primary, residual, np.full(4, 0.25), groups)
assert np.array_equal(first, second)
# scenario 0 values never leak into scenario 1
assert sorted(first[:2]) == sorted(primary[:2])
assert sorted(first[2:]) == sorted(primary[2:])
def test_model_without_a_trajectory_stack_returns_the_blend_unchanged():
import numpy as np
import pandas as pd
from batteryswapai.competition_model import EventTimeModel
model = EventTimeModel.__new__(EventTimeModel)
model.residual_model = None
model.forecasters = None
risk, weight = model._residual_risk(pd.DataFrame({"battery": ["d_a"]}))
assert risk is None and weight is None