| 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) |
| |
| 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 |
|
|
| |
| assert not set(CONTEXT_COLUMNS) & set(numeric_feature_columns(snapshot)) |
| |
| 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]) |
|
|
| 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) |
| |
| strong = blend_trajectory(primary, residual, np.full(4, 0.9), groups) |
| assert sorted(strong) == sorted(primary) |
| assert not np.allclose(strong, primary) |
|
|
| |
| 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) |
| |
| 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 |
|
|