From 7bf5b538a7c9df1efacf5fadfe52c471c3d3ae95 Mon Sep 17 00:00:00 2001 From: jayson_mulwa Date: Thu, 2 Jul 2026 17:51:07 +0300 Subject: [PATCH 1/5] refactor: add underscore separators to numeric literals for readability Apply PEP 515 underscore separators to all numeric literals >= 1000 across src/, tests/, and the notebook demo. e.g. 50000.0 -> 50_000.0, 1000.0 -> 1_000.0. Datetime year arguments left unchanged. --- examples/notebook_demo.ipynb | 137 ++---------------- src/matledger/api/app.py | 2 +- src/matledger/projections/framework.py | 4 +- src/matledger/sdk/client.py | 2 +- tests/conftest.py | 2 +- tests/integration/test_end_to_end.py | 22 +-- .../test_acquisition_efficiency.py | 6 +- tests/test_analytics/test_calibration.py | 10 +- tests/test_analytics/test_data_quality.py | 6 +- tests/test_analytics/test_discovery_rate.py | 28 ++-- tests/test_analytics/test_diversity.py | 6 +- tests/test_analytics/test_diversity_faiss.py | 8 +- tests/test_analytics/test_explore_exploit.py | 6 +- tests/test_analytics/test_model_learning.py | 10 +- tests/test_analytics/test_pareto.py | 10 +- tests/test_analytics/test_proxy_gaming.py | 22 +-- tests/test_analytics/test_regret.py | 6 +- tests/test_analytics/test_reproducibility.py | 10 +- tests/test_api/test_app.py | 26 ++-- tests/test_commands/test_handler.py | 104 ++++++------- tests/test_commands/test_ingest.py | 6 +- tests/test_compliance/test_alcoa.py | 6 +- tests/test_decisions/test_counterfactual.py | 20 +-- tests/test_decisions/test_policy.py | 12 +- tests/test_events/test_catalog.py | 12 +- tests/test_events/test_envelope.py | 6 +- tests/test_projections/test_budget.py | 38 ++--- .../test_iteration_summary.py | 24 +-- tests/test_projections/test_negledger.py | 40 ++--- tests/test_projections/test_snapshots.py | 14 +- tests/test_projections/test_timeline.py | 10 +- tests/test_report/test_html.py | 6 +- tests/test_report/test_plots.py | 16 +- tests/test_sdk/test_client.py | 22 +-- tests/test_store/test_hash_chain.py | 4 +- tests/test_store/test_property_based.py | 10 +- tests/test_store/test_sqlite.py | 2 +- 37 files changed, 282 insertions(+), 393 deletions(-) diff --git a/examples/notebook_demo.ipynb b/examples/notebook_demo.ipynb index 473c431..0898049 100644 --- a/examples/notebook_demo.ipynb +++ b/examples/notebook_demo.ipynb @@ -16,31 +16,11 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "id": "setup", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Campaign created.\n" - ] - } - ], - "source": [ - "%matplotlib inline\n", - "from matledger.sdk import CampaignClient\n", - "\n", - "# Create an in-memory campaign (use a file path for persistence)\n", - "client = CampaignClient(\":memory:\")\n", - "client.create_campaign(\n", - " campaign_id=\"perovskite-opt\",\n", - " goal=\"maximize bandgap\",\n", - " budget_cap=50000.0,\n", - ")\n", - "print(\"Campaign created.\")" - ] + "outputs": [], + "source": "%matplotlib inline\nfrom matledger.sdk import CampaignClient\n\n# Create an in-memory campaign (use a file path for persistence)\nclient = CampaignClient(\":memory:\")\nclient.create_campaign(\n campaign_id=\"perovskite-opt\",\n goal=\"maximize bandgap\",\n budget_cap=50_000.0,\n)\nprint(\"Campaign created.\")" }, { "cell_type": "markdown", @@ -83,43 +63,11 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "id": "iter1-results", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iteration 1 complete.\n" - ] - } - ], - "source": [ - "# Start experiments (commits budget)\n", - "for cid, _, _ in candidates_iter1:\n", - " client.start_experiment(cid, cost=5000.0, iteration=1)\n", - "\n", - "# Record validated lab results\n", - "client.record_result(\"CsPbI3\", value=1.68, uncertainty=0.05, iteration=1)\n", - "client.record_result(\"CsPbBr3\", value=2.25, uncertainty=0.03, iteration=1)\n", - "client.record_result(\"MAPbI3\", value=1.50, uncertainty=0.04, iteration=1)\n", - "client.record_result(\"FAPbI3\", value=1.45, uncertainty=0.06, iteration=1)\n", - "\n", - "# CsSnI3 failed — tin oxidises in air\n", - "client.record_failure(\"CsSnI3\", failure_class=\"stability\", iteration=1,\n", - " detail=\"Sn2+ oxidation under ambient conditions\")\n", - "\n", - "# Select the best, reject the failure\n", - "client.select(\"CsPbBr3\", rationale=\"Highest validated bandgap\",\n", - " model_version=\"v1.0\", iteration=1)\n", - "client.reject(\"CsSnI3\", rationale=\"Stability failure\",\n", - " model_version=\"v1.0\", iteration=1)\n", - "\n", - "# Close iteration 1\n", - "client.close_iteration(1, summary=\"5 candidates tested, 1 stability failure\")\n", - "print(\"Iteration 1 complete.\")" - ] + "outputs": [], + "source": "# Start experiments (commits budget)\nfor cid, _, _ in candidates_iter1:\n client.start_experiment(cid, cost=5_000.0, iteration=1)\n\n# Record validated lab results\nclient.record_result(\"CsPbI3\", value=1.68, uncertainty=0.05, iteration=1)\nclient.record_result(\"CsPbBr3\", value=2.25, uncertainty=0.03, iteration=1)\nclient.record_result(\"MAPbI3\", value=1.50, uncertainty=0.04, iteration=1)\nclient.record_result(\"FAPbI3\", value=1.45, uncertainty=0.06, iteration=1)\n\n# CsSnI3 failed — tin oxidises in air\nclient.record_failure(\"CsSnI3\", failure_class=\"stability\", iteration=1,\n detail=\"Sn2+ oxidation under ambient conditions\")\n\n# Select the best, reject the failure\nclient.select(\"CsPbBr3\", rationale=\"Highest validated bandgap\",\n model_version=\"v1.0\", iteration=1)\nclient.reject(\"CsSnI3\", rationale=\"Stability failure\",\n model_version=\"v1.0\", iteration=1)\n\n# Close iteration 1\nclient.close_iteration(1, summary=\"5 candidates tested, 1 stability failure\")\nprint(\"Iteration 1 complete.\")" }, { "cell_type": "markdown", @@ -131,42 +79,11 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "iter2", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iteration 2 complete.\n" - ] - } - ], - "source": [ - "# Surrogate retrained on iteration 1 data\n", - "client.update_model(model_version=\"v2.0\", iteration=2,\n", - " summary=\"Retrained on 4 results + 1 failure\")\n", - "\n", - "candidates_iter2 = [\n", - " (\"CsPb05Sn05I3\", {\"Cs\": 1, \"Pb\": 0.5, \"Sn\": 0.5, \"I\": 3}, 1.60),\n", - " (\"FA08Cs02PbI3\", {\"FA\": 0.8, \"Cs\": 0.2, \"Pb\": 1, \"I\": 3}, 1.52),\n", - " (\"CsPbI2Br\", {\"Cs\": 1, \"Pb\": 1, \"I\": 2, \"Br\": 1}, 1.92),\n", - "]\n", - "\n", - "for cid, features, predicted in candidates_iter2:\n", - " client.propose(cid, features, predicted, model_version=\"v2.0\", iteration=2)\n", - " client.start_experiment(cid, cost=5000.0, iteration=2)\n", - "\n", - "client.record_result(\"CsPb05Sn05I3\", value=1.55, uncertainty=0.04, iteration=2)\n", - "client.record_result(\"FA08Cs02PbI3\", value=1.58, uncertainty=0.03, iteration=2)\n", - "client.record_result(\"CsPbI2Br\", value=1.88, uncertainty=0.04, iteration=2)\n", - "\n", - "client.select(\"CsPbI2Br\", rationale=\"Best mixed-halide bandgap\",\n", - " model_version=\"v2.0\", iteration=2)\n", - "client.close_iteration(2, summary=\"3 candidates tested, all successful\")\n", - "print(\"Iteration 2 complete.\")" - ] + "outputs": [], + "source": "# Surrogate retrained on iteration 1 data\nclient.update_model(model_version=\"v2.0\", iteration=2,\n summary=\"Retrained on 4 results + 1 failure\")\n\ncandidates_iter2 = [\n (\"CsPb05Sn05I3\", {\"Cs\": 1, \"Pb\": 0.5, \"Sn\": 0.5, \"I\": 3}, 1.60),\n (\"FA08Cs02PbI3\", {\"FA\": 0.8, \"Cs\": 0.2, \"Pb\": 1, \"I\": 3}, 1.52),\n (\"CsPbI2Br\", {\"Cs\": 1, \"Pb\": 1, \"I\": 2, \"Br\": 1}, 1.92),\n]\n\nfor cid, features, predicted in candidates_iter2:\n client.propose(cid, features, predicted, model_version=\"v2.0\", iteration=2)\n client.start_experiment(cid, cost=5_000.0, iteration=2)\n\nclient.record_result(\"CsPb05Sn05I3\", value=1.55, uncertainty=0.04, iteration=2)\nclient.record_result(\"FA08Cs02PbI3\", value=1.58, uncertainty=0.03, iteration=2)\nclient.record_result(\"CsPbI2Br\", value=1.88, uncertainty=0.04, iteration=2)\n\nclient.select(\"CsPbI2Br\", rationale=\"Best mixed-halide bandgap\",\n model_version=\"v2.0\", iteration=2)\nclient.close_iteration(2, summary=\"3 candidates tested, all successful\")\nprint(\"Iteration 2 complete.\")" }, { "cell_type": "markdown", @@ -178,39 +95,11 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "id": "iter3", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Iteration 3 complete.\n" - ] - } - ], - "source": [ - "client.update_model(model_version=\"v3.0\", iteration=3,\n", - " summary=\"Retrained on 7 results + 1 failure\")\n", - "\n", - "candidates_iter3 = [\n", - " (\"CsPbIBr2\", {\"Cs\": 1, \"Pb\": 1, \"I\": 1, \"Br\": 2}, 2.10),\n", - " (\"CsPbBr2Cl\", {\"Cs\": 1, \"Pb\": 1, \"Br\": 2, \"Cl\": 1}, 2.40),\n", - "]\n", - "\n", - "for cid, features, predicted in candidates_iter3:\n", - " client.propose(cid, features, predicted, model_version=\"v3.0\", iteration=3)\n", - " client.start_experiment(cid, cost=5000.0, iteration=3)\n", - "\n", - "client.record_result(\"CsPbIBr2\", value=2.05, uncertainty=0.03, iteration=3)\n", - "client.record_result(\"CsPbBr2Cl\", value=2.35, uncertainty=0.04, iteration=3)\n", - "\n", - "client.select(\"CsPbBr2Cl\", rationale=\"Highest bandgap observed\",\n", - " model_version=\"v3.0\", iteration=3)\n", - "client.close_iteration(3, summary=\"2 candidates, converging on mixed halides\")\n", - "print(\"Iteration 3 complete.\")" - ] + "outputs": [], + "source": "client.update_model(model_version=\"v3.0\", iteration=3,\n summary=\"Retrained on 7 results + 1 failure\")\n\ncandidates_iter3 = [\n (\"CsPbIBr2\", {\"Cs\": 1, \"Pb\": 1, \"I\": 1, \"Br\": 2}, 2.10),\n (\"CsPbBr2Cl\", {\"Cs\": 1, \"Pb\": 1, \"Br\": 2, \"Cl\": 1}, 2.40),\n]\n\nfor cid, features, predicted in candidates_iter3:\n client.propose(cid, features, predicted, model_version=\"v3.0\", iteration=3)\n client.start_experiment(cid, cost=5_000.0, iteration=3)\n\nclient.record_result(\"CsPbIBr2\", value=2.05, uncertainty=0.03, iteration=3)\nclient.record_result(\"CsPbBr2Cl\", value=2.35, uncertainty=0.04, iteration=3)\n\nclient.select(\"CsPbBr2Cl\", rationale=\"Highest bandgap observed\",\n model_version=\"v3.0\", iteration=3)\nclient.close_iteration(3, summary=\"2 candidates, converging on mixed halides\")\nprint(\"Iteration 3 complete.\")" }, { "cell_type": "markdown", @@ -1054,4 +943,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/src/matledger/api/app.py b/src/matledger/api/app.py index cebbc94..ca8b056 100644 --- a/src/matledger/api/app.py +++ b/src/matledger/api/app.py @@ -195,7 +195,7 @@ def create_app( async def log_requests(request: Request, call_next): # type: ignore[no-untyped-def] t0 = time.monotonic() response = await call_next(request) - elapsed_ms = (time.monotonic() - t0) * 1000 + elapsed_ms = (time.monotonic() - t0) * 1_000 logger.info( "request method=%s path=%s status=%d elapsed_ms=%.1f", request.method, diff --git a/src/matledger/projections/framework.py b/src/matledger/projections/framework.py index 81249fd..2fbac0f 100644 --- a/src/matledger/projections/framework.py +++ b/src/matledger/projections/framework.py @@ -116,7 +116,7 @@ def rebuild(self, events: Iterator[Event]) -> int: for event in events: self.apply(event) count += 1 - elapsed_ms = (time.monotonic() - t0) * 1000 + elapsed_ms = (time.monotonic() - t0) * 1_000 logger.info("rebuild_complete events=%d elapsed_ms=%.1f", count, elapsed_ms) return count @@ -129,7 +129,7 @@ def apply_from(self, events: Iterator[Event]) -> int: self.apply(event) count += 1 if count > 0: - elapsed_ms = (time.monotonic() - t0) * 1000 + elapsed_ms = (time.monotonic() - t0) * 1_000 logger.info("apply_from events=%d elapsed_ms=%.1f", count, elapsed_ms) return count diff --git a/src/matledger/sdk/client.py b/src/matledger/sdk/client.py index bdbfea2..221301b 100644 --- a/src/matledger/sdk/client.py +++ b/src/matledger/sdk/client.py @@ -47,7 +47,7 @@ class CampaignClient: from matledger.sdk import CampaignClient client = CampaignClient("my_campaign.db") - client.create_campaign("perovskite-opt", "maximize bandgap", 50000.0) + client.create_campaign("perovskite-opt", "maximize bandgap", 50_000.0) client.propose("CsPbI3", {"Cs": 1, "Pb": 1, "I": 3}, 1.73, "v1.0", 1) client.record_result("CsPbI3", 1.68, 0.05, 1) print(client.proxy_gaming()) diff --git a/tests/conftest.py b/tests/conftest.py index 176ef3f..dade9ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ def sample_campaign_created_event() -> Event: payload={ "campaign_id": "camp-001", "goal": "Find thermally stable perovskites", - "budget_cap": 50000.0, + "budget_cap": 50_000.0, "space_spec": {"composition": ["ABX3"], "elements": ["Cs", "Pb", "I", "Br"]}, }, prev_hash="", diff --git a/tests/integration/test_end_to_end.py b/tests/integration/test_end_to_end.py index f3d1d00..f7ddaf3 100644 --- a/tests/integration/test_end_to_end.py +++ b/tests/integration/test_end_to_end.py @@ -27,7 +27,7 @@ def test_full_campaign(self) -> None: handler.create_campaign( campaign_id="perovskite-001", goal="Find thermally stable perovskites with bandgap > 1.5 eV", - budget_cap=50000.0, + budget_cap=50_000.0, actor="scientist:jayson", ) @@ -60,9 +60,9 @@ def test_full_campaign(self) -> None: actor="agent:bo-v2", ) - handler.start_experiment("CsPbI3", 5000.0, 1, caused_by=[c1.seq]) - handler.start_experiment("CsPbBr3", 5000.0, 1, caused_by=[c2.seq]) - handler.start_experiment("MAPbI3", 5000.0, 1, caused_by=[c3.seq]) + handler.start_experiment("CsPbI3", 5_000.0, 1, caused_by=[c1.seq]) + handler.start_experiment("CsPbBr3", 5_000.0, 1, caused_by=[c2.seq]) + handler.start_experiment("MAPbI3", 5_000.0, 1, caused_by=[c3.seq]) r1 = handler.record_result("CsPbI3", 1.68, 0.05, 1, "XRD-2026-001", actor="lims") handler.record_result("CsPbBr3", 2.31, 0.08, 1, "XRD-2026-002", actor="lims") @@ -101,7 +101,7 @@ def test_full_campaign(self) -> None: 2, actor="agent:bo-v2", ) - handler.start_experiment("CsSnI3", 5000.0, 2, caused_by=[c4.seq]) + handler.start_experiment("CsSnI3", 5_000.0, 2, caused_by=[c4.seq]) handler.record_failure( "CsSnI3", "synthesizability", @@ -120,7 +120,7 @@ def test_full_campaign(self) -> None: 3, actor="agent:bo-v2", ) - handler.start_experiment("CsPb0.5Sn0.5I3", 5000.0, 3, caused_by=[c5.seq]) + handler.start_experiment("CsPb0.5Sn0.5I3", 5_000.0, 3, caused_by=[c5.seq]) handler.record_result("CsPb0.5Sn0.5I3", 1.42, 0.03, 3, "XRD-2026-003", actor="lims") handler.select_candidate( "CsPb0.5Sn0.5I3", @@ -161,8 +161,8 @@ def test_full_campaign(self) -> None: assert nl.failed_candidates() == {"MAPbI3", "CsSnI3"} # --- Verify budget --- - assert bp.state.budget_cap == 50000.0 - assert bp.state.total_consumed == 25000.0 # 5 experiments × $5000 + assert bp.state.budget_cap == 50_000.0 + assert bp.state.total_consumed == 25_000.0 # 5 experiments × $5000 assert bp.state.experiments_started == 5 assert not bp.state.is_over_cap @@ -189,11 +189,11 @@ def test_rebuild_is_complete(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("test", "test", 10000.0) + handler.create_campaign("test", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.6, "v1", 1) - handler.start_experiment("c1", cost=1000.0, iteration=1) - handler.start_experiment("c2", cost=1000.0, iteration=1) + handler.start_experiment("c1", cost=1_000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) handler.record_result("c1", 0.45, 0.02, 1) handler.record_failure("c2", "out_of_spec", 1) diff --git a/tests/test_analytics/test_acquisition_efficiency.py b/tests/test_analytics/test_acquisition_efficiency.py index 12805d6..a645302 100644 --- a/tests/test_analytics/test_acquisition_efficiency.py +++ b/tests/test_analytics/test_acquisition_efficiency.py @@ -30,7 +30,7 @@ def _build_inefficient_campaign() -> SQLiteEventStore: """Campaign where predicted rankings don't match actual outcomes.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("acq-bad", "test acq", 100000.0) + handler.create_campaign("acq-bad", "test acq", 100_000.0) for iteration in range(1, 7): for j in range(3): @@ -58,7 +58,7 @@ def _build_efficient_campaign() -> SQLiteEventStore: """Campaign where predicted rankings match actual outcomes.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("acq-ok", "test acq ok", 100000.0) + handler.create_campaign("acq-ok", "test acq ok", 100_000.0) for iteration in range(1, 7): for j in range(3): @@ -115,7 +115,7 @@ def test_correlation_trend_populated(self) -> None: def test_too_few_iterations_ok(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("short", "short", 10000.0) + handler.create_campaign("short", "short", 10_000.0) handler.propose_candidate("c1", {"x": 1}, 1.0, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 2.0, 0.05, 1) diff --git a/tests/test_analytics/test_calibration.py b/tests/test_analytics/test_calibration.py index af48f49..27c185b 100644 --- a/tests/test_analytics/test_calibration.py +++ b/tests/test_analytics/test_calibration.py @@ -16,7 +16,7 @@ def _build_miscalibrated_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("cal-test", "test calibration", 100000.0) + handler.create_campaign("cal-test", "test calibration", 100_000.0) for iteration in range(1, 7): predicted = 2.0 + iteration * 0.1 @@ -28,7 +28,7 @@ def _build_miscalibrated_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", validated, 0.01, iteration) handler.close_iteration(iteration) @@ -39,7 +39,7 @@ def _build_calibrated_campaign() -> SQLiteEventStore: """Campaign where the surrogate's uncertainty covers validated results.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("cal-ok", "test calibration ok", 100000.0) + handler.create_campaign("cal-ok", "test calibration ok", 100_000.0) for iteration in range(1, 7): predicted = 2.0 + iteration * 0.1 @@ -51,7 +51,7 @@ def _build_calibrated_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", validated, 0.1, iteration) handler.close_iteration(iteration) @@ -90,7 +90,7 @@ def test_coverage_trend_populated(self) -> None: def test_too_few_iterations_ok(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("short", "short", 10000.0) + handler.create_campaign("short", "short", 10_000.0) handler.propose_candidate("c1", {"x": 1}, 1.0, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 5.0, 0.01, 1) diff --git a/tests/test_analytics/test_data_quality.py b/tests/test_analytics/test_data_quality.py index 10ab256..94dc328 100644 --- a/tests/test_analytics/test_data_quality.py +++ b/tests/test_analytics/test_data_quality.py @@ -16,7 +16,7 @@ def _build_noisy_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("noisy", "test noise", 100000.0) + handler.create_campaign("noisy", "test noise", 100_000.0) for iteration in range(1, 7): normal_vals = [1.0, 1.02, 0.98, 1.01, 0.99, 1.03, 0.97, 1.04, 0.96, 1.05] @@ -41,7 +41,7 @@ def _build_clean_campaign() -> SQLiteEventStore: """Campaign with consistent, outlier-free measurements.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("clean", "test clean", 100000.0) + handler.create_campaign("clean", "test clean", 100_000.0) for iteration in range(1, 7): vals = [1.0, 1.02, 0.98, 1.01, 0.99, 1.03, 0.97] @@ -90,7 +90,7 @@ def test_anomaly_rate_trend_populated(self) -> None: def test_too_few_observations_skipped(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("tiny", "tiny", 10000.0) + handler.create_campaign("tiny", "tiny", 10_000.0) handler.propose_candidate("c1", {"x": 1}, 1.0, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 1.0, 0.05, 1) diff --git a/tests/test_analytics/test_discovery_rate.py b/tests/test_analytics/test_discovery_rate.py index 1b6e0cf..15142cc 100644 --- a/tests/test_analytics/test_discovery_rate.py +++ b/tests/test_analytics/test_discovery_rate.py @@ -18,7 +18,7 @@ def _build_flatlined_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("flatline-test", "test flatline", 60000.0) + handler.create_campaign("flatline-test", "test flatline", 60_000.0) # Iterations 1-3: productive for iteration in range(1, 4): @@ -29,7 +29,7 @@ def _build_flatlined_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 5000.0, iteration) + handler.start_experiment(f"c{iteration}", 5_000.0, iteration) handler.record_result(f"c{iteration}", 1.0 + iteration * 0.1, 0.05, iteration) handler.close_iteration(iteration) @@ -42,7 +42,7 @@ def _build_flatlined_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 5000.0, iteration) + handler.start_experiment(f"c{iteration}", 5_000.0, iteration) handler.record_failure(f"c{iteration}", "synthesizability", iteration) handler.close_iteration(iteration) @@ -69,7 +69,7 @@ def test_healthy_campaign_no_flatline(self) -> None: """A campaign that keeps discovering should not report flatline.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("healthy", "test", 50000.0) + handler.create_campaign("healthy", "test", 50_000.0) for iteration in range(1, 6): handler.propose_candidate( @@ -79,7 +79,7 @@ def test_healthy_campaign_no_flatline(self) -> None: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 5000.0, iteration) + handler.start_experiment(f"c{iteration}", 5_000.0, iteration) handler.record_result(f"c{iteration}", 1.0 + iteration * 0.1, 0.05, iteration) handler.close_iteration(iteration) @@ -97,12 +97,12 @@ def test_cost_per_result(self) -> None: """Overall cost per result is computed correctly.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("cost-test", "test", 50000.0) + handler.create_campaign("cost-test", "test", 50_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 3000.0, 1) - handler.start_experiment("c2", 7000.0, 1) + handler.start_experiment("c1", 3_000.0, 1) + handler.start_experiment("c2", 7_000.0, 1) handler.record_result("c1", 1.5, 0.1, 1) analyzer = DiscoveryRateAnalyzer() @@ -111,16 +111,16 @@ def test_cost_per_result(self) -> None: registry.rebuild(store.iter_from(0)) verdict = analyzer.analyze() - assert verdict.overall_cost_per_result == 10000.0 # $10k / 1 result - assert verdict.total_cost == 10000.0 + assert verdict.overall_cost_per_result == 10_000.0 # $10k / 1 result + assert verdict.total_cost == 10_000.0 def test_no_results_infinite_cost(self) -> None: """If no results, cost per result is infinite.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("no-results", "test", 10000.0) + handler.create_campaign("no-results", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 5000.0, 1) + handler.start_experiment("c1", 5_000.0, 1) handler.record_failure("c1", "stability", 1) analyzer = DiscoveryRateAnalyzer() @@ -166,10 +166,10 @@ def test_closed_empty_iterations_count_toward_flatline(self) -> None: """Closed iterations with zero events still count toward flatline.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("gap-test", "test gaps", 50000.0) + handler.create_campaign("gap-test", "test gaps", 50_000.0) handler.propose_candidate("c1", {"x": 1.0}, 1.0, "v1.0", 1) - handler.start_experiment("c1", 5000.0, 1) + handler.start_experiment("c1", 5_000.0, 1) handler.record_result("c1", 1.5, 0.05, 1) handler.close_iteration(1) diff --git a/tests/test_analytics/test_diversity.py b/tests/test_analytics/test_diversity.py index 07a6123..9626ea9 100644 --- a/tests/test_analytics/test_diversity.py +++ b/tests/test_analytics/test_diversity.py @@ -27,7 +27,7 @@ def _build_collapsing_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("collapse-test", "detect collapse", 100000.0) + handler.create_campaign("collapse-test", "detect collapse", 100_000.0) for iteration in range(1, 7): # Spread contracts: iteration 1 uses full [0,10], iteration 6 uses [4.5,5.5] @@ -61,7 +61,7 @@ def _build_exploring_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("explore-test", "stay diverse", 100000.0) + handler.create_campaign("explore-test", "stay diverse", 100_000.0) for iteration in range(1, 7): # Spread grows: candidates explore wider as iterations progress @@ -134,7 +134,7 @@ def test_too_few_iterations_returns_ok(self) -> None: """With fewer iterations than the window, verdict is OK.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("short", "test", 10000.0) + handler.create_campaign("short", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0, "y": 2.0}, 1.0, "v1", 1) handler.propose_candidate("c2", {"x": 5.0, "y": 6.0}, 1.0, "v1", 1) diff --git a/tests/test_analytics/test_diversity_faiss.py b/tests/test_analytics/test_diversity_faiss.py index 2fe0de0..4264ab8 100644 --- a/tests/test_analytics/test_diversity_faiss.py +++ b/tests/test_analytics/test_diversity_faiss.py @@ -111,7 +111,7 @@ def tracking_python(vectors: list[list[float]]) -> float: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("test", "test", 10000.0) + handler.create_campaign("test", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0, "y": 2.0}, 1.0, "v1", 1) handler.propose_candidate("c2", {"x": 5.0, "y": 6.0}, 1.0, "v1", 1) @@ -143,7 +143,7 @@ def tracking_python(vectors: list[list[float]]) -> float: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("test", "test", 10000.0) + handler.create_campaign("test", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0, "y": 2.0}, 1.0, "v1", 1) handler.propose_candidate("c2", {"x": 5.0, "y": 6.0}, 1.0, "v1", 1) @@ -164,7 +164,7 @@ def test_collapsing_campaign_detected(self) -> None: """Collapsing fixture must still be flagged with numpy backend.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("collapse", "test", 100000.0) + handler.create_campaign("collapse", "test", 100_000.0) for iteration in range(1, 7): spread_factor = 5.0 / iteration @@ -191,7 +191,7 @@ def test_healthy_campaign_not_flagged(self) -> None: """Healthy fixture must not be flagged with numpy backend.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("explore", "test", 100000.0) + handler.create_campaign("explore", "test", 100_000.0) for iteration in range(1, 7): spread_factor = 2.0 + iteration * 0.5 diff --git a/tests/test_analytics/test_explore_exploit.py b/tests/test_analytics/test_explore_exploit.py index ad15511..e3bf84a 100644 --- a/tests/test_analytics/test_explore_exploit.py +++ b/tests/test_analytics/test_explore_exploit.py @@ -12,7 +12,7 @@ def _build_overexploiting_campaign() -> SQLiteEventStore: """Campaign where all proposals target the same high-predicted region.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("exploit-bad", "test exploit", 100000.0) + handler.create_campaign("exploit-bad", "test exploit", 100_000.0) for iteration in range(1, 7): for j in range(3): @@ -35,7 +35,7 @@ def _build_overexploring_campaign() -> SQLiteEventStore: """Campaign where all proposals are far below the running best.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("explore-bad", "test explore", 100000.0) + handler.create_campaign("explore-bad", "test explore", 100_000.0) # First iteration establishes a high running best handler.propose_candidate("anchor", {"x": 0}, 100.0, "v1.0", 1) @@ -65,7 +65,7 @@ def _build_balanced_campaign() -> SQLiteEventStore: """Campaign with a mix of high and low predictions.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("balanced", "balanced", 100000.0) + handler.create_campaign("balanced", "balanced", 100_000.0) for iteration in range(1, 7): # One exploitation candidate diff --git a/tests/test_analytics/test_model_learning.py b/tests/test_analytics/test_model_learning.py index b8a7608..05cdc42 100644 --- a/tests/test_analytics/test_model_learning.py +++ b/tests/test_analytics/test_model_learning.py @@ -15,7 +15,7 @@ def _build_not_learning_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("learn-bad", "test learning", 100000.0) + handler.create_campaign("learn-bad", "test learning", 100_000.0) for v in range(1, 5): version = f"v{v}.0" @@ -28,7 +28,7 @@ def _build_not_learning_campaign() -> SQLiteEventStore: version, iteration, ) - handler.start_experiment(f"c{v}a", 1000.0, iteration) + handler.start_experiment(f"c{v}a", 1_000.0, iteration) handler.record_result(f"c{v}a", 1.0, 0.05, iteration) # actual is 1.0, MAE=1.0 handler.update_model(version, iteration) @@ -41,7 +41,7 @@ def _build_learning_campaign() -> SQLiteEventStore: """Campaign where the model error shrinks with each retrain.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("learn-ok", "test learning ok", 100000.0) + handler.create_campaign("learn-ok", "test learning ok", 100_000.0) errors = [1.0, 0.7, 0.4, 0.15] for v in range(1, 5): @@ -56,7 +56,7 @@ def _build_learning_campaign() -> SQLiteEventStore: version, iteration, ) - handler.start_experiment(f"c{v}a", 1000.0, iteration) + handler.start_experiment(f"c{v}a", 1_000.0, iteration) handler.record_result(f"c{v}a", 1.0, 0.05, iteration) handler.update_model(version, iteration) @@ -98,7 +98,7 @@ def test_mae_trend_populated(self) -> None: def test_too_few_versions_ok(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("short", "short", 10000.0) + handler.create_campaign("short", "short", 10_000.0) handler.propose_candidate("c1", {"x": 1}, 2.0, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 1.0, 0.05, 1) diff --git a/tests/test_analytics/test_pareto.py b/tests/test_analytics/test_pareto.py index 426fb02..ea88422 100644 --- a/tests/test_analytics/test_pareto.py +++ b/tests/test_analytics/test_pareto.py @@ -35,7 +35,7 @@ def _build_stalled_campaign() -> SQLiteEventStore: """Multi-objective campaign where Pareto front stops improving.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("pareto-stall", "test pareto", 100000.0) + handler.create_campaign("pareto-stall", "test pareto", 100_000.0) for iteration in range(1, 7): handler.propose_candidate( @@ -45,7 +45,7 @@ def _build_stalled_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) # Objectives stagnate after iteration 2 val = 1.0 + (iteration * 0.3 if iteration <= 2 else 0.6) handler.record_result(f"c{iteration}", val, 0.05, iteration) @@ -58,7 +58,7 @@ def _build_progressing_campaign() -> SQLiteEventStore: """Campaign with steadily improving results (single objective).""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("pareto-ok", "test pareto ok", 100000.0) + handler.create_campaign("pareto-ok", "test pareto ok", 100_000.0) for iteration in range(1, 7): handler.propose_candidate( @@ -68,7 +68,7 @@ def _build_progressing_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", 1.0 + iteration * 0.5, 0.05, iteration) handler.close_iteration(iteration) @@ -108,7 +108,7 @@ def test_hypervolume_trend_populated(self) -> None: def test_too_few_iterations_ok(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("short", "short", 10000.0) + handler.create_campaign("short", "short", 10_000.0) handler.propose_candidate("c1", {"x": 1}, 1.0, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 2.0, 0.05, 1) diff --git a/tests/test_analytics/test_proxy_gaming.py b/tests/test_analytics/test_proxy_gaming.py index 928f9b5..89d0f98 100644 --- a/tests/test_analytics/test_proxy_gaming.py +++ b/tests/test_analytics/test_proxy_gaming.py @@ -29,7 +29,7 @@ def _build_gaming_campaign() -> SQLiteEventStore: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("gaming-test", "detect gaming", 100000.0) + handler.create_campaign("gaming-test", "detect gaming", 100_000.0) for iteration in range(1, 7): predicted = 1.0 + iteration * 0.3 # rising: 1.3, 1.6, 1.9, 2.2, 2.5, 2.8 @@ -42,7 +42,7 @@ def _build_gaming_campaign() -> SQLiteEventStore: "surrogate-v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", validated, 0.05, iteration) handler.close_iteration(iteration) @@ -58,7 +58,7 @@ def _build_healthy_campaign() -> SQLiteEventStore: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("healthy-test", "no gaming", 100000.0) + handler.create_campaign("healthy-test", "no gaming", 100_000.0) for iteration in range(1, 7): predicted = 1.0 + iteration * 0.2 # rising: 1.2, 1.4, 1.6, 1.8, 2.0, 2.2 @@ -71,7 +71,7 @@ def _build_healthy_campaign() -> SQLiteEventStore: "surrogate-v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", validated, 0.05, iteration) handler.close_iteration(iteration) @@ -114,7 +114,7 @@ def test_model_update_resets_window(self) -> None: """A ModelUpdated event should reset the gaming detection window.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("reset-test", "test reset", 100000.0) + handler.create_campaign("reset-test", "test reset", 100_000.0) # Iterations 1-2: gaming pattern for iteration in range(1, 3): @@ -126,7 +126,7 @@ def test_model_update_resets_window(self) -> None: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", 1.0, 0.05, iteration) handler.close_iteration(iteration) @@ -144,7 +144,7 @@ def test_model_update_resets_window(self) -> None: "v2.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", validated, 0.05, iteration) handler.close_iteration(iteration) @@ -160,9 +160,9 @@ def test_too_few_iterations_returns_ok(self) -> None: """With fewer iterations than the window, verdict is OK.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("short", "test", 10000.0) + handler.create_campaign("short", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 2.0, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) handler.record_result("c1", 1.0, 0.1, 1) detector = ProxyGamingDetector(window=3) @@ -179,7 +179,7 @@ def test_custom_window_size(self) -> None: # Build a longer gaming campaign with strictly flat validation store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("long-gaming", "detect gaming", 100000.0) + handler.create_campaign("long-gaming", "detect gaming", 100_000.0) for iteration in range(1, 9): predicted = 1.0 + iteration * 0.3 # always rising @@ -192,7 +192,7 @@ def test_custom_window_size(self) -> None: "surrogate-v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", validated, 0.05, iteration) handler.close_iteration(iteration) diff --git a/tests/test_analytics/test_regret.py b/tests/test_analytics/test_regret.py index fd81bba..5bbb233 100644 --- a/tests/test_analytics/test_regret.py +++ b/tests/test_analytics/test_regret.py @@ -12,7 +12,7 @@ def _build_high_regret_campaign() -> SQLiteEventStore: """Campaign where selections consistently miss the best candidate.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("regret-bad", "test regret", 100000.0) + handler.create_campaign("regret-bad", "test regret", 100_000.0) for iteration in range(1, 7): best_cid = f"best{iteration}" @@ -35,7 +35,7 @@ def _build_low_regret_campaign() -> SQLiteEventStore: """Campaign where selections consistently pick the best candidate.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("regret-ok", "test regret ok", 100000.0) + handler.create_campaign("regret-ok", "test regret ok", 100_000.0) for iteration in range(1, 7): best_cid = f"best{iteration}" @@ -86,7 +86,7 @@ def test_regret_trend_populated(self) -> None: def test_empty_campaign_ok(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("empty", "empty", 10000.0) + handler.create_campaign("empty", "empty", 10_000.0) tracker = CumulativeRegretTracker() registry = ProjectorRegistry() diff --git a/tests/test_analytics/test_reproducibility.py b/tests/test_analytics/test_reproducibility.py index 888eae6..7872dbb 100644 --- a/tests/test_analytics/test_reproducibility.py +++ b/tests/test_analytics/test_reproducibility.py @@ -60,7 +60,7 @@ def _build_unreliable_campaign() -> SQLiteEventStore: """Campaign with high measurement variance across replicates.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("repro-bad", "test repro", 100000.0) + handler.create_campaign("repro-bad", "test repro", 100_000.0) for iteration in range(1, 4): handler.propose_candidate( @@ -70,7 +70,7 @@ def _build_unreliable_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", 1.0, 0.05, iteration) handler.close_iteration(iteration) @@ -84,7 +84,7 @@ def _build_reliable_campaign() -> SQLiteEventStore: """Campaign with low measurement variance.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("repro-ok", "test repro ok", 100000.0) + handler.create_campaign("repro-ok", "test repro ok", 100_000.0) for iteration in range(1, 4): handler.propose_candidate( @@ -94,7 +94,7 @@ def _build_reliable_campaign() -> SQLiteEventStore: "v1.0", iteration, ) - handler.start_experiment(f"c{iteration}", 1000.0, iteration) + handler.start_experiment(f"c{iteration}", 1_000.0, iteration) handler.record_result(f"c{iteration}", 1.0, 0.05, iteration) handler.close_iteration(iteration) @@ -138,7 +138,7 @@ def test_cv_by_candidate_populated(self) -> None: def test_no_replicates_ok(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("single", "no replicates", 10000.0) + handler.create_campaign("single", "no replicates", 10_000.0) handler.propose_candidate("c1", {"x": 1}, 1.0, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 1.0, 0.05, 1) diff --git a/tests/test_api/test_app.py b/tests/test_api/test_app.py index ed1e108..caec489 100644 --- a/tests/test_api/test_app.py +++ b/tests/test_api/test_app.py @@ -22,7 +22,7 @@ def seeded_client(client: TestClient) -> TestClient: json={ "campaign_id": "test-campaign", "goal": "maximize bandgap", - "budget_cap": 50000.0, + "budget_cap": 50_000.0, }, ) client.post( @@ -39,7 +39,7 @@ def seeded_client(client: TestClient) -> TestClient: "/experiments/start", json={ "candidate_id": "CsPbI3", - "cost": 5000.0, + "cost": 5_000.0, "iteration": 1, }, ) @@ -63,7 +63,7 @@ def test_create_campaign(self, client: TestClient) -> None: json={ "campaign_id": "my-campaign", "goal": "find optimal perovskite", - "budget_cap": 100000.0, + "budget_cap": 100_000.0, }, ) assert resp.status_code == 200 @@ -75,7 +75,7 @@ def test_duplicate_campaign_returns_400(self, client: TestClient) -> None: payload = { "campaign_id": "dup", "goal": "test", - "budget_cap": 1000.0, + "budget_cap": 1_000.0, } client.post("/campaigns", json=payload) resp = client.post("/campaigns", json=payload) @@ -88,7 +88,7 @@ def test_propose_candidate(self, client: TestClient) -> None: json={ "campaign_id": "c1", "goal": "test", - "budget_cap": 1000.0, + "budget_cap": 1_000.0, }, ) resp = client.post( @@ -116,7 +116,7 @@ def test_record_failure(self, client: TestClient) -> None: json={ "campaign_id": "c1", "goal": "test", - "budget_cap": 1000.0, + "budget_cap": 1_000.0, }, ) client.post( @@ -133,7 +133,7 @@ def test_record_failure(self, client: TestClient) -> None: "/experiments/start", json={ "candidate_id": "CsSnI3", - "cost": 1000.0, + "cost": 1_000.0, "iteration": 1, }, ) @@ -155,7 +155,7 @@ def test_select_reject_candidate(self, client: TestClient) -> None: json={ "campaign_id": "c1", "goal": "test", - "budget_cap": 1000.0, + "budget_cap": 1_000.0, }, ) client.post( @@ -208,7 +208,7 @@ def test_update_model(self, client: TestClient) -> None: json={ "campaign_id": "c1", "goal": "test", - "budget_cap": 1000.0, + "budget_cap": 1_000.0, }, ) resp = client.post( @@ -258,7 +258,7 @@ def test_budget(self, seeded_client: TestClient) -> None: resp = seeded_client.get("/budget") assert resp.status_code == 200 data = resp.json() - assert data["total_spent"] == 5000.0 + assert data["total_spent"] == 5_000.0 def test_iterations(self, seeded_client: TestClient) -> None: resp = seeded_client.get("/iterations") @@ -300,7 +300,7 @@ def test_counterfactual(self, client: TestClient) -> None: json={ "campaign_id": "cf-test", "goal": "maximize bandgap", - "budget_cap": 50000.0, + "budget_cap": 50_000.0, }, ) client.post( @@ -317,7 +317,7 @@ def test_counterfactual(self, client: TestClient) -> None: "/experiments/start", json={ "candidate_id": "A", - "cost": 1000.0, + "cost": 1_000.0, "iteration": 1, }, ) @@ -343,7 +343,7 @@ def test_counterfactual(self, client: TestClient) -> None: "/experiments/start", json={ "candidate_id": "B", - "cost": 1000.0, + "cost": 1_000.0, "iteration": 1, }, ) diff --git a/tests/test_commands/test_handler.py b/tests/test_commands/test_handler.py index 2be7173..461ba42 100644 --- a/tests/test_commands/test_handler.py +++ b/tests/test_commands/test_handler.py @@ -21,7 +21,7 @@ def test_create_campaign(self, handler: CommandHandler) -> None: event = handler.create_campaign( campaign_id="camp-001", goal="Find stable perovskites", - budget_cap=50000.0, + budget_cap=50_000.0, actor="test", ) assert event.seq == 0 @@ -29,7 +29,7 @@ def test_create_campaign(self, handler: CommandHandler) -> None: assert event.payload["campaign_id"] == "camp-001" def test_propose_candidate(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) event = handler.propose_candidate( candidate_id="cand-001", features={"x": 1.0, "y": 2.0}, @@ -43,9 +43,9 @@ def test_propose_candidate(self, handler: CommandHandler) -> None: assert event.caused_by == [0] def test_record_result(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("cand-001", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("cand-001", cost=1000.0, iteration=1) + handler.start_experiment("cand-001", cost=1_000.0, iteration=1) event = handler.record_result( candidate_id="cand-001", value=0.78, @@ -57,9 +57,9 @@ def test_record_result(self, handler: CommandHandler) -> None: assert event.payload["uncertainty"] == 0.05 def test_record_failure(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("cand-002", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("cand-002", cost=1000.0, iteration=1) + handler.start_experiment("cand-002", cost=1_000.0, iteration=1) event = handler.record_failure( candidate_id="cand-002", failure_class="synthesizability", @@ -70,7 +70,7 @@ def test_record_failure(self, handler: CommandHandler) -> None: assert event.payload["failure_class"] == "synthesizability" def test_select_candidate(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("cand-001", {"x": 1.0}, 0.5, "v1", 1) event = handler.select_candidate( candidate_id="cand-001", @@ -81,7 +81,7 @@ def test_select_candidate(self, handler: CommandHandler) -> None: assert event.type == EventType.CANDIDATE_SELECTED def test_reject_candidate(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("cand-003", {"x": 1.0}, 0.5, "v1", 1) event = handler.reject_candidate( candidate_id="cand-003", @@ -92,12 +92,12 @@ def test_reject_candidate(self, handler: CommandHandler) -> None: assert event.type == EventType.CANDIDATE_REJECTED def test_consume_budget(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) event = handler.consume_budget(amount=500.0, cumulative=500.0, iteration=1) assert event.type == EventType.BUDGET_CONSUMED def test_update_model(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) event = handler.update_model( model_version="v2.0", iteration=2, @@ -106,20 +106,20 @@ def test_update_model(self, handler: CommandHandler) -> None: assert event.type == EventType.MODEL_UPDATED def test_close_iteration(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) event = handler.close_iteration(iteration=1, summary="Done") assert event.type == EventType.ITERATION_CLOSED class TestDedup: def test_duplicate_candidate_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 1.0, "v1", iteration=1) with pytest.raises(DuplicateEventError): handler.propose_candidate("c1", {"x": 1.0}, 1.0, "v1", iteration=1) def test_allow_duplicate(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 1.0, "v1", iteration=1) event = handler.handle( event_type=EventType.CANDIDATE_PROPOSED, @@ -135,39 +135,39 @@ def test_allow_duplicate(self, handler: CommandHandler) -> None: assert event.seq == 2 def test_second_campaign_rejected(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(LifecycleError, match="already exists"): - handler.create_campaign("camp-002", "other", 2000.0) + handler.create_campaign("camp-002", "other", 2_000.0) class TestLifecycleOrdering: def test_experiment_without_proposal_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(LifecycleError, match="no CandidateProposed"): handler.start_experiment("unknown", 100.0, 1) def test_result_without_proposal_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(LifecycleError, match="no CandidateProposed"): handler.record_result("unknown", 0.5, 0.1, 1) def test_failure_without_proposal_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(LifecycleError, match="no CandidateProposed"): handler.record_failure("unknown", "synthesizability", 1) def test_select_without_proposal_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(LifecycleError, match="no CandidateProposed"): handler.select_candidate("unknown", "reason", "v1", 1) def test_reject_without_proposal_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(LifecycleError, match="no CandidateProposed"): handler.reject_candidate("unknown", "reason", "v1", 1) def test_proposed_then_experiment_succeeds(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) event = handler.start_experiment("c1", 100.0, 1) assert event.type == EventType.EXPERIMENT_STARTED @@ -176,7 +176,7 @@ def test_budget_consumed_does_not_require_proposal( self, handler: CommandHandler, ) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) event = handler.consume_budget(100.0, 100.0, 1) assert event.type == EventType.BUDGET_CONSUMED @@ -185,53 +185,53 @@ class TestDeeperLifecycle: """Tests for the tightened lifecycle rules.""" def test_result_without_experiment_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) with pytest.raises(LifecycleError, match="no ExperimentStarted"): handler.record_result("c1", 0.78, 0.05, 1) def test_failure_without_experiment_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) with pytest.raises(LifecycleError, match="no ExperimentStarted"): handler.record_failure("c1", "synthesizability", 1) def test_result_after_experiment_succeeds(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) event = handler.record_result("c1", 0.78, 0.05, 1) assert event.type == EventType.RESULT_RECORDED def test_write_to_closed_iteration_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.close_iteration(1, "done") with pytest.raises(LifecycleError, match="closed iteration"): handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) def test_select_after_reject_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.reject_candidate("c1", "bad", "v1", 1) with pytest.raises(LifecycleError, match="already rejected"): handler.select_candidate("c1", "good", "v1", 1) def test_reject_after_select_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.select_candidate("c1", "good", "v1", 1) with pytest.raises(LifecycleError, match="already selected"): handler.reject_candidate("c1", "bad", "v1", 1) def test_close_iteration_twice_allowed(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.close_iteration(1, "first close") handler.close_iteration(1, "second close") def test_duplicate_result_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) handler.record_result("c1", 0.78, 0.05, 1) with pytest.raises( (LifecycleError, DuplicateEventError), @@ -240,65 +240,65 @@ def test_duplicate_result_raises(self, handler: CommandHandler) -> None: handler.record_result("c1", 0.99, 0.01, 1) def test_failure_after_result_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) handler.record_result("c1", 0.78, 0.05, 1) with pytest.raises(LifecycleError, match="outcome.*already been recorded"): handler.record_failure("c1", "synthesizability", 1) def test_result_in_later_iteration_succeeds(self, handler: CommandHandler) -> None: """Delayed results: experiment in iteration 1, result lands in iteration 2.""" - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) event = handler.record_result("c1", 0.78, 0.05, 2) assert event.type == EventType.RESULT_RECORDED assert event.iteration == 2 def test_delayed_result_blocks_second_outcome(self, handler: CommandHandler) -> None: """Experiment in iter 1, result in iter 2 -- no second result in iter 3.""" - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) handler.record_result("c1", 0.78, 0.05, 2) with pytest.raises(LifecycleError, match="outcome.*already been recorded"): handler.record_result("c1", 0.99, 0.01, 3) def test_two_experiments_two_outcomes_ok(self, handler: CommandHandler) -> None: """Two experiments for same candidate in different iterations, each gets one outcome.""" - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) - handler.start_experiment("c1", 1000.0, 2) + handler.start_experiment("c1", 1_000.0, 1) + handler.start_experiment("c1", 1_000.0, 2) handler.record_result("c1", 0.78, 0.05, 1) handler.record_result("c1", 0.82, 0.04, 2) def test_result_caused_by_wrong_type_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) e1 = handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) with pytest.raises(CausalLinkError, match="must include an ExperimentStarted"): handler.record_result("c1", 0.78, 0.05, 1, caused_by=[e1.seq]) def test_result_caused_by_wrong_candidate_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) - e_exp = handler.start_experiment("c1", 1000.0, 1) - handler.start_experiment("c2", 1000.0, 1) + e_exp = handler.start_experiment("c1", 1_000.0, 1) + handler.start_experiment("c2", 1_000.0, 1) with pytest.raises(CausalLinkError, match="must include an ExperimentStarted"): handler.record_result("c2", 0.78, 0.05, 1, caused_by=[e_exp.seq]) def test_result_caused_by_correct_experiment_ok(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - e_exp = handler.start_experiment("c1", 1000.0, 1) + e_exp = handler.start_experiment("c1", 1_000.0, 1) event = handler.record_result("c1", 0.78, 0.05, 1, caused_by=[e_exp.seq]) assert event.type == EventType.RESULT_RECORDED def test_select_caused_by_must_reference_outcome(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) e_prop = handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) with pytest.raises(CausalLinkError, match="must include a ResultRecorded"): handler.select_candidate("c1", "best", "v1", 1, caused_by=[e_prop.seq]) @@ -309,7 +309,7 @@ def test_allow_duplicate_produces_unique_content_hashes( self, handler: CommandHandler, ) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) e1 = handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) e2 = handler.handle( event_type=EventType.CANDIDATE_PROPOSED, @@ -328,7 +328,7 @@ def test_three_replicates_all_distinct( self, handler: CommandHandler, ) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) hashes = set() for i in range(3): e = handler.handle( @@ -348,7 +348,7 @@ def test_three_replicates_all_distinct( class TestCausalLinks: def test_invalid_caused_by_ref_raises(self, handler: CommandHandler) -> None: - handler.create_campaign("camp-001", "test", 1000.0) + handler.create_campaign("camp-001", "test", 1_000.0) with pytest.raises(CausalLinkError, match="does not exist"): handler.propose_candidate( candidate_id="cand-001", diff --git a/tests/test_commands/test_ingest.py b/tests/test_commands/test_ingest.py index 529dd6e..e75d2d5 100644 --- a/tests/test_commands/test_ingest.py +++ b/tests/test_commands/test_ingest.py @@ -18,7 +18,7 @@ def store() -> SQLiteEventStore: @pytest.fixture def handler(store: SQLiteEventStore) -> CommandHandler: h = CommandHandler(store) - h.create_campaign("test-ingest", "test", 50000.0) + h.create_campaign("test-ingest", "test", 50_000.0) return h @@ -57,7 +57,7 @@ def test_result_batch_commits_all( ) -> None: for i in range(3): handler.propose_candidate(f"c{i}", {"x": float(i)}, 1.0, "v1", 1) - handler.start_experiment(f"c{i}", 1000.0, 1) + handler.start_experiment(f"c{i}", 1_000.0, 1) count_before = store.count() data = [{"candidate_id": f"c{i}", "value": float(i + 1)} for i in range(3)] events = ingest_results(handler, data, iteration=1) @@ -71,7 +71,7 @@ def test_result_batch_rolls_back_on_lifecycle_error( ) -> None: """Results for candidates without experiments roll back entirely.""" handler.propose_candidate("c1", {"x": 1.0}, 1.0, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) handler.propose_candidate("c2", {"x": 2.0}, 1.0, "v1", 1) count_before = store.count() data = [ diff --git a/tests/test_compliance/test_alcoa.py b/tests/test_compliance/test_alcoa.py index 788dd64..febc910 100644 --- a/tests/test_compliance/test_alcoa.py +++ b/tests/test_compliance/test_alcoa.py @@ -17,7 +17,7 @@ def _setup() -> tuple[CommandHandler, SQLiteEventStore]: store = SQLiteEventStore() handler = CommandHandler(store) - handler.create_campaign("test", "test goal", 50000.0) + handler.create_campaign("test", "test goal", 50_000.0) return handler, store @@ -214,7 +214,7 @@ def test_campaign_with_urn_and_license(self) -> None: e = handler.create_campaign( "test", "test goal", - 50000.0, + 50_000.0, campaign_urn="urn:matledger:test-001", data_license="CC-BY-4.0", provenance="Smith Lab, MIT, NSF grant #12345", @@ -235,7 +235,7 @@ def test_campaign_urn_roundtrips(self) -> None: handler.create_campaign( "test", "test goal", - 50000.0, + 50_000.0, campaign_urn="urn:matledger:abc", ) e = store.get(0) diff --git a/tests/test_decisions/test_counterfactual.py b/tests/test_decisions/test_counterfactual.py index c05ce07..d26b989 100644 --- a/tests/test_decisions/test_counterfactual.py +++ b/tests/test_decisions/test_counterfactual.py @@ -28,13 +28,13 @@ def _build_campaign() -> SQLiteEventStore: """ store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("replay-test", "test counterfactual", 100000.0) + handler.create_campaign("replay-test", "test counterfactual", 100_000.0) # Iteration 1 handler.propose_candidate("A", {"x": 1.0}, 2.0, "v1.0", 1) handler.propose_candidate("B", {"x": 2.0}, 1.5, "v1.0", 1) - handler.start_experiment("A", 5000.0, 1) - handler.start_experiment("B", 5000.0, 1) + handler.start_experiment("A", 5_000.0, 1) + handler.start_experiment("B", 5_000.0, 1) handler.record_result("A", 1.8, 0.05, 1) handler.record_result("B", 2.5, 0.03, 1) handler.select_candidate("A", "highest predicted", "v1.0", 1) @@ -42,14 +42,14 @@ def _build_campaign() -> SQLiteEventStore: # Iteration 2 handler.propose_candidate("C", {"x": 3.0}, 2.2, "v1.0", 2) - handler.start_experiment("C", 5000.0, 2) + handler.start_experiment("C", 5_000.0, 2) handler.record_result("C", 1.9, 0.04, 2) handler.select_candidate("C", "highest predicted", "v1.0", 2) handler.close_iteration(2) # Iteration 3 handler.propose_candidate("D", {"x": 4.0}, 1.8, "v1.0", 3) - handler.start_experiment("D", 5000.0, 3) + handler.start_experiment("D", 5_000.0, 3) handler.record_result("D", 2.1, 0.04, 3) handler.select_candidate("D", "decent result", "v1.0", 3) handler.close_iteration(3) @@ -65,12 +65,12 @@ def _build_campaign_with_failures() -> SQLiteEventStore: """A campaign with failures to test the avoid-failures policy path.""" store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("fail-test", "test failures", 50000.0) + handler.create_campaign("fail-test", "test failures", 50_000.0) handler.propose_candidate("good", {"x": 1.0}, 2.0, "v1.0", 1) handler.propose_candidate("bad", {"x": 2.0}, 2.5, "v1.0", 1) - handler.start_experiment("good", 5000.0, 1) - handler.start_experiment("bad", 5000.0, 1) + handler.start_experiment("good", 5_000.0, 1) + handler.start_experiment("bad", 5_000.0, 1) handler.record_result("good", 1.9, 0.05, 1) handler.record_failure("bad", "synthesizability", 1, "couldn't synthesize") handler.select_candidate("good", "only successful", "v1.0", 1) @@ -125,8 +125,8 @@ def test_cost_tracking(self) -> None: engine = ReplayEngine(alternate_policy=GreedyPolicy()) result = engine.replay(store.iter_from(0)) - assert result.original_total_cost == 20000.0 - assert result.alternate_total_cost_at_find == 10000.0 + assert result.original_total_cost == 20_000.0 + assert result.alternate_total_cost_at_find == 10_000.0 def test_campaign_with_failures(self) -> None: store = _build_campaign_with_failures() diff --git a/tests/test_decisions/test_policy.py b/tests/test_decisions/test_policy.py index 35e4bcf..d8d1ef9 100644 --- a/tests/test_decisions/test_policy.py +++ b/tests/test_decisions/test_policy.py @@ -20,8 +20,8 @@ def _make_context() -> DecisionContext: "C": CandidateState("C", {"x": 3}, 3.0, validated_value=1.2), "D": CandidateState("D", {"x": 4}, 1.0, failed=True, failure_class="synthesizability"), }, - budget_remaining=10000.0, - total_spent=40000.0, + budget_remaining=10_000.0, + total_spent=40_000.0, failure_classes_seen={"synthesizability": 1}, ) @@ -43,8 +43,8 @@ def test_no_successful_returns_empty(self) -> None: candidates={ "X": CandidateState("X", {"x": 1}, 1.0, failed=True, failure_class="stability"), }, - budget_remaining=5000.0, - total_spent=5000.0, + budget_remaining=5_000.0, + total_spent=5_000.0, ) assert GreedyPolicy().select(context) == [] @@ -87,8 +87,8 @@ def test_keeps_other_failure_classes(self) -> None: "A": CandidateState("A", {"x": 1}, 2.0, validated_value=1.8), "B": CandidateState("B", {"x": 2}, 1.5, failed=True, failure_class="stability"), }, - budget_remaining=5000.0, - total_spent=5000.0, + budget_remaining=5_000.0, + total_spent=5_000.0, ) policy = AvoidFailuresPolicy(GreedyPolicy(top_k=10), avoid_classes={"synthesizability"}) picks = policy.select(context) diff --git a/tests/test_events/test_catalog.py b/tests/test_events/test_catalog.py index f3d8f3a..0013dff 100644 --- a/tests/test_events/test_catalog.py +++ b/tests/test_events/test_catalog.py @@ -25,10 +25,10 @@ def test_valid(self) -> None: e = CampaignCreated( campaign_id="camp-001", goal="Find stable perovskites", - budget_cap=50000.0, + budget_cap=50_000.0, ) assert e.campaign_id == "camp-001" - assert e.budget_cap == 50000.0 + assert e.budget_cap == 50_000.0 def test_negative_budget_rejected(self) -> None: with pytest.raises(ValueError): @@ -179,12 +179,12 @@ def test_rationale_required(self) -> None: class TestBudgetConsumed: def test_valid(self) -> None: - b = BudgetConsumed(amount=500.0, cumulative=1500.0) - assert b.cumulative == 1500.0 + b = BudgetConsumed(amount=500.0, cumulative=1_500.0) + assert b.cumulative == 1_500.0 def test_negative_amount_rejected(self) -> None: with pytest.raises(ValueError): - BudgetConsumed(amount=-100.0, cumulative=1000.0) + BudgetConsumed(amount=-100.0, cumulative=1_000.0) class TestModelUpdated: @@ -221,7 +221,7 @@ def test_all_event_types_registered(self) -> None: def test_validate_payload_valid(self) -> None: result = validate_payload( "CampaignCreated", - {"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + {"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, ) assert isinstance(result, CampaignCreated) diff --git a/tests/test_events/test_envelope.py b/tests/test_events/test_envelope.py index dd3340c..18dd356 100644 --- a/tests/test_events/test_envelope.py +++ b/tests/test_events/test_envelope.py @@ -15,7 +15,7 @@ def test_create_minimal_event(self) -> None: event = Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) assert event.seq == 0 @@ -28,7 +28,7 @@ def test_event_is_frozen(self) -> None: event = Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) with pytest.raises((AttributeError, ValidationError)): @@ -59,7 +59,7 @@ def test_compute_hash_deterministic(self) -> None: event = Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, prev_hash="", iteration=0, ) diff --git a/tests/test_projections/test_budget.py b/tests/test_projections/test_budget.py index 6cf3fe4..55223ea 100644 --- a/tests/test_projections/test_budget.py +++ b/tests/test_projections/test_budget.py @@ -21,30 +21,30 @@ class TestBudgetProjector: def test_budget_cap_from_campaign(self) -> None: handler, bp, registry = _setup() - handler.create_campaign("camp", "test", 50000.0) + handler.create_campaign("camp", "test", 50_000.0) registry.rebuild(handler.store.iter_from(0)) - assert bp.state.budget_cap == 50000.0 - assert bp.state.remaining == 50000.0 + assert bp.state.budget_cap == 50_000.0 + assert bp.state.remaining == 50_000.0 def test_experiment_consumes_budget(self) -> None: handler, bp, registry = _setup() - handler.create_campaign("camp", "test", 50000.0) + handler.create_campaign("camp", "test", 50_000.0) handler.propose_candidate("c1", {}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) handler.start_experiment("c1", 500.0, 1) registry.rebuild(handler.store.iter_from(0)) - assert bp.state.total_consumed == 1500.0 - assert bp.state.remaining == 48500.0 + assert bp.state.total_consumed == 1_500.0 + assert bp.state.remaining == 48_500.0 assert bp.state.experiments_started == 2 def test_over_cap_detection(self) -> None: handler, bp, registry = _setup() - handler.create_campaign("camp", "test", 1000.0) + handler.create_campaign("camp", "test", 1_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) handler.start_experiment("c1", 600.0, 1) @@ -58,25 +58,25 @@ def test_over_cap_detection(self) -> None: def test_cost_by_iteration(self) -> None: handler, bp, registry = _setup() - handler.create_campaign("camp", "test", 50000.0) + handler.create_campaign("camp", "test", 50_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) handler.propose_candidate("c3", {"x": 3.0}, 0.5, "v1", 2) - handler.start_experiment("c1", 1000.0, 1) - handler.start_experiment("c2", 1500.0, 1) - handler.start_experiment("c3", 2000.0, 2) + handler.start_experiment("c1", 1_000.0, 1) + handler.start_experiment("c2", 1_500.0, 1) + handler.start_experiment("c3", 2_000.0, 2) registry.rebuild(handler.store.iter_from(0)) - assert bp.state.cost_by_iteration[1] == 2500.0 - assert bp.state.cost_by_iteration[2] == 2000.0 + assert bp.state.cost_by_iteration[1] == 2_500.0 + assert bp.state.cost_by_iteration[2] == 2_000.0 def test_utilization(self) -> None: handler, bp, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 5000.0, 1) + handler.start_experiment("c1", 5_000.0, 1) registry.rebuild(handler.store.iter_from(0)) @@ -85,9 +85,9 @@ def test_utilization(self) -> None: def test_rebuild_idempotent(self) -> None: handler, bp, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) registry.rebuild(handler.store.iter_from(0)) first = bp.state.total_consumed @@ -95,4 +95,4 @@ def test_rebuild_idempotent(self) -> None: registry.rebuild(handler.store.iter_from(0)) second = bp.state.total_consumed - assert first == second == 1000.0 + assert first == second == 1_000.0 diff --git a/tests/test_projections/test_iteration_summary.py b/tests/test_projections/test_iteration_summary.py index 3059c6c..dc80f12 100644 --- a/tests/test_projections/test_iteration_summary.py +++ b/tests/test_projections/test_iteration_summary.py @@ -34,7 +34,7 @@ def _record_full_campaign() -> SQLiteEventStore: handler.create_campaign( campaign_id="perovskite-001", goal="Find thermally stable perovskites with bandgap > 1.5 eV", - budget_cap=50000.0, + budget_cap=50_000.0, actor="scientist:jayson", ) @@ -67,9 +67,9 @@ def _record_full_campaign() -> SQLiteEventStore: actor="agent:bo-v2", ) - handler.start_experiment("CsPbI3", 5000.0, 1, caused_by=[c1.seq]) - handler.start_experiment("CsPbBr3", 5000.0, 1, caused_by=[c2.seq]) - handler.start_experiment("MAPbI3", 5000.0, 1, caused_by=[c3.seq]) + handler.start_experiment("CsPbI3", 5_000.0, 1, caused_by=[c1.seq]) + handler.start_experiment("CsPbBr3", 5_000.0, 1, caused_by=[c2.seq]) + handler.start_experiment("MAPbI3", 5_000.0, 1, caused_by=[c3.seq]) r1 = handler.record_result("CsPbI3", 1.68, 0.05, 1, "XRD-2026-001", actor="lims") handler.record_result("CsPbBr3", 2.31, 0.08, 1, "XRD-2026-002", actor="lims") @@ -106,7 +106,7 @@ def _record_full_campaign() -> SQLiteEventStore: 2, actor="agent:bo-v2", ) - handler.start_experiment("CsSnI3", 5000.0, 2, caused_by=[c4.seq]) + handler.start_experiment("CsSnI3", 5_000.0, 2, caused_by=[c4.seq]) handler.record_failure( "CsSnI3", "synthesizability", @@ -125,7 +125,7 @@ def _record_full_campaign() -> SQLiteEventStore: 3, actor="agent:bo-v2", ) - handler.start_experiment("CsPb0.5Sn0.5I3", 5000.0, 3, caused_by=[c5.seq]) + handler.start_experiment("CsPb0.5Sn0.5I3", 5_000.0, 3, caused_by=[c5.seq]) handler.record_result("CsPb0.5Sn0.5I3", 1.42, 0.03, 3, "XRD-2026-003", actor="lims") handler.select_candidate( "CsPb0.5Sn0.5I3", @@ -167,10 +167,10 @@ def test_retroactive_rebuild_over_existing_data(self) -> None: assert it1.failures == 1 assert it1.selected == 1 assert it1.rejected == 1 - assert it1.cost == 15000.0 + assert it1.cost == 15_000.0 assert it1.tested == 3 # 2 results + 1 failure assert abs(it1.success_rate - 2 / 3) < 0.01 - assert it1.cost_per_result == 7500.0 # $15k / 2 results + assert it1.cost_per_result == 7_500.0 # $15k / 2 results assert it1.failure_classes == {"stability": 1} # Verify iteration 2 — the problematic one (100% failure rate) @@ -191,7 +191,7 @@ def test_retroactive_rebuild_over_existing_data(self) -> None: assert it3.results == 1 assert it3.selected == 1 assert it3.success_rate == 1.0 - assert it3.cost_per_result == 5000.0 + assert it3.cost_per_result == 5_000.0 def test_coexists_with_m1_projections(self) -> None: """New projection works alongside existing M1 projections in same registry.""" @@ -213,7 +213,7 @@ def test_coexists_with_m1_projections(self) -> None: # M1 projections still work assert len(tp.all_candidates()) == 5 assert len(nl.all_failures()) == 2 - assert bp.state.total_consumed == 25000.0 + assert bp.state.total_consumed == 25_000.0 # New projection also works — same event stream, different view assert isp.total_iterations() == 4 # iterations 0, 1, 2, 3 @@ -251,7 +251,7 @@ def test_incremental_apply(self) -> None: store = SQLiteEventStore(":memory:") handler = CommandHandler(store) - handler.create_campaign("test", "test", 10000.0) + handler.create_campaign("test", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) isp = IterationSummaryProjector() @@ -263,7 +263,7 @@ def test_incremental_apply(self) -> None: # Add more events after initial rebuild handler.propose_candidate("c2", {"x": 2.0}, 0.7, "v1", 1) - handler.start_experiment("c1", 1000.0, 1) + handler.start_experiment("c1", 1_000.0, 1) registry.apply_from(store.iter_from(0)) diff --git a/tests/test_projections/test_negledger.py b/tests/test_projections/test_negledger.py index 98799aa..af944e5 100644 --- a/tests/test_projections/test_negledger.py +++ b/tests/test_projections/test_negledger.py @@ -22,13 +22,13 @@ class TestNegledgerProjector: def test_record_failures(self) -> None: handler, nl, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) handler.propose_candidate("c3", {"x": 3.0}, 0.5, "v1", 2) - handler.start_experiment("c1", cost=1000.0, iteration=1) - handler.start_experiment("c2", cost=1000.0, iteration=1) - handler.start_experiment("c3", cost=1000.0, iteration=2) + handler.start_experiment("c1", cost=1_000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) + handler.start_experiment("c3", cost=1_000.0, iteration=2) handler.record_failure("c1", "synthesizability", 1, "Can't synthesize") handler.record_failure("c2", "stability", 1, "Decomposes at 300K") handler.record_failure("c3", "synthesizability", 2, "Missing precursor") @@ -40,13 +40,13 @@ def test_record_failures(self) -> None: def test_query_by_class(self) -> None: handler, nl, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) handler.propose_candidate("c3", {"x": 3.0}, 0.5, "v1", 2) - handler.start_experiment("c1", cost=1000.0, iteration=1) - handler.start_experiment("c2", cost=1000.0, iteration=1) - handler.start_experiment("c3", cost=1000.0, iteration=2) + handler.start_experiment("c1", cost=1_000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) + handler.start_experiment("c3", cost=1_000.0, iteration=2) handler.record_failure("c1", "synthesizability", 1) handler.record_failure("c2", "stability", 1) handler.record_failure("c3", "synthesizability", 2) @@ -61,13 +61,13 @@ def test_query_by_class(self) -> None: def test_query_by_iteration(self) -> None: handler, nl, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) handler.propose_candidate("c3", {"x": 3.0}, 0.5, "v1", 2) - handler.start_experiment("c1", cost=1000.0, iteration=1) - handler.start_experiment("c2", cost=1000.0, iteration=1) - handler.start_experiment("c3", cost=1000.0, iteration=2) + handler.start_experiment("c1", cost=1_000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) + handler.start_experiment("c3", cost=1_000.0, iteration=2) handler.record_failure("c1", "synthesizability", 1) handler.record_failure("c2", "stability", 1) handler.record_failure("c3", "out_of_spec", 2) @@ -80,12 +80,12 @@ def test_query_by_iteration(self) -> None: def test_failed_candidates_set(self) -> None: handler, nl, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) - handler.start_experiment("c1", cost=1000.0, iteration=1) - handler.start_experiment("c2", cost=1000.0, iteration=1) - handler.start_experiment("c1", cost=1000.0, iteration=2) + handler.start_experiment("c1", cost=1_000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) + handler.start_experiment("c1", cost=1_000.0, iteration=2) handler.record_failure("c1", "synthesizability", 1) handler.record_failure("c1", "stability", 2) handler.record_failure("c2", "out_of_spec", 1) @@ -97,13 +97,13 @@ def test_failed_candidates_set(self) -> None: def test_class_counts(self) -> None: handler, nl, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.5, "v1", 1) handler.propose_candidate("c2", {"x": 2.0}, 0.5, "v1", 1) handler.propose_candidate("c3", {"x": 3.0}, 0.5, "v1", 2) - handler.start_experiment("c1", cost=1000.0, iteration=1) - handler.start_experiment("c2", cost=1000.0, iteration=1) - handler.start_experiment("c3", cost=1000.0, iteration=2) + handler.start_experiment("c1", cost=1_000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) + handler.start_experiment("c3", cost=1_000.0, iteration=2) handler.record_failure("c1", "synthesizability", 1) handler.record_failure("c2", "synthesizability", 1) handler.record_failure("c3", "stability", 2) diff --git a/tests/test_projections/test_snapshots.py b/tests/test_projections/test_snapshots.py index b14ff9f..15b9b41 100644 --- a/tests/test_projections/test_snapshots.py +++ b/tests/test_projections/test_snapshots.py @@ -16,7 +16,7 @@ class TestSnapshotStore: def test_save_and_retrieve(self) -> None: store = SnapshotStore(":memory:") - state = {"candidates": 5, "cost": 10000.0} + state = {"candidates": 5, "cost": 10_000.0} store.save("timeline", 100, state) snap = store.latest("timeline") @@ -27,14 +27,14 @@ def test_save_and_retrieve(self) -> None: def test_latest_returns_most_recent(self) -> None: store = SnapshotStore(":memory:") - store.save("budget", 50, {"spent": 5000}) - store.save("budget", 100, {"spent": 10000}) - store.save("budget", 150, {"spent": 15000}) + store.save("budget", 50, {"spent": 5_000}) + store.save("budget", 100, {"spent": 10_000}) + store.save("budget", 150, {"spent": 15_000}) snap = store.latest("budget") assert snap is not None assert snap.seq == 150 - assert snap.state["spent"] == 15000 + assert snap.state["spent"] == 15_000 def test_get_specific(self) -> None: store = SnapshotStore(":memory:") @@ -135,7 +135,7 @@ def _make_event( { "campaign_id": "c1", "goal": "test", - "budget_cap": 50000.0, + "budget_cap": 50_000.0, }, iteration=0, ), @@ -154,7 +154,7 @@ def _make_event( EventType.EXPERIMENT_STARTED, { "candidate_id": "A", - "cost": 1000.0, + "cost": 1_000.0, }, ), _make_event( diff --git a/tests/test_projections/test_timeline.py b/tests/test_projections/test_timeline.py index 691f2b3..d04e217 100644 --- a/tests/test_projections/test_timeline.py +++ b/tests/test_projections/test_timeline.py @@ -21,7 +21,7 @@ class TestTimelineProjector: def test_candidate_lifecycle(self) -> None: handler, tp, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {"x": 1.0}, 0.8, "v1", 1) handler.start_experiment("c1", 100.0, 1) handler.record_result("c1", 0.75, 0.05, 1) @@ -37,9 +37,9 @@ def test_candidate_lifecycle(self) -> None: def test_failed_candidate(self) -> None: handler, tp, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c2", {"x": 2.0}, 0.9, "v1", 1) - handler.start_experiment("c2", cost=1000.0, iteration=1) + handler.start_experiment("c2", cost=1_000.0, iteration=1) handler.record_failure("c2", "synthesizability", 1, "Can't make it") registry.rebuild(handler.store.iter_from(0)) @@ -51,7 +51,7 @@ def test_failed_candidate(self) -> None: def test_multiple_candidates(self) -> None: handler, tp, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {}, 0.5, "v1", 1) handler.propose_candidate("c2", {}, 0.6, "v1", 1) handler.propose_candidate("c3", {}, 0.7, "v1", 1) @@ -63,7 +63,7 @@ def test_multiple_candidates(self) -> None: def test_rebuild_is_idempotent(self) -> None: handler, tp, registry = _setup() - handler.create_campaign("camp", "test", 10000.0) + handler.create_campaign("camp", "test", 10_000.0) handler.propose_candidate("c1", {}, 0.5, "v1", 1) registry.rebuild(handler.store.iter_from(0)) diff --git a/tests/test_report/test_html.py b/tests/test_report/test_html.py index 0ac70a0..e81df3f 100644 --- a/tests/test_report/test_html.py +++ b/tests/test_report/test_html.py @@ -20,7 +20,7 @@ def _campaign_with_data() -> tuple[SQLiteEventStore, ProjectorRegistry]: handler.create_campaign( campaign_id="perovskite-001", goal="Find stable perovskites", - budget_cap=50000.0, + budget_cap=50_000.0, actor="test", ) @@ -41,8 +41,8 @@ def _campaign_with_data() -> tuple[SQLiteEventStore, ProjectorRegistry]: actor="agent", ) - handler.start_experiment("CsPbI3", 5000.0, 1) - handler.start_experiment("CsPbBr3", 5000.0, 1) + handler.start_experiment("CsPbI3", 5_000.0, 1) + handler.start_experiment("CsPbBr3", 5_000.0, 1) handler.record_result( "CsPbI3", diff --git a/tests/test_report/test_plots.py b/tests/test_report/test_plots.py index ef6555c..a85232e 100644 --- a/tests/test_report/test_plots.py +++ b/tests/test_report/test_plots.py @@ -21,7 +21,7 @@ def _build_campaign() -> CampaignClient: """Build a multi-iteration campaign with enough data for all charts.""" client = CampaignClient(":memory:") - client.create_campaign("test-viz", "maximize bandgap", 50000.0) + client.create_campaign("test-viz", "maximize bandgap", 50_000.0) for it in range(1, 5): for j in range(3): @@ -34,7 +34,7 @@ def _build_campaign() -> CampaignClient: f"v{it}", iteration=it, ) - client.start_experiment(cid, cost=1000.0, iteration=it) + client.start_experiment(cid, cost=1_000.0, iteration=it) if j == 0 and it > 2: client.record_failure(cid, "synthesizability", iteration=it, detail="test") @@ -91,15 +91,15 @@ class TestPlotBudget: def test_basic_chart(self) -> None: fig = plot_budget( [1, 2, 3, 4], - [5000.0, 8000.0, 12000.0, 6000.0], - budget_cap=40000.0, + [5_000.0, 8_000.0, 12_000.0, 6_000.0], + budget_cap=40_000.0, ) assert fig is not None b64 = figure_to_base64(fig) assert len(b64) > 100 def test_no_cap(self) -> None: - fig = plot_budget([1, 2], [1000.0, 2000.0]) + fig = plot_budget([1, 2], [1_000.0, 2_000.0]) assert fig is not None @@ -109,7 +109,7 @@ def test_basic_chart(self) -> None: [1, 2, 3, 4], [3, 2, 0, 0], [1, 1, 2, 3], - [1000.0, 2000.0, float("inf"), float("inf")], + [1_000.0, 2_000.0, float("inf"), float("inf")], flatlined_at=3, ) assert fig is not None @@ -134,8 +134,8 @@ def test_with_all_data(self) -> None: "since_iteration": None, }, budget_data={ - "cost_by_iteration": {1: 5000.0, 2: 8000.0, 3: 3000.0}, - "budget_cap": 30000.0, + "cost_by_iteration": {1: 5_000.0, 2: 8_000.0, 3: 3_000.0}, + "budget_cap": 30_000.0, }, discovery_rate_data={ "marginal_rates": {1: 3, 2: 2, 3: 1}, diff --git a/tests/test_sdk/test_client.py b/tests/test_sdk/test_client.py index 6c64257..f00cbb1 100644 --- a/tests/test_sdk/test_client.py +++ b/tests/test_sdk/test_client.py @@ -7,14 +7,14 @@ def _build_campaign() -> CampaignClient: client = CampaignClient(":memory:") - client.create_campaign("test", "maximize bandgap", 50000.0) + client.create_campaign("test", "maximize bandgap", 50_000.0) client.propose("CsPbI3", {"Cs": 1, "Pb": 1, "I": 3}, 1.73, "v1.0", 1) - client.start_experiment("CsPbI3", 5000.0, 1) + client.start_experiment("CsPbI3", 5_000.0, 1) client.record_result("CsPbI3", 1.68, 0.05, 1) client.propose("CsSnI3", {"Cs": 1, "Sn": 1, "I": 3}, 1.30, "v1.0", 1) - client.start_experiment("CsSnI3", 3000.0, 1) + client.start_experiment("CsSnI3", 3_000.0, 1) client.record_failure("CsSnI3", "synthesizability", 1, "Sn oxidized") client.select("CsPbI3", "best bandgap", "v1.0", 1) @@ -27,7 +27,7 @@ def _build_campaign() -> CampaignClient: class TestCampaignClient: def test_write_operations(self) -> None: client = CampaignClient(":memory:") - event = client.create_campaign("c1", "test", 10000.0) + event = client.create_campaign("c1", "test", 10_000.0) assert event.seq == 0 assert client.event_count == 1 @@ -55,8 +55,8 @@ def test_negledger(self) -> None: def test_budget(self) -> None: client = _build_campaign() b = client.budget() - assert b["total_spent"] == 8000.0 - assert b["budget_cap"] == 50000.0 + assert b["total_spent"] == 8_000.0 + assert b["budget_cap"] == 50_000.0 def test_iterations(self) -> None: client = _build_campaign() @@ -81,7 +81,7 @@ def test_diversity(self) -> None: def test_discovery_rate(self) -> None: client = _build_campaign() verdict = client.discovery_rate() - assert verdict.total_cost == 8000.0 + assert verdict.total_cost == 8_000.0 assert verdict.total_results == 1 assert verdict.total_failures == 1 @@ -93,7 +93,7 @@ def test_verify(self) -> None: def test_counterfactual_replay(self) -> None: """Replay with greedy policy over a 2-iteration campaign.""" client = CampaignClient(":memory:") - client.create_campaign("cf", "maximize bandgap", 50000.0) + client.create_campaign("cf", "maximize bandgap", 50_000.0) # --- Iteration 1: two candidates, select the worse one --- client.propose( @@ -103,7 +103,7 @@ def test_counterfactual_replay(self) -> None: "v1.0", 1, ) - client.start_experiment("A", 1000.0, 1) + client.start_experiment("A", 1_000.0, 1) client.record_result("A", 1.40, 0.01, 1) client.propose( @@ -113,7 +113,7 @@ def test_counterfactual_replay(self) -> None: "v1.0", 1, ) - client.start_experiment("B", 1000.0, 1) + client.start_experiment("B", 1_000.0, 1) client.record_result("B", 1.75, 0.01, 1) # Original selects A (the worse one) @@ -137,7 +137,7 @@ def test_counterfactual_replay(self) -> None: def test_counterfactual_replay_random(self) -> None: """Random policy with seed produces deterministic output.""" client = CampaignClient(":memory:") - client.create_campaign("cf2", "maximize", 50000.0) + client.create_campaign("cf2", "maximize", 50_000.0) client.propose("X", {"a": 1}, 1.0, "v1", 1) client.start_experiment("X", 500.0, 1) diff --git a/tests/test_store/test_hash_chain.py b/tests/test_store/test_hash_chain.py index 8d2d78e..5139f3d 100644 --- a/tests/test_store/test_hash_chain.py +++ b/tests/test_store/test_hash_chain.py @@ -14,7 +14,7 @@ def _build_chain(n: int) -> list[Event]: Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) ) @@ -91,7 +91,7 @@ def test_returns_event_hash(self) -> None: event = Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, prev_hash="", iteration=0, ) diff --git a/tests/test_store/test_property_based.py b/tests/test_store/test_property_based.py index 2d9947c..616febc 100644 --- a/tests/test_store/test_property_based.py +++ b/tests/test_store/test_property_based.py @@ -78,7 +78,7 @@ def test_hash_chain_always_verifies(event_types: list[EventType]) -> None: Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) ) @@ -113,7 +113,7 @@ def test_roundtrip_preserves_all_events(event_types: list[EventType]) -> None: Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) ) @@ -154,7 +154,7 @@ def test_iter_from_offset_skips_correctly(event_types: list[EventType]) -> None: Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) ) @@ -194,7 +194,7 @@ def test_content_hash_dedup_detects_duplicates( Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ) ) @@ -232,7 +232,7 @@ def build_store() -> list[str]: Event( seq=0, type=EventType.CAMPAIGN_CREATED, - payload={"campaign_id": "test", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": "test", "goal": "test", "budget_cap": 1_000.0}, iteration=0, ts=fixed_ts, ) diff --git a/tests/test_store/test_sqlite.py b/tests/test_store/test_sqlite.py index f3d8524..3f61244 100644 --- a/tests/test_store/test_sqlite.py +++ b/tests/test_store/test_sqlite.py @@ -21,7 +21,7 @@ def _make_event( return Event( seq=seq, type=event_type, - payload={"campaign_id": f"test-{seq}", "goal": "test", "budget_cap": 1000.0}, + payload={"campaign_id": f"test-{seq}", "goal": "test", "budget_cap": 1_000.0}, iteration=iteration, ) From 01fc3c4d8be87ebf0e9c809db29adb5a24503991 Mon Sep 17 00:00:00 2001 From: jayson_mulwa Date: Fri, 3 Jul 2026 13:14:42 +0300 Subject: [PATCH 2/5] refactor: apply PEP 681, 698, 634 for modern Python idioms - PEP 681: add slots=True to all dataclasses for memory efficiency - PEP 698: add @override to all subclass method overrides - PEP 634: use match/case for event type dispatch in iteration_summary and discovery_rate - Add typing_extensions conditional dependency for Python 3.11 --- pyproject.toml | 1 + .../analytics/acquisition_efficiency.py | 7 ++- src/matledger/analytics/calibration.py | 7 ++- src/matledger/analytics/data_quality.py | 5 +- src/matledger/analytics/discovery_rate.py | 36 ++++++++------- src/matledger/analytics/diversity.py | 7 ++- src/matledger/analytics/explore_exploit.py | 5 +- src/matledger/analytics/model_learning.py | 7 ++- src/matledger/analytics/pareto.py | 7 ++- src/matledger/analytics/proxy_gaming.py | 7 ++- src/matledger/analytics/regret.py | 5 +- src/matledger/analytics/reproducibility.py | 5 +- src/matledger/commands/handler.py | 2 +- src/matledger/decisions/counterfactual.py | 4 +- src/matledger/decisions/policy.py | 4 +- src/matledger/projections/budget.py | 5 +- .../projections/iteration_summary.py | 46 ++++++++++--------- src/matledger/projections/negledger.py | 5 +- src/matledger/projections/snapshots.py | 2 +- src/matledger/projections/timeline.py | 7 ++- src/matledger/store/hash_chain.py | 2 +- src/matledger/store/sqlite.py | 7 +++ 22 files changed, 119 insertions(+), 64 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b35988b..34b7eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ dependencies = [ "pydantic>=2.0", "numpy>=1.24", + "typing_extensions>=4.0; python_version < '3.12'", ] [project.optional-dependencies] diff --git a/src/matledger/analytics/acquisition_efficiency.py b/src/matledger/analytics/acquisition_efficiency.py index 5fde434..fd6b28a 100644 --- a/src/matledger/analytics/acquisition_efficiency.py +++ b/src/matledger/analytics/acquisition_efficiency.py @@ -28,6 +28,8 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector @@ -59,7 +61,7 @@ def _rank(vals: list[float]) -> list[float]: return 1.0 - (6.0 * d_sq) / (n * (n * n - 1)) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class AcquisitionEfficiencyVerdict: """Evidence-bearing verdict from the acquisition efficiency tracker. @@ -79,7 +81,7 @@ class AcquisitionEfficiencyVerdict: threshold: float -@dataclass +@dataclass(slots=True) class _IterPredictions: pairs: list[tuple[str, float]] = field(default_factory=list) @@ -105,6 +107,7 @@ def __init__( self._predictions: dict[int, dict[str, float]] = {} self._results: dict[str, float] = {} + @override def apply(self, event: Event) -> None: if event.type == EventType.CANDIDATE_PROPOSED: cid = event.payload.get("candidate_id", "") diff --git a/src/matledger/analytics/calibration.py b/src/matledger/analytics/calibration.py index b6d70d0..9ddbcfc 100644 --- a/src/matledger/analytics/calibration.py +++ b/src/matledger/analytics/calibration.py @@ -29,17 +29,19 @@ from dataclasses import dataclass from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass +@dataclass(slots=True) class _CandidatePrediction: predicted_value: float iteration: int -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class CalibrationVerdict: """Evidence-bearing verdict from the calibration drift detector. @@ -83,6 +85,7 @@ def __init__( self._predictions: dict[str, _CandidatePrediction] = {} self._results: dict[str, list[tuple[float, float, int]]] = {} + @override def apply(self, event: Event) -> None: if event.type == EventType.CANDIDATE_PROPOSED: cid = event.payload.get("candidate_id", "") diff --git a/src/matledger/analytics/data_quality.py b/src/matledger/analytics/data_quality.py index 6934a9e..3147927 100644 --- a/src/matledger/analytics/data_quality.py +++ b/src/matledger/analytics/data_quality.py @@ -30,11 +30,13 @@ from dataclasses import dataclass from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class DataQualityVerdict: """Evidence-bearing verdict from the data quality tracker. @@ -81,6 +83,7 @@ def __init__( self._min_observations = min_observations self._values_by_iter: dict[int, list[float]] = {} + @override def apply(self, event: Event) -> None: if event.type == EventType.RESULT_RECORDED: val = event.payload.get("value") diff --git a/src/matledger/analytics/discovery_rate.py b/src/matledger/analytics/discovery_rate.py index beef355..753f195 100644 --- a/src/matledger/analytics/discovery_rate.py +++ b/src/matledger/analytics/discovery_rate.py @@ -24,6 +24,8 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.events.failures import FailureClass from matledger.projections.framework import Projector @@ -36,7 +38,7 @@ ) -@dataclass +@dataclass(slots=True) class IterationDiscoveryStats: """Discovery statistics for a single iteration.""" @@ -69,7 +71,7 @@ def cost_per_result(self) -> float: return self.cost / self.results -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class DiscoveryRateVerdict: """Summary verdict on campaign discovery efficiency. @@ -109,27 +111,29 @@ def _get_or_create(self, iteration: int) -> IterationDiscoveryStats: self._iterations[iteration] = IterationDiscoveryStats(iteration=iteration) return self._iterations[iteration] + @override def apply(self, event: Event) -> None: stats = self._get_or_create(event.iteration) - if event.type == EventType.EXPERIMENT_STARTED: - stats.cost += event.payload.get("cost", 0.0) + match event.type: + case EventType.EXPERIMENT_STARTED: + stats.cost += event.payload.get("cost", 0.0) - elif event.type == EventType.RESULT_RECORDED: - stats.results += 1 + case EventType.RESULT_RECORDED: + stats.results += 1 - elif event.type == EventType.FAILURE_RECORDED: - stats.failures += 1 - fc = event.payload.get("failure_class", "unknown") - stats.failure_classes[fc] = stats.failure_classes.get(fc, 0) + 1 - if fc in _DATA_QUALITY_FAILURES: - stats.data_quality_failures += 1 + case EventType.FAILURE_RECORDED: + stats.failures += 1 + fc = event.payload.get("failure_class", "unknown") + stats.failure_classes[fc] = stats.failure_classes.get(fc, 0) + 1 + if fc in _DATA_QUALITY_FAILURES: + stats.data_quality_failures += 1 - elif event.type == EventType.CANDIDATE_SELECTED: - stats.selections += 1 + case EventType.CANDIDATE_SELECTED: + stats.selections += 1 - elif event.type == EventType.ITERATION_CLOSED: - self._closed_iterations.add(event.iteration) + case EventType.ITERATION_CLOSED: + self._closed_iterations.add(event.iteration) def reset(self) -> None: self._iterations.clear() diff --git a/src/matledger/analytics/diversity.py b/src/matledger/analytics/diversity.py index 934d84b..2b83288 100644 --- a/src/matledger/analytics/diversity.py +++ b/src/matledger/analytics/diversity.py @@ -37,6 +37,8 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector @@ -132,7 +134,7 @@ def _build_feature_schema(feature_dicts: list[dict[str, Any]]) -> list[str]: return sorted(keys) -@dataclass +@dataclass(slots=True) class DiversityMetrics: """Diversity metrics for one iteration's proposed candidates.""" @@ -142,7 +144,7 @@ class DiversityMetrics: feature_vectors: list[list[float]] = field(default_factory=list) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class DiversityVerdict: """Evidence-bearing verdict from the diversity-collapse monitor. @@ -174,6 +176,7 @@ def __init__(self, window: int = 3) -> None: self._iterations: dict[int, list[dict[str, Any]]] = {} self._campaign_schema: list[str] | None = None + @override def apply(self, event: Event) -> None: if event.type != EventType.CANDIDATE_PROPOSED: return diff --git a/src/matledger/analytics/explore_exploit.py b/src/matledger/analytics/explore_exploit.py index 15aabca..4e48f5c 100644 --- a/src/matledger/analytics/explore_exploit.py +++ b/src/matledger/analytics/explore_exploit.py @@ -26,11 +26,13 @@ from dataclasses import dataclass from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ExploreExploitVerdict: """Evidence-bearing verdict from the explore-exploit tracker. @@ -77,6 +79,7 @@ def __init__( self._proposals: dict[int, list[float]] = {} self._running_best: float = float("-inf") + @override def apply(self, event: Event) -> None: if event.type == EventType.CANDIDATE_PROPOSED: pv = event.payload.get("predicted_value") diff --git a/src/matledger/analytics/model_learning.py b/src/matledger/analytics/model_learning.py index 1ba04b8..aa95875 100644 --- a/src/matledger/analytics/model_learning.py +++ b/src/matledger/analytics/model_learning.py @@ -28,18 +28,20 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass +@dataclass(slots=True) class _VersionData: """Tracks candidates proposed under a specific model version.""" predictions: dict[str, float] = field(default_factory=dict) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ModelLearningVerdict: """Evidence-bearing verdict from the model learning tracker. @@ -82,6 +84,7 @@ def __init__( self._version_data: dict[str, _VersionData] = {} self._results: dict[str, float] = {} + @override def apply(self, event: Event) -> None: if event.type == EventType.MODEL_UPDATED: version = event.payload.get("model_version", "") diff --git a/src/matledger/analytics/pareto.py b/src/matledger/analytics/pareto.py index bec410c..4c40391 100644 --- a/src/matledger/analytics/pareto.py +++ b/src/matledger/analytics/pareto.py @@ -31,6 +31,8 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector @@ -90,14 +92,14 @@ def _hypervolume_1d(values: list[float], ref: float) -> float: return max(0.0, best - ref) -@dataclass +@dataclass(slots=True) class _IterationObjectives: """Objective vectors collected in one iteration.""" points: list[list[float]] = field(default_factory=list) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ParetoVerdict: """Evidence-bearing verdict from the Pareto front tracker. @@ -144,6 +146,7 @@ def __init__( self._min_improvement = min_improvement self._iterations: dict[int, _IterationObjectives] = {} + @override def apply(self, event: Event) -> None: if event.type != EventType.RESULT_RECORDED: return diff --git a/src/matledger/analytics/proxy_gaming.py b/src/matledger/analytics/proxy_gaming.py index b5a9255..415adf8 100644 --- a/src/matledger/analytics/proxy_gaming.py +++ b/src/matledger/analytics/proxy_gaming.py @@ -36,11 +36,13 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass +@dataclass(slots=True) class IterationMetrics: """Predicted vs validated metrics for one iteration.""" @@ -66,7 +68,7 @@ def gap(self) -> float | None: return bp - bv -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ProxyGamingVerdict: """Evidence-bearing verdict from the proxy-gaming detector. @@ -105,6 +107,7 @@ def __init__(self, window: int = 3) -> None: self._iterations: dict[int, IterationMetrics] = {} self._model_update_iterations: list[int] = [] + @override def apply(self, event: Event) -> None: if event.type == EventType.CANDIDATE_PROPOSED: metrics = self._get_or_create(event.iteration) diff --git a/src/matledger/analytics/regret.py b/src/matledger/analytics/regret.py index 92ea313..61bae12 100644 --- a/src/matledger/analytics/regret.py +++ b/src/matledger/analytics/regret.py @@ -27,11 +27,13 @@ from dataclasses import dataclass from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class RegretVerdict: """Evidence-bearing verdict from the cumulative regret tracker. @@ -77,6 +79,7 @@ def __init__( self._best_by_iter: dict[int, float] = {} self._selected_best_by_iter: dict[int, float] = {} + @override def apply(self, event: Event) -> None: if event.type == EventType.RESULT_RECORDED: cid = event.payload.get("candidate_id", "") diff --git a/src/matledger/analytics/reproducibility.py b/src/matledger/analytics/reproducibility.py index 815b840..52efdd0 100644 --- a/src/matledger/analytics/reproducibility.py +++ b/src/matledger/analytics/reproducibility.py @@ -29,11 +29,13 @@ from dataclasses import dataclass from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ReproducibilityVerdict: """Evidence-bearing verdict from the reproducibility monitor. @@ -75,6 +77,7 @@ def __init__( self._fraction_threshold = fraction_threshold self._values: dict[str, list[float]] = {} + @override def apply(self, event: Event) -> None: if event.type != EventType.RESULT_RECORDED: return diff --git a/src/matledger/commands/handler.py b/src/matledger/commands/handler.py index 7fb5df1..46c8f36 100644 --- a/src/matledger/commands/handler.py +++ b/src/matledger/commands/handler.py @@ -37,7 +37,7 @@ ) -@dataclass +@dataclass(slots=True) class _LifecycleState: """Accumulated lifecycle state from scanning the event log.""" diff --git a/src/matledger/decisions/counterfactual.py b/src/matledger/decisions/counterfactual.py index 6037de2..20332ad 100644 --- a/src/matledger/decisions/counterfactual.py +++ b/src/matledger/decisions/counterfactual.py @@ -32,7 +32,7 @@ from matledger.events.envelope import Event, EventType -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class IterationComparison: """Side-by-side comparison of original vs alternate at one iteration.""" @@ -45,7 +45,7 @@ class IterationComparison: diverged_candidates: list[str] = field(default_factory=list) -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ReplayResult: """Full counterfactual replay comparison.""" diff --git a/src/matledger/decisions/policy.py b/src/matledger/decisions/policy.py index 67d092a..554a466 100644 --- a/src/matledger/decisions/policy.py +++ b/src/matledger/decisions/policy.py @@ -22,7 +22,7 @@ from typing import Any, Protocol -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class CandidateState: """What the policy knows about a candidate at a decision point.""" @@ -36,7 +36,7 @@ class CandidateState: rejected: bool = False -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class DecisionContext: """Campaign state visible to a policy at a decision point. diff --git a/src/matledger/projections/budget.py b/src/matledger/projections/budget.py index d3548b5..744cc01 100644 --- a/src/matledger/projections/budget.py +++ b/src/matledger/projections/budget.py @@ -9,11 +9,13 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass +@dataclass(slots=True) class BudgetState: budget_cap: float = 0.0 total_consumed: float = 0.0 @@ -43,6 +45,7 @@ class BudgetProjector(Projector): def __init__(self) -> None: self._state = BudgetState() + @override def apply(self, event: Event) -> None: if event.type == EventType.CAMPAIGN_CREATED: self._state.budget_cap = event.payload.get("budget_cap", 0.0) diff --git a/src/matledger/projections/iteration_summary.py b/src/matledger/projections/iteration_summary.py index 4642d89..532373a 100644 --- a/src/matledger/projections/iteration_summary.py +++ b/src/matledger/projections/iteration_summary.py @@ -20,6 +20,8 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.events.failures import FailureClass from matledger.projections.framework import Projector @@ -32,7 +34,7 @@ ) -@dataclass +@dataclass(slots=True) class IterationStats: """Statistics for a single propose → test → learn → decide cycle.""" @@ -95,34 +97,36 @@ def _get_or_create(self, iteration: int) -> IterationStats: self._iterations[iteration] = IterationStats(iteration=iteration) return self._iterations[iteration] + @override def apply(self, event: Event) -> None: stats = self._get_or_create(event.iteration) - if event.type == EventType.CANDIDATE_PROPOSED: - stats.proposed += 1 - cid = event.payload.get("candidate_id", "") - if cid: - stats.candidates.add(cid) + match event.type: + case EventType.CANDIDATE_PROPOSED: + stats.proposed += 1 + cid = event.payload.get("candidate_id", "") + if cid: + stats.candidates.add(cid) - elif event.type == EventType.EXPERIMENT_STARTED: - stats.experiments += 1 - stats.cost += event.payload.get("cost", 0.0) + case EventType.EXPERIMENT_STARTED: + stats.experiments += 1 + stats.cost += event.payload.get("cost", 0.0) - elif event.type == EventType.RESULT_RECORDED: - stats.results += 1 + case EventType.RESULT_RECORDED: + stats.results += 1 - elif event.type == EventType.FAILURE_RECORDED: - stats.failures += 1 - fc = event.payload.get("failure_class", "unknown") - stats.failure_classes[fc] = stats.failure_classes.get(fc, 0) + 1 - if fc in _DATA_QUALITY_FAILURES: - stats.data_quality_failures += 1 + case EventType.FAILURE_RECORDED: + stats.failures += 1 + fc = event.payload.get("failure_class", "unknown") + stats.failure_classes[fc] = stats.failure_classes.get(fc, 0) + 1 + if fc in _DATA_QUALITY_FAILURES: + stats.data_quality_failures += 1 - elif event.type == EventType.CANDIDATE_SELECTED: - stats.selected += 1 + case EventType.CANDIDATE_SELECTED: + stats.selected += 1 - elif event.type == EventType.CANDIDATE_REJECTED: - stats.rejected += 1 + case EventType.CANDIDATE_REJECTED: + stats.rejected += 1 def reset(self) -> None: self._iterations.clear() diff --git a/src/matledger/projections/negledger.py b/src/matledger/projections/negledger.py index 14520aa..15779a5 100644 --- a/src/matledger/projections/negledger.py +++ b/src/matledger/projections/negledger.py @@ -22,12 +22,14 @@ from dataclasses import dataclass from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.events.failures import FailureClass from matledger.projections.framework import Projector -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class FailureEntry: seq: int candidate_id: str @@ -47,6 +49,7 @@ def __init__(self) -> None: self._by_iteration: dict[int, list[FailureEntry]] = {} self._by_candidate: dict[str, list[FailureEntry]] = {} + @override def apply(self, event: Event) -> None: if event.type != EventType.FAILURE_RECORDED: return diff --git a/src/matledger/projections/snapshots.py b/src/matledger/projections/snapshots.py index 4e08f14..3af8688 100644 --- a/src/matledger/projections/snapshots.py +++ b/src/matledger/projections/snapshots.py @@ -31,7 +31,7 @@ """ -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class Snapshot: projector_name: str seq: int diff --git a/src/matledger/projections/timeline.py b/src/matledger/projections/timeline.py index 7c6254a..5e36d2a 100644 --- a/src/matledger/projections/timeline.py +++ b/src/matledger/projections/timeline.py @@ -9,11 +9,13 @@ from dataclasses import dataclass, field from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.projections.framework import Projector -@dataclass +@dataclass(slots=True) class TimelineEntry: seq: int event_type: str @@ -23,7 +25,7 @@ class TimelineEntry: payload: dict[str, Any] -@dataclass +@dataclass(slots=True) class CandidateTimeline: candidate_id: str entries: list[TimelineEntry] = field(default_factory=list) @@ -59,6 +61,7 @@ class TimelineProjector(Projector): def __init__(self) -> None: self._timelines: dict[str, CandidateTimeline] = {} + @override def apply(self, event: Event) -> None: if event.type not in _CANDIDATE_EVENT_TYPES: return diff --git a/src/matledger/store/hash_chain.py b/src/matledger/store/hash_chain.py index 45d44d9..5829b2b 100644 --- a/src/matledger/store/hash_chain.py +++ b/src/matledger/store/hash_chain.py @@ -23,7 +23,7 @@ from matledger.events.envelope import Event -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ChainBreak: """A detected break in the hash chain.""" diff --git a/src/matledger/store/sqlite.py b/src/matledger/store/sqlite.py index a0137d0..4ecb6ca 100644 --- a/src/matledger/store/sqlite.py +++ b/src/matledger/store/sqlite.py @@ -17,6 +17,8 @@ from pathlib import Path from typing import Any +from typing_extensions import override + from matledger.events.envelope import Event, EventType from matledger.store.base import EventStore from matledger.store.hash_chain import compute_next_prev_hash @@ -164,6 +166,7 @@ def rollback_batch(self) -> None: self._batch_local.active = False self._write_lock.release() + @override def append(self, event: Event) -> Event: wc = self._write_conn in_batch = getattr(self._batch_local, "active", False) @@ -243,9 +246,11 @@ def _active_read_conn(self) -> sqlite3.Connection: return self._write_conn return self._read_conn + @override def get(self, seq: int) -> Event | None: return self._read_event(self._active_read_conn, seq) + @override def iter_from(self, start_seq: int = 0) -> Iterator[Event]: cursor = self._active_read_conn.execute( "SELECT * FROM events WHERE seq >= ? ORDER BY seq", (start_seq,) @@ -256,12 +261,14 @@ def iter_from(self, start_seq: int = 0) -> Iterator[Event]: def last_seq(self) -> int: return self._read_seq(self._active_read_conn) + @override def last_event(self) -> Event | None: seq = self.last_seq() if seq < 0: return None return self.get(seq) + @override def count(self) -> int: row = self._active_read_conn.execute("SELECT COUNT(*) FROM events").fetchone() return row[0] if row else 0 From 9bbccb4fdf3e7317759e84732d29a29a98f9186a Mon Sep 17 00:00:00 2001 From: jayson_mulwa Date: Fri, 3 Jul 2026 13:26:02 +0300 Subject: [PATCH 3/5] refactor: narrow blind except, apply auto-fixable lint cleanups - Replace bare except Exception with (MatledgerError, ValidationError) in ingest - Use set comprehension instead of set(generator) in discovery_rate - Replace if-stmt with max() calls in explore_exploit - Remove superfluous else-return in framework --- src/matledger/analytics/discovery_rate.py | 2 +- src/matledger/analytics/explore_exploit.py | 9 +++------ src/matledger/commands/ingest.py | 18 +++++++++--------- src/matledger/projections/framework.py | 3 +-- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/matledger/analytics/discovery_rate.py b/src/matledger/analytics/discovery_rate.py index 753f195..50ebba4 100644 --- a/src/matledger/analytics/discovery_rate.py +++ b/src/matledger/analytics/discovery_rate.py @@ -160,7 +160,7 @@ def get_state(self) -> dict[str, Any]: def analyze(self) -> DiscoveryRateVerdict: """Compute the discovery rate verdict.""" - all_iters = set(it for it in self._iterations if it > 0) + all_iters = {it for it in self._iterations if it > 0} all_iters |= self._closed_iterations sorted_iters = sorted(all_iters) diff --git a/src/matledger/analytics/explore_exploit.py b/src/matledger/analytics/explore_exploit.py index 4e48f5c..c1e1fe5 100644 --- a/src/matledger/analytics/explore_exploit.py +++ b/src/matledger/analytics/explore_exploit.py @@ -86,8 +86,7 @@ def apply(self, event: Event) -> None: if pv is not None: pv_f = float(pv) self._proposals.setdefault(event.iteration, []).append(pv_f) - if pv_f > self._running_best: - self._running_best = pv_f + self._running_best = max(pv_f, self._running_best) def reset(self) -> None: self._proposals.clear() @@ -104,8 +103,7 @@ def _exploit_ratio_by_iteration(self) -> dict[int, float]: if running_best == float("-inf"): for pv in pvs: - if pv > running_best: - running_best = pv + running_best = max(pv, running_best) ratios[it] = 0.5 continue @@ -114,8 +112,7 @@ def _exploit_ratio_by_iteration(self) -> dict[int, float]: ratios[it] = n_exploit / len(pvs) for pv in pvs: - if pv > running_best: - running_best = pv + running_best = max(pv, running_best) return ratios diff --git a/src/matledger/commands/ingest.py b/src/matledger/commands/ingest.py index 6e9f1a5..4382d4c 100644 --- a/src/matledger/commands/ingest.py +++ b/src/matledger/commands/ingest.py @@ -11,10 +11,12 @@ from pathlib import Path from typing import Any +from pydantic import ValidationError + from matledger.commands.handler import CommandHandler from matledger.events.catalog import validate_payload from matledger.events.envelope import Event, EventType -from matledger.exceptions import IngestError +from matledger.exceptions import IngestError, MatledgerError from matledger.store.sqlite import SQLiteEventStore @@ -24,7 +26,7 @@ def _validate_batch(event_type: str, rows: list[dict[str, Any]]) -> None: for i, row in enumerate(rows): try: validate_payload(event_type, row) - except Exception as exc: + except (MatledgerError, ValidationError) as exc: errors.append(f"Row {i}: {exc}") if errors: raise IngestError( @@ -38,10 +40,10 @@ def _load_file(path: Path) -> Any: if suffix == ".json": return json.loads(text) - elif suffix == ".csv": + if suffix == ".csv": reader = csv.DictReader(StringIO(text)) return list(reader) - elif suffix in (".yml", ".yaml"): + if suffix in (".yml", ".yaml"): try: import yaml except ImportError as e: @@ -49,8 +51,7 @@ def _load_file(path: Path) -> Any: "PyYAML is required for YAML import: pip install matledger[ingest]" ) from e return yaml.safe_load(text) - else: - raise IngestError(f"Unsupported file format: {suffix}") + raise IngestError(f"Unsupported file format: {suffix}") def ingest_candidates( @@ -192,7 +193,6 @@ def ingest_from_file( if not model_version: raise IngestError("model_version is required for candidate ingest") return ingest_candidates(handler, data, iteration, model_version, actor) - elif data_type == "results": + if data_type == "results": return ingest_results(handler, data, iteration, actor) - else: - raise IngestError(f"Unknown data_type: {data_type}. Use 'candidates' or 'results'") + raise IngestError(f"Unknown data_type: {data_type}. Use 'candidates' or 'results'") diff --git a/src/matledger/projections/framework.py b/src/matledger/projections/framework.py index 2fbac0f..e45cf11 100644 --- a/src/matledger/projections/framework.py +++ b/src/matledger/projections/framework.py @@ -188,8 +188,7 @@ def rebuild_from_snapshot( snapshot_seqs[name] = -1 max_snapshot_seq = max(snapshot_seqs.values()) if snapshot_seqs else -1 - if max_snapshot_seq > self._last_applied_seq: - self._last_applied_seq = max_snapshot_seq + self._last_applied_seq = max(max_snapshot_seq, self._last_applied_seq) count = 0 for event in events: From 3b4bbe9c2d2b2fe80e21fb7a3f563abdc112b4b9 Mon Sep 17 00:00:00 2001 From: jayson_mulwa Date: Fri, 3 Jul 2026 13:28:22 +0300 Subject: [PATCH 4/5] docs(readme): add shields.io badges with doc links to tech stack table --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 86298b8..e94ddec 100644 --- a/README.md +++ b/README.md @@ -182,16 +182,16 @@ Every campaign fact is recorded as an immutable, hash-chained event. The write s | Layer | Technology | |-------|-----------| -| Language | Python 3.11+ | -| Schemas | Pydantic v2 | -| Event store | SQLite (WAL mode, append-only) | +| Language | [![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?logo=python&logoColor=white)](https://docs.python.org/3/) | +| Schemas | [![Pydantic](https://img.shields.io/badge/Pydantic-v2-E92063?logo=pydantic&logoColor=white)](https://docs.pydantic.dev/) | +| Event store | [![SQLite](https://img.shields.io/badge/SQLite-WAL_mode-003B57?logo=sqlite&logoColor=white)](https://www.sqlite.org/docs.html) | | Hash chain | SHA-256, linear tamper-evident chain | -| CLI | Typer | -| API | FastAPI + uvicorn | -| Analytics | numpy (core), FAISS (optional) | -| Charts | Matplotlib | -| Type checking | mypy (strict) | -| Linting | ruff | +| CLI | [![Typer](https://img.shields.io/badge/Typer-0.9%2B-009688)](https://typer.tiangolo.com/) | +| API | [![FastAPI](https://img.shields.io/badge/FastAPI-0.100%2B-009688?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) | +| Analytics | [![NumPy](https://img.shields.io/badge/NumPy-1.24%2B-013243?logo=numpy&logoColor=white)](https://numpy.org/doc/) [![FAISS](https://img.shields.io/badge/FAISS-optional-4285F4)](https://faiss.ai/) | +| Charts | [![Matplotlib](https://img.shields.io/badge/Matplotlib-3.7%2B-11557C)](https://matplotlib.org/stable/) | +| Type checking | [![mypy](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/) | +| Linting | [![Ruff](https://img.shields.io/badge/Ruff-zero_errors-D7FF64?logo=ruff&logoColor=black)](https://docs.astral.sh/ruff/) | ### Event Model From 0fe00e74bd09b5eca9b06b10d7b1b753e236a3a4 Mon Sep 17 00:00:00 2001 From: jayson_mulwa Date: Fri, 3 Jul 2026 13:29:43 +0300 Subject: [PATCH 5/5] docs(readme): clarify BO abbreviation before analytics table --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e94ddec..bcfd6ea 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,8 @@ src/matledger/ matledger's differentiators — trajectory analytics that CRUD experiment trackers structurally cannot provide. +*BO = Bayesian optimisation — the closed-loop surrogate-driven search that matledger records.* + | Analytic | What it detects | Signal | Field relevance | |----------|----------------|--------|----------------| | **Proxy gaming** | Surrogate predictions rising while validated results stay flat | Goodhart's Law in action | All BO |