Download eval/test_inventory_math.py from RV302001/restockiq: direct link, hf CLI and curl.
- Browser
- Download file 5.12 kB
-
https://huggingface.co/spaces/RV302001/restockiq/resolve/main/eval/test_inventory_math.py
- Command line
-
hf download hf://spaces/RV302001/restockiq/eval/test_inventory_math.py
-
curl -L -o test_inventory_math.py https://huggingface.co/spaces/RV302001/restockiq/resolve/main/eval/test_inventory_math.py
5.12 kB
| """Hand-checked unit tests for the safety-stock / reorder-point math. | |
| Every expected value below was computed by hand from the formulas in | |
| app/inventory_math.py — see the arithmetic in each test's comments. | |
| """ | |
| import math | |
| import sys | |
| from pathlib import Path | |
| import pytest | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| from app.inventory_math import ( | |
| Z_90, | |
| demand_std_from_quantiles, | |
| recommend, | |
| z_from_service_level, | |
| ) | |
| class TestZFromServiceLevel: | |
| def test_median_is_zero(self): | |
| assert z_from_service_level(0.5) == pytest.approx(0.0, abs=1e-8) | |
| def test_95_percent(self): | |
| # Phi^-1(0.95) = 1.6449 (standard normal table) | |
| assert z_from_service_level(0.95) == pytest.approx(1.6449, abs=1e-3) | |
| def test_90_percent_matches_Z_90_constant(self): | |
| assert z_from_service_level(0.90) == pytest.approx(Z_90, abs=1e-3) | |
| def test_99_percent(self): | |
| # Phi^-1(0.99) = 2.3263 | |
| assert z_from_service_level(0.99) == pytest.approx(2.3263, abs=1e-3) | |
| def test_tail_region(self): | |
| # 0.999 falls in the approximation's upper tail branch; Phi^-1(0.999) = 3.0902 | |
| assert z_from_service_level(0.999) == pytest.approx(3.0902, abs=1e-3) | |
| def test_rejects_out_of_range(self, bad): | |
| with pytest.raises(ValueError): | |
| z_from_service_level(bad) | |
| class TestDemandStd: | |
| def test_constant_spread(self): | |
| # spread = 12.816 - 10 = 2.816 every day; 2.816 / 1.2816 = 2.1972... | |
| p50 = [10.0, 10.0, 10.0] | |
| p90 = [12.816, 12.816, 12.816] | |
| assert demand_std_from_quantiles(p50, p90) == pytest.approx(2.816 / 1.2816, rel=1e-9) | |
| def test_zero_spread_means_zero_std(self): | |
| assert demand_std_from_quantiles([5.0, 5.0], [5.0, 5.0]) == 0.0 | |
| def test_negative_spread_clamped_to_zero(self): | |
| # crossed quantiles (can happen with independently trained models) never | |
| # produce a negative std | |
| assert demand_std_from_quantiles([10.0], [8.0]) == 0.0 | |
| def test_rejects_length_mismatch(self): | |
| with pytest.raises(ValueError): | |
| demand_std_from_quantiles([1.0, 2.0], [1.0]) | |
| class TestRecommend: | |
| def test_hand_checked_full_example(self): | |
| # 7-day horizon, p50 = 10/day, p90 = 12.5632/day everywhere: | |
| # avg_daily_demand = 10 | |
| # demand_std = (12.5632 - 10) / 1.2816 = 2.0 | |
| # z(0.95) = 1.6449 | |
| # safety_stock = 1.6449 * 2.0 * sqrt(7) = 8.7040... | |
| # reorder_point = 10 * 7 + 8.7040 = 78.7040... | |
| # order_qty (inventory 20) = 78.7040 - 20 = 58.7040... | |
| p50 = [10.0] * 7 | |
| p90 = [12.5632] * 7 | |
| rec = recommend(p50, p90, current_inventory=20.0, service_level=0.95, | |
| lead_time_days=7) | |
| expected_ss = 1.6449 * 2.0 * math.sqrt(7) | |
| assert rec.avg_daily_demand == pytest.approx(10.0) | |
| assert rec.demand_std == pytest.approx(2.0, rel=1e-9) | |
| assert rec.safety_stock == pytest.approx(expected_ss, abs=2e-3) | |
| assert rec.reorder_point == pytest.approx(70.0 + expected_ss, abs=2e-3) | |
| assert rec.suggested_order_qty == pytest.approx(50.0 + expected_ss, abs=2e-3) | |
| def test_zero_uncertainty_reduces_to_deterministic_reorder(self): | |
| # p90 == p50 -> no safety stock; reorder point is just demand * lead time | |
| rec = recommend([4.0] * 7, [4.0] * 7, current_inventory=0.0, | |
| service_level=0.95, lead_time_days=7) | |
| assert rec.safety_stock == 0.0 | |
| assert rec.reorder_point == pytest.approx(28.0) | |
| assert rec.suggested_order_qty == pytest.approx(28.0) | |
| def test_order_qty_floors_at_zero(self): | |
| # plenty of inventory -> no order, never negative | |
| rec = recommend([1.0] * 7, [1.5] * 7, current_inventory=1000.0) | |
| assert rec.suggested_order_qty == 0.0 | |
| def test_only_lead_time_days_are_used(self): | |
| # extra forecast days beyond the lead time must not affect the answer | |
| base = recommend([10.0] * 7, [12.0] * 7, lead_time_days=7) | |
| extended = recommend([10.0] * 7 + [999.0] * 21, [12.0] * 7 + [999.0] * 21, | |
| lead_time_days=7) | |
| assert base.reorder_point == extended.reorder_point | |
| def test_lead_time_scaling(self): | |
| # lead time 4 with constant demand: reorder = 5*4 + z*std*sqrt(4) = 20 + 2*z*std | |
| # p90-p50 = 1.2816 -> std = 1.0 exactly; z(0.90) = 1.2816 | |
| rec = recommend([5.0] * 4, [6.2816] * 4, current_inventory=0.0, | |
| service_level=0.90, lead_time_days=4) | |
| assert rec.demand_std == pytest.approx(1.0, rel=1e-9) | |
| assert rec.safety_stock == pytest.approx(1.2816 * 1.0 * 2.0, abs=2e-3) | |
| assert rec.reorder_point == pytest.approx(20.0 + 2.5632, abs=2e-3) | |
| def test_rejects_short_forecast(self): | |
| with pytest.raises(ValueError): | |
| recommend([1.0] * 3, [2.0] * 3, lead_time_days=7) | |
| def test_rejects_bad_lead_time(self): | |
| with pytest.raises(ValueError): | |
| recommend([1.0] * 7, [2.0] * 7, lead_time_days=0) | |