From f05b374eb6ea3e6f1015b70ebfded33262f524ab Mon Sep 17 00:00:00 2001 From: "Ziyang (Colin) Qi" <272929055+Colin-Qi@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:00:33 -0400 Subject: [PATCH 01/39] Add validated D2D MOBO workflow through Step 2C --- .DS_Store | Bin 10244 -> 0 bytes .gitignore | 6 + .vscode/settings.json | 7 - README.md | 177 +- configs/FA0.9CS0.1PbI3_260407_Config.yaml | 68 + configs/d2d_step2a_provisional.yaml | 96 + configs/d2d_step2b_debug.yaml | 133 + configs/d2d_step2c_debug.yaml | 129 + data/.DS_Store | Bin 6148 -> 0 bytes docs/D2D_CAMPAIGN_SPEC.md | 199 + docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md | 154 + docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md | 97 + docs/D2D_STEP2A_COMPUTATIONAL_CORE.md | 204 + docs/D2D_STEP2C_ROBUSTNESS_METHOD.md | 209 + docs/REPO_AUDIT_D2D.md | 142 + docs/STEP1_HANDOFF.md | 273 ++ docs/STEP2A_HANDOFF.md | 319 ++ docs/STEP2B_DEBUG_HANDOFF.md | 272 ++ docs/STEP2C_ROBUSTNESS_HANDOFF.md | 298 ++ examples/d2d_step2a_benchmark.py | 70 + examples/d2d_step2a_synthetic.py | 464 ++ examples/d2d_step2b_debug.py | 61 + examples/d2d_step2c_robustness.py | 89 + examples/d2d_step2c_synthetic_ci.py | 55 + ...Global Distance Candidate generation.ipynb | 1013 +++++ pyproject.toml | 60 +- requirements.txt | 28 +- requirements/constraints.txt | 19 + requirements/dev.txt | 4 + scripts/scripts.md | 0 setup.py | 46 +- src/__pycache__/__init__.cpython-310.pyc | Bin 156 -> 0 bytes src/__pycache__/__init__.cpython-311.pyc | Bin 200 -> 0 bytes src/__pycache__/acquisition.cpython-310.pyc | Bin 14276 -> 0 bytes src/__pycache__/constraints.cpython-310.pyc | Bin 3684 -> 0 bytes src/__pycache__/data.cpython-310.pyc | Bin 8628 -> 0 bytes src/__pycache__/design.cpython-310.pyc | Bin 4101 -> 0 bytes src/__pycache__/design.cpython-311.pyc | Bin 2495 -> 0 bytes src/__pycache__/lhs.cpython-310.pyc | Bin 9836 -> 0 bytes src/__pycache__/metrics.cpython-310.pyc | Bin 3945 -> 0 bytes src/__pycache__/model.cpython-311.pyc | Bin 1660 -> 0 bytes src/__pycache__/models.cpython-310.pyc | Bin 12339 -> 0 bytes src/__pycache__/plotting.cpython-310.pyc | Bin 20984 -> 0 bytes src/__pycache__/run_pipeline.cpython-311.pyc | Bin 5188 -> 0 bytes src/__pycache__/utils.cpython-310.pyc | Bin 6566 -> 0 bytes src/__pycache__/utils.cpython-311.pyc | Bin 3407 -> 0 bytes src/mobo_kit/batch_comparison.py | 180 + src/mobo_kit/batch_selection.py | 398 ++ src/mobo_kit/candidate_diagnostics.py | 367 ++ src/mobo_kit/candidate_pool.py | 247 ++ src/mobo_kit/cli.py | 158 +- src/mobo_kit/constraints.py | 203 +- src/mobo_kit/d2d_campaign.py | 1182 +++++ src/mobo_kit/d2d_r2_test.py | 162 + src/mobo_kit/d2d_scores.py | 663 +++ src/mobo_kit/d2d_step2b_debug.py | 1533 +++++++ src/mobo_kit/d2d_step2c_config.py | 918 ++++ src/mobo_kit/d2d_step2c_robustness.py | 3922 +++++++++++++++++ src/mobo_kit/d2d_step2c_synthetic.py | 349 ++ src/mobo_kit/design.py | 241 +- src/mobo_kit/discrete_refinement.py | 718 +++ src/mobo_kit/lhs.py | 743 ++-- src/mobo_kit/main.py | 325 +- src/mobo_kit/model_validation.py | 1009 +++++ src/mobo_kit/objectives.py | 351 ++ src/mobo_kit/observation_influence.py | 874 ++++ src/mobo_kit/production_gate.py | 528 +++ src/mobo_kit/qlognehvi_batch.py | 389 ++ src/mobo_kit/robust_regions.py | 752 ++++ src/mobo_kit/robustness_plots.py | 1322 ++++++ src/mobo_kit/sobol_pool.py | 399 ++ src/mobo_kit/step2c_artifacts.py | 2272 ++++++++++ src/mobo_kit/ucb_hvi.py | 860 ++++ src/mobo_kit/utils.py | 654 ++- src/mobo_kit/workbook_schema.py | 829 ++++ .../smoke_test.cpython-310-pytest-8.4.1.pyc | Bin 2398 -> 0 bytes ...t_acquisition.cpython-310-pytest-8.4.1.pyc | Bin 33216 -> 0 bytes .../test_design.cpython-310-pytest-8.4.1.pyc | Bin 5344 -> 0 bytes .../test_design.cpython-311-pytest-7.4.0.pyc | Bin 12651 -> 0 bytes ...st_gp_fitting.cpython-310-pytest-8.4.1.pyc | Bin 165 -> 0 bytes .../test_main.cpython-310-pytest-8.4.1.pyc | Bin 4019 -> 0 bytes .../test_main.cpython-311-pytest-7.4.0.pyc | Bin 8365 -> 0 bytes .../test_models.cpython-310-pytest-8.4.1.pyc | Bin 11843 -> 0 bytes ...test_plotting.cpython-310-pytest-8.4.1.pyc | Bin 2493 -> 0 bytes .../__pycache__/test_plotting.cpython-310.pyc | Bin 1351 -> 0 bytes ...hetic_bo_loop.cpython-310-pytest-8.4.1.pyc | Bin 3432 -> 0 bytes tests/simple_gp_test.py | 294 -- tests/smoke_test.py | 64 - tests/synthetic_test.py | 625 --- tests/test_acquisition.py | 905 +--- tests/test_batch_comparison.py | 123 + tests/test_batch_selection.py | 295 ++ tests/test_candidate_diagnostics.py | 132 + tests/test_candidate_pool.py | 150 + tests/test_csv_parser.py | 317 ++ tests/test_d2d_baseline.py | 338 ++ tests/test_d2d_campaign.py | 595 +++ tests/test_d2d_notebook.py | 151 + tests/test_d2d_r2_test.py | 184 + tests/test_d2d_scores.py | 299 ++ tests/test_d2d_step2b_debug.py | 308 ++ tests/test_d2d_step2c_config.py | 156 + tests/test_d2d_step2c_robustness_helpers.py | 624 +++ tests/test_d2d_step2c_synthetic.py | 113 + tests/test_design.py | 134 +- tests/test_discrete_refinement.py | 258 ++ tests/test_lhs.py | 165 + tests/test_model_validation.py | 419 ++ tests/test_models.py | 321 +- tests/test_objectives.py | 328 ++ tests/test_observation_influence.py | 319 ++ tests/test_plotting.py | 163 +- tests/test_production_gate.py | 281 ++ tests/test_qlognehvi_batch.py | 314 ++ tests/test_robust_regions.py | 313 ++ tests/test_robustness_plots.py | 539 +++ tests/test_sobol_pool.py | 236 + tests/test_step2a_synthetic.py | 81 + tests/test_step2c_artifacts.py | 917 ++++ tests/test_ucb_hvi.py | 447 ++ tests/test_workbook_schema.py | 681 +++ ...d_BatchBO_TwoConstraints_20201019_TH.ipynb | 0 .../Example/BMP/20210305141905.bmp | 0 .../Example/BMP/20210305142405.bmp | 0 .../Example/BMP/20210305143906.bmp | 0 .../Example/BMP/20210305145906.bmp | 0 .../Example/BMP/20210305150406.bmp | 0 .../Example/BMP/20210305151907.bmp | 0 .../Example/BMP/20210305152407.bmp | 0 .../Example/BMP/20210305153907.bmp | 0 .../Example/BMP/20210305154908.bmp | 0 .../Example/BMP/20210305155408.bmp | 0 .../Example/BMP/20210305160908.bmp | 0 .../Example/BMP/20210305161408.bmp | 0 .../Example/BMP/20210305162909.bmp | 0 .../Example/BMP/20210305163409.bmp | 0 .../Example/BMP/20210305164909.bmp | 0 .../Example/BMP/20210305165910.bmp | 0 .../Example/BMP/20210305170410.bmp | 0 .../Example/BMP/20210305173911.bmp | 0 .../Example/BMP/20210305174411.bmp | 0 .../Example/BMP/20210305175911.bmp | 0 .../Example/BMP/20210305180912.bmp | 0 .../Example/BMP/20210305183412.bmp | 0 .../Example/BMP/20210305184913.bmp | 0 .../Example/BMP/20210305185413.bmp | 0 .../Example/BMP/20210305191413.bmp | 0 .../Example/BMP/20210305193414.bmp | 0 .../Example/BMP/20210305194914.bmp | 0 .../Example/BMP/20210305195414.bmp | 0 .../Example/BMP/20210305200415.bmp | 0 .../Example/BMP/20210305203916.bmp | 0 .../Example/BMP/20210305204416.bmp | 0 .../Example/BMP/20210305205916.bmp | 0 .../Example/BMP/20210305211917.bmp | 0 .../Example/BMP/20210305212417.bmp | 0 .../Example/BMP/20210305214417.bmp | 0 .../Example/BMP/20210305221418.bmp | 0 .../Example/BMP/20210305222918.bmp | 0 .../Example/BMP/20210305224919.bmp | 0 .../Example/BMP/20210305225419.bmp | 0 .../Example/BMP/20210305230919.bmp | 0 .../Example/BMP/20210305232920.bmp | 0 .../Example/BMP/20210305234921.bmp | 0 .../Example/BMP/20210305235421.bmp | 0 .../Example/BMP/20210306000921.bmp | 0 .../Example/BMP/20210306003422.bmp | 0 .../Example/BMP/20210306004922.bmp | 0 .../Example/BMP/20210306005422.bmp | 0 .../Example/BMP/20210306011423.bmp | 0 .../Example/BMP/20210306012923.bmp | 0 .../Example/BMP/20210306013424.bmp | 0 .../Example/BMP/20210306014924.bmp | 0 .../Example/BMP/20210306015424.bmp | 0 .../Example/BMP/20210306020924.bmp | 0 .../Example/BMP/20210306021425.bmp | 0 .../Example/BMP/20210306022925.bmp | 0 .../Example/BMP/20210306023425.bmp | 0 .../Example/Color_operations.py | 0 .../Example/Crop_my_video.py | 0 .../Example/RGB_extractor.py | 0 .../Example/RGB_extractor_Xrite_CC.py | 0 .../Example/RGB_extractor_savefigs.py | 0 .../Example/Test_crop_box.py | 0 .../Example/input_extractor_2.ipynb | 0 .../Example/start and end values.txt | 0 ..._builtin_unknown_constraint_function.ipynb | 0 ...modified_unknown_constraint_function.ipynb | 0 .../Data Summary for New Plasma Cleaned.csv | 0 .../ML Perov Data/ML Perov Data Stanford.xlsx | 0 ...new_plamsa_previous_selected_20200927.xlsx | 0 ...vScaleup_Check_All_Experimental_Data.ipynb | 0 ...nd1_Check_Experimental_Data_20200922.ipynb | 0 ...irst_BatchBO_TwoConstraints_20200927.ipynb | 0 ...nd2_Check_Experimental_Data_20200930.ipynb | 0 ...cond_BatchBO_TwoConstraints_20200930.ipynb | 0 ...nd3_Check_Experimental_Data_20201019.ipynb | 0 ...hird_BatchBO_TwoConstraints_20201019.ipynb | 0 ...urth_BatchBO_TwoConstraints_20201026.ipynb | 0 ...ifth_BatchBO_TwoConstraints_20201108.ipynb | 0 ...tropy_Search_TwoConstraints_20201220.ipynb | 0 ...zation for Perovskite Solar Cells v1.ipynb | 0 ...zation for Perovskite Solar Cells v2.ipynb | 0 unsorted/Code/ReadMe.txt | 0 ...nd1_Check_Experimental_Data_20200922.ipynb | 1577 ------- unsorted/Constrained_MOBO.ipynb | 0 .../20221028 - MOBO Vectors.pptx | Bin 36823 -> 0 bytes .../round1_constraint_R1.png | Bin 84316 -> 0 bytes .../round2_selectionX_constraints_R1.png | Bin 296433 -> 0 bytes unsorted/Grid-snapped-LHS.ipynb | 0 ...und0_Initial_Sampling_FOM_MIT_g2g_v2.ipynb | 679 --- unsorted/UW-MIT FOM MOBO-Copy4.ipynb | 1560 ------- 212 files changed, 35802 insertions(+), 6843 deletions(-) delete mode 100644 .DS_Store delete mode 100644 .vscode/settings.json create mode 100644 configs/FA0.9CS0.1PbI3_260407_Config.yaml create mode 100644 configs/d2d_step2a_provisional.yaml create mode 100644 configs/d2d_step2b_debug.yaml create mode 100644 configs/d2d_step2c_debug.yaml delete mode 100644 data/.DS_Store create mode 100644 docs/D2D_CAMPAIGN_SPEC.md create mode 100644 docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md create mode 100644 docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md create mode 100644 docs/D2D_STEP2A_COMPUTATIONAL_CORE.md create mode 100644 docs/D2D_STEP2C_ROBUSTNESS_METHOD.md create mode 100644 docs/REPO_AUDIT_D2D.md create mode 100644 docs/STEP1_HANDOFF.md create mode 100644 docs/STEP2A_HANDOFF.md create mode 100644 docs/STEP2B_DEBUG_HANDOFF.md create mode 100644 docs/STEP2C_ROBUSTNESS_HANDOFF.md create mode 100644 examples/d2d_step2a_benchmark.py create mode 100644 examples/d2d_step2a_synthetic.py create mode 100644 examples/d2d_step2b_debug.py create mode 100644 examples/d2d_step2c_robustness.py create mode 100644 examples/d2d_step2c_synthetic_ci.py create mode 100644 notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb create mode 100644 requirements/constraints.txt create mode 100644 requirements/dev.txt delete mode 100644 scripts/scripts.md delete mode 100644 src/__pycache__/__init__.cpython-310.pyc delete mode 100644 src/__pycache__/__init__.cpython-311.pyc delete mode 100644 src/__pycache__/acquisition.cpython-310.pyc delete mode 100644 src/__pycache__/constraints.cpython-310.pyc delete mode 100644 src/__pycache__/data.cpython-310.pyc delete mode 100644 src/__pycache__/design.cpython-310.pyc delete mode 100644 src/__pycache__/design.cpython-311.pyc delete mode 100644 src/__pycache__/lhs.cpython-310.pyc delete mode 100644 src/__pycache__/metrics.cpython-310.pyc delete mode 100644 src/__pycache__/model.cpython-311.pyc delete mode 100644 src/__pycache__/models.cpython-310.pyc delete mode 100644 src/__pycache__/plotting.cpython-310.pyc delete mode 100644 src/__pycache__/run_pipeline.cpython-311.pyc delete mode 100644 src/__pycache__/utils.cpython-310.pyc delete mode 100644 src/__pycache__/utils.cpython-311.pyc create mode 100644 src/mobo_kit/batch_comparison.py create mode 100644 src/mobo_kit/batch_selection.py create mode 100644 src/mobo_kit/candidate_diagnostics.py create mode 100644 src/mobo_kit/candidate_pool.py create mode 100644 src/mobo_kit/d2d_campaign.py create mode 100644 src/mobo_kit/d2d_r2_test.py create mode 100644 src/mobo_kit/d2d_scores.py create mode 100644 src/mobo_kit/d2d_step2b_debug.py create mode 100644 src/mobo_kit/d2d_step2c_config.py create mode 100644 src/mobo_kit/d2d_step2c_robustness.py create mode 100644 src/mobo_kit/d2d_step2c_synthetic.py create mode 100644 src/mobo_kit/discrete_refinement.py create mode 100644 src/mobo_kit/model_validation.py create mode 100644 src/mobo_kit/objectives.py create mode 100644 src/mobo_kit/observation_influence.py create mode 100644 src/mobo_kit/production_gate.py create mode 100644 src/mobo_kit/qlognehvi_batch.py create mode 100644 src/mobo_kit/robust_regions.py create mode 100644 src/mobo_kit/robustness_plots.py create mode 100644 src/mobo_kit/sobol_pool.py create mode 100644 src/mobo_kit/step2c_artifacts.py create mode 100644 src/mobo_kit/ucb_hvi.py create mode 100644 src/mobo_kit/workbook_schema.py delete mode 100644 tests/__pycache__/smoke_test.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_acquisition.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_design.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_design.cpython-311-pytest-7.4.0.pyc delete mode 100644 tests/__pycache__/test_gp_fitting.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_main.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_main.cpython-311-pytest-7.4.0.pyc delete mode 100644 tests/__pycache__/test_models.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_plotting.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/__pycache__/test_plotting.cpython-310.pyc delete mode 100644 tests/__pycache__/test_synthetic_bo_loop.cpython-310-pytest-8.4.1.pyc delete mode 100644 tests/simple_gp_test.py delete mode 100644 tests/smoke_test.py delete mode 100644 tests/synthetic_test.py create mode 100644 tests/test_batch_comparison.py create mode 100644 tests/test_batch_selection.py create mode 100644 tests/test_candidate_diagnostics.py create mode 100644 tests/test_candidate_pool.py create mode 100644 tests/test_csv_parser.py create mode 100644 tests/test_d2d_baseline.py create mode 100644 tests/test_d2d_campaign.py create mode 100644 tests/test_d2d_notebook.py create mode 100644 tests/test_d2d_r2_test.py create mode 100644 tests/test_d2d_scores.py create mode 100644 tests/test_d2d_step2b_debug.py create mode 100644 tests/test_d2d_step2c_config.py create mode 100644 tests/test_d2d_step2c_robustness_helpers.py create mode 100644 tests/test_d2d_step2c_synthetic.py create mode 100644 tests/test_discrete_refinement.py create mode 100644 tests/test_lhs.py create mode 100644 tests/test_model_validation.py create mode 100644 tests/test_objectives.py create mode 100644 tests/test_observation_influence.py create mode 100644 tests/test_production_gate.py create mode 100644 tests/test_qlognehvi_batch.py create mode 100644 tests/test_robust_regions.py create mode 100644 tests/test_robustness_plots.py create mode 100644 tests/test_sobol_pool.py create mode 100644 tests/test_step2a_synthetic.py create mode 100644 tests/test_step2c_artifacts.py create mode 100644 tests/test_ucb_hvi.py create mode 100644 tests/test_workbook_schema.py delete mode 100644 unsorted/Code/20211101_PerovScaleup_Round3_Third_BatchBO_TwoConstraints_20201019_TH.ipynb delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305141905.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305142405.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305143906.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305145906.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305150406.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305151907.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305152407.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305153907.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305154908.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305155408.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305160908.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305161408.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305162909.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305163409.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305164909.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305165910.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305170410.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305173911.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305174411.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305175911.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305180912.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305183412.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305184913.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305185413.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305191413.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305193414.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305194914.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305195414.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305200415.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305203916.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305204416.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305205916.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305211917.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305212417.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305214417.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305221418.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305222918.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305224919.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305225419.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305230919.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305232920.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305234921.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305235421.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306000921.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306003422.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306004922.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306005422.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306011423.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306012923.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306013424.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306014924.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306015424.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306020924.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306021425.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306022925.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306023425.bmp delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Color_operations.py delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Crop_my_video.py delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor.py delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor_Xrite_CC.py delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor_savefigs.py delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Test_crop_box.py delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/input_extractor_2.ipynb delete mode 100644 unsorted/Code/DegradationChamber/DegradationAnalysis/Example/start and end values.txt delete mode 100644 unsorted/Code/Emukit BO Example/Emukit_test_builtin_unknown_constraint_function.ipynb delete mode 100644 unsorted/Code/Emukit BO Example/Emukit_test_modified_unknown_constraint_function.ipynb delete mode 100644 unsorted/Code/ML Perov Data/Data Summary for New Plasma Cleaned.csv delete mode 100644 unsorted/Code/ML Perov Data/ML Perov Data Stanford.xlsx delete mode 100644 unsorted/Code/ML Perov Data/new_plamsa_previous_selected_20200927.xlsx delete mode 100644 unsorted/Code/PerovScaleup_Check_All_Experimental_Data.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round1_First_BatchBO_TwoConstraints_20200927.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round2_Check_Experimental_Data_20200930.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round2_Second_BatchBO_TwoConstraints_20200930.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round3_Check_Experimental_Data_20201019.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round3_Third_BatchBO_TwoConstraints_20201019.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round4_Fourth_BatchBO_TwoConstraints_20201026.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round5_Fifth_BatchBO_TwoConstraints_20201108.ipynb delete mode 100644 unsorted/Code/PerovScaleup_Round6_Sixth_BatchBO_Entropy_Search_TwoConstraints_20201220.ipynb delete mode 100644 unsorted/Code/Process Optimization for Perovskite Solar Cells v1.ipynb delete mode 100644 unsorted/Code/Process Optimization for Perovskite Solar Cells v2.ipynb delete mode 100644 unsorted/Code/ReadMe.txt delete mode 100644 unsorted/Code/Salinan PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb delete mode 100644 unsorted/Constrained_MOBO.ipynb delete mode 100644 unsorted/Data Visualization/20221028 - MOBO Vectors.pptx delete mode 100644 unsorted/Data Visualization/round1_constraint_R1.png delete mode 100644 unsorted/Data Visualization/round2_selectionX_constraints_R1.png delete mode 100644 unsorted/Grid-snapped-LHS.ipynb delete mode 100644 unsorted/PerovScaleup_Round0_Initial_Sampling_FOM_MIT_g2g_v2.ipynb delete mode 100644 unsorted/UW-MIT FOM MOBO-Copy4.ipynb diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index dff2ebe6c923b381ad2ad7ea1b5255a2c439fdd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMzi$&U6n>YcG3=8I%V4LwR#LS0t5c$Wjkz1~dbj0nLDBKr`^aFo3_=w3x$O z>Q*zL8PE(IF+k4;3oDgrAzNIsqXQe30-%iJFe}(k(GE&W$h43xE>VNU;I1gSt07Vh zgS+FnCG65dwz%Z(V2I?y5Rna$p%^SW&TlbuFw$J=Rx_X(2s1!)_c&aGO>iMl^7pNg zeOXr_*{oeAHjbJ558nS;{`2LYs&|gnSGT0Qf#)rBg;P)fA3r4sz_%NIRm}g{u9jUt zn40=-#0KJnrxIqu95$ckwn!rv6oR^+Db$|Or$>a8?RLLlueqg0K6P%9ctOGSN;T%- zIwdq&f9!e=X=F&-C2}%GPdTcUHfi(xrMKcD@w0m8cO5T7}voC51^pL zh1j^PljB~zF8qd$G$Eh0TEU*$$P)Q-4Y~6T6X9@-clic><48&V!p1g zKmuyGEI!nsOwS$#J;W~K99~S#?nNwo(xDEt)_qMa1MnHH?S865>%T zWIR9fZ`TErA*&9a-c~9e7Ht5d)u4DX&Sdd2>LSapDU*5d@$XAPrtp(boUpbxd0ks> zkevj6kL8uLGMeS9t}W0g7M*2{wunGw;%4`7SqUyIJ(C=TkI@hG@;_uFV_7ja)&!Oa zyAGn`>=az12W$~^qZ!Z)Xa+O`ngPu~KN%R{!7O_Jf4}$t|NVrdV`&C71AS(|h-Y)z z3Wb}cTM3pUh*j @@ -52,48 +52,36 @@ We recommend creating a clean Python environment using `conda` or `venv`: ```bash # Create and activate environment -conda create -n mobo-fom python=3.10 -conda activate mobo-fom +conda create -n mobo-kit python=3.11 +conda activate mobo-kit # Or using venv python -m venv mobo-env source mobo-env/bin/activate # On Windows: mobo-env\Scripts\activate # Install MOBO-Kit -git clone https://github.com/PV-Lab/MOBO-FOM.git -cd MOBO-FOM -pip install -e . +git clone https://github.com/PV-Lab/MOBO-Kit.git +cd MOBO-Kit +python -m pip install -r requirements/dev.txt ``` -This will automatically install all required dependencies including: -- Core scientific computing: numpy, pandas, scipy, matplotlib, seaborn, scikit-learn -- Machine learning: torch, gpytorch, botorch, emukit -- Additional tools: shap, pyDOE, pyyaml +This installs the core scientific stack, the tested Torch/GPyTorch/BoTorch +combination, workbook auditing support, and the development test tools. -### GPU Support (CUDA [Windows]) +### Optional GPU support -MOBO-Kit uses PyTorch for machine learning models. By default, the installation includes the CPU-only version of PyTorch. For GPU acceleration, you'll need to install the CUDA version of PyTorch. +The Step 1 campaign baseline is tested on CPU with PyTorch 2.8.0. GPU wheels +are platform- and CUDA-specific and are not part of that tested baseline. If a +later workflow requires a GPU, install a PyTorch **2.8.0** build using the +[official PyTorch previous-versions instructions](https://pytorch.org/get-started/previous-versions/), +then rerun the import and test smoke checks. Installing an arbitrary newer +Torch release invalidates the pinned BoTorch/GPyTorch compatibility claim. **Check your CUDA version:** ```bash nvidia-smi ``` -**Install PyTorch with CUDA support:** -```bash -# For CUDA 12.1 (recommended for most systems) -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 - -# For CUDA 11.8 (more compatible with older systems) -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 - -# For CUDA 12.4 -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 - -# For CUDA 12.8 (latest) -pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128 -``` - **Verify GPU support:** ```python import torch @@ -101,51 +89,43 @@ print("CUDA available:", torch.cuda.is_available()) print("Device count:", torch.cuda.device_count()) ``` -### Option 2: Install with pip (once software license received) - -```bash -pip install mobo-kit -``` +### Distribution status -### Option 3: Google Colab - -**Option 3a: Direct Notebook Link** -- [Open MOBO-Kit notebook in Google Colab](https://colab.research.google.com/drive/1VzlCSTDw42kWxlI2xNUAOfmCZpLeKpN0?usp=sharing) - -**Option 3b: Install in your own Colab notebook** -```python -# Install in Google Colab -!pip install git+https://github.com/PV-Lab/MOBO-FOM.git - -# Import and use -import mobo_kit -``` +The source/editable installation above is the only installation path validated +for the Step 1 campaign baseline. A PyPI release and the historical Colab link +are not treated as production campaign environments until they have their own +versioned release and smoke-test workflow. ### Dependencies MOBO-Kit requires: -- Python 3.10+ -- PyTorch 1.12+ -- BoTorch 0.8+ -- GPyTorch 1.8+ +- Python 3.11 or 3.12 +- PyTorch 2.8.x +- BoTorch 0.15.1 +- GPyTorch 1.14 - NumPy, Pandas, Scikit-learn - Matplotlib, Seaborn +- openpyxl for read-only campaign-workbook auditing +- Pillow for image-artifact validation -See `requirements.txt` for the complete list of dependencies. +The tested probabilistic stack is pinned in `requirements/constraints.txt`. +See `requirements.txt` for the runtime install and `requirements/dev.txt` for +the editable test environment. ## Quick Start ### 1. Command Line Interface ```bash -# Run with default configuration -mobo-kit --csv data/processed/configCSV_example.csv +# The runner accepts the repository's metadata-style campaign CSV format. +# Run model fitting and diagnostics without proposing candidates: +mobo-kit run --csv data/processed/configCSV_example.csv # Run with custom output directory -mobo-kit --csv data/my_data.csv --out results/my_experiment +mobo-kit run --csv data/my_data.csv --out local_outputs/my_experiment # Run with verbose output -mobo-kit --csv data/my_data.csv --verbose +mobo-kit run --csv data/my_data.csv --verbose ``` ### 2. Python API @@ -175,23 +155,85 @@ from mobo_kit.main import run_mobo_experiment results = run_mobo_experiment( csv_path="data/processed/configCSV_example.csv", - save_dir="results/experiment", - verbose=True + save_dir="local_outputs/experiment", + verbose=True, + propose_candidates=False, ) ``` +`generate` writes an input-only R0 worklist. It is intentionally not accepted +directly by the legacy `run` proposal path. Campaign-specific workbook adapters +own that boundary and must validate their objective and state contracts first. + ### 4. Jupyter Notebooks See the `notebooks/` directory for interactive examples: - `MOBO_demo_annotated.ipynb` - Complete workflow demonstration +- `D2D_MOBO_TEST Global Distance Candidate generation.ipynb` - D2D Step 2B + score validation and guarded debug-adapter interface - *Note*: The LOOCV function may have trouble converging on small noisy datasets and is still in development. +### 5. D2D Step 2B algorithm debugging + +The D2D adapter requires explicit paths to an ignored private workbook and its +matching ignored private configuration. The tracked configuration is a +non-runnable public template with no workbook identity. The adapter reads the +workbook without saving it, uses the supplied Z/AA/AB final scores directly, +and writes only ignored, watermarked debug artifacts: + +```bash +python examples/d2d_step2b_debug.py \ + --workbook local_inputs/.xlsx \ + --config local_inputs/d2d_step2b_private.yaml +``` + +This command is deliberately **not** experimental approval. Its five-condition +proposal and 15-row replicate file are labelled `DEBUG ONLY - NOT APPROVED FOR +EXPERIMENT`. The legacy `run --propose-candidates` path remains blocked. + +### 6. D2D Step 2C robustness audit + +Step 2C validates the small-data GP models, measures all-observation influence, +checks nested Sobol search convergence, refines candidates on the exact discrete +grid, and summarizes persistent candidate regions. It remains read-only and +debug-only: + +```bash +# Generated sanitized workbook, small pools, and atomic artifact/ZIP CI coverage +python examples/d2d_step2c_synthetic_ci.py --overwrite + +# Quick integration smoke; never eligible to emit a consensus batch +python examples/d2d_step2c_robustness.py --mode fast + +# Declared 16,384/32,768/65,536/131,072 robustness study +python examples/d2d_step2c_robustness.py --mode full +``` + +Artifacts are written below the Git-ignored +`local_outputs/d2d_step2c_robustness/` directory. Every table and plot is +watermarked `DEBUG ONLY - NOT APPROVED FOR EXPERIMENT`; the source workbook is +hash/mtime checked before and after; no workbook writeback or real R2 proposal +is performed. A five-row consensus debug file is possible only in full mode and +only when every declared stability gate passes. Otherwise, the bundle records +the failed gates in `r1_no_stable_batch_reason.json`. + +The synthetic CI command creates no private fixture and cannot make the guarded +campaign command accept another workbook. It covers the real v3 read-only +adapter, GP/search orchestration, strict artifact validator, atomic replacement, +and an aggregate-only public summary ZIP on generated sanitized data. Private +campaign runs keep their complete evidence in a separately labelled +`*_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip` archive below the ignored output root. + ## Configuration MOBO-Kit uses YAML configuration files. See `configs/` directory for examples: - `demo_config.yaml` - Basic configuration - `configCSV_example_config.yaml` - Configuration from CSV metadata +- `d2d_step2b_debug.yaml` - Non-runnable public template; campaign runs require + a matching ignored private config and workbook +- `d2d_step2c_debug.yaml` - Public synthetic-CI template; private robustness + runs require explicit ignored campaign inputs ### Configuration Structure @@ -213,6 +255,11 @@ constraints: - clausius_clapeyron: true ah_col: "absolute_humidity" temp_c_col: "temperature_c" +``` + +Constraints are opt-in. A generic campaign configuration should use +`constraints: []`; the example above is only for a design that actually +contains the two named humidity/temperature inputs. ## Package Structure @@ -240,16 +287,16 @@ src/mobo_kit/ pip install -r requirements.txt ``` -2. **CUDA/GPU support**: Install PyTorch with CUDA (example, please use matching nvidia-smi): - ```bash - pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 - ``` +2. **CUDA/GPU support**: Keep Torch at 2.8.0 and follow the official + platform-specific installation instructions linked above. GPU behavior was + not validated in the Step 1 baseline. -3. **Python version compatibility**: Use Python 3.10 or 3.11: +3. **Python version compatibility**: Python 3.11-3.12 is supported; the Step 1 + CPU baseline was tested with Python 3.12.10: ```bash - conda create -n mobo-kit python=3.10 + conda create -n mobo-kit python=3.11 conda activate mobo-kit - pip install -e . + python -m pip install -r requirements/dev.txt ``` 4. **Jupyter notebook support**: @@ -266,8 +313,8 @@ src/mobo_kit/ ## Next Steps -1. **Try the demo**: `mobo-kit --csv data/processed/configCSV_example.csv --verbose` -2. **Generate initial experiments**: `mobo-kit generate --config configs/demo_config.yaml --n-samples 20 --out my_experiments.csv` +1. **Try the demo**: `mobo-kit run --csv data/processed/configCSV_example.csv --verbose` +2. **Generate initial experiments**: `mobo-kit generate --config configs/demo_config.yaml --n-samples 20 --out local_outputs/my_experiments.csv` 3. **Explore Jupyter notebooks** in the `notebooks/` directory 4. **Check configuration examples** in the `configs/` directory diff --git a/configs/FA0.9CS0.1PbI3_260407_Config.yaml b/configs/FA0.9CS0.1PbI3_260407_Config.yaml new file mode 100644 index 0000000..86d3018 --- /dev/null +++ b/configs/FA0.9CS0.1PbI3_260407_Config.yaml @@ -0,0 +1,68 @@ +# Canonical D2D input grid for Step 1 infrastructure tests. +# +# Objective formulas, control policy, process constraints, and campaign reference +# point are intentionally unresolved. This file must not be used to generate R1/R2 +# candidates until those scientific decisions are approved and recorded. +campaign: + name: D2D_FA0.9Cs0.1PbI3 + status: baseline_only + +inputs: + - name: speed_1 + unit: rpm + start: 1000 + stop: 6000 + step: 500 + - name: time_1 + unit: s + start: 5 + stop: 50 + step: 5 + - name: speed_2 + unit: rpm + start: 0 + stop: 5000 + step: 500 + - name: time_2 + unit: s + start: 10 + stop: 60 + step: 5 + - name: precur_conc + unit: M + start: 1.00 + stop: 2.00 + step: 0.05 + - name: precur_vol + unit: uL + start: 40 + stop: 200 + step: 10 + - name: anneal_temp + unit: C + start: 100 + stop: 185 + step: 5 + - name: anneal_time + unit: min + start: 10 + stop: 60 + step: 5 + - name: anti_vol + unit: uL + start: 100 + stop: 200 + step: 5 + - name: anti_time + unit: s + start: 9 + stop: 25 + step: 2 + +objectives: + # TBD - experimental-team decision. Empty by design in Step 1. + names: [] + +# No D2D process constraint has been approved. In particular, the legacy +# humidity/temperature Clausius-Clapeyron constraint does not apply here. +constraints: [] diff --git a/configs/d2d_step2a_provisional.yaml b/configs/d2d_step2a_provisional.yaml new file mode 100644 index 0000000..b26447f --- /dev/null +++ b/configs/d2d_step2a_provisional.yaml @@ -0,0 +1,96 @@ +# Step 2A configuration skeleton only. +# This file MUST NOT authorize a real candidate-generation run. + +schema_version: d2d-step2a-provisional-1 +campaign_id: PENDING +approved_for_production: false +approval: + approved_by: PENDING + approved_at: PENDING + decision_record: docs/D2D_MEETING_DECISIONS.md + +workbook: + profile: d2d_summary_v2 + sheet: Sheet1 + expected_content_range: A1:AC18 + expected_sha256: REQUIRED_IN_IGNORED_PRIVATE_CONFIG + input_aliases: + "precur_vol (uL)": precur_vol + +inputs: + - {name: speed_1, unit: rpm, start: 1000, stop: 6000, step: 500} + - {name: time_1, unit: s, start: 5, stop: 50, step: 5} + - {name: speed_2, unit: rpm, start: 0, stop: 5000, step: 500} + - {name: time_2, unit: s, start: 10, stop: 60, step: 5} + - {name: precur_conc, unit: M, start: 1.0, stop: 2.0, step: 0.05} + - {name: precur_vol, unit: uL, start: 40, stop: 200, step: 10} + - {name: anneal_temp, unit: C, start: 100, stop: 185, step: 5} + - {name: anneal_time, unit: min, start: 10, stop: 60, step: 5} + - {name: anti_vol, unit: uL, start: 100, stop: 200, step: 5} + - {name: anti_time, unit: s, start: 9, stop: 25, step: 2} + +objective_mapping_status: pending_meeting +objectives: + - name: uniformity_score + goal: maximize + model_source_column: PENDING_T_OR_OTHER + utility_transform: PENDING + scaling: PENDING + components: [Coverage, "1 - Uniformity", "Phase purity"] + + - name: optoelectronic_score + goal: maximize + model_source_column: PENDING_U_OR_OTHER + utility_transform: PENDING + scaling: PENDING + components: + - "PL - Implied Voc (Max) Normalized" + - "Photoconductance (Max) Normalized" + + - name: thickness + goal: target + model_source_column: PENDING_V_OR_W + target: 650.0 + target_transform: PENDING_GAUSSIAN_OR_OTHER + sigma: PENDING + scaling: PENDING + +reference_point_utility: PENDING + +qc_policy: + complete_case_rule: PENDING + failed_measurement_rule: PENDING + outlier_rule: PENDING + control_rule: PENDING + replicate_rule: PENDING + +constraints_status: pending_meeting +constraints: PENDING + +r1: + method: ucb_hvi + batch_size: 5 + beta: PENDING + posterior_samples: PENDING + candidate_pool_size: PENDING + +r2: + method: qlognehvi + batch_size: 3 + mc_samples: PENDING + candidate_pool_size: PENDING + sequential_pending: true + +local_penalization: + distance_metric: normalized_euclidean + radius: PENDING + min_batch_distance: PENDING + min_observed_distance: PENDING + dimension_weights: null + allow_hard_distance_relaxation: false + +reproducibility: + seed: PENDING + record_git_commit: true + record_environment_versions: true + record_resolved_config_hash: true diff --git a/configs/d2d_step2b_debug.yaml b/configs/d2d_step2b_debug.yaml new file mode 100644 index 0000000..4122782 --- /dev/null +++ b/configs/d2d_step2b_debug.yaml @@ -0,0 +1,133 @@ +# Public Step 2B algorithm-debugging template. This file never authorizes films. +# It intentionally has no private workbook identity and is not directly runnable. +schema_version: d2d-step2b-debug-1 +campaign_id: public-d2d-template-debug +template_only: true +run_mode: debug +debug_run_authorized: true +approved_for_production: false +approved_for_experiment: false + +decision_record: + source: public-template + status: runtime_private_config_required + known_data_issue: supplied_scores_must_be_validated_at_runtime + +workbook: + profile: d2d_summary_v3_scores + sheet: Sheet1 + expected_content_range: A1:AI20 + expected_sha256: REQUIRED_IN_IGNORED_PRIVATE_CONFIG + input_aliases: + "precur_vol (uL)": precur_vol + sample_id_column: Sample number + expected_sample_ids: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015] + +inputs: + - {name: speed_1, unit: rpm, start: 1000, stop: 6000, step: 500} + - {name: time_1, unit: s, start: 5, stop: 50, step: 5} + - {name: speed_2, unit: rpm, start: 0, stop: 5000, step: 500} + - {name: time_2, unit: s, start: 10, stop: 60, step: 5} + - {name: precur_conc, unit: M, start: 1.0, stop: 2.0, step: 0.05} + - {name: precur_vol, unit: uL, start: 40, stop: 200, step: 10} + - {name: anneal_temp, unit: C, start: 100, stop: 185, step: 5} + - {name: anneal_time, unit: min, start: 10, stop: 60, step: 5} + - {name: anti_vol, unit: uL, start: 100, stop: 200, step: 5} + - {name: anti_time, unit: s, start: 9, stop: 25, step: 2} + +objectives: + - name: uniformity_score + source_column: Uniformity score + excel_column: Z + direction: maximize + utility_transform: identity + support_formula: Coverage * (1 - Uniformity) * Phase purity + mismatch_policy: warn_use_supplied + - name: optoelectronic_score + source_column: Optoelectronic score + excel_column: AA + direction: maximize + utility_transform: identity + support_formula: log10((PL - Implied Voc (Max)) * Photoconductance (Max)) + mismatch_policy: error + - name: thickness_score + source_column: Thickness score + excel_column: AB + direction: maximize + utility_transform: identity + support_formula: exp(-((mean(valid T1:T4) - 650.0) / 250.0)^2) + target_nm: 650.0 + scale_nm: 250.0 + exponent_factor: 1.0 + exclude_columns: [T anom] + mismatch_policy: error + +ignored_model_columns: + - Stability score? + - Total combination - addition + - Total combination - multiplied + - Uniformity score absolute difference + - Optoelectronic score absolute difference + - Thickness absolute difference + - Total score absolute difference + +reference_point_utility: [-0.01, -10.0, -0.01] +constraints: [] + +r0: + condition_count: 15 + control_sample_ids: [1001] + include_control_in_debug_model: true + control_measurement_provenance_assumption: measured_in_synthetic_fixture + off_grid_observed_exceptions: + - sample_id: 1001 + input_name: speed_1 + observed_value_strategy: midpoint_between_first_two_grid_values + reason: algorithmic synthetic off-grid sentinel for public tests + retain_observed_value: true + +qc_policy: + final_scores_required: true + complete_case_rule: require_all_three_final_scores + failed_measurement_rule: block_row + outlier_rule: report_do_not_auto_remove + uniformity_known_mismatch: warn_and_continue_debug + optoelectronic_mismatch: fail + thickness_mismatch: fail + +r1: + method: ucb_hvi + batch_size_unique_conditions: 5 + replicates_per_condition: 3 + beta: 4.0 + posterior_samples: 256 + candidate_pool_size: 10000 + score_chunk_size: 512 + +r2_test_only: + method: qlognehvi + batch_size_unique_conditions: 3 + replicates_per_condition: 3 + mc_samples: 128 + candidate_pool_size: 5000 + sequential_pending: true + +local_penalization: + distance_metric: normalized_euclidean + radius: 0.25 + min_batch_distance: 0.15 + min_observed_distance: 0.0 + dimension_weights: null + allow_hard_distance_relaxation: false + +reproducibility: + seed: 73 + record_git_commit: true + record_environment_versions: true + record_resolved_config_hash: true + record_workbook_hash: true + +outputs: + root: local_outputs/d2d_step2b_debug + debug_watermark: DEBUG ONLY - NOT APPROVED FOR EXPERIMENT + write_source_workbook: false diff --git a/configs/d2d_step2c_debug.yaml b/configs/d2d_step2c_debug.yaml new file mode 100644 index 0000000..cb0a28c --- /dev/null +++ b/configs/d2d_step2c_debug.yaml @@ -0,0 +1,129 @@ +# Public-safe Step 2C robustness template for deterministic synthetic debugging. +schema_version: d2d-step2c-robustness-debug-1 +campaign_id: d2d-step2c-public-debug +run_mode: debug +approved_for_experiment: false +approved_for_production: false +debug_watermark: "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" +base_step2b_config: configs/d2d_step2b_debug.yaml + +workbook: + source_kind: sanitized_synthetic_ci + +objectives: + order: [uniformity_score, optoelectronic_score, thickness_score] + source_columns: [Uniformity score, Optoelectronic score, Thickness score] + reference_point: [-0.01, -10.0, -0.01] + transforms: [identity, identity, identity] + directions: [maximize, maximize, maximize] + bounds: + uniformity_score: {lower: 0.0, upper: 1.0} + optoelectronic_score: {lower: null, upper: null} + thickness_score: {lower: 0.0, upper: 1.0} + known_uniformity_mismatch: + allowed_in_debug: true + blocks_experimental_approval: true + +r0: + inherit_from_base: true + +constraints: [] + +ucb_hvi: + moment_method: analytic_identity + beta_values: [1.0, 4.0, 9.0] + primary_beta: 4.0 + positive_hvi_threshold: 0.0 + numeric_tolerance: 1.0e-12 + score_chunk_size: 2048 + bound_policies: [none, clip_ucb] + primary_bound_policy: clip_ucb + mc_comparison_samples: 2048 + mc_comparison_seed: 73 + +candidate_search: + method: nested_sobol_grid_indices + scramble: true + primary_seed: 73 + secondary_seeds: [137, 911] + nested_unique_sizes: [16384, 32768, 65536, 131072] + primary_full_size: 131072 + preserve_accepted_prefix_nesting: true + +local_refinement: + enabled: true + anchors_per_selection_step: 64 + max_sweeps: 10 + improvement_tolerance: 1.0e-10 + coordinate_values: all_allowed_grid_values + stable_tie_break: lower_grid_index + +local_penalty_study: + hard_distance_relaxation: false + variants: + - {label: no_soft_no_hard, radius: null, min_batch_distance: 0.0} + - {label: no_soft_hard_0_15, radius: null, min_batch_distance: 0.15} + - {label: radius_0_15, radius: 0.15, min_batch_distance: 0.15} + - {label: radius_0_25, radius: 0.25, min_batch_distance: 0.15} + - {label: radius_0_35, radius: 0.35, min_batch_distance: 0.15} + primary_variant: radius_0_25 + min_observed_distance: 0.0 + dimension_weights: null + +models: + variants: + - {name: default_current, type: existing_default} + - name: conservative + type: explicit_conservative + min_noise: 0.01 + min_lengthscale: 0.05 + primary_for_debug: default_current + exact_leave_one_out: true + report_training_posterior_only_as_diagnostic: true + +observation_influence: + enabled: true + omitted_sample_ids: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015] + common_pool_size: 32768 + common_pool_seed: 73 + top_k: 100 + regional_thresholds: [0.10, 0.15, 0.20] + +robust_regions: + clustering: agglomerative_complete_link + primary_distance_threshold: 0.15 + sensitivity_thresholds: [0.10, 0.20] + shortlist_min: 8 + shortlist_max: 12 + consensus_batch_size: 5 + consensus_required_criteria: + largest_two_nested_regional_matches_within_0_15: 4 + largest_two_nested_mean_matched_distance_max: 0.10 + minimum_region_family_coverage: 3 + require_grid_valid: true + require_hard_distance_valid: true + +execution_modes: + fast: + nested_unique_sizes: [512, 1024, 2048, 4096] + anchors_per_selection_step: 8 + omitted_sample_ids: [1001, 1002, 1003] + mc_comparison_samples: 256 + full: + nested_unique_sizes: [16384, 32768, 65536, 131072] + anchors_per_selection_step: 64 + omitted_sample_ids: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015] + mc_comparison_samples: 2048 + +reproducibility: + model_seed: 73 + record_git_commit: true + record_dirty_state: true + record_environment_versions: true + record_config_hash: true + record_workbook_hash_and_mtime_before_after: true + +outputs: + root: local_outputs/d2d_step2c_robustness + tracked_private_recipes: false + create_portable_ignored_zip: true diff --git a/data/.DS_Store b/data/.DS_Store deleted file mode 100644 index e733d791cb04b76f8807d888b722eb7c208b8984..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKK~BR!4D_azNCgr-a*X&vZyZAu_&`1YZ7PbCkcfh~aLAAN0Y|>U6F6~aymm`M z0^)!WLU!eyjn`f~vuQR)L;QTx(r}p6|7N?b)r5|0gs;b?0=~t%(OVBkej) zkM0YtL*v@K^hdk-{H)d(1IBHrpe*dy2yR2r i@mn!`c`H7EdVxLS4lq`11Yv>Lhk&QS8e`yB8F&LY{A|Sl diff --git a/docs/D2D_CAMPAIGN_SPEC.md b/docs/D2D_CAMPAIGN_SPEC.md new file mode 100644 index 0000000..639c60c --- /dev/null +++ b/docs/D2D_CAMPAIGN_SPEC.md @@ -0,0 +1,199 @@ +# D2D MOBO Campaign Data Contract (Step 1 Baseline) + +## Status and scope + +This document defines the machine-readable baseline for the D2D campaign. It +does **not** approve objective formulas, a thickness utility, a hypervolume +reference point, process constraints, UCB, local penalization, or production +candidate generation. Every field marked **TBD - experimental-team decision** +must be resolved before a real R1 run. + +The optimization engine remains a tested Python API/CLI. A future Excel button +will be a thin adapter that validates and passes campaign data to that engine; +scientific logic must not be implemented in VBA or worksheet formulas alone. + +## Canonical input grid + +Input columns are ordered exactly as shown. Units belong in schema metadata, +not in numeric cells. + +| Position | Name | Unit | Start | Stop | Step | Grid values | +|---:|---|---|---:|---:|---:|---:| +| 1 | `speed_1` | rpm | 1000 | 6000 | 500 | 11 | +| 2 | `time_1` | s | 5 | 50 | 5 | 10 | +| 3 | `speed_2` | rpm | 0 | 5000 | 500 | 11 | +| 4 | `time_2` | s | 10 | 60 | 5 | 11 | +| 5 | `precur_conc` | M | 1.00 | 2.00 | 0.05 | 21 | +| 6 | `precur_vol` | uL | 40 | 200 | 10 | 17 | +| 7 | `anneal_temp` | C | 100 | 185 | 5 | 18 | +| 8 | `anneal_time` | min | 10 | 60 | 5 | 11 | +| 9 | `anti_vol` | uL | 100 | 200 | 5 | 21 | +| 10 | `anti_time` | s | 9 | 25 | 2 | 9 | + +The full Cartesian product contains 177,816,994,740 recipes and must never be +materialized. Candidate algorithms must use sampling, discrete pools, or +discrete local search. + +For input `j`, a value `x` is on-grid only when all three conditions hold: + +1. `start_j <= x <= stop_j`; +2. `(x - start_j) / step_j` is an integer within the configured numeric + tolerance; +3. the serialized value round-trips without changing its grid index. + +Duplicate input names, non-finite bounds, non-positive steps, reversed bounds, +and endpoints that are not reachable by an integral number of steps are schema +errors. + +## Canonical record roles + +A future campaign table should contain one row per physical recipe execution. +The following identifiers are proposed now so later round trips are unambiguous: + +| Field | Role | Step 1 rule | +|---|---|---| +| `campaign_id` | Stable campaign identifier | Required later; value TBD | +| `sample_id` | Unique physical sample identifier | Required and never reused | +| `round` | `R0`, `R1`, or `R2` | Required later | +| `row_role` | `candidate`, `control`, or `replicate` | Required later; policy TBD | +| `candidate_status` | `proposed`, `run`, `measured`, `excluded`, `failed` | Required later | +| `replicate_group` | Links repeated recipes | Nullable; aggregation policy TBD | +| `include_in_model` | Explicit QC gate | Required before GP fitting; policy TBD | +| `exclusion_reason` | Human-readable audit reason | Required when excluded | + +Columns then appear in these logical groups: + +1. identifiers and workflow state; +2. the ten canonical optimizer inputs in the order above; +3. measured process covariates (for example, a measured rather than setpoint + temperature); +4. raw characterization measurements; +5. transparent derived scores; +6. three approved BO utilities in transformed/model space; +7. provenance and QC fields. + +Raw measurements must never be overwritten by cleaning or derived scores. +Blank measurements mean missing/unmeasured, not zero. Parsing preserves missing +values; the model boundary rejects partial or all-blank objective rows rather +than silently filling or dropping them. + +## Workbook findings and mapping guardrails + +The supplied private workbook was audited read-only. Header positions below are +1-based Excel column positions. + +- Sheet: `Sheet1` +- Non-empty data range: `A1:AC16` +- Columns: 29 +- Sample rows: 15 +- Canonical inputs: columns B:K +- `anneal_temp`: column H +- related but distinct `Anneal Temp`: column L +- duplicate `Uniformity score`: columns Q (17) and T (20) +- blank header: column Y (25), immediately after + `Total combination - addition` +- characterization and score cells are blank in the supplied copy + +The two anneal columns must not be merged automatically. Their meanings +(setpoint, measured temperature, second anneal, or legacy field) are **TBD - +experimental-team decision**. The two `Uniformity score` columns must remain +position-addressable until both meanings are approved. A reader must inspect raw +header cells before any library can rename duplicates. + +## Measurement, score, and objective roles + +The workbook currently exposes raw or semi-processed fields such as `Coverage`, +`Uniformity`, `Phase purity`, `PL - Implied Voc (Max)`, `Photoconductance (Max)`, +and `Thickness (avg)`, plus several derived scores. None is automatically a BO +objective. + +- Objective 1 source/formula/direction: **TBD - experimental-team decision** +- Objective 2 source/formula/direction: **TBD - experimental-team decision** +- Objective 3 source/formula/direction: **TBD - experimental-team decision** +- Thickness target, tolerance, and transform: **TBD - experimental-team + decision** +- Fixed scaling limits and clipping policy: **TBD - experimental-team decision** +- Fixed campaign reference point in original and transformed units: **TBD - + experimental-team decision** + +All three approved utilities and the reference point must be transformed by the +same versioned contract. Round-by-round min/max scaling must not be introduced +without explicit approval because it would make hypervolume incomparable. + +## Campaign state machine + +```text +CONFIGURED + -> R0_PROPOSED (deterministic, grid-valid LHS; count TBD) + -> R0_RUNNING + -> R0_MEASURED + -> R0_VALIDATED (QC/objective mapping/reference frozen) + -> R1_PROPOSED (5 candidates; approved multi-objective UCB TBD) + -> R1_RUNNING + -> R1_MEASURED + -> R1_VALIDATED + -> R2_PROPOSED (3 candidates; approved qNEHVI + penalization policy TBD) + -> R2_RUNNING + -> R2_MEASURED + -> COMPLETE +``` + +Transitions fail closed. Missing required measurements, duplicate sample IDs, +unapproved configuration fields, hard-constraint violations, an undersized +candidate batch, or a code/config hash mismatch prevent transition to the next +proposal state. + +## Proposed future workbook sheets + +This is an interface proposal, not a Step 1 workbook edit. + +- `Campaign`: locked campaign identity, code commit, schema version, approved + objective contract, fixed reference point, and round settings. +- `Experiments`: flat editable rows containing identifiers, inputs, raw + measurements, QC, and derived utilities. +- `Next Candidates`: generated worklist with candidate IDs, physical conditions, + acquisition diagnostics, and validation state. +- `Diagnostics`: Pareto/hypervolume history, model checks, pairwise candidate + distances, and acquisition plots. +- `Decision Log`: approvals, changes, warnings, operator, timestamps, input-file + hash, resolved configuration hash, seed, environment versions, and Git commit. + +## Validation and reproducibility requirements + +- Canonical names and order are exact; unknown input columns are errors unless + explicitly registered as measured covariates. +- Every proposed input is finite, within bounds, and exactly on-grid. +- A proposed batch has the requested number of unique recipes or fails clearly. +- Constraints are opt-in, evaluated in physical units after snapping, and never + relaxed silently. +- Controls and replicates are never inferred from identical recipes; their row + roles are explicit. +- Model-boundary inputs/objectives are numeric and complete according to the + approved QC policy. +- Every production run records: campaign/schema version, resolved config, source + workbook hash, row selection/QC decisions, random seed, dependency versions, + Git commit, acquisition name/parameters, reference point, candidates, and + diagnostics. + +## Unresolved decisions blocking real R1 + +1. R0 experimental count, control count, and whether/how controls train the GP. +2. The three authoritative objective formulas and source columns. +3. Uniformity definition, direction, coverage/phase-purity roles, and duplicate + score meanings. +4. Thickness target, tolerance, and utility transform. +5. Optoelectronic score formula and failure/zero semantics. +6. Fixed objective scales and out-of-range handling. +7. Fixed campaign hypervolume reference point. +8. Meanings and roles of `anneal_temp` and `Anneal Temp`. +9. Explicit process/equipment/safety constraints in physical units. +10. Literature-backed multi-objective UCB definition and beta policy. +11. Local-penalization metric, radius/weight, observed-point treatment, and hard + minimum-distance policy. +12. Missing-data, failed-film, QC, outlier, and replicate aggregation policy. +13. Excel deployment environment, macro policy, Python installation, and code + signing/trusted-location requirements. + +Until these decisions are approved, the D2D YAML contains no objective names and +no constraints, and the public runner requires explicit opt-in plus an explicit +reference point before invoking its existing qNEHVI proposal path. diff --git a/docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md b/docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md new file mode 100644 index 0000000..4537dbf --- /dev/null +++ b/docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md @@ -0,0 +1,154 @@ +# D2D MOBO Meeting Decision Record + +**Meeting date:** +**Participants:** +**Recorded by:** +**Campaign/composition:** +**Decision-record version:** + +Complete this document after the experimental-team meeting. Replace every +`PENDING` entry and record who approved it. This document should become the +source for the frozen Step 2B production configuration. + +## 1. R0 dataset and row roles + +- Number of R0 recipe executions: `PENDING` +- Are the 15 workbook rows the complete R0 set? `PENDING` +- Control recipe(s) and sample ID(s): `PENDING` +- Do controls train the GP? `PENDING` +- Replicate rows/groups: `PENDING` +- Replicate aggregation/noise policy: `PENDING` +- Required status/QC fields before model fitting: `PENDING` +- Missing-one-objective rule: `PENDING` +- Failed-film rule: `PENDING` +- Outlier/exclusion approval owner: `PENDING` + +## 2. Final three model outputs and BO utilities + +### Objective 1 — Uniformity + +- Model source column: `PENDING` +- Utility/source column: `PENDING` +- Direction: `maximize` +- Is T supplied externally or calculated by Python? `PENDING` +- Exact equation using L/N/O: `PENDING` +- Component weights: `PENDING` +- Component normalization/clipping: `PENDING` +- Missing/failure semantics: `PENDING` +- Approved by: `PENDING` + +### Objective 2 — Optoelectronic + +- Model source column: `PENDING` +- Utility/source column: `PENDING` +- Direction: `maximize` +- Is U supplied externally or calculated by Python? `PENDING` +- Raw PL normalization anchor/formula: `PENDING` +- Raw photoconductance normalization anchor/formula: `PENDING` +- Exact Q/S combination and weights: `PENDING` +- Linear/log treatment: `PENDING` +- Missing/zero/failure semantics: `PENDING` +- Approved by: `PENDING` + +### Objective 3 — Thickness + +- Model source: `V raw thickness` / `W precomputed score` / other: `PENDING` +- Target: `650 nm` — confirm: `PENDING` +- Utility transform: `PENDING` +- Sigma/tolerance/scale: `PENDING` +- Symmetric about target? `PENDING` +- Clipping/bounds: `PENDING` +- Is W calculated by Python or supplied externally? `PENDING` +- Approved by: `PENDING` + +### Final objective-column decision + +- Final mapping: `T,U,W` / `T,U,V` / other: `PENDING` +- Rationale: `PENDING` + +## 3. Fixed utility scales and reference point + +- Are all final BO utilities on `[0,1]`? `PENDING` +- Fixed scale/anchor for Uniformity: `PENDING` +- Fixed scale/anchor for Optoelectronic: `PENDING` +- Fixed scale/anchor for Thickness utility: `PENDING` +- Out-of-range/clipping policy: `PENDING` +- Fixed utility-space hypervolume reference point: `PENDING` +- Reference-point rationale: `PENDING` +- Must remain fixed across R0/R1/R2? `PENDING` +- Approved by: `PENDING` + +## 4. Process and equipment constraints + +List every rule in physical units. Use `NONE — constraints: [] approved` only if +no rules apply. + +| ID | Rule | Variables | Hard/soft | Rationale | Approved by | +|---|---|---|---|---|---| +| C1 | PENDING | | | | | + +Specific checks: + +- Rule when `speed_2 = 0`: `PENDING` +- Antisolvent-time relation to spin time: `PENDING` +- Allowed antisolvent volume/time combinations: `PENDING` +- Anneal temperature/time restrictions: `PENDING` +- Concentration/volume restrictions: `PENDING` +- Equipment-resolution restrictions beyond configured steps: `PENDING` +- Resolution of the local off-grid input discrepancy versus the approved input + contract: `PENDING` + +## 5. R1 UCB-HVI settings + +- Method approved: `ucb_hvi` — `PENDING` +- Batch size: `5` — `PENDING` +- Beta: `PENDING` +- Equivalent kappa `sqrt(beta)`: `PENDING` +- Latent posterior or observation-noise posterior: `PENDING` +- Posterior MC samples for utility moments: `PENDING` +- Candidate pool size: `PENDING` +- Seed policy: `PENDING` +- Approval/delegation owner: `PENDING` + +## 6. R2 qLogNEHVI settings + +- Method approved: `qlognehvi` — `PENDING` +- Batch size: `3` — `PENDING` +- MC samples: `PENDING` +- Candidate pool size: `PENDING` +- Sequential pending-point selection approved? `PENDING` +- Native joint-q comparator required? `PENDING` +- Seed policy: `PENDING` +- Approval/delegation owner: `PENDING` + +## 7. Local penalization and diversity + +- Distance metric: normalized Euclidean / weighted / other: `PENDING` +- Soft penalty formula approved: `PENDING` +- Penalty radius: `PENDING` +- Minimum within-batch distance: `PENDING` +- Minimum distance from observed/pending points: `PENDING` +- May hard distances ever be relaxed? Recommended `No`: `PENDING` +- Required numerical diversity report: `PENDING` +- Required plots: `PENDING` +- Approval/delegation owner: `PENDING` + +## 8. Workbook and score ownership + +- Who enters raw characterization values? `PENDING` +- Who approves/enters final T/U/W scores? `PENDING` +- Should Python calculate any scores? `PENDING` +- Source-of-truth workbook location: `PENDING` +- One workbook for full campaign or one per round? `PENDING` +- Sample-ID convention: `PENDING` +- Backup/audit requirement: `PENDING` + +## 9. Production approval + +- Configuration version approved for dry-run: `PENDING` +- Configuration version approved for real R1: `PENDING` +- Scientific approver(s): `PENDING` +- Computational approver(s): `PENDING` +- Experimental operator approver(s): `PENDING` +- Approval date/time: `PENDING` +- Notes/conditions: `PENDING` diff --git a/docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md b/docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md new file mode 100644 index 0000000..c83795b --- /dev/null +++ b/docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md @@ -0,0 +1,97 @@ +# D2D Objective Contract — Provisional, Not Approved + +## Status + +This document records current information without turning it into a production +decision. `configs/d2d_step2a_provisional.yaml` deliberately contains +placeholders and `approved_for_production: false`; the production validator must +reject it. + +## Updated workbook profile + +The updated private workbook was audited read-only from an ignored, explicitly +supplied local path. Its filename and content identity are intentionally absent +from tracked documentation. + +- Sheet and content range: `Sheet1`, `A1:AC18` +- 29 columns; 15 recipe rows in Excel rows 2–16 +- Canonical optimizer inputs: B:K +- Explicit display alias: `precur_vol (uL)` to `precur_vol` +- No second annealing-temperature column +- Scores: T `Uniformity score`, U `Optoelectronic score` +- Thickness: V raw average, W normalized target-score candidate +- Y has a blank header and must remain visible in the audit +- No formulas or populated objective values + +The local workbook contains an off-grid input discrepancy relative to the +provisional design contract. The v2 auditor exposes the discrepancy and does +not snap or reinterpret private recipe values. The meeting must decide whether +the source is a transcription issue, an intentional off-grid execution, or +evidence that the approved input grid needs revision. + +The supplied instruction text states that the row-18 notes are in P18/R18. +Direct cell inspection of the supplied workbook instead finds the note text in +**Q18/S18**, under the normalized PL and normalized photoconductance columns; +P18/R18 are blank. The contract fixture follows the pack's P18/R18 locations, +while a separate sanitized anomaly fixture and the local audit report the +Q18/S18 deviation without changing the workbook. + +## Current scientific intent + +The current, still provisional goals are: + +1. maximize uniformity performance; +2. maximize optoelectronic performance; and +3. match thickness to 650 nm. + +Aleks identified L (`Coverage`), N (`1 - Uniformity`), and O (`Phase purity`) as +uniformity components, and Q/S as normalized optoelectronic components. This is +not enough to calculate T or U: equations, weights, anchors, clipping, and +failure semantics remain unresolved. + +## Unresolved objective-source choice + +Step 2A supports both possible architectures but approves neither: + +| Candidate mapping | Model outputs | Utility treatment | +|---|---|---| +| `T,U,W` | three precomputed scores | identity maximize only after the three columns and formulas are approved | +| `T,U,V` | two precomputed scores plus raw thickness | transform every raw thickness posterior sample through the approved target utility | + +The generic engine supports a Gaussian target utility and a negative-absolute +comparison utility. The workbook's `sigma = 250` header is not treated as final +approval for either the model-source choice or the production transform. + +## Objective specification requirements + +Each approved objective must ultimately provide: + +- a unique objective name and exact workbook/model source column; +- goal (`maximize`, `minimize`, or `target`); +- a versioned transform and fixed parameters; +- fixed affine anchors or a declaration that the source is an already-approved + utility; +- clipping behavior; +- formula ownership and missing/failure behavior. + +The transform outputs all-maximize utilities. Pareto analysis, UCB-HVI, +qLogNEHVI, and the reference point must all use that same transformed space. + +## Decisions required before production + +- final `T,U,W`, `T,U,V`, or other mapping; +- exact T and U formulas, or confirmation that externally approved values are + entered directly; +- final thickness model source, target transform, sigma/scale, and symmetry; +- fixed utility scales, clipping, and fixed utility-space reference point; +- R0 row inclusion, control, replicate, QC, missing-data, failure, and outlier + policies; +- complete physical/equipment constraints, including an explicitly approved + empty list if none apply; +- R1 beta, posterior policy/sample count, pool size, and seed policy; +- R2 sample count, pool size, comparator policy, and seed policy; +- local-penalty radius and hard distance settings; +- approval provenance and a frozen resolved-config hash. + +Until these are encoded and approved, only schema audits and explicitly +synthetic low-level examples are permitted. diff --git a/docs/D2D_STEP2A_COMPUTATIONAL_CORE.md b/docs/D2D_STEP2A_COMPUTATIONAL_CORE.md new file mode 100644 index 0000000..582293a --- /dev/null +++ b/docs/D2D_STEP2A_COMPUTATIONAL_CORE.md @@ -0,0 +1,204 @@ +# D2D Step 2A Computational Core + +## Scope and safety boundary + +Step 2A supplies a generic, synthetic-tested engine for discrete R1 UCB-HVI +and R2 qLogNEHVI batch construction. It does **not** authorize a D2D campaign +proposal. The workbook objective mapping, utility formulas, fixed scales, +reference point, QC/control policy, physical constraints, and acquisition +settings remain subject to approval. + +The production gate is separate from the mathematical functions. Low-level +functions may run with visibly test-only synthetic settings; a workbook- or +campaign-facing run must pass `validate_production_config` first. During Step +2A, `run_mobo_experiment(..., propose_candidates=True)` is blocked before CSV +parsing, model fitting, or output creation. Even an approved configuration is +rejected there because the old runner does not use this objective contract, +discrete pool, or shared selector; wiring those pieces is a Step 2B task. + +## Coordinate and outcome spaces + +| Space | Representation | Used for | +|---|---|---| +| Physical input | Configured laboratory units on exact grids | worklists and physical constraints | +| Normalized input | Each input mapped to `[0, 1]` | GP inputs, candidate distances, local penalties | +| Raw/model outcome | Measurements fitted by the GP models | posterior sampling | +| Transformed utility | Every objective oriented so larger is better | Pareto filtering, hypervolume, UCB-HVI, qLogNEHVI | + +Raw outcomes and transformed utilities are never interchanged. In particular, +the fixed reference point is always expressed in transformed utility space. + +## Objective transformations + +`mobo_kit.objectives` defines an immutable `ObjectiveSpec`, a validated +`ObjectiveTransform`, and a BoTorch-compatible +`ConfiguredMCMultiOutputObjective`. Tensors use the shape contract +`[..., M] -> [..., M]`, preserving floating dtype and device. + +For fixed anchors `lo < hi`: + +```text +maximize: u(y) = (y - lo) / (hi - lo) +minimize: u(y) = (hi - y) / (hi - lo) +``` + +The target transformations are: + +```text +Gaussian: u(y) = exp(-0.5 * ((y - target) / sigma)^2) +Negative absolute u(y) = -abs(y - target) / scale +``` + +`sigma` or `scale` must be explicit, finite, and positive. Identity is valid +only for an already approved maximize utility. No anchor or reference point is +estimated from campaign observations. + +Nonlinear transformations are applied to every raw posterior Monte Carlo +sample. For candidate `x`: + +```text +Y_raw^(s)(x) ~ posterior(model, x) +U^(s)(x) = transform(Y_raw^(s)(x)) +mu_U(x) = mean_s U^(s)(x) +sigma_U(x) = population_std_s U^(s)(x) +``` + +The default is the latent posterior (`observation_noise=False`). Posterior +sampling uses a caller-supplied seed and Sobol QMC samples. + +## Discrete candidate pool + +`sample_discrete_candidate_pool` samples integer index tuples directly from the +configured axes. It never materializes the 177,816,994,740-point D2D Cartesian +product. Each accepted index tuple is converted exactly to a physical grid row +and then normalized. + +Observed, pending, and explicit avoid rows are converted back to integer grid +indices and excluded. Opt-in physical row constraints are evaluated only after +conversion to laboratory units. A request either returns the exact requested +pool in deterministic order for its seed or raises +`CandidatePoolSamplingError` with draw and rejection statistics. Constraints or +duplicate rules are never relaxed. + +## R1 UCB-HVI + +`posterior_utility_moments` evaluates candidates as singleton q-batches in +CPU-safe chunks. `score_ucb_hvi_pool` then forms, for each transformed utility +dimension, + +```text +kappa = sqrt(beta) +UCB_j(x) = mu_U,j(x) + kappa * sigma_U,j(x), beta >= 0 +``` + +For the non-dominated observed utility set `P` and explicit utility-space +reference `r`, the deterministic base score is + +```text +a_UCB-HVI(x) = HV(P union {UCB(x)}; r) - HV(P; r). +``` + +True dominated or non-contributing points retain a raw score of zero. A small +epsilon is used only to represent scores in log space. The proposal wrapper +requires enough candidates above its explicit positive-HVI threshold; it does +not fill a batch with arbitrary zero-HVI points. + +Public scoring APIs: + +- `posterior_utility_moments` +- `hypervolume_improvement_scores` +- `score_ucb_hvi_from_moments` +- `score_ucb_hvi_pool` +- `propose_ucb_hvi_batch` + +## Shared local-penalized selector + +Both acquisition methods use `select_local_penalized_batch`. For normalized +inputs and optional positive dimension weights `w`, distance is + +```text +d_w(x, z) = sqrt(sum_j w_j * (x_j - z_j)^2). +``` + +After selecting `x_i`, the soft exclusion factor applied to a remaining point +is + +```text +phi_i(x) = 1 - exp(-0.5 * (d_w(x, x_i) / rho)^2), rho > 0. +``` + +The selector operates in log space: + +```text +log a_pen(x) = log a_base(x) + sum_i log(max(phi_i(x), epsilon)). +``` + +Hard minimum selected-to-selected and optional selected-to-observed/pending +distances are masked before selection. Ties retain stable candidate-pool order. +If the exact batch is impossible, `UndersizedBatchError` reports the requested +and selected sizes, active thresholds, and remaining count; no fallback relaxes +the rules. + +## R2 qLogNEHVI + +`score_qlognehvi_singletons` constructs BoTorch 0.15.1 +`qLogNoisyExpectedHypervolumeImprovement` with: + +- the fitted `ModelListGP`; +- normalized `train_X` as `X_baseline`; +- the same configured Monte Carlo objective used by UCB-HVI; +- the same explicit transformed-utility reference point; +- a seeded Sobol sampler; and +- explicit pending points. + +The scorer requires `ConfiguredMCMultiOutputObjective`; passing `None` or an +unversioned arbitrary objective fails before BoTorch can silently operate in raw +outcome space. The utility reference dimension is checked against both the +objective contract and the model output count. + +The pool is evaluated in shape `N x 1 x D` and in caller-controlled chunks. +`propose_qlognehvi_penalized_batch` recomputes singleton base scores at every +selection step. Its pending set is the pre-existing pending set plus all points +already selected in the new batch. The shared selector then applies the same +soft penalty and hard distance policy used by UCB-HVI. + +## Diagnostics + +`mobo_kit.candidate_diagnostics` provides: + +- within-batch pairwise normalized distances and min/mean/max summaries; +- nearest observed/pending distance per candidate; +- duplicate, grid-membership, and normalized-boundary checks; +- PCA, selected-condition parallel coordinates, distance heatmap, and + base-versus-penalized acquisition plots. + +Selection results retain pool indices, order, base/log scores, penalty factors, +nearest distances, acquisition-specific diagnostics, seeds, and settings. +Plot functions use the headless `Agg` backend and write only to caller-supplied +paths. The Step 2A example uses ignored `local_outputs/` and writes a strict +JSON provenance report containing method/contract versions, every seed, +pool-draw statistics, beta/kappa or MC settings, local-penalty settings, +per-selection scores, UCB utility moments, qLogNEHVI pending counts, and runtime +versions. It contains no campaign recipes. + +## Determinism and performance + +- Pool sampling uses a seeded NumPy `Generator` and stable acceptance order. +- Posterior and qLogNEHVI Monte Carlo sampling use explicit Sobol seeds. +- Candidate-pool evaluation has explicit chunk sizes. +- Sequential distance work is `O(N*q*D)`; no pool-wide `N x N` matrix is made. +- Hypervolume work is singleton candidate scoring against the fixed observed + Pareto set. +- CPU floating-point reductions can differ below normal numerical tolerances + across chunk shapes; selected indices are tested for deterministic reruns. + +## Limitations before Step 2B + +Step 2A does not ingest completed R0 outcomes, write workbooks, manage campaign +state, or generate real candidates. The exact production objective contract and +all fields rejected by the gate must be resolved at the experimental-team +meeting and frozen in a versioned configuration before a dry run. The read-only +v2 workbook profile requires the exact 29-column header tuple, sample rows 2-16, +numeric sample identifiers 1-15, blank row 17, and content range A1:AC18. A +local off-grid discrepancy is reported without snapping or copying the private +condition into tracked fixtures. diff --git a/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md b/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md new file mode 100644 index 0000000..5eb5ded --- /dev/null +++ b/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md @@ -0,0 +1,209 @@ +# D2D Step 2C: R1 Robustness, Search Convergence, and Proposal Stabilization + +## Status and safety boundary + +Step 2C is a read-only computational audit. It does not approve fabrication, +change the campaign workbook, generate a real R2 proposal, or change the Sample +1 inclusion policy. All outputs carry: + +```text +DEBUG ONLY - NOT APPROVED FOR EXPERIMENT +``` + +The campaign runner accepts only the pinned private workbook and fail-closed +debug configuration. It verifies the workbook SHA-256 and modification +timestamp before computation, after computation, and after artifact export. +Output is limited to the configured Git-ignored directory and is published from +a fully validated staging directory. A separate synthetic-CI helper generates a +sanitized workbook under the ignored output root and exercises the same internal +orchestration without making the campaign command redirectable. + +The known supplied-Uniformity mismatch and the off-grid control exception remain +visible and continue to block experimental approval. + +## Objective contract + +The three supplied final scores in columns Z, AA, and AB are used directly, in +this order: + +1. Uniformity score; +2. Optoelectronic score; +3. Thickness score. + +All three are maximized. The objective transform is identity for Step 2C. The +declared UCB-support policy clips only Uniformity and Thickness UCB coordinates +to `[0, 1]`; it never clips training targets or the Optoelectronic score. + +## GP validation + +Two explicit model variants are evaluated: + +- `default_current`, matching the Step 2B model; +- `conservative`, with documented observation-noise and ARD-lengthscale floors. + +Every fit is strict: failed optimization raises rather than silently returning +an unfitted model. Exact leave-one-out validation fits 15 folds per variant and +reports per-observation predictive means, predictive uncertainty, residuals, +standardized residuals, interval inclusion, and objective-level accuracy and +calibration metrics. Full-fit and fold hyperparameters, optimizer warnings, and +leave-one-out hyperparameter stability summaries are exported separately. ARD +diagnostics flag normalized lengthscales at or below 0.05 as very small and at +or above 10.0 as extremely large/flat. Candidate posterior means outside the +observed range and outside declared objective bounds are reported separately. + +Training-posterior diagnostics are not substituted for leave-one-out results. + +## Analytic identity moments and UCB-HVI + +Because every Step 2C objective transform is identity, posterior mean and +standard deviation are obtained analytically from the GP posterior. The primary +search is therefore deterministic and does not depend on a Monte Carlo seed. + +A fixed-seed Monte Carlo comparison remains in the audit. It records moment +differences, selection correspondence, runtime, per-objective tolerances of +`max(0.02, 0.02 * max(observed_range, 1.0))`, and pass/fail flags. This +comparison is diagnostic and does not relax any consensus gate. + +Candidate utility is computed as: + +```text +UCB = posterior mean + sqrt(beta) * posterior standard deviation +``` + +followed by the selected UCB-bound policy and deterministic hypervolume +improvement relative to the declared reference point. + +## Nested Sobol search and exact-grid refinement + +Full mode uses exact accepted-unique scrambled Sobol prefixes of 16,384, +32,768, 65,536, and 131,072 points with primary seed 73. Every smaller accepted +pool is an exact prefix of the next. Full-size secondary scramble seeds 137 and +911 measure scramble sensitivity. Pool hashes and rejection counters prove the +search basis without materializing the full Cartesian grid. + +At each sequential batch step, the highest eligible pool anchors and eligible +previously discovered optima are refined by deterministic coordinate ascent. +Every allowed value of one grid dimension is evaluated at a time. Stable +lexicographic tie-breaking, positive-HVI eligibility, observed-row exclusion, +hard spacing, soft penalty, sweep limits, and termination reasons are recorded. +The trace includes anchor pool indices, start/end base HVI, start/end penalized +log score, accepted moves, changed dimensions, and aggregate sweeps. + +Each nested size exports both the unrefined pool batch and refined batch, +including refinement gain, HVI summaries, boundary counts, matched distances, +relative regret, and runtime. + +## One-factor robustness studies + +The core candidate-region registry contains 13 unique runs spanning: + +- four nested-search sizes; +- two alternate Sobol scrambles plus the primary reference; +- default and conservative models; +- unclipped and clipped UCB policies; +- beta values 1, 4, and 9; +- no-soft and three soft-radius penalty settings under fixed hard spacing. + +The primary run acts as the shared reference level. Family-level persistence is +weighted equally so families with more variants do not dominate. + +The local-penalty table separates the hard-spacing effect from the soft-penalty +effect: soft-radius variants are compared with the no-soft run that retains the +same hard spacing. Acquisition sacrifice, diversity, penalty factors, boundary +behavior, nearest-observed distance, runtime, and region change are retained. +Its human-readable activity classification considers regional correspondence +and minimum/mean pairwise-distance changes, not only score attenuation. The +overall soft-penalty interpretation is derived from `radius_*` variants only; +hard spacing is interpreted separately. + +## Observation influence + +The full model and every exact leave-one-out model are evaluated on the same +accepted pool and use the same local-refinement settings. The report keeps the +following components visible: + +- exact and regional batch displacement; +- prediction changes at the full-model batch and robust-region medoids; +- common-pool acquisition rank and top-K changes; +- observed Pareto membership changes; +- hyperparameter displacement; +- fit/proposal runtime and fitting-warning counts. + +The composite influence rank is a summary, not a replacement for these +components. Sample 1 remains included in the primary model regardless of its +diagnostic rank. + +## Robust regions and shortlist + +Core candidates are clustered in normalized input space with deterministic +agglomerative complete linkage. The primary distance threshold is 0.15, with +0.10 and 0.20 sensitivity counts. Each region exposes its medoid, diameter, +run/family coverage, equal-weight study-family persistence, within-run normalized +HVI statistics, prediction summaries, nearest-observed distance, boundary +frequency, and full-versus-omit-Sample-1 correspondence. + +The ignored robust shortlist contains 8-12 diverse medoids where geometry +permits. It includes predictions and uncertainty under both models, raw and +bounded UCB coordinates, observed-range flags, boundary flags, and omission +sensitivity at each medoid. + +Lower- and upper-boundary counts, rates, and enrichment are kept separate in +candidate, influence, shortlist, and plotting artifacts. This prevents opposite +boundary tendencies from cancelling in a combined statistic. + +## Future qLogNEHVI compatibility + +Step 2C does not generate a real R2 batch. The bounded posterior-sample objective +is nevertheless integrated with the singleton qLogNEHVI scorer and tested on a +small real BoTorch model with a fixed reference point and seed. Bounds apply only +to posterior utility samples; training targets remain unchanged. + +## Consensus gate + +A five-row `r1_consensus_debug_batch.csv` can be created only in full mode and +only when all declared checks pass on those exact five rows: + +1. the two largest nested searches match at least 4/5 regions within 0.15; +2. their mean matched distance is at most 0.10; +3. at least five regions, including every chosen region, cover at least three + non-baseline core study families; +4. the chosen medoids are finite, bounded, unique, exactly on-grid, and satisfy + the 0.15 hard pairwise distance; +5. debug-only and both approval-false flags are intact. + +Fast mode is categorically ineligible. If any check fails, the runner creates +`r1_no_stable_batch_reason.json` and does not create a consensus batch. + +Before publication, the validator independently checks required table schemas, +provenance fields, the canonical resolved/source config relationship, workbook +proofs, exact conditional artifacts, debug stamps, PNG metadata, and supported +file formats. It recomputes the convergence and family gates from their CSVs, +links robust regions to the shortlist and the shortlist to any consensus rows, +and requires the no-stable reason to reproduce the failed checks and observations +exactly. The ignored output directory remains the complete local evidence +surface. For private campaign runs, its full ZIP is explicitly named +`*_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip` and carries a warning because it contains +local provenance, sample-level data, and exact candidate recipes. The legacy +`.zip` sibling is now a separate, strict public-summary export: it contains +only allowlisted aggregate tables plus a sanitized manifest and excludes local +paths, profile names, workbook filenames, sample-level rows, recipes, and +recipe-coordinate plots. The directory and both requested ZIPs are published in +one rollback-safe transaction; prior artifacts are restored if publication or +either final validation fails. + +## Running the audit + +From the repository root: + +```bash +# Sanitized generated-data end-to-end CI coverage +python examples/d2d_step2c_synthetic_ci.py --overwrite + +python examples/d2d_step2c_robustness.py --mode fast +python examples/d2d_step2c_robustness.py --mode full +``` + +The synthetic command is the self-contained generated-data end-to-end CI path. +Campaign fast mode is a private-workbook integration smoke only. Full mode is the +declared robustness study. Every mode is ignored, read-only with respect to its +source workbook, and debug-only. diff --git a/docs/REPO_AUDIT_D2D.md b/docs/REPO_AUDIT_D2D.md new file mode 100644 index 0000000..f2064dc --- /dev/null +++ b/docs/REPO_AUDIT_D2D.md @@ -0,0 +1,142 @@ +# MOBO-Kit Repository Audit for the D2D Campaign + +## Snapshot + +- Repository: `PV-Lab/MOBO-Kit` +- Verified default branch: `main` +- Baseline/main commit: `459afc6d06f66f4d5fb48c3f563e65c61f5df16f` +- Reference branch: `Nicky's-MOBO-Testground` +- Reference tip: `546a5bbdf8fc2f6f9fc6764ada5f5d587e1faa6f` +- Step 1 feature branch: `feature/d2d-mobo-step1-baseline` +- Reference comparison: 10 commits ahead, 0 behind, 59 changed paths, + 15,887 insertions, and 166 deletions + +The remote was fetched before branching. The reference branch was inspected by +explicit Git refs only and was not merged. + +## Architecture and current workflow + +The package separates design construction (`design.py`), normalization and grid +snapping (`data.py`), constraints (`constraints.py`), LHS (`lhs.py`), independent +GP fitting (`models.py`), qNEHVI/qLogNEHVI (`acquisition.py`), metrics +(`metrics.py`), plots (`plotting.py`), the high-level runner (`main.py`), and CLI +dispatch (`cli.py`). That is a workable engine boundary for a future thin Excel +adapter. + +The existing runner assumes all objectives are maximized, fits one `SingleTaskGP` +per output in a `ModelListGP`, continuously optimizes qNEHVI/qLogNEHVI in the +normalized cube, snaps to the physical grid, and writes an append-style CSV. It +has no campaign/round/sample/status state model and neither branch contains the +required R1 UCB. + +## Verified main-branch defects + +1. **Broken YAML-to-LHS path.** `generate_initial_experiments` passed a raw config + dictionary to `build_design`, which expects `InputSpec` objects. +2. **First experiment dropped.** `pandas.read_csv` had already consumed the + header, but `split_XY` sliced at `iloc[6:]`; the first demo experiment is at + DataFrame row 5. +3. **Fixed-offset parsing.** CSV metadata, separator, and data rows were assumed + rather than detected. Plain data and malformed metadata were accidental + behaviors. +4. **Implicit constraint.** Generic CSV conversion always injected the legacy + humidity/temperature Clausius-Clapeyron constraint. +5. **Unsafe metadata defaults.** Malformed start/stop/step cells could be replaced + with generic numeric defaults instead of failing. +6. **Inconsistent public types.** `split_XY` was annotated to return arrays but + returned DataFrames; `csv_to_config` was annotated as `str` but returned a + dictionary and wrote YAML as a side effect. +7. **Incomplete objectives accepted.** Partial/all-blank objective rows could + reach tensor/GP code as NaNs; blanks were not distinguished from real zeros. +8. **Non-deterministic/unsafe LHS.** Attempts were reseeded, subset selection used + a separate unseeded RNG, snapped duplicates were not removed, correlation + thresholds could be violated, and the final fallback bypassed constraints. +9. **Latent plotting failure.** LHS diagnostics referenced nonexistent + `design.labels`. +10. **Reference-point mismatch.** The high-level runner computed one reference + for reporting, then optimized with a hard-coded `[-0.01] * M` vector. +11. **Candidate failure hidden.** Proposal exceptions and short/empty batches + could still lead to a top-level `status: success`. +12. **Grid collisions/observed duplicates.** Continuous optima can snap to the + same recipe; the main path has no final deduplication or observed-point + exclusion. +13. **Round-trip mismatch.** LHS emits input-only CSV while the runner expected a + metadata-style CSV with objectives. +14. **CLI drift.** README examples omitted the required `run` subcommand and + `--num-restarts` was parsed but unused. +15. **Packaging drift.** Install URLs and the `all` extra still referenced + MOBO-FOM; dependency minimums admitted mutually incompatible future stacks. +16. **Stale tests.** Existing tests imported removed `src.*` modules and expected + APIs/files that no longer exist, so they did not validate the packaged code. +17. **Tracked caches.** Main tracked Python bytecode under `src/__pycache__` and + `tests/__pycache__` despite ignore rules. + +The main branch also contains pre-existing demonstration result images/CSVs. +Step 1 classifies them as published demo artifacts and does not regenerate or +expand them; generated campaign outputs and private inputs remain ignored. + +## D2D reference-branch findings + +The reference branch contains useful research ideas: large discrete candidate +pools, observed-point exclusion, deduplication, discrete qNEHVI/local search, +normalized linear constraints, posterior diagnostics, conservative noise/kernel +options, and encoding fallback. These need selective review and synthetic tests +in later steps. + +It is not an authoritative executable D2D pipeline: + +- its D2D YAML defines ten inputs but enables a Clausius-Clapeyron constraint on + `absolute_humidity` and `temperature_c`, neither of which exists in that design; +- its D2D CSV is inherited from the eight-input slot-die example, conflicts with + the YAML, and has declared/data field-count mismatches; +- notebooks import `MixedMCMultiOutputObjective`, which is absent from the active + package and exists only in a stale `src/mobo_kit OLD` copy; +- notebook objective directions include a 650-nm thickness match, but qNEHVI + calls omit that transform while using a transformed reference point; +- posterior non-domination calculations then compare raw outputs as if all were + maximized; +- the automatically derived reference changes with the observed dataset, so + round-to-round hypervolume would not be comparable; +- the diversity selector is post-hoc Euclidean reranking, not documented local + hypervolume penalization, and its fallback can bypass the configured minimum + distance; +- notebooks contain personal macOS Dropbox paths, undefined variables, + inconsistent `str`/`Path` operations, stale campaign names, embedded outputs, + and saved execution errors; +- caches, egg-info, a complete old package copy, and generated results are + committed; and +- no tests accompany the large acquisition/model changes. + +The changed `results/experiment/next_batch.csv` is legacy slot-die data, not a +D2D R1 result. No Excel integration, stable row identifiers, or round-state +adapter exists on the branch. + +## Workbook audit + +An ignored private workbook was inspected read-only and identity-checked against +the explicitly supplied source. Its filename and digest are not tracked. + +- one sheet: `Sheet1` +- non-empty range: `A1:AC16` +- 29 columns and 15 sample rows +- `Uniformity score` duplicates at 1-based positions 17 and 20 +- related/ambiguous `anneal_temp` and `Anneal Temp` at positions 8 and 12 +- blank header at position 25, after `Total combination - addition` +- measurements and derived scores are blank +- formatting extends beyond the non-empty range, so auditors must calculate the + range from cell content rather than styled dimensions alone + +No workbook cells, formulas, formats, or macros were changed. + +## Step 1 remediation boundary + +Step 1 fixes schema/execution ambiguity, parsing, opt-in constraints, design +construction, deterministic LHS, workbook auditing, tests, packaging, and +hygiene. It adds a canonical input-only D2D YAML with empty objective names and +no constraints. + +Step 1 deliberately does not implement UCB, local penalization, qNEHVI round +changes, objective formulas, a thickness transform, a campaign reference point, +real candidates, or an Excel button. The existing qNEHVI runner is made opt-in +and requires an explicit same-space reference point so it cannot silently invent +one. diff --git a/docs/STEP1_HANDOFF.md b/docs/STEP1_HANDOFF.md new file mode 100644 index 0000000..dce77e5 --- /dev/null +++ b/docs/STEP1_HANDOFF.md @@ -0,0 +1,273 @@ +# Step 1 Handoff + +## 1. Status + +- Historical implementation branch: `feature/d2d-mobo-step1-baseline` +- Base commit: `459afc6d06f66f4d5fb48c3f563e65c61f5df16f` (`origin/main`) +- Reference branch inspected only: `Nicky's-MOBO-Testground` at + `546a5bbdf8fc2f6f9fc6764ada5f5d587e1faa6f` +- Original milestone state: Step 1 was reviewed locally before its checkpoint + commit. This handoff preserves that historical state; publication later uses a + clean squash containing the sanitized Step 1 through Step 2C tree. +- Overall result: **PASS**, with the documented deviations in section 11 + +No production R1/R2 candidates were generated. No UCB implementation, local +penalization, final objective transform, thickness target, or campaign reference +point was introduced. + +## 2. Summary of changes + +- Defined the exact ten-input D2D grid and campaign boundary without inventing + objectives or constraints. +- Replaced fixed-row CSV handling with a validated metadata-style parser that + preserves the first experiment and rejects ambiguous/incomplete model data. +- Corrected config-to-design construction and made grid validation explicit. +- Made constraints opt-in and removed the invalid D2D humidity constraint. +- Reworked R0 LHS generation to be deterministic, exactly sized, grid-valid, + unique after snapping, post-snap constrained, and fail-closed. +- Added a read-only raw-cell workbook schema auditor and a sanitized fixture. +- Pinned a CPU-tested dependency stack and repaired package/CLI documentation. +- Made candidate generation opt-in, require an explicit reference point, and + reject incomplete, duplicate, off-grid, non-finite, or observed recipes. +- Moved default generated output to ignored `local_outputs/`, enabled headless + plot generation, and made explicit missing config paths fail. +- Removed tracked bytecode caches and retired three obsolete experimental test + scripts whose active behavior is covered by the replacement test suite. + +## 3. Files changed + +| File or group | Purpose | +|---|---| +| `.gitignore` | Ignore private inputs, local outputs, caches, environments, coverage, and build artifacts. | +| `README.md` | Correct install, package, CLI, CPU/GPU, metadata-CSV, output, and round-trip guidance. | +| `pyproject.toml`, `setup.py`, `requirements.txt` | Normalize packaging, supported Python range, extras, dependency bounds, and pytest collection. | +| `requirements/constraints.txt`, `requirements/dev.txt` | Pin the tested direct stack and provide a reproducible editable development install. | +| `configs/FA0.9CS0.1PbI3_260407_Config.yaml` | Canonical ten-input D2D grid, empty objectives, and `constraints: []`. | +| `docs/D2D_CAMPAIGN_SPEC.md` | Canonical schema, workbook mapping guardrails, state model, and unresolved decisions. | +| `docs/REPO_AUDIT_D2D.md` | Verified main/reference-branch architecture, defects, risks, and workbook findings. | +| `docs/STEP1_HANDOFF.md` | This implementation and verification record. | +| `src/mobo_kit/design.py` | Strict `InputSpec`, endpoint-aligned grids, and supported config-to-design path. | +| `src/mobo_kit/utils.py` | Raw CSV metadata parser, explicit encodings, typed DataFrames, and strict model-boundary validation. | +| `src/mobo_kit/constraints.py` | Explicit-only constraint parsing and clear configuration/shape errors. | +| `src/mobo_kit/lhs.py` | Deterministic snapped-grid LHS with uniqueness, exact-size, constraint, and hard-correlation guarantees. | +| `src/mobo_kit/workbook_schema.py` | Read-only, content-range workbook audit preserving raw duplicate/blank headers. | +| `src/mobo_kit/main.py`, `src/mobo_kit/cli.py` | Correct design construction, safe defaults, CLI wiring, headless output, and proposal guards. | +| `tests/test_csv_parser.py` | Metadata boundary, first-row, encoding, objective, malformed-data, and return-type tests. | +| `tests/test_design.py`, `tests/test_lhs.py` | Grid/schema validation and deterministic/unique/constrained LHS regressions. | +| `tests/test_d2d_baseline.py` | Exact D2D contract/cardinality, 20-row smoke, constraints, imports, CLI, hygiene, and fail-closed guards. | +| `tests/test_workbook_schema.py` | Sanitized workbook fixture plus optional ignored-local-workbook audit. | +| `tests/test_acquisition.py`, `tests/test_models.py`, `tests/test_plotting.py` | Active-package, CPU-fast replacements for stale tests. | +| tracked `src/__pycache__/*`, `tests/__pycache__/*` | Removed 26 committed bytecode/cache artifacts. | +| `tests/smoke_test.py`, `tests/simple_gp_test.py`, `tests/synthetic_test.py` | Retired obsolete scripts using missing `src.*` APIs and uncontrolled experimental/candidate loops. | + +The private workbook remains only in ignored local storage. Its runtime identity +matches the explicitly supplied source, but neither filename nor digest is +tracked. + +## 4. Defects fixed + +1. `generate_initial_experiments` passed a raw dict to `build_design`. +2. CSV parsing used a fixed row offset and dropped the first experiment. +3. Duplicate headers could be mangled before ambiguity was reported. +4. Malformed metadata, missing/partial objectives, and nonnumeric model rows + could pass too far or be handled inconsistently. +5. Generic config conversion injected an unrelated Clausius-Clapeyron + constraint; the D2D config referenced nonexistent variables. +6. Public data-loading annotations and runtime return types disagreed. +7. LHS retries/subset choice were not fully deterministic and could return + duplicates, constraint violations, an undersized set, or a correlation-limit + violation. +8. LHS diagnostics referenced nonexistent `design.labels`. +9. The runner used an internally inconsistent hard-coded proposal reference + point, hid proposal failure, and did not wire `--num-restarts` through. +10. Proposal output could report success with an incomplete, duplicate, + off-grid, or already observed batch. +11. An explicitly supplied missing config path silently triggered schema + inference in the Python API. +12. Plot generation depended on Tcl/Tk even though the runner only writes files. +13. Default commands could overwrite tracked demonstration results. +14. README/package names, URLs, extras, CLI examples, and dependency policy had + drifted from MOBO-Kit. +15. Tracked caches and stale `src.*` test scripts obscured the actual package + test surface. + +## 5. Tests added + +1. Valid and invalid YAML/config to `Design`, including exact D2D input order, + bounds, steps, per-axis grids, and full product `177,816,994,740`. +2. Robust metadata CSV parsing: optional separator, first data row, encodings, + duplicate headers, missing objectives, malformed metadata, plain CSV, and + empty experimental data. +3. Model-boundary DataFrame behavior for missing, blank, partial, nonnumeric, + and non-finite values. +4. Empty/default, valid explicit, and missing-column constraint behavior. +5. Same-seed determinism, different-seed change, grid membership, bounds, + uniqueness, post-snap constraints, exact size, impossible requests, and hard + correlation limits for LHS. +6. A deterministic unique 20-by-10 D2D R0 smoke generation; the sample count is + still caller-configurable. +7. Sanitized and optional local workbook audits for range, sample count, + duplicate headers, blank header, anneal ambiguity, and file immutability. +8. Direct Torch/GPyTorch/BoTorch/package CPU imports and all three CLI help + surfaces. +9. Production source/notebook personal-path scan. +10. Fail-closed proposal batch validation and explicit missing-config behavior. +11. Active-package acquisition, model, and headless plotting smoke tests. + +## 6. Commands run + +```powershell +git -c http.sslBackend=openssl fetch --all --prune +git switch -c feature/d2d-mobo-step1-baseline main + +# Clean environment installation used the committed constraints. +uv pip install -c requirements/constraints.txt -e ".[dev]" + +.\.venv\Scripts\python.exe -m pytest -q +.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider ` + --basetemp + +.\.venv\Scripts\python.exe -m black --check +git diff --check +git ls-files + +.\.venv\Scripts\mobo-kit.exe --help +.\.venv\Scripts\mobo-kit.exe generate --help +.\.venv\Scripts\mobo-kit.exe run --help +.\.venv\Scripts\mobo-kit.exe run ` + --csv data/processed/configCSV_example.csv ` + --config configs/demo_config.yaml --device cpu --seed 42 --verbose ` + --out + +.\.venv\Scripts\python.exe -c "import torch, gpytorch, botorch, mobo_kit" +.\.venv\Scripts\python.exe -c "from mobo_kit.workbook_schema import audit_campaign_workbook; ..." +``` + +GitHub access, branch tips, and comparison were also verified against the +connected repository before implementation. The reference branch was never +checked out over or merged into the feature branch. + +## 7. Test results + +Final complete test command (after all code changes): + +```text +79 passed, 14 third-party Matplotlib/PyParsing deprecation warnings, +0 skipped, 0 failed +``` + +The literal `pytest -q` command also passes. This Windows sandbox denies pytest's +default cache directory, producing one additional cache warning; the recorded +clean result disables only the cache provider and uses an explicit writable +temporary base. `git diff --check`, `pip check`, CLI help, direct imports, the +safe CPU demo run, workbook hash/mtime checks, and the scoped Black check pass. + +The safe demo run loaded 12 rows with 8 inputs and 3 synthetic/demo objectives, +fit the CPU models, wrote `parity_plots.png`, wrote no `next_batch.csv`, and +exited 0. + +## 8. Tested environment + +- OS: Microsoft Windows NT `10.0.26200` +- Python: `3.12.10` +- torch: `2.8.0+cpu` +- gpytorch: `1.14` +- botorch: `0.15.1` +- linear_operator: `0.6` +- numpy: `2.2.6` +- scipy: `1.16.0` +- pandas: `2.3.1` +- scikit-learn: `1.7.1` +- matplotlib: `3.10.3` +- seaborn: `0.13.2` +- PyYAML: `6.0.2` +- Excel reader: openpyxl `3.1.5` +- Image handling: Pillow `12.3.0` +- pytest: `8.4.1` +- black: `25.1.0` +- CUDA available: `False` + +The committed package range is Python `>=3.11,<3.13`. Python 3.10 was removed +from the advertised baseline because the tested SciPy 1.16 stack requires +Python 3.11 or newer. + +## 9. Workbook audit + +- Input: explicitly supplied ignored private workbook +- Identity: verified locally before and after the audit; not tracked +- Sheet: `Sheet1` +- Used/non-empty range: `A1:AC16` +- Columns: 29 +- Sample rows: 15 +- Duplicate headers: `Uniformity score` at one-based columns 17 (`Q`) and 20 + (`T`) +- Ambiguous headers: `anneal_temp` at column 8 (`H`) and `Anneal Temp` at + column 12 (`L`) +- Other warning: blank header at column 25 (`Y`), immediately after + `Total combination - addition` +- Modification result: SHA-256 and modification time unchanged; no save was + performed + +No duplicate or related workbook headers were merged, renamed, or assigned a +scientific meaning. + +## 10. Unresolved scientific decisions + +1. R0 candidate/control counts and whether controls/replicates train the GP. +2. The three authoritative BO objective columns, formulas, and directions. +3. Uniformity definition and meanings of both `Uniformity score` columns. +4. Thickness target, tolerance, and utility transformation. +5. Optoelectronic score formula and missing/failed/zero semantics. +6. Fixed campaign objective scaling and out-of-range policy. +7. Fixed hypervolume reference point in original and transformed units. +8. Meanings of `anneal_temp` and `Anneal Temp` and which is the optimizer input. +9. Physical/equipment/safety constraints and control recipes. +10. Literature-backed multi-objective UCB definition and beta/noise policy. +11. Local-penalization metric, radius/weight, observed-point treatment, and hard + minimum distance. +12. Missing-data, failed-film, QC, outlier, and replicate aggregation policy. +13. Excel deployment environment, macro policy, Python installation, signing, + and trusted-location requirements. + +## 11. Deviations from specification + +- The pack preferred Python 3.10/3.11. The usable managed local runtime was + Python 3.12.10, so that exact CPU environment was tested. The resulting + package supports 3.11-3.12, but Python 3.11 still needs CI verification. +- Native pip dependency resolution repeatedly consumed CPU without terminating + cleanly in this managed environment. The environment was installed with `uv` + against the committed pip-compatible constraints; `pip check` and a local + no-dependency editable-install dry run pass. +- All changed/new Python files pass Black. A repository-wide Black check still + reports five untouched legacy source modules (`acquisition.py`, `data.py`, + `metrics.py`, `models.py`, and `plotting.py`) as style-only reformat targets. + They were not mass-formatted to avoid an unrelated full-source rewrite. +- The real-workbook integration test ran locally. It remains optional/skipped + for clean clones where the ignored private workbook is unavailable. + +## 12. Risks and limitations + +- The D2D YAML deliberately has no objective names; it is valid for R0 design + generation but cannot authorize a D2D model/acquisition run. +- R0 `generate` emits an input-only CSV. It does not round-trip directly into + the metadata-style `run` parser; the future workbook adapter must implement + that state transition. +- The existing qNEHVI path remains a legacy, explicit opt-in capability. Its + output is guarded for size/grid/uniqueness/observed recipes, but Step 1 does + not add round logic or local penalization. +- Only CPU/Python 3.12 was executed locally. GPU and Python 3.11 need separate + CI coverage before being claimed as tested campaign environments. +- The 14 test warnings are third-party Matplotlib/PyParsing deprecations. + +## 13. Recommended Step 2 + +- Obtain and encode the approved objective-transform contract, fixed scales, + QC policy, process constraints, and fixed reference point. +- Implement and mathematically document the approved R1 multi-objective UCB + acquisition using synthetic tests first. +- Add one reusable, fail-closed local-penalized batch-selection policy for R1 + and R2, including distance diagnostics and exact batch-size guarantees. +- Design the workbook round-trip/state adapter only after the scientific schema + is approved; keep optimization logic in the Python engine. +- Do not generate real R1 candidates until those approvals and a frozen campaign + configuration are present. diff --git a/docs/STEP2A_HANDOFF.md b/docs/STEP2A_HANDOFF.md new file mode 100644 index 0000000..5608edd --- /dev/null +++ b/docs/STEP2A_HANDOFF.md @@ -0,0 +1,319 @@ +# Step 2A Handoff + +## 1. Status + +- Step 1 checkpoint commit: `648efd0c81631726afed91e493b143e66e5f25c1` +- Historical Step 2A branch: `feature/d2d-mobo-step2a-core` +- Original milestone state: the implementation was reviewed before the Step 2A + checkpoint. This handoff preserves that history; publication later uses a + clean squash containing the sanitized Step 1 through Step 2C tree. +- Overall result: **PASS** +- Production candidate generation: **BLOCKED / NOT RUN** +- Production pull request or candidate release at this milestone: **NOT RUN** + +## 2. Summary of implementation + +- Added a versioned raw-output-to-utility framework, including nonlinear target + transforms applied to posterior samples. +- Added integer-index sampling of exact-size, discrete candidate pools without + materializing the 177.8-billion-row D2D Cartesian product. +- Added R1 UCB-HVI scoring and exact five-candidate, positive-HVI-only batch + selection. +- Added one sequential, log-space local-penalized selector shared by UCB-HVI and + qLogNEHVI. It enforces stable ties and hard distances without fallback. +- Added singleton-pool qLogNEHVI with a required configured MC objective and + pending-point reconstruction at every selection step. +- Added numerical/grid/distance diagnostics and four headless plots per method. +- Extended the read-only workbook auditor to preserve the historical Step 1 + profile and recognize the updated v2 profile. +- Added an explicit production approval gate. The supplied provisional config + is tested and rejected. The existing campaign proposal runner is also + blocked before CSV parsing, model fitting, or output creation; even an + otherwise approved config cannot enter that incompatible legacy path. +- Added a deterministic ten-input synthetic CPU example and optional + 10,000-point scoring benchmark. No workbook is read by either example. The + synthetic JSON report now records seeds, pool draws/rejections, method + versions, beta/kappa, MC settings, local penalties, per-selection scores, + UCB utility moments, pending counts, and runtime versions. + +## 3. Mathematical definitions implemented + +### Objective transforms + +All objective outputs are transformed into an all-maximize utility space. + +```text +affine maximize: u(y) = (y - lo) / (hi - lo) +affine minimize: u(y) = (hi - y) / (hi - lo) +Gaussian target: u(y) = exp(-0.5 * ((y - target) / sigma)^2) +negative absolute target: u(y) = -abs(y - target) / scale +``` + +Affine anchors, target, sigma, and scale are explicit and validated. The +nonlinear transforms are applied to every posterior MC sample before computing +utility mean and population standard deviation (`correction=0`). Latent +posterior sampling is the default and observation-noise inclusion is explicit. + +### UCB-HVI + +```text +kappa = sqrt(beta) +UCB_j(x) = mean_U,j(x) + kappa * std_U,j(x) +a_UCB-HVI(x) = HV(P union {UCB(x)}; r) - HV(P; r) +``` + +`P` is the non-dominated observed utility set and `r` is a fixed, caller-supplied +reference in the same utility space. True zero HVI remains zero. Only log-score +stabilization uses epsilon. Negative HVI beyond numerical tolerance raises. +Batch proposal requires enough candidates above a positive-HVI threshold. + +### Local penalization + +```text +d_w(x,z) = sqrt(sum_j w_j * (x_j - z_j)^2) +phi_i(x) = 1 - exp(-0.5 * (d_w(x,x_i) / rho)^2) +log a_pen(x) = log a_base(x) + sum_i log(max(phi_i(x), epsilon)) +``` + +Distances use normalized input space. Hard selected-to-selected and optional +selected-to-observed/pending thresholds are applied before stable `argmax`. +Exact observed/pending duplicates are always ineligible. An impossible exact +batch raises `UndersizedBatchError` and does not relax settings. + +### qLogNEHVI sequential selection + +Each remaining candidate is evaluated with shape `N x 1 x D`. At selection step +`t`, `X_pending` contains pre-existing pending points plus selections `1..t-1`. +The seeded qLogNEHVI acquisition is rebuilt and rescored before the shared local +penalty is applied. `ConfiguredMCMultiOutputObjective` is mandatory, preventing +an accidental identity transform in raw outcome space. + +## 4. Public APIs + +| API | Purpose | Input/output spaces | +|---|---|---| +| `ObjectiveSpec`, `ObjectiveTransform` | Validate and apply a versioned objective contract | raw/model outcome to all-maximize utility | +| `ConfiguredMCMultiOutputObjective` | Use the same transform inside BoTorch MC acquisition | posterior samples to utility samples | +| `sample_discrete_candidate_pool` | Sample exact unique grid tuples with exclusions/constraints | grid indices, physical inputs, normalized inputs | +| `physical_rows_to_grid_indices` | Validate exact grid membership | physical inputs to integer indices | +| `score_ucb_hvi_pool` | MC utility moments and singleton optimistic HVI | normalized pool/raw posterior to utility-space scores | +| `propose_ucb_hvi_batch` | Select an exact positive-HVI batch | candidate pool to selected normalized/physical rows | +| `select_local_penalized_batch` | Shared sequential diversity selector | log acquisition + normalized distances to batch | +| `score_qlognehvi_singletons` | Chunked discrete singleton qLogNEHVI | normalized pool to utility-space log acquisition | +| `propose_qlognehvi_penalized_batch` | Exact sequential qLogNEHVI batch with pending updates | candidate pool to selected normalized/physical rows | +| `summarize_candidate_batch` and plot helpers | Numeric and headless selection diagnostics | normalized/physical inputs to summaries/PNG files | +| `audit_campaign_workbook` | Recognize historical/v2 schemas without saving | workbook cells to structured audit | +| `validate_production_config` | Block unresolved campaign-facing proposals | resolved config to approval receipt/hash | +| `block_legacy_campaign_proposal` | Prevent an approved config from entering the incompatible old proposal runner | resolved config to an explicit Step 2B migration error | + +## 5. Files changed + +| File | Purpose | +|---|---| +| `configs/d2d_step2a_provisional.yaml` | Deliberately unapproved D2D contract skeleton | +| `docs/D2D_STEP2A_COMPUTATIONAL_CORE.md` | Equations, APIs, determinism, safety, limitations | +| `docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md` | Current workbook facts and unresolved objective choices | +| `docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md` | Step 2B scientific decision record | +| `docs/STEP2A_HANDOFF.md` | This implementation and verification record | +| `src/mobo_kit/objectives.py` | Objective contract and BoTorch MC adapter | +| `src/mobo_kit/candidate_pool.py` | Integer-index discrete-pool sampling | +| `src/mobo_kit/batch_selection.py` | Shared local-penalized selector | +| `src/mobo_kit/ucb_hvi.py` | Posterior utility moments, HVI, R1 proposal | +| `src/mobo_kit/qlognehvi_batch.py` | Singleton qLogNEHVI and R2 proposal | +| `src/mobo_kit/candidate_diagnostics.py` | Numerical diagnostics and headless plots | +| `src/mobo_kit/workbook_schema.py` | Historical/v2 read-only workbook audit | +| `src/mobo_kit/production_gate.py` | Fail-closed production approval validator | +| `src/mobo_kit/main.py` | Enforce the gate and Step 2A proposal block before campaign data is parsed | +| `src/mobo_kit/cli.py` | Expose the Step 2A proposal-disabled status without pre-creating output | +| `examples/d2d_step2a_synthetic.py` | Synthetic-only ten-input 5/3 smoke run | +| `examples/d2d_step2a_benchmark.py` | Optional synthetic 10,000-point benchmark | +| `tests/test_*.py` (Step 2A modules) | Mathematical, safety, reproducibility, integration tests | + +## 6. Tests added + +1. Objective identity/affine/target equations, shapes, dtype/device, validation, + nonlinear sample-before-mean behavior, and BoTorch MC equivalence. +2. Candidate-pool exact size/order, grid membership, normalization, exclusions, + physical constraints, rejection statistics, impossible requests, and proof + that Cartesian allocation helpers are not used. +3. UCB-HVI beta/kappa behavior, exact two-/three-objective HVI, Pareto filtering, + chunk/seed consistency, reference validation, and zero-HVI batch refusal. +4. Local-penalty formula, weights, stable ties, diversity, hard distances, + duplicate exclusion, and explicit undersized-batch failure. +5. qLogNEHVI singleton shape/chunks, configured objective requirement, reference + dimensions, observed/pending exclusion, sequential pending counts, exact + three-candidate batch, and hard-distance failure. +6. Candidate distance/grid/boundary diagnostics and all four headless plots. +7. Historical and v2 sanitized workbook fixtures, aliases, notes, formulas, + input-grid validation, local file integrity, and unapproved objective status. +8. Production-gate missing/false/nested-placeholder cases; fixed-scaling and + integer-count validation; distance-weight consistency; rejection of the + exact supplied provisional config; and pre-I/O campaign-runner blocking. +9. End-to-end CPU synthetic GP fitting, five UCB-HVI candidates, three + qLogNEHVI candidates, deterministic reruns, spacing, grids, and temporary-only + outputs. + +The suite increased from the protected Step 1 baseline of 79 tests to 170 tests. + +## 7. Commands run + +```powershell +# Step 1 reproduction before checkpoint +.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider ` + --basetemp \step2a-pytest-step1-20260724 + +# Final full suite +.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider ` + --basetemp \pytest-step2a-full-final + +# Formatting and dependency checks +.\.venv\Scripts\python.exe -m black --check +.\.venv\Scripts\python.exe -m pip check +git diff --check +git diff --no-index --check NUL +git status --short + +# Synthetic verification and optional performance benchmark +.\.venv\Scripts\python.exe examples\d2d_step2a_synthetic.py +.\.venv\Scripts\python.exe examples\d2d_step2a_benchmark.py +``` + +Focused objective, pool, selector, UCB-HVI, qLogNEHVI, diagnostics, workbook, +gate, and synthetic tests were also run during development. + +## 8. Test results + +```text +Step 1 checkpoint reproduction: 79 passed, 14 warnings, 18.19 s +Step 2A final full suite: 170 passed, 20 warnings, 7.64 s +Black 25.1.0 check: PASS +pip check: PASS +git diff --check: PASS +Untracked no-index check: PASS (22 files) +Private/generated artifact check: PASS (ignored and untracked) +``` + +Warnings are from the pinned third-party Matplotlib/PyParsing stack and +BoTorch/NumPy array-compatibility path during synthetic GP fitting. There are no +test failures or project-code warnings. + +## 9. Performance + +- Synthetic smoke elapsed time: 2.72 s for 15 observations, a 128-point R1 + pool, five-candidate UCB-HVI, a 96-point R2 pool, three-candidate qLogNEHVI, + deterministic reruns, diagnostics, and eight plots on CPU. +- Optional 10k-pool benchmark: + - pool sampling: 0.1750 s; + - deterministic three-objective UCB-HVI scoring: 2.2931 s; + - 6,986 positive-HVI points; + - full Cartesian grid not materialized. +- Tested environment: Windows 11, CPython 3.12.10, 24 logical processors, + Torch 2.8.0 CPU (`cuda_available=False`). + +| Dependency | Version | +|---|---:| +| NumPy | 2.2.6 | +| pandas | 2.3.1 | +| SciPy | 1.16.0 | +| scikit-learn | 1.7.1 | +| Matplotlib | 3.10.3 | +| seaborn | 0.13.2 | +| PyYAML | 6.0.2 | +| Torch | 2.8.0 | +| GPyTorch | 1.14 | +| BoTorch | 0.15.1 | +| SHAP | 0.48.0 | +| openpyxl | 3.1.5 | +| pytest | 8.4.1 | +| Black | 25.1.0 | + +- Observed limitation: singleton hypervolume evaluation scales linearly with + pool size but has a nontrivial per-point cost; qLogNEHVI construction and GP + posterior work should continue to use explicit chunks for production-sized + pools. + +## 10. Updated workbook audit + +- Input: explicitly supplied ignored private workbook +- Identity and modification time: verified unchanged locally; not tracked +- Sheet/range: `Sheet1`, `A1:AC18` +- Rows/columns: 15 sample rows (Excel 2–16), 29 columns, row 17 blank +- Header mapping: canonical inputs B:K; explicit + `precur_vol (uL)` to `precur_vol` alias; T uniformity, U optoelectronic, V raw + thickness, W normalized thickness; Y blank +- No duplicate annealing-temperature field, formulas, or populated objective + measurements +- Actual note locations: Q18/S18. P18/R18 are blank, contrary to the pack text. +- Grid warning: the local workbook contains an off-grid input discrepancy + relative to the provisional design contract. No private condition was + snapped, changed, or copied into tracked fixtures. +- Modification result: file hash and modification time were unchanged by the + read-only audit and tests; the workbook remains ignored and untracked. + +## 11. Production gate + +- Required fields include explicit production approval; approved objective and + constraint statuses; campaign/schema/input + versions; approver/time/decision record; three unique source/formula/transform + contracts; fixed scaling and reference point; QC/control/replicate policy; + explicit constraints list; R1/R2 method, batch, sample, pool, and beta values; + local radius/hard distances; seed and provenance recording. +- Null, blank, false approval, nested `PENDING`/`TBD`/`provisional`, dynamic + observed-data scaling, fractional sample/pool counts, inconsistent distance + weights, invalid numeric, unsupported distance, missing objective, and + missing policy cases are rejected. +- `allow_hard_distance_relaxation` must be false and R2 sequential pending must + be true. +- The exact `configs/d2d_step2a_provisional.yaml` template is tested and rejected + with more than ten independent unresolved reasons. +- `run_mobo_experiment(..., propose_candidates=True)` evaluates the gate before + CSV parsing or output creation. A fully resolved test config passes the gate + but is then deliberately rejected because the legacy runner does not use the + Step 2A transforms, discrete pool, or shared local selector. +- Provisional config status: `approved_for_production: false`. + +## 12. Deviations + +1. The instruction pack says the workbook notes are at P18/R18. Direct workbook + inspection and artifact-tool rendering show Q18/S18. The contract fixture + follows P18/R18; a separate sanitized anomaly fixture and the local audit + report Q18/S18 without modifying the source file. +2. The pack expects all 15 real workbook recipes to be on-grid. The supplied + workbook contains a local off-grid discrepancy. The sanitized v2 fixture + proves valid-grid behavior; the local audit fails closed without exposing, + snapping, or modifying the private condition. +3. Step 2A was originally reviewed as a feature diff before checkpointing. The + current public-review tree includes its sanitized implementation in a clean + Step 1 through Step 2C squash. + +## 13. Remaining decisions after the meeting + +- Final model/utility mapping: `T,U,W`, `T,U,V`, or another approved mapping. +- Exact T and U ownership, equations, weights, fixed normalization anchors, + clipping, and missing/failure semantics. +- Final raw thickness versus W model source, 650-nm confirmation, transform, + sigma/scale, symmetry, and output ownership. +- Fixed utility scales and fixed utility-space reference point. +- R0 row roles, controls, replicates, GP inclusion, QC, failure, outlier, and + missing-objective policies. +- Complete physical/equipment constraints or explicitly approved `constraints: []`. +- Resolution of the local off-grid discrepancy versus the approved input + contract. +- R1 beta/posterior samples/pool/seed and R2 MC samples/pool/seed. +- Local-penalty radius, within-batch distance, observed/pending distance, and + required review plots. +- Workbook/score ownership, campaign state, backup, and audit workflow. + +## 14. Recommended Step 2B + +- Encode the approved objective mapping and exact versioned formulas. +- Freeze fixed scales and the transformed-utility reference point. +- Encode controls, replicates, QC rules, and every process constraint. +- Resolve the off-grid workbook row or approve a revised input contract. +- Run a read-only dry-run audit on completed R0 objective data. +- Review the resulting model diagnostics and synthetic-equivalent proposal + audit without writing to the workbook. +- Implement the reviewed Step 2B campaign adapter; do not re-enable the legacy + raw-objective proposal path. +- Only after the resolved config passes the production gate should a real R1 + proposal be authorized. diff --git a/docs/STEP2B_DEBUG_HANDOFF.md b/docs/STEP2B_DEBUG_HANDOFF.md new file mode 100644 index 0000000..0fc0b91 --- /dev/null +++ b/docs/STEP2B_DEBUG_HANDOFF.md @@ -0,0 +1,272 @@ +# Step 2B Debug Handoff + +## 1. Git status + +- Step 1 checkpoint: `648efd0c81631726afed91e493b143e66e5f25c1` +- Step 2A checkpoint: `749c9de` (`Implement D2D MOBO Step 2A computational core`) +- Historical Step 2B branch: `feature/d2d-mobo-step2b-debug` +- Original milestone state: implemented and reviewed locally before the Step 2B + checkpoint; publication later uses a clean, sanitized Step 1 through Step 2C + squash. +- Experimental or production pull request at this milestone: not run + +Step 2A was reproduced before checkpointing: 170 tests passed, its scoped Black +check passed, `pip check` passed, and `git diff --check` passed. + +## 2. Overall result + +- Result: **PASS for read-only algorithm debugging** +- Debug R1 proposal: generated +- Experimental approval: **false** +- Production approval: **false** +- Source workbook write-back: not implemented and not run + +The generated conditions are not a lab worklist. Every candidate artifact is +watermarked `DEBUG ONLY - NOT APPROVED FOR EXPERIMENT`. + +## 3. Source workbook + +- Input: explicitly supplied ignored private workbook +- Identity and modification time: verified unchanged locally; not tracked +- Sheet/range: `Sheet1`, `A1:AI20` +- Workbook profile: `d2d_summary_v3_scores` +- Sample rows: 15 unique numeric Sample IDs at Excel rows 2-16 +- Note rows excluded: 17-20 +- Objective columns: Z, AA, AB +- Workbook modified: no + +The previous Step 2A workbook was preserved under an ignored private name. All +`local_inputs/` and `local_outputs/` files remain ignored and untracked. + +## 4. Resolved objective contract + +Ordered GP outputs and acquisition utilities: + +1. Z `Uniformity score` +2. AA `Optoelectronic score` +3. AB `Thickness score` + +All three use identity transforms and are maximized. No clipping or observed-data +min/max normalization is applied. GP outcome standardization remains internal to +the existing model implementation. Input normalization uses fixed configured +bounds. + +- Fixed utility-space reference: `[-0.01, -10.0, -0.01]` +- All 15 observations strictly dominate the reference component-wise. +- Ignored: AC `Stability score?` and all summary fields AD:AI. + +## 5. Score validation + +- Required final scores: 15/15 complete and finite for Z, AA, and AB. +- Authoritative Uniformity scores: 15/15 within the required `[0,1]` range; + out-of-range values now fail validation and model ingestion. +- Uniformity support `L*N*O`: 4/15 match at workbook precision; 11/15 structured + warnings. Z remains unchanged and authoritative. +- Optoelectronic support `log10(P*Q)`: 15/15 pass against R and AA. +- Thickness support + `exp(-((mean(valid T1:T4)-650.0)/250.0)^2)`: 15/15 pass against Y and AB. +- Thickness uses the unrounded valid T1:T4 mean, excludes `T anom`, normalizes + blank/whitespace/NBSP cells to missing, and has no `0.5` exponent factor. +- Missing/non-finite score errors: 0. + +The known uniformity discrepancy is recorded in the config, score table, run +manifest, and debug status. It remains a production blocker. + +## 6. Control and grid handling + +- Control identity: supplied only by the ignored private configuration +- Primary-model inclusion: yes; all 15 R0 observations train the debug GP. +- Measurement provenance assumption: outcomes were measured in the current + campaign; only the recipe was literature-derived. +- Observed-only exception: supplied only by the ignored private configuration. +- On-grid observed conditions: 14 +- Off-grid observed conditions: 1 +- Control value changed/snapped: no +- Search grid changed: no +- New candidates all finite, bounded, unique, and exactly on-grid: yes + +Strict Step 2A grid conversion still rejects the control. The adapter partitions +the row before grid-index exclusion, but includes all normalized observations in +GP fitting and distance diagnostics. + +## 7. GP and R1 debug run + +- Training rows: 15 +- Model: one CPU `SingleTaskGP` per direct score with internal `Standardize(m=1)` +- Observed Pareto count: 6 +- Current fixed-reference hypervolume: `1.709278134536184` +- Candidate pool: 10,000 accepted from 10,000 draws; no duplicate, avoid, or + constraint rejections in the sampled pool +- Beta/kappa: `4.0 / 2.0` +- Posterior samples: 256 +- Local radius: 0.25 +- Hard within-batch minimum: 0.15 +- Selected unique conditions: exactly 5, each with positive UCB-HVI +- Observed selected-batch minimum normalized distance: `0.8493280824045194` +- Minimum selected-to-observed normalized distance: `0.887172581912801` +- Complete full-study runtime: `73.89 s` on CPU + +Training-posterior diagnostics are near-interpolating (`R^2 > 0.99998` for each +objective). These are not cross-validation scores and must not be read as evidence +of out-of-sample accuracy with only 15 observations. + +Ignored debug bundle: + +```text +local_outputs/d2d_step2b_debug/baseline_seed73/ +``` + +Artifact provenance hashes were recorded in the ignored local audit bundle and +are intentionally not reproduced in tracked documentation. + +The tracked handoff deliberately does not reproduce private candidate recipes. + +## 8. Replicate worklist + +- Unique R1 conditions: 5 +- Replicates per condition: 3 +- Physical execution rows: 15 +- Candidate IDs: `R1-C01` through `R1-C05` +- Replicate numbers: 1, 2, 3 +- Inputs are identical within every replicate group. +- Measurement/final-score fields remain blank. +- Replicate aggregation records condition mean, sample standard deviation, count, + standard error, completeness, and source execution IDs. +- One-row standard deviation/SEM remain missing rather than zero; input mismatch + inside a group fails. + +The synthetic-only R2 boundary aggregates five triplicate R1 groups, combines +them with 15 R0 condition observations (20 total), and returns exactly three +on-grid qLogNEHVI conditions in tests. Its returned metadata is explicitly +synthetic/test-only and not approved for experiment. No real R2 batch was +generated. + +## 9. Sensitivity and control ablation + +All 13 requested baseline/one-factor/control runs completed without relaxing a +rule, but the debug batch is not robust enough for experimental approval. Pool +randomness and Monte Carlo randomness are independently seeded; the two pool-seed +comparisons below keep the MC seed fixed at 73. + +| Comparison | Exact overlap with baseline | +|---|---:| +| beta 1.0 | 5/5 | +| beta 9.0 | 2/5 | +| pool 5,000 | 1/5 | +| pool 20,000 | 4/5 | +| pool seed 137 | 0/5 | +| pool seed 911 | 0/5 | +| radius 0.15 | 5/5 | +| radius 0.35 | 3/5 | +| hard batch distance 0.10 | 5/5 | +| hard batch distance 0.20 | 5/5 | +| posterior samples 128 | 5/5 | +| control excluded | 0/5 | + +Control exclusion changed all five selections. Its mean absolute posterior change +on the baseline candidate locations was `0.3748446` across the three raw score +dimensions. The observed Pareto set changed from Samples +`[1, 4, 6, 9, 10, 15]` to `[4, 6, 9, 10, 15]`; the removed control was Pareto +nondominated in the primary fit. Alternative random candidate-pool seeds also +changed all selections. The computational pipeline is working, but the current +15-point campaign fit and finite random pool produce a configuration-sensitive +proposal. + +The summary has 13 run-level rows. `sensitivity_candidates_long.csv` records all +65 selected rows with exact inputs, prediction means/stds, raw/log/final +acquisition values, full pairwise-distance rows, nearest-observed distances, +boundaries, settings, and runtimes. `control_ablation.csv` records five aligned +baseline-candidate prediction comparisons and Pareto diagnostics. All are ignored, +watermarked debug artifacts. + +## 10. Notebook update + +- Added `notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb` from + the supplied download. +- Inserted `2.5 Calculate and Validate D2D Scores` between sections 2 and 3. +- Uses package score helpers and explicit Z/AA/AB name selection. +- Includes the exact D2D no-half-factor thickness equation. +- Replaced personal paths with repository-relative/configurable paths. +- Replaced unavailable legacy acquisition/model APIs in the active path. +- Retired the undefined legacy global-distance sandbox cells and contradictory + PCE/stability/log-nEHVI/reference-point guidance. +- Candidate generation is opt-in and routes through the guarded adapter. +- Notebook diagnostics and the guarded candidate bundle use separate output + directories, so diagnostic plots cannot make the adapter destination nonempty. +- All notebook execution counts and outputs are cleared. +- Programmatic notebook smoke tests pass. + +## 11. Tests and quality checks + +```text +Step 2A pre-checkpoint: 170 passed, 20 dependency warnings +Step 2B focused suite: 94 passed, 68 dependency warnings +Final complete suite: 251 passed, 74 dependency warnings +Black direct API check: PASS (14 changed/new Python files) +Python compilation: PASS +pip check: PASS +git diff --check: PASS +untracked whitespace check: PASS (13 files) +debug script --help: PASS +private/generated status: ignored and untracked +``` + +Warnings are from the pinned third-party Matplotlib/PyParsing stack and the +BoTorch/NumPy array-compatibility path. No project warning or test failure was +reported. + +## 12. Files changed + +| File | Purpose | +|---|---| +| `README.md` | Document the guarded Step 2B command and debug boundary. | +| `configs/d2d_step2b_debug.yaml` | Freeze the debug-only v3, objective, control, grid, replicate, and acquisition contract. | +| `src/mobo_kit/workbook_schema.py` | Recognize and audit exact v3 structure plus explicitly configured observed-only exceptions. | +| `src/mobo_kit/candidate_diagnostics.py` | Support visible, metadata-backed debug watermarks on every generated candidate plot. | +| `src/mobo_kit/d2d_scores.py` | Compute and structurally validate D2D support scores without overwriting supplied objectives. | +| `src/mobo_kit/d2d_campaign.py` | Validate config, read workbook rows, prepare control-aware training data, and handle replicates. | +| `src/mobo_kit/d2d_step2b_debug.py` | Run the read-only R1 debug proposal, diagnostics, sensitivity study, and artifact bundle. | +| `src/mobo_kit/d2d_r2_test.py` | Expose only a synthetic-test future R2 qLogNEHVI boundary. | +| `examples/d2d_step2b_debug.py` | Provide the local one-command debug entry point. | +| `notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb` | Add the supplied research notebook with the resolved Section 2.5 and guarded APIs. | +| `tests/test_workbook_schema.py` | Add sanitized v3/profile/immutability/error tests and update the optional local audit. | +| `tests/test_candidate_diagnostics.py` | Verify headless plot generation and embedded debug watermark metadata. | +| `tests/test_d2d_scores.py` | Test equations, missing semantics, tolerances, and severity policy. | +| `tests/test_d2d_campaign.py` | Test the pinned config/grid, objective order, control partition, row metadata, strict triplicates, and 20-condition aggregation. | +| `tests/test_d2d_step2b_debug.py` | Test fail-before-output, safe output containment, independent seeds, deterministic batch, detailed artifacts, watermarks, and workbook integrity. | +| `tests/test_d2d_r2_test.py` | Test synthetic triplicate aggregation, pool-coordinate consistency, test-only status, and exact three-condition qLogNEHVI. | +| `tests/test_d2d_notebook.py` | Test executable thickness helpers, section order, resolved narrative, active APIs, portability, and cleared output. | +| `docs/STEP2B_DEBUG_HANDOFF.md` | This handoff and production-blocker record. | + +## 13. Deviations and local issues + +1. The pack allowed the reference point to be chosen; its resolved + `[-0.01, -10.0, -0.01]` value was used exactly. +2. The workbook contains formulas only in R2:R3; R4:R16 are cached/static numeric + checks. Validation accepts either formulas with numeric cached values or static + numeric checks. +3. The supplied notebook required more than a single inserted cell because its + active path referenced unavailable APIs, undefined variables, and personal + paths. The research notes were retained where safe, but the legacy sandbox was + visibly retired. +4. Black's normal Windows worker CLI left non-responsive worker processes after + reporting completion. The final formatting/check used Black's direct serial + API, passed, and the task-owned stale workers were terminated. + +## 14. Remaining blockers before experimental R1 + +- Correct or explicitly approve the 11 inconsistent uniformity scores. +- Confirm the configured control's measurement provenance through the private + campaign record. +- Review the strong control-ablation and random-pool-seed sensitivity. +- Choose and freeze a reviewed pool construction/seed and acquisition configuration. +- Review the five-condition proposal with experimental and computational owners. +- Approve a production provenance/config record; both approval flags remain false. +- Define the final workbook write-back/interface and signing/trust policy. + +## 15. Recommended next step + +Hold a bounded computational/experimental review of control provenance and the +seed/pool sensitivity. After resolving the uniformity values, freeze the reviewed +configuration and rerun one production-gated dry run for sign-off. Do not fabricate +from the current debug bundle. diff --git a/docs/STEP2C_ROBUSTNESS_HANDOFF.md b/docs/STEP2C_ROBUSTNESS_HANDOFF.md new file mode 100644 index 0000000..0fb13fd --- /dev/null +++ b/docs/STEP2C_ROBUSTNESS_HANDOFF.md @@ -0,0 +1,298 @@ +# Step 2C Robustness Handoff + +## 1. Baseline and review status + +- Step 1 checkpoint: `648efd0c81631726afed91e493b143e66e5f25c1` +- Step 2A checkpoint: `749c9de` (`Implement D2D MOBO Step 2A computational core`) +- Step 2B checkpoint: `1c6a83a9dfd7e6ed5e69ce66765b8dc0fd8a86af` +- Step 2C implementation and read-only robustness evidence: complete for review + +## 2. Overall result + +- Implementation and read-only robustness study: **PASS** +- Full stable consensus criteria: **FAIL** +- Experimental approval: **false** +- Production approval: **false** +- Real R2 proposal: not run + +Step 2C completed the declared full study and produced a validated robust-region +bundle. It did not release an R1 consensus batch. This is the correct fail-closed +outcome: search convergence was strong, but only four regions met the required +study-family coverage and the GP validation remains poor. + +## 3. Source workbook + +- Input class: Git-ignored private campaign workbook +- Content identity and unchanged-file proof: verified before, during, after, and + at independent validation; exact identifiers are intentionally omitted +- Adapter/schema contract: verified locally; private workbook metadata is not + tracked +- Workbook modified: no +- Known Uniformity mismatch: retained as warning-only input for debugging and as + an experimental-approval blocker +- Control observation: retained in the primary model under the reviewed + off-grid policy; its exact override remains in the ignored private config + +No code path saves or writes back to this workbook. + +## 4. Objective and baseline contract + +The direct supplied scores in Z/AA/AB remain ordered as Uniformity, +Optoelectronic, and Thickness. All three use identity transforms and are +maximized. The fixed utility reference is `[-0.01, -10.0, -0.01]`. + +Step 2B was checkpointed before Step 2C. Step 2C intentionally replaces the +finite random-pool/MC-moment debug search with analytic identity moments, exact +nested Sobol prefixes, and deterministic grid refinement. The control inclusion, +score-source, input-grid, reference-point, and approval boundaries did not +change. + +## 5. Analytic identity moments + +- Primary API: `posterior_identity_moments` +- Full diagnostic comparison: 2,048 MC samples, seed 73, 2,048 candidates +- Maximum absolute mean difference: `0.00006279358632210741` +- Maximum absolute standard-deviation difference: `0.00036946014787431203` +- Exact selected overlap: 5/5 +- Regional matches within 0.15: 5/5 +- Analytic/MC debug gate: pass +- Observed runtime ratio, MC/analytic: `1.3323` +- Production Step 2C moment method: deterministic `analytic_identity` + +The per-objective comparison tolerance is +`max(0.02, 0.02 * max(observed_range, 1.0))`. Monte Carlo remains diagnostic; +it does not drive the full search. + +## 6. Nested Sobol pools + +Primary accepted prefixes were exact and nested: + +| Seed | Accepted size | Draws | Duplicate/avoid/constraint rejects | Prefix SHA-256 | +|---:|---:|---:|---:|---| +| 73 | 16,384 | 16,384 | 0/0/0 | `76B069D765949EDF045E8ACDBA0866EB27125FF84CB2AC8A3F874E38B9CBC3B2` | +| 73 | 32,768 | 32,768 | 0/0/0 | `896A8483D182BEC14584149F657E7C9EADE07C3207A4CAB28F877EB7E3CE6833` | +| 73 | 65,536 | 65,536 | 0/0/0 | `84ABB5185246D20336CB90BD1E32836E8C0EDCF73AA5FAF288353D97770B9EB7` | +| 73 | 131,072 | 131,072 | 0/0/0 | `848B418477DEF6DF9FF04287F4FA902AF8FE15490AFD6AC2801069799FA33DBC` | +| 137 | 131,072 | 131,072 | 0/0/0 | `4AEE3ECE6A3F181B4D0A1E7A1A4F6F6E4613C4C2B52E258DCC5820CB901665D9` | +| 911 | 131,072 | 131,072 | 0/0/0 | `F8CC95A722C309FB1B1BB21C62B1F8DC9AFBBD60E8227FB283E15C0B52BE9BD2` | + +The full Cartesian grid was never materialized. Observed on-grid recipes were +excluded by exact grid index; the off-grid control remained a continuous GP and +distance reference only. + +## 7. Local refinement and convergence + +- Full settings: 64 anchors per selection step, at most 10 sweeps, + `1e-10` improvement tolerance, all allowed coordinate values +- Mean nested-batch acquisition gain: `1.73963296072043` +- Gain range across the four prefixes: `1.6522526527608` to + `1.86429022721892` +- Baseline anchor summaries: 358 +- Distinct baseline converged optima: 21 +- Accepted baseline coordinate moves: 2,845 +- Study candidate rows: 75 +- Invalid grid, bounds, hard-distance, or duplicate rows: 0 + +All adjacent nested-prefix comparisons from 16,384 onward matched 5/5 within +0.15 with mean matched distance 0.0. Both secondary scramble seeds reproduced +the same five refined optima exactly; their acquisition regrets were numerical +roundoff (`-6.1e-14` and `2.4e-13`). Refinement is deterministic in regression +tests and never accepts a score decrease. + +## 8. Model validation + +Exact leave-one-out results for all 15 observations: + +| Variant | Objective | MAE | RMSE | R2 | 68% coverage | 95% coverage | Mean NLPD | Max abs. standardized residual | +|---|---|---:|---:|---:|---:|---:|---:|---:| +| default | Uniformity | 0.2062 | 0.2472 | -0.9102 | 0.400 | 0.533 | 4.096 | 9.420 | +| default | Optoelectronic | 0.3853 | 0.5266 | -0.2473 | 0.467 | 0.667 | 2.821 | 6.571 | +| default | Thickness | 0.3559 | 0.4299 | -0.4578 | 0.467 | 0.600 | 1.354 | 3.935 | +| conservative | Uniformity | 0.2045 | 0.2478 | -0.9188 | 0.467 | 0.667 | 2.384 | 7.851 | +| conservative | Optoelectronic | 0.3988 | 0.5414 | -0.3185 | 0.467 | 0.667 | 3.088 | 6.384 | +| conservative | Thickness | 0.3604 | 0.4328 | -0.4779 | 0.467 | 0.600 | 1.316 | 3.804 | + +Every one of the 96 full/fold objective fits had at least one normalized ARD +lengthscale at or above the declared flatness threshold of 10.0. One fit also +had a lengthscale at or below 0.05. Flat flags were most frequent for `time_2` +(90/96), `precur_conc` (89/96), `anneal_time` (87/96), `anti_vol` (86/96), and +`anti_time` (83/96). + +All 96 fits also reached their configured likelihood-noise lower bound: all 48 +default fits were at `0.001`, and all 48 conservative fits were at `0.01`. This +is a major calibration diagnostic, not evidence that either noise policy is +validated. + +The 576 recorded optimizer warnings are the same NumPy `copy=` deprecation +warning, not fit failures. During the full influence study, the console also +showed one GPyTorch numerical warning that clamped a tiny negative posterior +variance to `1e-10`. That console-observed warning was not suppressed, but it is +not present in the aggregate warning CSV. + +For both model variants, all five selected Optoelectronic posterior means were +above the observed maximum. No selected posterior mean violated the declared +Uniformity or Thickness bounds. These diagnostics reinforce that the models are +not ready to justify fabrication. + +## 9. Observation influence + +All 15 exact omission runs completed on the common accepted pool. The five most +influential observations were Samples 1, 9, 4, 6, and 10. + +Sample 1 was rank 1/15 at the 100th percentile. Omitting it produced 0/5 exact +or 0.15-regional matches, mean matched batch distance `2.1692`, prediction-mean +change `0.6183`, normalized acquisition-rank change `0.3345`, and mean absolute +hyperparameter log-ratio `2.8595`. Its omission removed Sample 1 from the +observed Pareto set (Pareto Jaccard `0.8333`). The control remains included in +the primary model; this result is a sensitivity warning, not an automatic +exclusion decision. + +## 10. Bounded-utility and qLogNEHVI compatibility + +The declared clip-UCB and unbounded-UCB policies had 0/5 exact and 0/5 regional +correspondence. The clip policy affected 77,286 of 131,072 pool rows (58.965%) +and all five selected rows; the largest selected-coordinate clip was `0.04911`. +The bounded and unbounded acquisition sums were `7.6932` and `13.2048`, +respectively. These policy-specific HVI sums should not be interpreted as a +shared-scale regret. + +Training targets were not mutated. A real synthetic BoTorch qLogNEHVI test now +uses the same bounded posterior-sample objective with a fixed reference and +seed. No real R2 qLogNEHVI proposal was generated. + +## 11. Local-penalty study + +| Variant | Comparator | Exact/regional overlap | Minimum batch distance | Acquisition sacrifice | Interpretation | +|---|---|---:|---:|---:|---| +| no soft, no hard spacing | self | 5/5 | 0.2828 | 0 | Base optima already separated | +| no soft, hard 0.15 | no soft, no hard | 5/5 | 0.2828 | 0 | Hard spacing inactive | +| radius 0.15 | no soft, hard 0.15 | 4/5 | 0.5657 | `2.21e-7` | Material diversity change | +| radius 0.25 | no soft, hard 0.15 | 3/5 | 0.7141 | 0.00378 | Material diversity change | +| radius 0.35 | no soft, hard 0.15 | 3/5 | 1.0000 | 0.11789 | Material diversity change | + +The primary radius 0.25 is active in the converged full search even though its +mean penalty factor remains close to one (`0.9865`). The classification uses +regional and pairwise-distance changes, not score attenuation alone. Hard +spacing did not relax. + +## 12. Beta, model, and boundary robustness + +Beta 1 and beta 9 each had 0/5 correspondence with beta 4 after refinement. The +conservative model retained 4/5 exact clustered regions, whereas unbounded UCB +retained 0/5. + +The baseline batch remains strongly boundary-seeking: + +- `speed_2`: 5/5 at the upper bound; +- `precur_conc` and `precur_vol`: 5/5 at the upper bound; +- `anneal_temp`, `anti_vol`, and `anti_time`: 5/5 at the lower bound; +- `time_2` and `anneal_time`: 2/5 at each lower and upper bound. + +Lower and upper rates and enrichment are reported separately. The combination +of beta sensitivity, bound-policy sensitivity, flat ARD directions, and boundary +seeking remains a fabrication blocker. + +## 13. Robust regions and shortlist + +- Clustering: deterministic complete-link agglomerative clustering +- Primary normalized distance threshold: 0.15 +- Sensitivity thresholds: 0.10 and 0.20 +- Robust regions: 18 +- Debug shortlist: 12 medoids +- Regions covering one, two, and four non-baseline families: 9, 5, and 4 +- Regions meeting the required at-least-three-family criterion: 4 + +The tracked handoff contains no private recipe rows. Those remain only in the +ignored local artifacts. + +## 14. Conditional consensus result + +- `r1_consensus_debug_batch.csv`: not created +- Full-mode eligibility: pass +- Largest-two nested regional match gate: pass, 5/5 +- Largest-two mean-distance gate: pass, 0.0 +- Five family-qualified regions: fail, only four available +- Exact five consensus candidates: fail, only four eligible medoids + +All candidate-row gates are consequently false because no exact five-row set +exists; this does not mean the global debug/approval flags were weakened. Both +approval flags remain false throughout. The explicit reason artifact is +`r1_no_stable_batch_reason.json`. + +## 15. Local and public artifacts + +Full campaign evidence remains below the ignored `local_outputs/` boundary and +must not be committed or shared. Private runs label their complete archive +`*_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip`. The sibling `.zip` is now a separate, +strictly allowlisted public summary containing aggregate tables and sanitized +metadata only. Input-specific artifact hashes are intentionally not tracked. + +## 16. Verification + +- Sanitized synthetic end-to-end suite: 2 passed, including the real workbook + adapter, strict GP/search orchestration, resolved-config provenance, + rollback-safe publication, and validation of the aggregate-only public ZIP +- Ignored private Step 2B/2C configuration compatibility smoke: pass +- Historical pre-sanitization private fast/full runs: pass with 46 validated + artifacts; these private bundles were not published +- Historical full-mode runtime: `1508.32 s` on CPU +- Complete pytest: 427 passed, 2 expected private-input skips, 270 dependency + warnings (`48.67 s` with single-threaded numerical libraries) +- Black: pass on all 67 changed/new Python files +- Python compilation: pass +- `pip check`: pass (`No broken requirements found`) +- `git diff --check`: pass +- Private/generated outputs: confirmed ignored by `.gitignore` + +Largest full-mode phase runtimes were observation influence `707.34 s`, +secondary/model/bound/beta studies `413.08 s`, primary moments and scores +`285.17 s`, and nested refinement `64.39 s`. + +## 17. Files changed + +| Area | Files/purpose | +|---|---| +| Config/docs | `configs/d2d_step2c_debug.yaml`, this handoff, the Step 2C method document, and README command/safety guidance | +| Entry points | Private fast/full runner and separate sanitized synthetic-CI example | +| Search | Analytic UCB-HVI moments, nested Sobol prefixes, regional batch comparison, and deterministic discrete refinement | +| Models | Strict GP variants, exact LOOCV, hyperparameter/flatness diagnostics, and all-observation influence | +| Policies | Bounded UCB, bounded qLogNEHVI objective compatibility, beta and five-variant penalty studies | +| Stabilization | Complete-link robust regions, diverse shortlist, exact consensus gates, and lower/upper boundary diagnostics | +| Safety | Strict artifact schemas/provenance, CSV evidence-chain validation, watermark and format checks, exact consensus/no-stable linkage, aggregate-only public ZIP validation, and rollback-safe public/private publication | +| Tests | Unit, regression, artifact-negative, real qLogNEHVI, pending-row, normalization, plotting, and sanitized end-to-end coverage | + +## 18. Deviations and limitations + +1. A separate sanitized synthetic runner was added because campaign fast mode is + correctly pinned to the private workbook and therefore cannot be portable CI. +2. The qLogNEHVI work is compatibility testing only; Step 2C deliberately did + not generate a real R2 proposal. +3. Region-threshold sensitivity exports counts at 0.10 and 0.20; full membership + rows are exported for the declared primary 0.15 threshold. +4. One console-observed posterior-variance clamp warning occurred in the full + study and is retained as a model-stability limitation; it was not captured in + the aggregate warning CSV. + +## 19. Remaining blockers before experimental R1 + +- Correct or explicitly approve the 11 inconsistent supplied Uniformity scores. +- Confirm Sample 1 outcome provenance with the experimental team. +- Resolve poor LOOCV calibration/accuracy, all 96 likelihood-noise fits reaching + their configured floor, and the pervasive flat ARD directions. +- Review the posterior-variance numerical warning. +- Choose and justify the bounded-versus-unbounded utility policy. +- Review the strong beta, control, and boundary sensitivity. +- Obtain at least five regions satisfying the declared family-coverage gate, or + explicitly redesign that gate through a new reviewed specification. +- Freeze a production configuration and approval record. +- Define workbook write-back/interface and signing policy only after candidate + sign-off. + +## 20. Recommended next step + +Do not fabricate from either Step 2B or Step 2C artifacts. First correct/approve +the Uniformity scores and confirm the control provenance. Then perform a focused +model review—kernel/priors, noise treatment, objective policy, and whether more +R0 information is required—before rerunning this same fail-closed full study. +Proceed to Excel button/write-back integration only after a five-row consensus +set and experimental sign-off both exist. diff --git a/examples/d2d_step2a_benchmark.py b/examples/d2d_step2a_benchmark.py new file mode 100644 index 0000000..15631b0 --- /dev/null +++ b/examples/d2d_step2a_benchmark.py @@ -0,0 +1,70 @@ +"""Optional TEST_ONLY 10,000-point CPU scoring benchmark for Step 2A.""" + +from __future__ import annotations + +import json +from pathlib import Path +import sys +from time import perf_counter + +import numpy as np + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +for source in (REPOSITORY_ROOT / "src", Path(__file__).resolve().parent): + if str(source) not in sys.path: + sys.path.insert(0, str(source)) + +from d2d_step2a_synthetic import build_test_only_d2d_design # noqa: E402 +from mobo_kit.candidate_pool import sample_discrete_candidate_pool # noqa: E402 +from mobo_kit.ucb_hvi import score_ucb_hvi_from_moments # noqa: E402 + + +def main() -> int: + design = build_test_only_d2d_design() + pool_started = perf_counter() + pool = sample_discrete_candidate_pool(design, 10_000, seed=99173) + pool_seconds = perf_counter() - pool_started + + X = pool.X_norm + utility_mean = np.column_stack( + [ + 0.15 + 0.65 * X[:, 0], + 0.10 + 0.70 * X[:, 4], + np.exp(-0.5 * ((X[:, 7] - 0.55) / 0.22) ** 2), + ] + ) + utility_std = 0.02 + 0.04 * np.column_stack([X[:, 1], 1.0 - X[:, 5], X[:, 9]]) + observed = np.array( + [ + [0.35, 0.75, 0.60], + [0.55, 0.55, 0.80], + [0.75, 0.35, 0.65], + ] + ) + score_started = perf_counter() + result = score_ucb_hvi_from_moments( + utility_mean, + utility_std, + observed, + np.array([-0.05, -0.05, -0.05]), + beta=1.0, + chunk_size=512, + objective_contract_version="TEST_ONLY-benchmark-utilities-v1", + ) + score_seconds = perf_counter() - score_started + report = { + "status": "TEST_ONLY_BENCHMARK", + "production_candidate_generation": "NOT_RUN", + "pool_size": pool.size, + "pool_sampling_seconds": round(pool_seconds, 4), + "ucb_hvi_scoring_seconds": round(score_seconds, 4), + "positive_hvi_count": int(np.count_nonzero(result.base_score > 0)), + "full_cartesian_grid_materialized": False, + } + print(json.dumps(report, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/d2d_step2a_synthetic.py b/examples/d2d_step2a_synthetic.py new file mode 100644 index 0000000..e17fca0 --- /dev/null +++ b/examples/d2d_step2a_synthetic.py @@ -0,0 +1,464 @@ +"""CPU-only TEST_ONLY exercise of the Step 2A computational core. + +This script never reads a campaign workbook and never authorizes or emits real +D2D R1/R2 recipes. All observations, objective settings, reference values, and +acquisition parameters below are synthetic fixtures for software verification. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from importlib.metadata import version as package_version +import json +import math +from pathlib import Path +import platform +import sys +from time import perf_counter +from typing import Any, Mapping + +import numpy as np +import torch +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from gpytorch.mlls import ExactMarginalLogLikelihood + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = REPOSITORY_ROOT / "src" +if str(SOURCE_ROOT) not in sys.path: + sys.path.insert(0, str(SOURCE_ROOT)) + +from mobo_kit.batch_selection import LocalPenalizationConfig # noqa: E402 +from mobo_kit.candidate_diagnostics import ( # noqa: E402 + plot_candidate_pca, + plot_distance_heatmap, + plot_parallel_coordinates, + plot_selection_scores, + summarize_candidate_batch, +) +from mobo_kit.candidate_pool import ( # noqa: E402 + CandidatePool, + sample_discrete_candidate_pool, +) +from mobo_kit.design import InputSpec, build_design # noqa: E402 +from mobo_kit.objectives import ( # noqa: E402 + ConfiguredMCMultiOutputObjective, + ObjectiveSpec, + ObjectiveTransform, +) +from mobo_kit.qlognehvi_batch import ( # noqa: E402 + propose_qlognehvi_penalized_batch, +) +from mobo_kit.ucb_hvi import propose_ucb_hvi_batch # noqa: E402 + + +TEST_ONLY_REFERENCE_POINT_UTILITY = np.array([-0.05, -0.05, -0.05]) +TEST_ONLY_SEED = 20260724 + + +@dataclass(frozen=True) +class SyntheticRunSummary: + elapsed_seconds: float + observed_count: int + ucb_pool_size: int + qlognehvi_pool_size: int + ucb_selected_pool_indices: np.ndarray + qlognehvi_selected_pool_indices: np.ndarray + ucb_minimum_distance: float + qlognehvi_minimum_distance: float + ucb_metadata: dict[str, Any] + qlognehvi_metadata: dict[str, Any] + plot_paths: tuple[Path, ...] + + +def _json_safe(value: Any) -> Any: + """Convert nested NumPy/PyTorch metadata to strict JSON-compatible values.""" + if isinstance(value, Mapping): + return {str(key): _json_safe(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_json_safe(item) for item in value] + if isinstance(value, np.ndarray): + return _json_safe(value.tolist()) + if isinstance(value, np.generic): + return _json_safe(value.item()) + if isinstance(value, torch.Tensor): + return _json_safe(value.detach().cpu().tolist()) + if isinstance(value, Path): + return str(value) + if isinstance(value, float) and not math.isfinite(value): + return None + return value + + +def _selection_table(selection: Any) -> list[dict[str, Any]]: + """Return all score, penalty, distance, and order fields for a batch.""" + return [ + { + "order": step.order, + "pool_index": step.pool_index, + "base_score": step.base_score, + "base_log_score": step.base_log_score, + "penalty_factor": step.penalty_factor, + "log_penalty": step.log_penalty, + "penalized_log_score": step.penalized_log_score, + "nearest_selected_distance_before": (step.nearest_selected_distance_before), + "nearest_observed_distance": step.nearest_observed_distance, + } + for step in selection.steps + ] + + +def build_test_only_d2d_design(): + """Build the exact ten-dimensional D2D grid without campaign semantics.""" + return build_design( + [ + InputSpec("speed_1", 1000, 6000, 500, unit="rpm"), + InputSpec("time_1", 5, 50, 5, unit="s"), + InputSpec("speed_2", 0, 5000, 500, unit="rpm"), + InputSpec("time_2", 10, 60, 5, unit="s"), + InputSpec("precur_conc", 1.0, 2.0, 0.05, unit="M"), + InputSpec("precur_vol", 40, 200, 10, unit="uL"), + InputSpec("anneal_temp", 100, 185, 5, unit="C"), + InputSpec("anneal_time", 10, 60, 5, unit="min"), + InputSpec("anti_vol", 100, 200, 5, unit="uL"), + InputSpec("anti_time", 9, 25, 2, unit="s"), + ] + ) + + +def _synthetic_raw_outcomes(X_norm: np.ndarray) -> np.ndarray: + """Create two bounded maximize outcomes and one raw target outcome.""" + first = 0.15 + 0.35 * X_norm[:, 0] + 0.25 * X_norm[:, 4] + 0.15 * X_norm[:, 7] + second = ( + 0.10 + 0.30 * X_norm[:, 1] + 0.20 * (1.0 - X_norm[:, 2]) + 0.25 * X_norm[:, 8] + ) + raw_target = 430.0 + 420.0 * (0.55 * X_norm[:, 3] + 0.45 * X_norm[:, 6]) + return np.column_stack([first, second, raw_target]) + + +def _fit_synthetic_model(train_X: torch.Tensor, train_Y: torch.Tensor) -> ModelListGP: + models = [] + for objective_index in range(train_Y.shape[1]): + output = train_Y[:, objective_index : objective_index + 1] + output_variance = output.var(correction=0).clamp_min(1e-8) + # Standardize(m=1) scales this to a stable TEST_ONLY variance of 1e-4. + known_noise = torch.full_like(output, float(output_variance * 1e-4)) + model = SingleTaskGP( + train_X, + output, + train_Yvar=known_noise, + outcome_transform=Standardize(m=1), + ) + mll = ExactMarginalLogLikelihood(model.likelihood, model) + fit_gpytorch_mll( + mll, + optimizer_kwargs={"options": {"maxiter": 25, "ftol": 1e-7}}, + ) + model.eval() + models.append(model) + return ModelListGP(*models) + + +def _objective_contract() -> ObjectiveTransform: + return ObjectiveTransform( + [ + ObjectiveSpec("synthetic_maximize_1", "maximize", "identity"), + ObjectiveSpec("synthetic_maximize_2", "maximize", "identity"), + ObjectiveSpec( + "synthetic_target", + "target", + "gaussian_target", + target=650.0, + sigma=120.0, + ), + ], + version="TEST_ONLY-synthetic-objectives-v1", + ) + + +def _plots_for_method( + method: str, + output_dir: Path, + design, + observed_norm: np.ndarray, + pool: CandidatePool, + selection, +) -> tuple[Path, ...]: + steps = selection.steps + return ( + plot_candidate_pca( + observed_norm, + selection.X_norm, + output_dir / f"{method}_pca.png", + pool_norm=pool.X_norm, + seed=TEST_ONLY_SEED, + ), + plot_parallel_coordinates( + selection.X_norm, + design.names, + output_dir / f"{method}_parallel_coordinates.png", + ), + plot_distance_heatmap( + selection.X_norm, output_dir / f"{method}_distance_heatmap.png" + ), + plot_selection_scores( + [step.order for step in steps], + [step.base_log_score for step in steps], + [step.penalized_log_score for step in steps], + output_dir / f"{method}_selection_scores.png", + ), + ) + + +def run_synthetic_step2a( + output_dir: str | Path, + *, + ucb_pool_size: int = 128, + qlognehvi_pool_size: int = 96, + posterior_samples: int = 32, + qlognehvi_samples: int = 16, +) -> SyntheticRunSummary: + """Fit synthetic GPs and deterministically propose TEST_ONLY 5/3 batches.""" + started = perf_counter() + output = Path(output_dir) + output.mkdir(parents=True, exist_ok=True) + torch.manual_seed(TEST_ONLY_SEED) + np.random.seed(TEST_ONLY_SEED) + + design = build_test_only_d2d_design() + observed_pool = sample_discrete_candidate_pool(design, 15, seed=TEST_ONLY_SEED) + train_X = torch.as_tensor(observed_pool.X_norm, dtype=torch.double) + train_Y = torch.as_tensor( + _synthetic_raw_outcomes(observed_pool.X_norm), dtype=torch.double + ) + model = _fit_synthetic_model(train_X, train_Y) + transform = _objective_contract() + mc_objective = ConfiguredMCMultiOutputObjective(transform) + + ucb_pool = sample_discrete_candidate_pool( + design, + ucb_pool_size, + seed=TEST_ONLY_SEED + 1, + observed_phys=observed_pool.X_phys, + ) + local_config = LocalPenalizationConfig( + radius=0.65, + min_batch_distance=0.20, + min_observed_distance=0.08, + ) + ucb_kwargs = dict( + q=5, + beta=1.0, + local_penalization_config=local_config, + observed_pending_norm=observed_pool.X_norm, + positive_score_tolerance=1e-12, + mc_samples=posterior_samples, + seed=TEST_ONLY_SEED + 2, + posterior_chunk_size=32, + hvi_chunk_size=64, + ) + ucb = propose_ucb_hvi_batch( + ucb_pool, + model, + train_Y, + transform, + TEST_ONLY_REFERENCE_POINT_UTILITY, + **ucb_kwargs, + ) + ucb_repeat = propose_ucb_hvi_batch( + ucb_pool, + model, + train_Y, + transform, + TEST_ONLY_REFERENCE_POINT_UTILITY, + **ucb_kwargs, + ) + if not np.array_equal( + ucb.selection.selected_pool_indices, + ucb_repeat.selection.selected_pool_indices, + ): + raise RuntimeError("TEST_ONLY UCB-HVI rerun was not deterministic.") + + qlog_pool = sample_discrete_candidate_pool( + design, + qlognehvi_pool_size, + seed=TEST_ONLY_SEED + 3, + observed_phys=observed_pool.X_phys, + pending_phys=ucb.selection.X_phys, + ) + ucb_pending = torch.as_tensor(ucb.selection.X_norm, dtype=torch.double) + qlog_kwargs = dict( + q=3, + local_penalization_config=local_config, + X_pending_norm=ucb_pending, + mc_samples=qlognehvi_samples, + seed=TEST_ONLY_SEED + 4, + chunk_size=32, + ) + qlog = propose_qlognehvi_penalized_batch( + qlog_pool, + model, + train_X, + mc_objective, + TEST_ONLY_REFERENCE_POINT_UTILITY, + **qlog_kwargs, + ) + qlog_repeat = propose_qlognehvi_penalized_batch( + qlog_pool, + model, + train_X, + mc_objective, + TEST_ONLY_REFERENCE_POINT_UTILITY, + **qlog_kwargs, + ) + if not np.array_equal( + qlog.selection.selected_pool_indices, + qlog_repeat.selection.selected_pool_indices, + ): + raise RuntimeError("TEST_ONLY qLogNEHVI rerun was not deterministic.") + + ucb_diagnostics = summarize_candidate_batch( + ucb.selection.X_norm, + observed_pending_norm=observed_pool.X_norm, + X_phys=ucb.selection.X_phys, + design=design, + metadata=ucb.metadata, + ) + qlog_diagnostics = summarize_candidate_batch( + qlog.selection.X_norm, + observed_pending_norm=np.vstack([observed_pool.X_norm, ucb.selection.X_norm]), + X_phys=qlog.selection.X_phys, + design=design, + metadata=qlog.metadata, + ) + if not np.all(ucb_diagnostics.grid_valid_rows) or not np.all( + qlog_diagnostics.grid_valid_rows + ): + raise RuntimeError("Synthetic proposal contained an off-grid row.") + if ucb_diagnostics.duplicate_row_pairs or qlog_diagnostics.duplicate_row_pairs: + raise RuntimeError("Synthetic proposal contained a duplicate row.") + + plot_paths = _plots_for_method( + "ucb_hvi", + output, + design, + observed_pool.X_norm, + ucb_pool, + ucb.selection, + ) + _plots_for_method( + "qlognehvi", + output, + design, + observed_pool.X_norm, + qlog_pool, + qlog.selection, + ) + elapsed = perf_counter() - started + summary = SyntheticRunSummary( + elapsed_seconds=elapsed, + observed_count=observed_pool.size, + ucb_pool_size=ucb_pool.size, + qlognehvi_pool_size=qlog_pool.size, + ucb_selected_pool_indices=ucb.selection.selected_pool_indices, + qlognehvi_selected_pool_indices=qlog.selection.selected_pool_indices, + ucb_minimum_distance=float(ucb_diagnostics.minimum_within_batch_distance), + qlognehvi_minimum_distance=float( + qlog_diagnostics.minimum_within_batch_distance + ), + ucb_metadata=dict(ucb_diagnostics.metadata), + qlognehvi_metadata=dict(qlog_diagnostics.metadata), + plot_paths=plot_paths, + ) + ucb_selected = ucb.selection.selected_pool_indices + ucb_utility_diagnostics = [ + { + "pool_index": int(pool_index), + "utility_mean": ucb.scoring.utility_mean[pool_index], + "utility_std": ucb.scoring.utility_std[pool_index], + "utility_ucb": ucb.scoring.utility_ucb[pool_index], + "raw_hvi": ucb.scoring.base_score[pool_index], + } + for pool_index in ucb_selected + ] + report = { + "status": "TEST_ONLY_SYNTHETIC_PASS", + "production_candidate_generation": "NOT_RUN", + "elapsed_seconds": round(elapsed, 3), + "observed_count": summary.observed_count, + "seeds": { + "global": TEST_ONLY_SEED, + "observed_pool": TEST_ONLY_SEED, + "ucb_pool": TEST_ONLY_SEED + 1, + "ucb_posterior": TEST_ONLY_SEED + 2, + "qlognehvi_pool": TEST_ONLY_SEED + 3, + "qlognehvi_mc": TEST_ONLY_SEED + 4, + }, + "ucb_hvi": { + "pool_size": summary.ucb_pool_size, + "batch_size": len(summary.ucb_selected_pool_indices), + "selected_pool_indices": summary.ucb_selected_pool_indices.tolist(), + "minimum_normalized_distance": summary.ucb_minimum_distance, + "metadata": summary.ucb_metadata, + "selection_steps": _selection_table(ucb.selection), + "selected_utility_diagnostics": ucb_utility_diagnostics, + "nearest_observed_pending_distance": ( + ucb_diagnostics.nearest_observed_pending_distance + ), + "boundary_flags": ucb_diagnostics.boundary_flags, + "grid_valid_rows": ucb_diagnostics.grid_valid_rows, + }, + "qlognehvi": { + "pool_size": summary.qlognehvi_pool_size, + "batch_size": len(summary.qlognehvi_selected_pool_indices), + "selected_pool_indices": summary.qlognehvi_selected_pool_indices.tolist(), + "minimum_normalized_distance": summary.qlognehvi_minimum_distance, + "metadata": summary.qlognehvi_metadata, + "selection_steps": _selection_table(qlog.selection), + "pending_counts_by_selection_step": [ + item.pending_count for item in qlog.score_history + ], + "nearest_observed_pending_distance": ( + qlog_diagnostics.nearest_observed_pending_distance + ), + "boundary_flags": qlog_diagnostics.boundary_flags, + "grid_valid_rows": qlog_diagnostics.grid_valid_rows, + }, + "objective_contract_version": transform.version, + "reference_point": { + "space": "TEST_ONLY transformed utility", + "value": TEST_ONLY_REFERENCE_POINT_UTILITY.tolist(), + }, + "runtime": { + "platform": platform.platform(), + "python": platform.python_version(), + "numpy": package_version("numpy"), + "torch": package_version("torch"), + "botorch": package_version("botorch"), + "gpytorch": package_version("gpytorch"), + "device": "cpu", + }, + "plots": [path.name for path in plot_paths], + } + (output / "synthetic_summary.json").write_text( + json.dumps(_json_safe(report), indent=2, allow_nan=False), encoding="utf-8" + ) + return summary + + +def main() -> int: + output = REPOSITORY_ROOT / "local_outputs" / "step2a_synthetic" + summary = run_synthetic_step2a(output) + print( + "TEST_ONLY synthetic Step 2A PASS: " + f"R1={len(summary.ucb_selected_pool_indices)}, " + f"R2={len(summary.qlognehvi_selected_pool_indices)}, " + f"elapsed={summary.elapsed_seconds:.2f}s, output={output}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/d2d_step2b_debug.py b/examples/d2d_step2b_debug.py new file mode 100644 index 0000000..3340fd0 --- /dev/null +++ b/examples/d2d_step2b_debug.py @@ -0,0 +1,61 @@ +"""Run the read-only D2D Step 2B algorithm-debug adapter from the repo root.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from mobo_kit.d2d_step2b_debug import run_d2d_step2b_debug + + +def main() -> int: + repository_root = Path(__file__).resolve().parents[1] + parser = argparse.ArgumentParser( + description=( + "Generate a watermarked D2D R1 debug bundle. This never approves or " + "writes an experimental worklist to Excel." + ) + ) + parser.add_argument( + "--workbook", + type=Path, + required=True, + help="Explicit path to the ignored private campaign workbook.", + ) + parser.add_argument( + "--config", + type=Path, + required=True, + help="Explicit path to the matching ignored private debug configuration.", + ) + parser.add_argument( + "--output", + type=Path, + default=( + repository_root / "local_outputs" / "d2d_step2b_debug" / "baseline_seed73" + ), + ) + parser.add_argument("--overwrite", action="store_true") + parser.add_argument( + "--skip-sensitivity", + action="store_true", + help="Run only the configured baseline (useful for a quick local smoke test).", + ) + args = parser.parse_args() + result = run_d2d_step2b_debug( + args.workbook, + args.config, + args.output, + overwrite=args.overwrite, + run_sensitivity=not args.skip_sensitivity, + ) + print("DEBUG ONLY - NOT APPROVED FOR EXPERIMENT") + print(f"Output directory: {result.output_dir}") + print(f"Unique R1 conditions: {len(result.candidates_unique)}") + print(f"Replicate execution rows: {len(result.replicate_worklist)}") + print(f"Sensitivity rows: {len(result.sensitivity_summary)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/d2d_step2c_robustness.py b/examples/d2d_step2c_robustness.py new file mode 100644 index 0000000..086710e --- /dev/null +++ b/examples/d2d_step2c_robustness.py @@ -0,0 +1,89 @@ +"""Run the read-only D2D Step 2C robustness study from the repository root.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from mobo_kit.d2d_step2c_robustness import run_d2d_step2c_robustness + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Generate a watermarked, read-only D2D R1 robustness audit. " + "This command never writes the workbook or approves fabrication." + ) + ) + parser.add_argument( + "--workbook", + type=Path, + required=True, + help="Path to the Git-ignored campaign workbook.", + ) + parser.add_argument( + "--config", + type=Path, + required=True, + help="Path to the matching Git-ignored private Step 2C configuration.", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help=( + "Run directory below local_outputs/d2d_step2c_robustness. " + "Defaults to _seed73." + ), + ) + parser.add_argument( + "--mode", + choices=("fast", "full"), + default="full", + help="Use fast for a smoke audit or full for the declared Step 2C study.", + ) + parser.add_argument("--overwrite", action="store_true") + parser.add_argument( + "--no-portable-zip", + action="store_true", + help="Skip the ignored portable ZIP while retaining the validated directory.", + ) + return parser + + +def main() -> int: + repository_root = Path(__file__).resolve().parents[1] + args = build_parser().parse_args() + output = args.output or ( + repository_root + / "local_outputs" + / "d2d_step2c_robustness" + / f"{args.mode}_seed73" + ) + result = run_d2d_step2c_robustness( + args.workbook, + args.config, + output, + mode=args.mode, + overwrite=args.overwrite, + create_portable_zip=not args.no_portable_zip, + ) + print("DEBUG ONLY - NOT APPROVED FOR EXPERIMENT") + print(f"Mode: {result.mode}") + print(f"Output directory: {result.output_dir}") + print(f"Robust regions: {len(result.robust_regions)}") + print(f"Debug shortlist rows: {len(result.shortlist)}") + print(f"Consensus criteria passed: {result.consensus.passed}") + print( + "Sample 1 influence rank: " + f"{result.run_manifest['sample_1_influence_rank']} " + f"({result.run_manifest['sample_1_influence_percentile']:.1f} percentile)" + ) + print(f"Validated artifacts: {len(result.artifact_hashes)}") + print("Workbook writeback performed: false") + print("Real R2 proposal generated: false") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/d2d_step2c_synthetic_ci.py b/examples/d2d_step2c_synthetic_ci.py new file mode 100644 index 0000000..dbd10f2 --- /dev/null +++ b/examples/d2d_step2c_synthetic_ci.py @@ -0,0 +1,55 @@ +"""Run the sanitized synthetic Step 2C fast end-to-end fixture.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from mobo_kit.d2d_step2c_synthetic import run_synthetic_step2c_fast_ci + + +def build_parser(repository_root: Path) -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Run Step 2C fast orchestration on generated sanitized data. " + "This is CI/debug coverage only and cannot approve an experiment." + ) + ) + parser.add_argument( + "--config", + type=Path, + default=repository_root / "configs" / "d2d_step2c_debug.yaml", + ) + parser.add_argument( + "--output", + type=Path, + default=( + repository_root / "local_outputs" / "d2d_step2c_robustness" / "synthetic_ci" + ), + ) + parser.add_argument("--overwrite", action="store_true") + parser.add_argument("--no-portable-zip", action="store_true") + return parser + + +def main() -> int: + repository_root = Path(__file__).resolve().parents[1] + args = build_parser(repository_root).parse_args() + result = run_synthetic_step2c_fast_ci( + args.config, + args.output, + overwrite=args.overwrite, + create_portable_zip=not args.no_portable_zip, + ) + print("DEBUG ONLY - NOT APPROVED FOR EXPERIMENT") + print("Input data: sanitized synthetic CI fixture") + print(f"Output directory: {result.output_dir}") + print(f"Validated artifacts: {len(result.artifact_hashes)}") + print(f"Consensus criteria passed: {result.consensus.passed}") + print("Private campaign workbook read: false") + print("Experimental approval enabled: false") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb b/notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb new file mode 100644 index 0000000..5ec30f9 --- /dev/null +++ b/notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb @@ -0,0 +1,1013 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "84796167", + "metadata": {}, + "source": [ + "# D2D MOBO Debug Notebook\n", + "\n", + "This notebook is a research/debug interface for the package implementation. The\n", + "authoritative Step 2B run is `mobo_kit.d2d_step2b_debug.run_d2d_step2b_debug`,\n", + "which reads the completed workbook without modifying it and watermarks every\n", + "candidate artifact as **DEBUG ONLY - NOT APPROVED FOR EXPERIMENT**.\n", + "\n", + "## Outline\n", + "- 0. Setup and portable paths\n", + "- 1. Read the completed R0 workbook\n", + "- 2. Normalize inputs\n", + "- 2.5. Calculate and validate D2D scores\n", + "- 3. Fit GP models\n", + "- 4-5. Optional model diagnostics\n", + "- 6. Run the guarded Step 2B debug adapter\n", + "- 7-9. Review the adapter's watermarked outputs" + ] + }, + { + "cell_type": "markdown", + "id": "c22b92d9", + "metadata": {}, + "source": [ + "## 0. Setup & Imports\n" + ] + }, + { + "cell_type": "markdown", + "id": "24c9601d", + "metadata": {}, + "source": [ + "**Imports** Bring in libraries for data handling, modeling, and plotting. \n", + "If CUDA isn't available, the CPU path still works for a workshop-scale demo.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69eaaa20", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Imports & setup ---\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "# Core\n", + "import os, sys\n", + "from pathlib import Path\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "parent_dir = os.path.dirname(os.getcwd())\n", + "if parent_dir not in sys.path:\n", + " sys.path.insert(0, parent_dir)\n", + "\n", + "# Torch / BoTorch / GPyTorch\n", + "import torch\n", + "from mobo_kit.utils import csv_to_config, split_XY, get_objective_names, np_to_torch\n", + "from mobo_kit.design import build_design_from_config\n", + "\n", + "from mobo_kit.lhs import lhs_dataframe, lhs_dataframe_optimized\n", + "from mobo_kit.constraints import constraints_from_config\n", + "\n", + "from mobo_kit.plotting import plot_distribution, plot_correlation_heatmap, plot_PCA\n", + "\n", + "from mobo_kit.data import x_normalizer_np\n", + "\n", + "from mobo_kit.models import fit_gp_models\n", + "from mobo_kit.constraints import check_clausius_clapeyron_np\n", + "from mobo_kit.lhs import lhs_dataframe\n", + "#from mobo_kit.plotting import plot_pareto, plot_hypervolume_trajectory\n", + "\n", + "# if torch.cuda.is_available():\n", + "# device = torch.device(\"cuda\")\n", + "# elif torch.backends.mps.is_available():\n", + "# device = torch.device(\"mps\")\n", + "# else:\n", + "device = torch.device(\"cpu\")\n", + "\n", + "print(f\"Using device: {device}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "18716c1e", + "metadata": {}, + "source": [ + "## Portable workbook, configuration, and debug-output paths" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83c88e1e", + "metadata": {}, + "outputs": [], + "source": [ + "# Supply private inputs explicitly through environment variables; no private path is tracked.\n", + "import os\n", + "\n", + "cwd = Path.cwd().resolve()\n", + "repo_root = cwd.parent if cwd.name == \"notebooks\" else cwd\n", + "private_workbook = os.environ.get(\"MOBO_KIT_D2D_PRIVATE_WORKBOOK\")\n", + "private_config = os.environ.get(\"MOBO_KIT_D2D_PRIVATE_CONFIG\")\n", + "if not private_workbook or not private_config:\n", + " raise RuntimeError(\n", + " \"Set MOBO_KIT_D2D_PRIVATE_WORKBOOK and MOBO_KIT_D2D_PRIVATE_CONFIG \"\n", + " \"to explicit ignored local files before running campaign cells.\"\n", + " )\n", + "workbook_path = Path(private_workbook).expanduser().resolve()\n", + "config_path = Path(private_config).expanduser().resolve()\n", + "save_path = repo_root / \"local_outputs\" / \"d2d_step2b_debug\" / \"notebook_seed73\"\n", + "diagnostics_path = repo_root / \"local_outputs\" / \"d2d_step2b_debug\" / \"notebook_diagnostics_seed73\"\n", + "diagnostics_path.mkdir(parents=True, exist_ok=True)\n", + "\n", + "print(\"Workbook:\", workbook_path)\n", + "print(\"Config:\", config_path)\n", + "print(\"Notebook diagnostics:\", diagnostics_path)\n", + "print(\"Debug bundle (created only when enabled):\", save_path)" + ] + }, + { + "cell_type": "markdown", + "id": "155a087c", + "metadata": {}, + "source": [ + "## 1. Read the completed R0 workbook" + ] + }, + { + "cell_type": "markdown", + "id": "8f41089d", + "metadata": {}, + "source": [ + "The Step 2B reader selects the 15 numeric `Sample number` rows from the\n", + "35-column v3 workbook. Inputs are mapped from B:K, the final objectives are\n", + "selected by their exact Z/AA/AB headers, and Stability plus AD:AI are excluded.\n", + "The source workbook is opened read-only and is never saved." + ] + }, + { + "cell_type": "markdown", + "id": "459b2ca0", + "metadata": {}, + "source": [ + "### Completed R0 data\n", + "\n", + "Control identities and any observed-only off-grid exceptions are supplied only\n", + "by the ignored private configuration. They are retained for GP training and\n", + "distance diagnostics, while every new candidate remains on the configured grid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9525ed10", + "metadata": {}, + "outputs": [], + "source": [ + "from mobo_kit.d2d_campaign import (\n", + " D2D_INPUT_COLUMNS,\n", + " D2D_OBJECTIVE_COLUMNS,\n", + " load_d2d_debug_config,\n", + " load_d2d_workbook_frame,\n", + " prepare_d2d_training_data,\n", + ")\n", + "\n", + "resolved_config = load_d2d_debug_config(config_path)\n", + "design = resolved_config.design\n", + "data_rows, workbook_audit = load_d2d_workbook_frame(\n", + " workbook_path,\n", + " expected_profile=resolved_config.workbook_profile,\n", + " expected_sample_ids=resolved_config.expected_sample_ids,\n", + " allowed_input_exceptions=resolved_config.off_grid_exceptions,\n", + ")\n", + "training_data = prepare_d2d_training_data(data_rows, resolved_config)\n", + "\n", + "X_df = pd.DataFrame(training_data.X_phys_all, columns=D2D_INPUT_COLUMNS)\n", + "Y_df = pd.DataFrame(training_data.Y_objectives, columns=D2D_OBJECTIVE_COLUMNS)\n", + "obj_names = list(D2D_OBJECTIVE_COLUMNS)\n", + "\n", + "print(\"Workbook profile:\", workbook_audit.profile)\n", + "print(\"Rows:\", len(data_rows), \"Inputs:\", X_df.shape, \"Objectives:\", Y_df.shape)\n", + "display(X_df.head())\n", + "display(Y_df.head())" + ] + }, + { + "cell_type": "markdown", + "id": "e719d6af", + "metadata": {}, + "source": [ + "### Optional R0 LHS reference (not used by Step 2B)\n", + "\n", + "The completed workbook already contains R0 observations, so the guarded debug\n", + "path does not regenerate LHS points. Set the flag below only for a separate,\n", + "input-only design exercise." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c94998c8", + "metadata": {}, + "outputs": [], + "source": [ + "run_optional_lhs = False\n", + "row_constraints = []\n", + "max_abs_corr = 0.32\n", + "lhs_batch_size = 14\n", + "\n", + "if run_optional_lhs:\n", + " lhs_df = lhs_dataframe_optimized(\n", + " design,\n", + " n=lhs_batch_size,\n", + " seed=42,\n", + " row_constraints=row_constraints,\n", + " max_abs_corr=max_abs_corr,\n", + " verbose=True,\n", + " )\n", + " display(lhs_df)\n", + "else:\n", + " lhs_df = pd.DataFrame(columns=design.names)\n", + " print(\"Skipping LHS because completed R0 data are available.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb722d98", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "# 1. Pull the pre-calculated choices from the Design object\n", + "# 'var_array' was created by build_design using the 'make_linspace' function\n", + "summary_data = []\n", + "for i, name in enumerate(design.names):\n", + " # .size gives the number of valid grid points for this parameter\n", + " n_choices = design.var_array[i].size \n", + " summary_data.append({\n", + " \"Parameter\": name,\n", + " \"Choices\": n_choices,\n", + " \"Min\": design.lowers[i],\n", + " \"Max\": design.uppers[i],\n", + " \"Step\": design.steps[i]\n", + " })\n", + "\n", + "# 2. Display the table\n", + "space_summary_df = pd.DataFrame(summary_data)\n", + "display(space_summary_df)\n", + "\n", + "# 3. Calculate Total Conditions (Product of all choices)\n", + "total_conditions = np.prod(space_summary_df[\"Choices\"].values)\n", + "print(f\"\\nTOTAL UNIQUE CONDITIONS IN SEARCH SPACE: {total_conditions:,.0f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f62c8fdd", + "metadata": {}, + "outputs": [], + "source": [ + "if run_optional_lhs:\n", + " round_worklist_path = diagnostics_path / \"Round_0_Input_Only_Reference.csv\"\n", + " round_worklist_path.parent.mkdir(parents=True, exist_ok=True)\n", + " input_reference = lhs_df.copy()\n", + " for objective in D2D_OBJECTIVE_COLUMNS:\n", + " input_reference[objective] = \"\"\n", + " input_reference.to_csv(round_worklist_path, index=False)\n", + " print(\"Input-only reference saved to:\", round_worklist_path)" + ] + }, + { + "cell_type": "markdown", + "id": "e15c0ed2", + "metadata": {}, + "source": [ + "### Visualize data distribution\n" + ] + }, + { + "cell_type": "markdown", + "id": "eb7ce27a", + "metadata": {}, + "source": [ + "#### Standardize inputs to visualize correlation (NE addition 260407)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ffd61aa", + "metadata": {}, + "outputs": [], + "source": [ + "if run_optional_lhs:\n", + " scaler = StandardScaler()\n", + " lhs_standardized = scaler.fit_transform(lhs_df)\n", + " lhs_df_std = pd.DataFrame(lhs_standardized, columns=lhs_df.columns)\n", + "else:\n", + " lhs_df_std = pd.DataFrame(columns=design.names)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd4e947c", + "metadata": {}, + "outputs": [], + "source": [ + "if run_optional_lhs:\n", + " _ = plot_distribution(lhs_df, title=\"LHS Feature Distributions\", save=diagnostics_path / \"lhs_dist.png\")\n", + " _ = plot_correlation_heatmap(lhs_df_std, title=\"LHS Pearson Correlation\", save=diagnostics_path / \"lhs_corr.png\")\n", + " _ = plot_PCA(lhs_df_std, title=\"LHS PCA (2D)\", save=diagnostics_path / \"lhs_pca.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "ddd5fa6f", + "metadata": {}, + "source": [ + "## 2. Normalize Data and Create Tensors/Arrays\n" + ] + }, + { + "cell_type": "markdown", + "id": "3cf47096", + "metadata": {}, + "source": [ + "**Inputs.** Scale to [0, 1] per dimension. \n", + "\n", + "This helps GP hyperparameters learn sensibly. There are utility functions to normalize and standardize the outputs, but this is currently handled internally in `fit_gp_models`.\n", + "\n", + "**Tensors.** Some Botorch functions use tensor objects rather than arrays and vice versa. It's good to have both!\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf09e4d7", + "metadata": {}, + "outputs": [], + "source": [ + "# Inputs -> fixed [0, 1] bounds from the configured design.\n", + "X_np = X_df.to_numpy(dtype=float)\n", + "X_norm = x_normalizer_np(X_np, design)\n", + "X_t, _ = np_to_torch(X_norm, device=device, return_device=True)\n", + "print(\"Normalized input shape:\", X_t.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "d2d-score-contract", + "metadata": {}, + "source": [ + "## 2.5 Calculate and Validate D2D Scores\n", + "\n", + "The final BO objectives are Excel Z `Uniformity score`, AA\n", + "`Optoelectronic score`, and AB `Thickness score`. All three are maximized and\n", + "used directly; they are not clipped to `[0, 1]`. Experimental analysis owns the\n", + "final scores, while the support equations below are validation only. The known\n", + "uniformity inconsistency is a debug warning, and Stability is ignored.\n", + "\n", + "- Uniformity support: `Coverage * (1 - Uniformity) * Phase purity`\n", + "- Optoelectronic support: `log10(P * Q)`\n", + "- Thickness support: `exp(-((mean(valid T1:T4) - 650.0) / 250.0) ** 2)`\n", + "\n", + "The thickness equation has **no factor 0.5**, uses the unrounded valid T1:T4\n", + "mean, and excludes `T anom`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2d-score-imports", + "metadata": {}, + "outputs": [], + "source": [ + "from mobo_kit.d2d_scores import (\n", + " compute_thickness_average,\n", + " compute_thickness_score,\n", + " validate_supplied_d2d_scores,\n", + ")\n", + "\n", + "def normalize_missing_thickness_cell(value):\n", + " # Convert blank/whitespace/NBSP thickness cells to NaN.\n", + " if value is None:\n", + " return np.nan\n", + " if isinstance(value, str) and not value.replace(\"\\u00a0\", \" \").strip():\n", + " return np.nan\n", + " return value\n", + "\n", + "thickness_columns = [\"T1\", \"T2\", \"T3\", \"T4\"]\n", + "normalized_thickness = data_rows[thickness_columns].map(\n", + " normalize_missing_thickness_cell\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2d-thickness-score", + "metadata": {}, + "outputs": [], + "source": [ + "# Package helper implements exp(-((mean_t - 650.0) / 250.0) ** 2).\n", + "thickness_means = normalized_thickness.apply(\n", + " lambda row: compute_thickness_average(*row.tolist()), axis=1\n", + ")\n", + "calculated_thickness_scores = normalized_thickness.apply(\n", + " lambda row: compute_thickness_score(row.tolist(), target=650.0, scale=250.0),\n", + " axis=1,\n", + ")\n", + "display(\n", + " pd.DataFrame(\n", + " {\n", + " \"Sample number\": data_rows[\"Sample number\"],\n", + " \"unrounded T1:T4 mean\": thickness_means,\n", + " \"calculated thickness score\": calculated_thickness_scores,\n", + " \"supplied Thickness score\": data_rows[\"Thickness score\"],\n", + " }\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2d-score-validation", + "metadata": {}, + "outputs": [], + "source": [ + "score_validation = validate_supplied_d2d_scores(data_rows)\n", + "display(score_validation.frame)\n", + "print(\"Uniformity warnings:\", score_validation.uniformity_warning_count)\n", + "score_validation.raise_for_errors() # fatal for missing, opto, or thickness errors" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d2d-objective-selection", + "metadata": {}, + "outputs": [], + "source": [ + "objective_columns = [\n", + " \"Uniformity score\",\n", + " \"Optoelectronic score\",\n", + " \"Thickness score\",\n", + "]\n", + "Y = data_rows[objective_columns].astype(float)\n", + "Y_df = Y.copy()\n", + "Y_np = Y.to_numpy(dtype=float)\n", + "(X_t, Y_t), _ = np_to_torch(X_norm, Y_np, device=device, return_device=True)\n", + "obj_names = objective_columns\n", + "print(\"Tensor shapes:\", X_t.shape, Y_t.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "ee5a6060", + "metadata": {}, + "source": [ + "## 3. Fit Gaussian Process (GP) Models\n" + ] + }, + { + "cell_type": "markdown", + "id": "cef36573", + "metadata": {}, + "source": [ + "We fit one **Gaussian Process (GP)** for each authoritative final score: Excel Z\n", + "`Uniformity score`, AA `Optoelectronic score`, and AB `Thickness score`. The\n", + "three models preserve the direct score scales and the resolved optimization\n", + "direction is **max/max/max**. This gives an uncertainty-aware model of how each\n", + "final score varies with the process parameters.\n", + "\n", + "- **Defaults:** \n", + " - Kernel: `MaternKernel(nu=2.5, ard_num_dims=d)` (smooth, ARD per feature) \n", + " - Likelihood: `GaussianLikelihood` with an inferred homoskedastic noise level \n", + " (includes a small positive floor to prevent overfitting)\n", + "\n", + "- **Optional overrides:** \n", + " - You may pass your own **kernel function(s)** (e.g., RBF, Matern with different ν, etc.) \n", + " - You may pass **noise priors** (e.g., LogNormal or Gamma) to guide the noise estimate.\n", + "\n", + "This example shows passing custom priors and a kernel to demonstrate how it works, but you can omit them entirely to use the defaults.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb83dfde", + "metadata": {}, + "outputs": [], + "source": [ + "from mobo_kit.models import fit_gp_models\n", + "\n", + "# The model internally standardizes each output but returns posterior values on\n", + "# the original Z/AA/AB scales.\n", + "model = fit_gp_models(X_t, Y_t)" + ] + }, + { + "cell_type": "markdown", + "id": "b11f931a", + "metadata": {}, + "source": [ + "## 4. LOOCV Model Selection (optional)\n", + "\n", + "Instead of hand-choosing kernel functions and noise priors, you can let the repo \n", + "**automatically compare candidates** using **Leave-One-Out Cross-Validation (LOOCV)**.\n", + "\n", + "- The function `loocv_select_models` tries multiple `(kernel × noise)` combinations.\n", + "- By default, it uses:\n", + " - **Kernel options:** RBF, Matern(ν=0.5), Matern(ν=1.5), Matern(ν=2.5) \n", + " - **Noise priors:** `None` (free noise level) and `LogNormal(-4.0, 0.5)`\n", + "- For each fold (leave one point out), it re-fits the GP, predicts the held-out point, \n", + " and records **R²** and **RMSE**. \n", + "- After sweeping all combinations, it selects the best configuration per objective \n", + " and re-fits on the full dataset.\n", + "\n", + "**Pros:** \n", + "- removes guesswork.\n", + "- gives metrics for each option. \n", + "\n", + "**Cons:** \n", + "- expensive when you have many data points (since it re-fits N×(#kernels×#priors) times).\n", + "- poor fits when handling very small and noisy datasets and have convergence errors.\n", + "\n", + "You can skip this step if you are happy with the defaults from Step 3.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d88052f8", + "metadata": {}, + "outputs": [], + "source": [ + "# Optional LOOCV model selection. Disabled by default because it refits many GPs.\n", + "run_loocv = False\n", + "if run_loocv:\n", + " from mobo_kit.models import loocv_select_models\n", + " model_cv, results_df = loocv_select_models(\n", + " X_t,\n", + " Y_t,\n", + " objective_names=obj_names, # list of objective column names\n", + " device=X_t.device, # optional; inferred from X_t if omitted\n", + " )\n", + " display(results_df.sort_values([\"objective\", \"rmse\"]))\n", + "else:\n", + " results_df = pd.DataFrame()\n", + " model_cv = None\n", + " print(\"Skipping optional LOOCV. Set run_loocv = True to enable it.\")" + ] + }, + { + "cell_type": "markdown", + "id": "1d73d832", + "metadata": {}, + "source": [ + "### Display LOOCV Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "206e169a", + "metadata": {}, + "outputs": [], + "source": [ + "# Display fitted model details from the default model (in source code).\n", + "for i, gp in enumerate(model.models):\n", + " print(f\"--- Model {i+1} ({obj_names[i]}) ---\")\n", + " kernel = gp.covar_module.base_kernel\n", + " print(\"Kernel:\", type(kernel).__name__)\n", + " if hasattr(kernel, \"nu\"):\n", + " print(\"Matern nu:\", kernel.nu)\n", + " prior_type = type(getattr(gp.likelihood.noise_covar, \"noise_prior\", None)).__name__\n", + " print(\"Noise prior:\", prior_type)\n", + " print(\"Lengthscales:\", gp.covar_module.base_kernel.lengthscale.detach().cpu().numpy().flatten())\n", + " print(\"Outputscale:\", gp.covar_module.outputscale.item())\n", + " print(\"Noise:\", gp.likelihood.noise.item())\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "id": "91798dcb", + "metadata": {}, + "source": [ + "## 5. Posterior Predictions & Diagnostics\n" + ] + }, + { + "cell_type": "markdown", + "id": "bdaa215f", + "metadata": {}, + "source": [ + "With a fitted GP model (`model` from Step 3 or 4), we can now evaluate how well it explains the data. \n", + "This step does two things:\n", + "\n", + "1. **Posterior predictions:** \n", + " - Use `posterior_report(model, X_t)` to get the **predicted mean** and **uncertainty (std)** for each objective at the training points. \n", + " - These predictions are automatically converted back into the original units (because the model internally standardizes outputs).\n", + "\n", + "2. **Diagnostics:** \n", + " - `plot_parity_np` shows **predicted vs. true values** for each objective, with optional error bars from the GP’s predictive uncertainty. \n", + " - `plot_shap` estimates **feature importance** (mean absolute SHAP values per input dimension), so you can see which process parameters most influence each objective. \n", + " - `compute_metrics` calculates **R²** and **RMSE**, and can also return per-point residuals and z-scores to help check model fit quality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc509011", + "metadata": {}, + "outputs": [], + "source": [ + "from mobo_kit.models import posterior_report\n", + "from mobo_kit.plotting import plot_parity_np, plot_shap\n", + "from mobo_kit.metrics import compute_metrics\n", + "pred_mean, pred_std = posterior_report(model, X_t)\n", + "fig_train, metrics_train = plot_parity_np(\n", + " Y_np,\n", + " pred_mean,\n", + " pred_std,\n", + " objective_names=obj_names,\n", + " save=str(diagnostics_path / \"parity_train.png\"),\n", + ")\n", + "run_shap = False\n", + "if run_shap:\n", + " # plot_shap calls the fitted GP posterior directly, so explain the same\n", + " # normalized input space used to train the model.\n", + " fig_shap = plot_shap(\n", + " design,\n", + " X_norm,\n", + " model,\n", + " objective_names=obj_names,\n", + " save=str(diagnostics_path / \"shap.png\"),\n", + " )\n", + "else:\n", + " fig_shap = None\n", + " print(\"Skipping SHAP by default to keep memory usage low. Set run_shap = True to enable it.\")\n", + "metrics_df = compute_metrics(\n", + " true_Y=Y_np,\n", + " pred_mean=pred_mean,\n", + " pred_std=pred_std,\n", + " objective_names=obj_names,\n", + " add_residuals=True,\n", + " add_zscores=True,\n", + ")\n", + "display(metrics_df)" + ] + }, + { + "cell_type": "markdown", + "id": "4ebd9ea3", + "metadata": {}, + "source": [ + "## 6. R1 UCB-HVI Debug Candidate Generation\n", + "\n", + "The guarded Step 2B path ranks grid-valid conditions with **R1 UCB-HVI** and\n", + "shared local penalization. The optimization contract is Excel Z `Uniformity\n", + "score`, AA `Optoelectronic score`, and AB `Thickness score`, with\n", + "**max/max/max** directions and the fixed reference point\n", + "`[-0.01, -10.0, -0.01]`. The reference point is a campaign constant; it is not\n", + "derived from the observed worst values.\n", + "\n", + "The adapter selects exactly five unique conditions, expands each condition to\n", + "three replicate rows, and watermarks every artifact **DEBUG ONLY - NOT APPROVED\n", + "FOR EXPERIMENT**. The batch is for algorithm diagnostics, not experimental\n", + "execution.\n", + "\n", + "### Pareto front and hypervolume diagnostics\n", + "- The **Pareto front** consists of non-dominated points: no other point is strictly better across *all* objectives. \n", + "- The **hypervolume** measures the space dominated by the Pareto front relative to the fixed campaign reference point. \n", + "- As we add better candidates, the hypervolume increases." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e54ea0a6", + "metadata": {}, + "outputs": [], + "source": [ + "from botorch.utils.multi_objective.pareto import is_non_dominated\n", + "from mobo_kit.d2d_campaign import (\n", + " D2D_REFERENCE_POINT_UTILITY,\n", + " build_d2d_objective_transform,\n", + ")\n", + "from mobo_kit.metrics import compute_ref_pareto_hv\n", + "\n", + "directions = [\"max\", \"max\", \"max\"]\n", + "objective_transform = build_d2d_objective_transform()\n", + "Y_t_transformed = objective_transform(Y_t) # identity; values remain Z/AA/AB\n", + "transformed_ref_point_t = torch.as_tensor(\n", + " D2D_REFERENCE_POINT_UTILITY,\n", + " dtype=Y_t.dtype,\n", + " device=Y_t.device,\n", + ")\n", + "if not torch.all(Y_t_transformed > transformed_ref_point_t):\n", + " raise ValueError(\"Every observed score must strictly dominate the fixed reference point.\")\n", + "\n", + "_, pareto_Y_t_transformed, hv_val = compute_ref_pareto_hv(\n", + " Y_t_transformed,\n", + " D2D_REFERENCE_POINT_UTILITY.copy(),\n", + ")\n", + "pareto_mask = is_non_dominated(Y_t_transformed)\n", + "pareto_Y_t_real_world = Y_t[pareto_mask]\n", + "print(\"Fixed reference point:\", transformed_ref_point_t.cpu().numpy())\n", + "print(\"Observed hypervolume:\", hv_val)\n", + "print(\"Pareto count:\", pareto_Y_t_real_world.shape[0])" + ] + }, + { + "cell_type": "markdown", + "id": "c5b6f156", + "metadata": {}, + "source": [ + "### Guarded R1 algorithm-debug adapter\n", + "\n", + "The package adapter calls the Step 2A discrete pool, UCB-HVI, and shared local\n", + "penalization APIs directly. It selects five unique conditions and expands them\n", + "to three physical replicates only after acquisition. It never invokes the\n", + "legacy raw-objective proposal path and never writes to Excel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d929ef1", + "metadata": {}, + "outputs": [], + "source": [ + "from mobo_kit.d2d_step2b_debug import run_d2d_step2b_debug\n", + "\n", + "# Deliberate opt-in: candidate files are always watermarked debug-only.\n", + "run_campaign_debug = False\n", + "debug_result = None\n", + "if run_campaign_debug:\n", + " debug_result = run_d2d_step2b_debug(\n", + " workbook_path,\n", + " config_path,\n", + " save_path,\n", + " overwrite=False,\n", + " run_sensitivity=True,\n", + " )\n", + " display(debug_result.candidates_unique)\n", + "else:\n", + " print(\"Debug proposal not run. Set run_campaign_debug=True to create the guarded bundle.\")" + ] + }, + { + "cell_type": "markdown", + "id": "81c1e56f", + "metadata": {}, + "source": [ + "### Legacy global-distance sandbox retired\n", + "\n", + "The earlier experimental cells used APIs and constraints that are not part of\n", + "the active package. The Step 2A shared normalized-distance selector and the\n", + "Step 2B adapter above are authoritative." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69f2e4d5", + "metadata": {}, + "outputs": [], + "source": [ + "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", + "pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1eeac466", + "metadata": {}, + "outputs": [], + "source": [ + "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", + "pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35dff6b8", + "metadata": {}, + "outputs": [], + "source": [ + "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", + "pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3ac1ffc", + "metadata": {}, + "outputs": [], + "source": [ + "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", + "pass" + ] + }, + { + "cell_type": "markdown", + "id": "fb3517fb", + "metadata": {}, + "source": [ + "### Review the guarded adapter result" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "626723c3", + "metadata": {}, + "outputs": [], + "source": [ + "if debug_result is not None:\n", + " X_next_df = debug_result.candidates_unique.copy()\n", + " display(X_next_df)\n", + "else:\n", + " X_next_df = pd.DataFrame()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b3dfb4c", + "metadata": {}, + "outputs": [], + "source": [ + "if debug_result is not None:\n", + " print(\"Debug artifacts:\", debug_result.output_dir)\n", + " print(\"Unique conditions:\", len(debug_result.candidates_unique))\n", + " print(\"Replicate rows:\", len(debug_result.replicate_worklist))" + ] + }, + { + "cell_type": "markdown", + "id": "d94c5fd8", + "metadata": {}, + "source": [ + "## 7. Review R1 UCB-HVI Debug Diagnostics\n", + "\n", + "When `debug_result` is available, review the guarded adapter outputs directly:\n", + "\n", + "1. `debug_result.candidates_unique` contains five grid-valid conditions in\n", + " physical units, plus predicted means and standard deviations for Z\n", + " `Uniformity score`, AA `Optoelectronic score`, and AB `Thickness score`.\n", + "2. `base_ucb_hvi`, `penalty_factor`, and `penalized_log_score` document the R1\n", + " UCB-HVI ranking and local-penalization effect. These values are R1 algorithm\n", + " diagnostics from the guarded adapter.\n", + "3. `debug_result.model_diagnostics` and the saved sensitivity summary expose\n", + " model-fit and batch-robustness checks.\n", + "\n", + "The objectives remain direct-score **max/max/max**, the reference point remains\n", + "fixed at `[-0.01, -10.0, -0.01]`, and every displayed or saved candidate is\n", + "**DEBUG ONLY - NOT APPROVED FOR EXPERIMENT**.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f7cb13e", + "metadata": {}, + "outputs": [], + "source": [ + "if debug_result is not None:\n", + " display(debug_result.candidates_unique)\n", + " display(debug_result.model_diagnostics)\n", + "else:\n", + " print(\"Run the guarded adapter to review predictions and diagnostics.\")" + ] + }, + { + "cell_type": "markdown", + "id": "a6014a48", + "metadata": {}, + "source": [ + "## 8. Visualize Your Progress (Work in Progress...)\n", + "\n", + "We track learning progress by plotting the **hypervolume (HV)** after each batch of **observed** results. \n", + "As you collect new data and the Pareto front improves, HV should **monotonically increase** (or stay flat).\n", + "\n", + "**Key points**\n", + "- Use **observed outcomes** (not predictions). \n", + "- Keep the campaign reference point fixed at `[-0.01, -10.0, -0.01]`; do not recompute it from observed minima or worst values. \n", + "- Compute HV on the **cumulative dataset** up through each batch." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc1980a3", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Current observed hypervolume: {hv_val:.6f}\")\n", + "if debug_result is not None:\n", + " print(\"Candidate and sensitivity diagnostics are stored in the guarded bundle.\")" + ] + }, + { + "cell_type": "markdown", + "id": "1230729a", + "metadata": {}, + "source": [ + "This plot shows the **current Pareto front** (green) in 3D objective space and an optional **predicted next batch**. \n", + "\n", + "The **reference point** is drawn as a red star." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99ec1de4", + "metadata": {}, + "outputs": [], + "source": [ + "# All three displayed axes are higher-is-better final scores.\n", + "directions = [\"max\", \"max\", \"max\"]\n", + "print(\"Pareto objectives:\", list(D2D_OBJECTIVE_COLUMNS))\n", + "print(\"Reference point:\", D2D_REFERENCE_POINT_UTILITY.tolist())" + ] + }, + { + "cell_type": "markdown", + "id": "e07896ef", + "metadata": {}, + "source": [ + "## 9. Save Outputs\n" + ] + }, + { + "cell_type": "markdown", + "id": "0e533308", + "metadata": {}, + "source": [ + "Only the guarded adapter writes Step 2B outputs, under `local_outputs`. It never\n", + "modifies the source workbook, and every candidate artifact remains **DEBUG ONLY\n", + "- NOT APPROVED FOR EXPERIMENT**.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9292a4fc", + "metadata": {}, + "outputs": [], + "source": [ + "if debug_result is not None:\n", + " print(\"The adapter has already saved the watermarked candidate and replicate CSV files.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "88ff7744", + "metadata": {}, + "outputs": [], + "source": [ + "if debug_result is not None:\n", + " display(debug_result.replicate_worklist)\n", + "else:\n", + " print(\"No outputs written; the debug adapter remains opt-in.\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mobo-fom", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.20" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index d37e35b..07ce24b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,39 +32,34 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Chemistry", "Topic :: Scientific/Engineering :: Physics", ] -requires-python = ">=3.10" +requires-python = ">=3.11,<3.13" dependencies = [ - "numpy>=1.21.0", - "pandas>=1.3.0", - "scikit-learn>=1.0.0", - "matplotlib>=3.5.0", - "seaborn>=0.11.0", - "pyyaml>=6.0", - "scipy>=1.7.0", - "torch>=1.12.0", - "gpytorch>=1.8.0", - "botorch>=0.8.0", - "emukit>=0.4.10", - "shap>=0.41.0", - "pyDOE>=0.3.8", - "jupyter>=1.0.0", - "ipykernel>=6.0.0", + "numpy>=1.26,<3", + "pandas>=2.1,<3", + "scikit-learn>=1.4,<2", + "matplotlib>=3.8,<4", + "seaborn>=0.13,<1", + "pyyaml>=6.0,<7", + "scipy>=1.12,<2", + "torch>=2.8,<2.9", + "gpytorch==1.14", + "botorch==0.15.1", + "shap>=0.46,<1", + "openpyxl>=3.1,<4", + "pillow>=10,<13", ] [project.optional-dependencies] dev = [ - "pytest>=6.0", - "pytest-cov>=2.0", - "black>=22.0", - "flake8>=4.0", - "mypy>=0.950", + "pytest>=8.2,<9", + "pytest-cov>=5,<7", + "black>=24,<26", ] jupyter = [ "jupyter>=1.0.0", @@ -72,14 +67,14 @@ jupyter = [ "notebook>=6.0.0", ] all = [ - "mobo-fom[dev,jupyter]", + "mobo-kit[dev,jupyter]", ] [project.urls] -Homepage = "https://github.com/PV-Lab/MOBO-FOM" -Repository = "https://github.com/PV-Lab/MOBO-FOM" -Documentation = "https://github.com/PV-Lab/MOBO-FOM#readme" -"Bug Tracker" = "https://github.com/PV-Lab/MOBO-FOM/issues" +Homepage = "https://github.com/PV-Lab/MOBO-Kit" +Repository = "https://github.com/PV-Lab/MOBO-Kit" +Documentation = "https://github.com/PV-Lab/MOBO-Kit#readme" +"Bug Tracker" = "https://github.com/PV-Lab/MOBO-Kit/issues" [project.scripts] mobo-kit = "mobo_kit.cli:main" @@ -93,7 +88,7 @@ where = ["src"] [tool.black] line-length = 88 -target-version = ['py310'] +target-version = ['py311'] include = '\.pyi?$' extend-exclude = ''' /( @@ -115,9 +110,6 @@ python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "-v --tb=short" - -[tool.mypy] -python_version = "3.10" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true +markers = [ + "local_input: optional integration test requiring an ignored local_inputs artifact", +] diff --git a/requirements.txt b/requirements.txt index 045d123..56341a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,4 @@ -# Core scientific stack -numpy>=1.21.0 -pandas>=1.3.0 -scikit-learn>=1.0.0 -matplotlib>=3.5.0 -seaborn>=0.11.0 -pyyaml>=6.0 -scipy>=1.7.0 - -# PyTorch & probabilistic programming -torch>=1.12.0 -gpytorch>=1.8.0 -botorch>=0.8.0 - -# Experimental design / optimization -emukit>=0.4.10 - -# Optional: Jupyter for notebooks -jupyter>=1.0.0 -ipykernel>=6.0.0 - -# Additional dependencies -shap>=0.41.0 -pyDOE>=0.3.8 +# Reproducible runtime install from the repository root: +# python -m pip install -r requirements.txt +-c requirements/constraints.txt +. diff --git a/requirements/constraints.txt b/requirements/constraints.txt new file mode 100644 index 0000000..b81aaff --- /dev/null +++ b/requirements/constraints.txt @@ -0,0 +1,19 @@ +# Step 1 CPU-tested direct dependency stack. Keep these versions synchronized +# with pyproject.toml and docs/STEP1_HANDOFF.md. +numpy==2.2.6 +pandas==2.3.1 +scikit-learn==1.7.1 +matplotlib==3.10.3 +seaborn==0.13.2 +PyYAML==6.0.2 +scipy==1.16.0 +torch==2.8.0 +gpytorch==1.14 +botorch==0.15.1 +linear_operator==0.6 +shap==0.48.0 +openpyxl==3.1.5 +Pillow==12.3.0 +pytest==8.4.1 +pytest-cov==6.2.1 +black==25.1.0 diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..a5a17e7 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,4 @@ +# From the repository root: +# python -m pip install -r requirements/dev.txt +-c constraints.txt +-e .[dev] diff --git a/scripts/scripts.md b/scripts/scripts.md deleted file mode 100644 index e69de29..0000000 diff --git a/setup.py b/setup.py index 6618195..9635433 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,9 @@ -from setuptools import setup, find_packages +"""Compatibility shim for tools that still invoke ``setup.py`` directly. -# Read requirements from requirements.txt -with open("requirements.txt", "r") as f: - requirements = [line.strip() for line in f if line.strip() and not line.startswith("#")] +All package metadata and dependencies live in ``pyproject.toml``. +""" -# Read README for long description -with open("README.md", "r", encoding="utf-8") as f: - long_description = f.read() +from setuptools import setup -setup( - name="mobo-kit", - version="0.1.0", - description="Multi-objective Bayesian optimization toolkit for functional thin-film fabrication", - long_description=long_description, - long_description_content_type="text/markdown", - author="Ethan Schwartz, Daniel Abdoue, Nicky Evans, Tonio Buonassisi", - author_email="ebuddy23@uw.edu", - url="https://github.com/PV-Lab/MOBO-FOM", - packages=find_packages(where="src"), - package_dir={"": "src"}, - install_requires=requirements, - classifiers=[ - "Development Status :: 3 - Alpha", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Scientific/Engineering :: Chemistry", - "Topic :: Scientific/Engineering :: Physics", - ], - python_requires=">=3.10", - keywords="bayesian-optimization, multi-objective, materials-science, thin-films, machine-learning", - project_urls={ - "Bug Reports": "https://github.com/PV-Lab/MOBO-FOM/issues", - "Source": "https://github.com/PV-Lab/MOBO-FOM", - "Documentation": "https://github.com/PV-Lab/MOBO-FOM#readme", - }, -) + +setup() diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 71dde1b2ec9bbd1f3e27cdafefac57a8add259dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156 zcmd1j<>g`k0=`{&86f&Gh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6w>*(xTqIJKxa zCbc*@qddkXKe;qFHLs*N#@FA;KR!P%HKi!CEEUYtb@TU)DK1KmiI30B%PfhH*DI*J W#bJ}1pHiBWY6mj7mOz67#`|2m97MJK26eT8? zWG1H;BgBh~lJ(=`GxIV_;^XxSDt~d<$P^xu%#ONPJ*sWMurn03(W+ GfnosS@i5{5 diff --git a/src/__pycache__/acquisition.cpython-310.pyc b/src/__pycache__/acquisition.cpython-310.pyc deleted file mode 100644 index e927c13b86496e791dd57572fa61065e0484fe10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14276 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 diff --git a/src/__pycache__/constraints.cpython-310.pyc b/src/__pycache__/constraints.cpython-310.pyc deleted file mode 100644 index 8dbb414e01e60a74786357cdd3e7f7e2a7b43361..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3684 zcmai1-ESOM6`y-&c6PmXwu$3>n6$}_6Y6Y@$gZb|(WC`w|7B!zb4FpT%!U5`CG zv%YsW&RVl5#I0H+A`m>5O16ZA@`8Be842+}%p3Fxf%pURK=|D|&e~3jnDyR0_iOGs z=l;&`9LI+al`TB!uYbRC zCMp4`y~M}(Nj{2^!~7h7g`b9Wgxj~Q>iK_Y?W!aj^rI+F{3M7YRV`$tIX?{jM0?cq$ZpS0ZB9>EJ%`(gwQX}JbvQj~jac|CoU~3^J7mP_ z719ECQ)dMIo}1FtT_afSr0x!xwqRVb^DAOmWrpXb(Ty#~aVY$VwS2Y48nI;e{4n6G z9S2dOn1cDL(G8*|n+yGp3OefV4}Jr&ExIy}SRKAeq>r_e=A~C6)saFme_6$0ClPF= z(+YTyblHXG>#ZMuv&wjkY50|i`*k6>KA|IxlbN5eW^hkLED^1?z#v{VSvK#z;Y+_I z5+Rl8PV^ts^;bM-t+Z8aiWxsogu$A)_$7MWF?!rJ`}8?pJ>Pq9-#!i4oj?C^?k-~& zVCy|06X=}8>?(uJLgM1XZZ+32cjy5H%_+vAp7HM#+e5+~?fG1f$0RH8q}vwFhJJ?E zr<+#%Gv_bA{Nml_$N&7npH5%7G~N8e_x`py^6#1H=3jr?{MXetFTV>+(x{ykePt}p z3XL%Klgt+P+gS;>7DPcJstA`|_tXaW*;}JC@73(vUOgkXGb&Wgop}#B-hXV#5diVY z?A+AS`wISE5~{wku{0moJ1qf7EL~rmUG(BeaEZGzy@~HGUSCqOzO;u1HQDZFr|K)B zzUI|;fun^SHeS@O;1D&0rc$Yp5;<)T*~3&G$3W&pxa!OXt}>UKKzdRd&m(F)v*Gpw zF{J536O4(WX$YIel&o6Yh@&0yBVgV^Jax|yt4E2oIh5EOGoOK4^s3LS&z&uj(ub5| z{-<<vkzC`=&>Rbo(CrShxTB40)GnAV)Y^H+46 z%aR4XLfFY(ombX z?!M7;5|TOz-Jgdui(Z*xiR_5o1+1TQ4uE%xjlBTsvHq6lFpb3rM#ge={@3XZ zKuI9=ont6EVkvn4y_1u>`Exfn!PF3U$KnP9@+S7|V}PS6ZWO!Fj+F|Q!!AR1OuE%P zv1fyOi`7E4D66>EtQ@ESX!lX|WDeI=(TI2J{TnwHZ{EH#@69eQUh}SA`S2D<5ca6d z(J2qy&~A%}R|n+R@Qxg~mmt;bMx-)Z`Wv-!eI-^R^43H*v(eOwCUk_%Y-H2tdGf?Q z)x3s`TRH|CLGvbFs&v2(RUCp3Es?UuxqX-nfkyQheNY0Amh557%D1pIb3gFfD_vzc z_sQ93)!m(E#ocqTzgq{D_MZ|Yjr}>zFu#M4TC1Q}U=c`Hm4vKJDbNLFe$$eJy$CN3($0`gfQwb26adbOP8 z=zZ>L+SdE<1;h16oV0iymE`)Uq3h!2BLkS3b{m(zDP+u02DShs1CosZ;M(Wmw~-Nb zRf{6;VDUJ!Fh8LSMIb51%=a7n_vuu*fVx4y+#u`X-iyMJ-9`xI+a#jXl7na#Hizc= zY|74UzRFr1pf^AN`Qw3UwSoeJUDzyLtiH{fs4=U>tY8M^A-pbgaJ-V=(BgoIP|u*o z6jE0q@-&)iNq$SqIvtz)&q$Oxx^T*HiC(D)B{tKKn%?AJg diff --git a/src/__pycache__/data.cpython-310.pyc b/src/__pycache__/data.cpython-310.pyc deleted file mode 100644 index 0669ae6d9d88820eda741355e9bff95087ba45a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8628 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 diff --git a/src/__pycache__/design.cpython-310.pyc b/src/__pycache__/design.cpython-310.pyc deleted file mode 100644 index 9524e650d0004f94a4743fa659567c882e8e7135..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4101 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 diff --git a/src/__pycache__/lhs.cpython-310.pyc b/src/__pycache__/lhs.cpython-310.pyc deleted file mode 100644 index 037a58e23729c5fcfcec3462221be2d3d6634f10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9836 zcmb_iOK=?5b?x8$Klp!0>eHeu2_u1sB}$e42xLMx2@f0ddVm3YdWNri zKw{Dp%Z3!ov1P|nezr<3a2KVrN>vtFWsz0N*`%^aRWIX(!pb74@+O5!N;&s-&j%po z(jtSJH?QBn-@WgibMHf|RLU#(IRE>X8=t+XD8HhI(T{>HKS0Dg5egx|y*u&8(Ge=B!*ZZ{?c>tAMtKNV&yk$tpF=R+;;z-LdAlHQt=C zCipqyPBy2kDQ=r}r<*g@4Ciz1Y;(?<<2IT*ZynK?DhV_{E9Wf(jkvl5h7LSNWk$YUMh@;|hk4TNU3H%i<#Ho)+4QQmNjfU9PB6!S=jXUNs8v1y?+@xd)Lb6)l>v+iiEdDqFX!HH;ESyWs_XRC>F0``q9e z?aG&IX*Zp~kt=N*qfDVTXn37g#~;+tir&O(yz`FVSoiRl9^}S+&u&+PR&`xAMAd7f zQonTW41UThf$a%fij|t}IxHN@n+npQ9vWe4N52cbyp;(uA^KyF z)$WIR6Q8r4hAT|lGr!U7+-wBq;AMFm%#Ppi*3FjZZkrF-kR=}CLQ9(T)~0AQ{YCS( zgI)TleWsV>)314x0`JjQ^X@xvQf_QI!#B~0R-tmAbymp zyDdA2(r)XvBmF4dX}9?of^|G0i`X09zDin7A|IvOBuPI~HzR{ah%~nqX&a4C75OOY z_fDKUas7&qA+9@qZR7U!^Q{_Ygi>68>*Cpq)t2W7+1PUU>DYyfZ(a9g?Yg_+FSWO$ z$*NDxtZtwg=Lm-pIBBz$Ra%UP7|ld+Bhk~7Qr zJTd{K8^VqV?E{UZy{m=lUA3nn2N?_fuBK8ONLc8w8?YQ77^qF5J{@XzW67qw@3C0k zvhOsSou+B8`7O5-IOZkCmVV1KYb_}qmrs>x*JR80QRH@-o^Q75=G9mf%l5Kw(rcPs zN4Ab_*|K5dbPZbC(lG<+Ku*ve-~8(6sEJz8`M|kD*LA!be?E(U{_Ew69_gO#L0Bzl zSUpO6&N_Z8nMm{6QI?iiYdQ5u$Lga}z2Ul5(XiL~?B~&uP8t-2_cYR>Ulm;*#o&>8 zE>h31pyR}(!crA2su&73K*|La`DEG4sk~ZLNow+0YNmRT4(kx*f(_~T8!cBvsX#WM zgSzzA_og6A3H@y~mtJ)|($SXOn`iTLZ7AkNll67Amco$z59!B1(tm^poLb2FkkU(q zIHxx?ITk7bWWS@?WQVUQI#+qW@v_65A}1EO!u!@UoOjSGk!hO@2vSw z!0O;Px{m3^nsC(&T4ue06O2P*-fjdNbh1O*)6hJ5${cK4tN?GbY4gp6HOCLC@tBJy zKae=6TV=_7y>5CmT+?xa;RuUnY#4oRZ`NBKPsF22vXaXrsJhG|d*M?pGH8`iMmlX5 zr?ZldQvQYwD?w6?RV0NsTX*CV3Zisvqt!sOB5RC%jv75r$qSTxjhd* zeJ88P3pBeX>U+huSUPV9_60(vnug;3hpa~ORsBUgd|#=nWHa8)hU%tro{&I$Um+CO zR0C~OUsr|xfx2tlRbcmx<$WfI1|}LaMKE(1nRTeiba9Ek(iv{ZzXJmEm{& zRE;u;vB17-OJ9xD+NY{~6+K7JTmGcm@Pk^b**<*-QCtGONFR8n#Vovl*RO9b0($Cnk9!De)tYXDTE)@pC#{PQ9h z0#M@m@p=e4rAf`F9x(ewRQUA%)9Sn`FHrFIcC5_bk^eul< zG-~li)8cufXO-@w{Howbj_Z$X@SyE|DnPc0RbU<&R)K5}3b3nSP3m23L;)ZheccJ< z8)*2cYLy6K$r$rwLluy;vsdutCCz`~l7=z|AaomTmNu}SmtN5r(Is{((XMY%&O{Ow zMqZKXS&H*GR`NQY?g?sB3P2v+Dsv1N@%)+{)HbSYD-7Rp#9pb-C$GWmuhvL1{Z9@V zvf$@PQftVBx==f+%o9cfmzhrmI$7+V0dAIpm1YU<=wN3vu-~bWp40Qn4s0-Zn6abo zX2J~cJR7EVvtgDQ*(|U-7pCr#vgqAc*%Bw{;+Su~wh}WW$2S|E$?He94#KtO1jL`3 zPO}BO-LyS>-60l}pf_8|NcporZc8Hc=_F@!MK!A#>bOSCs;K5Qxq;$)|Bizc7_EBMk+waZwU4d)+NdD^H z!}nwL5V!Y>pZItNUl#vAAroqT0T8Y1!laVW?=nzB@Uifr9C0~-niSw!D1ZTo)#sGB zoRk1S4fuTzhPju8dcaDA+71DDHU@C0MVe-yR(CZjBNah^z`9Ux8uGQ!fC}YVappop zUO{WnfxAQ%S&0bRkY#;C+1$Dk6afPowWt`~@3NwuA>RnhZQvJ;Y8kNJ z-fDr?Cpy;f{SK6mVc!L(a}w1wi7FgGd_W_-phP;0X8Z`^0NC{_BRI|o4wI5%yIv_J z`U(OMw2pMw@mN)54HA%aB%`!*Cm`BSS}GkXNin;QQcklSY|B$rejG`qAU7#bY9wz` zLi%f!_AM<+9+RapQMGch-eMLjb2Zk|$Y|RBO{)-h=K6#BJfG-u>~D_jsgG$H{Ehr1 zpx;oZwMnRQQO~NgYEjDr_zi7ZJ*vqqwEXPnKcmmzy&Pd~o-WFQ?b3b+QE`;UMmc(l z`RO#MJ-l2$Xu${Vlh+67J9E=Th#BUNjDG(!fy{uQE`VZ%DK>Y7#N5F^_0k?34KjXr z6;MjJoupsTYPggjmnKe!K_^3KbsZEIW-drEh0TG^=E)Hi#$iRIa1sh|Lkmo27kb4o zy_uykL|SA(XG=IGC5&6fXk{4Jv2ZNRfzDnF$8b{0yE)9B4f7)VA}BBFw9uGSF#k1G z@zh(#!yKrtA%6n5H$SiR#xc`4&-CP%p6SP5G!tg|G0(C=CpOH}dscGY@9|k$Y1%SK zm@|$9aU6sp;^V^7F-baYa;M1$wuOyY#ug3{Dr*}D&d>dfnb>mTv@OE%f>Tn3mo1XR zw9NKi{x+m|kLZeAMRGx&pe&K(1PLSoSyT{C-R`(S6;4fjUIsilK4^pv@?}ciroI#M z4&}Eg=~D9hl!TPrpoAE+e1(!TNTM7sp4J=XhYPg|w>m_PxkR3&*08OLJdeZzUz5@r zlTH)S8SGHf>HuQQRejpyKD!?5@ta}0gvF=(|+7pG`|3kR#*4N z1|4IkF*)IYb{I1-mev;H6pV3EF?OJm1D`>~TFZB8nffNKog#ET#E7;99xPR73o+{6O$=<@Jn$> z)vz>;LmC$3=^zXLRQ(u7J1iqjf0Ek4954W5K~7{a(s+;;dYBdfL!!#7@AoFquNqE> z?5rYk@P4ZpBOgwncOguFtbBs`0ek@5u#BUo?HIkuP?yc17*4{(XuZjxM7gjSW_Jv* z=c(Q_Rynl;whB}6BaHsT!HC7U24k1vmEC$HD2G#cgH-!NQKo~ule(_=W_q)|IrRLi zLC>*pR*Zjah>36xy(d4xyI}9gFe?7vgtK7^2H_vkle+1RZ&!ZDW9e_*U%VJx)adFMVs1OWR$;~+i|kApD^ z^1sn)p7;C^?Uy(lzZvhCn(WwosQHvteWr z?ie9Su;cJs0}orleV64Q;LNd^?ap7@w~?#nWd`mOW_PwvB!e~N#uY&l|dwr;^(IBmvsD=Mj+7(+rY; zLVcPbR5P^MI9#QTBe1OjHjU2>giR@z{k&)x>R|-0G#$YExnVq_jssq2DFlvR4r$?+ zLS2SF3ouS2*fp-npJBRtIwdC38tHuhL-B`!-(~y;2xCY?ARN3j55OWfypX`!!wIah z%PpT6z*;7THPn^>PGy)-z#)UwC^&rt!(8-FhpLwj@&E}iQwF{X7*}xC-C{U_ISGzd zVh|!Y0#sH3Dhx^R=%$dH0*9RpCpX800f1|TIq3!CfXH!}>vU|`Z+$&1?M{S~?ByxD zlVJ%XPlwZ+Q^Gie(96w~{AHNm9h9JUCY%muFfw3sW~yYnC3CF-GWuP-N2>P$Qd=a8n$`k z0FyJ1oi>T$!4CR2Z}5aP0G%6nxyM|XJNJS2C~Fj;j)xre(Qf2k$O#}!QC2AJmY ziWxigc(G&r%Je4Yg0r-~v}m4lZR}Ua|F?HOLbmN}%a&Jp35FauU>)2DISHDebK=pP z=JOQ_(c@BY!-ltV`V=k$sCto+obF=)LlpEj?LHm&jTZyFAN`wwffbJ3r2Z|`sZ(Zr zx3FeW(Jefk8hrPzf|kOk`=)qt1i zjzMk%hnHfQn>}IH`WAQu3FS{q=e``hd|hgiwd~#Xm;Y;a`_R}{Rt&A zl#q8MDGysR?BnQVdW?2CNROOe+@6v{hUg!*h;Kg_pZ*MG zQ7Vp{9HsF()Cj-^1&TevKc~{apyYtxSK0?%XF*wgibrN29)}N?)kZ*zeY{C+Rxcx$ zSB)&{;SnN!2#|tjsFt;Hc!yc-A$6L8Dw{x6>=Pc<$R{)qo*7r=FEB;Lh&QTCtD2&O z;({pUH+XzdN@D9@Cvhor2;wU7#S)A41$yMNuTc*FP?C)Kb>wh)&DR&NQRyTl!#Sz! z{*_#zQjHz`Ww`glKD0OSJk>$&2IXu@_$_FS6;(FqCm)B&c-g~2IAG>dhv6XHwMl(+ z?kaC1zg#KDyo3kf7y-W{GbVm-juvnklO-;@K_d^>)1yK2%a@mz`F-RLRZ|FKIM0R3 z;gNS7?(Yq1#Un9WdygKOvVEU&yo&cJ$K;Jy!3>Ub!&hT9Kd?sDdLD0z24E>QRwa-TCkuCB?QOO*7XASO|8n574T#dXFoVI8 zIxW|rgaR8fO$iaLI5f(Zg?N}%aQOc~mJpF~Sljs@EUbl*;w8jrf(9;DVk7M@(f<;7 z$y02z28t9Jgq>`@tPDb3Yc1(ny6^ecMAHu1ZYyvbYxvI!%3*it3CwNFb*?O#5YU7_{*pB6ZvBPNZ!zjnp*rH?Fhb4f)%U9 YJ?&-s#h;e^Qt7X?08jd`bo|%<2OQYuqyPW_ diff --git a/src/__pycache__/metrics.cpython-310.pyc b/src/__pycache__/metrics.cpython-310.pyc deleted file mode 100644 index cc37c3527783fdeeb7d1cea0a55a230fc52946cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3945 zcmai1&2Jn@74Pcq`RJZcJC3tqB`C7lBomxT)_hypK!_6&3EL>iqGaNzw>?!e({_KQ zs>YG6o&z|D3qj%ps|CqHT#(?*jT?8)bwPpy3V(tnBwkg|m$PVTO;^{edR6u6z4v?X zO;W4520T~){>Q5$YhM`etZGR>z9$j;(1cEOd%6 zZiS^z3BGn%j+~C8=L(@4RXUY1U$s+%`C?d)8l6Ve>@@XwDO`vaJB!dS6DK?qEp?WV z;aHAK-Y1vH8M5@U&^ZhAS?4UdOs)`2z6>MFq($B%7hl?)bHsXRw64BU^0$zz_;H*F zUj#|aTc#{L8m1wY<*ihW_~E8v`Tjvl*?tlZBMM|Q;9i`>9!a7g_5~%dtjtzD-c1;l z=U9>mFQR_z@#jOIQQ}d?5(WzkDG9iEgT@EPsV5TnvF_ksYd^p7k6#?zy(Q6;ld2wS zKcX>Dn5;6|^U@@Ug%_tMOB1=C#%VhyK4bpDGilM3%R(ajI4Ct7?m?b z(9mG-3wyVafzgTe%G@oEoXpA0cZ`u6uZmKJM0wXaGWm7k68i}K1Z8NqLJE5p`;MrR zVpjarAi%Y2ero)7GGEJzFuw)!aK>&8I2^^7&%aUT<3_-$A3ahvt|n(nC`pRD04n=5#ug|%hW!enQcc15sHd3(w>H7T%ftW20; zlrkE3Dc0u3y@Z{TqU(pTIwiQ@@4fDtkLmc_6AH-l|44yM&6#GNyzrAa~F&8Z8 zitqqWJviS_%lAOL&)l8ngnjUqghNir+hWc{OSMWp(8(#~>dVb3BgfSmo&(uL;q&Xp z`rb?g9Se6A--cVy^b0x%TPApi;*Hyn?{D1i++AiyU&UiBV4*?CaRPb;FX4VfF`@fGmtH3VJOuk3fBfX*eAy-&L)#)j zba-*9H~=n=hFmDgd>jv>^k9NrrsC@1m!_;!z7N)w5<(jH#Q+~xS8zWOm=An7@^E3s zK4G{Fwxk?GBm*}P-GMq~hqBWSh&MijP*u$3mj!(R2usd%yVfFG26@;yXkI2=jm5h&)aih0tGK@-ybky^k5 zk^2t@Yr|$Ix4LWFA92d~Hs#&Hi|zHK3voRbe0yW-ds|)-Q^EkQn!0vxYh#m)!uid$K6lka|x} z+*u@-PO9>SN*-`6)#ZZ7#?-~V82pc)G($)uPXWcxaE3QQU|I7C0eN^phJMJUy^|y% zWHuN))LDt2R8!Jkhp=^z!4_R*)XB31of5n{1qm1Pf9^2~vQSUQ&Vor6KIBe1z6jf^bA3b5#G?&w$H?zzi!Os~bNxGq-1+Hz0A$ zQAg&e3W?(LIcix&l!cj9j}W90Q}=6mpA>#$jp~`r4o>fTdPhh|jjVC(yf(5rDc&@) z0{c^D^H=|GjWCYg%$%2(l=ke?BcP$mYircZ8d>vQW3-ShjL#{AhRAaNl>)Q;;JjE~M_XCQSsP>g77c2%62Jdn){RewEH zDO(|>4+;BOX#{%B5F|?Tm*#U5RwHSFBWVmk6foO2B+4m0#ID0i>E2YXr5ya$4}j)s z?`7lgy@|_DU~n@`AYk%a?HT_YRYtTd>1<6Bo-`khrB7_JLacRn>e=HD6cFH&yd(Xr#@O7o1&E^kqdCG}zdO zdQSDF$>2UL;&2Y|Ld`e66ogV5Cy^}XV3XAypHr`^q7K9(cdRsz^m0wBD3q(a@s?h1 z!VLcgGc}=#=xCZ3&;^7|+r+2}V>ZHQ1;R>I1s9-Pq+S<#?1_>UJ^r8Wq2YZI z7zDTnfCcS=CJ8u(0I(B_*Z_8@$Bxa{;7P55|1S8LE(q@AQJL!m=iVqw%2Vc*I4`033XZ)V90%nQ zZ{-!-X9^x&7O&yYTy=6mEx{WFjXu2k0Uw>{0dQ8#Ee>F?AU`t)SRglnj#?(`D8iX+ z#ozXba}$r4DkJ|}u?Neza{Ow=2FsTb^MPoYo2;t7(ryohr4mGpbR6 zzGZFRyVt7cP*_!?8V|asL7{sZ8OjfHczjFs6h@ft>CM)FY}xucOs~8>3Uvo zsC4XkOz8<=dXPeO#Z-)7Dp&!!R2ih9&%U4rT02Sp?_?-~khh~@C<1Sq`Py^kxGovw z#n0!@i-Np*DW>2_K4&VVbsBsxq&|z=V_43`lx5xxpfuwt)YyL5o){@NU_r~pD(VI* zforg7E6mw&2JQ0AC?Ug;-cq5OE8JC46YQpO3vE;37nubURsUnInh3rp_3Qry?D$VI diff --git a/src/__pycache__/model.cpython-311.pyc b/src/__pycache__/model.cpython-311.pyc deleted file mode 100644 index 4cbc300ae164bbbce075f115d3fc81fb404a2d02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1660 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⩔HAIZZ<4;Il05l7zf+lA3 z8Ci=3Y&tL5EQpZ=_<5UAGP$(vTM;wMvSvr8m5e--)2E5-QyAwkCL%B~3?>Lr!$+S@ zOY=50XH%=Tua3U~h>kkKc4?}Z&&yIBnl%D}ywmV5xV86?_z`kgaIu^$2p<4mFI?K+ zfRxZS9rTV?!;$r08-3$u__!55UZ%G~UF+{`B%RO4o6!wzV|x9(6&fy6z%e4nYTXLG zVudDQEYiL4Vp%V9&#GMiHu8E$n*;*D=z7a!lNOsa*km<&@Vlc%bli-NThZ~duiA6K z>^W%l94rT_eJ|Y~|L!%j?+vT(jdIskWMES_A|nP{@7o**e)jB{Mi%$!i6n*oK_$79 z5zjxnD4=ceL-x>#pLw`x&j!ut(aEfO>+nyc`x_iUREUx#ckb`eUAlcX1yKj+NzW8s zI14*2OEt&2uIP)rwkYQ12|nh<`#>P>2LkSv{6LsWUP=lVl9x_9mbbmm#2AHnn4ysX z=W9j`I7V~~_Y#bb5_SSe2|W&Q>-%nBF#{u3V5D@?fv-g6{jcYYU;?_y9I==q26JSK z3zyC~5Mwh$PTGYJ5aR;P-{Il&j;-p17YdcpJSEXLS>1&Ygl@G$j;9GE?VB{!8Rk^$ z7T-cesw2#4Jpd}z+mf5ciXEE|8_&!X_FTYny+j}pN|p(6xnrdS@t+<9a+~@JfUk(I zbgfahNOcB4S)^J)@;CH)1Y}b)xEtmJXwdN^Hl^T0@T8H|^L)v%&=0}MjH2o3TsD8A zFp_um5F8~YA?(GBb|T)KG)5%s2lA|hjP8kAuTUF38zT8Yv`N3)>oOd}Js~EG`RcBM zMRh^OF&M%mNP6A^B!nVU`z}DLUGuf3 z%pzU%b*#`|^i^7tbn_?g!ELkJue3zfkIgCiNzy0f;O_UARh2~4I~RDTMu(_^MCXs1{!t!Jl6%#c{LIW&Xw7u$b zHoqEU@ry8uhY5?F6Y3J!)yAFovH(JC@};OYds)yRu4Kvf){YKDBwufJ;3O5G!^G3T zJPw!k7a(=_9x@r;Vt9k$A4huDi<`-BX1~5-M8?d>m=zf-ovuE2blZo*d%isT#o5jG z%4f~ckVX1|(y6Ub&+|sRU)X%b?B*-8_hbf3$vG;EDA?fe_b4S*1Q0xM2%og1M_ABm7Dzl|? zCUeMQh=Cle9JzhX2#!EEnNf=wH5$EUJ&^4-*Lwegp?2%F$0_V>i$&UbAHvGk&>E$? zSrM=Tb=Va0*(al|ITkRZBgZ;&JWz1AU_!Cus7d~dxY%CocN{uuGf4UK;hEZh1@+39 zZ`7SoJ_C;M@(sWYg&e&!9!$U!6Eu8)*!v@rF`d)@&iehMIHW(4zMb;b$yiQUJ0^78 z1iYn*Aig>0&j&n3-Z%wBBU|O*Lk%66tO!8#9t>V0+-pDxFzhjzUW@59nBFSKo7}L) z4SzCKN|rvZhWj_)Gs8nxcnCbf&z9Dzdq*nM_mk${xV1NaD^g}R_E*{d%{h}jV6g{` zMt@A|-;Bv-EjDX3y5ko%D*&V^svyMra1A%}uq~oiD;3`al`yFl>>!+HyNpKPLMM&q^({1K{J*}1 zMvV3jfE?Xl3U77AO58TXP;j7jNr1;&wlww0^v&s#^V#+wW-kmoJNRD+SA+Hd diff --git a/src/__pycache__/utils.cpython-310.pyc b/src/__pycache__/utils.cpython-310.pyc deleted file mode 100644 index 5aea0f01f2ed6d50082cd90c93da9576a65f9926..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6566 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 rq{LH7*GY4}@+(>Fw7Ep8q|Hv-@UcwVC_M@_x int: + """Concise alias for report-building callers.""" + + return self.exact_overlap_count + + @property + def matched_distances(self) -> np.ndarray: + """Concise alias for the optimal matched-pair distances.""" + + return self.matched_pair_distances + + @property + def max_matched_distance(self) -> float: + """Concise alias for the maximum optimal matched-pair distance.""" + + return self.maximum_matched_distance + + @property + def chamfer_distance(self) -> float: + """Concise alias for the symmetric Chamfer distance.""" + + return self.symmetric_chamfer_distance + + def regional_match_count(self, threshold: float) -> int: + """Return the Hungarian match count for an evaluated threshold.""" + + key = float(threshold) + if key not in self.regional_match_counts: + raise KeyError(f"Threshold {key} was not evaluated.") + return self.regional_match_counts[key] + + +def _normalized_batch(value: np.ndarray, *, name: str) -> np.ndarray: + batch = np.asarray(value, dtype=float) + if batch.ndim != 2 or batch.shape[0] == 0 or batch.shape[1] == 0: + raise ValueError(f"{name} must be a non-empty two-dimensional matrix.") + if not np.all(np.isfinite(batch)): + raise ValueError(f"{name} must contain only finite values.") + if np.any(batch < 0.0) or np.any(batch > 1.0): + raise ValueError(f"{name} must contain normalized coordinates in [0, 1].") + return batch + + +def _thresholds(values: Sequence[float]) -> tuple[float, ...]: + if isinstance(values, (str, bytes)): + raise TypeError("thresholds must be a non-empty sequence of finite numbers.") + try: + raw = tuple(values) + except TypeError as exc: + raise TypeError( + "thresholds must be a non-empty sequence of finite numbers." + ) from exc + if not raw: + raise ValueError("thresholds must not be empty.") + result: list[float] = [] + for value in raw: + if isinstance(value, (bool, np.bool_)): + raise ValueError("thresholds must contain finite non-negative numbers.") + try: + number = float(value) + except (TypeError, ValueError) as exc: + raise ValueError( + "thresholds must contain finite non-negative numbers." + ) from exc + if not np.isfinite(number) or number < 0.0: + raise ValueError("thresholds must contain finite non-negative numbers.") + result.append(number) + if len(set(result)) != len(result): + raise ValueError("thresholds must not contain duplicates.") + return tuple(sorted(result)) + + +def _canonical_rows(batch: np.ndarray) -> np.ndarray: + keys = tuple(batch[:, column] for column in reversed(range(batch.shape[1]))) + return batch[np.lexsort(keys)].copy() + + +def _exact_row_set(batch: np.ndarray) -> set[tuple[float, ...]]: + return {tuple(float(value) for value in row) for row in batch} + + +def regional_match_batches( + reference_norm: np.ndarray, + comparison_norm: np.ndarray, + *, + thresholds: Sequence[float] = DEFAULT_REGIONAL_THRESHOLDS, +) -> RegionalBatchComparison: + """Compare two normalized batches using canonical Hungarian assignment. + + Canonical lexicographic row order is applied before assignment. Therefore all + returned coordinate pairs, distances, and aggregate metrics are invariant to + row reordering in either input batch. + """ + + reference = _normalized_batch(reference_norm, name="reference_norm") + comparison = _normalized_batch(comparison_norm, name="comparison_norm") + if reference.shape[1] != comparison.shape[1]: + raise ValueError( + "reference_norm and comparison_norm must have the same input dimension." + ) + evaluated_thresholds = _thresholds(thresholds) + reference = _canonical_rows(reference) + comparison = _canonical_rows(comparison) + distances = cdist(reference, comparison, metric="euclidean") + reference_indices, comparison_indices = linear_sum_assignment(distances) + matched_distances = distances[reference_indices, comparison_indices] + + reference_set = _exact_row_set(reference) + comparison_set = _exact_row_set(comparison) + exact_overlap = len(reference_set & comparison_set) + union = len(reference_set | comparison_set) + jaccard = float(exact_overlap / union) + + directed_reference = distances.min(axis=1) + directed_comparison = distances.min(axis=0) + chamfer = 0.5 * ( + float(directed_reference.mean()) + float(directed_comparison.mean()) + ) + hausdorff = max(float(directed_reference.max()), float(directed_comparison.max())) + regional_counts = { + threshold: int(np.count_nonzero(matched_distances <= threshold)) + for threshold in evaluated_thresholds + } + + return RegionalBatchComparison( + exact_overlap_count=exact_overlap, + jaccard_overlap=jaccard, + matched_reference_rows=reference[reference_indices].copy(), + matched_comparison_rows=comparison[comparison_indices].copy(), + matched_pair_distances=matched_distances.copy(), + mean_matched_distance=float(matched_distances.mean()), + maximum_matched_distance=float(matched_distances.max()), + regional_match_counts=regional_counts, + symmetric_chamfer_distance=chamfer, + hausdorff_distance=hausdorff, + thresholds=evaluated_thresholds, + ) + + +compare_candidate_batches = regional_match_batches + + +__all__ = [ + "DEFAULT_REGIONAL_THRESHOLDS", + "RegionalBatchComparison", + "compare_candidate_batches", + "regional_match_batches", +] diff --git a/src/mobo_kit/batch_selection.py b/src/mobo_kit/batch_selection.py new file mode 100644 index 0000000..a97cf83 --- /dev/null +++ b/src/mobo_kit/batch_selection.py @@ -0,0 +1,398 @@ +"""Reusable sequential local penalization for discrete MOBO candidate pools.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from numbers import Real +from typing import Any, Callable + +import numpy as np + +from .candidate_pool import CandidatePool + + +@dataclass(frozen=True) +class LocalPenalizationConfig: + radius: float | None + min_batch_distance: float + min_observed_distance: float = 0.0 + dimension_weights: np.ndarray | None = None + epsilon: float = 1e-12 + + def __post_init__(self) -> None: + for field_name in ("min_batch_distance", "min_observed_distance", "epsilon"): + field_value = getattr(self, field_name) + if isinstance(field_value, (bool, np.bool_)) or not isinstance( + field_value, Real + ): + raise ValueError(f"{field_name} must be a real non-boolean number.") + if self.radius is None: + radius = None + else: + if isinstance(self.radius, (bool, np.bool_)) or not isinstance( + self.radius, Real + ): + raise ValueError("radius must be None or a real non-boolean number.") + radius = float(self.radius) + minimum_batch = float(self.min_batch_distance) + minimum_observed = float(self.min_observed_distance) + epsilon = float(self.epsilon) + if radius is not None and (not np.isfinite(radius) or radius <= 0): + raise ValueError("radius must be None or finite and strictly positive.") + if not np.isfinite(minimum_batch) or minimum_batch < 0: + raise ValueError("min_batch_distance must be finite and non-negative.") + if not np.isfinite(minimum_observed) or minimum_observed < 0: + raise ValueError("min_observed_distance must be finite and non-negative.") + if not np.isfinite(epsilon) or epsilon <= 0 or epsilon >= 1: + raise ValueError("epsilon must be finite and between zero and one.") + object.__setattr__(self, "radius", radius) + object.__setattr__(self, "min_batch_distance", minimum_batch) + object.__setattr__(self, "min_observed_distance", minimum_observed) + object.__setattr__(self, "epsilon", epsilon) + if self.dimension_weights is not None: + raw_weights = np.asarray(self.dimension_weights) + if np.issubdtype(raw_weights.dtype, np.bool_) or any( + not isinstance(value, Real) or isinstance(value, (bool, np.bool_)) + for value in raw_weights.ravel() + ): + raise ValueError( + "dimension_weights must contain real non-boolean numbers." + ) + weights = np.asarray(self.dimension_weights, dtype=float).copy() + if weights.ndim != 1 or weights.size == 0: + raise ValueError("dimension_weights must be a non-empty vector.") + if not np.all(np.isfinite(weights)) or np.any(weights <= 0): + raise ValueError( + "dimension_weights must be finite and strictly positive." + ) + weights.setflags(write=False) + object.__setattr__(self, "dimension_weights", weights) + + +@dataclass(frozen=True) +class BaseScoreResult: + base_log_score: np.ndarray + base_score: np.ndarray | None = None + diagnostics: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class SelectionStep: + order: int + pool_index: int + base_score: float | None + base_log_score: float + penalty_factor: float + log_penalty: float + penalized_log_score: float + nearest_selected_distance_before: float | None + nearest_observed_distance: float | None + + +@dataclass(frozen=True) +class BatchSelectionResult: + X_norm: np.ndarray + X_phys: np.ndarray + selected_pool_indices: np.ndarray + steps: tuple[SelectionStep, ...] + method_diagnostics: dict[str, Any] + distance_diagnostics: dict[str, Any] + + +class UndersizedBatchError(RuntimeError): + """Raised when an exact batch would require relaxing a hard rule.""" + + def __init__( + self, + *, + requested_size: int, + selected_size: int, + pool_size: int, + remaining_candidate_count: int, + min_batch_distance: float, + min_observed_distance: float, + selected_pool_indices: np.ndarray, + hard_valid_candidate_count: int | None = None, + ) -> None: + self.requested_size = requested_size + self.selected_size = selected_size + self.pool_size = pool_size + self.remaining_candidate_count = remaining_candidate_count + self.min_batch_distance = min_batch_distance + self.min_observed_distance = min_observed_distance + self.selected_pool_indices = selected_pool_indices.copy() + self.hard_valid_candidate_count = hard_valid_candidate_count + super().__init__( + "Unable to select the exact requested batch without violating an " + "eligibility or hard-distance rule: " + f"requested={requested_size}, selected={selected_size}, " + f"pool_size={pool_size}, remaining={remaining_candidate_count}, " + f"min_batch_distance={min_batch_distance}, " + f"min_observed_distance={min_observed_distance}." + ) + + +def _weights(config: LocalPenalizationConfig, dimension: int) -> np.ndarray: + if config.dimension_weights is None: + return np.ones(dimension, dtype=float) + if config.dimension_weights.shape != (dimension,): + raise ValueError( + "dimension_weights must have shape " + f"({dimension},); got {config.dimension_weights.shape}." + ) + return config.dimension_weights + + +def _distances( + X: np.ndarray, references: np.ndarray, weights: np.ndarray +) -> np.ndarray: + if references.shape[0] == 0: + return np.empty((X.shape[0], 0), dtype=float) + differences = X[:, None, :] - references[None, :, :] + return np.sqrt(np.sum(weights * differences**2, axis=-1)) + + +def soft_local_penalty( + distances: np.ndarray, *, radius: float, epsilon: float = 1e-12 +) -> tuple[np.ndarray, np.ndarray]: + """Return stabilized exclusion factors and their natural logarithms.""" + distance = np.asarray(distances, dtype=float) + if not np.all(np.isfinite(distance)) or np.any(distance < 0): + raise ValueError("distances must be finite and non-negative.") + if isinstance(radius, (bool, np.bool_)) or not isinstance(radius, Real): + raise ValueError("radius must be a real non-boolean number.") + if isinstance(epsilon, (bool, np.bool_)) or not isinstance(epsilon, Real): + raise ValueError("epsilon must be a real non-boolean number.") + radius_value = float(radius) + epsilon_value = float(epsilon) + if not np.isfinite(radius_value) or radius_value <= 0: + raise ValueError("radius must be finite and strictly positive.") + if not np.isfinite(epsilon_value) or epsilon_value <= 0 or epsilon_value >= 1: + raise ValueError("epsilon must be finite and between zero and one.") + factor = 1.0 - np.exp(-0.5 * (distance / radius_value) ** 2) + return factor, np.log(np.maximum(factor, epsilon_value)) + + +def _validate_pool(candidate_pool: CandidatePool) -> tuple[np.ndarray, np.ndarray]: + if not isinstance(candidate_pool, CandidatePool): + raise TypeError("candidate_pool must be a CandidatePool.") + X_norm = np.asarray(candidate_pool.X_norm, dtype=float) + X_phys = np.asarray(candidate_pool.X_phys, dtype=float) + if X_norm.ndim != 2 or X_phys.shape != X_norm.shape: + raise ValueError( + "Candidate pool physical and normalized arrays must be (N, D)." + ) + if X_norm.shape[0] == 0 or not np.all(np.isfinite(X_norm)): + raise ValueError("Candidate pool must contain finite rows.") + if not np.all(np.isfinite(X_phys)): + raise ValueError( + "Candidate pool physical rows must contain only finite values." + ) + if np.any(X_norm < -1e-12) or np.any(X_norm > 1 + 1e-12): + raise ValueError("Candidate pool normalized rows must lie in [0, 1].") + grid_indices = np.asarray(candidate_pool.grid_indices) + if grid_indices.shape != X_norm.shape or not np.issubdtype( + grid_indices.dtype, np.integer + ): + raise ValueError( + "Candidate pool grid_indices must be an integer array aligned with " + "physical and normalized rows." + ) + if np.unique(grid_indices, axis=0).shape[0] != X_norm.shape[0]: + raise ValueError("Candidate pool contains duplicate grid-index tuples.") + if np.unique(X_norm, axis=0).shape[0] != X_norm.shape[0]: + raise ValueError("Candidate pool contains duplicate normalized rows.") + if np.unique(X_phys, axis=0).shape[0] != X_phys.shape[0]: + raise ValueError("Candidate pool contains duplicate physical rows.") + return X_norm, X_phys + + +def select_local_penalized_batch( + candidate_pool: CandidatePool, + q: int, + score_remaining: Callable[[np.ndarray, np.ndarray], BaseScoreResult], + config: LocalPenalizationConfig, + *, + observed_pending_norm: np.ndarray | None = None, +) -> BatchSelectionResult: + """Sequentially select exactly ``q`` candidates or fail without relaxation.""" + X_norm, X_phys = _validate_pool(candidate_pool) + if isinstance(q, bool) or not isinstance(q, (int, np.integer)) or int(q) <= 0: + raise ValueError("q must be a positive integer.") + requested = int(q) + if requested > X_norm.shape[0]: + raise UndersizedBatchError( + requested_size=requested, + selected_size=0, + pool_size=X_norm.shape[0], + remaining_candidate_count=X_norm.shape[0], + min_batch_distance=config.min_batch_distance, + min_observed_distance=config.min_observed_distance, + selected_pool_indices=np.empty(0, dtype=int), + hard_valid_candidate_count=X_norm.shape[0], + ) + if not callable(score_remaining): + raise TypeError("score_remaining must be callable.") + weights = _weights(config, X_norm.shape[1]) + if observed_pending_norm is None: + observed = np.empty((0, X_norm.shape[1]), dtype=float) + else: + observed = np.asarray(observed_pending_norm, dtype=float) + if observed.ndim != 2 or observed.shape[1] != X_norm.shape[1]: + raise ValueError( + "observed_pending_norm must have shape " f"(N, {X_norm.shape[1]})." + ) + if not np.all(np.isfinite(observed)): + raise ValueError("observed_pending_norm must contain only finite values.") + if np.any(observed < 0.0) or np.any(observed > 1.0): + raise ValueError("observed_pending_norm must lie within [0, 1].") + + if observed.shape[0]: + observed_distances = _distances(X_norm, observed, weights) + nearest_observed_all = observed_distances.min(axis=1) + else: + nearest_observed_all = np.full(X_norm.shape[0], np.inf) + nearest_selected_all = np.full(X_norm.shape[0], np.inf) + cumulative_log_penalty = np.zeros(X_norm.shape[0], dtype=float) + + remaining = np.arange(X_norm.shape[0], dtype=int) + selected: list[int] = [] + steps: list[SelectionStep] = [] + score_diagnostics: list[dict[str, Any]] = [] + for order in range(1, requested + 1): + selected_array = np.asarray(selected, dtype=int) + scored = score_remaining(remaining.copy(), selected_array.copy()) + if not isinstance(scored, BaseScoreResult): + raise TypeError("score_remaining must return BaseScoreResult.") + base_log = np.asarray(scored.base_log_score, dtype=float) + if base_log.shape != (remaining.size,): + raise ValueError( + "base_log_score must align with remaining_indices; " + f"expected {(remaining.size,)}, got {base_log.shape}." + ) + if np.any(np.isnan(base_log)) or np.any(np.isposinf(base_log)): + raise ValueError("base_log_score may be finite or -inf, not NaN/+inf.") + base_score: np.ndarray | None = None + if scored.base_score is not None: + base_score = np.asarray(scored.base_score, dtype=float) + if base_score.shape != (remaining.size,) or not np.all( + np.isfinite(base_score) + ): + raise ValueError( + "base_score must be finite and align with remaining_indices." + ) + score_diagnostics.append(dict(scored.diagnostics)) + + if selected: + nearest_selected = nearest_selected_all[remaining] + log_penalty = cumulative_log_penalty[remaining] + else: + nearest_selected = np.full(remaining.size, np.inf) + log_penalty = np.zeros(remaining.size, dtype=float) + + nearest_observed = nearest_observed_all[remaining] + + hard_valid = np.ones(remaining.size, dtype=bool) + if selected: + hard_valid &= nearest_selected >= config.min_batch_distance + if observed.shape[0]: + # Exact observed/pending recipes are always forbidden, even when the + # configured distance threshold is explicitly zero. + hard_valid &= nearest_observed > 0.0 + if config.min_observed_distance > 0: + hard_valid &= nearest_observed >= config.min_observed_distance + penalized = base_log + log_penalty + penalized[~hard_valid] = -np.inf + valid_positions = np.flatnonzero(np.isfinite(penalized)) + if valid_positions.size == 0: + raise UndersizedBatchError( + requested_size=requested, + selected_size=len(selected), + pool_size=X_norm.shape[0], + remaining_candidate_count=int(valid_positions.size), + min_batch_distance=config.min_batch_distance, + min_observed_distance=config.min_observed_distance, + selected_pool_indices=np.asarray(selected, dtype=int), + hard_valid_candidate_count=int(np.count_nonzero(hard_valid)), + ) + # np.argmax returns the first maximum. Remaining pool indices preserve + # ascending/stable pool order, which is the documented tie-break. + chosen_position = int(np.argmax(penalized)) + chosen_pool_index = int(remaining[chosen_position]) + chosen_log_penalty = float(log_penalty[chosen_position]) + steps.append( + SelectionStep( + order=order, + pool_index=chosen_pool_index, + base_score=( + None if base_score is None else float(base_score[chosen_position]) + ), + base_log_score=float(base_log[chosen_position]), + penalty_factor=float(np.exp(chosen_log_penalty)), + log_penalty=chosen_log_penalty, + penalized_log_score=float(penalized[chosen_position]), + nearest_selected_distance_before=( + None if not selected else float(nearest_selected[chosen_position]) + ), + nearest_observed_distance=( + None + if not observed.shape[0] + else float(nearest_observed[chosen_position]) + ), + ) + ) + selected.append(chosen_pool_index) + remaining = np.delete(remaining, chosen_position) + new_distances = _distances( + X_norm, X_norm[chosen_pool_index : chosen_pool_index + 1], weights + )[:, 0] + nearest_selected_all = np.minimum(nearest_selected_all, new_distances) + if config.radius is not None: + _, new_log_penalty = soft_local_penalty( + new_distances, radius=config.radius, epsilon=config.epsilon + ) + cumulative_log_penalty += new_log_penalty + + selected_array = np.asarray(selected, dtype=int) + selected_distances = _distances( + X_norm[selected_array], X_norm[selected_array], weights + ) + if requested >= 2: + triangle = selected_distances[np.triu_indices(requested, k=1)] + within = { + "minimum_within_batch_distance": float(triangle.min()), + "mean_within_batch_distance": float(triangle.mean()), + "maximum_within_batch_distance": float(triangle.max()), + } + else: + within = { + "minimum_within_batch_distance": None, + "mean_within_batch_distance": None, + "maximum_within_batch_distance": None, + } + return BatchSelectionResult( + X_norm=X_norm[selected_array].copy(), + X_phys=X_phys[selected_array].copy(), + selected_pool_indices=selected_array, + steps=tuple(steps), + method_diagnostics={"score_steps": score_diagnostics}, + distance_diagnostics={ + "pairwise_distance_matrix": selected_distances, + **within, + "dimension_weights": weights.copy(), + "radius": config.radius, + "min_batch_distance": config.min_batch_distance, + "min_observed_distance": config.min_observed_distance, + }, + ) + + +__all__ = [ + "BaseScoreResult", + "BatchSelectionResult", + "LocalPenalizationConfig", + "SelectionStep", + "UndersizedBatchError", + "select_local_penalized_batch", + "soft_local_penalty", +] diff --git a/src/mobo_kit/candidate_diagnostics.py b/src/mobo_kit/candidate_diagnostics.py new file mode 100644 index 0000000..16462d6 --- /dev/null +++ b/src/mobo_kit/candidate_diagnostics.py @@ -0,0 +1,367 @@ +"""Numerical and plotting diagnostics for discrete candidate batches. + +All distances in this module are defined in normalized input space. Plotting +helpers are intentionally file-oriented and use a headless Matplotlib backend +so they are safe in automated, CPU-only campaign checks. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, Mapping, Sequence + +import matplotlib +import numpy as np +from sklearn.decomposition import PCA + +matplotlib.use("Agg") +from matplotlib import pyplot as plt # noqa: E402 + +from .design import Design + + +def _matrix(value: np.ndarray, *, name: str) -> np.ndarray: + array = np.asarray(value, dtype=float) + if array.ndim != 2: + raise ValueError(f"{name} must have shape (N, D); got {array.shape}.") + if not np.all(np.isfinite(array)): + raise ValueError(f"{name} must contain only finite values.") + return array + + +def _weights(dimension_weights: np.ndarray | None, dimension: int) -> np.ndarray: + if dimension_weights is None: + return np.ones(dimension, dtype=float) + weights = np.asarray(dimension_weights, dtype=float) + if weights.shape != (dimension,): + raise ValueError( + "dimension_weights must have shape " f"({dimension},); got {weights.shape}." + ) + if not np.all(np.isfinite(weights)) or np.any(weights <= 0): + raise ValueError("dimension_weights must be finite and strictly positive.") + return weights + + +def pairwise_normalized_distances( + X_norm: np.ndarray, + *, + dimension_weights: np.ndarray | None = None, +) -> np.ndarray: + """Return the weighted Euclidean pairwise-distance matrix for ``(N, D)``.""" + X = _matrix(X_norm, name="X_norm") + weights = _weights(dimension_weights, X.shape[1]) + differences = X[:, None, :] - X[None, :, :] + return np.sqrt(np.sum(weights * differences**2, axis=-1)) + + +def nearest_reference_distances( + X_norm: np.ndarray, + reference_norm: np.ndarray | None, + *, + dimension_weights: np.ndarray | None = None, +) -> np.ndarray: + """Return each candidate's nearest observed/pending normalized distance.""" + X = _matrix(X_norm, name="X_norm") + weights = _weights(dimension_weights, X.shape[1]) + if reference_norm is None: + return np.full(X.shape[0], np.nan, dtype=float) + reference = _matrix(reference_norm, name="reference_norm") + if reference.shape[1] != X.shape[1]: + raise ValueError( + "reference_norm and X_norm must have the same final dimension." + ) + if reference.shape[0] == 0: + return np.full(X.shape[0], np.nan, dtype=float) + differences = X[:, None, :] - reference[None, :, :] + distances = np.sqrt(np.sum(weights * differences**2, axis=-1)) + return distances.min(axis=1) + + +def grid_membership_mask( + X_phys: np.ndarray, + design: Design, + *, + atol: float = 1e-9, +) -> np.ndarray: + """Return a per-row mask indicating exact membership in every design grid.""" + X = _matrix(X_phys, name="X_phys") + dimension = len(design.var_array) + if X.shape[1] != dimension: + raise ValueError( + f"X_phys has {X.shape[1]} columns but the design has {dimension}." + ) + if not np.isfinite(atol) or atol < 0: + raise ValueError("atol must be finite and non-negative.") + valid = np.ones(X.shape[0], dtype=bool) + for column, grid in enumerate(design.var_array): + grid_values = np.asarray(grid, dtype=float) + valid &= np.any( + np.isclose( + X[:, column, None], + grid_values[None, :], + rtol=0.0, + atol=atol, + ), + axis=1, + ) + return valid + + +def boundary_flags(X_norm: np.ndarray, *, atol: float = 1e-12) -> np.ndarray: + """Flag candidate dimensions lying on either normalized boundary.""" + X = _matrix(X_norm, name="X_norm") + if not np.isfinite(atol) or atol < 0: + raise ValueError("atol must be finite and non-negative.") + return np.isclose(X, 0.0, rtol=0.0, atol=atol) | np.isclose( + X, 1.0, rtol=0.0, atol=atol + ) + + +@dataclass(frozen=True) +class BatchDistanceDiagnostics: + """Compact distance and validity summary for a selected batch.""" + + pairwise_distance_matrix: np.ndarray + minimum_within_batch_distance: float | None + mean_within_batch_distance: float | None + maximum_within_batch_distance: float | None + nearest_observed_pending_distance: np.ndarray + duplicate_row_pairs: tuple[tuple[int, int], ...] + grid_valid_rows: np.ndarray | None + boundary_flags: np.ndarray + metadata: dict[str, Any] + + def as_dict(self) -> dict[str, Any]: + """Return a serialization-friendly shallow mapping.""" + return asdict(self) + + +def summarize_candidate_batch( + X_norm: np.ndarray, + *, + observed_pending_norm: np.ndarray | None = None, + X_phys: np.ndarray | None = None, + design: Design | None = None, + dimension_weights: np.ndarray | None = None, + duplicate_atol: float = 1e-12, + metadata: Mapping[str, Any] | None = None, +) -> BatchDistanceDiagnostics: + """Compute batch-only ``O(q^2 D)`` diagnostics and reference distances.""" + X = _matrix(X_norm, name="X_norm") + if not np.isfinite(duplicate_atol) or duplicate_atol < 0: + raise ValueError("duplicate_atol must be finite and non-negative.") + distances = pairwise_normalized_distances(X, dimension_weights=dimension_weights) + if X.shape[0] >= 2: + triangle = distances[np.triu_indices(X.shape[0], k=1)] + minimum = float(triangle.min()) + mean = float(triangle.mean()) + maximum = float(triangle.max()) + else: + minimum = mean = maximum = None + + duplicate_pairs: list[tuple[int, int]] = [] + for left in range(X.shape[0]): + for right in range(left + 1, X.shape[0]): + if np.allclose(X[left], X[right], rtol=0.0, atol=duplicate_atol): + duplicate_pairs.append((left, right)) + + if (X_phys is None) != (design is None): + raise ValueError("X_phys and design must be supplied together.") + if X_phys is not None: + physical = _matrix(X_phys, name="X_phys") + if physical.shape[0] != X.shape[0]: + raise ValueError("X_phys and X_norm must contain the same row count.") + else: + physical = None + grid_valid = ( + None + if physical is None + else grid_membership_mask(physical, design) # type: ignore[arg-type] + ) + return BatchDistanceDiagnostics( + pairwise_distance_matrix=distances, + minimum_within_batch_distance=minimum, + mean_within_batch_distance=mean, + maximum_within_batch_distance=maximum, + nearest_observed_pending_distance=nearest_reference_distances( + X, + observed_pending_norm, + dimension_weights=dimension_weights, + ), + duplicate_row_pairs=tuple(duplicate_pairs), + grid_valid_rows=grid_valid, + boundary_flags=boundary_flags(X), + metadata={} if metadata is None else dict(metadata), + ) + + +def _output_path(output_path: str | Path) -> Path: + path = Path(output_path) + path.parent.mkdir(parents=True, exist_ok=True) + return path + + +def _save_figure(fig: Any, path: Path, watermark: str | None) -> None: + metadata: dict[str, str] | None = None + if watermark is not None: + if not isinstance(watermark, str) or not watermark.strip(): + raise ValueError("watermark must be a nonblank string when provided.") + label = watermark.strip() + fig.text( + 0.5, + 0.015, + label, + ha="center", + va="bottom", + color="firebrick", + fontsize=9, + fontweight="bold", + bbox={"facecolor": "white", "edgecolor": "firebrick", "alpha": 0.9}, + ) + metadata = {"Description": label} + fig.tight_layout(rect=(0.0, 0.06, 1.0, 1.0) if watermark else None) + fig.savefig(path, dpi=160, metadata=metadata) + plt.close(fig) + + +def plot_candidate_pca( + observed_norm: np.ndarray, + selected_norm: np.ndarray, + output_path: str | Path, + *, + pool_norm: np.ndarray | None = None, + pool_sample_size: int = 1000, + seed: int = 0, + watermark: str | None = None, +) -> Path: + """Save a two-component PCA view of observed, pool, and selected points.""" + observed = _matrix(observed_norm, name="observed_norm") + selected = _matrix(selected_norm, name="selected_norm") + if observed.shape[1] != selected.shape[1]: + raise ValueError("observed_norm and selected_norm dimensions must match.") + groups: list[tuple[str, np.ndarray]] = [("Observed", observed)] + if pool_norm is not None: + pool = _matrix(pool_norm, name="pool_norm") + if pool.shape[1] != observed.shape[1]: + raise ValueError("pool_norm and observed_norm dimensions must match.") + if pool_sample_size <= 0: + raise ValueError("pool_sample_size must be positive.") + if pool.shape[0] > pool_sample_size: + rng = np.random.default_rng(seed) + indices = np.sort( + rng.choice(pool.shape[0], size=pool_sample_size, replace=False) + ) + pool = pool[indices] + groups.append(("Pool", pool)) + groups.append(("Selected", selected)) + combined = np.vstack([values for _, values in groups]) + if combined.shape[0] < 2 or combined.shape[1] < 2: + raise ValueError("PCA plot requires at least two rows and two inputs.") + projected = PCA(n_components=2).fit_transform(combined) + + path = _output_path(output_path) + fig, axis = plt.subplots(figsize=(7, 5)) + offset = 0 + styles: Mapping[str, Mapping[str, Any]] = { + "Observed": {"marker": "o", "alpha": 0.75, "s": 38}, + "Pool": {"marker": ".", "alpha": 0.2, "s": 14}, + "Selected": {"marker": "*", "alpha": 1.0, "s": 130}, + } + for label, values in groups: + count = values.shape[0] + points = projected[offset : offset + count] + axis.scatter(points[:, 0], points[:, 1], label=label, **styles[label]) + offset += count + axis.set(xlabel="PC1", ylabel="PC2", title="Candidate acquisition in input space") + axis.legend() + _save_figure(fig, path, watermark) + return path + + +def plot_parallel_coordinates( + selected_norm: np.ndarray, + input_names: Sequence[str], + output_path: str | Path, + *, + watermark: str | None = None, +) -> Path: + """Save normalized selected conditions as a parallel-coordinates plot.""" + selected = _matrix(selected_norm, name="selected_norm") + if len(input_names) != selected.shape[1]: + raise ValueError("input_names must match the selected input dimension.") + path = _output_path(output_path) + fig, axis = plt.subplots(figsize=(max(8, selected.shape[1]), 4.5)) + positions = np.arange(selected.shape[1]) + for row_index, row in enumerate(selected): + axis.plot(positions, row, marker="o", label=f"Selection {row_index + 1}") + axis.set_xticks(positions, input_names, rotation=40, ha="right") + axis.set_ylim(-0.03, 1.03) + axis.set_ylabel("Normalized condition") + axis.set_title("Selected candidate conditions") + axis.legend(ncol=min(3, max(1, selected.shape[0]))) + _save_figure(fig, path, watermark) + return path + + +def plot_distance_heatmap( + X_norm: np.ndarray, + output_path: str | Path, + *, + dimension_weights: np.ndarray | None = None, + watermark: str | None = None, +) -> Path: + """Save the within-batch normalized-distance matrix as a heatmap.""" + matrix = pairwise_normalized_distances(X_norm, dimension_weights=dimension_weights) + path = _output_path(output_path) + fig, axis = plt.subplots(figsize=(5.5, 4.8)) + image = axis.imshow(matrix, cmap="viridis") + labels = [str(index + 1) for index in range(matrix.shape[0])] + axis.set_xticks(range(matrix.shape[0]), labels) + axis.set_yticks(range(matrix.shape[0]), labels) + axis.set(xlabel="Selection", ylabel="Selection", title="Batch distances") + for row in range(matrix.shape[0]): + for column in range(matrix.shape[1]): + axis.text( + column, + row, + f"{matrix[row, column]:.2f}", + ha="center", + va="center", + color="white" if matrix[row, column] < matrix.max() * 0.55 else "black", + fontsize=8, + ) + fig.colorbar(image, ax=axis, label="Normalized Euclidean distance") + _save_figure(fig, path, watermark) + return path + + +def plot_selection_scores( + selection_order: Sequence[int], + base_log_scores: Sequence[float], + penalized_log_scores: Sequence[float], + output_path: str | Path, + *, + watermark: str | None = None, +) -> Path: + """Save base-versus-penalized acquisition values by selection order.""" + order = np.asarray(selection_order) + base = np.asarray(base_log_scores, dtype=float) + penalized = np.asarray(penalized_log_scores, dtype=float) + if order.ndim != 1 or base.shape != order.shape or penalized.shape != order.shape: + raise ValueError("selection order and score arrays must share shape (q,).") + if not np.all(np.isfinite(base)) or not np.all(np.isfinite(penalized)): + raise ValueError("selection scores must be finite.") + path = _output_path(output_path) + fig, axis = plt.subplots(figsize=(6.5, 4.2)) + axis.plot(order, base, marker="o", label="Base log acquisition") + axis.plot(order, penalized, marker="s", label="Penalized log acquisition") + axis.set( + xlabel="Selection order", + ylabel="Log acquisition", + title="Sequential acquisition and local penalty", + ) + axis.set_xticks(order) + axis.legend() + _save_figure(fig, path, watermark) + return path diff --git a/src/mobo_kit/candidate_pool.py b/src/mobo_kit/candidate_pool.py new file mode 100644 index 0000000..fd4927e --- /dev/null +++ b/src/mobo_kit/candidate_pool.py @@ -0,0 +1,247 @@ +"""Deterministic sampling of finite pools from very large discrete designs.""" + +from __future__ import annotations + +from dataclasses import dataclass +from math import prod +from typing import Sequence + +import numpy as np + +from .constraints import RowConstraint, apply_row_constraints +from .design import Design + + +@dataclass(frozen=True) +class CandidatePool: + """A discrete candidate pool in grid-index, physical, and normalized spaces.""" + + grid_indices: np.ndarray + X_phys: np.ndarray + X_norm: np.ndarray + seed: int + draws: int + rejected_duplicate: int + rejected_avoid: int + rejected_constraint: int + + @property + def size(self) -> int: + return int(self.grid_indices.shape[0]) + + +class CandidatePoolSamplingError(RuntimeError): + """Raised when an exact-size discrete pool cannot be produced safely.""" + + def __init__( + self, + *, + requested: int, + accepted: int, + draws: int, + max_draws: int, + rejected_duplicate: int, + rejected_avoid: int, + rejected_constraint: int, + reason: str, + ) -> None: + self.requested = requested + self.accepted = accepted + self.draws = draws + self.max_draws = max_draws + self.rejected_duplicate = rejected_duplicate + self.rejected_avoid = rejected_avoid + self.rejected_constraint = rejected_constraint + self.reason = reason + super().__init__( + "Could not sample the requested discrete candidate pool: " + f"requested={requested}, accepted={accepted}, draws={draws}, " + f"max_draws={max_draws}, duplicate_rejections={rejected_duplicate}, " + f"avoid_rejections={rejected_avoid}, " + f"constraint_rejections={rejected_constraint}. {reason}" + ) + + +def _design_grids(design: Design) -> tuple[np.ndarray, ...]: + if not isinstance(design, Design): + raise TypeError("design must be a Design.") + grids = tuple(np.asarray(grid, dtype=float) for grid in design.var_array) + if not grids or len(grids) != len(design.names): + raise ValueError("design must contain one non-empty grid per input name.") + for name, grid in zip(design.names, grids): + if grid.ndim != 1 or grid.size == 0: + raise ValueError(f"Design grid {name!r} must be a non-empty vector.") + if not np.all(np.isfinite(grid)) or np.unique(grid).size != grid.size: + raise ValueError(f"Design grid {name!r} must be finite and unique.") + return grids + + +def _physical_matrix( + value: np.ndarray | None, *, name: str, dimension: int +) -> np.ndarray: + if value is None: + return np.empty((0, dimension), dtype=float) + array = np.asarray(value, dtype=float) + if array.ndim != 2 or array.shape[1] != dimension: + raise ValueError(f"{name} must have shape (N, {dimension}); got {array.shape}.") + if not np.all(np.isfinite(array)): + raise ValueError(f"{name} must contain only finite values.") + return array + + +def physical_rows_to_grid_indices( + X_phys: np.ndarray, + design: Design, + *, + atol: float = 0.0, +) -> np.ndarray: + """Convert physical rows to exact integer grid tuples or fail off-grid.""" + grids = _design_grids(design) + X = _physical_matrix(X_phys, name="X_phys", dimension=len(grids)) + if not np.isfinite(atol) or atol < 0: + raise ValueError("atol must be finite and non-negative.") + indices = np.empty(X.shape, dtype=np.int64) + for column, (name, grid) in enumerate(zip(design.names, grids)): + differences = np.abs(X[:, column, None] - grid[None, :]) + closest = differences.argmin(axis=1) + invalid = differences[np.arange(X.shape[0]), closest] > atol + if np.any(invalid): + rows = np.flatnonzero(invalid).tolist() + raise ValueError(f"Physical rows {rows} are off-grid for input {name!r}.") + indices[:, column] = closest + return indices + + +def _indices_to_physical( + grid_indices: np.ndarray, grids: tuple[np.ndarray, ...] +) -> np.ndarray: + physical = np.empty(grid_indices.shape, dtype=float) + for column, grid in enumerate(grids): + physical[:, column] = grid[grid_indices[:, column]] + return physical + + +def _normalize_physical(X_phys: np.ndarray, design: Design) -> np.ndarray: + lower = np.asarray(design.lowers, dtype=float) + upper = np.asarray(design.uppers, dtype=float) + spans = upper - lower + normalized = np.zeros_like(X_phys, dtype=float) + changing = spans > 0 + normalized[:, changing] = (X_phys[:, changing] - lower[changing]) / spans[changing] + return normalized + + +def sample_discrete_candidate_pool( + design: Design, + pool_size: int, + *, + seed: int, + observed_phys: np.ndarray | None = None, + pending_phys: np.ndarray | None = None, + avoid_phys: np.ndarray | None = None, + row_constraints: Sequence[RowConstraint] | None = None, + max_draws: int | None = None, +) -> CandidatePool: + """Sample an exact-size unique pool without allocating the Cartesian grid.""" + grids = _design_grids(design) + if isinstance(pool_size, bool) or not isinstance(pool_size, (int, np.integer)): + raise ValueError("pool_size must be a positive integer.") + requested = int(pool_size) + if requested <= 0: + raise ValueError("pool_size must be a positive integer.") + if ( + isinstance(seed, (bool, np.bool_)) + or not isinstance(seed, (int, np.integer)) + or int(seed) < 0 + ): + raise ValueError("seed must be a non-negative integer.") + if max_draws is None: + draw_limit = max(1000, requested * 50) + elif isinstance(max_draws, bool) or not isinstance(max_draws, (int, np.integer)): + raise ValueError("max_draws must be a positive integer.") + else: + draw_limit = int(max_draws) + if draw_limit <= 0: + raise ValueError("max_draws must be a positive integer.") + + dimension = len(grids) + avoid_rows = np.vstack( + [ + _physical_matrix(observed_phys, name="observed_phys", dimension=dimension), + _physical_matrix(pending_phys, name="pending_phys", dimension=dimension), + _physical_matrix(avoid_phys, name="avoid_phys", dimension=dimension), + ] + ) + avoid_indices = physical_rows_to_grid_indices(avoid_rows, design) + avoid_set = {tuple(int(value) for value in row) for row in avoid_indices} + total_grid_size = prod(int(grid.size) for grid in grids) + available_without_constraints = total_grid_size - len(avoid_set) + if requested > available_without_constraints: + raise CandidatePoolSamplingError( + requested=requested, + accepted=0, + draws=0, + max_draws=draw_limit, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + reason=( + "The request exceeds the number of grid tuples remaining after " + f"explicit exclusions ({available_without_constraints})." + ), + ) + + generator = np.random.default_rng(int(seed)) + axis_sizes = np.asarray([grid.size for grid in grids], dtype=np.int64) + seen: set[tuple[int, ...]] = set() + accepted_indices: list[tuple[int, ...]] = [] + draws = rejected_duplicate = rejected_avoid = rejected_constraint = 0 + while len(accepted_indices) < requested and draws < draw_limit: + index_tuple = tuple(int(generator.integers(0, high)) for high in axis_sizes) + draws += 1 + if index_tuple in seen: + rejected_duplicate += 1 + continue + seen.add(index_tuple) + if index_tuple in avoid_set: + rejected_avoid += 1 + continue + row_indices = np.asarray(index_tuple, dtype=np.int64)[None, :] + row_physical = _indices_to_physical(row_indices, grids) + if not bool(apply_row_constraints(row_physical, design, row_constraints)[0]): + rejected_constraint += 1 + continue + accepted_indices.append(index_tuple) + + if len(accepted_indices) != requested: + raise CandidatePoolSamplingError( + requested=requested, + accepted=len(accepted_indices), + draws=draws, + max_draws=draw_limit, + rejected_duplicate=rejected_duplicate, + rejected_avoid=rejected_avoid, + rejected_constraint=rejected_constraint, + reason="Maximum draws reached; constraints and exclusions were not relaxed.", + ) + index_array = np.asarray(accepted_indices, dtype=np.int64) + physical = _indices_to_physical(index_array, grids) + normalized = _normalize_physical(physical, design) + return CandidatePool( + grid_indices=index_array, + X_phys=physical, + X_norm=normalized, + seed=int(seed), + draws=draws, + rejected_duplicate=rejected_duplicate, + rejected_avoid=rejected_avoid, + rejected_constraint=rejected_constraint, + ) + + +__all__ = [ + "CandidatePool", + "CandidatePoolSamplingError", + "physical_rows_to_grid_indices", + "sample_discrete_candidate_pool", +] diff --git a/src/mobo_kit/cli.py b/src/mobo_kit/cli.py index 331ccc9..bbe9386 100644 --- a/src/mobo_kit/cli.py +++ b/src/mobo_kit/cli.py @@ -15,7 +15,7 @@ def main(): """ Command line interface for MOBO-Kit. - + Usage examples: mobo-kit run --csv data/my_data.csv mobo-kit generate --config configs/my_config.yaml --n-samples 20 --out initial_experiments.csv @@ -37,87 +37,123 @@ def main(): # Run with specific device and seed mobo-kit run --csv data/my_data.csv --device cpu --seed 123 --verbose - # Run with custom batch size for acquisition - mobo-kit run --csv data/my_data.csv --batch-size 10 --num-restarts 50 - """ + # Step 2A analysis is allowed, but campaign proposal remains blocked until + # the reviewed Step 2B adapter is implemented. + mobo-kit run --csv data/my_data.csv --config configs/my_config.yaml + """, ) - - subparsers = parser.add_subparsers(dest='command', help='Available commands') - + + subparsers = parser.add_subparsers(dest="command", help="Available commands") + # Generate subcommand - generate_parser = subparsers.add_parser('generate', help='Generate initial experiments using LHS') - generate_parser.add_argument('--config', required=True, help='Path to YAML configuration file') - generate_parser.add_argument('--n-samples', type=int, required=True, help='Number of initial experiments to generate') - generate_parser.add_argument('--out', required=True, help='Output CSV file path') - generate_parser.add_argument('--seed', type=int, default=42, help='Random seed for reproducibility (default: 42)') - generate_parser.add_argument('--max-corr', type=float, help='Maximum absolute correlation between variables') - generate_parser.add_argument('--max-attempts', type=int, default=100, help='Maximum attempts for LHS generation (default: 100)') - generate_parser.add_argument('--verbose', action='store_true', help='Enable verbose output') - + generate_parser = subparsers.add_parser( + "generate", help="Generate initial experiments using LHS" + ) + generate_parser.add_argument( + "--config", required=True, help="Path to YAML configuration file" + ) + generate_parser.add_argument( + "--n-samples", + type=int, + required=True, + help="Number of initial experiments to generate", + ) + generate_parser.add_argument("--out", required=True, help="Output CSV file path") + generate_parser.add_argument( + "--seed", + type=int, + default=42, + help="Random seed for reproducibility (default: 42)", + ) + generate_parser.add_argument( + "--max-corr", type=float, help="Maximum absolute correlation between variables" + ) + generate_parser.add_argument( + "--max-attempts", + type=int, + default=100, + help="Maximum attempts for LHS generation (default: 100)", + ) + generate_parser.add_argument( + "--verbose", action="store_true", help="Enable verbose output" + ) + # Run subcommand - run_parser = subparsers.add_parser('run', help='Run MOBO optimization with existing data') + run_parser = subparsers.add_parser( + "run", help="Run MOBO optimization with existing data" + ) run_parser.add_argument( - "--csv", - required=True, - help="Path to CSV file with experimental data" + "--csv", required=True, help="Path to CSV file with experimental data" ) - + # Optional arguments for run command run_parser.add_argument( "--config", - help="Path to YAML configuration file (optional, will auto-generate from CSV if not provided)" + help="Path to YAML configuration file (optional, will auto-generate from CSV if not provided)", ) - + run_parser.add_argument( "--out", - default="results/experiment", - help="Output directory for results (default: results/experiment)" + default="local_outputs/experiment", + help="Output directory for results (default: local_outputs/experiment)", ) - + run_parser.add_argument( "--seed", type=int, default=42, - help="Random seed for reproducibility (default: 42)" + help="Random seed for reproducibility (default: 42)", ) - + run_parser.add_argument( "--device", choices=["auto", "cpu", "cuda"], default="auto", - help="Device to use for computation (default: auto)" + help="Device to use for computation (default: auto)", + ) + + run_parser.add_argument( + "--verbose", action="store_true", help="Enable verbose output" ) - + + # Retained only to fail closed with a clear migration message. The legacy + # qNEHVI implementation is not a Step 2A production adapter. run_parser.add_argument( - "--verbose", + "--propose-candidates", action="store_true", - help="Enable verbose output" + help="Blocked in Step 2A; requires a reviewed Step 2B campaign adapter", ) - - # Advanced options (for future expansion) + + run_parser.add_argument( + "--reference-point", + type=float, + nargs="+", + help="One approved hypervolume reference value per transformed objective", + ) + run_parser.add_argument( "--batch-size", type=int, default=5, - help="Batch size for acquisition (default: 5)" + help="Batch size for acquisition (default: 5)", ) - + run_parser.add_argument( "--num-restarts", type=int, default=20, - help="Number of restarts for acquisition optimization (default: 20)" + help="Number of restarts for acquisition optimization (default: 20)", ) - + args = parser.parse_args() - + # Handle subcommands - if args.command == 'generate': + if args.command == "generate": # Validate inputs for generate command if not os.path.exists(args.config): print(f"Error: Config file not found: {args.config}") sys.exit(1) - + try: # Generate initial experiments results = generate_initial_experiments( @@ -127,42 +163,44 @@ def main(): seed=args.seed, verbose=args.verbose, max_abs_corr=args.max_corr, - max_attempts=args.max_attempts + max_attempts=args.max_attempts, ) - + if args.verbose: print("\n" + "=" * 60) print("GENERATION SUMMARY:") print(f" Config: {args.config}") print(f" Samples: {results['n_samples']}") print(f" Variables: {', '.join(results['variables'])}") - print(f" Constraints: {'Applied' if results['constraints_applied'] else 'None'}") + print( + f" Constraints: {'Applied' if results['constraints_applied'] else 'None'}" + ) print(f" Seed: {results['seed']}") print(f" Output: {results['save_path']}") print("=" * 60) else: - print(f"Success! Generated {results['n_samples']} experiments in {results['save_path']}") - + print( + f"Success! Generated {results['n_samples']} experiments in {results['save_path']}" + ) + except Exception as e: print(f"Error: {e}") if args.verbose: import traceback + traceback.print_exc() sys.exit(1) - - elif args.command == 'run': + + elif args.command == "run": # Validate inputs for run command if not os.path.exists(args.csv): print(f"Error: CSV file not found: {args.csv}") sys.exit(1) - + if args.config and not os.path.exists(args.config): print(f"Error: Config file not found: {args.config}") sys.exit(1) - - # Create output directory - os.makedirs(args.out, exist_ok=True) - + try: # Run the experiment results = run_mobo_experiment( @@ -172,9 +210,12 @@ def main(): seed=args.seed, device=args.device, verbose=args.verbose, - batch_size=args.batch_size + batch_size=args.batch_size, + propose_candidates=args.propose_candidates, + reference_point=args.reference_point, + num_restarts=args.num_restarts, ) - + # Print summary if args.verbose: print("\n" + "=" * 60) @@ -188,19 +229,20 @@ def main(): print(f" Device: {results['device']}") print(f" Seed: {results['seed']}") print(f" Results: {results['save_dir']}") - if results.get('candidates'): + if results.get("candidates"): print(f" Next batch: {args.batch_size} candidates proposed") print("=" * 60) else: success_msg = f"Success! Results saved to {results['save_dir']}" - if results.get('candidates'): + if results.get("candidates"): success_msg += f" ({args.batch_size} candidates proposed)" print(success_msg) - + except Exception as e: print(f"Error: {e}") if args.verbose: import traceback + traceback.print_exc() sys.exit(1) else: diff --git a/src/mobo_kit/constraints.py b/src/mobo_kit/constraints.py index 4e34860..3edb0a0 100644 --- a/src/mobo_kit/constraints.py +++ b/src/mobo_kit/constraints.py @@ -1,124 +1,128 @@ -# src/constraints.py +"""Opt-in physical-space constraints for campaign designs.""" + from __future__ import annotations -from typing import Callable, Dict, List, Optional, Sequence -import numpy as np -# Public type: row-wise constraint (PHYSICAL units in, boolean mask out) -RowConstraint = Callable[[np.ndarray, "Design"], np.ndarray] +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Sequence +import numpy as np -# ========================= -# Clausius–Clapeyron (C → K) -# ========================= +if TYPE_CHECKING: + from .design import Design -def check_clausius_clapeyron_np(ah_vals, temp_c_vals) -> np.ndarray: - """ - Return boolean mask for valid points satisfying Clausius–Clapeyron constraint. - Ensures absolute humidity (g/m^3) does not exceed saturation at given temperature. - Parameters - ---------- - ah_vals : array-like, absolute humidity [g/m^3] - temp_c_vals : array-like, temperature [°C] (converted to K internally) +RowConstraint = Callable[[np.ndarray, "Design"], np.ndarray] - Returns - ------- - valid_mask : np.ndarray[bool] - """ - AH = np.asarray(ah_vals, dtype=float) - T_c = np.asarray(temp_c_vals, dtype=float) # Celsius in - T = T_c + 273.15 # Kelvin - # Saturation vapor pressure (kPa) - es = 0.6113 * np.exp((17.27 * (T - 273.15)) / (T - 35.86)) +def check_clausius_clapeyron_np(ah_vals, temp_c_vals) -> np.ndarray: + """Return a validity mask for the Clausius-Clapeyron constraint. - # Max absolute humidity (g/m^3) - AH_max = es / (4.61e-4 * T) + Absolute humidity is expressed in g/m^3 and temperature in degrees C. + The calculation converts temperature to kelvin internally. + """ - return (AH <= AH_max) & np.isfinite(AH_max) + absolute_humidity = np.asarray(ah_vals, dtype=float) + temperature_k = np.asarray(temp_c_vals, dtype=float) + 273.15 + saturation_pressure = 0.6113 * np.exp( + (17.27 * (temperature_k - 273.15)) / (temperature_k - 35.86) + ) + maximum_absolute_humidity = saturation_pressure / (4.61e-4 * temperature_k) + return (absolute_humidity <= maximum_absolute_humidity) & np.isfinite( + maximum_absolute_humidity + ) -# ========================= -# Builders (one per supported constraint key) -# ========================= def _idx_for(name: str, design: "Design") -> int: try: return design.names.index(name) - except ValueError as e: + except ValueError as exc: raise KeyError( - f"Constraint refers to column '{name}', but it is not in design.names={design.names}" - ) from e + f"Constraint refers to column '{name}', but it is not in " + f"design.names={design.names}." + ) from exc -def _build_cc(spec: Dict, design: "Design") -> RowConstraint: - ah_col = spec.get("ah_col") - t_col = spec.get("temp_c_col") - if ah_col is None or t_col is None: +def _build_clausius_clapeyron(specification: Dict, design: "Design") -> RowConstraint: + absolute_humidity_column = specification.get("ah_col") + temperature_column = specification.get("temp_c_col") + if absolute_humidity_column is None or temperature_column is None: raise KeyError( - "Clausius–Clapeyron constraint needs 'absolute_humidity_col' and 'temperature_col'." + "Clausius-Clapeyron constraint requires 'ah_col' and 'temp_c_col'." ) - i_ah = _idx_for(ah_col, design) - i_tC = _idx_for(t_col, design) + absolute_humidity_index = _idx_for(absolute_humidity_column, design) + temperature_index = _idx_for(temperature_column, design) def row_constraint_fn(X: np.ndarray, _design: "Design") -> np.ndarray: - return check_clausius_clapeyron_np(X[:, i_ah], X[:, i_tC]) + return check_clausius_clapeyron_np( + X[:, absolute_humidity_index], X[:, temperature_index] + ) return row_constraint_fn -# Map of **boolean keys** in YAML → builder _SUPPORTED_BOOL_KEYS = { - "clausius_clapeyron": _build_cc, - # add more: "my_constraint_key": _build_my_constraint + "clausius_clapeyron": _build_clausius_clapeyron, } -# ========================= -# Config parsing & application -# ========================= - def constraints_from_config(cfg: Dict, design: "Design") -> List[RowConstraint]: - """ - Parse a YAML layout like: + """Build only explicitly configured campaign constraints. - constraints: - - clausius_clapeyron: true - absolute_humidity_col: "absolute_humidity" - temperature_col: "temperature_c" + A supported entry has one boolean type flag and the parameters required by + that type, for example:: - - clausius_clapeyron: false # ignored - ... + constraints: + - clausius_clapeyron: true + ah_col: absolute_humidity + temp_c_col: temperature_c - Returns a list of row-constraint callables (possibly empty). + Missing ``constraints`` and an empty list both mean no constraints. Invalid + explicit entries fail instead of being silently ignored. """ + + if not isinstance(cfg, dict): + raise TypeError("Constraint configuration must be a mapping.") + items = cfg.get("constraints", []) if not items: return [] + if not isinstance(items, list): + raise TypeError("Config 'constraints' must be a list of mappings.") - fns: List[RowConstraint] = [] - for raw in items: + constraints: List[RowConstraint] = [] + for index, raw in enumerate(items): if not isinstance(raw, dict): - continue + raise TypeError( + f"Constraint entry at index {index} must be a mapping; " + f"got {type(raw).__name__}." + ) - # find which supported boolean key (if any) is enabled - chosen_key = None - for key, builder in _SUPPORTED_BOOL_KEYS.items(): - val = raw.get(key, None) - if isinstance(val, bool) and val: - chosen_key = key - break + known_keys = [key for key in _SUPPORTED_BOOL_KEYS if key in raw] + if not known_keys: + raise KeyError( + f"Constraint entry at index {index} has no supported type; " + f"expected one of {sorted(_SUPPORTED_BOOL_KEYS)}." + ) + if len(known_keys) > 1: + raise ValueError( + f"Constraint entry at index {index} enables multiple types: " + f"{known_keys}. Use one constraint type per entry." + ) - if chosen_key is None: - # no supported boolean flag set to true -> skip this entry + chosen_key = known_keys[0] + enabled = raw[chosen_key] + if not isinstance(enabled, bool): + raise TypeError( + f"Constraint flag '{chosen_key}' must be true or false; " + f"got {enabled!r}." + ) + if not enabled: continue - # build constraint from the same dict (which also holds column names, etc.) - builder = _SUPPORTED_BOOL_KEYS[chosen_key] - fns.append(builder(raw, design)) + constraints.append(_SUPPORTED_BOOL_KEYS[chosen_key](raw, design)) - return fns + return constraints def apply_row_constraints( @@ -126,21 +130,44 @@ def apply_row_constraints( design: "Design", constraints: Optional[Sequence[RowConstraint]], ) -> np.ndarray: - """ - Apply zero or more row-wise constraints; returns a boolean mask AND-ing all. - If constraints is None or empty, returns all True. - """ - n = X_phys.shape[0] - if not constraints: - return np.ones(n, dtype=bool) + """AND zero or more row constraints over physical-space input rows.""" + + X_phys = np.asarray(X_phys, dtype=float) + if X_phys.ndim != 2 or X_phys.shape[1] != len(design.names): + raise ValueError( + "Physical input array must have shape (n_rows, n_design_inputs); " + f"got {X_phys.shape} for {len(design.names)} design inputs." + ) - mask = np.ones(n, dtype=bool) - for k, fn in enumerate(constraints): - m = fn(X_phys, design) - if m is None or m.dtype != bool or m.shape != (n,): + row_count = X_phys.shape[0] + if not constraints: + return np.ones(row_count, dtype=bool) + + mask = np.ones(row_count, dtype=bool) + for index, constraint in enumerate(constraints): + result = constraint(X_phys, design) + if ( + result is None + or not isinstance(result, np.ndarray) + or result.dtype != bool + or result.shape != (row_count,) + ): + actual = ( + None + if result is None + else (getattr(result, "dtype", None), getattr(result, "shape", None)) + ) raise ValueError( - f"Constraint #{k} must return a boolean mask of shape ({n},); " - f"got {None if m is None else (m.dtype, m.shape)}" + f"Constraint #{index} must return a boolean mask of shape " + f"({row_count},); got {actual}." ) - mask &= m + mask &= result return mask + + +__all__ = [ + "RowConstraint", + "apply_row_constraints", + "check_clausius_clapeyron_np", + "constraints_from_config", +] diff --git a/src/mobo_kit/d2d_campaign.py b/src/mobo_kit/d2d_campaign.py new file mode 100644 index 0000000..cd9ffa7 --- /dev/null +++ b/src/mobo_kit/d2d_campaign.py @@ -0,0 +1,1182 @@ +"""Resolved D2D Step 2B debug campaign contract and data preparation. + +This module deliberately separates historical observations from proposed grid +points. The one approved off-grid control remains a continuous GP training +coordinate, while the strict Step 2A grid APIs continue to govern every new +candidate. +""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +from numbers import Real +from pathlib import Path +from typing import Any, Mapping, Sequence + +import numpy as np +import pandas as pd +import yaml +from openpyxl import load_workbook + +from .candidate_pool import physical_rows_to_grid_indices +from .data import x_normalizer_np +from .design import Design, build_design_from_config +from .objectives import ObjectiveSpec, ObjectiveTransform +from .workbook_schema import ( + WorkbookAudit, + WorkbookInputExceptionRule, + audit_campaign_workbook, +) + + +D2D_INPUT_COLUMNS = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", +) +D2D_WORKBOOK_INPUT_COLUMNS = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol (uL)", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", +) +D2D_OBJECTIVE_COLUMNS = ( + "Uniformity score", + "Optoelectronic score", + "Thickness score", +) +D2D_BOUNDED_OBJECTIVE_COLUMNS = ("Uniformity score", "Thickness score") +D2D_OBJECTIVE_NAMES = ( + "uniformity_score", + "optoelectronic_score", + "thickness_score", +) +D2D_REFERENCE_POINT_UTILITY = np.asarray((-0.01, -10.0, -0.01), dtype=float) +D2D_DEBUG_WATERMARK = "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" +D2D_OBJECTIVE_CONTRACT_VERSION = "d2d-step2b-debug-objectives-v1" +D2D_APPROVED_INPUT_SPECS = ( + ("speed_1", "rpm", 1000.0, 6000.0, 500.0), + ("time_1", "s", 5.0, 50.0, 5.0), + ("speed_2", "rpm", 0.0, 5000.0, 500.0), + ("time_2", "s", 10.0, 60.0, 5.0), + ("precur_conc", "M", 1.0, 2.0, 0.05), + ("precur_vol", "uL", 40.0, 200.0, 10.0), + ("anneal_temp", "C", 100.0, 185.0, 5.0), + ("anneal_time", "min", 10.0, 60.0, 5.0), + ("anti_vol", "uL", 100.0, 200.0, 5.0), + ("anti_time", "s", 9.0, 25.0, 2.0), +) + + +class D2DDebugConfigError(ValueError): + """Raised when a debug configuration weakens the resolved contract.""" + + +@dataclass(frozen=True) +class OffGridObservedException: + sample_id: int + input_name: str + observed_value: float + reason: str + + +@dataclass(frozen=True) +class ResolvedD2DDebugConfig: + raw: dict[str, Any] + design: Design + config_hash: str + workbook_profile: str + workbook_sheet: str + expected_content_range: str + expected_workbook_sha256: str + expected_sample_ids: tuple[int, ...] + reference_point_utility: np.ndarray + control_sample_ids: tuple[int, ...] + include_control_in_debug_model: bool + control_measurement_provenance_assumption: str + off_grid_exceptions: tuple[OffGridObservedException, ...] + seed: int + r1_batch_size: int + replicates_per_condition: int + beta: float + posterior_samples: int + candidate_pool_size: int + score_chunk_size: int + local_radius: float + min_batch_distance: float + min_observed_distance: float + dimension_weights: np.ndarray | None + output_root: str + debug_watermark: str + + +@dataclass(frozen=True) +class D2DTrainingData: + campaign_id: str + X_phys_all: np.ndarray + X_norm_all: np.ndarray + Y_objectives: np.ndarray + sample_ids: np.ndarray + row_roles: tuple[str, ...] + include_in_model: np.ndarray + on_grid_mask: np.ndarray + off_grid_exceptions: tuple[OffGridObservedException, ...] + on_grid_grid_indices: np.ndarray + on_grid_sample_ids: np.ndarray + measurement_provenance: str + off_grid_exception_reasons: tuple[str, ...] + warnings: tuple[str, ...] + + def manifest_frame(self) -> pd.DataFrame: + """Return an auditable row-level training manifest.""" + frame = pd.DataFrame( + { + "campaign_id": self.campaign_id, + "round": "R0", + "sample_id": self.sample_ids, + "candidate_id": [ + f"R0-S{int(sample_id):02d}" for sample_id in self.sample_ids + ], + "row_role": self.row_roles, + "replicate_group": [ + f"R0-S{int(sample_id):02d}" for sample_id in self.sample_ids + ], + "replicate_number": 1, + "candidate_status": "observed_debug_input", + "include_in_model": self.include_in_model, + "measurement_provenance": self.measurement_provenance, + "on_grid": self.on_grid_mask, + "off_grid_exception": ~self.on_grid_mask, + "off_grid_exception_reason": self.off_grid_exception_reasons, + "exclusion_reason": "", + } + ) + for index, name in enumerate(D2D_INPUT_COLUMNS): + frame[name] = self.X_phys_all[:, index] + for index, name in enumerate(D2D_OBJECTIVE_COLUMNS): + frame[name] = self.Y_objectives[:, index] + return frame + + +@dataclass(frozen=True) +class ReplicateAggregationResult: + frame: pd.DataFrame + warnings: tuple[str, ...] + + +def _canonical_json_hash(value: Mapping[str, Any]) -> str: + payload = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ) + return hashlib.sha256(payload.encode("utf-8")).hexdigest() + + +def _mapping(value: Any, *, field: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise D2DDebugConfigError(f"{field} must be a mapping.") + return value + + +def _positive_int(value: Any, *, field: str) -> int: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)): + raise D2DDebugConfigError(f"{field} must be a positive integer.") + result = int(value) + if result <= 0: + raise D2DDebugConfigError(f"{field} must be a positive integer.") + return result + + +def _finite_number(value: Any, *, field: str, positive: bool = False) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise D2DDebugConfigError(f"{field} must be a real non-boolean number.") + result = float(value) + if not np.isfinite(result) or (positive and result <= 0): + qualifier = "finite and strictly positive" if positive else "finite" + raise D2DDebugConfigError(f"{field} must be {qualifier}.") + return result + + +def load_d2d_debug_config( + path: str | Path, + *, + allow_public_template: bool = False, +) -> ResolvedD2DDebugConfig: + """Load and strictly validate a private config or explicit public fixture.""" + if not isinstance(allow_public_template, bool): + raise TypeError("allow_public_template must be a boolean.") + config_path = Path(path) + if not config_path.is_file(): + raise FileNotFoundError(f"D2D debug config not found: {config_path}") + with config_path.open("r", encoding="utf-8") as handle: + raw = yaml.safe_load(handle) + if not isinstance(raw, dict): + raise D2DDebugConfigError("D2D debug config must contain a mapping.") + template_only = raw.get("template_only", False) + if not isinstance(template_only, bool): + raise D2DDebugConfigError("template_only must be a boolean when supplied.") + if template_only: + if not allow_public_template: + raise D2DDebugConfigError( + "The tracked Step 2B config is a public template. Copy it into an " + "ignored private location and supply runtime workbook identity fields." + ) + if raw.get("campaign_id") != "public-d2d-template-debug": + raise D2DDebugConfigError( + "allow_public_template may resolve only the tracked public template." + ) + workbook_template = _mapping(raw.get("workbook"), field="workbook") + if workbook_template.get("expected_sha256") != ( + "REQUIRED_IN_IGNORED_PRIVATE_CONFIG" + ): + raise D2DDebugConfigError( + "The public template workbook identity placeholder was modified." + ) + workbook_template["expected_sha256"] = _canonical_json_hash( + { + "schema_version": raw.get("schema_version"), + "campaign_id": raw.get("campaign_id"), + "identity": "runtime-generated-public-synthetic-fixture", + } + ) + raw["template_only"] = False + if raw.get("schema_version") != "d2d-step2b-debug-1": + raise D2DDebugConfigError( + "schema_version must be exactly 'd2d-step2b-debug-1'." + ) + campaign_id = raw.get("campaign_id") + if not isinstance(campaign_id, str) or not campaign_id.strip(): + raise D2DDebugConfigError("campaign_id must be a nonblank string.") + + required_flags = { + "run_mode": "debug", + "debug_run_authorized": True, + "approved_for_production": False, + "approved_for_experiment": False, + } + for field, expected in required_flags.items(): + value = raw.get(field) + exact_boolean = not isinstance(expected, bool) or ( + isinstance(value, bool) and value is expected + ) + if value != expected or not exact_boolean: + raise D2DDebugConfigError( + f"{field} must be exactly {expected!r} for a Step 2B debug run." + ) + + inputs = raw.get("inputs") + if not isinstance(inputs, list) or len(inputs) != len(D2D_APPROVED_INPUT_SPECS): + raise D2DDebugConfigError( + "inputs must contain the exact ten approved Step 2A grid definitions." + ) + for index, (item, approved) in enumerate(zip(inputs, D2D_APPROVED_INPUT_SPECS)): + input_spec = _mapping(item, field=f"inputs[{index}]") + name, unit, start, stop, step = approved + if input_spec.get("name") != name or input_spec.get("unit") != unit: + raise D2DDebugConfigError( + f"inputs[{index}] must preserve approved name/unit {name!r}/{unit!r}." + ) + for field, approved_value in ( + ("start", start), + ("stop", stop), + ("step", step), + ): + actual = _finite_number( + input_spec.get(field), field=f"inputs[{index}].{field}" + ) + if actual != approved_value: + raise D2DDebugConfigError( + f"inputs[{index}].{field} must preserve the approved Step 2A " + f"grid value {approved_value}." + ) + design = build_design_from_config(raw) + + workbook = _mapping(raw.get("workbook"), field="workbook") + if workbook.get("profile") != "d2d_summary_v3_scores": + raise D2DDebugConfigError("workbook.profile must be 'd2d_summary_v3_scores'.") + if workbook.get("sheet") != "Sheet1": + raise D2DDebugConfigError("workbook.sheet must be 'Sheet1'.") + if workbook.get("expected_content_range") != "A1:AI20": + raise D2DDebugConfigError( + "workbook.expected_content_range must be exactly 'A1:AI20'." + ) + if workbook.get("input_aliases") != {"precur_vol (uL)": "precur_vol"}: + raise D2DDebugConfigError( + "workbook.input_aliases must preserve the resolved precursor-volume alias." + ) + if workbook.get("sample_id_column") != "Sample number": + raise D2DDebugConfigError( + "workbook.sample_id_column must be exactly 'Sample number'." + ) + expected_hash = str(workbook.get("expected_sha256", "")).upper() + if len(expected_hash) != 64 or any( + c not in "0123456789ABCDEF" for c in expected_hash + ): + raise D2DDebugConfigError( + "workbook.expected_sha256 must be a SHA-256 hex digest." + ) + expected_ids_raw = workbook.get("expected_sample_ids") + if not isinstance(expected_ids_raw, list) or not expected_ids_raw: + raise D2DDebugConfigError( + "workbook.expected_sample_ids must be a nonempty list of unique integers." + ) + if any( + isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)) + for value in expected_ids_raw + ): + raise D2DDebugConfigError( + "workbook.expected_sample_ids must contain only integers." + ) + expected_ids = tuple(int(value) for value in expected_ids_raw) + if len(set(expected_ids)) != len(expected_ids): + raise D2DDebugConfigError( + "workbook.expected_sample_ids must contain unique integers." + ) + + expected_objectives = [ + { + "name": "uniformity_score", + "source_column": "Uniformity score", + "excel_column": "Z", + "direction": "maximize", + "utility_transform": "identity", + "support_formula": "Coverage * (1 - Uniformity) * Phase purity", + "mismatch_policy": "warn_use_supplied", + }, + { + "name": "optoelectronic_score", + "source_column": "Optoelectronic score", + "excel_column": "AA", + "direction": "maximize", + "utility_transform": "identity", + "support_formula": "log10((PL - Implied Voc (Max)) * Photoconductance (Max))", + "mismatch_policy": "error", + }, + { + "name": "thickness_score", + "source_column": "Thickness score", + "excel_column": "AB", + "direction": "maximize", + "utility_transform": "identity", + "support_formula": "exp(-((mean(valid T1:T4) - 650.0) / 250.0)^2)", + "target_nm": 650.0, + "scale_nm": 250.0, + "exponent_factor": 1.0, + "exclude_columns": ["T anom"], + "mismatch_policy": "error", + }, + ] + objectives = raw.get("objectives") + if not isinstance(objectives, list) or len(objectives) != 3: + raise D2DDebugConfigError("objectives must contain exactly three mappings.") + for index, (item, expected) in enumerate(zip(objectives, expected_objectives)): + objective = _mapping(item, field=f"objectives[{index}]") + for field, expected_value in expected.items(): + if objective.get(field) != expected_value: + raise D2DDebugConfigError( + f"objectives[{index}].{field} must be {expected_value!r}." + ) + + expected_qc_policy = { + "final_scores_required": True, + "complete_case_rule": "require_all_three_final_scores", + "failed_measurement_rule": "block_row", + "outlier_rule": "report_do_not_auto_remove", + "uniformity_known_mismatch": "warn_and_continue_debug", + "optoelectronic_mismatch": "fail", + "thickness_mismatch": "fail", + } + if raw.get("qc_policy") != expected_qc_policy: + raise D2DDebugConfigError( + "qc_policy must preserve the resolved Step 2B score-validation policy." + ) + expected_ignored_columns = [ + "Stability score?", + "Total combination - addition", + "Total combination - multiplied", + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", + ] + if raw.get("ignored_model_columns") != expected_ignored_columns: + raise D2DDebugConfigError( + "ignored_model_columns must preserve the resolved AC:AI exclusion list." + ) + + reference = np.asarray(raw.get("reference_point_utility"), dtype=float) + if reference.shape != (3,) or not np.array_equal( + reference, D2D_REFERENCE_POINT_UTILITY + ): + raise D2DDebugConfigError( + "reference_point_utility must be exactly [-0.01, -10.0, -0.01]." + ) + if raw.get("constraints") != []: + raise D2DDebugConfigError("constraints must be an explicit empty list.") + + r0 = _mapping(raw.get("r0"), field="r0") + condition_count = _positive_int( + r0.get("condition_count"), field="r0.condition_count" + ) + if condition_count != len(expected_ids): + raise D2DDebugConfigError( + "r0.condition_count must match workbook.expected_sample_ids." + ) + control_ids_raw = r0.get("control_sample_ids") + if not isinstance(control_ids_raw, list) or not control_ids_raw: + raise D2DDebugConfigError( + "r0.control_sample_ids must be a nonempty list of sample identifiers." + ) + if any( + isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)) + for value in control_ids_raw + ): + raise D2DDebugConfigError("r0.control_sample_ids must contain only integers.") + control_ids = tuple(int(value) for value in control_ids_raw) + if len(set(control_ids)) != len(control_ids) or not set(control_ids).issubset( + expected_ids + ): + raise D2DDebugConfigError( + "r0.control_sample_ids must be unique members of expected_sample_ids." + ) + if r0.get("include_control_in_debug_model") is not True: + raise D2DDebugConfigError( + "The control must be included in the primary debug model." + ) + provenance = r0.get("control_measurement_provenance_assumption") + if not isinstance(provenance, str) or not provenance.strip(): + raise D2DDebugConfigError( + "The control measurement provenance assumption must be a nonblank string." + ) + raw_exceptions = r0.get("off_grid_observed_exceptions") + if not isinstance(raw_exceptions, list): + raise D2DDebugConfigError("r0.off_grid_observed_exceptions must be a list.") + exceptions: list[OffGridObservedException] = [] + exception_keys: set[tuple[int, str]] = set() + for index, item in enumerate(raw_exceptions): + exception_raw = _mapping( + item, field=f"r0.off_grid_observed_exceptions[{index}]" + ) + sample_id_raw = exception_raw.get("sample_id") + if isinstance(sample_id_raw, (bool, np.bool_)) or not isinstance( + sample_id_raw, (int, np.integer) + ): + raise D2DDebugConfigError( + "Off-grid exception sample_id must be an integer." + ) + sample_id = int(sample_id_raw) + input_name = str(exception_raw.get("input_name", "")).strip() + if sample_id not in expected_ids or input_name not in design.names: + raise D2DDebugConfigError( + "Off-grid exceptions must reference a configured sample and input." + ) + if "observed_value" in exception_raw: + observed_value = _finite_number( + exception_raw.get("observed_value"), field="off-grid observed_value" + ) + elif ( + exception_raw.get("observed_value_strategy") + == "midpoint_between_first_two_grid_values" + ): + dimension = design.names.index(input_name) + grid = np.asarray(design.var_array[dimension], dtype=float) + if grid.size < 2: + raise D2DDebugConfigError( + "Synthetic midpoint exceptions require at least two grid values." + ) + observed_value = float((grid[0] + grid[1]) / 2.0) + else: + raise D2DDebugConfigError( + "Off-grid exceptions require observed_value or the supported " + "synthetic midpoint strategy." + ) + dimension = design.names.index(input_name) + grid = np.asarray(design.var_array[dimension], dtype=float) + if ( + observed_value < design.lowers[dimension] + or observed_value > design.uppers[dimension] + ): + raise D2DDebugConfigError( + "Off-grid exception values must remain in bounds." + ) + if np.any(np.isclose(observed_value, grid, rtol=0.0, atol=1e-9)): + raise D2DDebugConfigError("Off-grid exception values must not be on-grid.") + reason = str(exception_raw.get("reason", "")).strip() + if not reason or exception_raw.get("retain_observed_value") is not True: + raise D2DDebugConfigError( + "Off-grid exceptions require a reason and retain_observed_value=true." + ) + key = (sample_id, input_name) + if key in exception_keys: + raise D2DDebugConfigError(f"Duplicate off-grid exception for {key!r}.") + exception_keys.add(key) + exceptions.append( + OffGridObservedException( + sample_id=sample_id, + input_name=input_name, + observed_value=observed_value, + reason=reason, + ) + ) + + r1 = _mapping(raw.get("r1"), field="r1") + if r1.get("method") != "ucb_hvi": + raise D2DDebugConfigError("r1.method must be 'ucb_hvi'.") + expected_r1 = { + "method": "ucb_hvi", + "batch_size_unique_conditions": 5, + "replicates_per_condition": 3, + "beta": 4.0, + "posterior_samples": 256, + "candidate_pool_size": 10000, + "score_chunk_size": 512, + } + if r1 != expected_r1: + raise D2DDebugConfigError( + "r1 must preserve the resolved Step 2B baseline acquisition settings." + ) + expected_r2 = { + "method": "qlognehvi", + "batch_size_unique_conditions": 3, + "replicates_per_condition": 3, + "mc_samples": 128, + "candidate_pool_size": 5000, + "sequential_pending": True, + } + if raw.get("r2_test_only") != expected_r2: + raise D2DDebugConfigError( + "r2_test_only must preserve the resolved synthetic-only settings." + ) + local = _mapping(raw.get("local_penalization"), field="local_penalization") + if local.get("distance_metric") != "normalized_euclidean": + raise D2DDebugConfigError( + "local_penalization.distance_metric must be 'normalized_euclidean'." + ) + if local.get("allow_hard_distance_relaxation") is not False: + raise D2DDebugConfigError("Hard-distance relaxation must remain false.") + weights_raw = local.get("dimension_weights") + weights = None if weights_raw is None else np.asarray(weights_raw, dtype=float) + if weights is not None and ( + weights.shape != (10,) + or not np.all(np.isfinite(weights)) + or np.any(weights <= 0) + ): + raise D2DDebugConfigError( + "local_penalization.dimension_weights must be null or ten positive values." + ) + expected_local = { + "distance_metric": "normalized_euclidean", + "radius": 0.25, + "min_batch_distance": 0.15, + "min_observed_distance": 0.0, + "dimension_weights": None, + "allow_hard_distance_relaxation": False, + } + if local != expected_local: + raise D2DDebugConfigError( + "local_penalization must preserve the resolved Step 2B baseline settings." + ) + + outputs = _mapping(raw.get("outputs"), field="outputs") + if outputs.get("root") != "local_outputs/d2d_step2b_debug": + raise D2DDebugConfigError( + "outputs.root must be exactly 'local_outputs/d2d_step2b_debug'." + ) + if outputs.get("debug_watermark") != D2D_DEBUG_WATERMARK: + raise D2DDebugConfigError( + f"outputs.debug_watermark must be {D2D_DEBUG_WATERMARK!r}." + ) + if outputs.get("write_source_workbook") is not False: + raise D2DDebugConfigError("outputs.write_source_workbook must be false.") + reproducibility = _mapping(raw.get("reproducibility"), field="reproducibility") + expected_reproducibility = { + "seed": 73, + "record_git_commit": True, + "record_environment_versions": True, + "record_resolved_config_hash": True, + "record_workbook_hash": True, + } + if reproducibility != expected_reproducibility: + raise D2DDebugConfigError( + "reproducibility must preserve seed 73 and all provenance records." + ) + resolved = ResolvedD2DDebugConfig( + raw=raw, + design=design, + config_hash=_canonical_json_hash(raw), + workbook_profile="d2d_summary_v3_scores", + workbook_sheet="Sheet1", + expected_content_range=str(workbook.get("expected_content_range")), + expected_workbook_sha256=expected_hash, + expected_sample_ids=expected_ids, + reference_point_utility=reference.copy(), + control_sample_ids=tuple(int(value) for value in control_ids), + include_control_in_debug_model=True, + control_measurement_provenance_assumption=str(provenance), + off_grid_exceptions=tuple(exceptions), + seed=_positive_int(reproducibility.get("seed"), field="reproducibility.seed"), + r1_batch_size=_positive_int( + r1.get("batch_size_unique_conditions"), + field="r1.batch_size_unique_conditions", + ), + replicates_per_condition=_positive_int( + r1.get("replicates_per_condition"), field="r1.replicates_per_condition" + ), + beta=_finite_number(r1.get("beta"), field="r1.beta", positive=True), + posterior_samples=_positive_int( + r1.get("posterior_samples"), field="r1.posterior_samples" + ), + candidate_pool_size=_positive_int( + r1.get("candidate_pool_size"), field="r1.candidate_pool_size" + ), + score_chunk_size=_positive_int( + r1.get("score_chunk_size"), field="r1.score_chunk_size" + ), + local_radius=_finite_number( + local.get("radius"), field="local_penalization.radius", positive=True + ), + min_batch_distance=_finite_number( + local.get("min_batch_distance"), + field="local_penalization.min_batch_distance", + ), + min_observed_distance=_finite_number( + local.get("min_observed_distance"), + field="local_penalization.min_observed_distance", + ), + dimension_weights=None if weights is None else weights.copy(), + output_root=str(outputs.get("root")), + debug_watermark=D2D_DEBUG_WATERMARK, + ) + if tuple(resolved.design.names) != D2D_INPUT_COLUMNS: + raise D2DDebugConfigError( + f"inputs must use the exact canonical order {D2D_INPUT_COLUMNS}." + ) + if resolved.r1_batch_size != 5 or resolved.replicates_per_condition != 3: + raise D2DDebugConfigError( + "R1 must select five conditions with three replicates each." + ) + if resolved.min_batch_distance < 0 or resolved.min_observed_distance < 0: + raise D2DDebugConfigError("Configured minimum distances must be non-negative.") + return resolved + + +def sha256_file(path: str | Path) -> str: + digest = hashlib.sha256() + with Path(path).open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest().upper() + + +def _normalize_missing(value: Any) -> Any: + if value is None: + return None + if isinstance(value, str) and not value.replace("\u00a0", " ").strip(): + return None + return value + + +def _numeric_sample_id(value: Any) -> int | None: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + return None + number = float(value) + if not np.isfinite(number) or not number.is_integer(): + return None + return int(number) + + +def load_d2d_workbook_frame( + path: str | Path, + *, + expected_profile: str = "d2d_summary_v3_scores", + expected_sample_ids: Sequence[int] | None = None, + allowed_input_exceptions: Sequence[OffGridObservedException] = (), +) -> tuple[pd.DataFrame, WorkbookAudit]: + """Read only the numeric v3 sample rows and cached values from the workbook.""" + workbook_path = Path(path) + audit = audit_campaign_workbook( + workbook_path, + allowed_input_exceptions=tuple( + WorkbookInputExceptionRule( + sample_id=exception.sample_id, + input_name=exception.input_name, + observed_value=exception.observed_value, + reason=exception.reason, + ) + for exception in allowed_input_exceptions + ), + ) + if audit.profile != expected_profile: + raise ValueError( + f"Expected workbook profile {expected_profile!r}; found {audit.profile!r}." + ) + if audit.active_sheet != "Sheet1" or audit.used_range != "A1:AI20": + raise ValueError( + "The v3 workbook must use Sheet1 with content range A1:AI20; " + f"found {audit.active_sheet!r} and {audit.used_range!r}." + ) + if audit.input_rows_valid is not True: + raise ValueError( + "The v3 workbook input-row audit failed: " + f"{audit.input_validation_errors or ['unspecified input error']}." + ) + headers = tuple(audit.raw_headers) + if len(headers) != 35 or any(header is None for header in headers): + raise ValueError("The v3 workbook must contain 35 nonblank unique headers.") + if len(set(headers)) != 35: + raise ValueError("The v3 workbook headers must be unique.") + + workbook = load_workbook(workbook_path, read_only=True, data_only=True) + try: + worksheet = workbook["Sheet1"] + rows: list[list[Any]] = [] + for values in worksheet.iter_rows( + min_row=2, + max_row=worksheet.max_row, + min_col=1, + max_col=35, + values_only=True, + ): + sample_id = _numeric_sample_id(values[0]) + if sample_id is None: + continue + normalized = [_normalize_missing(value) for value in values] + normalized[0] = sample_id + rows.append(normalized) + finally: + workbook.close() + frame = pd.DataFrame(rows, columns=list(headers)) + observed_ids = tuple(int(value) for value in frame["Sample number"].tolist()) + if not observed_ids or len(set(observed_ids)) != len(observed_ids): + raise ValueError( + "The v3 workbook must contain unique numeric sample identifiers; " + f"found {observed_ids}." + ) + if expected_sample_ids is not None and observed_ids != tuple(expected_sample_ids): + raise ValueError( + f"Expected ordered sample identifiers {tuple(expected_sample_ids)}; " + f"found {observed_ids}." + ) + return frame, audit + + +def build_d2d_objective_transform() -> ObjectiveTransform: + """Return the resolved ordered identity/maximize objective contract.""" + specs = [ + ObjectiveSpec( + name=name, + goal="maximize", + transform="identity", + source_column=source, + ) + for name, source in zip(D2D_OBJECTIVE_NAMES, D2D_OBJECTIVE_COLUMNS) + ] + return ObjectiveTransform(specs, version=D2D_OBJECTIVE_CONTRACT_VERSION) + + +def _numeric_matrix( + frame: pd.DataFrame, columns: Sequence[str], *, label: str +) -> np.ndarray: + missing = [column for column in columns if column not in frame.columns] + if missing: + raise ValueError(f"Missing {label} column(s): {missing}.") + numeric = frame.loc[:, list(columns)].apply(pd.to_numeric, errors="coerce") + values = numeric.to_numpy(dtype=float) + if not np.all(np.isfinite(values)): + rows, cols = np.where(~np.isfinite(values)) + details = [(int(row), columns[int(col)]) for row, col in zip(rows, cols)] + raise ValueError( + f"{label} values must be complete and finite; invalid {details}." + ) + return values + + +def prepare_d2d_training_data( + frame: pd.DataFrame, + config: ResolvedD2DDebugConfig, + *, + include_control: bool = True, +) -> D2DTrainingData: + """Prepare control-aware, fixed-bound training arrays from workbook rows.""" + if not isinstance(frame, pd.DataFrame): + raise TypeError("frame must be a pandas DataFrame.") + sample_values = _numeric_matrix( + frame, ("Sample number",), label="sample identifier" + )[:, 0] + if not np.all(sample_values == np.floor(sample_values)): + raise ValueError("Sample identifiers must be integers.") + sample_ids = sample_values.astype(int) + if tuple(sample_ids.tolist()) != config.expected_sample_ids: + raise ValueError( + f"Expected ordered sample IDs {config.expected_sample_ids}; found {sample_ids.tolist()}." + ) + X_phys = _numeric_matrix(frame, D2D_WORKBOOK_INPUT_COLUMNS, label="input") + duplicate_inputs = pd.DataFrame(X_phys).duplicated(keep=False).to_numpy() + if np.any(duplicate_inputs): + duplicate_samples = sample_ids[duplicate_inputs].tolist() + raise ValueError( + "Observed training recipes must be unique; duplicate input rows were " + f"found for samples {duplicate_samples}." + ) + lower = np.asarray(config.design.lowers, dtype=float) + upper = np.asarray(config.design.uppers, dtype=float) + out_of_bounds = np.where((X_phys < lower) | (X_phys > upper)) + if out_of_bounds[0].size: + details = [ + (int(sample_ids[row]), D2D_INPUT_COLUMNS[col], float(X_phys[row, col])) + for row, col in zip(*out_of_bounds) + ] + raise ValueError( + f"Observed inputs must remain within configured bounds: {details}." + ) + + exceptions_by_key = { + (exception.sample_id, exception.input_name): exception + for exception in config.off_grid_exceptions + } + on_grid_mask = np.ones(X_phys.shape[0], dtype=bool) + encountered: list[OffGridObservedException] = [] + for row_index, (sample_id, row) in enumerate(zip(sample_ids, X_phys)): + mismatches: list[tuple[int, str, float]] = [] + for column, (name, grid) in enumerate( + zip(D2D_INPUT_COLUMNS, config.design.var_array) + ): + if not np.any(np.isclose(row[column], grid, rtol=0.0, atol=1e-9)): + mismatches.append((column, name, float(row[column]))) + if not mismatches: + continue + on_grid_mask[row_index] = False + for _, name, observed in mismatches: + exception = exceptions_by_key.get((int(sample_id), name)) + if exception is None or not np.isclose( + observed, exception.observed_value, rtol=0.0, atol=1e-12 + ): + raise ValueError( + f"Sample {sample_id} has an unapproved off-grid value " + f"{name}={observed}." + ) + encountered.append(exception) + if set(encountered) != set(config.off_grid_exceptions): + raise ValueError( + "The configured off-grid exception was not found exactly in the data." + ) + + grid_indices = physical_rows_to_grid_indices(X_phys[on_grid_mask], config.design) + X_norm = x_normalizer_np(X_phys, config.design) + if np.any(X_norm < 0.0) or np.any(X_norm > 1.0): + raise RuntimeError("Bounded observations failed fixed input normalization.") + objectives = _numeric_matrix(frame, D2D_OBJECTIVE_COLUMNS, label="objective") + for objective_name in D2D_BOUNDED_OBJECTIVE_COLUMNS: + objective_index = D2D_OBJECTIVE_COLUMNS.index(objective_name) + invalid_rows = np.flatnonzero( + (objectives[:, objective_index] < 0.0) + | (objectives[:, objective_index] > 1.0) + ) + if invalid_rows.size: + details = [ + (int(sample_ids[row]), float(objectives[row, objective_index])) + for row in invalid_rows + ] + raise ValueError( + f"Authoritative {objective_name} values must remain in [0, 1]; " + f"violations: {details}." + ) + if not np.all(objectives > config.reference_point_utility): + failing = np.argwhere(objectives <= config.reference_point_utility) + details = [ + ( + int(sample_ids[row]), + D2D_OBJECTIVE_COLUMNS[col], + float(objectives[row, col]), + ) + for row, col in failing + ] + raise ValueError( + "Every objective row must strictly dominate the fixed reference point; " + f"violations: {details}." + ) + roles = tuple( + "control" if int(sample_id) in config.control_sample_ids else "r0_lhs" + for sample_id in sample_ids + ) + included = np.ones(sample_ids.shape[0], dtype=bool) + if not include_control: + included &= ~np.isin(sample_ids, config.control_sample_ids) + warnings = ( + "Configured control observations are included under the declared " + "measurement-provenance assumption.", + *tuple( + "A configured observed-only off-grid input exception is retained for " + f"sample {exception.sample_id} and input {exception.input_name!r}; " + "new candidates remain on the approved grid." + for exception in config.off_grid_exceptions + ), + ) + return D2DTrainingData( + campaign_id=str(config.raw.get("campaign_id")), + X_phys_all=X_phys, + X_norm_all=X_norm, + Y_objectives=objectives, + sample_ids=sample_ids, + row_roles=roles, + include_in_model=included, + on_grid_mask=on_grid_mask, + off_grid_exceptions=tuple(encountered), + on_grid_grid_indices=grid_indices, + on_grid_sample_ids=sample_ids[on_grid_mask], + measurement_provenance=config.control_measurement_provenance_assumption, + off_grid_exception_reasons=tuple( + next( + ( + exception.reason + for exception in encountered + if exception.sample_id == int(sample_id) + ), + "", + ) + for sample_id in sample_ids + ), + warnings=warnings, + ) + + +def expand_candidates_to_replicates( + candidates: pd.DataFrame, + *, + replicates_per_condition: int = 3, + round_name: str = "R1", +) -> pd.DataFrame: + """Expand unique candidate conditions to explicitly numbered debug executions.""" + if not isinstance(candidates, pd.DataFrame): + raise TypeError("candidates must be a pandas DataFrame.") + replicate_count = _positive_int( + replicates_per_condition, field="replicates_per_condition" + ) + _numeric_matrix(candidates, D2D_INPUT_COLUMNS, label="candidate input") + if candidates.shape[0] == 0: + raise ValueError("At least one unique candidate is required.") + if candidates.loc[:, list(D2D_INPUT_COLUMNS)].duplicated().any(): + raise ValueError( + "Unique candidate conditions must not contain duplicate inputs." + ) + if not isinstance(round_name, str) or not round_name.strip(): + raise ValueError("round_name must be a nonblank string.") + if "candidate_id" in candidates.columns: + candidate_ids = candidates["candidate_id"].map(str).map(str.strip) + if candidate_ids.eq("").any() or candidates["candidate_id"].isna().any(): + raise ValueError("candidate_id values must be nonblank.") + if candidate_ids.duplicated().any(): + raise ValueError("candidate_id values must be unique across conditions.") + else: + candidate_ids = pd.Series( + [ + f"{round_name}-C{position:02d}" + for position in range(1, len(candidates) + 1) + ], + index=candidates.index, + ) + rows: list[dict[str, Any]] = [] + if "campaign_id" in candidates.columns: + campaign_ids = candidates["campaign_id"].map(str).map(str.strip) + if candidates["campaign_id"].isna().any() or campaign_ids.eq("").any(): + raise ValueError("campaign_id values must be nonblank when supplied.") + if campaign_ids.nunique() != 1: + raise ValueError("campaign_id must be consistent across candidates.") + campaign_id = str(campaign_ids.iloc[0]) + else: + campaign_id = "public-debug-campaign" + for position, (_, candidate) in enumerate(candidates.iterrows(), start=1): + candidate_id = str(candidate_ids.iloc[position - 1]) + for replicate in range(1, replicate_count + 1): + row: dict[str, Any] = { + "campaign_id": campaign_id, + "round": round_name, + "sample_id": pd.NA, + "execution_id": f"{candidate_id}-R{replicate}", + "candidate_id": candidate_id, + "replicate_group": candidate_id, + "replicate_number": replicate, + "row_role": "candidate_replicate", + "candidate_status": D2D_DEBUG_WATERMARK, + "debug_only": True, + "approved_for_experiment": False, + "include_in_model": False, + "measurement_provenance": "pending_measurement", + "off_grid_exception": False, + "exclusion_reason": "awaiting_measurement", + } + for name in D2D_INPUT_COLUMNS: + row[name] = float(candidate[name]) + for objective in D2D_OBJECTIVE_COLUMNS: + row[objective] = pd.NA + rows.append(row) + expanded = pd.DataFrame(rows) + if not expanded["execution_id"].is_unique: + raise RuntimeError("Expanded candidate execution IDs must be unique.") + return expanded + + +def aggregate_replicate_objectives( + records: pd.DataFrame, + *, + group_column: str = "replicate_group", + objective_columns: Sequence[str] = D2D_OBJECTIVE_COLUMNS, + input_columns: Sequence[str] = D2D_INPUT_COLUMNS, + expected_replicates: int = 3, + sample_id_column: str = "execution_id", +) -> ReplicateAggregationResult: + """Aggregate physical repeats to one condition-level model observation.""" + if not isinstance(records, pd.DataFrame): + raise TypeError("records must be a pandas DataFrame.") + expected_count = _positive_int(expected_replicates, field="expected_replicates") + required = [ + group_column, + sample_id_column, + "replicate_number", + *input_columns, + *objective_columns, + ] + missing = [column for column in required if column not in records.columns] + if missing: + raise ValueError(f"Missing replicate column(s): {missing}.") + source_identifiers = records[sample_id_column] + normalized_identifiers = source_identifiers.map(str).map(str.strip) + if source_identifiers.isna().any() or normalized_identifiers.eq("").any(): + raise ValueError(f"{sample_id_column} values must be nonblank.") + if normalized_identifiers.duplicated().any(): + raise ValueError(f"{sample_id_column} values must be unique.") + output_rows: list[dict[str, Any]] = [] + warnings: list[str] = [] + for group, group_frame in records.groupby(group_column, sort=False, dropna=False): + if pd.isna(group) or not str(group).strip(): + raise ValueError("replicate_group values must be nonblank.") + input_values = _numeric_matrix( + group_frame, input_columns, label="replicate input" + ) + if not np.all(input_values == input_values[0]): + raise ValueError( + f"Replicate group {group!r} contains different requested input conditions." + ) + row: dict[str, Any] = {group_column: group} + for column, value in zip(input_columns, input_values[0]): + row[column] = float(value) + replicate_numbers = pd.to_numeric( + group_frame["replicate_number"], errors="coerce" + ).to_numpy(dtype=float) + if ( + not np.all(np.isfinite(replicate_numbers)) + or not np.all(replicate_numbers == np.floor(replicate_numbers)) + or np.any(replicate_numbers < 1) + or np.any(replicate_numbers > expected_count) + ): + raise ValueError( + f"Replicate group {group!r} has invalid replicate_number values." + ) + if np.unique(replicate_numbers).size != replicate_numbers.size: + raise ValueError( + f"Replicate group {group!r} has duplicate replicate_number values." + ) + source_ids = group_frame[sample_id_column].astype(str).tolist() + row["source_sample_ids"] = "|".join(source_ids) + objective_counts: list[int] = [] + for objective in objective_columns: + values = ( + pd.to_numeric(group_frame[objective], errors="coerce") + .dropna() + .to_numpy(dtype=float) + ) + if not np.all(np.isfinite(values)): + raise ValueError( + f"Replicate group {group!r} objective {objective!r} contains non-finite values." + ) + if objective in D2D_BOUNDED_OBJECTIVE_COLUMNS and np.any( + (values < 0.0) | (values > 1.0) + ): + raise ValueError( + f"Replicate group {group!r} objective {objective!r} must " + "remain in [0, 1]." + ) + count = int(values.size) + objective_counts.append(count) + mean = float(np.mean(values)) if count else np.nan + std = float(np.std(values, ddof=1)) if count > 1 else np.nan + sem = float(std / np.sqrt(count)) if count > 1 else np.nan + row[f"{objective}_mean"] = mean + row[f"{objective}_sample_std"] = std + row[f"{objective}_count"] = count + row[f"{objective}_standard_error"] = sem + complete = group_frame.shape[0] == expected_count and all( + count == expected_count for count in objective_counts + ) + row["complete_replicate_set"] = complete + row["include_in_next_model"] = complete + if not complete: + warnings.append( + f"Replicate group {group!r} is incomplete: rows={group_frame.shape[0]}, " + f"objective_counts={objective_counts}, expected={expected_count}." + ) + output_rows.append(row) + return ReplicateAggregationResult(pd.DataFrame(output_rows), tuple(warnings)) + + +def combine_r0_and_aggregated_r1( + r0_training: D2DTrainingData, + r1_aggregated: ReplicateAggregationResult, +) -> tuple[np.ndarray, np.ndarray]: + """Build the future 20-condition R2 training matrices without triplicate inflation.""" + complete = r1_aggregated.frame[ + r1_aggregated.frame["include_in_next_model"].astype(bool) + ] + X_r1 = _numeric_matrix(complete, D2D_INPUT_COLUMNS, label="aggregated R1 input") + mean_columns = tuple(f"{objective}_mean" for objective in D2D_OBJECTIVE_COLUMNS) + Y_r1 = _numeric_matrix(complete, mean_columns, label="aggregated R1 objective") + for objective_name in D2D_BOUNDED_OBJECTIVE_COLUMNS: + objective_index = D2D_OBJECTIVE_COLUMNS.index(objective_name) + if np.any((Y_r1[:, objective_index] < 0.0) | (Y_r1[:, objective_index] > 1.0)): + raise ValueError( + f"Aggregated {objective_name} values must remain in [0, 1]." + ) + X = np.vstack([r0_training.X_phys_all[r0_training.include_in_model], X_r1]) + Y = np.vstack([r0_training.Y_objectives[r0_training.include_in_model], Y_r1]) + if X.shape[0] != np.unique(X, axis=0).shape[0]: + raise ValueError( + "Combined condition-level training data contain duplicate recipes." + ) + return X, Y + + +__all__ = [ + "D2D_DEBUG_WATERMARK", + "D2D_BOUNDED_OBJECTIVE_COLUMNS", + "D2D_INPUT_COLUMNS", + "D2D_OBJECTIVE_COLUMNS", + "D2D_OBJECTIVE_NAMES", + "D2D_REFERENCE_POINT_UTILITY", + "D2DDebugConfigError", + "D2DTrainingData", + "OffGridObservedException", + "ReplicateAggregationResult", + "ResolvedD2DDebugConfig", + "aggregate_replicate_objectives", + "build_d2d_objective_transform", + "combine_r0_and_aggregated_r1", + "expand_candidates_to_replicates", + "load_d2d_debug_config", + "load_d2d_workbook_frame", + "prepare_d2d_training_data", + "sha256_file", +] diff --git a/src/mobo_kit/d2d_r2_test.py b/src/mobo_kit/d2d_r2_test.py new file mode 100644 index 0000000..2978560 --- /dev/null +++ b/src/mobo_kit/d2d_r2_test.py @@ -0,0 +1,162 @@ +"""Synthetic-only future R2 boundary for the resolved D2D objective contract.""" + +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +import numpy as np +import torch + +from .batch_selection import LocalPenalizationConfig +from .candidate_pool import CandidatePool, physical_rows_to_grid_indices +from .data import x_normalizer_np +from .d2d_campaign import ResolvedD2DDebugConfig, build_d2d_objective_transform +from .objectives import ConfiguredMCMultiOutputObjective +from .qlognehvi_batch import QLogNEHVIBatchProposal, propose_qlognehvi_penalized_batch + + +D2D_R2_TEST_WATERMARK = "SYNTHETIC R2 TEST ONLY - NOT APPROVED FOR EXPERIMENT" + + +def _validate_resolved_candidate_pool( + candidate_pool: CandidatePool, + config: ResolvedD2DDebugConfig, +) -> None: + """Require exact agreement among physical, grid-index, and normalized rows.""" + if not isinstance(candidate_pool, CandidatePool): + raise TypeError("candidate_pool must be a CandidatePool.") + expected_indices = physical_rows_to_grid_indices( + candidate_pool.X_phys, config.design + ) + actual_indices = np.asarray(candidate_pool.grid_indices) + if actual_indices.shape != expected_indices.shape or not np.array_equal( + actual_indices, expected_indices + ): + raise ValueError( + "Candidate pool grid_indices do not match its physical rows on the " + "approved D2D grid." + ) + expected_norm = x_normalizer_np(candidate_pool.X_phys, config.design) + actual_norm = np.asarray(candidate_pool.X_norm, dtype=float) + if actual_norm.shape != expected_norm.shape or not np.allclose( + actual_norm, expected_norm, rtol=0.0, atol=1e-12 + ): + raise ValueError( + "Candidate pool normalized rows do not match its physical/grid rows." + ) + + +def propose_d2d_qlognehvi_test_batch( + candidate_pool: CandidatePool, + model: Any, + train_X_norm: torch.Tensor, + config: ResolvedD2DDebugConfig, + *, + test_only: bool, + mc_samples: int | None = None, + seed: int | None = None, + chunk_size: int = 512, +) -> QLogNEHVIBatchProposal: + """Exercise the future three-condition R2 path on sanitized data only. + + Real R2 generation is intentionally impossible through this wrapper unless + the caller supplies the explicit test-only acknowledgement. No workbook or + measured R1 candidate artifact is accepted by this API. + """ + if test_only is not True: + raise ValueError("The Step 2B qLogNEHVI boundary is synthetic test-only.") + _validate_resolved_candidate_pool(candidate_pool, config) + r2 = config.raw.get("r2_test_only", {}) + if r2.get("method") != "qlognehvi" or r2.get("sequential_pending") is not True: + raise ValueError( + "The resolved config must retain sequential test-only qLogNEHVI." + ) + if r2.get("batch_size_unique_conditions") != 3: + raise ValueError( + "The synthetic R2 test batch must contain exactly three conditions." + ) + sample_value = r2.get("mc_samples") if mc_samples is None else mc_samples + if isinstance(sample_value, (bool, np.bool_)) or not isinstance( + sample_value, (int, np.integer) + ): + raise ValueError("mc_samples must be a positive non-boolean integer.") + samples = int(sample_value) + if samples <= 0: + raise ValueError("mc_samples must be a positive non-boolean integer.") + seed_value = config.seed if seed is None else seed + if isinstance(seed_value, (bool, np.bool_)) or not isinstance( + seed_value, (int, np.integer) + ): + raise ValueError("seed must be a non-negative non-boolean integer.") + selected_seed = int(seed_value) + if selected_seed < 0: + raise ValueError("seed must be a non-negative non-boolean integer.") + local = LocalPenalizationConfig( + radius=config.local_radius, + min_batch_distance=config.min_batch_distance, + min_observed_distance=config.min_observed_distance, + dimension_weights=config.dimension_weights, + ) + objective = ConfiguredMCMultiOutputObjective(build_d2d_objective_transform()) + result = propose_qlognehvi_penalized_batch( + candidate_pool, + model, + train_X_norm, + objective, + config.reference_point_utility, + q=3, + local_penalization_config=local, + mc_samples=samples, + seed=selected_seed, + chunk_size=chunk_size, + prune_baseline=False, + ) + if result.selection.X_phys.shape != (3, len(config.design.names)): + raise RuntimeError( + "The synthetic R2 path did not return exactly three conditions." + ) + if np.unique(result.selection.X_phys, axis=0).shape[0] != 3: + raise RuntimeError("The synthetic R2 path returned duplicate conditions.") + selected_indices = physical_rows_to_grid_indices( + result.selection.X_phys, config.design + ) + selected_pool_indices = result.selection.selected_pool_indices + if not np.array_equal( + selected_indices, candidate_pool.grid_indices[selected_pool_indices] + ): + raise RuntimeError( + "The synthetic R2 selection is inconsistent with the approved grid." + ) + expected_selected_norm = x_normalizer_np(result.selection.X_phys, config.design) + if not np.allclose( + result.selection.X_norm, expected_selected_norm, rtol=0.0, atol=1e-12 + ): + raise RuntimeError( + "The synthetic R2 selection has inconsistent normalized coordinates." + ) + selection = replace( + result.selection, + method_diagnostics={ + **result.selection.method_diagnostics, + "test_only": True, + "synthetic_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_R2_TEST_WATERMARK, + }, + ) + return replace( + result, + selection=selection, + metadata={ + **result.metadata, + "test_only": True, + "synthetic_only": True, + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_R2_TEST_WATERMARK, + }, + ) + + +__all__ = ["D2D_R2_TEST_WATERMARK", "propose_d2d_qlognehvi_test_batch"] diff --git a/src/mobo_kit/d2d_scores.py b/src/mobo_kit/d2d_scores.py new file mode 100644 index 0000000..a92bb1f --- /dev/null +++ b/src/mobo_kit/d2d_scores.py @@ -0,0 +1,663 @@ +"""Campaign-specific D2D score calculations and read-only validation. + +The final scores supplied in workbook columns Z, AA, and AB are authoritative. +The helpers in this module calculate independent support values for validation; +they never replace or mutate those supplied objective values. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +from enum import Enum +import math +from numbers import Real +from typing import Iterable, Self, Sequence + +import numpy as np +import pandas as pd + + +SAMPLE_ID_COLUMN = "Sample number" +UNIFORMITY_COMPONENT_COLUMNS = ("Coverage", "1 - Uniformity", "Phase purity") +OPTOELECTRONIC_COMPONENT_COLUMNS = ( + "PL - Implied Voc (Max)", + "Photoconductance (Max)", +) +OPTOELECTRONIC_CHECK_COLUMN = "Log10 (Photoconductance (Max) x PL - Implied Voc (Max))" +THICKNESS_MEASUREMENT_COLUMNS = ("T1", "T2", "T3", "T4") +THICKNESS_CHECK_COLUMN = "Normalized thickness (sigma = 250)" +OBJECTIVE_COLUMNS = ( + "Uniformity score", + "Optoelectronic score", + "Thickness score", +) + +_REQUIRED_VALIDATION_COLUMNS = ( + *UNIFORMITY_COMPONENT_COLUMNS, + *OPTOELECTRONIC_COMPONENT_COLUMNS, + *THICKNESS_MEASUREMENT_COLUMNS, + *OBJECTIVE_COLUMNS, +) + + +class ValidationSeverity(str, Enum): + """Severity attached to a score-validation finding.""" + + WARNING = "warning" + ERROR = "error" + + +@dataclass(frozen=True) +class D2DScoreTolerances: + """Absolute comparison tolerances for two-decimal workbook score fields. + + A two-decimal rounded value can differ from its unrounded calculation by + exactly 0.005. ``rounding_slack`` absorbs only floating-point noise at that + boundary; it does not materially relax the documented rounding tolerance. + """ + + uniformity_atol: float = 0.005 + optoelectronic_atol: float = 0.005 + thickness_atol: float = 0.005 + rounding_slack: float = 1e-12 + + def __post_init__(self) -> None: + for name in ( + "uniformity_atol", + "optoelectronic_atol", + "thickness_atol", + "rounding_slack", + ): + value = getattr(self, name) + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise TypeError(f"{name} must be a real non-boolean number.") + if not math.isfinite(float(value)) or float(value) < 0: + raise ValueError(f"{name} must be finite and non-negative.") + + +@dataclass(frozen=True) +class D2DScoreFinding: + """One warning or error tied to a source DataFrame row.""" + + severity: ValidationSeverity + code: str + row_position: int + row_label: object + sample_id: object | None + column: str | None + message: str + + +@dataclass(frozen=True) +class D2DScoreRowValidation: + """Calculated and supplied score comparisons for one campaign row.""" + + row_position: int + row_label: object + sample_id: object | None + uniformity_score_authoritative: float | None + uniformity_score_calculated: float | None + uniformity_absolute_difference: float | None + uniformity_matches: bool | None + optoelectronic_check_supplied: float | None + optoelectronic_score_authoritative: float | None + optoelectronic_score_calculated: float | None + optoelectronic_check_absolute_difference: float | None + optoelectronic_check_matches: bool | None + optoelectronic_objective_absolute_difference: float | None + optoelectronic_objective_matches: bool | None + thickness_average_calculated: float | None + thickness_check_supplied: float | None + thickness_score_authoritative: float | None + thickness_score_calculated: float | None + thickness_check_absolute_difference: float | None + thickness_check_matches: bool | None + thickness_objective_absolute_difference: float | None + thickness_objective_matches: bool | None + warning_codes: tuple[str, ...] + error_codes: tuple[str, ...] + + +class D2DScoreValidationError(ValueError): + """Raised when a structured score-validation result contains errors.""" + + def __init__(self, findings: Sequence[D2DScoreFinding]) -> None: + self.findings = tuple(findings) + details = "\n- ".join(finding.message for finding in self.findings) + super().__init__(f"D2D score validation failed:\n- {details}") + + +@dataclass(frozen=True) +class D2DScoreValidationResult: + """Structured, exportable result of validating supplied D2D final scores.""" + + rows: tuple[D2DScoreRowValidation, ...] + findings: tuple[D2DScoreFinding, ...] + tolerances: D2DScoreTolerances + + @property + def warnings(self) -> tuple[D2DScoreFinding, ...]: + return tuple( + finding + for finding in self.findings + if finding.severity is ValidationSeverity.WARNING + ) + + @property + def errors(self) -> tuple[D2DScoreFinding, ...]: + return tuple( + finding + for finding in self.findings + if finding.severity is ValidationSeverity.ERROR + ) + + @property + def has_errors(self) -> bool: + return bool(self.errors) + + @property + def known_uniformity_score_mismatch(self) -> bool: + return any(finding.code == "uniformity_mismatch" for finding in self.findings) + + @property + def uniformity_warning_count(self) -> int: + """Return the number of rows with a calculated-vs-supplied mismatch.""" + return sum(finding.code == "uniformity_mismatch" for finding in self.findings) + + @property + def frame(self) -> pd.DataFrame: + """Return a new one-row-per-input-row validation DataFrame.""" + return self.to_frame() + + def to_frame(self) -> pd.DataFrame: + """Export per-row comparisons without sharing mutable source state.""" + return pd.DataFrame(asdict(row) for row in self.rows) + + def findings_frame(self) -> pd.DataFrame: + """Export warnings and errors as a separate tidy DataFrame.""" + records = [] + for finding in self.findings: + record = asdict(finding) + record["severity"] = finding.severity.value + records.append(record) + return pd.DataFrame(records) + + def raise_for_errors(self) -> Self: + """Raise :class:`D2DScoreValidationError` or return this result.""" + if self.errors: + raise D2DScoreValidationError(self.errors) + return self + + +def _finite_real(value: object, *, name: str) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise TypeError(f"{name} must be a real non-boolean number.") + number = float(value) + if not math.isfinite(number): + raise ValueError(f"{name} must be finite.") + return number + + +def _is_missing(value: object) -> bool: + if value is None: + return True + if isinstance(value, str): + return not value.replace("\u00a0", " ").strip() + missing = pd.isna(value) + return isinstance(missing, (bool, np.bool_)) and bool(missing) + + +def _optional_finite_real(value: object, *, name: str) -> float | None: + if _is_missing(value): + return None + return _finite_real(value, name=name) + + +def compute_uniformity_score( + coverage: Real, + one_minus_uniformity: Real, + phase_purity: Real, +) -> float: + """Calculate the D2D uniformity support score ``L * N * O``.""" + components = ( + _finite_real(coverage, name="coverage"), + _finite_real(one_minus_uniformity, name="one_minus_uniformity"), + _finite_real(phase_purity, name="phase_purity"), + ) + score = components[0] * components[1] * components[2] + if not math.isfinite(score): + raise ValueError("The calculated uniformity score is non-finite.") + return score + + +def compute_optoelectronic_score( + implied_voc_max: Real, + photoconductance_max: Real, +) -> float: + """Calculate ``log10(P * Q)`` for finite, strictly positive P and Q. + + Summing the two base-10 logarithms is algebraically identical to logging + the product and avoids intermediate floating-point overflow or underflow. + """ + implied_voc = _finite_real(implied_voc_max, name="implied_voc_max") + photoconductance = _finite_real(photoconductance_max, name="photoconductance_max") + if implied_voc <= 0: + raise ValueError("implied_voc_max must be strictly positive.") + if photoconductance <= 0: + raise ValueError("photoconductance_max must be strictly positive.") + return math.log10(implied_voc) + math.log10(photoconductance) + + +def compute_thickness_average( + t1: object, + t2: object, + t3: object, + t4: object, +) -> float: + """Average numeric T1:T4 values, ignoring normalized blank cells. + + Only the four named campaign measurements are accepted by the API, so the + adjacent ``T anom`` workbook field cannot enter this calculation. + """ + measurements: list[float] = [] + for position, value in enumerate((t1, t2, t3, t4), start=1): + if _is_missing(value): + continue + measurements.append(_finite_real(value, name=f"T{position}")) + if not measurements: + raise ValueError("At least one valid T1:T4 thickness value is required.") + try: + average = math.fsum(measurements) / len(measurements) + except OverflowError as exc: + raise ValueError("The calculated thickness average is non-finite.") from exc + if not math.isfinite(average): + raise ValueError("The calculated thickness average is non-finite.") + return average + + +def compute_thickness_score( + thickness_values: Iterable[object], + target: Real = 650.0, + scale: Real = 250.0, +) -> float: + """Calculate the D2D no-half-factor target score from exactly T1:T4.""" + if isinstance(thickness_values, (str, bytes)): + raise TypeError("thickness_values must contain exactly T1:T4 values.") + values = tuple(thickness_values) + if len(values) != 4: + raise ValueError("thickness_values must contain exactly T1:T4 values.") + center = _finite_real(target, name="target") + width = _finite_real(scale, name="scale") + if width <= 0: + raise ValueError("scale must be strictly positive.") + average = compute_thickness_average(*values) + standardized = (average - center) / width + return math.exp(-(standardized * standardized)) + + +def _comparison( + calculated: float, + supplied: float, + *, + absolute_tolerance: float, + rounding_slack: float, +) -> tuple[float, bool]: + difference = abs(calculated - supplied) + return difference, difference <= absolute_tolerance + rounding_slack + + +def _validate_required_columns(frame: pd.DataFrame) -> None: + duplicate_required = sorted( + { + str(column) + for column in frame.columns[frame.columns.duplicated(keep=False)] + if column in _REQUIRED_VALIDATION_COLUMNS + or column in (OPTOELECTRONIC_CHECK_COLUMN, THICKNESS_CHECK_COLUMN) + } + ) + if duplicate_required: + raise ValueError( + "Ambiguous duplicate D2D score-validation column(s): " + + ", ".join(repr(column) for column in duplicate_required) + + "." + ) + missing = [column for column in _REQUIRED_VALIDATION_COLUMNS if column not in frame] + if missing: + raise ValueError( + "Missing required D2D score-validation column(s): " + + ", ".join(repr(column) for column in missing) + + "." + ) + + +def validate_supplied_d2d_scores( + frame: pd.DataFrame, + tolerances: D2DScoreTolerances | None = None, +) -> D2DScoreValidationResult: + """Validate support equations while preserving Z/AA/AB as authoritative. + + Uniformity discrepancies are warnings. Unexpected optoelectronic or + thickness discrepancies, invalid support inputs, and missing/non-finite + final scores are errors. The authoritative Uniformity score must also stay + in its resolved [0, 1] range. Optional calculated check columns R and Y are + compared whenever their row value is present. + """ + if not isinstance(frame, pd.DataFrame): + raise TypeError("frame must be a pandas DataFrame.") + if tolerances is None: + tolerances = D2DScoreTolerances() + elif not isinstance(tolerances, D2DScoreTolerances): + raise TypeError("tolerances must be a D2DScoreTolerances instance.") + _validate_required_columns(frame) + + findings: list[D2DScoreFinding] = [] + rows: list[D2DScoreRowValidation] = [] + + for row_position in range(len(frame)): + source = frame.iloc[row_position] + row_label = frame.index[row_position] + sample_id = source.get(SAMPLE_ID_COLUMN) + if _is_missing(sample_id): + sample_id = None + row_findings: list[D2DScoreFinding] = [] + + def add_finding( + severity: ValidationSeverity, + code: str, + message: str, + *, + column: str | None = None, + ) -> None: + finding = D2DScoreFinding( + severity=severity, + code=code, + row_position=row_position, + row_label=row_label, + sample_id=sample_id, + column=column, + message=f"Row {row_label!r}: {message}", + ) + findings.append(finding) + row_findings.append(finding) + + authoritative: dict[str, float | None] = {} + for column in OBJECTIVE_COLUMNS: + try: + value = _optional_finite_real(source[column], name=column) + except (TypeError, ValueError) as exc: + value = None + add_finding( + ValidationSeverity.ERROR, + "authoritative_score_invalid", + f"authoritative {column!r} is invalid: {exc}", + column=column, + ) + else: + if value is None: + add_finding( + ValidationSeverity.ERROR, + "authoritative_score_missing", + f"authoritative {column!r} is missing.", + column=column, + ) + authoritative[column] = value + + uniformity_authoritative = authoritative[OBJECTIVE_COLUMNS[0]] + if uniformity_authoritative is not None and not ( + 0.0 <= uniformity_authoritative <= 1.0 + ): + add_finding( + ValidationSeverity.ERROR, + "uniformity_score_out_of_range", + "authoritative 'Uniformity score' must remain in [0, 1]; " + f"found {uniformity_authoritative}.", + column=OBJECTIVE_COLUMNS[0], + ) + + uniformity_calculated: float | None = None + uniformity_difference: float | None = None + uniformity_matches: bool | None = None + try: + uniformity_calculated = compute_uniformity_score( + source[UNIFORMITY_COMPONENT_COLUMNS[0]], + source[UNIFORMITY_COMPONENT_COLUMNS[1]], + source[UNIFORMITY_COMPONENT_COLUMNS[2]], + ) + except (TypeError, ValueError) as exc: + add_finding( + ValidationSeverity.WARNING, + "uniformity_support_invalid", + f"uniformity support value could not be calculated: {exc}", + ) + if uniformity_calculated is not None and uniformity_authoritative is not None: + uniformity_difference, uniformity_matches = _comparison( + uniformity_calculated, + uniformity_authoritative, + absolute_tolerance=tolerances.uniformity_atol, + rounding_slack=tolerances.rounding_slack, + ) + if not uniformity_matches: + add_finding( + ValidationSeverity.WARNING, + "uniformity_mismatch", + "calculated L*N*O differs from authoritative 'Uniformity " + f"score' by {uniformity_difference:.12g}; the supplied score " + "remains unchanged.", + column=OBJECTIVE_COLUMNS[0], + ) + + optoelectronic_calculated: float | None = None + optoelectronic_check: float | None = None + optoelectronic_check_difference: float | None = None + optoelectronic_check_matches: bool | None = None + optoelectronic_objective_difference: float | None = None + optoelectronic_objective_matches: bool | None = None + try: + optoelectronic_calculated = compute_optoelectronic_score( + source[OPTOELECTRONIC_COMPONENT_COLUMNS[0]], + source[OPTOELECTRONIC_COMPONENT_COLUMNS[1]], + ) + except (TypeError, ValueError) as exc: + add_finding( + ValidationSeverity.ERROR, + "optoelectronic_support_invalid", + f"optoelectronic support value could not be calculated: {exc}", + ) + if OPTOELECTRONIC_CHECK_COLUMN in frame: + try: + optoelectronic_check = _optional_finite_real( + source[OPTOELECTRONIC_CHECK_COLUMN], + name=OPTOELECTRONIC_CHECK_COLUMN, + ) + except (TypeError, ValueError) as exc: + add_finding( + ValidationSeverity.ERROR, + "optoelectronic_check_invalid", + f"calculated-check column R is invalid: {exc}", + column=OPTOELECTRONIC_CHECK_COLUMN, + ) + if optoelectronic_calculated is not None and optoelectronic_check is not None: + ( + optoelectronic_check_difference, + optoelectronic_check_matches, + ) = _comparison( + optoelectronic_calculated, + optoelectronic_check, + absolute_tolerance=tolerances.optoelectronic_atol, + rounding_slack=tolerances.rounding_slack, + ) + if not optoelectronic_check_matches: + add_finding( + ValidationSeverity.ERROR, + "optoelectronic_check_mismatch", + "calculated log10(P*Q) differs from check column R by " + f"{optoelectronic_check_difference:.12g}.", + column=OPTOELECTRONIC_CHECK_COLUMN, + ) + optoelectronic_authoritative = authoritative[OBJECTIVE_COLUMNS[1]] + if ( + optoelectronic_calculated is not None + and optoelectronic_authoritative is not None + ): + ( + optoelectronic_objective_difference, + optoelectronic_objective_matches, + ) = _comparison( + optoelectronic_calculated, + optoelectronic_authoritative, + absolute_tolerance=tolerances.optoelectronic_atol, + rounding_slack=tolerances.rounding_slack, + ) + if not optoelectronic_objective_matches: + add_finding( + ValidationSeverity.ERROR, + "optoelectronic_objective_mismatch", + "calculated log10(P*Q) differs from authoritative " + f"'Optoelectronic score' by " + f"{optoelectronic_objective_difference:.12g}.", + column=OBJECTIVE_COLUMNS[1], + ) + + thickness_average: float | None = None + thickness_calculated: float | None = None + thickness_check: float | None = None + thickness_check_difference: float | None = None + thickness_check_matches: bool | None = None + thickness_objective_difference: float | None = None + thickness_objective_matches: bool | None = None + thickness_values = tuple( + source[column] for column in THICKNESS_MEASUREMENT_COLUMNS + ) + try: + thickness_average = compute_thickness_average(*thickness_values) + thickness_calculated = compute_thickness_score(thickness_values) + except (TypeError, ValueError) as exc: + add_finding( + ValidationSeverity.ERROR, + "thickness_support_invalid", + f"thickness support value could not be calculated: {exc}", + ) + if THICKNESS_CHECK_COLUMN in frame: + try: + thickness_check = _optional_finite_real( + source[THICKNESS_CHECK_COLUMN], name=THICKNESS_CHECK_COLUMN + ) + except (TypeError, ValueError) as exc: + add_finding( + ValidationSeverity.ERROR, + "thickness_check_invalid", + f"calculated-check column Y is invalid: {exc}", + column=THICKNESS_CHECK_COLUMN, + ) + if thickness_calculated is not None and thickness_check is not None: + thickness_check_difference, thickness_check_matches = _comparison( + thickness_calculated, + thickness_check, + absolute_tolerance=tolerances.thickness_atol, + rounding_slack=tolerances.rounding_slack, + ) + if not thickness_check_matches: + add_finding( + ValidationSeverity.ERROR, + "thickness_check_mismatch", + "calculated no-half-factor thickness score differs from check " + f"column Y by {thickness_check_difference:.12g}.", + column=THICKNESS_CHECK_COLUMN, + ) + thickness_authoritative = authoritative[OBJECTIVE_COLUMNS[2]] + if thickness_calculated is not None and thickness_authoritative is not None: + ( + thickness_objective_difference, + thickness_objective_matches, + ) = _comparison( + thickness_calculated, + thickness_authoritative, + absolute_tolerance=tolerances.thickness_atol, + rounding_slack=tolerances.rounding_slack, + ) + if not thickness_objective_matches: + add_finding( + ValidationSeverity.ERROR, + "thickness_objective_mismatch", + "calculated no-half-factor thickness score differs from " + f"authoritative 'Thickness score' by " + f"{thickness_objective_difference:.12g}.", + column=OBJECTIVE_COLUMNS[2], + ) + + rows.append( + D2DScoreRowValidation( + row_position=row_position, + row_label=row_label, + sample_id=sample_id, + uniformity_score_authoritative=uniformity_authoritative, + uniformity_score_calculated=uniformity_calculated, + uniformity_absolute_difference=uniformity_difference, + uniformity_matches=uniformity_matches, + optoelectronic_check_supplied=optoelectronic_check, + optoelectronic_score_authoritative=optoelectronic_authoritative, + optoelectronic_score_calculated=optoelectronic_calculated, + optoelectronic_check_absolute_difference=( + optoelectronic_check_difference + ), + optoelectronic_check_matches=optoelectronic_check_matches, + optoelectronic_objective_absolute_difference=( + optoelectronic_objective_difference + ), + optoelectronic_objective_matches=optoelectronic_objective_matches, + thickness_average_calculated=thickness_average, + thickness_check_supplied=thickness_check, + thickness_score_authoritative=thickness_authoritative, + thickness_score_calculated=thickness_calculated, + thickness_check_absolute_difference=thickness_check_difference, + thickness_check_matches=thickness_check_matches, + thickness_objective_absolute_difference=( + thickness_objective_difference + ), + thickness_objective_matches=thickness_objective_matches, + warning_codes=tuple( + finding.code + for finding in row_findings + if finding.severity is ValidationSeverity.WARNING + ), + error_codes=tuple( + finding.code + for finding in row_findings + if finding.severity is ValidationSeverity.ERROR + ), + ) + ) + + return D2DScoreValidationResult( + rows=tuple(rows), findings=tuple(findings), tolerances=tolerances + ) + + +def raise_for_errors(result: D2DScoreValidationResult) -> D2DScoreValidationResult: + """Raise for a failed result; convenient for campaign-adapter pipelines.""" + if not isinstance(result, D2DScoreValidationResult): + raise TypeError("result must be a D2DScoreValidationResult.") + return result.raise_for_errors() + + +__all__ = [ + "D2DScoreFinding", + "D2DScoreRowValidation", + "D2DScoreTolerances", + "D2DScoreValidationError", + "D2DScoreValidationResult", + "OBJECTIVE_COLUMNS", + "OPTOELECTRONIC_CHECK_COLUMN", + "OPTOELECTRONIC_COMPONENT_COLUMNS", + "SAMPLE_ID_COLUMN", + "THICKNESS_CHECK_COLUMN", + "THICKNESS_MEASUREMENT_COLUMNS", + "UNIFORMITY_COMPONENT_COLUMNS", + "ValidationSeverity", + "compute_optoelectronic_score", + "compute_thickness_average", + "compute_thickness_score", + "compute_uniformity_score", + "raise_for_errors", + "validate_supplied_d2d_scores", +] diff --git a/src/mobo_kit/d2d_step2b_debug.py b/src/mobo_kit/d2d_step2b_debug.py new file mode 100644 index 0000000..e55b2ed --- /dev/null +++ b/src/mobo_kit/d2d_step2b_debug.py @@ -0,0 +1,1533 @@ +"""Read-only, debug-only D2D campaign adapter for the completed R0 workbook.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, is_dataclass +import hashlib +import json +from pathlib import Path +import platform +import subprocess +from time import perf_counter +from typing import Any + +import botorch +import gpytorch +import numpy as np +import pandas as pd +import torch + +from .batch_selection import LocalPenalizationConfig +from .candidate_diagnostics import ( + plot_candidate_pca, + plot_distance_heatmap, + plot_parallel_coordinates, + plot_selection_scores, + summarize_candidate_batch, +) +from .candidate_pool import CandidatePool, sample_discrete_candidate_pool +from .d2d_campaign import ( + D2D_DEBUG_WATERMARK, + D2D_INPUT_COLUMNS, + D2D_OBJECTIVE_COLUMNS, + D2D_REFERENCE_POINT_UTILITY, + D2DTrainingData, + ResolvedD2DDebugConfig, + build_d2d_objective_transform, + expand_candidates_to_replicates, + load_d2d_debug_config, + load_d2d_workbook_frame, + prepare_d2d_training_data, + sha256_file, +) +from .d2d_scores import validate_supplied_d2d_scores +from .models import fit_gp_models, posterior_report +from .ucb_hvi import UCBHVIBatchProposal, propose_ucb_hvi_batch + + +@dataclass(frozen=True) +class ProposalComputation: + candidate_pool: CandidatePool + proposal: UCBHVIBatchProposal + model: Any + train_X: torch.Tensor + train_Y: torch.Tensor + runtime_seconds: float + + +@dataclass(frozen=True) +class D2DDebugRunResult: + output_dir: Path + candidates_unique: pd.DataFrame + replicate_worklist: pd.DataFrame + sensitivity_summary: pd.DataFrame + sensitivity_candidates: pd.DataFrame + control_ablation: pd.DataFrame + model_diagnostics: pd.DataFrame + run_manifest: dict[str, Any] + + +@dataclass(frozen=True) +class SensitivityArtifacts: + summary: pd.DataFrame + candidates: pd.DataFrame + control_ablation: pd.DataFrame + + +def _fit_debug_model( + training: D2DTrainingData, + *, + seed: int, + include_mask: np.ndarray | None = None, +) -> tuple[Any, torch.Tensor, torch.Tensor]: + mask = ( + training.include_in_model if include_mask is None else np.asarray(include_mask) + ) + if mask.shape != training.include_in_model.shape or mask.dtype != bool: + raise ValueError( + "include_mask must be a boolean vector aligned with training rows." + ) + if np.count_nonzero(mask) < 2: + raise ValueError("At least two condition-level observations are required.") + np.random.seed(int(seed)) + torch.manual_seed(int(seed)) + X = torch.as_tensor(training.X_norm_all[mask], dtype=torch.double, device="cpu") + Y = torch.as_tensor(training.Y_objectives[mask], dtype=torch.double, device="cpu") + model = fit_gp_models(X, Y) + return model, X, Y + + +def _compute_proposal( + config: ResolvedD2DDebugConfig, + training: D2DTrainingData, + model: Any, + train_X: torch.Tensor, + train_Y: torch.Tensor, + *, + pool_size: int, + pool_seed: int, + mc_seed: int, + beta: float, + posterior_samples: int, + radius: float, + min_batch_distance: float, +) -> ProposalComputation: + started = perf_counter() + pool = sample_discrete_candidate_pool( + config.design, + pool_size, + seed=pool_seed, + observed_phys=training.X_phys_all[training.on_grid_mask], + row_constraints=[], + ) + local = LocalPenalizationConfig( + radius=radius, + min_batch_distance=min_batch_distance, + min_observed_distance=config.min_observed_distance, + dimension_weights=config.dimension_weights, + ) + proposal = propose_ucb_hvi_batch( + pool, + model, + train_Y, + build_d2d_objective_transform(), + config.reference_point_utility, + q=config.r1_batch_size, + beta=beta, + local_penalization_config=local, + observed_pending_norm=training.X_norm_all, + positive_score_tolerance=1e-12, + mc_samples=posterior_samples, + seed=mc_seed, + posterior_chunk_size=config.score_chunk_size, + hvi_chunk_size=1024, + observation_noise=False, + ) + if proposal.selection.X_phys.shape != (5, len(D2D_INPUT_COLUMNS)): + raise RuntimeError("The debug adapter did not produce exactly five conditions.") + return ProposalComputation( + candidate_pool=pool, + proposal=proposal, + model=model, + train_X=train_X, + train_Y=train_Y, + runtime_seconds=perf_counter() - started, + ) + + +def _candidate_key_set(X_phys: np.ndarray) -> set[tuple[float, ...]]: + return {tuple(float(value) for value in row) for row in np.asarray(X_phys)} + + +def _objective_prefix(objective: str) -> str: + return objective.lower().replace(" ", "_") + + +def _ordered_batch_sha256(X_phys: np.ndarray) -> str: + ordered = np.ascontiguousarray(np.asarray(X_phys, dtype=np.float64)) + return hashlib.sha256(ordered.tobytes()).hexdigest() + + +def _proposal_settings( + config: ResolvedD2DDebugConfig, + *, + pool_size: int, + pool_seed: int, + mc_seed: int, + beta: float, + posterior_samples: int, + radius: float, + min_batch_distance: float, + include_control: bool, +) -> dict[str, Any]: + return { + "candidate_pool_size": int(pool_size), + "pool_seed": int(pool_seed), + "mc_seed": int(mc_seed), + "model_seed": int(config.seed), + "beta": float(beta), + "posterior_samples": int(posterior_samples), + "local_radius": float(radius), + "min_batch_distance": float(min_batch_distance), + "min_observed_distance": float(config.min_observed_distance), + "include_control": bool(include_control), + } + + +def _observed_pareto_sample_ids( + train_Y: torch.Tensor, + sample_ids: np.ndarray, +) -> list[int | float | str]: + values = train_Y.detach().cpu().numpy() + ids = np.asarray(sample_ids) + if values.shape[0] != ids.shape[0]: + raise ValueError("sample_ids must align with train_Y rows.") + nondominated = np.ones(values.shape[0], dtype=bool) + for index, row in enumerate(values): + dominates = np.all(values >= row, axis=1) & np.any(values > row, axis=1) + nondominated[index] = not bool(np.any(dominates)) + result: list[int | float | str] = [] + for value in ids[nondominated]: + item = value.item() if isinstance(value, np.generic) else value + if isinstance(item, float) and item.is_integer(): + item = int(item) + result.append(item) + return result + + +def _pareto_summary_fields( + observed_ids: list[int | float | str], + baseline_ids: list[int | float | str], +) -> dict[str, Any]: + observed_set = {str(value) for value in observed_ids} + baseline_set = {str(value) for value in baseline_ids} + overlap = len(observed_set & baseline_set) + union = len(observed_set | baseline_set) + return { + "observed_pareto_count": len(observed_ids), + "observed_pareto_sample_ids": json.dumps(observed_ids), + "baseline_observed_pareto_count": len(baseline_ids), + "baseline_observed_pareto_sample_ids": json.dumps(baseline_ids), + "observed_pareto_overlap_count": overlap, + "observed_pareto_jaccard_with_baseline": ( + float(overlap / union) if union else 1.0 + ), + } + + +def _batch_summary( + label: str, + computation: ProposalComputation, + baseline_phys: np.ndarray, + baseline_norm: np.ndarray, + *, + parameter: str, + value: Any, + settings: dict[str, Any], + fit_runtime_seconds: float, + observed_pareto_ids: list[int | float | str], + baseline_pareto_ids: list[int | float | str], +) -> dict[str, Any]: + selected = computation.proposal.selection.X_phys + selected_norm = computation.proposal.selection.X_norm + baseline_keys = _candidate_key_set(baseline_phys) + keys = _candidate_key_set(selected) + intersection = len(keys & baseline_keys) + union = len(keys | baseline_keys) + differences = selected_norm[:, None, :] - baseline_norm[None, :, :] + nearest = np.sqrt(np.sum(differences**2, axis=-1)).min(axis=1) + distance = computation.proposal.selection.distance_diagnostics + row = { + "run_label": label, + "parameter": parameter, + "value": value, + "status": "pass", + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "selected_count": int(selected.shape[0]), + "exact_overlap_with_baseline": intersection, + "jaccard_overlap_with_baseline": float(intersection / union), + "mean_nearest_batch_distance_to_baseline": float(nearest.mean()), + "minimum_within_batch_distance": distance["minimum_within_batch_distance"], + "mean_within_batch_distance": distance["mean_within_batch_distance"], + "maximum_within_batch_distance": distance["maximum_within_batch_distance"], + "boundary_coordinate_count": int( + np.count_nonzero( + np.isclose(selected_norm, 0.0, atol=1e-12) + | np.isclose(selected_norm, 1.0, atol=1e-12) + ) + ), + "ordered_batch_sha256": _ordered_batch_sha256(selected), + **settings, + "fit_runtime_seconds": float(fit_runtime_seconds), + "proposal_runtime_seconds": float(computation.runtime_seconds), + "total_fit_proposal_runtime_seconds": float( + fit_runtime_seconds + computation.runtime_seconds + ), + "warning": "", + } + row.update(_pareto_summary_fields(observed_pareto_ids, baseline_pareto_ids)) + return row + + +def _sensitivity_candidate_rows( + label: str, + computation: ProposalComputation, + config: ResolvedD2DDebugConfig, + training: D2DTrainingData, + *, + settings: dict[str, Any], + fit_runtime_seconds: float, +) -> list[dict[str, Any]]: + selection = computation.proposal.selection + predicted_mean, predicted_std = posterior_report( + computation.model, + torch.as_tensor(selection.X_norm, dtype=torch.double), + ) + diagnostics = summarize_candidate_batch( + selection.X_norm, + observed_pending_norm=training.X_norm_all, + X_phys=selection.X_phys, + design=config.design, + dimension_weights=config.dimension_weights, + metadata={"debug_only": True, "run_label": label}, + ) + pairwise = diagnostics.pairwise_distance_matrix + pairwise_nearest = pairwise.copy() + np.fill_diagonal(pairwise_nearest, np.inf) + ordered_hash = _ordered_batch_sha256(selection.X_phys) + rows: list[dict[str, Any]] = [] + for index, step in enumerate(selection.steps): + boundary_names = [ + name + for name, is_boundary in zip( + D2D_INPUT_COLUMNS, diagnostics.boundary_flags[index] + ) + if is_boundary + ] + row: dict[str, Any] = { + "record_type": "selected_candidate", + "run_label": label, + "candidate_id": f"{label}-C{index + 1:02d}", + "selection_order": index + 1, + "selected_pool_index": int(step.pool_index), + "status": "pass", + "warning": "", + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + } + for column, candidate_value in zip(D2D_INPUT_COLUMNS, selection.X_phys[index]): + row[column] = float(candidate_value) + for objective_index, objective in enumerate(D2D_OBJECTIVE_COLUMNS): + prefix = _objective_prefix(objective) + row[f"predicted_{prefix}_mean"] = float( + predicted_mean[index, objective_index] + ) + row[f"predicted_{prefix}_std"] = float( + predicted_std[index, objective_index] + ) + final_penalized_score = ( + np.nan + if step.base_score is None + else float(step.base_score * step.penalty_factor) + ) + row.update( + { + "base_raw_score": step.base_score, + "base_log_score": step.base_log_score, + "penalty_factor": step.penalty_factor, + "log_penalty": step.log_penalty, + "penalized_log_score": step.penalized_log_score, + "final_penalized_score": final_penalized_score, + "pairwise_distance_row": json.dumps( + [float(distance) for distance in pairwise[index]] + ), + "nearest_selected_distance": float(pairwise_nearest[index].min()), + "nearest_observed_distance": float( + diagnostics.nearest_observed_pending_distance[index] + ), + "boundary_coordinates": json.dumps(boundary_names), + "boundary_coordinate_count": len(boundary_names), + "grid_valid": bool(diagnostics.grid_valid_rows[index]), + "bounds_valid": bool( + np.all(selection.X_norm[index] >= 0.0) + and np.all(selection.X_norm[index] <= 1.0) + ), + "ordered_batch_sha256": ordered_hash, + **settings, + "candidate_pool_accepted": int(computation.candidate_pool.size), + "candidate_pool_draws": int(computation.candidate_pool.draws), + "candidate_pool_duplicate_rejections": int( + computation.candidate_pool.rejected_duplicate + ), + "candidate_pool_avoid_rejections": int( + computation.candidate_pool.rejected_avoid + ), + "candidate_pool_constraint_rejections": int( + computation.candidate_pool.rejected_constraint + ), + "fit_runtime_seconds": float(fit_runtime_seconds), + "proposal_runtime_seconds": float(computation.runtime_seconds), + "total_fit_proposal_runtime_seconds": float( + fit_runtime_seconds + computation.runtime_seconds + ), + } + ) + rows.append(row) + return rows + + +def _failed_summary_row( + label: str, + *, + parameter: str, + value: Any, + settings: dict[str, Any], + fit_runtime_seconds: float, + proposal_runtime_seconds: float, + warning: str, + observed_pareto_ids: list[int | float | str], + baseline_pareto_ids: list[int | float | str], +) -> dict[str, Any]: + row = { + "run_label": label, + "parameter": parameter, + "value": value, + "status": "failed", + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "selected_count": 0, + "exact_overlap_with_baseline": 0, + "jaccard_overlap_with_baseline": 0.0, + "mean_nearest_batch_distance_to_baseline": np.nan, + "minimum_within_batch_distance": np.nan, + "mean_within_batch_distance": np.nan, + "maximum_within_batch_distance": np.nan, + "boundary_coordinate_count": np.nan, + "ordered_batch_sha256": "", + **settings, + "fit_runtime_seconds": float(fit_runtime_seconds), + "proposal_runtime_seconds": float(proposal_runtime_seconds), + "total_fit_proposal_runtime_seconds": float( + fit_runtime_seconds + proposal_runtime_seconds + ), + "warning": warning, + } + row.update(_pareto_summary_fields(observed_pareto_ids, baseline_pareto_ids)) + return row + + +def _failed_sensitivity_candidate_row( + label: str, + *, + settings: dict[str, Any], + fit_runtime_seconds: float, + proposal_runtime_seconds: float, + warning: str, +) -> dict[str, Any]: + row: dict[str, Any] = { + "record_type": "failed_run_marker", + "run_label": label, + "candidate_id": "", + "selection_order": np.nan, + "selected_pool_index": np.nan, + "status": "failed", + "warning": warning, + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "base_raw_score": np.nan, + "base_log_score": np.nan, + "penalty_factor": np.nan, + "log_penalty": np.nan, + "penalized_log_score": np.nan, + "final_penalized_score": np.nan, + "pairwise_distance_row": "[]", + "nearest_selected_distance": np.nan, + "nearest_observed_distance": np.nan, + "boundary_coordinates": "[]", + "boundary_coordinate_count": np.nan, + "grid_valid": False, + "bounds_valid": False, + "ordered_batch_sha256": "", + **settings, + "fit_runtime_seconds": float(fit_runtime_seconds), + "proposal_runtime_seconds": float(proposal_runtime_seconds), + "total_fit_proposal_runtime_seconds": float( + fit_runtime_seconds + proposal_runtime_seconds + ), + } + for column in D2D_INPUT_COLUMNS: + row[column] = np.nan + for objective in D2D_OBJECTIVE_COLUMNS: + prefix = _objective_prefix(objective) + row[f"predicted_{prefix}_mean"] = np.nan + row[f"predicted_{prefix}_std"] = np.nan + return row + + +def _control_ablation_columns() -> list[str]: + columns = [ + "record_type", + "candidate_id", + "selection_order", + *D2D_INPUT_COLUMNS, + ] + for objective in D2D_OBJECTIVE_COLUMNS: + prefix = _objective_prefix(objective) + columns.extend( + [ + f"baseline_predicted_{prefix}_mean", + f"baseline_predicted_{prefix}_std", + f"control_excluded_predicted_{prefix}_mean", + f"control_excluded_predicted_{prefix}_std", + f"delta_{prefix}_mean", + f"delta_{prefix}_std", + ] + ) + columns.extend( + [ + "selected_exact_overlap_count", + "selected_jaccard_overlap", + "selected_mean_nearest_distance_to_baseline", + "control_excluded_ordered_batch_sha256", + "baseline_observed_pareto_count", + "baseline_observed_pareto_sample_ids", + "control_excluded_observed_pareto_count", + "control_excluded_observed_pareto_sample_ids", + "observed_pareto_overlap_count", + "observed_pareto_jaccard_with_baseline", + "fit_runtime_seconds", + "proposal_runtime_seconds", + "total_fit_proposal_runtime_seconds", + "status", + "warning", + "debug_only", + "approved_for_experiment", + "candidate_status", + ] + ) + return columns + + +def _empty_control_ablation() -> pd.DataFrame: + return pd.DataFrame(columns=_control_ablation_columns()) + + +def _stamp_uniformity_mismatch( + artifacts: SensitivityArtifacts, + known_uniformity_score_mismatch: bool, +) -> SensitivityArtifacts: + frames = [ + artifacts.summary.copy(), + artifacts.candidates.copy(), + artifacts.control_ablation.copy(), + ] + for frame in frames: + frame["known_uniformity_score_mismatch"] = bool(known_uniformity_score_mismatch) + return SensitivityArtifacts(*frames) + + +def _control_ablation_rows( + baseline: ProposalComputation, + ablation: ProposalComputation, + baseline_summary: dict[str, Any], + *, + baseline_pareto_ids: list[int | float | str], + ablation_pareto_ids: list[int | float | str], + fit_runtime_seconds: float, +) -> list[dict[str, Any]]: + baseline_phys = baseline.proposal.selection.X_phys + baseline_norm = baseline.proposal.selection.X_norm + baseline_mean, baseline_std = posterior_report( + baseline.model, torch.as_tensor(baseline_norm, dtype=torch.double) + ) + ablation_mean, ablation_std = posterior_report( + ablation.model, torch.as_tensor(baseline_norm, dtype=torch.double) + ) + pareto_fields = _pareto_summary_fields(ablation_pareto_ids, baseline_pareto_ids) + rows: list[dict[str, Any]] = [] + for index in range(baseline_phys.shape[0]): + row: dict[str, Any] = { + "record_type": "baseline_candidate_prediction_comparison", + "candidate_id": f"R1-C{index + 1:02d}", + "selection_order": index + 1, + } + for column, candidate_value in zip(D2D_INPUT_COLUMNS, baseline_phys[index]): + row[column] = float(candidate_value) + for objective_index, objective in enumerate(D2D_OBJECTIVE_COLUMNS): + prefix = _objective_prefix(objective) + row.update( + { + f"baseline_predicted_{prefix}_mean": float( + baseline_mean[index, objective_index] + ), + f"baseline_predicted_{prefix}_std": float( + baseline_std[index, objective_index] + ), + f"control_excluded_predicted_{prefix}_mean": float( + ablation_mean[index, objective_index] + ), + f"control_excluded_predicted_{prefix}_std": float( + ablation_std[index, objective_index] + ), + f"delta_{prefix}_mean": float( + ablation_mean[index, objective_index] + - baseline_mean[index, objective_index] + ), + f"delta_{prefix}_std": float( + ablation_std[index, objective_index] + - baseline_std[index, objective_index] + ), + } + ) + row.update( + { + "selected_exact_overlap_count": baseline_summary[ + "exact_overlap_with_baseline" + ], + "selected_jaccard_overlap": baseline_summary[ + "jaccard_overlap_with_baseline" + ], + "selected_mean_nearest_distance_to_baseline": baseline_summary[ + "mean_nearest_batch_distance_to_baseline" + ], + "control_excluded_ordered_batch_sha256": baseline_summary[ + "ordered_batch_sha256" + ], + "baseline_observed_pareto_count": len(baseline_pareto_ids), + "baseline_observed_pareto_sample_ids": json.dumps(baseline_pareto_ids), + "control_excluded_observed_pareto_count": len(ablation_pareto_ids), + "control_excluded_observed_pareto_sample_ids": json.dumps( + ablation_pareto_ids + ), + "observed_pareto_overlap_count": pareto_fields[ + "observed_pareto_overlap_count" + ], + "observed_pareto_jaccard_with_baseline": pareto_fields[ + "observed_pareto_jaccard_with_baseline" + ], + "fit_runtime_seconds": float(fit_runtime_seconds), + "proposal_runtime_seconds": float(ablation.runtime_seconds), + "total_fit_proposal_runtime_seconds": float( + fit_runtime_seconds + ablation.runtime_seconds + ), + "status": "pass", + "warning": "", + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + } + ) + rows.append(row) + return rows + + +def _failed_control_ablation_row( + *, + fit_runtime_seconds: float, + proposal_runtime_seconds: float, + warning: str, +) -> dict[str, Any]: + row = {column: np.nan for column in _control_ablation_columns()} + row.update( + { + "record_type": "failed_run_marker", + "candidate_id": "", + "status": "failed", + "warning": warning, + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "fit_runtime_seconds": float(fit_runtime_seconds), + "proposal_runtime_seconds": float(proposal_runtime_seconds), + "total_fit_proposal_runtime_seconds": float( + fit_runtime_seconds + proposal_runtime_seconds + ), + } + ) + return row + + +def _run_sensitivity( + config: ResolvedD2DDebugConfig, + training: D2DTrainingData, + baseline: ProposalComputation, + *, + baseline_fit_runtime_seconds: float, + known_uniformity_score_mismatch: bool, +) -> SensitivityArtifacts: + baseline_phys = baseline.proposal.selection.X_phys + baseline_norm = baseline.proposal.selection.X_norm + baseline_pareto_ids = _observed_pareto_sample_ids( + baseline.train_Y, + training.sample_ids[training.include_in_model], + ) + rows: list[dict[str, Any]] = [] + candidate_rows: list[dict[str, Any]] = [] + control_rows: list[dict[str, Any]] = [] + baseline_settings = _proposal_settings( + config, + pool_size=config.candidate_pool_size, + pool_seed=config.seed, + mc_seed=config.seed, + beta=config.beta, + posterior_samples=config.posterior_samples, + radius=config.local_radius, + min_batch_distance=config.min_batch_distance, + include_control=True, + ) + baseline_row = _batch_summary( + "baseline", + baseline, + baseline_phys, + baseline_norm, + parameter="baseline", + value="configured", + settings=baseline_settings, + fit_runtime_seconds=baseline_fit_runtime_seconds, + observed_pareto_ids=baseline_pareto_ids, + baseline_pareto_ids=baseline_pareto_ids, + ) + rows.append(baseline_row) + candidate_rows.extend( + _sensitivity_candidate_rows( + "baseline", + baseline, + config, + training, + settings=baseline_settings, + fit_runtime_seconds=baseline_fit_runtime_seconds, + ) + ) + + settings = { + "pool_size": config.candidate_pool_size, + "pool_seed": config.seed, + "mc_seed": config.seed, + "beta": config.beta, + "posterior_samples": config.posterior_samples, + "radius": config.local_radius, + "min_batch_distance": config.min_batch_distance, + } + variations = [ + ("beta_1", "beta", 1.0), + ("beta_9", "beta", 9.0), + ("pool_5000", "pool_size", 5000), + ("pool_20000", "pool_size", 20000), + ("pool_seed_137", "pool_seed", 137), + ("pool_seed_911", "pool_seed", 911), + ("radius_0.15", "radius", 0.15), + ("radius_0.35", "radius", 0.35), + ("min_batch_0.10", "min_batch_distance", 0.10), + ("min_batch_0.20", "min_batch_distance", 0.20), + ("posterior_128", "posterior_samples", 128), + ] + for label, parameter, value in variations: + current = dict(settings) + current[parameter] = value + actual_settings = _proposal_settings( + config, + pool_size=int(current["pool_size"]), + pool_seed=int(current["pool_seed"]), + mc_seed=int(current["mc_seed"]), + beta=float(current["beta"]), + posterior_samples=int(current["posterior_samples"]), + radius=float(current["radius"]), + min_batch_distance=float(current["min_batch_distance"]), + include_control=True, + ) + variation_started = perf_counter() + try: + computation = _compute_proposal( + config, + training, + baseline.model, + baseline.train_X, + baseline.train_Y, + pool_size=int(current["pool_size"]), + pool_seed=int(current["pool_seed"]), + mc_seed=int(current["mc_seed"]), + beta=float(current["beta"]), + posterior_samples=int(current["posterior_samples"]), + radius=float(current["radius"]), + min_batch_distance=float(current["min_batch_distance"]), + ) + row = _batch_summary( + label, + computation, + baseline_phys, + baseline_norm, + parameter=parameter, + value=value, + settings=actual_settings, + fit_runtime_seconds=0.0, + observed_pareto_ids=baseline_pareto_ids, + baseline_pareto_ids=baseline_pareto_ids, + ) + candidate_rows.extend( + _sensitivity_candidate_rows( + label, + computation, + config, + training, + settings=actual_settings, + fit_runtime_seconds=0.0, + ) + ) + except Exception as exc: # sensitivity failures are reported, never hidden + warning = f"{type(exc).__name__}: {exc}" + failed_runtime = perf_counter() - variation_started + row = _failed_summary_row( + label, + parameter=parameter, + value=value, + settings=actual_settings, + fit_runtime_seconds=0.0, + proposal_runtime_seconds=failed_runtime, + warning=warning, + observed_pareto_ids=baseline_pareto_ids, + baseline_pareto_ids=baseline_pareto_ids, + ) + candidate_rows.append( + _failed_sensitivity_candidate_row( + label, + settings=actual_settings, + fit_runtime_seconds=0.0, + proposal_runtime_seconds=failed_runtime, + warning=warning, + ) + ) + rows.append(row) + + ablation_mask = training.include_in_model.copy() + ablation_mask[np.isin(training.sample_ids, config.control_sample_ids)] = False + ablation_settings = _proposal_settings( + config, + pool_size=config.candidate_pool_size, + pool_seed=config.seed, + mc_seed=config.seed, + beta=config.beta, + posterior_samples=config.posterior_samples, + radius=config.local_radius, + min_batch_distance=config.min_batch_distance, + include_control=False, + ) + ablation_started = perf_counter() + ablation_fit_runtime = 0.0 + try: + fit_started = perf_counter() + model, train_X, train_Y = _fit_debug_model( + training, seed=config.seed, include_mask=ablation_mask + ) + ablation_fit_runtime = perf_counter() - fit_started + ablation = _compute_proposal( + config, + training, + model, + train_X, + train_Y, + pool_size=config.candidate_pool_size, + pool_seed=config.seed, + mc_seed=config.seed, + beta=config.beta, + posterior_samples=config.posterior_samples, + radius=config.local_radius, + min_batch_distance=config.min_batch_distance, + ) + row = _batch_summary( + "control_excluded", + ablation, + baseline_phys, + baseline_norm, + parameter="include_control", + value=False, + settings=ablation_settings, + fit_runtime_seconds=ablation_fit_runtime, + observed_pareto_ids=_observed_pareto_sample_ids( + train_Y, training.sample_ids[ablation_mask] + ), + baseline_pareto_ids=baseline_pareto_ids, + ) + ablation_pareto_ids = _observed_pareto_sample_ids( + train_Y, training.sample_ids[ablation_mask] + ) + candidate_rows.extend( + _sensitivity_candidate_rows( + "control_excluded", + ablation, + config, + training, + settings=ablation_settings, + fit_runtime_seconds=ablation_fit_runtime, + ) + ) + control_rows.extend( + _control_ablation_rows( + baseline, + ablation, + row, + baseline_pareto_ids=baseline_pareto_ids, + ablation_pareto_ids=ablation_pareto_ids, + fit_runtime_seconds=ablation_fit_runtime, + ) + ) + mean_delta_columns = [ + f"delta_{_objective_prefix(objective)}_mean" + for objective in D2D_OBJECTIVE_COLUMNS + ] + row["mean_abs_prediction_change_on_baseline_candidates"] = float( + pd.DataFrame(control_rows) + .loc[:, mean_delta_columns] + .abs() + .to_numpy() + .mean() + ) + except Exception as exc: + warning = f"{type(exc).__name__}: {exc}" + total_failure_runtime = perf_counter() - ablation_started + proposal_failure_runtime = max( + 0.0, total_failure_runtime - ablation_fit_runtime + ) + row = _failed_summary_row( + "control_excluded", + parameter="include_control", + value=False, + settings=ablation_settings, + fit_runtime_seconds=ablation_fit_runtime, + proposal_runtime_seconds=proposal_failure_runtime, + warning=warning, + observed_pareto_ids=[], + baseline_pareto_ids=baseline_pareto_ids, + ) + row["mean_abs_prediction_change_on_baseline_candidates"] = np.nan + candidate_rows.append( + _failed_sensitivity_candidate_row( + "control_excluded", + settings=ablation_settings, + fit_runtime_seconds=ablation_fit_runtime, + proposal_runtime_seconds=proposal_failure_runtime, + warning=warning, + ) + ) + control_rows.append( + _failed_control_ablation_row( + fit_runtime_seconds=ablation_fit_runtime, + proposal_runtime_seconds=proposal_failure_runtime, + warning=warning, + ) + ) + rows.append(row) + return _stamp_uniformity_mismatch( + SensitivityArtifacts( + summary=pd.DataFrame(rows), + candidates=pd.DataFrame(candidate_rows), + control_ablation=pd.DataFrame(control_rows).reindex( + columns=_control_ablation_columns() + ), + ), + known_uniformity_score_mismatch, + ) + + +def _model_diagnostics( + model: Any, + train_X: torch.Tensor, + train_Y: torch.Tensor, +) -> pd.DataFrame: + predicted, uncertainty = posterior_report(model, train_X) + observed = train_Y.detach().cpu().numpy() + rows: list[dict[str, Any]] = [] + for index, objective in enumerate(D2D_OBJECTIVE_COLUMNS): + residual = predicted[:, index] - observed[:, index] + denominator = float( + np.sum((observed[:, index] - observed[:, index].mean()) ** 2) + ) + r_squared = ( + np.nan + if denominator <= 0 + else 1.0 - float(np.sum(residual**2)) / denominator + ) + rows.append( + { + "objective": objective, + "training_count": int(observed.shape[0]), + "r_squared_training_posterior": r_squared, + "rmse_training_posterior": float(np.sqrt(np.mean(residual**2))), + "mae_training_posterior": float(np.mean(np.abs(residual))), + "mean_posterior_std": float(np.mean(uncertainty[:, index])), + "maximum_abs_residual": float(np.max(np.abs(residual))), + } + ) + return pd.DataFrame(rows) + + +def _candidates_frame( + computation: ProposalComputation, + config: ResolvedD2DDebugConfig, + training: D2DTrainingData, + *, + known_uniformity_score_mismatch: bool, +) -> tuple[pd.DataFrame, pd.DataFrame]: + selection = computation.proposal.selection + selected_t = torch.as_tensor(selection.X_norm, dtype=torch.double) + predicted_mean, predicted_std = posterior_report(computation.model, selected_t) + diagnostics = summarize_candidate_batch( + selection.X_norm, + observed_pending_norm=training.X_norm_all, + X_phys=selection.X_phys, + design=config.design, + dimension_weights=config.dimension_weights, + metadata={"debug_only": True}, + ) + pairwise = diagnostics.pairwise_distance_matrix.copy() + np.fill_diagonal(pairwise, np.inf) + nearest_selected = pairwise.min(axis=1) + rows: list[dict[str, Any]] = [] + diagnostic_rows: list[dict[str, Any]] = [] + for index, step in enumerate(selection.steps): + candidate_id = f"R1-C{index + 1:02d}" + row: dict[str, Any] = { + "campaign_id": str(config.raw["campaign_id"]), + "round": "R1", + "sample_id": pd.NA, + "candidate_id": candidate_id, + "row_role": "candidate_condition", + "replicate_group": candidate_id, + "replicate_number": pd.NA, + "selection_order": index + 1, + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "include_in_model": False, + "measurement_provenance": "proposed_unmeasured", + "off_grid_exception": False, + "exclusion_reason": "awaiting_measurement", + "known_uniformity_score_mismatch": bool(known_uniformity_score_mismatch), + } + for column, value in zip(D2D_INPUT_COLUMNS, selection.X_phys[index]): + row[column] = float(value) + for objective_index, objective in enumerate(D2D_OBJECTIVE_COLUMNS): + prefix = _objective_prefix(objective) + row[f"predicted_{prefix}_mean"] = float( + predicted_mean[index, objective_index] + ) + row[f"predicted_{prefix}_std"] = float( + predicted_std[index, objective_index] + ) + row.update( + { + "base_ucb_hvi": step.base_score, + "base_log_ucb_hvi": step.base_log_score, + "penalty_factor": step.penalty_factor, + "penalized_log_score": step.penalized_log_score, + "final_penalized_score": ( + np.nan + if step.base_score is None + else float(step.base_score * step.penalty_factor) + ), + "nearest_selected_distance": float(nearest_selected[index]), + "nearest_observed_distance": float( + diagnostics.nearest_observed_pending_distance[index] + ), + "grid_valid": bool(diagnostics.grid_valid_rows[index]), + "bounds_valid": bool( + np.all(selection.X_norm[index] >= 0.0) + and np.all(selection.X_norm[index] <= 1.0) + ), + "boundary_coordinate_count": int( + np.count_nonzero(diagnostics.boundary_flags[index]) + ), + "pool_seed": config.seed, + "mc_seed": config.seed, + "beta": config.beta, + "kappa": float(np.sqrt(config.beta)), + "candidate_pool_size": config.candidate_pool_size, + "posterior_samples": config.posterior_samples, + "reference_point_utility": json.dumps( + config.reference_point_utility.tolist() + ), + "config_sha256": config.config_hash, + } + ) + rows.append(row) + diagnostic_rows.append( + { + "candidate_id": candidate_id, + "selection_order": index + 1, + "nearest_selected_distance": nearest_selected[index], + "nearest_observed_distance": diagnostics.nearest_observed_pending_distance[ + index + ], + "grid_valid": diagnostics.grid_valid_rows[index], + "bounds_valid": row["bounds_valid"], + "boundary_dimensions": "|".join( + name + for name, is_boundary in zip( + D2D_INPUT_COLUMNS, diagnostics.boundary_flags[index] + ) + if is_boundary + ), + "debug_only": True, + "approved_for_experiment": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "known_uniformity_score_mismatch": bool( + known_uniformity_score_mismatch + ), + } + ) + frame = pd.DataFrame(rows) + if not frame["grid_valid"].all() or not frame["bounds_valid"].all(): + raise RuntimeError( + "A selected debug candidate failed grid or bounds validation." + ) + if frame.loc[:, list(D2D_INPUT_COLUMNS)].duplicated().any(): + raise RuntimeError("The selected debug batch contains duplicate conditions.") + if any( + step.base_score is None or step.base_score <= 1e-12 for step in selection.steps + ): + raise RuntimeError("Every selected debug candidate must have positive UCB-HVI.") + return frame, pd.DataFrame(diagnostic_rows) + + +def _jsonable(value: Any) -> Any: + if is_dataclass(value): + return _jsonable(asdict(value)) + if isinstance(value, Path): + return str(value) + if isinstance(value, np.ndarray): + return value.tolist() + if isinstance(value, np.generic): + return value.item() + if isinstance(value, tuple): + return [_jsonable(item) for item in value] + if isinstance(value, list): + return [_jsonable(item) for item in value] + if isinstance(value, dict): + return {str(key): _jsonable(item) for key, item in value.items()} + return value + + +def _git_commit(repository_root: Path) -> str: + try: + completed = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=repository_root, + check=True, + capture_output=True, + text=True, + timeout=10, + ) + return completed.stdout.strip() + except (OSError, subprocess.SubprocessError): + return "unavailable" + + +def _validate_output_destination( + destination: Path, + config_file: Path, + config: ResolvedD2DDebugConfig, +) -> None: + """Keep in-repository debug writes below the configured ignored output root.""" + del config_file # The guard must also apply when callers use a copied config. + repository_root = Path(__file__).resolve().parents[2] + if destination != repository_root and repository_root not in destination.parents: + # Pytest and other callers may deliberately use an isolated temp directory. + return + allowed_root = (repository_root / config.output_root).resolve() + if destination != allowed_root and allowed_root not in destination.parents: + raise ValueError( + "An output destination inside the repository must be under the " + f"configured debug output root: {allowed_root}." + ) + + +def _write_debug_bundle( + output_dir: Path, + *, + audit_payload: dict[str, Any], + score_validation: pd.DataFrame, + training_manifest: pd.DataFrame, + model_diagnostics: pd.DataFrame, + candidates: pd.DataFrame, + worklist: pd.DataFrame, + candidate_diagnostics: pd.DataFrame, + sensitivity: pd.DataFrame, + sensitivity_candidates: pd.DataFrame, + control_ablation: pd.DataFrame, + run_manifest: dict[str, Any], + computation: ProposalComputation, + training: D2DTrainingData, + config: ResolvedD2DDebugConfig, +) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + plots = output_dir / "plots" + plots.mkdir(parents=True, exist_ok=True) + (output_dir / "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt").write_text( + D2D_DEBUG_WATERMARK + + "\nThese candidates are generated only to test the algorithm.\n", + encoding="utf-8", + ) + (output_dir / "workbook_audit.json").write_text( + json.dumps(_jsonable(audit_payload), indent=2, sort_keys=True), + encoding="utf-8", + ) + score_validation.to_csv(output_dir / "score_validation.csv", index=False) + training_manifest.to_csv(output_dir / "training_row_manifest.csv", index=False) + model_diagnostics.to_csv(output_dir / "model_diagnostics.csv", index=False) + candidates.to_csv(output_dir / "r1_debug_candidates_unique.csv", index=False) + worklist.to_csv(output_dir / "r1_debug_replicate_worklist.csv", index=False) + candidate_diagnostics.to_csv(output_dir / "candidate_diagnostics.csv", index=False) + sensitivity.to_csv(output_dir / "sensitivity_summary.csv", index=False) + sensitivity_candidates.to_csv( + output_dir / "sensitivity_candidates_long.csv", index=False + ) + control_ablation.to_csv(output_dir / "control_ablation.csv", index=False) + + selection = computation.proposal.selection + plot_candidate_pca( + training.X_norm_all, + selection.X_norm, + plots / "r1_candidate_pca.png", + pool_norm=computation.candidate_pool.X_norm, + seed=config.seed, + watermark=D2D_DEBUG_WATERMARK, + ) + plot_parallel_coordinates( + selection.X_norm, + D2D_INPUT_COLUMNS, + plots / "r1_parallel_coordinates.png", + watermark=D2D_DEBUG_WATERMARK, + ) + plot_distance_heatmap( + selection.X_norm, + plots / "r1_distance_heatmap.png", + dimension_weights=config.dimension_weights, + watermark=D2D_DEBUG_WATERMARK, + ) + plot_selection_scores( + [step.order for step in selection.steps], + [step.base_log_score for step in selection.steps], + [step.penalized_log_score for step in selection.steps], + plots / "r1_acquisition_scores.png", + watermark=D2D_DEBUG_WATERMARK, + ) + (output_dir / "run_manifest.json").write_text( + json.dumps(_jsonable(run_manifest), indent=2, sort_keys=True), + encoding="utf-8", + ) + + +def run_d2d_step2b_debug( + workbook_path: str | Path, + config_path: str | Path, + output_dir: str | Path, + *, + overwrite: bool = False, + run_sensitivity: bool = True, +) -> D2DDebugRunResult: + """Generate a fully watermarked five-condition R1 algorithm-debug bundle. + + The source workbook is opened read-only and never saved. The legacy + ``run_mobo_experiment(..., propose_candidates=True)`` path is not used. + """ + started = perf_counter() + workbook = Path(workbook_path).resolve() + config_file = Path(config_path).resolve() + destination = Path(output_dir).resolve() + if ( + destination == workbook + or destination == workbook.parent + or workbook.parent in destination.parents + ): + raise ValueError( + "Debug output must not target the source workbook or its directory." + ) + config = load_d2d_debug_config(config_file) + _validate_output_destination(destination, config_file, config) + source_hash_before = sha256_file(workbook) + source_mtime_before = workbook.stat().st_mtime_ns + if source_hash_before != config.expected_workbook_sha256: + raise ValueError( + "Workbook hash does not match the resolved Step 2B config: " + f"expected={config.expected_workbook_sha256}, actual={source_hash_before}." + ) + if destination.exists() and any(destination.iterdir()) and not overwrite: + raise FileExistsError( + f"Debug output directory is not empty: {destination}. " + "Use a new run directory or set overwrite=True." + ) + + frame, audit = load_d2d_workbook_frame( + workbook, + expected_profile=config.workbook_profile, + expected_sample_ids=config.expected_sample_ids, + allowed_input_exceptions=config.off_grid_exceptions, + ) + if audit.used_range != config.expected_content_range: + raise ValueError( + f"Workbook range must be {config.expected_content_range}; found {audit.used_range}." + ) + validation = validate_supplied_d2d_scores(frame) + validation.raise_for_errors() + training = prepare_d2d_training_data(frame, config, include_control=True) + if np.count_nonzero(training.include_in_model) != 15: + raise RuntimeError( + "The primary debug model must contain all 15 R0 observations." + ) + fit_started = perf_counter() + model, train_X, train_Y = _fit_debug_model(training, seed=config.seed) + baseline_fit_runtime = perf_counter() - fit_started + computation = _compute_proposal( + config, + training, + model, + train_X, + train_Y, + pool_size=config.candidate_pool_size, + pool_seed=config.seed, + mc_seed=config.seed, + beta=config.beta, + posterior_samples=config.posterior_samples, + radius=config.local_radius, + min_batch_distance=config.min_batch_distance, + ) + candidates, candidate_diagnostics = _candidates_frame( + computation, + config, + training, + known_uniformity_score_mismatch=(validation.known_uniformity_score_mismatch), + ) + worklist = expand_candidates_to_replicates( + candidates, + replicates_per_condition=config.replicates_per_condition, + round_name="R1", + ) + worklist["known_uniformity_score_mismatch"] = bool( + validation.known_uniformity_score_mismatch + ) + if worklist.shape[0] != 15: + raise RuntimeError("Five R1 conditions must expand to exactly 15 executions.") + diagnostics = _model_diagnostics(model, train_X, train_Y) + if run_sensitivity: + sensitivity_artifacts = _run_sensitivity( + config, + training, + computation, + baseline_fit_runtime_seconds=baseline_fit_runtime, + known_uniformity_score_mismatch=( + validation.known_uniformity_score_mismatch + ), + ) + else: + baseline_settings = _proposal_settings( + config, + pool_size=config.candidate_pool_size, + pool_seed=config.seed, + mc_seed=config.seed, + beta=config.beta, + posterior_samples=config.posterior_samples, + radius=config.local_radius, + min_batch_distance=config.min_batch_distance, + include_control=True, + ) + baseline_pareto_ids = _observed_pareto_sample_ids( + train_Y, training.sample_ids[training.include_in_model] + ) + baseline_summary = _batch_summary( + "baseline", + computation, + computation.proposal.selection.X_phys, + computation.proposal.selection.X_norm, + parameter="baseline", + value="configured", + settings=baseline_settings, + fit_runtime_seconds=baseline_fit_runtime, + observed_pareto_ids=baseline_pareto_ids, + baseline_pareto_ids=baseline_pareto_ids, + ) + sensitivity_artifacts = _stamp_uniformity_mismatch( + SensitivityArtifacts( + summary=pd.DataFrame([baseline_summary]), + candidates=pd.DataFrame( + _sensitivity_candidate_rows( + "baseline", + computation, + config, + training, + settings=baseline_settings, + fit_runtime_seconds=baseline_fit_runtime, + ) + ), + control_ablation=_empty_control_ablation(), + ), + validation.known_uniformity_score_mismatch, + ) + sensitivity = sensitivity_artifacts.summary + + source_hash_after_compute = sha256_file(workbook) + source_mtime_after_compute = workbook.stat().st_mtime_ns + if ( + source_hash_after_compute != source_hash_before + or source_mtime_after_compute != source_mtime_before + ): + raise RuntimeError( + "The source workbook changed during the read-only debug run." + ) + + training_manifest = training.manifest_frame() + training_manifest["measurement_provenance"] = "measured_in_current_campaign" + training_manifest["debug_only"] = True + training_manifest["approved_for_experiment"] = False + training_manifest["candidate_status"] = D2D_DEBUG_WATERMARK + training_manifest["known_uniformity_score_mismatch"] = bool( + validation.known_uniformity_score_mismatch + ) + audit_payload = asdict(audit) + audit_payload.update( + { + "source_sha256_before": source_hash_before, + "source_sha256_after_compute": source_hash_after_compute, + "source_mtime_ns_before": source_mtime_before, + "source_mtime_ns_after_compute": source_mtime_after_compute, + "source_workbook_modified": False, + } + ) + manifest: dict[str, Any] = { + "schema_version": "d2d-step2b-debug-run-v1", + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "watermark": D2D_DEBUG_WATERMARK, + "known_uniformity_score_mismatch": (validation.known_uniformity_score_mismatch), + "uniformity_warning_count": validation.uniformity_warning_count, + "score_validation_error_count": len(validation.errors), + "objective_order": list(D2D_OBJECTIVE_COLUMNS), + "objective_transforms": ["identity", "identity", "identity"], + "objective_directions": ["maximize", "maximize", "maximize"], + "reference_point_utility": D2D_REFERENCE_POINT_UTILITY.tolist(), + "ignored_model_columns": ["Stability score?", "AD:AI"], + "training_row_count": int(train_X.shape[0]), + "control_sample_ids": list(config.control_sample_ids), + "control_included": True, + "off_grid_observed_exceptions": [ + asdict(exception) for exception in config.off_grid_exceptions + ], + "candidate_count_unique": int(candidates.shape[0]), + "replicate_execution_count": int(worklist.shape[0]), + "baseline_hypervolume": computation.proposal.scoring.baseline_hypervolume, + "observed_pareto_count": int( + computation.proposal.scoring.pareto_utility.shape[0] + ), + "candidate_pool": { + "requested": config.candidate_pool_size, + "accepted": computation.candidate_pool.size, + "draws": computation.candidate_pool.draws, + "duplicate_rejections": computation.candidate_pool.rejected_duplicate, + "avoid_rejections": computation.candidate_pool.rejected_avoid, + "constraint_rejections": computation.candidate_pool.rejected_constraint, + }, + "r1_settings": { + "model_seed": config.seed, + "pool_seed": config.seed, + "mc_seed": config.seed, + "beta": config.beta, + "kappa": float(np.sqrt(config.beta)), + "posterior_samples": config.posterior_samples, + "score_chunk_size": config.score_chunk_size, + "local_radius": config.local_radius, + "min_batch_distance": config.min_batch_distance, + "min_observed_distance": config.min_observed_distance, + }, + "config_sha256": config.config_hash, + "workbook_sha256": source_hash_before, + "git_commit": _git_commit(Path(__file__).resolve().parents[2]), + "runtime_versions": { + "python": platform.python_version(), + "numpy": np.__version__, + "pandas": pd.__version__, + "torch": torch.__version__, + "botorch": botorch.__version__, + "gpytorch": gpytorch.__version__, + }, + "warnings": [*validation.warnings, *training.warnings], + "sensitivity_run_count": int(sensitivity.shape[0]), + "sensitivity_failure_count": int((sensitivity["status"] == "failed").sum()), + "sensitivity_candidate_record_count": int( + sensitivity_artifacts.candidates.shape[0] + ), + "control_ablation_record_count": int( + sensitivity_artifacts.control_ablation.shape[0] + ), + "runtime_seconds_before_output": perf_counter() - started, + "source_workbook_modified": False, + } + + _write_debug_bundle( + destination, + audit_payload=audit_payload, + score_validation=validation.frame, + training_manifest=training_manifest, + model_diagnostics=diagnostics, + candidates=candidates, + worklist=worklist, + candidate_diagnostics=candidate_diagnostics, + sensitivity=sensitivity, + sensitivity_candidates=sensitivity_artifacts.candidates, + control_ablation=sensitivity_artifacts.control_ablation, + run_manifest=manifest, + computation=computation, + training=training, + config=config, + ) + source_hash_after = sha256_file(workbook) + source_mtime_after = workbook.stat().st_mtime_ns + if ( + source_hash_after != source_hash_before + or source_mtime_after != source_mtime_before + ): + raise RuntimeError( + "The source workbook changed while writing the debug bundle." + ) + manifest["source_sha256_after"] = source_hash_after + manifest["source_mtime_ns_after"] = source_mtime_after + manifest["runtime_seconds_total"] = perf_counter() - started + (destination / "run_manifest.json").write_text( + json.dumps(_jsonable(manifest), indent=2, sort_keys=True), encoding="utf-8" + ) + return D2DDebugRunResult( + output_dir=destination, + candidates_unique=candidates, + replicate_worklist=worklist, + sensitivity_summary=sensitivity, + sensitivity_candidates=sensitivity_artifacts.candidates, + control_ablation=sensitivity_artifacts.control_ablation, + model_diagnostics=diagnostics, + run_manifest=manifest, + ) + + +__all__ = ["D2DDebugRunResult", "run_d2d_step2b_debug"] diff --git a/src/mobo_kit/d2d_step2c_config.py b/src/mobo_kit/d2d_step2c_config.py new file mode 100644 index 0000000..6de5118 --- /dev/null +++ b/src/mobo_kit/d2d_step2c_config.py @@ -0,0 +1,918 @@ +"""Fail-closed configuration for the D2D Step 2C robustness study.""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import json +from numbers import Real +from pathlib import Path +from typing import Any, Mapping + +import numpy as np +import yaml + +from .d2d_campaign import ( + D2D_DEBUG_WATERMARK, + D2D_INPUT_COLUMNS, + D2D_OBJECTIVE_COLUMNS, + D2D_OBJECTIVE_NAMES, + D2D_REFERENCE_POINT_UTILITY, + OffGridObservedException, + ResolvedD2DDebugConfig, + load_d2d_debug_config, +) + + +STEP2C_SCHEMA_VERSION = "d2d-step2c-robustness-debug-1" +STEP2C_OUTPUT_ROOT = "local_outputs/d2d_step2c_robustness" +STEP2C_NESTED_POOL_SIZES = (16384, 32768, 65536, 131072) +STEP2C_SECONDARY_SOBOL_SEEDS = (137, 911) +STEP2C_BETA_VALUES = (1.0, 4.0, 9.0) +STEP2C_BOUND_POLICIES = ("none", "clip_ucb") +STEP2C_PRIVATE_SOURCE_KIND = "private_pinned_workbook" +STEP2C_SYNTHETIC_SOURCE_KIND = "sanitized_synthetic_ci" +STEP2C_RUNTIME_GENERATED = "runtime_generated" + + +class Step2CConfigError(ValueError): + """Raised when a Step 2C config weakens the debug-only contract.""" + + +@dataclass(frozen=True) +class PenaltyVariant: + label: str + radius: float | None + min_batch_distance: float + + +@dataclass(frozen=True) +class ExecutionModeSettings: + nested_unique_sizes: tuple[int, ...] + anchors_per_selection_step: int + omitted_sample_ids: tuple[int, ...] + mc_comparison_samples: int + + +@dataclass(frozen=True) +class ResolvedStep2CConfig: + """Validated Step 2C settings plus the audited Step 2B ingestion contract.""" + + raw: dict[str, Any] + config_path: Path + config_sha256: str + resolved_config_hash: str + base_config_path: Path + base: ResolvedD2DDebugConfig + workbook_source_kind: str + workbook_relative_path: str + workbook_expected_sha256: str + resolved_off_grid_exceptions: tuple[OffGridObservedException, ...] + objective_bounds: tuple[tuple[float | None, float | None], ...] + beta_values: tuple[float, ...] + primary_beta: float + moment_method: str + score_chunk_size: int + positive_hvi_threshold: float + numeric_tolerance: float + bound_policies: tuple[str, ...] + primary_bound_policy: str + mc_comparison_samples: int + mc_comparison_seed: int + primary_sobol_seed: int + secondary_sobol_seeds: tuple[int, ...] + nested_pool_sizes: tuple[int, ...] + refinement_anchors: int + refinement_max_sweeps: int + refinement_tolerance: float + penalty_variants: tuple[PenaltyVariant, ...] + primary_penalty_variant: str + model_variant_names: tuple[str, ...] + primary_model_variant: str + influence_pool_size: int + influence_pool_seed: int + influence_sample_ids: tuple[int, ...] + influence_top_k: int + regional_thresholds: tuple[float, ...] + region_threshold: float + region_sensitivity_thresholds: tuple[float, ...] + shortlist_min: int + shortlist_max: int + consensus_batch_size: int + consensus_nested_match_min: int + consensus_mean_distance_max: float + consensus_family_coverage_min: int + execution_modes: dict[str, ExecutionModeSettings] + model_seed: int + output_root: str + create_portable_zip: bool + + # These properties make the object safe to pass to the already-audited + # Step 2B workbook/training adapter without duplicating that boundary. + @property + def design(self): + return self.base.design + + @property + def expected_sample_ids(self) -> tuple[int, ...]: + return self.base.expected_sample_ids + + @property + def reference_point_utility(self) -> np.ndarray: + return self.base.reference_point_utility.copy() + + @property + def control_sample_ids(self) -> tuple[int, ...]: + return self.base.control_sample_ids + + @property + def control_measurement_provenance_assumption(self) -> str: + return self.base.control_measurement_provenance_assumption + + @property + def off_grid_exceptions(self): + return self.resolved_off_grid_exceptions + + def mode(self, name: str) -> ExecutionModeSettings: + try: + return self.execution_modes[name] + except KeyError as exc: + raise Step2CConfigError("execution mode must be 'fast' or 'full'.") from exc + + +def _mapping(value: Any, *, field: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise Step2CConfigError(f"{field} must be a mapping.") + return value + + +def _integer(value: Any, *, field: str, minimum: int = 0) -> int: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)): + raise Step2CConfigError(f"{field} must be an integer >= {minimum}.") + result = int(value) + if result < minimum: + raise Step2CConfigError(f"{field} must be an integer >= {minimum}.") + return result + + +def _number(value: Any, *, field: str, minimum: float | None = None) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise Step2CConfigError(f"{field} must be a finite real number.") + result = float(value) + if not np.isfinite(result) or (minimum is not None and result < minimum): + raise Step2CConfigError(f"{field} must be finite and >= {minimum}.") + return result + + +def _int_tuple(value: Any, *, field: str, minimum: int = 0) -> tuple[int, ...]: + if not isinstance(value, list) or not value: + raise Step2CConfigError(f"{field} must be a non-empty integer list.") + return tuple( + _integer(item, field=f"{field}[{index}]", minimum=minimum) + for index, item in enumerate(value) + ) + + +def _number_tuple(value: Any, *, field: str) -> tuple[float, ...]: + if not isinstance(value, list) or not value: + raise Step2CConfigError(f"{field} must be a non-empty numeric list.") + return tuple( + _number(item, field=f"{field}[{index}]") for index, item in enumerate(value) + ) + + +def _canonical_hash(value: Mapping[str, Any]) -> str: + payload = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest().upper() + + +def _file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest().upper() + + +def _resolve_repository_path(raw_path: Any, *, field: str) -> Path: + if not isinstance(raw_path, str) or not raw_path.strip(): + raise Step2CConfigError(f"{field} must be a nonblank repository path.") + repository_root = Path(__file__).resolve().parents[2] + candidate = Path(raw_path) + resolved = ( + (repository_root / candidate).resolve() + if not candidate.is_absolute() + else candidate.resolve() + ) + if resolved != repository_root and repository_root not in resolved.parents: + raise Step2CConfigError(f"{field} must resolve inside the repository.") + return resolved + + +def _validate_debug_boundary(raw: dict[str, Any]) -> None: + exact = { + "schema_version": STEP2C_SCHEMA_VERSION, + "run_mode": "debug", + "approved_for_experiment": False, + "approved_for_production": False, + "debug_watermark": D2D_DEBUG_WATERMARK, + } + for field, expected in exact.items(): + actual = raw.get(field) + if isinstance(expected, bool): + valid = isinstance(actual, bool) and actual is expected + else: + valid = actual == expected + if not valid: + raise Step2CConfigError(f"{field} must be exactly {expected!r}.") + campaign_id = raw.get("campaign_id") + if ( + not isinstance(campaign_id, str) + or not campaign_id.strip() + or len(campaign_id) > 128 + or not campaign_id.endswith("-debug") + or "/" in campaign_id + or "\\" in campaign_id + ): + raise Step2CConfigError( + "campaign_id must be a nonblank, path-free identifier ending in '-debug'." + ) + + +def _sha256(value: Any, *, field: str) -> str: + if not isinstance(value, str) or len(value) != 64: + raise Step2CConfigError(f"{field} must be a 64-character SHA-256 digest.") + try: + int(value, 16) + except ValueError as exc: + raise Step2CConfigError( + f"{field} must be a 64-character SHA-256 digest." + ) from exc + return value.upper() + + +def _relative_path_under(raw_path: Any, root: str, *, field: str) -> str: + if not isinstance(raw_path, str) or not raw_path.strip(): + raise Step2CConfigError(f"{field} must be a nonblank relative path.") + candidate = Path(raw_path) + if candidate.is_absolute() or ".." in candidate.parts: + raise Step2CConfigError(f"{field} must be a safe repository-relative path.") + normalized = candidate.as_posix() + if not normalized.startswith(f"{root}/"): + raise Step2CConfigError(f"{field} must be below {root}/.") + _resolve_repository_path(normalized, field=field) + return normalized + + +def _validate_workbook_and_objectives( + raw: dict[str, Any], base: ResolvedD2DDebugConfig +) -> tuple[str, str, str, tuple[tuple[float | None, float | None], ...]]: + workbook = _mapping(raw.get("workbook"), field="workbook") + source_kind = workbook.get("source_kind") + if source_kind == STEP2C_SYNTHETIC_SOURCE_KIND: + allowed_keys = {"source_kind", "path", "expected_sha256"} + if set(workbook) not in ({"source_kind"}, allowed_keys): + raise Step2CConfigError( + "Synthetic workbook settings must be runtime-generated or contain " + "only source_kind, path, and expected_sha256." + ) + if set(workbook) == {"source_kind"}: + workbook_path = STEP2C_RUNTIME_GENERATED + workbook_hash = STEP2C_RUNTIME_GENERATED.upper() + else: + workbook_path = _relative_path_under( + workbook["path"], STEP2C_OUTPUT_ROOT, field="workbook.path" + ) + workbook_hash = _sha256( + workbook["expected_sha256"], field="workbook.expected_sha256" + ) + else: + source_kind = STEP2C_PRIVATE_SOURCE_KIND + expected_keys = { + "profile", + "path", + "sheet", + "expected_range", + "expected_sha256", + "objective_columns", + } + if set(workbook) != expected_keys: + raise Step2CConfigError( + "Private workbook settings must preserve the complete read-only " + "ingestion contract." + ) + if ( + workbook["profile"] != base.workbook_profile + or workbook["sheet"] != base.workbook_sheet + or workbook["expected_range"] != base.expected_content_range + or workbook["objective_columns"] + != [objective["excel_column"] for objective in base.raw["objectives"]] + ): + raise Step2CConfigError( + "Private workbook settings differ from the audited base contract." + ) + workbook_path = _relative_path_under( + workbook["path"], "local_inputs", field="workbook.path" + ) + workbook_hash = _sha256( + workbook["expected_sha256"], field="workbook.expected_sha256" + ) + if workbook_hash != base.expected_workbook_sha256: + raise Step2CConfigError( + "Private workbook hash differs from the audited base contract." + ) + + objectives = _mapping(raw.get("objectives"), field="objectives") + if objectives.get("order") != list(D2D_OBJECTIVE_NAMES): + raise Step2CConfigError("objectives.order must preserve the three D2D scores.") + if objectives.get("source_columns") != list(D2D_OBJECTIVE_COLUMNS): + raise Step2CConfigError( + "objectives.source_columns must preserve Z/AA/AB roles." + ) + reference = np.asarray(objectives.get("reference_point"), dtype=float) + if reference.shape != (3,) or not np.array_equal( + reference, D2D_REFERENCE_POINT_UTILITY + ): + raise Step2CConfigError( + "objectives.reference_point must be [-0.01, -10.0, -0.01]." + ) + if objectives.get("transforms") != ["identity"] * 3: + raise Step2CConfigError("All Step 2C objective transforms must be identity.") + if objectives.get("directions") != ["maximize"] * 3: + raise Step2CConfigError("All Step 2C objectives must be maximized.") + expected_bounds = { + "uniformity_score": {"lower": 0.0, "upper": 1.0}, + "optoelectronic_score": {"lower": None, "upper": None}, + "thickness_score": {"lower": 0.0, "upper": 1.0}, + } + if objectives.get("bounds") != expected_bounds: + raise Step2CConfigError( + "objectives.bounds must preserve the declared score support." + ) + mismatch = objectives.get("known_uniformity_mismatch") + if mismatch != { + "allowed_in_debug": True, + "blocks_experimental_approval": True, + }: + raise Step2CConfigError( + "The known Uniformity mismatch must remain visible and approval-blocking." + ) + return ( + source_kind, + workbook_path, + workbook_hash, + ((0.0, 1.0), (None, None), (0.0, 1.0)), + ) + + +def _validate_r0( + raw: dict[str, Any], + base: ResolvedD2DDebugConfig, + *, + workbook_source_kind: str, +) -> tuple[OffGridObservedException, ...]: + section = _mapping(raw.get("r0"), field="r0") + if section.get("inherit_from_base") is True: + allowed_keys = {"inherit_from_base", "synthetic_off_grid_override"} + if set(section) - allowed_keys: + raise Step2CConfigError("r0 contains unsupported inherited settings.") + override = section.get("synthetic_off_grid_override") + if override is None: + resolved = base.off_grid_exceptions + else: + if workbook_source_kind != STEP2C_SYNTHETIC_SOURCE_KIND: + raise Step2CConfigError( + "An r0 synthetic override requires a synthetic workbook source." + ) + value = _mapping(override, field="r0.synthetic_off_grid_override") + if set(value) != {"sample_id", "field", "value"}: + raise Step2CConfigError( + "r0.synthetic_off_grid_override has unsupported fields." + ) + if len(base.off_grid_exceptions) != 1: + raise Step2CConfigError( + "A synthetic override requires one inherited off-grid exception." + ) + inherited = base.off_grid_exceptions[0] + sample_id = _integer( + value["sample_id"], field="r0.synthetic_off_grid_override.sample_id" + ) + field = value["field"] + observed = _number( + value["value"], field="r0.synthetic_off_grid_override.value" + ) + if sample_id != inherited.sample_id or field != inherited.input_name: + raise Step2CConfigError( + "The synthetic off-grid override must target the inherited row " + "and input." + ) + dimension = tuple(base.design.names).index(field) + grid = np.asarray(base.design.var_array[dimension], dtype=float) + if np.any(np.isclose(observed, grid, rtol=0.0, atol=1.0e-12)): + raise Step2CConfigError( + "The synthetic off-grid override must remain outside the grid." + ) + if observed < float(grid.min()) or observed > float(grid.max()): + raise Step2CConfigError( + "The synthetic off-grid override must remain within input bounds." + ) + resolved = ( + OffGridObservedException( + sample_id=sample_id, + input_name=field, + observed_value=observed, + reason="sanitized synthetic off-grid fixture", + ), + ) + else: + expected_exceptions = [ + { + "sample_id": exception.sample_id, + "field": exception.input_name, + "value": exception.observed_value, + "include_in_gp": True, + "include_in_distance_reference": True, + "include_in_grid_index_exclusion": False, + } + for exception in base.off_grid_exceptions + ] + expected = { + "sample_count": len(base.expected_sample_ids), + "control_sample_ids": list(base.control_sample_ids), + "include_control_in_primary_model": True, + "control_measurement_provenance_assumption": ( + base.control_measurement_provenance_assumption + ), + "off_grid_observed_exceptions": expected_exceptions, + } + if section != expected: + raise Step2CConfigError( + "r0 must preserve the audited control/off-grid policy." + ) + resolved = base.off_grid_exceptions + if raw.get("constraints") != []: + raise Step2CConfigError("constraints must remain an explicit empty list.") + return tuple(resolved) + + +def _validate_penalties(raw: dict[str, Any]) -> tuple[tuple[PenaltyVariant, ...], str]: + section = _mapping(raw.get("local_penalty_study"), field="local_penalty_study") + if section.get("hard_distance_relaxation") is not False: + raise Step2CConfigError("Hard-distance relaxation must remain false.") + expected = ( + PenaltyVariant("no_soft_no_hard", None, 0.0), + PenaltyVariant("no_soft_hard_0_15", None, 0.15), + PenaltyVariant("radius_0_15", 0.15, 0.15), + PenaltyVariant("radius_0_25", 0.25, 0.15), + PenaltyVariant("radius_0_35", 0.35, 0.15), + ) + variants = section.get("variants") + if not isinstance(variants, list) or len(variants) != len(expected): + raise Step2CConfigError( + "local_penalty_study.variants must contain five variants." + ) + parsed: list[PenaltyVariant] = [] + for index, item in enumerate(variants): + value = _mapping(item, field=f"local_penalty_study.variants[{index}]") + radius_raw = value.get("radius") + radius = ( + None + if radius_raw is None + else _number( + radius_raw, + field=f"local_penalty_study.variants[{index}].radius", + minimum=0.0, + ) + ) + parsed.append( + PenaltyVariant( + label=str(value.get("label", "")), + radius=radius, + min_batch_distance=_number( + value.get("min_batch_distance"), + field=f"local_penalty_study.variants[{index}].min_batch_distance", + minimum=0.0, + ), + ) + ) + if tuple(parsed) != expected: + raise Step2CConfigError("local_penalty_study variants changed from the pack.") + if section.get("primary_variant") != "radius_0_25": + raise Step2CConfigError("primary local-penalty variant must be radius_0_25.") + if ( + section.get("min_observed_distance") != 0.0 + or section.get("dimension_weights") is not None + ): + raise Step2CConfigError( + "Observed-distance and dimension-weight settings changed." + ) + return tuple(parsed), "radius_0_25" + + +def _validate_execution_modes( + raw: dict[str, Any], *, expected_sample_ids: tuple[int, ...] +) -> dict[str, ExecutionModeSettings]: + section = _mapping(raw.get("execution_modes"), field="execution_modes") + if set(section) != {"fast", "full"}: + raise Step2CConfigError("execution_modes must contain exactly fast and full.") + result: dict[str, ExecutionModeSettings] = {} + for name in ("fast", "full"): + item = _mapping(section[name], field=f"execution_modes.{name}") + result[name] = ExecutionModeSettings( + nested_unique_sizes=_int_tuple( + item.get("nested_unique_sizes"), + field=f"execution_modes.{name}.nested_unique_sizes", + minimum=1, + ), + anchors_per_selection_step=_integer( + item.get("anchors_per_selection_step"), + field=f"execution_modes.{name}.anchors_per_selection_step", + minimum=1, + ), + omitted_sample_ids=_int_tuple( + item.get("omitted_sample_ids"), + field=f"execution_modes.{name}.omitted_sample_ids", + minimum=1, + ), + mc_comparison_samples=_integer( + item.get("mc_comparison_samples"), + field=f"execution_modes.{name}.mc_comparison_samples", + minimum=2, + ), + ) + expected = { + "fast": ExecutionModeSettings( + nested_unique_sizes=(512, 1024, 2048, 4096), + anchors_per_selection_step=8, + omitted_sample_ids=expected_sample_ids[:3], + mc_comparison_samples=256, + ), + "full": ExecutionModeSettings( + nested_unique_sizes=STEP2C_NESTED_POOL_SIZES, + anchors_per_selection_step=64, + omitted_sample_ids=expected_sample_ids, + mc_comparison_samples=2048, + ), + } + if result != expected: + raise Step2CConfigError( + "execution_modes must preserve the declared fast/full audit settings." + ) + return result + + +def load_step2c_config(path: str | Path) -> ResolvedStep2CConfig: + """Load and validate the full debug-only Step 2C robustness contract.""" + config_path = Path(path).resolve() + if not config_path.is_file(): + raise FileNotFoundError(f"Step 2C config not found: {config_path}") + with config_path.open("r", encoding="utf-8") as handle: + raw = yaml.safe_load(handle) + if not isinstance(raw, dict): + raise Step2CConfigError("Step 2C config must contain a mapping.") + _validate_debug_boundary(raw) + + base_path = _resolve_repository_path( + raw.get("base_step2b_config"), field="base_step2b_config" + ) + workbook_section = _mapping(raw.get("workbook"), field="workbook") + allow_public_template = ( + workbook_section.get("source_kind") == STEP2C_SYNTHETIC_SOURCE_KIND + ) + base = load_d2d_debug_config(base_path, allow_public_template=allow_public_template) + ( + workbook_source_kind, + workbook_path, + workbook_hash, + objective_bounds, + ) = _validate_workbook_and_objectives(raw, base) + off_grid_exceptions = _validate_r0( + raw, base, workbook_source_kind=workbook_source_kind + ) + + ucb = _mapping(raw.get("ucb_hvi"), field="ucb_hvi") + beta_values = _number_tuple(ucb.get("beta_values"), field="ucb_hvi.beta_values") + if beta_values != STEP2C_BETA_VALUES: + raise Step2CConfigError("ucb_hvi.beta_values must be [1.0, 4.0, 9.0].") + bound_policies_raw = ucb.get("bound_policies") + if ( + not isinstance(bound_policies_raw, list) + or tuple(bound_policies_raw) != STEP2C_BOUND_POLICIES + ): + raise Step2CConfigError("ucb_hvi.bound_policies must be [none, clip_ucb].") + if ucb.get("moment_method") != "analytic_identity": + raise Step2CConfigError("ucb_hvi.moment_method must be analytic_identity.") + primary_beta = _number(ucb.get("primary_beta"), field="ucb_hvi.primary_beta") + score_chunk_size = _integer( + ucb.get("score_chunk_size"), field="ucb_hvi.score_chunk_size", minimum=1 + ) + numeric_tolerance = _number( + ucb.get("numeric_tolerance"), + field="ucb_hvi.numeric_tolerance", + minimum=0.0, + ) + primary_bound_policy = str(ucb.get("primary_bound_policy")) + mc_comparison_samples = _integer( + ucb.get("mc_comparison_samples"), + field="ucb_hvi.mc_comparison_samples", + minimum=2, + ) + mc_comparison_seed = _integer( + ucb.get("mc_comparison_seed"), field="ucb_hvi.mc_comparison_seed" + ) + if ( + primary_beta, + score_chunk_size, + numeric_tolerance, + primary_bound_policy, + mc_comparison_samples, + mc_comparison_seed, + ) != (4.0, 2048, 1.0e-12, "clip_ucb", 2048, 73): + raise Step2CConfigError( + "The primary UCB-HVI, clipping, numeric, and MC-audit settings changed." + ) + positive_threshold = _number( + ucb.get("positive_hvi_threshold"), + field="ucb_hvi.positive_hvi_threshold", + minimum=0.0, + ) + if positive_threshold != 0.0: + raise Step2CConfigError("positive_hvi_threshold must express raw HVI > 0.") + + search = _mapping(raw.get("candidate_search"), field="candidate_search") + if ( + search.get("method") != "nested_sobol_grid_indices" + or search.get("scramble") is not True + ): + raise Step2CConfigError( + "candidate_search must use scrambled nested Sobol grid indices." + ) + nested = _int_tuple( + search.get("nested_unique_sizes"), + field="candidate_search.nested_unique_sizes", + minimum=1, + ) + if ( + nested != STEP2C_NESTED_POOL_SIZES + or search.get("primary_full_size") != nested[-1] + ): + raise Step2CConfigError( + "candidate_search must preserve the four declared full sizes." + ) + if search.get("preserve_accepted_prefix_nesting") is not True: + raise Step2CConfigError("Accepted-prefix nesting must remain mandatory.") + primary_seed = _integer( + search.get("primary_seed"), field="candidate_search.primary_seed" + ) + secondary_seeds = _int_tuple( + search.get("secondary_seeds"), field="candidate_search.secondary_seeds" + ) + if primary_seed != 73 or secondary_seeds != STEP2C_SECONDARY_SOBOL_SEEDS: + raise Step2CConfigError("Sobol seeds must remain 73 / [137, 911].") + + refinement = _mapping(raw.get("local_refinement"), field="local_refinement") + expected_refinement_literals = { + "enabled": True, + "coordinate_values": "all_allowed_grid_values", + "stable_tie_break": "lower_grid_index", + } + for field, expected in expected_refinement_literals.items(): + if refinement.get(field) != expected: + raise Step2CConfigError(f"local_refinement.{field} must be {expected!r}.") + anchors = _integer( + refinement.get("anchors_per_selection_step"), + field="local_refinement.anchors_per_selection_step", + minimum=1, + ) + sweeps = _integer( + refinement.get("max_sweeps"), field="local_refinement.max_sweeps", minimum=1 + ) + tolerance = _number( + refinement.get("improvement_tolerance"), + field="local_refinement.improvement_tolerance", + minimum=0.0, + ) + if (anchors, sweeps, tolerance) != (64, 10, 1.0e-10): + raise Step2CConfigError("local_refinement settings changed from the pack.") + + penalties, primary_penalty = _validate_penalties(raw) + + models = _mapping(raw.get("models"), field="models") + expected_models = [ + {"name": "default_current", "type": "existing_default"}, + { + "name": "conservative", + "type": "explicit_conservative", + "min_noise": 0.01, + "min_lengthscale": 0.05, + }, + ] + if models.get("variants") != expected_models: + raise Step2CConfigError( + "models.variants must preserve default and conservative settings." + ) + if ( + models.get("primary_for_debug") != "default_current" + or models.get("exact_leave_one_out") is not True + or models.get("report_training_posterior_only_as_diagnostic") is not True + ): + raise Step2CConfigError("models must preserve the strict validation policy.") + + influence = _mapping( + raw.get("observation_influence"), field="observation_influence" + ) + influence_ids = _int_tuple( + influence.get("omitted_sample_ids"), + field="observation_influence.omitted_sample_ids", + minimum=1, + ) + regional_thresholds = _number_tuple( + influence.get("regional_thresholds"), + field="observation_influence.regional_thresholds", + ) + if ( + influence.get("enabled") is not True + or influence_ids != base.expected_sample_ids + or regional_thresholds != (0.10, 0.15, 0.20) + ): + raise Step2CConfigError( + "observation_influence must cover every configured sample and three " + "thresholds." + ) + influence_pool_size = _integer( + influence.get("common_pool_size"), + field="observation_influence.common_pool_size", + minimum=1, + ) + influence_pool_seed = _integer( + influence.get("common_pool_seed"), + field="observation_influence.common_pool_seed", + ) + influence_top_k = _integer( + influence.get("top_k"), field="observation_influence.top_k", minimum=1 + ) + if (influence_pool_size, influence_pool_seed, influence_top_k) != ( + 32768, + 73, + 100, + ): + raise Step2CConfigError( + "The observation-influence common pool, seed, or top-K changed." + ) + + regions = _mapping(raw.get("robust_regions"), field="robust_regions") + if regions.get("clustering") != "agglomerative_complete_link": + raise Step2CConfigError("robust_regions.clustering must use complete link.") + criteria = _mapping( + regions.get("consensus_required_criteria"), + field="robust_regions.consensus_required_criteria", + ) + expected_boolean_criteria = { + "require_grid_valid": True, + "require_hard_distance_valid": True, + } + for field, expected in expected_boolean_criteria.items(): + if criteria.get(field) is not expected: + raise Step2CConfigError(f"consensus criterion {field} must remain true.") + region_threshold = _number( + regions.get("primary_distance_threshold"), + field="robust_regions.primary_distance_threshold", + minimum=0.0, + ) + region_sensitivity_thresholds = _number_tuple( + regions.get("sensitivity_thresholds"), + field="robust_regions.sensitivity_thresholds", + ) + shortlist_min = _integer( + regions.get("shortlist_min"), field="robust_regions.shortlist_min", minimum=1 + ) + shortlist_max = _integer( + regions.get("shortlist_max"), field="robust_regions.shortlist_max", minimum=1 + ) + consensus_batch_size = _integer( + regions.get("consensus_batch_size"), + field="robust_regions.consensus_batch_size", + minimum=1, + ) + consensus_nested_match_min = _integer( + criteria.get("largest_two_nested_regional_matches_within_0_15"), + field="consensus.nested_match_min", + minimum=1, + ) + consensus_mean_distance_max = _number( + criteria.get("largest_two_nested_mean_matched_distance_max"), + field="consensus.mean_distance_max", + minimum=0.0, + ) + consensus_family_coverage_min = _integer( + criteria.get("minimum_region_family_coverage"), + field="consensus.family_coverage_min", + minimum=1, + ) + if ( + region_threshold, + region_sensitivity_thresholds, + shortlist_min, + shortlist_max, + consensus_batch_size, + consensus_nested_match_min, + consensus_mean_distance_max, + consensus_family_coverage_min, + ) != (0.15, (0.10, 0.20), 8, 12, 5, 4, 0.10, 3): + raise Step2CConfigError( + "Robust-region thresholds, shortlist limits, or consensus gates changed." + ) + + execution_modes = _validate_execution_modes( + raw, expected_sample_ids=base.expected_sample_ids + ) + + reproducibility = _mapping(raw.get("reproducibility"), field="reproducibility") + expected_reproducibility = { + "model_seed": 73, + "record_git_commit": True, + "record_dirty_state": True, + "record_environment_versions": True, + "record_config_hash": True, + "record_workbook_hash_and_mtime_before_after": True, + } + if reproducibility != expected_reproducibility: + raise Step2CConfigError("reproducibility settings must remain fully enabled.") + outputs = _mapping(raw.get("outputs"), field="outputs") + if outputs != { + "root": STEP2C_OUTPUT_ROOT, + "tracked_private_recipes": False, + "create_portable_ignored_zip": True, + }: + raise Step2CConfigError("outputs must remain ignored, private, and debug-only.") + + return ResolvedStep2CConfig( + raw=raw, + config_path=config_path, + config_sha256=_file_sha256(config_path), + resolved_config_hash=_canonical_hash(raw), + base_config_path=base_path, + base=base, + workbook_source_kind=workbook_source_kind, + workbook_relative_path=workbook_path, + workbook_expected_sha256=workbook_hash, + resolved_off_grid_exceptions=off_grid_exceptions, + objective_bounds=objective_bounds, + beta_values=beta_values, + primary_beta=primary_beta, + moment_method="analytic_identity", + score_chunk_size=score_chunk_size, + positive_hvi_threshold=positive_threshold, + numeric_tolerance=numeric_tolerance, + bound_policies=tuple(bound_policies_raw), + primary_bound_policy=primary_bound_policy, + mc_comparison_samples=mc_comparison_samples, + mc_comparison_seed=mc_comparison_seed, + primary_sobol_seed=primary_seed, + secondary_sobol_seeds=secondary_seeds, + nested_pool_sizes=nested, + refinement_anchors=anchors, + refinement_max_sweeps=sweeps, + refinement_tolerance=tolerance, + penalty_variants=penalties, + primary_penalty_variant=primary_penalty, + model_variant_names=("default_current", "conservative"), + primary_model_variant="default_current", + influence_pool_size=influence_pool_size, + influence_pool_seed=influence_pool_seed, + influence_sample_ids=influence_ids, + influence_top_k=influence_top_k, + regional_thresholds=regional_thresholds, + region_threshold=region_threshold, + region_sensitivity_thresholds=region_sensitivity_thresholds, + shortlist_min=shortlist_min, + shortlist_max=shortlist_max, + consensus_batch_size=consensus_batch_size, + consensus_nested_match_min=consensus_nested_match_min, + consensus_mean_distance_max=consensus_mean_distance_max, + consensus_family_coverage_min=consensus_family_coverage_min, + execution_modes=execution_modes, + model_seed=_integer( + reproducibility.get("model_seed"), field="reproducibility.model_seed" + ), + output_root=STEP2C_OUTPUT_ROOT, + create_portable_zip=True, + ) + + +__all__ = [ + "ExecutionModeSettings", + "PenaltyVariant", + "ResolvedStep2CConfig", + "STEP2C_BOUND_POLICIES", + "STEP2C_NESTED_POOL_SIZES", + "STEP2C_OUTPUT_ROOT", + "STEP2C_PRIVATE_SOURCE_KIND", + "STEP2C_RUNTIME_GENERATED", + "STEP2C_SCHEMA_VERSION", + "STEP2C_SYNTHETIC_SOURCE_KIND", + "Step2CConfigError", + "load_step2c_config", +] diff --git a/src/mobo_kit/d2d_step2c_robustness.py b/src/mobo_kit/d2d_step2c_robustness.py new file mode 100644 index 0000000..928c84f --- /dev/null +++ b/src/mobo_kit/d2d_step2c_robustness.py @@ -0,0 +1,3922 @@ +"""Read-only D2D Step 2C robustness, convergence, and stabilization study.""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, is_dataclass +import hashlib +import json +import os +from pathlib import Path +import platform +import shutil +import subprocess +from time import perf_counter +from typing import Any, Callable, Iterable, Mapping, Sequence +import zipfile + +import botorch +import gpytorch +import matplotlib +import numpy as np +import pandas as pd +import scipy +import sklearn +import torch +import yaml + +from .batch_comparison import RegionalBatchComparison, regional_match_batches +from .batch_selection import ( + BaseScoreResult, + LocalPenalizationConfig, + select_local_penalized_batch, +) +from .candidate_diagnostics import summarize_candidate_batch +from .candidate_pool import CandidatePool +from .d2d_campaign import ( + D2D_DEBUG_WATERMARK, + D2D_INPUT_COLUMNS, + D2D_OBJECTIVE_COLUMNS, + D2D_OBJECTIVE_NAMES, + build_d2d_objective_transform, + load_d2d_workbook_frame, + prepare_d2d_training_data, + sha256_file, +) +from .d2d_scores import validate_supplied_d2d_scores +from .d2d_step2c_config import ( + ExecutionModeSettings, + PenaltyVariant, + ResolvedStep2CConfig, + load_step2c_config, +) +from .discrete_refinement import ( + CachedGridScorer, + RefinedBatchResult, + RefinementConfig, + grid_indices_to_physical_and_normalized, + propose_refined_discrete_batch, +) +from .model_validation import ( + CONSERVATIVE, + DEFAULT_CURRENT, + ModelFitCache, + ModelValidationResult, + extract_model_hyperparameters, + fit_warnings_frame, + validate_model_variant, +) +from .observation_influence import ( + InfluenceRunInput, + ObservationInfluenceStudyResult, + run_observation_influence_study, +) +from .robust_regions import ( + ConsensusCriteriaResult, + RobustRegionResult, + cluster_candidate_regions, + evaluate_consensus_criteria, + select_robust_shortlist, +) +from .robustness_plots import ( + plot_acquisition_quality_vs_persistence, + plot_ard_lengthscale_comparison, + plot_boundary_enrichment, + plot_bounded_utility_comparison, + plot_candidate_predictions_vs_observed_ranges, + plot_local_penalty_tradeoff, + plot_loocv_diagnostics, + plot_model_policy_region_correspondence, + plot_nested_search_convergence, + plot_observation_influence_ranking, + plot_robust_region_overview, + plot_run_region_persistence_heatmap, + plot_control_omission_candidate_region_comparison, + plot_shortlist_medoid_parallel_coordinates, + plot_shortlist_region_influence_sensitivity, +) +from .sobol_pool import NestedSobolPoolResult, build_nested_sobol_discrete_pool +from .step2c_artifacts import ( + PUBLIC_SUMMARY_ARCHIVE_ROOT, + PUBLIC_SUMMARY_CSV_FILES, + PUBLIC_SUMMARY_DEBUG_MARKER_FILE, + PUBLIC_SUMMARY_MANIFEST_FILE, + PUBLIC_SUMMARY_README_FILE, + PUBLIC_SUMMARY_SCHEMA_VERSION, + validate_step2c_artifact_bundle, + validate_step2c_public_summary_archive, +) +from .ucb_hvi import ( + PosteriorIdentityMoments, + PosteriorUtilityMoments, + UCBHVIScoreResult, + posterior_identity_moments, + posterior_utility_moments, + score_ucb_hvi_from_moments, +) + + +STEP2C_METHOD_VERSION = "d2d-step2c-robustness-v1" +STEP2C_BATCH_SIZE = 5 + + +@dataclass(frozen=True) +class StudyBatch: + """One five-condition result in the one-factor-at-a-time registry.""" + + run_id: str + run_family: str + core_run: bool + grid_indices: np.ndarray + X_phys: np.ndarray + X_norm: np.ndarray + base_scores: np.ndarray + penalized_scores: np.ndarray + model_variant: str + pool_seed: int + pool_size: int + pool_hash: str + beta: float + bound_policy: str + penalty_label: str + refinement_enabled: bool + refinement_runtime_seconds: float + proposal_runtime_seconds: float + scoring: UCBHVIScoreResult | None = None + refinement: RefinedBatchResult | None = None + + def __post_init__(self) -> None: + grid = np.asarray(self.grid_indices) + physical = np.asarray(self.X_phys, dtype=float) + normalized = np.asarray(self.X_norm, dtype=float) + base = np.asarray(self.base_scores, dtype=float) + penalized = np.asarray(self.penalized_scores, dtype=float) + if grid.shape != physical.shape or physical.shape != normalized.shape: + raise ValueError("StudyBatch coordinate arrays must align.") + if grid.shape != (STEP2C_BATCH_SIZE, len(D2D_INPUT_COLUMNS)): + raise ValueError("Every Step 2C study batch must contain five D2D rows.") + if not np.issubdtype(grid.dtype, np.integer): + raise ValueError("StudyBatch grid indices must be integers.") + if base.shape != (STEP2C_BATCH_SIZE,) or penalized.shape != base.shape: + raise ValueError("StudyBatch acquisition arrays must contain five values.") + if not ( + np.all(np.isfinite(physical)) + and np.all(np.isfinite(normalized)) + and np.all(np.isfinite(base)) + and np.all(np.isfinite(penalized)) + ): + raise ValueError("StudyBatch arrays must be finite.") + if np.any(base <= 0) or np.unique(grid, axis=0).shape[0] != STEP2C_BATCH_SIZE: + raise ValueError("StudyBatch rows must be unique with positive HVI.") + if np.any(normalized < 0) or np.any(normalized > 1): + raise ValueError("StudyBatch normalized rows must lie in [0, 1].") + if ( + not np.isfinite(self.refinement_runtime_seconds) + or self.refinement_runtime_seconds < 0.0 + ): + raise ValueError( + "refinement_runtime_seconds must be finite and non-negative." + ) + if ( + not np.isfinite(self.proposal_runtime_seconds) + or self.proposal_runtime_seconds < 0.0 + ): + raise ValueError( + "proposal_runtime_seconds must be finite and non-negative." + ) + for name, value in (("run_id", self.run_id), ("run_family", self.run_family)): + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must be a nonblank string.") + + +@dataclass(frozen=True) +class Step2CRobustnessResult: + output_dir: Path + mode: str + run_manifest: dict[str, Any] + consensus: ConsensusCriteriaResult + robust_regions: pd.DataFrame + shortlist: pd.DataFrame + influence_summary: pd.DataFrame + artifact_hashes: dict[str, str] + + +def _jsonable(value: Any) -> Any: + if is_dataclass(value): + return _jsonable(asdict(value)) + if isinstance(value, Path): + return str(value) + if isinstance(value, np.ndarray): + return value.tolist() + if isinstance(value, torch.Tensor): + return value.detach().cpu().tolist() + if isinstance(value, np.generic): + return value.item() + if isinstance(value, Mapping): + return {str(key): _jsonable(item) for key, item in value.items()} + if isinstance(value, (tuple, list, set)): + return [_jsonable(item) for item in value] + return value + + +def _canonical_json_sha256(value: Any) -> str: + payload = json.dumps( + _jsonable(value), sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest().upper() + + +def _git_state(repository_root: Path) -> tuple[str, bool, tuple[str, ...]]: + try: + commit = subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=repository_root, + check=True, + capture_output=True, + text=True, + timeout=15, + ).stdout.strip() + status_text = subprocess.run( + ["git", "status", "--short"], + cwd=repository_root, + check=True, + capture_output=True, + text=True, + timeout=15, + ).stdout + except (OSError, subprocess.SubprocessError) as exc: + raise RuntimeError("Step 2C requires readable Git provenance.") from exc + status = tuple(line for line in status_text.splitlines() if line.strip()) + return commit, bool(status), status + + +def _hardware_summary() -> dict[str, Any]: + return { + "platform": platform.platform(), + "processor": platform.processor(), + "logical_cpu_count": os.cpu_count(), + "torch_threads": torch.get_num_threads(), + "cuda_available": torch.cuda.is_available(), + "cuda_device_count": torch.cuda.device_count(), + } + + +def _runtime_versions() -> dict[str, str]: + return { + "python": platform.python_version(), + "numpy": np.__version__, + "pandas": pd.__version__, + "scipy": scipy.__version__, + "scikit_learn": sklearn.__version__, + "matplotlib": matplotlib.__version__, + "torch": torch.__version__, + "botorch": botorch.__version__, + "gpytorch": gpytorch.__version__, + } + + +def _validate_output_destination( + destination: Path, config: ResolvedStep2CConfig +) -> Path: + repository_root = Path(__file__).resolve().parents[2] + allowed_root = (repository_root / config.output_root).resolve() + resolved = destination.resolve() + if resolved == repository_root or repository_root not in resolved.parents: + raise ValueError("Step 2C output must remain inside the repository.") + if resolved == allowed_root: + raise ValueError("Choose a run directory below the Step 2C output root.") + if allowed_root not in resolved.parents: + raise ValueError( + f"Step 2C output must remain below the ignored root {allowed_root}." + ) + return resolved + + +def _stamp_frame(frame: pd.DataFrame) -> pd.DataFrame: + result = frame.copy() + result["debug_only"] = True + result["approved_for_experiment"] = False + result["approved_for_production"] = False + result["candidate_status"] = D2D_DEBUG_WATERMARK + return result + + +def _declared_bound_flags( + values: np.ndarray, + bounds: tuple[float | None, float | None], +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Return lower, upper, and combined declared-support flags.""" + array = np.asarray(values, dtype=float) + lower, upper = bounds + below = np.zeros(array.shape, dtype=bool) + above = np.zeros(array.shape, dtype=bool) + if lower is not None: + below = array < lower + if upper is not None: + above = array > upper + return below, above, below | above + + +def _frame_from_dataclasses( + rows: Iterable[Any], *, columns: Sequence[str] | None = None +) -> pd.DataFrame: + values = [asdict(row) if is_dataclass(row) else dict(row) for row in rows] + return pd.DataFrame(values, columns=columns) + + +def _safe_write_json(path: Path, payload: Mapping[str, Any]) -> None: + stamped = dict(payload) + stamped.update( + { + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": D2D_DEBUG_WATERMARK, + } + ) + path.write_text( + json.dumps(_jsonable(stamped), indent=2, sort_keys=True), encoding="utf-8" + ) + + +def _pairwise_minimum(X_norm: np.ndarray) -> float: + matrix = np.asarray(X_norm, dtype=float) + if matrix.shape[0] < 2: + return np.nan + distances = np.linalg.norm(matrix[:, None, :] - matrix[None, :, :], axis=-1) + return float(distances[np.triu_indices(matrix.shape[0], k=1)].min()) + + +def _pool_with_rows( + pool: CandidatePool, extra_grid_indices: np.ndarray, design +) -> CandidatePool: + extras = np.asarray(extra_grid_indices, dtype=np.int64) + combined = np.vstack([pool.grid_indices, extras]) + _, first = np.unique(combined, axis=0, return_index=True) + ordered = combined[np.sort(first)] + physical, normalized = grid_indices_to_physical_and_normalized(ordered, design) + return CandidatePool( + grid_indices=ordered, + X_phys=physical, + X_norm=normalized, + seed=pool.seed, + draws=pool.draws, + rejected_duplicate=pool.rejected_duplicate, + rejected_avoid=pool.rejected_avoid, + rejected_constraint=pool.rejected_constraint, + ) + + +def _analytic_numpy_moments( + model: Any, + X_norm: np.ndarray, + *, + objective_transform: Any, + chunk_size: int, +) -> tuple[np.ndarray, np.ndarray, PosteriorIdentityMoments, float]: + started = perf_counter() + result = posterior_identity_moments( + model, + torch.as_tensor(X_norm, dtype=torch.double, device="cpu"), + objective_transform, + chunk_size=chunk_size, + observation_noise=False, + ) + if result.utility_mean.ndim != 2 or result.utility_std.ndim != 2: + raise RuntimeError("Step 2C requires unbatched (N, M) analytic moments.") + return ( + result.utility_mean.detach().cpu().double().numpy(), + result.utility_std.detach().cpu().double().numpy(), + result, + perf_counter() - started, + ) + + +def _score_moments( + utility_mean: np.ndarray, + utility_std: np.ndarray, + observed_y: np.ndarray, + config: ResolvedStep2CConfig, + *, + beta: float, + bound_policy: str, +) -> tuple[UCBHVIScoreResult, float]: + started = perf_counter() + result = score_ucb_hvi_from_moments( + utility_mean, + utility_std, + observed_y, + config.reference_point_utility, + beta=beta, + numeric_tolerance=config.numeric_tolerance, + chunk_size=2048, + log_epsilon=1.0e-300, + mc_samples=None, + seed=None, + observation_noise=False, + objective_contract_version=build_d2d_objective_transform().version, + moment_method="analytic_identity", + bound_policy=bound_policy, + utility_bounds=config.objective_bounds, + ) + return result, perf_counter() - started + + +def _grid_score_function( + model: Any, + design: Any, + observed_y: np.ndarray, + config: ResolvedStep2CConfig, + *, + beta: float, + bound_policy: str, +): + transform = build_d2d_objective_transform() + + def score(grid_indices: np.ndarray) -> np.ndarray: + _, X_norm = grid_indices_to_physical_and_normalized(grid_indices, design) + means, stds, _, _ = _analytic_numpy_moments( + model, + X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + scoring, _ = _score_moments( + means, + stds, + observed_y, + config, + beta=beta, + bound_policy=bound_policy, + ) + return scoring.base_score + + return score + + +def _static_select( + pool: CandidatePool, + base_scores: np.ndarray, + *, + q: int, + penalty: PenaltyVariant, + observed_norm: np.ndarray, +) -> Any: + scores = np.asarray(base_scores, dtype=float) + if scores.shape != (pool.size,) or not np.all(np.isfinite(scores)): + raise ValueError("base_scores must be finite and align with the pool.") + if np.count_nonzero(scores > 0.0) < q: + raise RuntimeError("Fewer than q candidates have positive UCB-HVI.") + log_scores = np.full(scores.shape, -np.inf, dtype=float) + positive = scores > 0.0 + log_scores[positive] = np.log(scores[positive]) + + def score_remaining( + remaining_indices: np.ndarray, selected_indices: np.ndarray + ) -> BaseScoreResult: + del selected_indices + return BaseScoreResult( + base_log_score=log_scores[remaining_indices], + base_score=scores[remaining_indices], + ) + + return select_local_penalized_batch( + pool, + q, + score_remaining, + LocalPenalizationConfig( + radius=penalty.radius, + min_batch_distance=penalty.min_batch_distance, + min_observed_distance=0.0, + dimension_weights=None, + ), + observed_pending_norm=observed_norm, + ) + + +def _penalty_by_label(config: ResolvedStep2CConfig, label: str) -> PenaltyVariant: + for variant in config.penalty_variants: + if variant.label == label: + return variant + raise KeyError(f"Unknown penalty variant {label!r}.") + + +def _study_batch_from_refined( + *, + run_id: str, + run_family: str, + core_run: bool, + refined: RefinedBatchResult, + model_variant: str, + pool: CandidatePool, + pool_hash: str, + beta: float, + bound_policy: str, + penalty_label: str, + scoring: UCBHVIScoreResult | None, + refinement_runtime_seconds: float, +) -> StudyBatch: + return StudyBatch( + run_id=run_id, + run_family=run_family, + core_run=core_run, + grid_indices=refined.grid_indices.copy(), + X_phys=refined.X_phys.copy(), + X_norm=refined.X_norm.copy(), + base_scores=refined.base_scores.copy(), + penalized_scores=refined.penalized_scores_at_selection.copy(), + model_variant=model_variant, + pool_seed=pool.seed, + pool_size=pool.size, + pool_hash=pool_hash, + beta=float(beta), + bound_policy=bound_policy, + penalty_label=penalty_label, + refinement_enabled=True, + refinement_runtime_seconds=refinement_runtime_seconds, + proposal_runtime_seconds=refinement_runtime_seconds, + scoring=scoring, + refinement=refined, + ) + + +def _study_batch_from_static( + *, + run_id: str, + run_family: str, + core_run: bool, + selection: Any, + model_variant: str, + pool: CandidatePool, + pool_hash: str, + beta: float, + bound_policy: str, + penalty_label: str, + scoring: UCBHVIScoreResult | None, + selection_runtime_seconds: float = 0.0, +) -> StudyBatch: + return StudyBatch( + run_id=run_id, + run_family=run_family, + core_run=core_run, + grid_indices=pool.grid_indices[selection.selected_pool_indices].copy(), + X_phys=selection.X_phys.copy(), + X_norm=selection.X_norm.copy(), + base_scores=np.asarray( + [step.base_score for step in selection.steps], dtype=float + ), + penalized_scores=np.exp( + np.asarray([step.penalized_log_score for step in selection.steps]) + ), + model_variant=model_variant, + pool_seed=pool.seed, + pool_size=pool.size, + pool_hash=pool_hash, + beta=float(beta), + bound_policy=bound_policy, + penalty_label=penalty_label, + refinement_enabled=False, + refinement_runtime_seconds=0.0, + proposal_runtime_seconds=selection_runtime_seconds, + scoring=scoring, + refinement=None, + ) + + +def _run_refined_batch( + *, + run_id: str, + run_family: str, + core_run: bool, + pool: CandidatePool, + pool_hash: str, + master_base_scores: np.ndarray, + shared_grid_scorer: CachedGridScorer, + config: ResolvedStep2CConfig, + mode_settings: ExecutionModeSettings, + training: Any, + model_variant: str, + beta: float, + bound_policy: str, + penalty_label: str, + scoring: UCBHVIScoreResult | None, +) -> StudyBatch: + penalty = _penalty_by_label(config, penalty_label) + refinement_started = perf_counter() + refined = propose_refined_discrete_batch( + pool, + config.design, + shared_grid_scorer, + q=STEP2C_BATCH_SIZE, + config=RefinementConfig( + anchors_per_selection_step=mode_settings.anchors_per_selection_step, + max_sweeps=config.refinement_max_sweeps, + improvement_tolerance=config.refinement_tolerance, + radius=penalty.radius, + min_batch_distance=penalty.min_batch_distance, + min_observed_distance=0.0, + dimension_weights=None, + ), + master_base_scores=master_base_scores, + observed_grid_indices=training.on_grid_grid_indices, + observed_norm=training.X_norm_all, + positive_score_threshold=config.positive_hvi_threshold, + ) + refinement_runtime = perf_counter() - refinement_started + return _study_batch_from_refined( + run_id=run_id, + run_family=run_family, + core_run=core_run, + refined=refined, + model_variant=model_variant, + pool=pool, + pool_hash=pool_hash, + beta=beta, + bound_policy=bound_policy, + penalty_label=penalty_label, + scoring=scoring, + refinement_runtime_seconds=refinement_runtime, + ) + + +def _comparison_row( + reference: StudyBatch, + comparison: StudyBatch, + *, + comparison_type: str, +) -> dict[str, Any]: + match = regional_match_batches(reference.X_norm, comparison.X_norm) + reference_sum = float(reference.base_scores.sum()) + comparison_sum = float(comparison.base_scores.sum()) + reference_boundary_count = int( + np.count_nonzero( + np.isclose(reference.X_norm, 0.0) | np.isclose(reference.X_norm, 1.0) + ) + ) + comparison_boundary_count = int( + np.count_nonzero( + np.isclose(comparison.X_norm, 0.0) | np.isclose(comparison.X_norm, 1.0) + ) + ) + row: dict[str, Any] = { + "comparison_type": comparison_type, + "reference_run_id": reference.run_id, + "comparison_run_id": comparison.run_id, + "reference_pool_size": reference.pool_size, + "comparison_pool_size": comparison.pool_size, + "exact_overlap_count": match.exact_overlap_count, + "jaccard_overlap": match.jaccard_overlap, + "mean_matched_distance": match.mean_matched_distance, + "maximum_matched_distance": match.maximum_matched_distance, + "symmetric_chamfer_distance": match.symmetric_chamfer_distance, + "hausdorff_distance": match.hausdorff_distance, + "reference_acquisition_sum": reference_sum, + "comparison_acquisition_sum": comparison_sum, + "acquisition_regret_vs_reference": reference_sum - comparison_sum, + "relative_acquisition_regret_vs_reference": ( + (reference_sum - comparison_sum) / reference_sum + if reference_sum > 0.0 + else np.nan + ), + "reference_boundary_coordinate_count": reference_boundary_count, + "comparison_boundary_coordinate_count": comparison_boundary_count, + "boundary_coordinate_count_difference": ( + comparison_boundary_count - reference_boundary_count + ), + } + for threshold, count in match.regional_match_counts.items(): + row[f"regional_matches_within_{threshold:.2f}"] = count + row["matched_pair_distances"] = "|".join( + f"{value:.12g}" for value in match.matched_pair_distances + ) + return row + + +def _study_summary(batches: Sequence[StudyBatch]) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + for batch in batches: + pairwise = np.linalg.norm( + batch.X_norm[:, None, :] - batch.X_norm[None, :, :], axis=-1 + ) + triangle = pairwise[np.triu_indices(STEP2C_BATCH_SIZE, k=1)] + rows.append( + { + "run_id": batch.run_id, + "run_family": batch.run_family, + "core_run": batch.core_run, + "model_variant": batch.model_variant, + "pool_seed": batch.pool_seed, + "pool_size": batch.pool_size, + "pool_hash": batch.pool_hash, + "beta": batch.beta, + "bound_policy": batch.bound_policy, + "penalty_label": batch.penalty_label, + "refinement_enabled": batch.refinement_enabled, + "refinement_runtime_seconds": batch.refinement_runtime_seconds, + "proposal_runtime_seconds": batch.proposal_runtime_seconds, + "acquisition_sum": float(batch.base_scores.sum()), + "acquisition_mean": float(batch.base_scores.mean()), + "minimum_within_batch_distance": float(triangle.min()), + "mean_within_batch_distance": float(triangle.mean()), + "maximum_within_batch_distance": float(triangle.max()), + "mean_penalty_factor": float( + np.mean(batch.penalized_scores / batch.base_scores) + ), + "distinct_refined_optima": ( + batch.refinement.distinct_converged_optima + if batch.refinement is not None + else np.nan + ), + } + ) + return _stamp_frame(pd.DataFrame(rows)) + + +def _batch_candidate_rows( + batches: Sequence[StudyBatch], + *, + models: Mapping[str, Any], + config: ResolvedStep2CConfig, + training: Any, +) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + transform = build_d2d_objective_transform() + control_mask = np.isin(training.sample_ids, config.control_sample_ids) + control_norm = training.X_norm_all[control_mask] + for batch in batches: + model = models[batch.model_variant] + means, stds, _, _ = _analytic_numpy_moments( + model, + batch.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + selected_scoring, _ = _score_moments( + means, + stds, + training.Y_objectives, + config, + beta=batch.beta, + bound_policy=batch.bound_policy, + ) + if not np.allclose( + selected_scoring.base_score, + batch.base_scores, + rtol=1.0e-9, + atol=1.0e-12, + ): + raise RuntimeError( + f"Selected score recomputation changed for run {batch.run_id}." + ) + diagnostics = summarize_candidate_batch( + batch.X_norm, + observed_pending_norm=training.X_norm_all, + X_phys=batch.X_phys, + design=config.design, + metadata={"debug_only": True, "run_id": batch.run_id}, + ) + control_distances = np.linalg.norm( + batch.X_norm[:, None, :] - control_norm[None, :, :], axis=-1 + ).min(axis=1) + minimum_distance = _pairwise_minimum(batch.X_norm) + required_distance = _penalty_by_label( + config, batch.penalty_label + ).min_batch_distance + for index in range(STEP2C_BATCH_SIZE): + row: dict[str, Any] = { + "run_id": batch.run_id, + "run_family": batch.run_family, + "core_run": batch.core_run, + "candidate_id": f"{batch.run_id}-C{index + 1:02d}", + "selection_order": index + 1, + "model_variant": batch.model_variant, + "pool_seed": batch.pool_seed, + "pool_size": batch.pool_size, + "pool_hash": batch.pool_hash, + "beta": batch.beta, + "bound_policy": batch.bound_policy, + "penalty_label": batch.penalty_label, + "refinement_enabled": batch.refinement_enabled, + "acquisition_score": float(batch.base_scores[index]), + "penalized_acquisition_score": float(batch.penalized_scores[index]), + "penalty_factor": float( + batch.penalized_scores[index] / batch.base_scores[index] + ), + "nearest_observed_distance": float( + diagnostics.nearest_observed_pending_distance[index] + ), + "nearest_control_distance": float(control_distances[index]), + "minimum_within_batch_distance": minimum_distance, + "hard_distance_required": required_distance, + "hard_distance_valid": bool( + minimum_distance + 1.0e-12 >= required_distance + ), + "grid_valid": bool(diagnostics.grid_valid_rows[index]), + "bounds_valid": bool( + np.all(batch.X_norm[index] >= 0.0) + and np.all(batch.X_norm[index] <= 1.0) + and np.all(np.isfinite(batch.X_phys[index])) + ), + "boundary_coordinate_count": int( + np.count_nonzero(diagnostics.boundary_flags[index]) + ), + "boundary_dimensions": "|".join( + name + for name, flag in zip( + D2D_INPUT_COLUMNS, diagnostics.boundary_flags[index] + ) + if flag + ), + "lower_boundary_coordinate_count": int( + np.count_nonzero(np.isclose(batch.X_norm[index], 0.0)) + ), + "upper_boundary_coordinate_count": int( + np.count_nonzero(np.isclose(batch.X_norm[index], 1.0)) + ), + "lower_boundary_dimensions": "|".join( + name + for name, value in zip(D2D_INPUT_COLUMNS, batch.X_norm[index]) + if np.isclose(value, 0.0) + ), + "upper_boundary_dimensions": "|".join( + name + for name, value in zip(D2D_INPUT_COLUMNS, batch.X_norm[index]) + if np.isclose(value, 1.0) + ), + } + for dimension, name in enumerate(D2D_INPUT_COLUMNS): + row[name] = float(batch.X_phys[index, dimension]) + row[f"phys_{dimension}"] = float(batch.X_phys[index, dimension]) + row[f"norm_{dimension}"] = float(batch.X_norm[index, dimension]) + row[f"grid_{dimension}"] = int(batch.grid_indices[index, dimension]) + for objective, objective_name in enumerate(D2D_OBJECTIVE_NAMES): + observed_minimum = float(training.Y_objectives[:, objective].min()) + observed_maximum = float(training.Y_objectives[:, objective].max()) + row[f"pred_mean_{objective}"] = float(means[index, objective]) + row[f"pred_std_{objective}"] = float(stds[index, objective]) + row[f"observed_minimum_{objective}"] = observed_minimum + row[f"observed_maximum_{objective}"] = observed_maximum + row[f"pred_mean_below_observed_{objective}"] = bool( + means[index, objective] < observed_minimum + ) + row[f"pred_mean_above_observed_{objective}"] = bool( + means[index, objective] > observed_maximum + ) + lower, upper = config.objective_bounds[objective] + below, above, outside = _declared_bound_flags( + means[index, objective], config.objective_bounds[objective] + ) + row[f"pred_mean_below_declared_bounds_{objective}"] = bool(below) + row[f"pred_mean_above_declared_bounds_{objective}"] = bool(above) + row[f"pred_mean_outside_declared_bounds_{objective}"] = bool(outside) + row[f"ucb_raw_{objective}"] = float( + selected_scoring.utility_ucb_raw[index, objective] + ) + row[f"ucb_effective_{objective}"] = float( + selected_scoring.utility_ucb_effective[index, objective] + ) + row[f"ucb_clip_amount_{objective}"] = float( + selected_scoring.utility_ucb_clip_amount[index, objective] + ) + row[f"ucb_effective_outside_declared_bounds_{objective}"] = bool( + ( + lower is not None + and selected_scoring.utility_ucb_effective[index, objective] + < lower + ) + or ( + upper is not None + and selected_scoring.utility_ucb_effective[index, objective] + > upper + ) + ) + row[f"objective_name_{objective}"] = objective_name + rows.append(row) + return _stamp_frame(pd.DataFrame(rows)) + + +def _refinement_trace_frame(batches: Sequence[StudyBatch]) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + for batch in batches: + if batch.refinement is None: + continue + for trace in batch.refinement.trace: + row = asdict(trace) + row.update( + { + "record_type": "coordinate_move", + "run_id": batch.run_id, + "run_family": batch.run_family, + "pool_size": batch.pool_size, + "pool_seed": batch.pool_seed, + "model_variant": batch.model_variant, + "beta": batch.beta, + "bound_policy": batch.bound_policy, + "penalty_label": batch.penalty_label, + "start_grid_index": "|".join( + str(value) for value in trace.start_grid_index + ), + "chosen_grid_index": "|".join( + str(value) for value in trace.chosen_grid_index + ), + } + ) + rows.append(row) + for anchor in batch.refinement.anchors: + row = asdict(anchor) + row.update( + { + "record_type": "anchor_summary", + "run_id": batch.run_id, + "run_family": batch.run_family, + "pool_size": batch.pool_size, + "pool_seed": batch.pool_seed, + "model_variant": batch.model_variant, + "beta": batch.beta, + "bound_policy": batch.bound_policy, + "penalty_label": batch.penalty_label, + "start_grid_index": "|".join( + str(value) for value in anchor.anchor_grid_index + ), + "chosen_grid_index": "|".join( + str(value) for value in anchor.refined_grid_index + ), + "changed_dimensions": "|".join(anchor.changed_dimensions), + "total_coordinate_moves": anchor.accepted_move_count, + "total_sweeps": anchor.sweeps, + "end_base_score": anchor.base_score, + "end_penalized_score": anchor.penalized_score, + } + ) + rows.append(row) + return _stamp_frame(pd.DataFrame(rows)) + + +def _nested_pool_manifest( + results: Sequence[NestedSobolPoolResult], *, role_by_seed: Mapping[int, str] +) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + for result in results: + for size in result.accepted_sizes: + pool = result.pools[size] + rows.append( + { + "pool_role": role_by_seed[result.scramble_seed], + "scramble_seed": result.scramble_seed, + "accepted_size": size, + "prefix_sha256": result.prefix_hashes[size], + "draws_at_prefix": pool.draws, + "duplicate_rejections_at_prefix": pool.rejected_duplicate, + "avoid_rejections_at_prefix": pool.rejected_avoid, + "constraint_rejections_at_prefix": pool.rejected_constraint, + "final_raw_sobol_draws": result.raw_sobol_draws, + "ignored_off_grid_observed_rows": result.ignored_off_grid_observed, + "scipy_version": result.scipy_version, + "exact_accepted_prefix": True, + "full_cartesian_grid_materialized": False, + } + ) + return _stamp_frame(pd.DataFrame(rows)) + + +def _nested_convergence_table( + nested_batches: Sequence[StudyBatch], + raw_pool_batches: Sequence[StudyBatch], +) -> pd.DataFrame: + ordered = sorted(nested_batches, key=lambda batch: batch.pool_size) + raw_by_size = {batch.pool_size: batch for batch in raw_pool_batches} + if set(raw_by_size) != {batch.pool_size for batch in ordered}: + raise RuntimeError("Raw and refined nested-pool batches must align by size.") + largest = ordered[-1] + rows: list[dict[str, Any]] = [] + for index, batch in enumerate(ordered): + reference = largest + row = _comparison_row( + reference, + batch, + comparison_type="prefix_vs_largest", + ) + row["prefix_order"] = index + 1 + row["is_largest_prefix"] = batch is largest + raw_batch = raw_by_size[batch.pool_size] + raw_comparison = _comparison_row( + raw_batch, + batch, + comparison_type="raw_pool_vs_refined", + ) + row.update( + { + "raw_pool_acquisition_sum": float(raw_batch.base_scores.sum()), + "raw_pool_first_acquisition": float(raw_batch.base_scores[0]), + "raw_pool_median_acquisition": float(np.median(raw_batch.base_scores)), + "refined_acquisition_sum": float(batch.base_scores.sum()), + "refined_first_acquisition": float(batch.base_scores[0]), + "refined_median_acquisition": float(np.median(batch.base_scores)), + "refinement_acquisition_gain": float( + batch.base_scores.sum() - raw_batch.base_scores.sum() + ), + "refinement_runtime_seconds": batch.refinement_runtime_seconds, + "raw_vs_refined_exact_overlap_count": raw_comparison[ + "exact_overlap_count" + ], + "raw_vs_refined_mean_matched_distance": raw_comparison[ + "mean_matched_distance" + ], + "raw_vs_refined_maximum_matched_distance": raw_comparison[ + "maximum_matched_distance" + ], + "raw_pool_boundary_coordinate_count": raw_comparison[ + "reference_boundary_coordinate_count" + ], + "refined_boundary_coordinate_count": raw_comparison[ + "comparison_boundary_coordinate_count" + ], + } + ) + if index > 0: + adjacent = _comparison_row( + ordered[index - 1], batch, comparison_type="adjacent_prefixes" + ) + for key, value in adjacent.items(): + if key not in { + "comparison_type", + "reference_run_id", + "comparison_run_id", + }: + row[f"adjacent_{key}"] = value + row["adjacent_reference_run_id"] = ordered[index - 1].run_id + rows.append(row) + return _stamp_frame(pd.DataFrame(rows)) + + +def _sobol_scramble_table( + baseline: StudyBatch, secondary_batches: Sequence[StudyBatch] +) -> pd.DataFrame: + return _stamp_frame( + pd.DataFrame( + [ + _comparison_row( + baseline, batch, comparison_type="secondary_sobol_scramble" + ) + for batch in secondary_batches + ] + ) + ) + + +def _bounded_utility_table( + clip_batch: StudyBatch, + none_batch: StudyBatch, + clip_scoring: UCBHVIScoreResult, + selected_candidate_rows: pd.DataFrame, +) -> pd.DataFrame: + row = _comparison_row( + clip_batch, none_batch, comparison_type="clip_ucb_vs_unbounded" + ) + clip_amount = np.asarray(clip_scoring.utility_ucb_clip_amount, dtype=float) + selected = selected_candidate_rows[ + selected_candidate_rows["run_id"].eq(clip_batch.run_id) + ].sort_values("selection_order") + selected_clip_columns = [ + f"ucb_clip_amount_{index}" for index in range(len(D2D_OBJECTIVE_NAMES)) + ] + if selected.shape[0] != STEP2C_BATCH_SIZE or any( + column not in selected for column in selected_clip_columns + ): + raise RuntimeError( + "Bounded-utility selected-candidate diagnostics are incomplete." + ) + selected_clip = selected.loc[:, selected_clip_columns].to_numpy(dtype=float) + row.update( + { + "bounded_reference_run": clip_batch.run_id, + "unbounded_comparison_run": none_batch.run_id, + "clipped_pool_coordinate_count": int(np.count_nonzero(clip_amount > 0)), + "clipped_pool_row_count": int( + np.count_nonzero(np.any(clip_amount > 0, axis=1)) + ), + "maximum_pool_clip_amount": float(clip_amount.max()), + "mean_positive_pool_clip_amount": ( + float(clip_amount[clip_amount > 0].mean()) + if np.any(clip_amount > 0) + else 0.0 + ), + "uniformity_pool_clipped_count": int( + np.count_nonzero(clip_amount[:, 0] > 0) + ), + "optoelectronic_pool_clipped_count": int( + np.count_nonzero(clip_amount[:, 1] > 0) + ), + "thickness_pool_clipped_count": int( + np.count_nonzero(clip_amount[:, 2] > 0) + ), + "clipped_selected_coordinate_count": int( + np.count_nonzero(selected_clip > 0) + ), + "clipped_selected_candidate_count": int( + np.count_nonzero(np.any(selected_clip > 0, axis=1)) + ), + "maximum_selected_clip_amount": float(selected_clip.max()), + "training_targets_mutated": False, + } + ) + return _stamp_frame(pd.DataFrame([row])) + + +def _penalty_tradeoff_table( + batches: Sequence[StudyBatch], + *, + observed_norm: np.ndarray, + reference_label: str = "no_soft_no_hard", +) -> pd.DataFrame: + by_label = {batch.penalty_label: batch for batch in batches} + no_hard_reference = by_label[reference_label] + fixed_hard_reference = by_label["no_soft_hard_0_15"] + rows: list[dict[str, Any]] = [] + for batch in batches: + reference = ( + no_hard_reference + if batch.penalty_label in {reference_label, "no_soft_hard_0_15"} + else fixed_hard_reference + ) + row = _comparison_row( + reference, batch, comparison_type="local_penalty_isolation" + ) + pairwise = np.linalg.norm( + batch.X_norm[:, None, :] - batch.X_norm[None, :, :], axis=-1 + ) + triangle = pairwise[np.triu_indices(STEP2C_BATCH_SIZE, k=1)] + reference_pairwise = np.linalg.norm( + reference.X_norm[:, None, :] - reference.X_norm[None, :, :], axis=-1 + ) + reference_triangle = reference_pairwise[np.triu_indices(STEP2C_BATCH_SIZE, k=1)] + factors = batch.penalized_scores / batch.base_scores + acquisition_sacrifice = float( + reference.base_scores.sum() - batch.base_scores.sum() + ) + nearest_observed = np.linalg.norm( + batch.X_norm[:, None, :] - observed_norm[None, :, :], axis=-1 + ).min(axis=1) + boundary_count = int( + np.count_nonzero( + np.isclose(batch.X_norm, 0.0) | np.isclose(batch.X_norm, 1.0) + ) + ) + minimum_distance_change = float(triangle.min() - reference_triangle.min()) + mean_distance_change = float(triangle.mean() - reference_triangle.mean()) + regional_matches = int(row["regional_matches_within_0.15"]) + materially_changes_diversity = bool( + regional_matches <= 3 + or abs(minimum_distance_change) >= 0.05 + or abs(mean_distance_change) >= 0.05 + ) + modestly_changes_diversity = bool( + materially_changes_diversity + or regional_matches < STEP2C_BATCH_SIZE + or abs(minimum_distance_change) >= 0.01 + or abs(mean_distance_change) >= 0.01 + ) + if materially_changes_diversity: + activity = "active and materially changes diversity" + elif modestly_changes_diversity: + activity = "active but only modestly changes diversity" + else: + activity = ( + "implemented but effectively inactive because base optima " + "are already separated" + ) + row.update( + { + "penalty_label": batch.penalty_label, + "comparison_reference_penalty_label": reference.penalty_label, + "soft_radius": ( + np.nan + if batch.penalty_label.startswith("no_soft") + else float(".".join(batch.penalty_label.split("_")[-2:])) + ), + "minimum_within_batch_distance": float(triangle.min()), + "mean_within_batch_distance": float(triangle.mean()), + "maximum_within_batch_distance": float(triangle.max()), + "minimum_within_batch_distance_change_vs_reference": ( + minimum_distance_change + ), + "mean_within_batch_distance_change_vs_reference": ( + mean_distance_change + ), + "acquisition_sum": float(batch.base_scores.sum()), + "acquisition_median": float(np.median(batch.base_scores)), + "acquisition_minimum": float(batch.base_scores.min()), + "acquisition_sacrifice": acquisition_sacrifice, + "relative_acquisition_sacrifice": ( + acquisition_sacrifice / float(reference.base_scores.sum()) + ), + "minimum_penalty_factor": float(factors.min()), + "mean_penalty_factor": float(factors.mean()), + "maximum_penalty_factor": float(factors.max()), + "minimum_nearest_observed_distance": float(nearest_observed.min()), + "mean_nearest_observed_distance": float(nearest_observed.mean()), + "boundary_coordinate_count": boundary_count, + "proposal_runtime_seconds": batch.proposal_runtime_seconds, + "candidate_region_changed_vs_reference": bool( + regional_matches < STEP2C_BATCH_SIZE + ), + "material_diversity_distance_change_threshold": 0.05, + "modest_diversity_distance_change_threshold": 0.01, + "material_diversity_maximum_regional_matches": 3, + "materially_changes_diversity": materially_changes_diversity, + "modestly_changes_diversity": modestly_changes_diversity, + "penalty_activity_classification": activity, + "hard_distance_relaxed": False, + } + ) + rows.append(row) + return _stamp_frame(pd.DataFrame(rows)) + + +def _beta_robustness_table( + beta_four: StudyBatch, beta_variants: Sequence[StudyBatch] +) -> pd.DataFrame: + rows = [ + _comparison_row(beta_four, batch, comparison_type="beta_robustness") + | {"beta": batch.beta} + for batch in (beta_four, *beta_variants) + ] + return _stamp_frame(pd.DataFrame(rows)) + + +def _boundary_enrichment_table( + pool: CandidatePool, + base_scores: np.ndarray, + selected: StudyBatch, + comparison_batches: Sequence[StudyBatch], +) -> pd.DataFrame: + scores = np.asarray(base_scores, dtype=float) + top_count = max(1, int(np.ceil(pool.size * 0.01))) + top_indices = np.argsort(-scores, kind="stable")[:top_count] + groups: list[tuple[str, np.ndarray, StudyBatch | None]] = [ + ("pool", np.asarray(pool.X_norm), None), + ( + "top_1_percent_acquisition", + np.asarray(pool.X_norm)[top_indices], + None, + ), + ("selected_batch", selected.X_norm, selected), + *[ + (f"selected_run:{batch.run_id}", batch.X_norm, batch) + for batch in comparison_batches + if batch.run_id != selected.run_id + ], + ] + pool_lower_rates = np.mean(np.isclose(pool.X_norm, 0.0), axis=0) + pool_upper_rates = np.mean(np.isclose(pool.X_norm, 1.0), axis=0) + pool_rates = np.mean( + np.isclose(pool.X_norm, 0.0) | np.isclose(pool.X_norm, 1.0), axis=0 + ) + rows: list[dict[str, Any]] = [] + for dimension, name in enumerate(D2D_INPUT_COLUMNS): + for group_name, X, batch in groups: + lower_flags = np.isclose(X[:, dimension], 0.0) + upper_flags = np.isclose(X[:, dimension], 1.0) + flags = lower_flags | upper_flags + rate = float(flags.mean()) + lower_rate = float(lower_flags.mean()) + upper_rate = float(upper_flags.mean()) + rows.append( + { + "dimension_index": dimension, + "input_name": name, + "group": group_name, + "group_kind": ( + "search_pool" + if batch is None and group_name == "pool" + else ("top_acquisition" if batch is None else "selected_batch") + ), + "run_id": batch.run_id if batch is not None else None, + "run_family": batch.run_family if batch is not None else None, + "model_variant": ( + batch.model_variant if batch is not None else None + ), + "beta": batch.beta if batch is not None else np.nan, + "bound_policy": (batch.bound_policy if batch is not None else None), + "row_count": int(X.shape[0]), + "boundary_count": int(flags.sum()), + "boundary_rate": rate, + "lower_boundary_count": int(lower_flags.sum()), + "lower_boundary_rate": lower_rate, + "upper_boundary_count": int(upper_flags.sum()), + "upper_boundary_rate": upper_rate, + "pool_boundary_rate": float(pool_rates[dimension]), + "pool_lower_boundary_rate": float(pool_lower_rates[dimension]), + "pool_upper_boundary_rate": float(pool_upper_rates[dimension]), + "enrichment_ratio_vs_pool": ( + rate / float(pool_rates[dimension]) + if pool_rates[dimension] > 0 + else np.nan + ), + "lower_enrichment_ratio_vs_pool": ( + lower_rate / float(pool_lower_rates[dimension]) + if pool_lower_rates[dimension] > 0 + else np.nan + ), + "upper_enrichment_ratio_vs_pool": ( + upper_rate / float(pool_upper_rates[dimension]) + if pool_upper_rates[dimension] > 0 + else np.nan + ), + } + ) + return _stamp_frame(pd.DataFrame(rows)) + + +def _model_validation_frames( + validation_results: Sequence[ModelValidationResult], +) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]: + summary_rows: list[dict[str, Any]] = [] + prediction_rows: list[dict[str, Any]] = [] + hyperparameter_rows: list[dict[str, Any]] = [] + fit_records: list[Any] = [] + for result in validation_results: + summary_rows.extend(asdict(metric) for metric in result.loocv.metrics) + prediction_rows.extend(asdict(row) for row in result.loocv.predictions) + hyperparameter_rows.extend(row.as_flat_dict() for row in result.hyperparameters) + fit_records.extend([result.full_fit, *result.loocv.fold_records.values()]) + warnings_frame = fit_warnings_frame(fit_records) + return ( + _stamp_frame(pd.DataFrame(summary_rows)), + _stamp_frame(pd.DataFrame(prediction_rows)), + _stamp_frame(pd.DataFrame(hyperparameter_rows)), + _stamp_frame(warnings_frame), + ) + + +def _augment_model_summary_with_candidate_diagnostics( + summary: pd.DataFrame, + candidate_rows: pd.DataFrame, + training_y: np.ndarray, + objective_bounds: Sequence[tuple[float | None, float | None]], +) -> pd.DataFrame: + result = summary.copy() + for objective_index, objective_name in enumerate(D2D_OBJECTIVE_NAMES): + result.loc[result["objective_index"] == objective_index, "observed_minimum"] = ( + float(training_y[:, objective_index].min()) + ) + result.loc[result["objective_index"] == objective_index, "observed_maximum"] = ( + float(training_y[:, objective_index].max()) + ) + for variant in result["variant_name"].unique(): + selected = candidate_rows[ + (candidate_rows["run_id"] == "baseline") + & (candidate_rows["model_variant"] == variant) + ] + if selected.empty: + selected = candidate_rows[ + candidate_rows["model_variant"] == variant + ].head(STEP2C_BATCH_SIZE) + values = selected[f"pred_mean_{objective_index}"].to_numpy(dtype=float) + mask = (result["variant_name"] == variant) & ( + result["objective_index"] == objective_index + ) + if values.size: + result.loc[mask, "selected_prediction_minimum"] = float(values.min()) + result.loc[mask, "selected_prediction_maximum"] = float(values.max()) + result.loc[mask, "selected_prediction_outside_observed_count"] = int( + np.count_nonzero( + (values < training_y[:, objective_index].min()) + | (values > training_y[:, objective_index].max()) + ) + ) + _, _, outside_declared = _declared_bound_flags( + values, objective_bounds[objective_index] + ) + result.loc[ + mask, "selected_prediction_outside_declared_bounds_count" + ] = int(np.count_nonzero(outside_declared)) + result.loc[mask, "objective_name_contract"] = objective_name + return _stamp_frame(result) + + +def _hyperparameter_stability_summary( + hyperparameters: pd.DataFrame, +) -> pd.DataFrame: + parameter_columns = [ + "likelihood_noise", + "outputscale", + *[ + column + for column in hyperparameters.columns + if column.startswith("ard_lengthscale_") + and not column.endswith( + ( + "_near_floor", + "_very_small_normalized_domain", + "_extremely_large_flat", + ) + ) + ], + ] + rows: list[dict[str, Any]] = [] + for (variant, objective), group in hyperparameters.groupby( + ["variant_name", "objective_name"], sort=True + ): + full = group[group["omitted_sample_id"].isna()] + leave_one_out = group[group["omitted_sample_id"].notna()] + if full.shape[0] != 1 or leave_one_out.empty: + raise RuntimeError( + "Hyperparameter stability requires one full fit and LOOCV fits." + ) + for parameter in parameter_columns: + values = leave_one_out[parameter].to_numpy(dtype=float) + full_value = float(full.iloc[0][parameter]) + log_displacement = np.abs(np.log(values / full_value)) + rows.append( + { + "variant_name": variant, + "objective_name": objective, + "parameter_name": parameter, + "full_fit_value": full_value, + "loocv_minimum": float(values.min()), + "loocv_q1": float(np.quantile(values, 0.25)), + "loocv_median": float(np.median(values)), + "loocv_q3": float(np.quantile(values, 0.75)), + "loocv_maximum": float(values.max()), + "loocv_maximum_absolute_log_ratio_vs_full": float( + log_displacement.max() + ), + "loocv_fit_count": int(values.size), + } + ) + return _stamp_frame(pd.DataFrame(rows)) + + +def _analytic_mc_comparison( + *, + model: Any, + pool: CandidatePool, + training_y: np.ndarray, + observed_norm: np.ndarray, + config: ResolvedStep2CConfig, + mode_settings: ExecutionModeSettings, +) -> tuple[pd.DataFrame, dict[str, Any]]: + comparison_count = min( + pool.size, + 512 if mode_settings.nested_unique_sizes[-1] <= 4096 else 2048, + ) + indices = pool.grid_indices[:comparison_count].copy() + physical = pool.X_phys[:comparison_count].copy() + normalized = pool.X_norm[:comparison_count].copy() + comparison_pool = CandidatePool( + grid_indices=indices, + X_phys=physical, + X_norm=normalized, + seed=pool.seed, + draws=comparison_count, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + transform = build_d2d_objective_transform() + analytic_mean, analytic_std, _, analytic_runtime = _analytic_numpy_moments( + model, + normalized, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + mc_started = perf_counter() + mc: PosteriorUtilityMoments = posterior_utility_moments( + model, + torch.as_tensor(normalized, dtype=torch.double), + transform, + mc_samples=mode_settings.mc_comparison_samples, + seed=config.mc_comparison_seed, + chunk_size=min(config.score_chunk_size, 512), + observation_noise=False, + ) + mc_runtime = perf_counter() - mc_started + analytic_scoring, _ = _score_moments( + analytic_mean, + analytic_std, + training_y, + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ) + mc_scoring = score_ucb_hvi_from_moments( + mc.utility_mean, + mc.utility_std, + training_y, + config.reference_point_utility, + beta=config.primary_beta, + numeric_tolerance=config.numeric_tolerance, + chunk_size=2048, + log_epsilon=1.0e-300, + mc_samples=mc.mc_samples, + seed=mc.seed, + observation_noise=False, + objective_contract_version=transform.version, + moment_method="monte_carlo", + bound_policy=config.primary_bound_policy, + utility_bounds=config.objective_bounds, + ) + penalty = _penalty_by_label(config, config.primary_penalty_variant) + analytic_selection = _static_select( + comparison_pool, + analytic_scoring.base_score, + q=STEP2C_BATCH_SIZE, + penalty=penalty, + observed_norm=observed_norm, + ) + mc_selection = _static_select( + comparison_pool, + mc_scoring.base_score, + q=STEP2C_BATCH_SIZE, + penalty=penalty, + observed_norm=observed_norm, + ) + match = regional_match_batches(analytic_selection.X_norm, mc_selection.X_norm) + mean_delta = np.abs(analytic_mean - mc.utility_mean) + std_delta = np.abs(analytic_std - mc.utility_std) + observed_ranges = np.ptp(np.asarray(training_y, dtype=float), axis=0) + moment_difference_tolerances = np.maximum( + 0.02, 0.02 * np.maximum(observed_ranges, 1.0) + ) + maximum_moment_difference_tolerance = float(moment_difference_tolerances.max()) + minimum_regional_matches = 4 + maximum_selection_mean_distance = 0.15 + moment_check_passed = bool( + np.all(mean_delta.max(axis=0) <= moment_difference_tolerances) + and np.all(std_delta.max(axis=0) <= moment_difference_tolerances) + ) + selection_check_passed = bool( + match.regional_match_count(0.15) >= minimum_regional_matches + and match.mean_matched_distance <= maximum_selection_mean_distance + ) + row: dict[str, Any] = { + "comparison_candidate_count": comparison_count, + "mc_samples": mode_settings.mc_comparison_samples, + "mc_seed": config.mc_comparison_seed, + "analytic_runtime_seconds": analytic_runtime, + "mc_runtime_seconds": mc_runtime, + "runtime_speedup_mc_over_analytic": ( + mc_runtime / analytic_runtime if analytic_runtime > 0 else np.nan + ), + "mean_absolute_mean_difference": float(mean_delta.mean()), + "maximum_absolute_mean_difference": float(mean_delta.max()), + "mean_absolute_std_difference": float(std_delta.mean()), + "maximum_absolute_std_difference": float(std_delta.max()), + "exact_selection_overlap": match.exact_overlap_count, + "mean_selection_matched_distance": match.mean_matched_distance, + "maximum_selection_matched_distance": match.maximum_matched_distance, + "analytic_selection_indices": "|".join( + str(value) for value in analytic_selection.selected_pool_indices + ), + "mc_selection_indices": "|".join( + str(value) for value in mc_selection.selected_pool_indices + ), + "analytic_deterministic_without_mc_seed": True, + "primary_step2c_moment_method": "analytic_identity", + "maximum_moment_difference_tolerance": (maximum_moment_difference_tolerance), + "moment_difference_tolerance_policy": ( + "per objective max(0.02, 0.02 * max(observed_range, 1.0))" + ), + "moment_difference_tolerances": "|".join( + f"{value:.17g}" for value in moment_difference_tolerances + ), + "minimum_regional_matches_within_0_15": minimum_regional_matches, + "maximum_selection_mean_distance_tolerance": (maximum_selection_mean_distance), + "moment_comparison_passed": moment_check_passed, + "selection_comparison_passed": selection_check_passed, + "analytic_mc_debug_check_passed": ( + moment_check_passed and selection_check_passed + ), + } + for threshold, count in match.regional_match_counts.items(): + row[f"regional_matches_within_{threshold:.2f}"] = count + for objective, objective_name in enumerate(D2D_OBJECTIVE_NAMES): + row[f"{objective_name}_maximum_absolute_mean_difference"] = float( + mean_delta[:, objective].max() + ) + row[f"{objective_name}_maximum_absolute_std_difference"] = float( + std_delta[:, objective].max() + ) + row[f"{objective_name}_moment_difference_tolerance"] = float( + moment_difference_tolerances[objective] + ) + payload = { + "comparison_candidate_count": comparison_count, + "mc_samples": mode_settings.mc_comparison_samples, + "mean_absolute_mean_difference": row["mean_absolute_mean_difference"], + "maximum_absolute_mean_difference": row["maximum_absolute_mean_difference"], + "mean_absolute_std_difference": row["mean_absolute_std_difference"], + "maximum_absolute_std_difference": row["maximum_absolute_std_difference"], + "exact_selection_overlap": match.exact_overlap_count, + "regional_matches_within_0_15": match.regional_match_count(0.15), + "analytic_runtime_seconds": analytic_runtime, + "mc_runtime_seconds": mc_runtime, + "maximum_moment_difference_tolerance": (maximum_moment_difference_tolerance), + "moment_difference_tolerance_policy": row["moment_difference_tolerance_policy"], + "moment_difference_tolerances": { + objective_name: float(moment_difference_tolerances[objective]) + for objective, objective_name in enumerate(D2D_OBJECTIVE_NAMES) + }, + "minimum_regional_matches_within_0_15": minimum_regional_matches, + "maximum_selection_mean_distance_tolerance": (maximum_selection_mean_distance), + "moment_comparison_passed": moment_check_passed, + "selection_comparison_passed": selection_check_passed, + "analytic_mc_debug_check_passed": ( + moment_check_passed and selection_check_passed + ), + } + return _stamp_frame(pd.DataFrame([row])), payload + + +def _pareto_sample_ids(Y: np.ndarray, sample_ids: Sequence[Any]) -> tuple[Any, ...]: + from botorch.utils.multi_objective.pareto import is_non_dominated + + values = torch.as_tensor(Y, dtype=torch.double) + mask = is_non_dominated(values).detach().cpu().numpy() + ids = np.asarray(tuple(sample_ids), dtype=object) + if ids.shape != (values.shape[0],): + raise ValueError("sample_ids must align with objective rows.") + return tuple(ids[mask].tolist()) + + +def _hyperparameter_mapping(record: Any) -> dict[str, float]: + rows = extract_model_hyperparameters(record, input_names=D2D_INPUT_COLUMNS) + result: dict[str, float] = {} + for row in rows: + prefix = f"objective_{row.objective_index}" + result[f"{prefix}.noise"] = row.likelihood_noise + result[f"{prefix}.outputscale"] = row.outputscale + for input_name, value in zip(row.input_names, row.ard_lengthscales): + result[f"{prefix}.lengthscale.{input_name}"] = value + if not result or any( + not np.isfinite(value) or value <= 0 for value in result.values() + ): + raise RuntimeError("Influence hyperparameters must be finite and positive.") + return result + + +def _influence_candidate_rows( + batch: StudyBatch, + *, + omitted_sample_id: int | None, + model: Any, + model_training_y: np.ndarray, + observed_y: np.ndarray, + observed_norm: np.ndarray, + config: ResolvedStep2CConfig, +) -> list[dict[str, Any]]: + means, stds, _, _ = _analytic_numpy_moments( + model, + batch.X_norm, + objective_transform=build_d2d_objective_transform(), + chunk_size=config.score_chunk_size, + ) + scoring, _ = _score_moments( + means, + stds, + np.asarray(model_training_y, dtype=float), + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ) + observed_values = np.asarray(observed_y, dtype=float) + observed_coordinates = np.asarray(observed_norm, dtype=float) + nearest_observed = np.linalg.norm( + batch.X_norm[:, None, :] - observed_coordinates[None, :, :], axis=-1 + ).min(axis=1) + rows: list[dict[str, Any]] = [] + for index in range(STEP2C_BATCH_SIZE): + row: dict[str, Any] = { + "run_id": batch.run_id, + "omitted_sample_id": omitted_sample_id, + "candidate_id": f"{batch.run_id}-C{index + 1:02d}", + "selection_order": index + 1, + "common_pool_sha256": batch.pool_hash, + "acquisition_score": float(batch.base_scores[index]), + "penalized_acquisition_score": float(batch.penalized_scores[index]), + "nearest_observed_distance": float(nearest_observed[index]), + "lower_boundary_coordinate_count": int( + np.count_nonzero(np.isclose(batch.X_norm[index], 0.0)) + ), + "upper_boundary_coordinate_count": int( + np.count_nonzero(np.isclose(batch.X_norm[index], 1.0)) + ), + "boundary_coordinate_count": int( + np.count_nonzero( + np.isclose(batch.X_norm[index], 0.0) + | np.isclose(batch.X_norm[index], 1.0) + ) + ), + "lower_boundary_dimensions": "|".join( + name + for name, value in zip(D2D_INPUT_COLUMNS, batch.X_norm[index]) + if np.isclose(value, 0.0) + ), + "upper_boundary_dimensions": "|".join( + name + for name, value in zip(D2D_INPUT_COLUMNS, batch.X_norm[index]) + if np.isclose(value, 1.0) + ), + } + for dimension, name in enumerate(D2D_INPUT_COLUMNS): + row[name] = float(batch.X_phys[index, dimension]) + row[f"grid_{dimension}"] = int(batch.grid_indices[index, dimension]) + row[f"norm_{dimension}"] = float(batch.X_norm[index, dimension]) + for objective, objective_name in enumerate(D2D_OBJECTIVE_NAMES): + observed_minimum = float(observed_values[:, objective].min()) + observed_maximum = float(observed_values[:, objective].max()) + below, above, outside = _declared_bound_flags( + means[index, objective], config.objective_bounds[objective] + ) + lower, upper = config.objective_bounds[objective] + row[f"pred_mean_{objective}"] = float(means[index, objective]) + row[f"pred_std_{objective}"] = float(stds[index, objective]) + row[f"observed_minimum_{objective}"] = observed_minimum + row[f"observed_maximum_{objective}"] = observed_maximum + row[f"pred_mean_below_observed_{objective}"] = bool( + means[index, objective] < observed_minimum + ) + row[f"pred_mean_above_observed_{objective}"] = bool( + means[index, objective] > observed_maximum + ) + row[f"pred_mean_below_declared_bounds_{objective}"] = bool(below) + row[f"pred_mean_above_declared_bounds_{objective}"] = bool(above) + row[f"pred_mean_outside_declared_bounds_{objective}"] = bool(outside) + row[f"ucb_raw_{objective}"] = float( + scoring.utility_ucb_raw[index, objective] + ) + row[f"ucb_effective_{objective}"] = float( + scoring.utility_ucb_effective[index, objective] + ) + row[f"ucb_clip_amount_{objective}"] = float( + scoring.utility_ucb_clip_amount[index, objective] + ) + row[f"ucb_effective_outside_declared_bounds_{objective}"] = bool( + ( + lower is not None + and scoring.utility_ucb_effective[index, objective] < lower + ) + or ( + upper is not None + and scoring.utility_ucb_effective[index, objective] > upper + ) + ) + row[f"objective_name_{objective}"] = objective_name + rows.append(row) + return rows + + +def _influence_boundary_summary( + influence_batches: Sequence[StudyBatch], +) -> pd.DataFrame: + """Compare full versus each omission's lower/upper boundary preference.""" + by_id = {batch.run_id: batch for batch in influence_batches} + full = by_id.get("influence_full") + if full is None: + raise RuntimeError("Influence boundary summary requires influence_full.") + omission_batches = sorted( + ( + batch + for batch in influence_batches + if batch.run_id.startswith("influence_omit_") + ), + key=lambda batch: int(batch.run_id.rsplit("_", 1)[1]), + ) + if not omission_batches: + raise RuntimeError("Influence boundary summary requires omission batches.") + + rows: list[dict[str, Any]] = [] + for batch in omission_batches: + row: dict[str, Any] = {"omitted_sample_id": int(batch.run_id.rsplit("_", 1)[1])} + for dimension, name in enumerate(D2D_INPUT_COLUMNS): + for prefix, source in (("full", full), ("omitted", batch)): + lower = np.isclose(source.X_norm[:, dimension], 0.0) + upper = np.isclose(source.X_norm[:, dimension], 1.0) + boundary = lower | upper + row[f"{prefix}_lower_boundary_count_{dimension}"] = int(lower.sum()) + row[f"{prefix}_lower_boundary_rate_{dimension}"] = float(lower.mean()) + row[f"{prefix}_upper_boundary_count_{dimension}"] = int(upper.sum()) + row[f"{prefix}_upper_boundary_rate_{dimension}"] = float(upper.mean()) + row[f"{prefix}_boundary_count_{dimension}"] = int(boundary.sum()) + row[f"{prefix}_boundary_rate_{dimension}"] = float(boundary.mean()) + row[f"lower_boundary_rate_change_{dimension}"] = ( + row[f"omitted_lower_boundary_rate_{dimension}"] + - row[f"full_lower_boundary_rate_{dimension}"] + ) + row[f"upper_boundary_rate_change_{dimension}"] = ( + row[f"omitted_upper_boundary_rate_{dimension}"] + - row[f"full_upper_boundary_rate_{dimension}"] + ) + row[f"boundary_rate_change_{dimension}"] = ( + row[f"omitted_boundary_rate_{dimension}"] + - row[f"full_boundary_rate_{dimension}"] + ) + row[f"input_name_{dimension}"] = name + rows.append(row) + return pd.DataFrame(rows) + + +def _run_observation_influence( + *, + validation: ModelValidationResult, + common_pool: CandidatePool, + common_pool_hash: str, + full_pool_scores: np.ndarray, + config: ResolvedStep2CConfig, + mode_settings: ExecutionModeSettings, + training: Any, +) -> tuple[ + ObservationInfluenceStudyResult, + list[StudyBatch], + pd.DataFrame, + pd.DataFrame, +]: + full_grid_scorer = CachedGridScorer( + _grid_score_function( + validation.full_fit.model, + config.design, + validation.full_fit.train_Y.detach().cpu().numpy(), + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ), + len(D2D_INPUT_COLUMNS), + ) + full_grid_scorer.seed(common_pool.grid_indices, full_pool_scores) + full_batch = _run_refined_batch( + run_id="influence_full", + run_family="observation_influence", + core_run=False, + pool=common_pool, + pool_hash=common_pool_hash, + master_base_scores=full_pool_scores, + shared_grid_scorer=full_grid_scorer, + config=config, + mode_settings=mode_settings, + training=training, + model_variant="default_current", + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=None, + ) + transform = build_d2d_objective_transform() + full_mean, full_std, _, _ = _analytic_numpy_moments( + validation.full_fit.model, + full_batch.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + full_input = InfluenceRunInput( + run_label="full", + omitted_sample_id=None, + common_pool_sha256=common_pool_hash, + selected_X_norm=full_batch.X_norm, + pool_acquisition_scores=full_pool_scores, + prediction_mean_at_full_candidates=full_mean, + prediction_std_at_full_candidates=full_std, + pareto_sample_ids=_pareto_sample_ids( + validation.full_fit.train_Y.detach().cpu().numpy(), + validation.full_fit.sample_ids, + ), + hyperparameters=_hyperparameter_mapping(validation.full_fit), + prediction_uncertainty_kind="latent", + ) + omission_inputs: list[InfluenceRunInput] = [] + study_batches: list[StudyBatch] = [full_batch] + candidate_rows = _influence_candidate_rows( + full_batch, + omitted_sample_id=None, + model=validation.full_fit.model, + model_training_y=validation.full_fit.train_Y.detach().cpu().numpy(), + observed_y=training.Y_objectives, + observed_norm=training.X_norm_all, + config=config, + ) + for sample_id in mode_settings.omitted_sample_ids: + record = validation.loocv.fold_records[sample_id] + means, stds, _, _ = _analytic_numpy_moments( + record.model, + common_pool.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + scoring, _ = _score_moments( + means, + stds, + record.train_Y.detach().cpu().numpy(), + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ) + omission_grid_scorer = CachedGridScorer( + _grid_score_function( + record.model, + config.design, + record.train_Y.detach().cpu().numpy(), + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ), + len(D2D_INPUT_COLUMNS), + ) + omission_grid_scorer.seed(common_pool.grid_indices, scoring.base_score) + batch = _run_refined_batch( + run_id=f"influence_omit_{int(sample_id):02d}", + run_family="observation_influence", + core_run=False, + pool=common_pool, + pool_hash=common_pool_hash, + master_base_scores=scoring.base_score, + shared_grid_scorer=omission_grid_scorer, + config=config, + mode_settings=mode_settings, + training=training, + model_variant="default_current", + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=scoring, + ) + prediction_mean, prediction_std, _, _ = _analytic_numpy_moments( + record.model, + full_batch.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + omission_inputs.append( + InfluenceRunInput( + run_label=batch.run_id, + omitted_sample_id=sample_id, + common_pool_sha256=common_pool_hash, + selected_X_norm=batch.X_norm, + pool_acquisition_scores=scoring.base_score, + prediction_mean_at_full_candidates=prediction_mean, + prediction_std_at_full_candidates=prediction_std, + pareto_sample_ids=_pareto_sample_ids( + record.train_Y.detach().cpu().numpy(), record.sample_ids + ), + hyperparameters=_hyperparameter_mapping(record), + prediction_uncertainty_kind="latent", + ) + ) + study_batches.append(batch) + candidate_rows.extend( + _influence_candidate_rows( + batch, + omitted_sample_id=sample_id, + model=record.model, + model_training_y=record.train_Y.detach().cpu().numpy(), + observed_y=training.Y_objectives, + observed_norm=training.X_norm_all, + config=config, + ) + ) + objective_scales = np.ptp(training.Y_objectives, axis=0) + if np.any(objective_scales <= 0): + raise RuntimeError("Influence normalization requires nonconstant objectives.") + roles = { + int(sample_id): str(role) + for sample_id, role in zip(training.sample_ids, training.row_roles) + } + include_policy = { + int(sample_id): bool(include) + for sample_id, include in zip(training.sample_ids, training.include_in_model) + } + influence = run_observation_influence_study( + full_input, + omission_inputs, + expected_common_pool_sha256=common_pool_hash, + objective_names=D2D_OBJECTIVE_NAMES, + objective_scales=objective_scales, + row_roles=roles, + primary_include_policy=include_policy, + regional_thresholds=config.regional_thresholds, + top_k=min(config.influence_top_k, common_pool.size), + require_complete_coverage=(set(mode_settings.omitted_sample_ids) == set(roles)), + ) + return ( + influence, + study_batches, + _stamp_frame(pd.DataFrame(candidate_rows)), + _stamp_frame(influence.prediction_changes_frame()), + ) + + +def _augment_shortlist_model_and_influence_diagnostics( + shortlist: pd.DataFrame, + *, + validations_by_name: Mapping[str, ModelValidationResult], + config: ResolvedStep2CConfig, + mode_settings: ExecutionModeSettings, + training: Any, +) -> tuple[pd.DataFrame, pd.DataFrame]: + """Attach model/policy reports and omission sensitivity at region medoids.""" + result = shortlist.copy().reset_index(drop=True) + norm_columns = [f"medoid_norm_{index}" for index in range(len(D2D_INPUT_COLUMNS))] + missing = sorted(set(norm_columns) - set(result.columns)) + if missing: + raise RuntimeError(f"Shortlist is missing medoid coordinates: {missing}.") + X_norm = result.loc[:, norm_columns].to_numpy(dtype=float) + if not np.all(np.isfinite(X_norm)) or np.any(X_norm < 0.0) or np.any(X_norm > 1.0): + raise RuntimeError("Shortlist medoids must be finite normalized coordinates.") + lower_boundary_flags = np.isclose(X_norm, 0.0) + upper_boundary_flags = np.isclose(X_norm, 1.0) + result["lower_boundary_coordinate_count"] = lower_boundary_flags.sum(axis=1) + result["upper_boundary_coordinate_count"] = upper_boundary_flags.sum(axis=1) + result["lower_boundary_dimensions"] = [ + "|".join(name for name, flag in zip(D2D_INPUT_COLUMNS, candidate_flags) if flag) + for candidate_flags in lower_boundary_flags + ] + result["upper_boundary_dimensions"] = [ + "|".join(name for name, flag in zip(D2D_INPUT_COLUMNS, candidate_flags) if flag) + for candidate_flags in upper_boundary_flags + ] + + transform = build_d2d_objective_transform() + full_default_mean: np.ndarray | None = None + full_default_std: np.ndarray | None = None + for variant_name in config.model_variant_names: + validation = validations_by_name[variant_name] + means, stds, _, _ = _analytic_numpy_moments( + validation.full_fit.model, + X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + scoring, _ = _score_moments( + means, + stds, + validation.full_fit.train_Y.detach().cpu().numpy(), + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ) + if variant_name == config.primary_model_variant: + full_default_mean = means + full_default_std = stds + for objective, objective_name in enumerate(D2D_OBJECTIVE_NAMES): + prefix = f"{variant_name}_{objective_name}" + result[f"{prefix}_pred_mean"] = means[:, objective] + result[f"{prefix}_pred_std"] = stds[:, objective] + result[f"{prefix}_ucb_raw"] = scoring.utility_ucb_raw[:, objective] + result[f"{prefix}_ucb_effective"] = scoring.utility_ucb_effective[ + :, objective + ] + result[f"{prefix}_ucb_clip_amount"] = scoring.utility_ucb_clip_amount[ + :, objective + ] + observed_minimum = float(training.Y_objectives[:, objective].min()) + observed_maximum = float(training.Y_objectives[:, objective].max()) + result[f"{prefix}_observed_minimum"] = observed_minimum + result[f"{prefix}_observed_maximum"] = observed_maximum + result[f"{prefix}_pred_mean_below_observed"] = ( + means[:, objective] < observed_minimum + ) + result[f"{prefix}_pred_mean_above_observed"] = ( + means[:, objective] > observed_maximum + ) + below_declared, above_declared, outside_declared = _declared_bound_flags( + means[:, objective], config.objective_bounds[objective] + ) + result[f"{prefix}_pred_mean_below_declared_bounds"] = below_declared + result[f"{prefix}_pred_mean_above_declared_bounds"] = above_declared + result[f"{prefix}_pred_mean_outside_declared_bounds"] = outside_declared + + if full_default_mean is None or full_default_std is None: + raise RuntimeError("The primary model variant was not evaluated at medoids.") + objective_scales = np.ptp(training.Y_objectives, axis=0) + if np.any(objective_scales <= 0.0): + raise RuntimeError("Shortlist influence requires nonconstant objectives.") + + per_omission_mean: list[np.ndarray] = [] + per_omission_std: list[np.ndarray] = [] + if len(config.control_sample_ids) != 1: + raise RuntimeError("Step 2C requires exactly one configured control sample.") + control_sample_id = int(config.control_sample_ids[0]) + control_omission_mean: np.ndarray | None = None + control_omission_std: np.ndarray | None = None + prediction_rows: list[dict[str, Any]] = [] + default_validation = validations_by_name[config.primary_model_variant] + for sample_id in mode_settings.omitted_sample_ids: + fit = default_validation.loocv.fold_records[sample_id] + omitted_mean, omitted_std, _, _ = _analytic_numpy_moments( + fit.model, + X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + mean_delta = omitted_mean - full_default_mean + std_delta = omitted_std - full_default_std + normalized_mean = np.abs(mean_delta) / objective_scales[None, :] + normalized_std = np.abs(std_delta) / objective_scales[None, :] + per_omission_mean.append(normalized_mean) + per_omission_std.append(normalized_std) + if int(sample_id) == control_sample_id: + control_omission_mean = normalized_mean + control_omission_std = normalized_std + for candidate_index, region_id in enumerate(result["region_id"].astype(str)): + for objective, objective_name in enumerate(D2D_OBJECTIVE_NAMES): + prediction_rows.append( + { + "omitted_sample_id": int(sample_id), + "candidate_index": candidate_index, + "objective_index": objective, + "objective_name": objective_name, + "full_mean": float( + full_default_mean[candidate_index, objective] + ), + "omitted_mean": float(omitted_mean[candidate_index, objective]), + "mean_delta": float(mean_delta[candidate_index, objective]), + "absolute_mean_delta": float( + abs(mean_delta[candidate_index, objective]) + ), + "full_std": float(full_default_std[candidate_index, objective]), + "omitted_std": float(omitted_std[candidate_index, objective]), + "std_delta": float(std_delta[candidate_index, objective]), + "absolute_std_delta": float( + abs(std_delta[candidate_index, objective]) + ), + "objective_scale": float(objective_scales[objective]), + "normalized_absolute_mean_delta": float( + normalized_mean[candidate_index, objective] + ), + "normalized_absolute_std_delta": float( + normalized_std[candidate_index, objective] + ), + "prediction_location_kind": "robust_region_medoid", + "location_id": region_id, + } + ) + + mean_stack = np.stack(per_omission_mean, axis=0) + std_stack = np.stack(per_omission_std, axis=0) + result["influence_omission_count"] = len(mode_settings.omitted_sample_ids) + result["influence_mean_normalized_prediction_mean_change"] = mean_stack.mean( + axis=(0, 2) + ) + result["influence_max_normalized_prediction_mean_change"] = mean_stack.max( + axis=(0, 2) + ) + result["influence_mean_normalized_prediction_std_change"] = std_stack.mean( + axis=(0, 2) + ) + result["influence_max_normalized_prediction_std_change"] = std_stack.max( + axis=(0, 2) + ) + if control_omission_mean is not None and control_omission_std is not None: + result["control_omission_mean_normalized_prediction_mean_change"] = ( + control_omission_mean.mean(axis=1) + ) + result["control_omission_mean_normalized_prediction_std_change"] = ( + control_omission_std.mean(axis=1) + ) + nearest_observed = np.linalg.norm( + X_norm[:, None, :] - training.X_norm_all[None, :, :], axis=-1 + ).min(axis=1) + result["nearest_observed_distance"] = nearest_observed + boundary_flags = np.isclose(X_norm, 0.0) | np.isclose(X_norm, 1.0) + result["boundary_coordinate_count"] = boundary_flags.sum(axis=1) + result["boundary_dimensions"] = [ + "|".join( + name for name, is_boundary in zip(D2D_INPUT_COLUMNS, flags) if is_boundary + ) + for flags in boundary_flags + ] + return _stamp_frame(result), _stamp_frame(pd.DataFrame(prediction_rows)) + + +def _augment_regions_with_control_influence_correspondence( + regions: pd.DataFrame, + *, + influence_batches: Sequence[StudyBatch], + control_sample_id: int, + threshold: float, +) -> pd.DataFrame: + by_id = {batch.run_id: batch for batch in influence_batches} + omit_control_run_id = f"influence_omit_{int(control_sample_id):02d}" + if "influence_full" not in by_id or omit_control_run_id not in by_id: + raise RuntimeError( + "Control correspondence requires full and omit-control runs." + ) + medoid_columns = sorted( + (column for column in regions if column.startswith("medoid_norm_")), + key=lambda value: int(value.rsplit("_", 1)[1]), + ) + medoids = regions.loc[:, medoid_columns].to_numpy(dtype=float) + full = by_id["influence_full"].X_norm + omit_control = by_id[omit_control_run_id].X_norm + full_distance = np.linalg.norm(medoids[:, None, :] - full[None, :, :], axis=-1).min( + axis=1 + ) + omit_distance = np.linalg.norm( + medoids[:, None, :] - omit_control[None, :, :], axis=-1 + ).min(axis=1) + result = regions.copy() + result["control_sample_id"] = int(control_sample_id) + result["control_influence_correspondence_threshold"] = float(threshold) + result["full_model_batch_minimum_distance"] = full_distance + result["omit_control_batch_minimum_distance"] = omit_distance + result["full_model_batch_region_hit"] = full_distance <= float(threshold) + result["omit_control_batch_region_hit"] = omit_distance <= float(threshold) + result["control_included_vs_omit_control_correspondence"] = ( + result["full_model_batch_region_hit"] & result["omit_control_batch_region_hit"] + ) + result["control_omit_correspondence_available"] = True + result["control_omit_correspondence_definition"] = ( + "both_full_and_omit_control_batches_within_region_threshold" + ) + return _stamp_frame(result) + + +def _require_debug_stamps(frame: pd.DataFrame, *, name: str) -> pd.DataFrame: + """Fail closed if an exported table has lost its debug-only contract.""" + required = { + "debug_only", + "approved_for_experiment", + "approved_for_production", + "candidate_status", + } + missing = sorted(required - set(frame.columns)) + if missing: + raise RuntimeError(f"Artifact table {name!r} is missing stamps: {missing}.") + if not frame.empty: + if not frame["debug_only"].astype(bool).all(): + raise RuntimeError(f"Artifact table {name!r} is not entirely debug-only.") + if frame["approved_for_experiment"].astype(bool).any(): + raise RuntimeError( + f"Artifact table {name!r} contains experimental approval." + ) + if frame["approved_for_production"].astype(bool).any(): + raise RuntimeError(f"Artifact table {name!r} contains production approval.") + if not frame["candidate_status"].eq(D2D_DEBUG_WATERMARK).all(): + raise RuntimeError(f"Artifact table {name!r} lost the watermark.") + return frame + + +def _bounded_plot_frame(scoring: UCBHVIScoreResult) -> pd.DataFrame: + raw = np.asarray(scoring.utility_ucb_raw, dtype=float) + effective = np.asarray(scoring.utility_ucb_effective, dtype=float) + if raw.shape != effective.shape or raw.ndim != 2: + raise RuntimeError("Bounded-utility plotting arrays must have shape (N, M).") + if raw.shape[1] != len(D2D_OBJECTIVE_NAMES): + raise RuntimeError("Bounded-utility plotting objective count changed.") + rows = [ + { + "policy": f"clip_ucb:{name}", + "raw_value": float(raw[:, objective].mean()), + "bounded_value": float(effective[:, objective].mean()), + } + for objective, name in enumerate(D2D_OBJECTIVE_NAMES) + ] + return pd.DataFrame(rows) + + +def _boundary_plot_frame(enrichment: pd.DataFrame) -> pd.DataFrame: + required_groups = ( + "pool", + "top_1_percent_acquisition", + "selected_batch", + ) + result: pd.DataFrame | None = None + for side in ("lower", "upper"): + pivot = enrichment.pivot( + index="input_name", + columns="group", + values=f"{side}_boundary_rate", + ) + missing = sorted(set(required_groups) - set(pivot.columns)) + if missing: + raise RuntimeError( + f"Boundary-enrichment plot is missing {side} groups: {missing}." + ) + side_frame = pivot.loc[:, list(required_groups)].rename( + columns={ + "pool": f"pool_{side}_boundary_frequency", + "top_1_percent_acquisition": f"top_{side}_boundary_frequency", + "selected_batch": f"selected_{side}_boundary_frequency", + } + ) + result = side_frame if result is None else result.join(side_frame) + if result is None: + raise RuntimeError("Boundary-enrichment plot has no lower/upper data.") + return result.reset_index() + + +def _shortlist_prediction_plot_frame(shortlist: pd.DataFrame) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + for _, candidate in shortlist.iterrows(): + for variant_name in ("default_current", "conservative"): + for objective_name in D2D_OBJECTIVE_NAMES: + prefix = f"{variant_name}_{objective_name}" + rows.append( + { + "candidate_id": candidate["shortlist_id"], + "model_variant": variant_name, + "objective_name": objective_name, + "predicted_mean": candidate[f"{prefix}_pred_mean"], + "predicted_std": candidate[f"{prefix}_pred_std"], + "observed_minimum": candidate[f"{prefix}_observed_minimum"], + "observed_maximum": candidate[f"{prefix}_observed_maximum"], + } + ) + return pd.DataFrame(rows) + + +def _consensus_debug_frame( + shortlist: pd.DataFrame, + *, + consensus_batch_size: int, +) -> pd.DataFrame: + if shortlist.shape[0] < consensus_batch_size: + raise RuntimeError("Consensus passed without enough shortlist rows.") + result = shortlist.head(consensus_batch_size).copy().reset_index(drop=True) + missing_inputs = sorted(set(D2D_INPUT_COLUMNS) - set(result.columns)) + if missing_inputs: + raise RuntimeError( + f"Consensus shortlist is missing physical inputs: {missing_inputs}." + ) + result.insert( + 0, + "consensus_candidate_id", + [f"R1-CONSENSUS-DEBUG-{index:02d}" for index in range(1, len(result) + 1)], + ) + result.insert(1, "selection_order", np.arange(1, len(result) + 1)) + result["consensus_basis"] = "robust_region_medoid" + result["experimental_release_blocked"] = True + return _stamp_frame(result) + + +def _build_private_evidence_zip_staging( + source_dir: Path, + temporary_zip: Path, + *, + archive_root_name: str, + overwrite: bool, +) -> Path: + """Build the complete local evidence ZIP with an unambiguous warning.""" + if not source_dir.is_dir(): + raise FileNotFoundError( + f"Private evidence ZIP source directory is missing: {source_dir}." + ) + if ( + not archive_root_name + or Path(archive_root_name).name != archive_root_name + or archive_root_name in {".", ".."} + ): + raise ValueError("archive_root_name must be one safe path component.") + if temporary_zip.exists(): + if not overwrite: + raise FileExistsError( + f"Private evidence ZIP staging path exists: {temporary_zip}." + ) + temporary_zip.unlink() + try: + with zipfile.ZipFile( + temporary_zip, mode="w", compression=zipfile.ZIP_DEFLATED + ) as archive: + archive.writestr( + f"{archive_root_name}/PRIVATE_EVIDENCE_DO_NOT_SHARE.txt", + ( + "PRIVATE EVIDENCE - DO NOT SHARE\n\n" + "This archive contains the complete local Step 2C audit " + "surface, including private source provenance, local paths, " + "sample-level tables, and exact candidate recipes. It is not " + "a portable or public export. Use the sibling public summary " + "ZIP for sharing.\n" + ), + ) + for path in sorted( + (item for item in source_dir.rglob("*") if item.is_file()), + key=lambda item: item.relative_to(source_dir).as_posix(), + ): + archive.write( + path, + arcname=( + f"{archive_root_name}/" + f"{path.relative_to(source_dir).as_posix()}" + ), + ) + with zipfile.ZipFile(temporary_zip, mode="r") as archive: + if archive.testzip() is not None: + raise RuntimeError("Private evidence ZIP failed its integrity check.") + except Exception: + if temporary_zip.exists(): + temporary_zip.unlink() + raise + return temporary_zip + + +def _public_summary_manifest( + private_manifest: Mapping[str, Any], payloads: Mapping[str, bytes] +) -> dict[str, Any]: + input_data_kind = private_manifest.get("input_data_kind") + if input_data_kind == "sanitized_synthetic_ci": + public_data_kind = "generated_synthetic_dataset" + elif input_data_kind == "private_pinned_workbook": + public_data_kind = "private_campaign_dataset_redacted" + else: + raise RuntimeError("Step 2C public export received an unknown data kind.") + return { + "schema_version": PUBLIC_SUMMARY_SCHEMA_VERSION, + "method_version": private_manifest["method_version"], + "mode": private_manifest["mode"], + "input_data_kind": public_data_kind, + "source_dataset": public_data_kind, + "public_export": True, + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "contains_candidate_recipes": False, + "contains_sample_level_data": False, + "contains_local_paths": False, + "full_private_evidence_included": False, + "git_commit": private_manifest["git_commit"], + "objective_order": private_manifest["objective_order"], + "reference_point": private_manifest["reference_point"], + "objective_bounds": private_manifest["objective_bounds"], + "moment_method": private_manifest["moment_method"], + "robust_region_count": private_manifest["robust_region_count"], + "shortlist_count": private_manifest["shortlist_count"], + "consensus_passed": private_manifest["consensus_passed"], + "consensus_checks": private_manifest["consensus_checks"], + "consensus_observed": private_manifest["consensus_observed"], + "runtime_versions": private_manifest["runtime_versions"], + "archive_root": PUBLIC_SUMMARY_ARCHIVE_ROOT, + "included_files": { + relative: hashlib.sha256(payload).hexdigest().upper() + for relative, payload in sorted(payloads.items()) + }, + } + + +def _build_public_summary_zip_staging( + source_dir: Path, + temporary_zip: Path, + *, + private_manifest: Mapping[str, Any], + overwrite: bool, +) -> Path: + """Build and validate a strict aggregate-only public summary ZIP.""" + if not source_dir.is_dir(): + raise FileNotFoundError( + f"Public summary ZIP source directory is missing: {source_dir}." + ) + if temporary_zip.exists(): + if not overwrite: + raise FileExistsError( + f"Public summary ZIP staging path exists: {temporary_zip}." + ) + temporary_zip.unlink() + + payloads: dict[str, bytes] = { + PUBLIC_SUMMARY_DEBUG_MARKER_FILE: (D2D_DEBUG_WATERMARK + "\n").encode("utf-8"), + PUBLIC_SUMMARY_README_FILE: ( + "MOBO-Kit D2D Step 2C sanitized public summary\n\n" + "This debug-only export contains allowlisted aggregate model and " + "search diagnostics. Candidate recipes, row-level measurements, " + "source provenance, local paths, and the complete evidence surface " + "are intentionally excluded. This summary cannot independently " + "validate the private source data and is not approved for experiment " + "or production use.\n" + ).encode("utf-8"), + } + for relative in PUBLIC_SUMMARY_CSV_FILES: + source = source_dir / relative + if not source.is_file(): + raise FileNotFoundError( + f"Public summary source table is missing: {relative}." + ) + payloads[relative] = source.read_bytes() + public_manifest = _public_summary_manifest(private_manifest, payloads) + payloads[PUBLIC_SUMMARY_MANIFEST_FILE] = json.dumps( + _jsonable(public_manifest), indent=2, sort_keys=True + ).encode("utf-8") + + try: + with zipfile.ZipFile( + temporary_zip, mode="w", compression=zipfile.ZIP_DEFLATED + ) as archive: + for relative, payload in sorted(payloads.items()): + archive.writestr(f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{relative}", payload) + validate_step2c_public_summary_archive(temporary_zip) + except Exception: + if temporary_zip.exists(): + temporary_zip.unlink() + raise + return temporary_zip + + +def _remove_publication_path(path: Path) -> None: + if not path.exists(): + return + if path.is_dir() and not path.is_symlink(): + shutil.rmtree(path) + else: + path.unlink() + + +def _publish_validated_bundle_transaction( + staging: Path, + destination: Path, + backup: Path, + *, + zip_staging: Path, + zip_path: Path, + zip_backup: Path, + overwrite: bool, + publish_zip: bool, + validate_published: Callable[[], Any], + private_zip_staging: Path | None = None, + private_zip_path: Path | None = None, + private_zip_backup: Path | None = None, + publish_private_zip: bool = False, + validate_public_zip: Callable[[], Any] | None = None, +) -> Any: + """Publish local evidence and public/private ZIPs as one transaction.""" + private_paths = (private_zip_staging, private_zip_path, private_zip_backup) + private_paths_supplied = tuple(path is not None for path in private_paths) + if any(private_paths_supplied) and not all(private_paths_supplied): + raise ValueError("Private evidence ZIP publication paths are all-or-none.") + if publish_private_zip and not all(private_paths_supplied): + raise ValueError("Private evidence ZIP paths are required for publication.") + private_enabled = all(private_paths_supplied) + if not staging.is_dir(): + raise FileNotFoundError(f"Step 2C staging directory is missing: {staging}.") + if ( + backup.exists() + or zip_backup.exists() + or ( + private_enabled + and private_zip_backup is not None + and private_zip_backup.exists() + ) + ): + raise FileExistsError("Step 2C publication backup paths must not pre-exist.") + if publish_zip and not zip_staging.is_file(): + raise FileNotFoundError( + f"Public summary ZIP staging file is missing: {zip_staging}." + ) + if ( + publish_private_zip + and private_zip_staging is not None + and not private_zip_staging.is_file() + ): + raise FileNotFoundError( + f"Private evidence ZIP staging file is missing: {private_zip_staging}." + ) + if destination.exists(): + if not destination.is_dir(): + raise FileExistsError( + f"Step 2C output destination is not a directory: {destination}." + ) + if any(destination.iterdir()) and not overwrite: + raise FileExistsError( + f"Step 2C output directory is not empty: {destination}." + ) + if zip_path.exists() and not overwrite: + raise FileExistsError(f"Public summary ZIP already exists: {zip_path}.") + if ( + private_enabled + and private_zip_path is not None + and private_zip_path.exists() + and not overwrite + ): + raise FileExistsError( + f"Private evidence ZIP already exists: {private_zip_path}." + ) + + original_directory_moved = False + original_zip_moved = False + original_private_zip_moved = False + new_directory_published = False + new_zip_published = False + new_private_zip_published = False + try: + if destination.exists(): + destination.rename(backup) + original_directory_moved = True + if zip_path.exists(): + zip_path.rename(zip_backup) + original_zip_moved = True + if ( + private_enabled + and private_zip_path is not None + and private_zip_path.exists() + ): + assert private_zip_backup is not None + private_zip_path.rename(private_zip_backup) + original_private_zip_moved = True + staging.rename(destination) + new_directory_published = True + if publish_zip: + os.replace(zip_staging, zip_path) + new_zip_published = True + if publish_private_zip: + assert private_zip_staging is not None + assert private_zip_path is not None + os.replace(private_zip_staging, private_zip_path) + new_private_zip_published = True + validated = validate_published() + if publish_zip and validate_public_zip is not None: + validate_public_zip() + except Exception: + if new_private_zip_published and private_zip_path is not None: + _remove_publication_path(private_zip_path) + if new_zip_published: + _remove_publication_path(zip_path) + if new_directory_published: + _remove_publication_path(destination) + if original_directory_moved and backup.exists() and not destination.exists(): + backup.rename(destination) + if original_zip_moved and zip_backup.exists() and not zip_path.exists(): + zip_backup.rename(zip_path) + if ( + original_private_zip_moved + and private_zip_backup is not None + and private_zip_path is not None + and private_zip_backup.exists() + and not private_zip_path.exists() + ): + private_zip_backup.rename(private_zip_path) + raise + finally: + if zip_staging.exists(): + _remove_publication_path(zip_staging) + if private_zip_staging is not None and private_zip_staging.exists(): + _remove_publication_path(private_zip_staging) + if original_directory_moved: + _remove_publication_path(backup) + if original_zip_moved: + _remove_publication_path(zip_backup) + if original_private_zip_moved and private_zip_backup is not None: + _remove_publication_path(private_zip_backup) + return validated + + +def _write_step2c_bundle( + *, + destination: Path, + config: ResolvedStep2CConfig, + workbook: Path, + workbook_audit: dict[str, Any], + tables: Mapping[str, pd.DataFrame], + manifest: dict[str, Any], + consensus: ConsensusCriteriaResult, + shortlist: pd.DataFrame, + consensus_candidates: pd.DataFrame, + loocv_predictions: pd.DataFrame, + model_hyperparameters: pd.DataFrame, + nested_convergence: pd.DataFrame, + bounded_scoring: UCBHVIScoreResult, + penalty_tradeoff: pd.DataFrame, + influence_summary: pd.DataFrame, + influence_candidates: pd.DataFrame, + influence_predictions: pd.DataFrame, + boundary_enrichment: pd.DataFrame, + robust_regions: pd.DataFrame, + robust_membership: pd.DataFrame, + create_portable_zip: bool, + overwrite: bool, + total_started: float, +) -> dict[str, str]: + """Write, validate, then atomically publish a private Step 2C bundle.""" + repository_root = Path(__file__).resolve().parents[2] + allowed_root = (repository_root / config.output_root).resolve() + if allowed_root not in destination.parents: + raise RuntimeError("Step 2C bundle escaped the configured ignored root.") + destination.parent.mkdir(parents=True, exist_ok=True) + staging = destination.with_name(f".{destination.name}.staging") + backup = destination.with_name(f".{destination.name}.backup") + # The legacy sibling ``.zip`` path is retained for callers, but now + # contains only the sanitized public summary. Full private evidence receives + # an explicit filename and is produced only for the pinned campaign source. + zip_path = destination.with_suffix(".zip") + zip_staging = zip_path.with_name(f".{zip_path.name}.staging") + zip_backup = zip_path.with_name(f".{zip_path.name}.backup") + private_zip_path = destination.with_name( + f"{destination.name}_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip" + ) + private_zip_staging = private_zip_path.with_name( + f".{private_zip_path.name}.staging" + ) + private_zip_backup = private_zip_path.with_name(f".{private_zip_path.name}.backup") + publish_private_evidence = ( + create_portable_zip + and manifest.get("input_data_kind") == "private_pinned_workbook" + ) + for internal in ( + staging, + backup, + zip_staging, + zip_backup, + private_zip_staging, + private_zip_backup, + ): + if internal.exists(): + if not overwrite: + raise FileExistsError( + f"Internal Step 2C publication path exists: {internal}." + ) + if internal.is_dir() and not internal.is_symlink(): + shutil.rmtree(internal) + else: + internal.unlink() + + if zip_path.exists() and not overwrite: + raise FileExistsError(f"Public summary ZIP already exists: {zip_path}.") + if private_zip_path.exists() and not overwrite: + raise FileExistsError( + f"Private evidence ZIP already exists: {private_zip_path}." + ) + + staging.mkdir(parents=False, exist_ok=False) + try: + (staging / "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt").write_text( + D2D_DEBUG_WATERMARK + "\n", + encoding="utf-8", + ) + (staging / "resolved_debug_config.yaml").write_text( + yaml.safe_dump(config.raw, sort_keys=False), encoding="utf-8" + ) + for name, frame in sorted(tables.items()): + if Path(name).name != name or not name.endswith(".csv"): + raise RuntimeError(f"Unsafe Step 2C table artifact name: {name!r}.") + _require_debug_stamps(frame, name=name).to_csv(staging / name, index=False) + + if consensus.passed: + consensus_frame = _consensus_debug_frame( + consensus_candidates, + consensus_batch_size=config.consensus_batch_size, + ) + consensus_frame.to_csv( + staging / "r1_consensus_debug_batch.csv", index=False + ) + else: + _safe_write_json( + staging / "r1_no_stable_batch_reason.json", + { + "consensus_passed": False, + "failed_checks": list(consensus.reasons), + "checks": consensus.checks, + "observed": consensus.observed, + "message": ( + "No stable R1 batch is released. Review the failed robustness " + "criteria before any experimental decision." + ), + }, + ) + + plot_loocv_diagnostics( + loocv_predictions, + staging / "plots/model_validation/loocv_diagnostics.png", + objective_column="objective_name", + std_column="predictive_std", + ) + plot_ard_lengthscale_comparison( + model_hyperparameters, + staging / "plots/model_validation/ard_lengthscale_comparison.png", + ) + plot_candidate_predictions_vs_observed_ranges( + _shortlist_prediction_plot_frame(shortlist), + staging + / "plots/model_validation/candidate_predictions_vs_observed_ranges.png", + ) + plot_nested_search_convergence( + nested_convergence, + staging / "plots/search_convergence/nested_pool_convergence.png", + pool_size_column="comparison_pool_size", + regional_columns={ + 0.10: "regional_matches_within_0.10", + 0.15: "regional_matches_within_0.15", + 0.20: "regional_matches_within_0.20", + }, + ) + plot_bounded_utility_comparison( + _bounded_plot_frame(bounded_scoring), + staging / "plots/bounded_utility/bounded_utility_comparison.png", + ) + plot_local_penalty_tradeoff( + penalty_tradeoff, + staging / "plots/local_penalty/local_penalty_tradeoff.png", + variant_column="penalty_label", + acquisition_column="comparison_acquisition_sum", + ) + plot_observation_influence_ranking( + influence_summary, + staging / "plots/influence/observation_influence_ranking.png", + sample_column="omitted_sample_id", + score_column="composite_influence_score", + ) + influence_norm_columns = sorted( + ( + column + for column in influence_candidates.columns + if column.startswith("norm_") and column[5:].isdigit() + ), + key=lambda value: int(value.split("_", 1)[1]), + ) + full_influence_rows = influence_candidates[ + influence_candidates["omitted_sample_id"].isna() + ] + if len(config.control_sample_ids) != 1: + raise RuntimeError( + "Step 2C requires exactly one configured control sample." + ) + control_sample_id = int(config.control_sample_ids[0]) + omit_control_rows = influence_candidates[ + pd.to_numeric( + influence_candidates["omitted_sample_id"], errors="coerce" + ).eq(control_sample_id) + ] + plot_control_omission_candidate_region_comparison( + full_influence_rows.loc[:, influence_norm_columns].to_numpy(dtype=float), + omit_control_rows.loc[:, influence_norm_columns].to_numpy(dtype=float), + staging / "plots/influence/full_vs_omit_control_candidates.png", + ) + plot_shortlist_region_influence_sensitivity( + influence_predictions[ + influence_predictions["prediction_location_kind"].eq( + "robust_region_medoid" + ) + ], + staging / "plots/influence/shortlist_region_influence_sensitivity.png", + region_column="location_id", + sensitivity_column="normalized_absolute_mean_delta", + ) + plot_boundary_enrichment( + _boundary_plot_frame(boundary_enrichment), + staging / "plots/search_convergence/boundary_enrichment.png", + ) + norm_columns = sorted( + ( + column + for column in robust_membership.columns + if column.startswith("norm_") and column[5:].isdigit() + ), + key=lambda value: int(value.split("_", 1)[1]), + ) + plot_robust_region_overview( + robust_membership.loc[:, norm_columns].to_numpy(dtype=float), + robust_membership["region_id"].astype(str).tolist(), + staging / "plots/robust_regions/robust_region_overview.png", + input_names=D2D_INPUT_COLUMNS, + ) + plot_shortlist_medoid_parallel_coordinates( + shortlist, + staging / "plots/robust_regions/shortlist_parallel_coordinates.png", + input_names=D2D_INPUT_COLUMNS, + ) + plot_run_region_persistence_heatmap( + robust_membership, + staging / "plots/robust_regions/run_region_persistence_heatmap.png", + ) + plot_model_policy_region_correspondence( + robust_membership, + staging / "plots/robust_regions/model_policy_region_correspondence.png", + ) + plot_acquisition_quality_vs_persistence( + robust_regions, + staging / "plots/robust_regions/acquisition_quality_vs_persistence.png", + ) + + workbook_hash_after = sha256_file(workbook) + workbook_mtime_after = workbook.stat().st_mtime_ns + if ( + workbook_hash_after != manifest["workbook_sha256_before"] + or workbook_mtime_after != manifest["workbook_mtime_ns_before"] + ): + raise RuntimeError( + "The private workbook changed while exporting artifacts." + ) + manifest.update( + { + "workbook_sha256_after": workbook_hash_after, + "workbook_mtime_ns_after": workbook_mtime_after, + "source_workbook_modified": False, + "output_directory": str(destination), + "public_summary_archive_requested": create_portable_zip, + "private_evidence_archive_requested": publish_private_evidence, + "runtime_seconds_total": perf_counter() - total_started, + } + ) + workbook_audit.update( + { + "workbook_sha256_after": workbook_hash_after, + "workbook_mtime_ns_after": workbook_mtime_after, + "source_workbook_modified": False, + } + ) + _safe_write_json(staging / "workbook_audit.json", workbook_audit) + _safe_write_json(staging / "run_manifest.json", manifest) + + validate_step2c_artifact_bundle(staging, repository_root=repository_root) + + if create_portable_zip: + _build_public_summary_zip_staging( + staging, + zip_staging, + private_manifest=manifest, + overwrite=overwrite, + ) + if publish_private_evidence: + _build_private_evidence_zip_staging( + staging, + private_zip_staging, + archive_root_name=("MOBO_Kit_Step2C_PRIVATE_EVIDENCE_DO_NOT_SHARE"), + overwrite=overwrite, + ) + validated = _publish_validated_bundle_transaction( + staging, + destination, + backup, + zip_staging=zip_staging, + zip_path=zip_path, + zip_backup=zip_backup, + overwrite=overwrite, + publish_zip=create_portable_zip, + validate_published=lambda: validate_step2c_artifact_bundle( + destination, repository_root=repository_root + ), + private_zip_staging=private_zip_staging, + private_zip_path=private_zip_path, + private_zip_backup=private_zip_backup, + publish_private_zip=publish_private_evidence, + validate_public_zip=( + (lambda: validate_step2c_public_summary_archive(zip_path)) + if create_portable_zip + else None + ), + ) + return validated.artifact_sha256 + except Exception: + if staging.exists(): + _remove_publication_path(staging) + if zip_staging.exists(): + _remove_publication_path(zip_staging) + if private_zip_staging.exists(): + _remove_publication_path(private_zip_staging) + raise + + +def _run_d2d_step2c_robustness_resolved( + workbook_path: str | Path, + config: ResolvedStep2CConfig, + output_dir: str | Path, + *, + mode: str = "full", + overwrite: bool = False, + create_portable_zip: bool | None = None, + input_data_kind: str = "private_pinned_workbook", +) -> Step2CRobustnessResult: + """Run Step 2C from an already resolved, fail-closed input contract.""" + total_started = perf_counter() + phase_started = total_started + phase_runtimes: dict[str, float] = {} + repository_root = Path(__file__).resolve().parents[2] + mode_settings = config.mode(mode) + if input_data_kind not in { + "private_pinned_workbook", + "sanitized_synthetic_ci", + }: + raise ValueError("input_data_kind is not an approved Step 2C source kind.") + workbook = Path(workbook_path).resolve() + expected_workbook = (repository_root / config.workbook_relative_path).resolve() + if workbook != expected_workbook: + raise ValueError( + f"Step 2C may read only the pinned private workbook {expected_workbook}." + ) + destination = _validate_output_destination(Path(output_dir), config) + if not isinstance(overwrite, bool): + raise ValueError("overwrite must be a boolean.") + if destination.exists() and any(destination.iterdir()) and not overwrite: + raise FileExistsError(f"Step 2C output directory is not empty: {destination}.") + create_archives = ( + config.create_portable_zip + if create_portable_zip is None + else create_portable_zip + ) + if not isinstance(create_archives, bool): + raise ValueError("create_portable_zip must be a boolean or None.") + source_hash_before = sha256_file(workbook) + source_mtime_before = workbook.stat().st_mtime_ns + if source_hash_before != config.workbook_expected_sha256: + raise ValueError( + "Workbook hash differs from the Step 2C contract: " + f"expected={config.workbook_expected_sha256}, actual={source_hash_before}." + ) + git_commit, git_dirty, git_status = _git_state(repository_root) + + frame, audit = load_d2d_workbook_frame( + workbook, + expected_sample_ids=config.expected_sample_ids, + allowed_input_exceptions=config.off_grid_exceptions, + ) + validation = validate_supplied_d2d_scores(frame) + validation.raise_for_errors() + training = prepare_d2d_training_data(frame, config, include_control=True) + if np.count_nonzero(training.include_in_model) != 15: + raise RuntimeError( + "The Step 2C primary model must include all 15 observations." + ) + if not validation.known_uniformity_score_mismatch: + raise RuntimeError( + "The pinned workbook was expected to retain the known Uniformity mismatch." + ) + phase_runtimes["ingestion_and_validation"] = perf_counter() - phase_started + + phase_started = perf_counter() + train_X = torch.as_tensor(training.X_norm_all, dtype=torch.double, device="cpu") + train_Y = torch.as_tensor(training.Y_objectives, dtype=torch.double, device="cpu") + fit_cache = ModelFitCache() + validation_results = [ + validate_model_variant( + train_X, + train_Y, + sample_ids=training.sample_ids.tolist(), + input_names=D2D_INPUT_COLUMNS, + objective_names=D2D_OBJECTIVE_NAMES, + variant=variant, + seed=config.model_seed, + row_roles=training.row_roles, + control_sample_ids=config.control_sample_ids, + cache=fit_cache, + ) + for variant in (DEFAULT_CURRENT, CONSERVATIVE) + ] + validations_by_name = {result.variant.name: result for result in validation_results} + models = { + name: result.full_fit.model for name, result in validations_by_name.items() + } + phase_runtimes["model_validation"] = perf_counter() - phase_started + + phase_started = perf_counter() + primary_sobol = build_nested_sobol_discrete_pool( + config.design, + mode_settings.nested_unique_sizes, + scramble_seed=config.primary_sobol_seed, + observed_phys=training.X_phys_all, + row_constraints=[], + ) + secondary_sobol = [ + build_nested_sobol_discrete_pool( + config.design, + (mode_settings.nested_unique_sizes[-1],), + scramble_seed=seed, + observed_phys=training.X_phys_all, + row_constraints=[], + ) + for seed in config.secondary_sobol_seeds + ] + phase_runtimes["nested_sobol_pools"] = perf_counter() - phase_started + + phase_started = perf_counter() + transform = build_d2d_objective_transform() + largest_size = mode_settings.nested_unique_sizes[-1] + largest_pool = primary_sobol.pools[largest_size] + primary_mean, primary_std, primary_analytic, primary_moment_runtime = ( + _analytic_numpy_moments( + models["default_current"], + largest_pool.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + ) + scoring_by_policy: dict[tuple[float, str], UCBHVIScoreResult] = {} + scoring_runtime: dict[str, float] = {} + for beta, bound_policy in ( + (config.primary_beta, config.primary_bound_policy), + (1.0, config.primary_bound_policy), + (9.0, config.primary_bound_policy), + (config.primary_beta, "none"), + ): + scored, runtime = _score_moments( + primary_mean, + primary_std, + training.Y_objectives, + config, + beta=beta, + bound_policy=bound_policy, + ) + scoring_by_policy[(beta, bound_policy)] = scored + scoring_runtime[f"default_beta_{beta:g}_{bound_policy}"] = runtime + baseline_scoring = scoring_by_policy[ + (config.primary_beta, config.primary_bound_policy) + ] + analytic_mc_frame, analytic_mc_payload = _analytic_mc_comparison( + model=models["default_current"], + pool=largest_pool, + training_y=training.Y_objectives, + observed_norm=training.X_norm_all, + config=config, + mode_settings=mode_settings, + ) + phase_runtimes["primary_moments_and_scores"] = perf_counter() - phase_started + + phase_started = perf_counter() + dimension = len(D2D_INPUT_COLUMNS) + primary_scorers: dict[tuple[float, str], CachedGridScorer] = {} + for key, scoring in scoring_by_policy.items(): + beta, bound_policy = key + cached = CachedGridScorer( + _grid_score_function( + models["default_current"], + config.design, + training.Y_objectives, + config, + beta=beta, + bound_policy=bound_policy, + ), + dimension, + ) + cached.seed(largest_pool.grid_indices, scoring.base_score) + primary_scorers[key] = cached + + nested_batches: list[StudyBatch] = [] + nested_raw_batches: list[StudyBatch] = [] + for size in mode_settings.nested_unique_sizes: + pool = primary_sobol.pools[size] + run_id = "baseline" if size == largest_size else f"nested_{size}" + raw_selection = _static_select( + pool, + baseline_scoring.base_score[:size], + q=STEP2C_BATCH_SIZE, + penalty=_penalty_by_label(config, config.primary_penalty_variant), + observed_norm=training.X_norm_all, + ) + nested_raw_batches.append( + _study_batch_from_static( + run_id=f"nested_raw_{size}", + run_family="nested_pool_raw", + core_run=False, + selection=raw_selection, + model_variant="default_current", + pool=pool, + pool_hash=primary_sobol.prefix_hashes[size], + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=baseline_scoring, + ) + ) + nested_batches.append( + _run_refined_batch( + run_id=run_id, + run_family="baseline" if size == largest_size else "nested_pool", + core_run=True, + pool=pool, + pool_hash=primary_sobol.prefix_hashes[size], + master_base_scores=baseline_scoring.base_score[:size], + shared_grid_scorer=primary_scorers[ + (config.primary_beta, config.primary_bound_policy) + ], + config=config, + mode_settings=mode_settings, + training=training, + model_variant="default_current", + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=baseline_scoring, + ) + ) + baseline_batch = nested_batches[-1] + phase_runtimes["nested_refinement"] = perf_counter() - phase_started + + phase_started = perf_counter() + secondary_batches: list[StudyBatch] = [] + secondary_moment_runtime: dict[str, float] = {} + for result in secondary_sobol: + pool = result.largest_pool + mean, std, _, moment_runtime = _analytic_numpy_moments( + models["default_current"], + pool.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + scored, runtime = _score_moments( + mean, + std, + training.Y_objectives, + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ) + secondary_moment_runtime[str(result.scramble_seed)] = moment_runtime + runtime + scorer = CachedGridScorer( + _grid_score_function( + models["default_current"], + config.design, + training.Y_objectives, + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ), + dimension, + ) + scorer.seed(pool.grid_indices, scored.base_score) + secondary_batches.append( + _run_refined_batch( + run_id=f"sobol_seed_{result.scramble_seed}", + run_family="sobol_scramble", + core_run=True, + pool=pool, + pool_hash=result.prefix_hashes[largest_size], + master_base_scores=scored.base_score, + shared_grid_scorer=scorer, + config=config, + mode_settings=mode_settings, + training=training, + model_variant="default_current", + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=scored, + ) + ) + + conservative_mean, conservative_std, _, conservative_moment_runtime = ( + _analytic_numpy_moments( + models["conservative"], + largest_pool.X_norm, + objective_transform=transform, + chunk_size=config.score_chunk_size, + ) + ) + conservative_scoring, conservative_score_runtime = _score_moments( + conservative_mean, + conservative_std, + training.Y_objectives, + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ) + conservative_scorer = CachedGridScorer( + _grid_score_function( + models["conservative"], + config.design, + training.Y_objectives, + config, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + ), + dimension, + ) + conservative_scorer.seed(largest_pool.grid_indices, conservative_scoring.base_score) + conservative_batch = _run_refined_batch( + run_id="model_conservative", + run_family="model_variant", + core_run=True, + pool=largest_pool, + pool_hash=primary_sobol.prefix_hashes[largest_size], + master_base_scores=conservative_scoring.base_score, + shared_grid_scorer=conservative_scorer, + config=config, + mode_settings=mode_settings, + training=training, + model_variant="conservative", + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=conservative_scoring, + ) + + bound_none_scoring = scoring_by_policy[(config.primary_beta, "none")] + bound_none_batch = _run_refined_batch( + run_id="bound_none", + run_family="bounded_utility", + core_run=True, + pool=largest_pool, + pool_hash=primary_sobol.prefix_hashes[largest_size], + master_base_scores=bound_none_scoring.base_score, + shared_grid_scorer=primary_scorers[(config.primary_beta, "none")], + config=config, + mode_settings=mode_settings, + training=training, + model_variant="default_current", + beta=config.primary_beta, + bound_policy="none", + penalty_label=config.primary_penalty_variant, + scoring=bound_none_scoring, + ) + beta_batches: list[StudyBatch] = [] + for beta in (1.0, 9.0): + scored = scoring_by_policy[(beta, config.primary_bound_policy)] + beta_batches.append( + _run_refined_batch( + run_id=f"beta_{int(beta)}", + run_family="beta", + core_run=True, + pool=largest_pool, + pool_hash=primary_sobol.prefix_hashes[largest_size], + master_base_scores=scored.base_score, + shared_grid_scorer=primary_scorers[(beta, config.primary_bound_policy)], + config=config, + mode_settings=mode_settings, + training=training, + model_variant="default_current", + beta=beta, + bound_policy=config.primary_bound_policy, + penalty_label=config.primary_penalty_variant, + scoring=scored, + ) + ) + phase_runtimes["secondary_model_bound_beta_studies"] = ( + perf_counter() - phase_started + ) + + phase_started = perf_counter() + baseline_optima = np.asarray( + [anchor.refined_grid_index for anchor in baseline_batch.refinement.anchors], + dtype=np.int64, + ) + converged_pool = _pool_with_rows(largest_pool, baseline_optima, config.design) + converged_pool_hash = _canonical_json_sha256(converged_pool.grid_indices.tolist()) + converged_scores = primary_scorers[ + (config.primary_beta, config.primary_bound_policy) + ](converged_pool.grid_indices) + penalty_batches: list[StudyBatch] = [] + penalty_core_labels = { + "no_soft_hard_0_15", + "radius_0_15", + "radius_0_35", + } + for penalty in config.penalty_variants: + penalty_selection_started = perf_counter() + selection = _static_select( + converged_pool, + converged_scores, + q=STEP2C_BATCH_SIZE, + penalty=penalty, + observed_norm=training.X_norm_all, + ) + penalty_selection_runtime = perf_counter() - penalty_selection_started + penalty_batches.append( + _study_batch_from_static( + run_id=f"penalty_{penalty.label}", + run_family="local_penalty", + core_run=penalty.label in penalty_core_labels, + selection=selection, + model_variant="default_current", + pool=converged_pool, + pool_hash=converged_pool_hash, + beta=config.primary_beta, + bound_policy=config.primary_bound_policy, + penalty_label=penalty.label, + scoring=baseline_scoring, + selection_runtime_seconds=penalty_selection_runtime, + ) + ) + phase_runtimes["local_penalty_isolation"] = perf_counter() - phase_started + + core_batches = [ + *nested_batches, + *secondary_batches, + conservative_batch, + bound_none_batch, + *beta_batches, + *[batch for batch in penalty_batches if batch.core_run], + ] + if len(core_batches) != 13 or len({batch.run_id for batch in core_batches}) != 13: + raise RuntimeError( + "The Step 2C core one-factor run registry must contain 13 runs." + ) + all_batches_by_id = {batch.run_id: batch for batch in core_batches} + for batch in penalty_batches: + all_batches_by_id.setdefault(batch.run_id, batch) + all_batches = list(all_batches_by_id.values()) + + phase_started = perf_counter() + study_summary = _study_summary(all_batches) + candidate_rows = _batch_candidate_rows( + all_batches, models=models, config=config, training=training + ) + nested_candidates = pd.concat( + [ + candidate_rows[ + candidate_rows["run_id"].isin( + [batch.run_id for batch in nested_batches] + ) + ], + _batch_candidate_rows( + nested_raw_batches, + models=models, + config=config, + training=training, + ), + ], + ignore_index=True, + sort=False, + ) + nested_candidates["search_stage"] = np.where( + nested_candidates["run_id"].str.startswith("nested_raw_"), + "raw_pool_selection", + "locally_refined_selection", + ) + refinement_trace = _refinement_trace_frame(all_batches) + nested_convergence = _nested_convergence_table(nested_batches, nested_raw_batches) + nested_convergence["shared_largest_pool_moment_runtime_seconds"] = ( + primary_moment_runtime + ) + nested_convergence["shared_largest_pool_hvi_runtime_seconds"] = scoring_runtime[ + f"default_beta_{config.primary_beta:g}_{config.primary_bound_policy}" + ] + nested_convergence["prefix_scores_reused_from_exact_largest_prefix"] = True + scramble_comparison = _sobol_scramble_table(baseline_batch, secondary_batches) + bounded_comparison = _bounded_utility_table( + baseline_batch, + bound_none_batch, + baseline_scoring, + candidate_rows, + ) + penalty_tradeoff = _penalty_tradeoff_table( + penalty_batches, observed_norm=training.X_norm_all + ) + beta_robustness = _beta_robustness_table(baseline_batch, beta_batches) + model_summary, loocv_predictions, model_hyperparameters, model_fit_warnings = ( + _model_validation_frames(validation_results) + ) + model_summary = _augment_model_summary_with_candidate_diagnostics( + model_summary, + candidate_rows, + training.Y_objectives, + config.objective_bounds, + ) + hyperparameter_stability = _hyperparameter_stability_summary(model_hyperparameters) + phase_runtimes["diagnostic_tables"] = perf_counter() - phase_started + + phase_started = perf_counter() + influence_size = ( + config.influence_pool_size + if config.influence_pool_size in primary_sobol.pools + else max( + size + for size in primary_sobol.accepted_sizes + if size <= min(config.influence_pool_size, largest_size) + ) + ) + common_pool = primary_sobol.pools[influence_size] + common_pool_hash = primary_sobol.prefix_hashes[influence_size] + influence, influence_batches, influence_candidates, influence_predictions = ( + _run_observation_influence( + validation=validations_by_name["default_current"], + common_pool=common_pool, + common_pool_hash=common_pool_hash, + full_pool_scores=baseline_scoring.base_score[:influence_size], + config=config, + mode_settings=mode_settings, + training=training, + ) + ) + influence_runtime_rows = [] + influence_batch_by_id = { + int(batch.run_id.rsplit("_", 1)[1]): batch + for batch in influence_batches + if batch.run_id.startswith("influence_omit_") + } + for sample_id in mode_settings.omitted_sample_ids: + fit = validations_by_name["default_current"].loocv.fold_records[sample_id] + batch = influence_batch_by_id[int(sample_id)] + influence_runtime_rows.append( + { + "omitted_sample_id": int(sample_id), + "fit_runtime_seconds": fit.fit_runtime_seconds, + "fit_warning_count": len(fit.warnings), + "proposal_runtime_seconds": batch.proposal_runtime_seconds, + "refinement_anchor_count": len(batch.refinement.anchors), + "refinement_trace_row_count": len(batch.refinement.trace), + } + ) + influence_boundary = _influence_boundary_summary(influence_batches) + influence_summary = _stamp_frame( + influence.summary_frame() + .merge( + pd.DataFrame(influence_runtime_rows), + on="omitted_sample_id", + how="left", + validate="one_to_one", + ) + .merge( + influence_boundary, + on="omitted_sample_id", + how="left", + validate="one_to_one", + ) + ) + boundary_enrichment = _boundary_enrichment_table( + largest_pool, + baseline_scoring.base_score, + baseline_batch, + [*core_batches, *influence_batches], + ) + phase_runtimes["observation_influence"] = perf_counter() - phase_started + + phase_started = perf_counter() + core_candidate_rows = candidate_rows[ + candidate_rows["run_id"].isin([batch.run_id for batch in core_batches]) + ].reset_index(drop=True) + core_registry = {batch.run_id: batch.run_family for batch in core_batches} + region_result: RobustRegionResult = cluster_candidate_regions( + core_candidate_rows, + distance_threshold=config.region_threshold, + core_run_registry=core_registry, + ) + region_result = RobustRegionResult( + regions=_augment_regions_with_control_influence_correspondence( + region_result.regions, + influence_batches=influence_batches, + control_sample_id=int(config.control_sample_ids[0]), + threshold=config.region_threshold, + ), + membership=region_result.membership, + threshold=region_result.threshold, + region_count=region_result.region_count, + ) + region_sensitivity = { + f"threshold_{threshold:.2f}": cluster_candidate_regions( + core_candidate_rows, + distance_threshold=threshold, + core_run_registry=core_registry, + ).region_count + for threshold in config.region_sensitivity_thresholds + } + robust_regions = region_result.regions.copy() + robust_membership = region_result.membership.copy() + shortlist = select_robust_shortlist( + region_result, + minimum_count=config.shortlist_min, + maximum_count=config.shortlist_max, + minimum_normalized_distance=0.15, + ) + for dimension, name in enumerate(D2D_INPUT_COLUMNS): + source = f"medoid_phys_{dimension}" + if source in shortlist: + shortlist[name] = shortlist[source] + shortlist, medoid_influence_predictions = ( + _augment_shortlist_model_and_influence_diagnostics( + shortlist, + validations_by_name=validations_by_name, + config=config, + mode_settings=mode_settings, + training=training, + ) + ) + full_batch_predictions = influence_predictions.copy() + full_batch_predictions["prediction_location_kind"] = "full_model_batch" + full_batch_predictions["location_id"] = [ + f"FULL-C{int(index) + 1:02d}" + for index in full_batch_predictions["candidate_index"] + ] + influence_predictions = _stamp_frame( + pd.concat( + [full_batch_predictions, medoid_influence_predictions], + ignore_index=True, + sort=False, + ) + ) + medoid_influence_summary = ( + medoid_influence_predictions.groupby("omitted_sample_id", sort=True) + .agg( + medoid_mean_normalized_prediction_mean_change=( + "normalized_absolute_mean_delta", + "mean", + ), + medoid_max_normalized_prediction_mean_change=( + "normalized_absolute_mean_delta", + "max", + ), + medoid_mean_normalized_prediction_std_change=( + "normalized_absolute_std_delta", + "mean", + ), + medoid_max_normalized_prediction_std_change=( + "normalized_absolute_std_delta", + "max", + ), + ) + .reset_index() + ) + influence_summary = _stamp_frame( + influence_summary.drop( + columns=[ + "debug_only", + "approved_for_experiment", + "approved_for_production", + "candidate_status", + ] + ).merge( + medoid_influence_summary, + on="omitted_sample_id", + how="left", + validate="one_to_one", + ) + ) + family_count_column = ( + "distinct_nonbaseline_family_count" + if "distinct_nonbaseline_family_count" in shortlist + else "distinct_family_count" + ) + consensus_candidates = shortlist[ + ( + pd.to_numeric(shortlist[family_count_column], errors="coerce") + >= config.consensus_family_coverage_min + ) + & shortlist["all_grid_valid"].astype(bool) + & shortlist["all_hard_distance_valid"].astype(bool) + ].head(config.consensus_batch_size) + largest_match: RegionalBatchComparison = regional_match_batches( + nested_batches[-2].X_norm, + nested_batches[-1].X_norm, + thresholds=config.regional_thresholds, + ) + consensus = evaluate_consensus_criteria( + robust_regions, + consensus_candidates, + largest_two_regional_matches_within_0_15=( + largest_match.regional_match_count(0.15) + ), + largest_two_mean_matched_distance=largest_match.mean_matched_distance, + nested_match_minimum=config.consensus_nested_match_min, + mean_distance_maximum=config.consensus_mean_distance_max, + minimum_family_coverage=config.consensus_family_coverage_min, + consensus_batch_size=config.consensus_batch_size, + required_minimum_distance=0.15, + full_mode_eligible=( + mode == "full" + and mode_settings.nested_unique_sizes == config.nested_pool_sizes + and mode_settings.omitted_sample_ids == config.influence_sample_ids + ), + ) + phase_runtimes["robust_regions_and_consensus"] = perf_counter() - phase_started + + source_hash_after_compute = sha256_file(workbook) + source_mtime_after_compute = workbook.stat().st_mtime_ns + if ( + source_hash_after_compute != source_hash_before + or source_mtime_after_compute != source_mtime_before + ): + raise RuntimeError("The private workbook changed during Step 2C computation.") + + pool_manifest = _nested_pool_manifest( + [primary_sobol, *secondary_sobol], + role_by_seed={ + config.primary_sobol_seed: "primary_nested", + **{seed: "secondary_scramble" for seed in config.secondary_sobol_seeds}, + }, + ) + training_manifest = _stamp_frame(training.manifest_frame()) + score_validation = _stamp_frame(validation.frame) + workbook_audit = asdict(audit) + workbook_audit.update( + { + "workbook_path": str(workbook), + "workbook_sha256_before": source_hash_before, + "workbook_sha256_after_compute": source_hash_after_compute, + "workbook_mtime_ns_before": source_mtime_before, + "workbook_mtime_ns_after_compute": source_mtime_after_compute, + "source_workbook_modified": False, + "known_uniformity_score_mismatch": True, + "control_off_grid_exception_retained": True, + } + ) + if len(config.control_sample_ids) != 1: + raise RuntimeError("Step 2C requires exactly one configured control sample.") + control_sample_id = int(config.control_sample_ids[0]) + control_influence = influence.rank_for_sample(control_sample_id) + penalty_classifications = dict( + zip( + penalty_tradeoff["penalty_label"], + penalty_tradeoff["penalty_activity_classification"], + ) + ) + classification_priority = { + ( + "implemented but effectively inactive because base optima are already " + "separated" + ): 0, + "active but only modestly changes diversity": 1, + "active and materially changes diversity": 2, + } + soft_penalty_classifications = { + label: value + for label, value in penalty_classifications.items() + if str(label).startswith("radius_") + } + if not soft_penalty_classifications: + raise RuntimeError("The local soft-penalty study produced no radius variants.") + overall_penalty_interpretation = max( + soft_penalty_classifications.values(), + key=lambda value: classification_priority[str(value)], + ) + hard_spacing_interpretation = penalty_classifications["no_soft_hard_0_15"] + manifest: dict[str, Any] = { + "schema_version": "d2d-step2c-robustness-run-v1", + "method_version": STEP2C_METHOD_VERSION, + "mode": mode, + "input_data_kind": input_data_kind, + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": D2D_DEBUG_WATERMARK, + "real_r2_proposal_generated": False, + "workbook_writeback_performed": False, + "git_commit": git_commit, + "git_dirty": git_dirty, + "git_status_at_start": git_status, + "step2b_checkpoint": "1c6a83a9dfd7e6ed5e69ce66765b8dc0fd8a86af", + "workbook_path": str(workbook), + "workbook_sha256_before": source_hash_before, + "workbook_sha256_after": source_hash_after_compute, + "workbook_mtime_ns_before": source_mtime_before, + "workbook_mtime_ns_after": source_mtime_after_compute, + "source_workbook_modified": False, + "config_path": str(config.config_path), + "config_sha256": config.config_sha256, + "resolved_config_hash": config.resolved_config_hash, + "objective_order": D2D_OBJECTIVE_NAMES, + "objective_source_columns": D2D_OBJECTIVE_COLUMNS, + "reference_point": config.reference_point_utility, + "objective_bounds": config.objective_bounds, + "moment_method": "analytic_identity", + "analytic_mc_comparison": analytic_mc_payload, + "primary_analytic_moment_runtime_seconds": primary_moment_runtime, + "scoring_runtime_seconds": scoring_runtime, + "secondary_scoring_runtime_seconds": secondary_moment_runtime, + "conservative_moment_runtime_seconds": conservative_moment_runtime, + "conservative_score_runtime_seconds": conservative_score_runtime, + "sobol_seeds": [config.primary_sobol_seed, *config.secondary_sobol_seeds], + "nested_pool_sizes": mode_settings.nested_unique_sizes, + "pool_prefix_hashes": { + str(result.scramble_seed): result.prefix_hashes + for result in [primary_sobol, *secondary_sobol] + }, + "local_refinement": { + "anchors_per_selection_step": mode_settings.anchors_per_selection_step, + "max_sweeps": config.refinement_max_sweeps, + "improvement_tolerance": config.refinement_tolerance, + "coordinate_values": "all_allowed_grid_values", + }, + "beta_values": config.beta_values, + "bound_policies": config.bound_policies, + "local_penalty_variants": [asdict(value) for value in config.penalty_variants], + "local_penalty_activity_by_variant": penalty_classifications, + "local_penalty_overall_interpretation": overall_penalty_interpretation, + "hard_spacing_interpretation": hard_spacing_interpretation, + "model_variants": [asdict(result.variant) for result in validation_results], + "influence_common_pool_hash": common_pool_hash, + "influence_common_pool_size": influence_size, + "influence_omitted_sample_ids": mode_settings.omitted_sample_ids, + "control_sample_id": control_sample_id, + "control_influence_rank": control_influence.influence_rank, + "control_influence_percentile": control_influence.influence_percentile, + "robust_region_clustering": "agglomerative_complete_link", + "robust_region_threshold": config.region_threshold, + "robust_region_sensitivity_counts": region_sensitivity, + "robust_region_count": region_result.region_count, + "shortlist_count": int(shortlist.shape[0]), + "consensus_passed": consensus.passed, + "consensus_checks": consensus.checks, + "consensus_observed": consensus.observed, + "stability_criteria": { + "consensus_batch_size": config.consensus_batch_size, + "regional_match_threshold": 0.15, + "largest_two_nested_regional_match_minimum": ( + config.consensus_nested_match_min + ), + "largest_two_nested_mean_matched_distance_maximum": ( + config.consensus_mean_distance_max + ), + "minimum_nonbaseline_core_family_coverage": ( + config.consensus_family_coverage_min + ), + "required_pairwise_minimum_distance": 0.15, + "require_finite_and_bounded": True, + "require_unique_and_on_grid": True, + "require_debug_only_and_approval_false": True, + }, + "known_uniformity_score_mismatch": True, + "uniformity_warning_count": validation.uniformity_warning_count, + "control_assumption": config.control_measurement_provenance_assumption, + "off_grid_control_exception": [ + asdict(value) for value in config.off_grid_exceptions + ], + "runtime_versions": _runtime_versions(), + "hardware": _hardware_summary(), + "phase_runtime_seconds": phase_runtimes, + "runtime_seconds_before_output": perf_counter() - total_started, + } + tables: dict[str, pd.DataFrame] = { + "score_validation.csv": score_validation, + "training_row_manifest.csv": training_manifest, + "model_validation_summary.csv": model_summary, + "loocv_predictions_long.csv": loocv_predictions, + "model_hyperparameters.csv": model_hyperparameters, + "model_hyperparameter_stability_summary.csv": hyperparameter_stability, + "model_fit_warnings.csv": model_fit_warnings, + "nested_pool_manifest.csv": pool_manifest, + "nested_pool_convergence_summary.csv": nested_convergence, + "nested_pool_candidates_long.csv": _stamp_frame(nested_candidates), + "sobol_scramble_comparison.csv": scramble_comparison, + "local_refinement_trace.csv": refinement_trace, + "bounded_utility_comparison.csv": bounded_comparison, + "bounded_utility_candidates_long.csv": _stamp_frame( + robust_membership[ + robust_membership["run_id"].isin( + [baseline_batch.run_id, bound_none_batch.run_id] + ) + ].reset_index(drop=True) + ), + "local_penalty_tradeoff.csv": penalty_tradeoff, + "beta_robustness.csv": beta_robustness, + "boundary_enrichment.csv": boundary_enrichment, + "observation_influence_summary.csv": influence_summary, + "observation_influence_candidates_long.csv": influence_candidates, + "observation_influence_predictions.csv": influence_predictions, + "robust_regions.csv": _stamp_frame(robust_regions), + "r1_robust_shortlist_debug.csv": _stamp_frame(shortlist), + "study_run_summary.csv": study_summary, + "study_candidates_long.csv": candidate_rows, + "robust_region_membership.csv": _stamp_frame(robust_membership), + "analytic_mc_comparison.csv": analytic_mc_frame, + } + artifact_hashes = _write_step2c_bundle( + destination=destination, + config=config, + workbook=workbook, + workbook_audit=workbook_audit, + tables=tables, + manifest=manifest, + consensus=consensus, + shortlist=shortlist, + consensus_candidates=consensus_candidates, + loocv_predictions=loocv_predictions, + model_hyperparameters=model_hyperparameters, + nested_convergence=nested_convergence, + bounded_scoring=baseline_scoring, + penalty_tradeoff=penalty_tradeoff, + influence_summary=influence_summary, + influence_candidates=influence_candidates, + influence_predictions=influence_predictions, + boundary_enrichment=boundary_enrichment, + robust_regions=robust_regions, + robust_membership=robust_membership, + create_portable_zip=create_archives, + overwrite=overwrite, + total_started=total_started, + ) + source_hash_after = sha256_file(workbook) + source_mtime_after = workbook.stat().st_mtime_ns + if ( + source_hash_after != source_hash_before + or source_mtime_after != source_mtime_before + ): + raise RuntimeError( + "The private workbook changed while writing Step 2C outputs." + ) + return Step2CRobustnessResult( + output_dir=destination, + mode=mode, + run_manifest=manifest, + consensus=consensus, + robust_regions=tables["robust_regions.csv"], + shortlist=tables["r1_robust_shortlist_debug.csv"], + influence_summary=influence_summary, + artifact_hashes=artifact_hashes, + ) + + +def run_d2d_step2c_robustness( + workbook_path: str | Path, + config_path: str | Path, + output_dir: str | Path, + *, + mode: str = "full", + overwrite: bool = False, + create_portable_zip: bool | None = None, +) -> Step2CRobustnessResult: + """Run the private-workbook Step 2C audit through its pinned contract. + + The public campaign command always resolves the tracked fail-closed config + itself. Sanitized CI exercises use a separate helper and cannot redirect this + command to an alternate workbook. + """ + config = load_step2c_config(config_path) + return _run_d2d_step2c_robustness_resolved( + workbook_path, + config, + output_dir, + mode=mode, + overwrite=overwrite, + create_portable_zip=create_portable_zip, + input_data_kind="private_pinned_workbook", + ) + + +__all__ = [ + "STEP2C_METHOD_VERSION", + "Step2CRobustnessResult", + "StudyBatch", + "_run_d2d_step2c_robustness_resolved", + "run_d2d_step2c_robustness", +] diff --git a/src/mobo_kit/d2d_step2c_synthetic.py b/src/mobo_kit/d2d_step2c_synthetic.py new file mode 100644 index 0000000..7d0afae --- /dev/null +++ b/src/mobo_kit/d2d_step2c_synthetic.py @@ -0,0 +1,349 @@ +"""Sanitized, test-only end-to-end fixture for the Step 2C fast pipeline.""" + +from __future__ import annotations + +from copy import deepcopy +from dataclasses import replace +import hashlib +import json +import math +from pathlib import Path +from typing import Sequence + +import numpy as np +from openpyxl import Workbook +from openpyxl.utils.cell import range_boundaries +import yaml + +from .d2d_campaign import D2D_WORKBOOK_INPUT_COLUMNS, sha256_file +from .d2d_step2c_config import ( + ExecutionModeSettings, + ResolvedStep2CConfig, + load_step2c_config, +) +from .d2d_step2c_robustness import ( + Step2CRobustnessResult, + _run_d2d_step2c_robustness_resolved, +) + + +SYNTHETIC_STEP2C_HEADERS = ( + "Sample number", + *D2D_WORKBOOK_INPUT_COLUMNS, + "Coverage", + "Uniformity", + "1 - Uniformity", + "Phase purity", + "PL - Implied Voc (Max)", + "Photoconductance (Max)", + "Log10 (Photoconductance (Max) x PL - Implied Voc (Max))", + "T1", + "T2", + "T3", + "T4", + "T anom", + "Thickness (avg)", + "Normalized thickness (sigma = 250)", + "Uniformity score", + "Optoelectronic score", + "Thickness score", + "Stability score?", + "Total combination - addition", + "Total combination - multiplied", + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", +) + + +def _canonical_hash(value: dict) -> str: + payload = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest().upper() + + +def _synthetic_off_grid_exception( + config: ResolvedStep2CConfig, +): + """Return a deterministic, non-campaign off-grid fixture exception.""" + + if len(config.off_grid_exceptions) != 1: + raise ValueError("Synthetic Step 2C requires one inherited off-grid exception.") + inherited = config.off_grid_exceptions[0] + dimension = tuple(config.design.names).index(inherited.input_name) + grid = np.asarray(config.design.var_array[dimension], dtype=float) + observed = float(inherited.observed_value) + if np.any(np.isclose(observed, grid, rtol=0.0, atol=1.0e-12)): + raise RuntimeError("Synthetic sentinel unexpectedly lies on the input grid.") + return replace( + inherited, + observed_value=observed, + reason="sanitized synthetic off-grid fixture", + ) + + +def _synthetic_row(config: ResolvedStep2CConfig, sample_index: int) -> list[object]: + sample_id = config.expected_sample_ids[sample_index] + physical: list[float] = [] + for dimension, grid in enumerate(config.design.var_array): + grid_rng = np.random.default_rng(20_260_300 + dimension) + grid_index = int(grid_rng.integers(0, len(grid), size=15)[sample_index]) + physical.append(float(grid[grid_index])) + exception = _synthetic_off_grid_exception(config) + if sample_id == exception.sample_id: + dimension = tuple(config.design.names).index(exception.input_name) + physical[dimension] = exception.observed_value + + normalized_physical = np.asarray( + [ + (value - float(np.min(grid))) / (float(np.max(grid)) - float(np.min(grid))) + for value, grid in zip(physical, config.design.var_array, strict=True) + ], + dtype=float, + ) + # Smooth public-CI responses keep every strict leave-one-out GP fold + # deterministic across supported platforms. + smooth_response_weights = np.asarray( + [0.31, 0.23, 0.17, 0.11, 0.07, 0.05, 0.03, 0.02, 0.008, 0.002], + dtype=float, + ) + response_weight_total = float(np.sum(smooth_response_weights)) + + response_rng = np.random.default_rng(73_000 + sample_index) + coverage = 0.68 + 0.25 * float(response_rng.random()) + one_minus_uniformity = 0.45 + 0.40 * float(response_rng.random()) + phase_purity = 0.70 + 0.25 * float(response_rng.random()) + uniformity_calculated = coverage * one_minus_uniformity * phase_purity + # Deliberately retain the campaign's warning-only mismatch in sanitized form. + uniformity_supplied = ( + 0.20 + + 0.70 + * float(normalized_physical @ smooth_response_weights[::-1]) + / response_weight_total + ) + + implied_voc = 0.60 + 0.30 * float(response_rng.random()) + optoelectronic = ( + 1.50 + + float(normalized_physical @ smooth_response_weights) + + 0.08 * math.sin(math.pi * normalized_physical[0]) + + 0.04 * normalized_physical[1] * normalized_physical[2] + ) + photoconductance = (10.0**optoelectronic) / implied_voc + + thickness_target = ( + 0.55 + + 0.35 + * float(normalized_physical @ smooth_response_weights) + / response_weight_total + ) + thickness_average = 650.0 + 250.0 * math.sqrt(-math.log(thickness_target)) + thickness = math.exp(-(((thickness_average - 650.0) / 250.0) ** 2)) + thickness_values = [ + thickness_average - 6.0, + thickness_average - 2.0, + thickness_average + 2.0, + thickness_average + 6.0, + ] + return [ + sample_id, + *physical, + coverage, + 1.0 - one_minus_uniformity, + one_minus_uniformity, + phase_purity, + implied_voc, + photoconductance, + optoelectronic, + *thickness_values, + None, + thickness_average, + thickness, + uniformity_supplied, + optoelectronic, + thickness, + None, + None, + None, + None, + None, + None, + None, + ] + + +def write_sanitized_step2c_workbook( + path: str | Path, + config: ResolvedStep2CConfig, + *, + overwrite: bool = False, +) -> Path: + """Create a formula-free synthetic v3 workbook with no private recipes.""" + destination = Path(path).resolve() + if destination.exists() and not overwrite: + raise FileExistsError(f"Synthetic Step 2C workbook exists: {destination}.") + destination.parent.mkdir(parents=True, exist_ok=True) + workbook = Workbook() + worksheet = workbook.active + worksheet.title = config.base.workbook_sheet + worksheet.append(list(SYNTHETIC_STEP2C_HEADERS)) + for sample_index in range(15): + row = _synthetic_row(config, sample_index) + if len(row) != len(SYNTHETIC_STEP2C_HEADERS): + raise RuntimeError("Internal synthetic Step 2C row shape mismatch.") + worksheet.append(row) + _, _, expected_max_column, expected_max_row = range_boundaries( + config.base.expected_content_range + ) + if expected_max_column != len(SYNTHETIC_STEP2C_HEADERS): + raise RuntimeError("Synthetic headers do not match the adapter column count.") + worksheet.cell(row=expected_max_row, column=expected_max_column).value = ( + "SANITIZED SYNTHETIC CI FIXTURE - NO PRIVATE RECIPE" + ) + workbook.save(destination) + workbook.close() + return destination + + +def _synthetic_config( + config: ResolvedStep2CConfig, + *, + repository_root: Path, + workbook: Path, + nested_unique_sizes: Sequence[int], + anchors_per_selection_step: int, + omitted_sample_ids: Sequence[int], + mc_comparison_samples: int, +) -> ResolvedStep2CConfig: + sizes = tuple(int(value) for value in nested_unique_sizes) + omissions = tuple(int(value) for value in omitted_sample_ids) + if len(sizes) != 4 or sizes != tuple(sorted(set(sizes))) or sizes[0] <= 0: + raise ValueError( + "Synthetic nested_unique_sizes must be four increasing values." + ) + if anchors_per_selection_step <= 0 or mc_comparison_samples <= 0: + raise ValueError("Synthetic anchor and MC counts must be positive.") + if not omissions or any( + value not in config.expected_sample_ids for value in omissions + ): + raise ValueError( + "Synthetic omission IDs must be a nonempty subset of configured samples." + ) + workbook_hash = sha256_file(workbook) + relative_workbook = workbook.relative_to(repository_root).as_posix() + fast = ExecutionModeSettings( + nested_unique_sizes=sizes, + anchors_per_selection_step=int(anchors_per_selection_step), + omitted_sample_ids=omissions, + mc_comparison_samples=int(mc_comparison_samples), + ) + raw = deepcopy(config.raw) + raw["workbook"]["path"] = relative_workbook + raw["workbook"]["expected_sha256"] = workbook_hash.lower() + synthetic_exception = _synthetic_off_grid_exception(config) + raw["r0"] = { + "inherit_from_base": True, + "synthetic_off_grid_override": { + "sample_id": synthetic_exception.sample_id, + "field": synthetic_exception.input_name, + "value": synthetic_exception.observed_value, + }, + } + raw["execution_modes"]["fast"] = { + "nested_unique_sizes": list(sizes), + "anchors_per_selection_step": int(anchors_per_selection_step), + "omitted_sample_ids": list(omissions), + "mc_comparison_samples": int(mc_comparison_samples), + } + base = replace( + config.base, + expected_workbook_sha256=workbook_hash, + off_grid_exceptions=(synthetic_exception,), + ) + modes = dict(config.execution_modes) + modes["fast"] = fast + return replace( + config, + raw=raw, + resolved_config_hash=_canonical_hash(raw), + base=base, + workbook_relative_path=relative_workbook, + workbook_expected_sha256=workbook_hash, + resolved_off_grid_exceptions=(synthetic_exception,), + execution_modes=modes, + ) + + +def run_synthetic_step2c_fast_ci( + config_path: str | Path, + output_dir: str | Path, + *, + overwrite: bool = False, + create_portable_zip: bool = True, + nested_unique_sizes: Sequence[int] = (64, 128, 256, 512), + anchors_per_selection_step: int = 2, + omitted_sample_ids: Sequence[int] | None = None, + mc_comparison_samples: int = 128, +) -> Step2CRobustnessResult: + """Exercise the full fast orchestration on a sanitized generated workbook. + + The synthetic source and output remain under the ignored Step 2C output root. + This helper cannot produce a consensus batch because it always runs in fast mode. + """ + repository_root = Path(__file__).resolve().parents[2] + config = load_step2c_config(config_path) + resolved_omissions = ( + (config.expected_sample_ids[0],) + if omitted_sample_ids is None + else tuple(omitted_sample_ids) + ) + destination = Path(output_dir).resolve() + allowed_root = (repository_root / config.output_root).resolve() + if allowed_root not in destination.parents: + raise ValueError( + "Synthetic Step 2C output must be a child of the configured ignored root." + ) + source_dir = allowed_root / "synthetic_ci_sources" + source = source_dir / f"{destination.name}.xlsx" + write_sanitized_step2c_workbook(source, config, overwrite=overwrite) + resolved = _synthetic_config( + config, + repository_root=repository_root, + workbook=source, + nested_unique_sizes=nested_unique_sizes, + anchors_per_selection_step=anchors_per_selection_step, + omitted_sample_ids=resolved_omissions, + mc_comparison_samples=mc_comparison_samples, + ) + synthetic_config_path = source_dir / f"{destination.name}_resolved_config.yaml" + if synthetic_config_path.exists() and not overwrite: + raise FileExistsError( + f"Synthetic resolved config already exists: {synthetic_config_path}." + ) + synthetic_config_path.write_text( + yaml.safe_dump(resolved.raw, sort_keys=False), + encoding="utf-8", + ) + resolved = replace( + resolved, + config_path=synthetic_config_path.resolve(), + config_sha256=sha256_file(synthetic_config_path), + ) + return _run_d2d_step2c_robustness_resolved( + source, + resolved, + destination, + mode="fast", + overwrite=overwrite, + create_portable_zip=create_portable_zip, + input_data_kind="sanitized_synthetic_ci", + ) + + +__all__ = [ + "SYNTHETIC_STEP2C_HEADERS", + "run_synthetic_step2c_fast_ci", + "write_sanitized_step2c_workbook", +] diff --git a/src/mobo_kit/design.py b/src/mobo_kit/design.py index bb8b7d4..6274cb6 100644 --- a/src/mobo_kit/design.py +++ b/src/mobo_kit/design.py @@ -4,14 +4,107 @@ from typing import List, Optional, Dict, Any import numpy as np -def make_linspace(start: float, stop: float, step: float, decimals: int = 6) -> np.ndarray: - """Create a grid of values from start to stop with given step size.""" - num_points = int(round((stop - start) / step)) + 1 - return np.round(np.linspace(start, stop, num_points), decimals) + +def _finite_float(value: Any, *, field: str, input_name: str) -> float: + """Return ``value`` as a finite float with a campaign-friendly error.""" + if isinstance(value, (bool, np.bool_)): + raise ValueError( + f"Input '{input_name}' field '{field}' must be a finite number, not bool." + ) + try: + number = float(value) + except (TypeError, ValueError) as exc: + raise ValueError( + f"Input '{input_name}' field '{field}' must be a finite number; " + f"got {value!r}." + ) from exc + if not np.isfinite(number): + raise ValueError( + f"Input '{input_name}' field '{field}' must be finite; got {value!r}." + ) + return number + + +def _validate_decimals(value: Any, *, input_name: str) -> int: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)): + raise ValueError( + f"Input '{input_name}' field 'decimals' must be a non-negative integer; " + f"got {value!r}." + ) + decimals = int(value) + if decimals < 0: + raise ValueError( + f"Input '{input_name}' field 'decimals' must be a non-negative integer; " + f"got {decimals}." + ) + return decimals + + +def make_linspace( + start: float, + stop: float, + step: float, + decimals: int = 6, +) -> np.ndarray: + """Create an endpoint-aligned grid with the requested step. + + ``numpy.linspace`` can silently change the requested spacing when the endpoint + is not aligned. Campaign inputs must instead satisfy + ``stop == start + k * step`` (within floating-point tolerance). + """ + input_name = "" + start_f = _finite_float(start, field="start", input_name=input_name) + stop_f = _finite_float(stop, field="stop", input_name=input_name) + step_f = _finite_float(step, field="step", input_name=input_name) + decimals_i = _validate_decimals(decimals, input_name=input_name) + + if step_f <= 0: + raise ValueError(f"Grid step must be > 0; got {step_f}.") + if stop_f < start_f: + raise ValueError( + f"Grid stop must be greater than or equal to start; " + f"got start={start_f}, stop={stop_f}." + ) + + span = stop_f - start_f + if span == 0: + return np.asarray([round(start_f, decimals_i)], dtype=float) + + interval_count = int(round(span / step_f)) + if interval_count < 1: + raise ValueError( + "Grid endpoint is not aligned with step: " + f"start={start_f}, stop={stop_f}, step={step_f}. " + "Require stop = start + k * step for an integer k." + ) + aligned_span = interval_count * step_f + alignment_atol = max(1e-12, abs(step_f) * 1e-9) + if not np.isclose(span, aligned_span, rtol=0.0, atol=alignment_atol): + raise ValueError( + "Grid endpoint is not aligned with step: " + f"start={start_f}, stop={stop_f}, step={step_f}. " + "Require stop = start + k * step for an integer k." + ) + + grid = np.round( + start_f + step_f * np.arange(interval_count + 1, dtype=float), + decimals_i, + ) + grid[0] = round(start_f, decimals_i) + grid[-1] = round(stop_f, decimals_i) + + if np.unique(grid).size != grid.size: + raise ValueError( + f"Rounding the grid to {decimals_i} decimals creates duplicate values. " + "Increase 'decimals' or use a larger step." + ) + return grid + @dataclass class InputSpec: """Specification for an input parameter with grid-based discretization.""" + name: str start: float stop: float @@ -19,53 +112,120 @@ class InputSpec: unit: Optional[str] = None decimals: int = 6 + def __post_init__(self) -> None: + if not isinstance(self.name, str) or not self.name.strip(): + raise ValueError("Input name must be a non-empty string.") + self.name = self.name.strip() + self.start = _finite_float(self.start, field="start", input_name=self.name) + self.stop = _finite_float(self.stop, field="stop", input_name=self.name) + self.step = _finite_float(self.step, field="step", input_name=self.name) + self.decimals = _validate_decimals(self.decimals, input_name=self.name) + if self.step <= 0: + raise ValueError( + f"Input '{self.name}' field 'step' must be > 0; got {self.step}." + ) + if self.stop < self.start: + raise ValueError( + f"Input '{self.name}' requires stop >= start; " + f"got start={self.start}, stop={self.stop}." + ) + # Constructing the grid here validates endpoint alignment and precision. + try: + make_linspace(self.start, self.stop, self.step, self.decimals) + except ValueError as exc: + raise ValueError(f"Invalid grid for input '{self.name}': {exc}") from exc + + def build_input_spec_list(cfg_inputs: List[Dict[str, Any]]) -> List[InputSpec]: """Build InputSpec objects from config dictionary.""" + if not isinstance(cfg_inputs, list) or not cfg_inputs: + raise ValueError("Config 'inputs' must be a non-empty list.") + specs = [] - for item in cfg_inputs: - # Handle both old format (lower/upper) and new format (start/stop/step) - if 'start' in item and 'stop' in item and 'step' in item: - # New format: start/stop/step - spec = InputSpec( - name=item['name'], - unit=item.get('unit'), - start=float(item['start']), - stop=float(item['stop']), - step=float(item['step']), - decimals=item.get('decimals', 6) + seen_names = set() + for index, item in enumerate(cfg_inputs): + if not isinstance(item, dict): + raise ValueError( + f"Config input at index {index} must be a mapping; " + f"got {type(item).__name__}." + ) + missing = [key for key in ("name", "start", "stop", "step") if key not in item] + if missing: + display_name = item.get("name", f"index {index}") + raise ValueError( + f"Input '{display_name}' is missing required field(s): " + f"{', '.join(missing)}." + ) + + spec = InputSpec( + name=item["name"], + unit=item.get("unit"), + start=item["start"], + stop=item["stop"], + step=item["step"], + decimals=item.get("decimals", 6), + ) + if spec.name in seen_names: + raise ValueError( + f"Input names must be unique; duplicate name '{spec.name}'." ) - else: - raise ValueError(f"Input '{item.get('name', 'unknown')}' must have either (start, stop, step) or (lower, upper).") - + seen_names.add(spec.name) specs.append(spec) - + return specs + @dataclass class Design: """Design space specification with grid-based discretization.""" + names: List[str] units: List[Optional[str]] - lowers: np.ndarray # (D,) minimum values - uppers: np.ndarray # (D,) maximum values - steps: np.ndarray # (D,) step sizes - var_array: List[np.ndarray] # grid values for each feature - var_list: List[np.ndarray] # grid values for each feature (same as var_array for consistency) + lowers: np.ndarray # (D,) minimum values + uppers: np.ndarray # (D,) maximum values + steps: np.ndarray # (D,) step sizes + var_array: List[np.ndarray] # grid values for each feature + # Grid values for each feature (same as var_array for compatibility). + var_list: List[np.ndarray] + def build_design(specs: List[InputSpec]) -> Design: """Build a Design object from InputSpec objects.""" + if not isinstance(specs, list) or not specs: + raise ValueError("At least one InputSpec is required to build a design.") + names, units = [], [] lowers, uppers, steps = [], [], [] var_array: List[np.ndarray] = [] + seen_names = set() - for spec in specs: + for index, raw_spec in enumerate(specs): + if not isinstance(raw_spec, InputSpec): + raise TypeError( + f"Design item at index {index} must be an InputSpec; " + f"got {type(raw_spec).__name__}." + ) + # InputSpec is mutable, so revalidate a fresh copy at the boundary. + spec = InputSpec( + name=raw_spec.name, + start=raw_spec.start, + stop=raw_spec.stop, + step=raw_spec.step, + unit=raw_spec.unit, + decimals=raw_spec.decimals, + ) + if spec.name in seen_names: + raise ValueError( + f"Input names must be unique; duplicate name '{spec.name}'." + ) + seen_names.add(spec.name) names.append(spec.name) units.append(spec.unit) - + # Create grid for this parameter grid = make_linspace(spec.start, spec.stop, spec.step, spec.decimals) var_array.append(grid) - + # Store bounds and step lowers.append(float(grid.min())) uppers.append(float(grid.max())) @@ -81,33 +241,44 @@ def build_design(specs: List[InputSpec]) -> Design: var_list=var_array, # For backward compatibility ) + def build_design_from_config(config: Dict[str, Any]) -> Design: """Build a Design object directly from a config dictionary.""" - if 'inputs' not in config: + if not isinstance(config, dict): + raise ValueError( + "Config must be a mapping containing a non-empty 'inputs' list." + ) + if "inputs" not in config: raise ValueError("Config must contain 'inputs' key.") - - specs = build_input_spec_list(config['inputs']) + + specs = build_input_spec_list(config["inputs"]) return build_design(specs) + def get_variable_space() -> List[np.ndarray]: """Get the variable space as a list of arrays (for backward compatibility).""" # This function would need a config to work with the new system # For now, return empty list - users should use build_design_from_config instead return [] + def get_parameter_space(): """Get the parameter space (for backward compatibility).""" # This function would need a config to work with the new system # For now, return None - users should use build_design_from_config instead return None -def generate_initial_design(n_samples: int, config: Optional[Dict[str, Any]] = None) -> np.ndarray: + +def generate_initial_design( + n_samples: int, + config: Optional[Dict[str, Any]] = None, +) -> np.ndarray: """Generate initial design using Latin Hypercube Sampling. - + Args: n_samples: Number of samples to generate config: Optional config dictionary. If provided, uses the new design system. - + Returns: Array of shape (n_samples, n_features) """ @@ -117,9 +288,7 @@ def generate_initial_design(n_samples: int, config: Optional[Dict[str, Any]] = N # This would integrate with the LHS module # For now, return random samples in the bounds samples = np.random.uniform( - low=design.lowers, - high=design.uppers, - size=(n_samples, len(design.names)) + low=design.lowers, high=design.uppers, size=(n_samples, len(design.names)) ) return samples else: diff --git a/src/mobo_kit/discrete_refinement.py b/src/mobo_kit/discrete_refinement.py new file mode 100644 index 0000000..ab974d3 --- /dev/null +++ b/src/mobo_kit/discrete_refinement.py @@ -0,0 +1,718 @@ +"""Deterministic coordinate refinement on an exact finite design grid.""" + +from __future__ import annotations + +from dataclasses import dataclass +from numbers import Real +from typing import Callable, Sequence + +import numpy as np + +from .batch_selection import soft_local_penalty +from .candidate_pool import CandidatePool +from .design import Design + + +GridScoreFunction = Callable[[np.ndarray], np.ndarray] + + +@dataclass(frozen=True) +class RefinementConfig: + anchors_per_selection_step: int = 64 + max_sweeps: int = 10 + improvement_tolerance: float = 1.0e-10 + radius: float | None = 0.25 + min_batch_distance: float = 0.15 + min_observed_distance: float = 0.0 + dimension_weights: np.ndarray | None = None + epsilon: float = 1.0e-12 + + def __post_init__(self) -> None: + for name in ("anchors_per_selection_step", "max_sweeps"): + value = getattr(self, name) + if ( + isinstance(value, (bool, np.bool_)) + or not isinstance(value, (int, np.integer)) + or int(value) <= 0 + ): + raise ValueError(f"{name} must be a positive integer.") + object.__setattr__(self, name, int(value)) + for name in ( + "improvement_tolerance", + "min_batch_distance", + "min_observed_distance", + "epsilon", + ): + value = getattr(self, name) + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise ValueError(f"{name} must be a real non-boolean number.") + number = float(value) + if not np.isfinite(number) or number < 0: + raise ValueError(f"{name} must be finite and non-negative.") + object.__setattr__(self, name, number) + if self.epsilon <= 0 or self.epsilon >= 1: + raise ValueError("epsilon must be strictly between zero and one.") + if self.radius is not None: + if isinstance(self.radius, (bool, np.bool_)) or not isinstance( + self.radius, Real + ): + raise ValueError("radius must be null or a real number.") + radius = float(self.radius) + if not np.isfinite(radius) or radius <= 0: + raise ValueError("radius must be null or finite and positive.") + object.__setattr__(self, "radius", radius) + if self.dimension_weights is not None: + weights = np.asarray(self.dimension_weights, dtype=float).copy() + if weights.ndim != 1 or weights.size == 0: + raise ValueError("dimension_weights must be a non-empty vector.") + if not np.all(np.isfinite(weights)) or np.any(weights <= 0): + raise ValueError("dimension_weights must be finite and positive.") + weights.setflags(write=False) + object.__setattr__(self, "dimension_weights", weights) + + +@dataclass(frozen=True) +class RefinementTraceRow: + selection_step: int + anchor_rank: int + anchor_pool_index: int | None + sweep: int + coordinate: int | None + coordinate_name: str | None + start_grid_index: tuple[int, ...] + chosen_grid_index: tuple[int, ...] + score_before: float + score_after: float + base_score_before: float + base_score_after: float + penalized_log_score_before: float + penalized_log_score_after: float + accepted_move: bool + termination_reason: str | None + + +@dataclass(frozen=True) +class RefinedAnchor: + selection_step: int + anchor_rank: int + anchor_pool_index: int | None + anchor_grid_index: tuple[int, ...] + refined_grid_index: tuple[int, ...] + start_base_score: float + start_penalized_score: float + start_penalized_log_score: float + base_score: float + penalized_score: float + end_penalized_log_score: float + sweeps: int + accepted_move_count: int + changed_dimensions: tuple[str, ...] + termination_reason: str + + +@dataclass(frozen=True) +class RefinedBatchResult: + grid_indices: np.ndarray + X_phys: np.ndarray + X_norm: np.ndarray + base_scores: np.ndarray + penalized_scores_at_selection: np.ndarray + anchors: tuple[RefinedAnchor, ...] + trace: tuple[RefinementTraceRow, ...] + distinct_converged_optima: int + + +class CachedGridScorer: + """Memoize a vectorized exact-grid score function by integer tuple.""" + + def __init__(self, score_function: GridScoreFunction, dimension: int) -> None: + if not callable(score_function): + raise TypeError("score_function must be callable.") + if isinstance(dimension, bool) or int(dimension) <= 0: + raise ValueError("dimension must be a positive integer.") + self._score_function = score_function + self._dimension = int(dimension) + self._cache: dict[tuple[int, ...], float] = {} + + @property + def cache_size(self) -> int: + return len(self._cache) + + def seed(self, grid_indices: np.ndarray, scores: np.ndarray) -> None: + rows = _grid_matrix(grid_indices, self._dimension, name="grid_indices") + values = _score_vector(scores, rows.shape[0]) + for row, value in zip(rows, values): + key = tuple(int(item) for item in row) + previous = self._cache.get(key) + if previous is not None and previous != float(value): + raise ValueError("Conflicting score supplied for a cached grid tuple.") + self._cache[key] = float(value) + + def __call__(self, grid_indices: np.ndarray) -> np.ndarray: + rows = _grid_matrix(grid_indices, self._dimension, name="grid_indices") + keys = [tuple(int(item) for item in row) for row in rows] + missing_keys = list( + dict.fromkeys(key for key in keys if key not in self._cache) + ) + if missing_keys: + missing = np.asarray(missing_keys, dtype=np.int64) + values = _score_vector(self._score_function(missing), missing.shape[0]) + for key, value in zip(missing_keys, values): + self._cache[key] = float(value) + return np.asarray([self._cache[key] for key in keys], dtype=float) + + +def _grid_matrix(value: np.ndarray, dimension: int, *, name: str) -> np.ndarray: + raw = np.asarray(value) + if raw.ndim != 2 or raw.shape[1] != dimension: + raise ValueError(f"{name} must have shape (N, {dimension}); got {raw.shape}.") + if not np.issubdtype(raw.dtype, np.integer): + if not np.all(np.isfinite(raw)) or not np.all(raw == np.floor(raw)): + raise ValueError(f"{name} must contain integer grid indices.") + return raw.astype(np.int64, copy=False) + + +def _score_vector(value: np.ndarray, size: int) -> np.ndarray: + scores = np.asarray(value, dtype=float) + if scores.shape != (size,) or not np.all(np.isfinite(scores)): + raise ValueError(f"score_function must return {size} finite scores.") + if np.any(scores < 0): + raise ValueError("Acquisition scores must be non-negative.") + return scores + + +def _validate_design(design: Design) -> tuple[np.ndarray, ...]: + if not isinstance(design, Design): + raise TypeError("design must be a Design.") + grids = tuple(np.asarray(grid, dtype=float) for grid in design.var_array) + if len(grids) != len(design.names) or not grids: + raise ValueError("design must contain one grid per named dimension.") + if any(grid.ndim != 1 or grid.size == 0 for grid in grids): + raise ValueError("Every design grid must be a non-empty vector.") + return grids + + +def grid_indices_to_physical_and_normalized( + grid_indices: np.ndarray, design: Design +) -> tuple[np.ndarray, np.ndarray]: + """Resolve exact integer tuples without allocating the Cartesian product.""" + grids = _validate_design(design) + rows = _grid_matrix(grid_indices, len(grids), name="grid_indices") + physical = np.empty(rows.shape, dtype=float) + for column, grid in enumerate(grids): + if np.any(rows[:, column] < 0) or np.any(rows[:, column] >= grid.size): + raise ValueError( + f"grid_indices contains an out-of-range value for {design.names[column]!r}." + ) + physical[:, column] = grid[rows[:, column]] + + # Canonicalize exactly as the Sobol candidate-pool path. Index fractions are + # mathematically equivalent on an evenly spaced grid, but decimal-valued axes + # can differ by one floating-point bit. Using physical bounds here keeps pool + # anchors, refined candidates, distance checks, and exact-overlap diagnostics + # on one representation. + lower = np.asarray(design.lowers, dtype=float) + upper = np.asarray(design.uppers, dtype=float) + spans = upper - lower + normalized = np.zeros_like(physical, dtype=float) + changing = spans > 0.0 + normalized[:, changing] = (physical[:, changing] - lower[changing]) / spans[ + changing + ] + return physical, normalized + + +def _weights(config: RefinementConfig, dimension: int) -> np.ndarray: + if config.dimension_weights is None: + return np.ones(dimension, dtype=float) + if config.dimension_weights.shape != (dimension,): + raise ValueError( + f"dimension_weights must have shape ({dimension},); got " + f"{config.dimension_weights.shape}." + ) + return config.dimension_weights + + +def _distance_to_references( + X_norm: np.ndarray, references: np.ndarray, weights: np.ndarray +) -> np.ndarray: + if references.shape[0] == 0: + return np.full(X_norm.shape[0], np.inf, dtype=float) + difference = X_norm[:, None, :] - references[None, :, :] + return np.sqrt(np.sum(weights * difference**2, axis=-1)).min(axis=1) + + +def _penalized_scores( + base_scores: np.ndarray, + X_norm: np.ndarray, + *, + selected_norm: np.ndarray, + observed_norm: np.ndarray, + grid_indices: np.ndarray, + forbidden_grid_keys: set[tuple[int, ...]], + config: RefinementConfig, + positive_score_threshold: float, +) -> np.ndarray: + weights = _weights(config, X_norm.shape[1]) + penalized = np.asarray(base_scores, dtype=float).copy() + valid = penalized > positive_score_threshold + if forbidden_grid_keys: + valid &= np.asarray( + [ + tuple(int(value) for value in row) not in forbidden_grid_keys + for row in grid_indices + ], + dtype=bool, + ) + nearest_selected = _distance_to_references(X_norm, selected_norm, weights) + nearest_observed = _distance_to_references(X_norm, observed_norm, weights) + if selected_norm.shape[0]: + valid &= nearest_selected >= config.min_batch_distance + if observed_norm.shape[0] and config.min_observed_distance > 0: + valid &= nearest_observed >= config.min_observed_distance + if selected_norm.shape[0] and config.radius is not None: + difference = X_norm[:, None, :] - selected_norm[None, :, :] + distances = np.sqrt(np.sum(weights * difference**2, axis=-1)) + factors, _ = soft_local_penalty( + distances, radius=config.radius, epsilon=config.epsilon + ) + penalized *= np.prod(factors, axis=1) + penalized[~valid] = -np.inf + return penalized + + +def _lexicographic_best( + grid_indices: np.ndarray, scores: np.ndarray, *, tie_tolerance: float = 1.0e-15 +) -> int: + valid = np.flatnonzero(np.isfinite(scores)) + if valid.size == 0: + raise RuntimeError("No eligible finite acquisition score remains.") + maximum = float(np.max(scores[valid])) + tied = valid[np.abs(scores[valid] - maximum) <= tie_tolerance] + if tied.size == 1: + return int(tied[0]) + keys = tuple( + grid_indices[tied, column] for column in reversed(range(grid_indices.shape[1])) + ) + return int(tied[np.lexsort(keys)[0]]) + + +def refine_discrete_acquisition_anchors( + design: Design, + anchor_grid_indices: np.ndarray, + score_function: GridScoreFunction, + *, + config: RefinementConfig, + selection_step: int, + anchor_pool_indices: Sequence[int | None] | None = None, + selected_grid_indices: np.ndarray | None = None, + selected_norm: np.ndarray | None = None, + observed_grid_indices: np.ndarray | None = None, + observed_norm: np.ndarray | None = None, + avoid_grid_indices: np.ndarray | None = None, + positive_score_threshold: float = 0.0, +) -> tuple[tuple[RefinedAnchor, ...], tuple[RefinementTraceRow, ...]]: + """Coordinate-ascent every anchor using every allowed value per dimension.""" + grids = _validate_design(design) + dimension = len(grids) + anchors = _grid_matrix(anchor_grid_indices, dimension, name="anchor_grid_indices") + if anchors.shape[0] == 0: + raise ValueError("At least one refinement anchor is required.") + if isinstance(selection_step, bool) or int(selection_step) <= 0: + raise ValueError("selection_step must be a positive integer.") + if not isinstance(config, RefinementConfig): + raise TypeError("config must be a RefinementConfig.") + if anchor_pool_indices is None: + pool_indices: tuple[int | None, ...] = (None,) * anchors.shape[0] + else: + if len(anchor_pool_indices) != anchors.shape[0]: + raise ValueError("anchor_pool_indices must align with anchor rows.") + parsed_indices: list[int | None] = [] + for value in anchor_pool_indices: + if value is None: + parsed_indices.append(None) + elif isinstance(value, (bool, np.bool_)) or int(value) < 0: + raise ValueError( + "anchor_pool_indices values must be non-negative integers or None." + ) + else: + parsed_indices.append(int(value)) + pool_indices = tuple(parsed_indices) + threshold = float(positive_score_threshold) + if not np.isfinite(threshold) or threshold < 0: + raise ValueError("positive_score_threshold must be finite and non-negative.") + scorer = ( + score_function + if isinstance(score_function, CachedGridScorer) + else CachedGridScorer(score_function, dimension) + ) + + empty_grid = np.empty((0, dimension), dtype=np.int64) + selected_grid = _grid_matrix( + empty_grid if selected_grid_indices is None else selected_grid_indices, + dimension, + name="selected_grid_indices", + ) + observed_grid = _grid_matrix( + empty_grid if observed_grid_indices is None else observed_grid_indices, + dimension, + name="observed_grid_indices", + ) + avoid_grid = _grid_matrix( + empty_grid if avoid_grid_indices is None else avoid_grid_indices, + dimension, + name="avoid_grid_indices", + ) + empty_norm = np.empty((0, dimension), dtype=float) + selected_X = ( + empty_norm if selected_norm is None else np.asarray(selected_norm, dtype=float) + ) + observed_X = ( + empty_norm if observed_norm is None else np.asarray(observed_norm, dtype=float) + ) + for name, value in (("selected_norm", selected_X), ("observed_norm", observed_X)): + if ( + value.ndim != 2 + or value.shape[1] != dimension + or not np.all(np.isfinite(value)) + ): + raise ValueError(f"{name} must be a finite (N, {dimension}) matrix.") + forbidden = { + tuple(int(value) for value in row) + for row in np.vstack([selected_grid, observed_grid, avoid_grid]) + } + + refined: list[RefinedAnchor] = [] + trace: list[RefinementTraceRow] = [] + for anchor_rank, (anchor, anchor_pool_index) in enumerate( + zip(anchors, pool_indices), start=1 + ): + current = anchor.copy() + current_phys, current_norm = grid_indices_to_physical_and_normalized( + current[None, :], design + ) + del current_phys + current_base = scorer(current[None, :])[0] + current_penalized = _penalized_scores( + np.asarray([current_base]), + current_norm, + selected_norm=selected_X, + observed_norm=observed_X, + grid_indices=current[None, :], + forbidden_grid_keys=forbidden, + config=config, + positive_score_threshold=threshold, + )[0] + if not np.isfinite(current_penalized): + raise RuntimeError("A refinement anchor violates an eligibility rule.") + start_base = float(current_base) + start_penalized = float(current_penalized) + accepted_move_count = 0 + changed_dimensions: set[str] = set() + termination = "max_sweeps" + sweeps_completed = 0 + for sweep in range(1, config.max_sweeps + 1): + sweep_improved = False + sweeps_completed = sweep + for coordinate, (name, grid) in enumerate(zip(design.names, grids)): + candidates = np.repeat(current[None, :], grid.size, axis=0) + candidates[:, coordinate] = np.arange(grid.size, dtype=np.int64) + _, candidates_norm = grid_indices_to_physical_and_normalized( + candidates, design + ) + base = scorer(candidates) + penalized = _penalized_scores( + base, + candidates_norm, + selected_norm=selected_X, + observed_norm=observed_X, + grid_indices=candidates, + forbidden_grid_keys=forbidden, + config=config, + positive_score_threshold=threshold, + ) + chosen = _lexicographic_best(candidates, penalized) + proposed = candidates[chosen] + proposed_score = float(penalized[chosen]) + accepted = proposed_score > ( + current_penalized + config.improvement_tolerance + ) + start_key = tuple(int(value) for value in current) + chosen_key = tuple(int(value) for value in proposed) + before = float(current_penalized) + base_before = float(current_base) + if accepted: + current = proposed.copy() + current_base = float(base[chosen]) + current_penalized = proposed_score + sweep_improved = True + accepted_move_count += 1 + changed_dimensions.add(name) + trace.append( + RefinementTraceRow( + selection_step=int(selection_step), + anchor_rank=anchor_rank, + anchor_pool_index=anchor_pool_index, + sweep=sweep, + coordinate=coordinate, + coordinate_name=name, + start_grid_index=start_key, + chosen_grid_index=chosen_key, + score_before=before, + score_after=float(current_penalized), + base_score_before=base_before, + base_score_after=float(current_base), + penalized_log_score_before=float(np.log(before)), + penalized_log_score_after=float(np.log(current_penalized)), + accepted_move=accepted, + termination_reason=None, + ) + ) + if not sweep_improved: + termination = "no_improvement" + break + trace.append( + RefinementTraceRow( + selection_step=int(selection_step), + anchor_rank=anchor_rank, + anchor_pool_index=anchor_pool_index, + sweep=sweeps_completed, + coordinate=None, + coordinate_name=None, + start_grid_index=tuple(int(value) for value in current), + chosen_grid_index=tuple(int(value) for value in current), + score_before=float(current_penalized), + score_after=float(current_penalized), + base_score_before=float(current_base), + base_score_after=float(current_base), + penalized_log_score_before=float(np.log(current_penalized)), + penalized_log_score_after=float(np.log(current_penalized)), + accepted_move=False, + termination_reason=termination, + ) + ) + refined.append( + RefinedAnchor( + selection_step=int(selection_step), + anchor_rank=anchor_rank, + anchor_pool_index=anchor_pool_index, + anchor_grid_index=tuple(int(value) for value in anchor), + refined_grid_index=tuple(int(value) for value in current), + start_base_score=start_base, + start_penalized_score=start_penalized, + start_penalized_log_score=float(np.log(start_penalized)), + base_score=float(current_base), + penalized_score=float(current_penalized), + end_penalized_log_score=float(np.log(current_penalized)), + sweeps=sweeps_completed, + accepted_move_count=accepted_move_count, + changed_dimensions=tuple(sorted(changed_dimensions)), + termination_reason=termination, + ) + ) + return tuple(refined), tuple(trace) + + +def propose_refined_discrete_batch( + master_pool: CandidatePool, + design: Design, + score_function: GridScoreFunction, + *, + q: int, + config: RefinementConfig, + master_base_scores: np.ndarray | None = None, + observed_grid_indices: np.ndarray | None = None, + observed_norm: np.ndarray | None = None, + avoid_grid_indices: np.ndarray | None = None, + positive_score_threshold: float = 0.0, +) -> RefinedBatchResult: + """Sequentially refine top pool anchors and select an exact-grid batch.""" + if not isinstance(master_pool, CandidatePool): + raise TypeError("master_pool must be a CandidatePool.") + grids = _validate_design(design) + dimension = len(grids) + pool_grid = _grid_matrix( + master_pool.grid_indices, dimension, name="pool.grid_indices" + ) + if isinstance(q, bool) or not isinstance(q, (int, np.integer)) or int(q) <= 0: + raise ValueError("q must be a positive integer.") + requested = int(q) + scorer = CachedGridScorer(score_function, dimension) + if master_base_scores is None: + pool_base = scorer(pool_grid) + else: + pool_base = _score_vector(master_base_scores, pool_grid.shape[0]) + scorer.seed(pool_grid, pool_base) + empty_grid = np.empty((0, dimension), dtype=np.int64) + observed_grid = _grid_matrix( + empty_grid if observed_grid_indices is None else observed_grid_indices, + dimension, + name="observed_grid_indices", + ) + avoid_grid = _grid_matrix( + empty_grid if avoid_grid_indices is None else avoid_grid_indices, + dimension, + name="avoid_grid_indices", + ) + observed_X = ( + np.empty((0, dimension), dtype=float) + if observed_norm is None + else np.asarray(observed_norm, dtype=float) + ) + if ( + observed_X.ndim != 2 + or observed_X.shape[1] != dimension + or not np.all(np.isfinite(observed_X)) + ): + raise ValueError(f"observed_norm must be a finite (N, {dimension}) matrix.") + + selected_grid: list[np.ndarray] = [] + selected_norm: list[np.ndarray] = [] + selected_base: list[float] = [] + selected_penalized: list[float] = [] + all_anchors: list[RefinedAnchor] = [] + all_trace: list[RefinementTraceRow] = [] + discovered_optima: set[tuple[int, ...]] = set() + pool_position_by_key = { + tuple(int(value) for value in row): index for index, row in enumerate(pool_grid) + } + for selection_step in range(1, requested + 1): + selected_grid_array = ( + np.asarray(selected_grid, dtype=np.int64) + if selected_grid + else empty_grid.copy() + ) + selected_norm_array = ( + np.asarray(selected_norm, dtype=float) + if selected_norm + else np.empty((0, dimension), dtype=float) + ) + forbidden = { + tuple(int(value) for value in row) + for row in np.vstack([observed_grid, avoid_grid, selected_grid_array]) + } + pool_penalized = _penalized_scores( + pool_base, + np.asarray(master_pool.X_norm, dtype=float), + selected_norm=selected_norm_array, + observed_norm=observed_X, + grid_indices=pool_grid, + forbidden_grid_keys=forbidden, + config=config, + positive_score_threshold=positive_score_threshold, + ) + valid = np.flatnonzero(np.isfinite(pool_penalized)) + if valid.size == 0: + raise RuntimeError( + "No master-pool anchors remain without relaxing an eligibility rule." + ) + lex_order = np.lexsort( + tuple(pool_grid[valid, column] for column in reversed(range(dimension))) + ) + lex_valid = valid[lex_order] + score_order = np.argsort(-pool_penalized[lex_valid], kind="stable") + anchor_positions = lex_valid[score_order][ + : min(config.anchors_per_selection_step, valid.size) + ] + anchor_rows = [pool_grid[position].copy() for position in anchor_positions] + anchor_pool_indices: list[int | None] = [ + int(position) for position in anchor_positions + ] + seen_anchor_keys = {tuple(int(value) for value in row) for row in anchor_rows} + for previous_key in sorted(discovered_optima): + if previous_key in seen_anchor_keys or previous_key in forbidden: + continue + anchor_rows.append(np.asarray(previous_key, dtype=np.int64)) + anchor_pool_indices.append(pool_position_by_key.get(previous_key)) + seen_anchor_keys.add(previous_key) + combined_anchors = np.asarray(anchor_rows, dtype=np.int64) + _, combined_norm = grid_indices_to_physical_and_normalized( + combined_anchors, design + ) + combined_penalized = _penalized_scores( + scorer(combined_anchors), + combined_norm, + selected_norm=selected_norm_array, + observed_norm=observed_X, + grid_indices=combined_anchors, + forbidden_grid_keys=forbidden, + config=config, + positive_score_threshold=positive_score_threshold, + ) + eligible_anchor_mask = np.isfinite(combined_penalized) + combined_anchors = combined_anchors[eligible_anchor_mask] + eligible_pool_indices = tuple( + value + for value, eligible in zip(anchor_pool_indices, eligible_anchor_mask) + if eligible + ) + if combined_anchors.shape[0] == 0: + raise RuntimeError("No eligible local-refinement anchor remains.") + refined, trace = refine_discrete_acquisition_anchors( + design, + combined_anchors, + scorer, + config=config, + selection_step=selection_step, + anchor_pool_indices=eligible_pool_indices, + selected_grid_indices=selected_grid_array, + selected_norm=selected_norm_array, + observed_grid_indices=observed_grid, + observed_norm=observed_X, + avoid_grid_indices=avoid_grid, + positive_score_threshold=positive_score_threshold, + ) + all_anchors.extend(refined) + all_trace.extend(trace) + discovered_optima.update(item.refined_grid_index for item in refined) + unique_optima = np.asarray( + sorted({item.refined_grid_index for item in refined}), dtype=np.int64 + ) + _, optima_norm = grid_indices_to_physical_and_normalized(unique_optima, design) + optima_base = scorer(unique_optima) + optima_penalized = _penalized_scores( + optima_base, + optima_norm, + selected_norm=selected_norm_array, + observed_norm=observed_X, + grid_indices=unique_optima, + forbidden_grid_keys=forbidden, + config=config, + positive_score_threshold=positive_score_threshold, + ) + chosen = _lexicographic_best(unique_optima, optima_penalized) + selected_grid.append(unique_optima[chosen].copy()) + selected_norm.append(optima_norm[chosen].copy()) + selected_base.append(float(optima_base[chosen])) + selected_penalized.append(float(optima_penalized[chosen])) + + selected_grid_array = np.asarray(selected_grid, dtype=np.int64) + physical, normalized = grid_indices_to_physical_and_normalized( + selected_grid_array, design + ) + if np.unique(selected_grid_array, axis=0).shape[0] != requested: + raise RuntimeError("Refinement produced duplicate selected grid tuples.") + return RefinedBatchResult( + grid_indices=selected_grid_array, + X_phys=physical, + X_norm=normalized, + base_scores=np.asarray(selected_base, dtype=float), + penalized_scores_at_selection=np.asarray(selected_penalized, dtype=float), + anchors=tuple(all_anchors), + trace=tuple(all_trace), + distinct_converged_optima=len( + {anchor.refined_grid_index for anchor in all_anchors} + ), + ) + + +__all__ = [ + "CachedGridScorer", + "RefinedAnchor", + "RefinedBatchResult", + "RefinementConfig", + "RefinementTraceRow", + "grid_indices_to_physical_and_normalized", + "propose_refined_discrete_batch", + "refine_discrete_acquisition_anchors", +] diff --git a/src/mobo_kit/lhs.py b/src/mobo_kit/lhs.py index 98b7905..d57838c 100644 --- a/src/mobo_kit/lhs.py +++ b/src/mobo_kit/lhs.py @@ -1,52 +1,35 @@ -# src/lhs.py +"""Deterministic, grid-safe Latin hypercube campaign design.""" + from __future__ import annotations + +from math import prod +from typing import Optional, Sequence, Tuple, Union + import numpy as np import pandas as pd -from typing import Optional, Callable, Tuple, Sequence, Union -from .constraints import apply_row_constraints, RowConstraint - -from emukit.core import ParameterSpace, ContinuousParameter -from emukit.core.initial_designs.latin_design import LatinDesign +from .constraints import RowConstraint, apply_row_constraints from .design import Design -from .data import snap_to_grid_np # snaps only dims with grids (design.var_list) -import seaborn as sns -import matplotlib.pyplot as plt -from sklearn.decomposition import PCA -from sklearn.preprocessing import StandardScaler - -# ---------------------------- -# Emukit space & core sampling -# ---------------------------- - -def _space_from_design(design: Design) -> ParameterSpace: - """ - Build an Emukit ParameterSpace using only ContinuousParameter. - For grid dims, we use [min(grid), max(grid)] and snap after sampling. - """ - params = [] - for j, name in enumerate(design.names): - lo = float(design.lowers[j]) - hi = float(design.uppers[j]) - params.append(ContinuousParameter(name, lo, hi)) - return ParameterSpace(params) +def _max_abs_corr(X: np.ndarray) -> float: + """Return the largest absolute Pearson correlation between varying columns.""" + values = np.asarray(X, dtype=float) + if values.ndim != 2: + raise ValueError(f"X must be two-dimensional; got shape {values.shape}.") + if values.shape[0] < 2 or values.shape[1] < 2: + return 0.0 -# ---------------------------- -# Dataset-level corr utilities -# ---------------------------- + varying = np.ptp(values, axis=0) > 0 + varying_values = values[:, varying] + if varying_values.shape[1] < 2: + return 0.0 -def _max_abs_corr(X: np.ndarray) -> float: - """ - Maximum absolute Pearson correlation across columns of X. - NaNs from zero-variance columns are treated as 0 correlation. - """ - C = np.corrcoef(X, rowvar=False) # DxD - A = np.abs(C) - np.fill_diagonal(A, 0.0) - A = np.nan_to_num(A, nan=0.0, posinf=1.0, neginf=1.0) - return float(np.max(A)) + corr = np.corrcoef(varying_values, rowvar=False) + absolute = np.abs(corr) + np.fill_diagonal(absolute, 0.0) + absolute = np.nan_to_num(absolute, nan=0.0, posinf=1.0, neginf=1.0) + return float(np.max(absolute)) def _pick_subset_with_corr( @@ -56,154 +39,329 @@ def _pick_subset_with_corr( tries: int, rng: np.random.Generator, ) -> Tuple[np.ndarray, float]: - """ - Try random subsets of size n from X to find one with max|corr| <= threshold. - Returns (best_subset, best_max_corr). If none meet threshold, returns best found. - """ - m = X.shape[0] - if m == n: - return X, _max_abs_corr(X) - - best_X = None - best_val = float("inf") - for _ in range(max(1, tries)): - idx = rng.choice(m, size=n, replace=False) - X_sub = X[idx] - val = _max_abs_corr(X_sub) - if val < best_val: - best_val, best_X = val, X_sub - if val <= threshold: - break - return best_X, best_val + """Search deterministically (for a seeded RNG) for a qualifying subset.""" + if X.shape[0] < n: + raise ValueError( + f"Need at least {n} rows for subset selection; got {X.shape[0]}." + ) + + best = X[:n].copy() + best_corr = _max_abs_corr(best) + if best_corr <= threshold or X.shape[0] == n: + return best, best_corr + + for _ in range(tries): + indices = rng.choice(X.shape[0], size=n, replace=False) + candidate = X[indices] + correlation = _max_abs_corr(candidate) + if correlation < best_corr: + best = candidate.copy() + best_corr = correlation + if correlation <= threshold: + return candidate.copy(), correlation + + return best, best_corr + def _lhs_select_numeric(df: pd.DataFrame, design: Design) -> pd.DataFrame: - cols = [c for c in design.names if c in df.columns] - return df[cols].select_dtypes(include=[np.number]).copy() + columns = [name for name in design.names if name in df.columns] + return df[columns].select_dtypes(include=[np.number]).copy() -def _lhs_labels(design: Design, cols) -> list: - name_to_label = {n: l for n, l in zip(design.names, design.labels)} - return [name_to_label.get(c, c) for c in cols] +def _lhs_labels(design: Design, columns: Sequence[str]) -> list[str]: + """Build labels from the fields Design actually owns (names and units).""" + labels = {} + for name, unit in zip(design.names, design.units): + labels[name] = f"{name} [{unit}]" if unit else name + return [labels.get(column, column) for column in columns] -# ---------------------------- -# OPTIMIZED LHS IMPLEMENTATION -# ---------------------------- -def _batch_generate_lhs_samples( - design: Design, - total_samples: int, - batch_size: int = 100, - seed: Optional[int] = None +def _validate_positive_int(value: object, *, name: str) -> int: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)): + raise ValueError(f"{name} must be a positive integer; got {value!r}.") + integer = int(value) + if integer <= 0: + raise ValueError(f"{name} must be a positive integer; got {integer}.") + return integer + + +def _validate_design(design: Design) -> None: + if not isinstance(design, Design): + raise TypeError( + f"design must be a Design instance; got {type(design).__name__}." + ) + dimension = len(design.names) + if dimension == 0: + raise ValueError("Design must contain at least one input.") + if len(set(design.names)) != dimension: + raise ValueError("Design input names must be unique.") + if len(design.units) != dimension: + raise ValueError( + f"Design has {dimension} names but {len(design.units)} unit entries." + ) + if len(design.var_list) != dimension: + raise ValueError( + f"Design has {dimension} names but {len(design.var_list)} variable grids." + ) + for field_name in ("lowers", "uppers", "steps"): + values = np.asarray(getattr(design, field_name), dtype=float) + if values.shape != (dimension,) or not np.all(np.isfinite(values)): + raise ValueError( + f"Design field '{field_name}' must contain {dimension} finite values." + ) + if np.any(np.asarray(design.steps, dtype=float) <= 0): + raise ValueError("Design steps must all be > 0.") + + for index, (name, raw_grid) in enumerate(zip(design.names, design.var_list)): + grid = np.asarray(raw_grid, dtype=float) + if grid.ndim != 1 or grid.size == 0: + raise ValueError( + f"Design grid for input '{name}' must be a non-empty 1D array." + ) + if not np.all(np.isfinite(grid)): + raise ValueError( + f"Design grid for input '{name}' contains non-finite values." + ) + if np.unique(grid).size != grid.size or np.any(np.diff(grid) <= 0): + raise ValueError( + f"Design grid for input '{name}' must be strictly increasing " + "and unique." + ) + if grid.size > 1: + step = float(design.steps[index]) + if not np.allclose( + np.diff(grid), + step, + rtol=0.0, + atol=max(1e-12, abs(step) * 1e-9), + ): + raise ValueError( + f"Design grid for input '{name}' is not aligned to step {step}." + ) + if not np.isclose(grid[0], design.lowers[index], rtol=0.0, atol=1e-12): + raise ValueError( + f"Design lower bound for input '{name}' does not match its grid." + ) + if not np.isclose(grid[-1], design.uppers[index], rtol=0.0, atol=1e-12): + raise ValueError( + f"Design upper bound for input '{name}' does not match its grid." + ) + + +def _normalize_constraints( + row_constraints: Optional[Union[RowConstraint, Sequence[RowConstraint]]], +) -> list[RowConstraint]: + if row_constraints is None: + return [] + if callable(row_constraints): + return [row_constraints] + constraints = list(row_constraints) + if not all(callable(constraint) for constraint in constraints): + raise ValueError("Every row constraint must be callable.") + return constraints + + +def _latin_hypercube_unit( + sample_count: int, + dimension: int, + rng: np.random.Generator, ) -> np.ndarray: - """ - Generate LHS samples in batches for better memory management. - """ - if seed is not None: - np.random.seed(seed) - - space = _space_from_design(design) - all_samples = [] - - for i in range(0, total_samples, batch_size): - current_batch = min(batch_size, total_samples - i) - batch_samples = LatinDesign(space).get_samples(current_batch) - all_samples.append(batch_samples) - - return np.vstack(all_samples) + """Generate one randomized Latin hypercube using a persistent RNG.""" + jitter = rng.random((sample_count, dimension)) + unit = (np.arange(sample_count, dtype=float)[:, None] + jitter) / sample_count + for column in range(dimension): + rng.shuffle(unit[:, column]) + return unit + + +def _snap_to_design_grid(X: np.ndarray, design: Design) -> np.ndarray: + """Snap physical values to exact configured grid values.""" + values = np.asarray(X, dtype=float) + snapped = values.copy() + for column, raw_grid in enumerate(design.var_list): + grid = np.asarray(raw_grid, dtype=float) + nearest = np.argmin( + np.abs(snapped[:, column, None] - grid[None, :]), + axis=1, + ) + snapped[:, column] = grid[nearest] + return snapped + + +def _generate_lhs_samples( + design: Design, + total_samples: int, + rng: np.random.Generator, +) -> np.ndarray: + """Generate one physical-space LHS without resetting RNG state.""" + unit = _latin_hypercube_unit(total_samples, len(design.names), rng) + return design.lowers + unit * (design.uppers - design.lowers) def _batch_apply_constraints( - X_samples: np.ndarray, - design: Design, + X_samples: np.ndarray, + design: Design, constraints: Sequence[RowConstraint], - batch_size: int = 1000 -) -> Tuple[np.ndarray, np.ndarray]: - """ - Apply constraints in batches to avoid memory issues with large sample sets. - Returns (valid_samples, valid_indices). - """ + batch_size: int, +) -> np.ndarray: + """Apply constraints to already-snapped rows in bounded-size chunks.""" if not constraints: - return X_samples, np.arange(len(X_samples)) - - valid_samples = [] - valid_indices = [] - - for i in range(0, len(X_samples), batch_size): - batch_end = min(i + batch_size, len(X_samples)) - X_batch = X_samples[i:batch_end] - - # Apply constraints to this batch - mask = apply_row_constraints(X_batch, design, constraints) - valid_batch = X_batch[mask] - valid_indices.extend(np.arange(i, batch_end)[mask]) - - if len(valid_batch) > 0: - valid_samples.append(valid_batch) - - if valid_samples: - return np.vstack(valid_samples), np.array(valid_indices) - else: - return np.empty((0, X_samples.shape[1])), np.array([], dtype=int) - + return X_samples + + valid_batches = [] + for start in range(0, X_samples.shape[0], batch_size): + batch = X_samples[start : start + batch_size] + mask = apply_row_constraints(batch, design, constraints) + if np.any(mask): + valid_batches.append(batch[mask]) + if not valid_batches: + return np.empty((0, X_samples.shape[1]), dtype=float) + return np.vstack(valid_batches) + + +def _append_unique_rows( + pool: list[np.ndarray], + seen: set[tuple[float, ...]], + rows: np.ndarray, +) -> None: + """Append rows in encounter order, deduplicating exact snapped grid values.""" + for row in rows: + key = tuple(float(value) for value in row) + if key not in seen: + seen.add(key) + pool.append(np.asarray(row, dtype=float).copy()) + + +def _validate_grid_membership(X: np.ndarray, design: Design) -> None: + """Guard the campaign invariant that every emitted value is on its grid.""" + for column, (name, raw_grid) in enumerate(zip(design.names, design.var_list)): + grid = np.asarray(raw_grid, dtype=float) + if not np.all(np.isin(X[:, column], grid)): + raise RuntimeError( + "Internal LHS error: generated value outside configured grid " + f"for '{name}'." + ) + + +def _strict_lhs_dataframe( + *, + design: Design, + n: int, + seed: Optional[int], + snap_to_grids: bool, + row_constraints: Optional[Union[RowConstraint, Sequence[RowConstraint]]], + max_abs_corr: Optional[float], + max_attempts: int, + oversample: int, + batch_size: int, + subset_tries: int, + samples_per_attempt: Optional[int], + verbose: bool, +) -> pd.DataFrame: + _validate_design(design) + n = _validate_positive_int(n, name="n") + max_attempts = _validate_positive_int(max_attempts, name="max_attempts") + oversample = _validate_positive_int(oversample, name="oversample") + batch_size = _validate_positive_int(batch_size, name="batch_size") + subset_tries = _validate_positive_int(subset_tries, name="subset_tries") + + if not snap_to_grids: + raise ValueError( + "Campaign LHS requires snap_to_grids=True so every condition is exactly " + "on the configured processing grid." + ) -def _smart_subset_selection( - X_valid: np.ndarray, - n_target: int, - max_abs_corr: float, - max_tries: int = 1000, - early_stop_threshold: float = 0.1, - seed: Optional[int] = None -) -> Tuple[np.ndarray, float, int]: - """ - Smart subset selection with early stopping and adaptive search. - Returns (best_subset, best_correlation, tries_used). - """ if seed is not None: - rng = np.random.default_rng(seed) + if isinstance(seed, (bool, np.bool_)) or not isinstance( + seed, (int, np.integer) + ): + raise ValueError(f"seed must be an integer or None; got {seed!r}.") + seed = int(seed) + if seed < 0: + raise ValueError(f"seed must be non-negative; got {seed}.") + + if max_abs_corr is not None: + try: + max_abs_corr = float(max_abs_corr) + except (TypeError, ValueError) as exc: + raise ValueError("max_abs_corr must be a finite number in [0, 1].") from exc + if not np.isfinite(max_abs_corr) or not 0.0 <= max_abs_corr <= 1.0: + raise ValueError("max_abs_corr must be a finite number in [0, 1].") + + if samples_per_attempt is None: + samples_per_attempt = max(n, n * oversample) else: - rng = np.random.default_rng() - - m = X_valid.shape[0] - if m <= n_target: - return X_valid, _max_abs_corr(X_valid), 0 - - best_X = None + samples_per_attempt = _validate_positive_int( + samples_per_attempt, name="samples_per_attempt" + ) + + total_grid_points = prod(len(grid) for grid in design.var_list) + if n > total_grid_points: + raise ValueError( + f"Requested n={n} unique conditions, but the design contains only " + f"{total_grid_points} unique grid combinations." + ) + + constraints = _normalize_constraints(row_constraints) + rng = np.random.default_rng(seed) + unique_pool: list[np.ndarray] = [] + seen: set[tuple[float, ...]] = set() best_corr = float("inf") - tries_used = 0 - - # Phase 1: Quick random sampling - quick_tries = min(max_tries // 2, 100) - for _ in range(quick_tries): - idx = rng.choice(m, size=n_target, replace=False) - X_sub = X_valid[idx] - corr = _max_abs_corr(X_sub) - - if corr < best_corr: - best_corr = corr - best_X = X_sub.copy() - tries_used += 1 - - # Early stopping if we're close to target - if corr <= max_abs_corr + early_stop_threshold: - break - - # Phase 2: Targeted improvement if needed - if best_corr > max_abs_corr: - remaining_tries = max_tries - tries_used - for _ in range(remaining_tries): - idx = rng.choice(m, size=n_target, replace=False) - X_sub = X_valid[idx] - corr = _max_abs_corr(X_sub) - - if corr < best_corr: - best_corr = corr - best_X = X_sub.copy() - tries_used += 1 - - if corr <= max_abs_corr: - break - - return best_X, best_corr, tries_used + + for attempt in range(1, max_attempts + 1): + raw = _generate_lhs_samples( + design=design, + total_samples=samples_per_attempt, + rng=rng, + ) + # Constraints deliberately see executable, snapped processing conditions. + snapped = _snap_to_design_grid(raw, design) + _validate_grid_membership(snapped, design) + valid = _batch_apply_constraints(snapped, design, constraints, batch_size) + _append_unique_rows(unique_pool, seen, valid) + + if verbose: + print( + f"[LHS] attempt {attempt}/{max_attempts}: " + f"{len(unique_pool)} unique valid grid points" + ) + + if len(unique_pool) < n: + continue + + pool_array = np.vstack(unique_pool) + if max_abs_corr is None: + selected = pool_array[:n].copy() + _validate_grid_membership(selected, design) + return pd.DataFrame(selected, columns=design.names) + + selected, correlation = _pick_subset_with_corr( + pool_array, + n=n, + threshold=max_abs_corr, + tries=subset_tries, + rng=rng, + ) + best_corr = min(best_corr, correlation) + if verbose: + print( + f"[LHS] attempt {attempt}: best max|corr|={correlation:.6f}; " + f"required <= {max_abs_corr:.6f}" + ) + if correlation <= max_abs_corr: + _validate_grid_membership(selected, design) + return pd.DataFrame(selected, columns=design.names) + + details = ( + f"Generated {len(unique_pool)} unique constraint-valid grid points after " + f"{max_attempts} attempt(s), using {samples_per_attempt} samples per attempt." + ) + if max_abs_corr is not None and len(unique_pool) >= n: + details += f" Best max|corr|={best_corr:.6f}, required <= {max_abs_corr:.6f}." + raise RuntimeError( + f"Unable to generate exactly n={n} campaign conditions. {details} " + "Increase samples_per_attempt/max_attempts or revise the design constraints." + ) def lhs_dataframe_optimized( @@ -213,214 +371,73 @@ def lhs_dataframe_optimized( snap_to_grids: bool = True, row_constraints: Optional[Union[RowConstraint, Sequence[RowConstraint]]] = None, max_abs_corr: Optional[float] = None, - # Performance tuning: max_attempts: int = 100, - oversample: int = 5, # Increased for better constraint satisfaction + oversample: int = 5, batch_size: int = 100, subset_tries: int = 1000, early_stop_threshold: float = 0.1, verbose: bool = False, + samples_per_attempt: Optional[int] = None, ) -> pd.DataFrame: + """Generate exactly ``n`` deterministic, unique, constraint-valid grid rows. + + ``samples_per_attempt`` controls the candidate count directly. When omitted, + it is ``max(n, n * oversample)``. A configured correlation limit is hard: the + function raises instead of returning a noncompliant or unconstrained fallback. + + ``early_stop_threshold`` remains accepted for API compatibility but is not + used; stopping occurs only when the hard ``max_abs_corr`` limit is satisfied. """ - OPTIMIZED LHS generation with batch processing, smart subset selection, and early stopping. - - Key optimizations: - 1. Batch LHS generation to manage memory - 2. Batch constraint application - 3. Smart subset selection with early stopping - 4. Adaptive oversampling based on constraint strictness - """ - if seed is not None: - np.random.seed(int(seed)) - - # Normalize constraints - if row_constraints is None: - constraints_list = [] - elif callable(row_constraints): - constraints_list = [row_constraints] - else: - constraints_list = list(row_constraints) - - # Adjust oversampling based on constraint complexity - if constraints_list: - effective_oversample = max(oversample, len(constraints_list) * 2) - else: - effective_oversample = 1 - - total_samples_needed = n * effective_oversample - - if verbose: - print(f"[OPTIMIZED LHS] Target: {n} samples, Generating: {total_samples_needed} samples") - - best_candidate = None - best_corr = float("inf") - - for attempt in range(1, max_attempts + 1): - if verbose and attempt % 10 == 0: - print(f"[OPTIMIZED LHS] Attempt {attempt}/{max_attempts}") - - # Generate LHS samples in batches - X_raw = _batch_generate_lhs_samples( - design, total_samples_needed, batch_size, seed - ) - - # Snap to grids if requested - if snap_to_grids: - X_raw = snap_to_grid_np(X_raw, design) - - # Apply constraints in batches - X_valid, valid_indices = _batch_apply_constraints( - X_raw, design, constraints_list, batch_size - ) - - if len(X_valid) < n: - if verbose: - print(f"[OPTIMIZED LHS] Attempt {attempt}: Only {len(X_valid)} valid samples") - continue - - # Smart subset selection - X_best, corr_val, tries_used = _smart_subset_selection( - X_valid, n, max_abs_corr or 1.0, subset_tries, early_stop_threshold - ) - - if verbose: - print(f"[OPTIMIZED LHS] Attempt {attempt}: max|corr|={corr_val:.3f} (tries: {tries_used})") - - # Check if we met the correlation target - if max_abs_corr is None or corr_val <= max_abs_corr: - if verbose: - print(f"[OPTIMIZED LHS] Success! max|corr|={corr_val:.3f} <= {max_abs_corr}") - return pd.DataFrame(X_best, columns=design.names) - - # Keep track of best candidate - if corr_val < best_corr: - best_corr = corr_val - best_candidate = X_best.copy() - - # Return best candidate if we didn't meet the target - if best_candidate is not None: - if verbose: - print(f"[OPTIMIZED LHS] Best achieved: max|corr|={best_corr:.3f} (> target)") - return pd.DataFrame(best_candidate, columns=design.names) - - # Fallback: return whatever we have - if verbose: - print(f"[OPTIMIZED LHS] Failed to generate {n} samples after {max_attempts} attempts") - - # Try one more time with minimal constraints - X_fallback = _batch_generate_lhs_samples(design, n, batch_size, seed) - if snap_to_grids: - X_fallback = snap_to_grid_np(X_fallback, design) - - return pd.DataFrame(X_fallback, columns=design.names) - - -# ---------------------------- -# Original LHS (kept for backward compatibility) -# ---------------------------- + try: + compatibility_threshold = float(early_stop_threshold) + except (TypeError, ValueError) as exc: + raise ValueError( + "early_stop_threshold must be a finite non-negative number." + ) from exc + if not np.isfinite(compatibility_threshold) or compatibility_threshold < 0: + raise ValueError("early_stop_threshold must be a finite non-negative number.") + return _strict_lhs_dataframe( + design=design, + n=n, + seed=seed, + snap_to_grids=snap_to_grids, + row_constraints=row_constraints, + max_abs_corr=max_abs_corr, + max_attempts=max_attempts, + oversample=oversample, + batch_size=batch_size, + subset_tries=subset_tries, + samples_per_attempt=samples_per_attempt, + verbose=verbose, + ) + def lhs_dataframe( design: Design, n: int, seed: Optional[int] = None, snap_to_grids: bool = True, - # Constraints: - #row_constraint_fn: Optional[Callable[[np.ndarray, Design], np.ndarray]] = None, row_constraints: Optional[Union[RowConstraint, Sequence[RowConstraint]]] = None, max_abs_corr: Optional[float] = None, - # Sampling controls: max_attempts: int = 100, oversample: int = 3, subset_tries: int = 800, verbose: bool = False, + samples_per_attempt: Optional[int] = None, + batch_size: int = 100, ) -> pd.DataFrame: - """ - Generate an LHS using Emukit, then enforce: - - pointwise constraints via `row_constraints -> bool mask` - - dataset-level Pearson correlation bound via `max_abs_corr` - - Strategy: - 1) Sample in continuous box via Emukit. - 2) Snap grid features (if requested). - 3) Apply row-wise mask (e.g., Clausius–Clapeyron); accumulate valid rows. - 4) If we have >= n rows, optionally pick a subset with max|corr| <= threshold. - - Returns DataFrame with columns == design.names (physical units). - """ - # Seed Emukit's internal RNG - if seed is not None: - np.random.seed(int(seed)) - rng = np.random.default_rng(seed) - - space = _space_from_design(design) - - # Normalize row constraints to a list (None -> empty list) - if row_constraints is None: - row_constraints_list: Sequence[RowConstraint] = [] - elif callable(row_constraints): - row_constraints_list = [row_constraints] # backward-compatible single fn - else: - row_constraints_list = list(row_constraints) - - collected = [] - best_candidate = None - best_corr_val = float("inf") - - for attempt in range(1, max_attempts + 1): - # Heuristic: oversample to survive row constraints - #batch = n if row_constraint_fn is None else max(n, oversample * n) - batch = n if not row_constraints_list else max(n, oversample * n) - - X = LatinDesign(space).get_samples(batch) # (batch, D) - if snap_to_grids: - X = snap_to_grid_np(X, design) - - # if row_constraint_fn is not None: - # mask = row_constraint_fn(X, design) - # if mask is None or mask.dtype != bool or mask.shape[0] != X.shape[0]: - # raise ValueError("row_constraint_fn must return a boolean mask of shape (batch,).") - # X = X[mask] - - if row_constraints_list: - mask = apply_row_constraints(X, design, row_constraints_list) - X = X[mask] - - if X.size == 0: - if verbose: - print(f"[LHS] attempt {attempt}: 0 valid after row constraints; retrying.") - continue - - collected.append(X) - X_all = np.vstack(collected) - - if X_all.shape[0] >= n: - # If no correlation constraint, take first n (shuffled) - idx = rng.permutation(X_all.shape[0]) - X_all = X_all[idx] - - if max_abs_corr is None: - return pd.DataFrame(X_all[:n], columns=design.names) - - # Try to find subset meeting correlation threshold - X_best, corr_val = _pick_subset_with_corr(X_all, n, max_abs_corr, subset_tries, rng) - if verbose: - print(f"[LHS] attempt {attempt}: candidate max|corr|={corr_val:.3f}") - if corr_val <= max_abs_corr: - return pd.DataFrame(X_best, columns=design.names) - - # Keep best-so-far - if corr_val < best_corr_val: - best_corr_val = corr_val - best_candidate = X_best - - # If we got here, we didn't meet correlation threshold; return best we found - if best_candidate is None: - # Maybe we never amassed n rows; return whatever we have (possibly < n) - X_all = np.vstack(collected) if collected else np.empty((0, len(design.names))) - X_all = X_all[:n] - return pd.DataFrame(X_all, columns=design.names) - - if verbose: - print(f"[LHS] giving best candidate with max|corr|={best_corr_val:.3f} (> target).") - return pd.DataFrame(best_candidate, columns=design.names) - + """Compatibility wrapper around the strict campaign LHS implementation.""" + return _strict_lhs_dataframe( + design=design, + n=n, + seed=seed, + snap_to_grids=snap_to_grids, + row_constraints=row_constraints, + max_abs_corr=max_abs_corr, + max_attempts=max_attempts, + oversample=oversample, + batch_size=batch_size, + subset_tries=subset_tries, + samples_per_attempt=samples_per_attempt, + verbose=verbose, + ) diff --git a/src/mobo_kit/main.py b/src/mobo_kit/main.py index c222b44..a5c9e2e 100644 --- a/src/mobo_kit/main.py +++ b/src/mobo_kit/main.py @@ -7,17 +7,21 @@ import os import sys -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, Sequence +import numpy as np import pandas as pd import torch import matplotlib.pyplot as plt import yaml from .utils import ( - load_csv, split_XY, csv_to_config, set_seeds, select_device, - get_objective_names + parse_campaign_csv, + split_XY, + set_seeds, + select_device, + get_objective_names, ) -from .design import build_input_spec_list, build_design +from .design import Design, build_design_from_config from .data import x_normalizer_np from .models import fit_gp_models, posterior_report from .plotting import plot_parity_np @@ -25,6 +29,10 @@ from .metrics import compute_ref_pareto_hv from .constraints import constraints_from_config from .lhs import lhs_dataframe_optimized +from .production_gate import ( + ProductionApprovalError, + block_legacy_campaign_proposal, +) def generate_initial_experiments( @@ -34,14 +42,14 @@ def generate_initial_experiments( seed: int = 42, verbose: bool = True, max_abs_corr: Optional[float] = None, - max_attempts: int = 100 + max_attempts: int = 100, ) -> Dict[str, Any]: """ Generate initial experiments using Latin Hypercube Sampling. - + This function is useful when you have a design space but no existing data. It generates a CSV file with initial experiments to run. - + Args: config_path: Path to YAML configuration file n_samples: Number of initial experiments to generate @@ -50,7 +58,7 @@ def generate_initial_experiments( verbose: Whether to print progress information max_abs_corr: Maximum absolute correlation between variables (optional) max_attempts: Maximum attempts for LHS generation - + Returns: Dictionary with generation results and metadata """ @@ -60,28 +68,28 @@ def generate_initial_experiments( print(f"Generating {n_samples} initial experiments...") print(f"Config: {config_path}") print(f"Output: {save_path}") - + # Set random seed set_seeds(seed) - + # Load configuration - with open(config_path, 'r') as f: + with open(config_path, "r") as f: config = yaml.safe_load(f) - - # Build design space - space = build_design(config) - + + # Build design space through the validated config-to-design path. + space = build_design_from_config(config) + if verbose: print(f"Design space: {len(space.names)} variables") print(f"Variables: {', '.join(space.names)}") - + # Get constraints row_constraints = constraints_from_config(config, space) - + # Generate LHS samples if verbose: print("Generating Latin Hypercube samples...") - + lhs_df = lhs_dataframe_optimized( design=space, n=n_samples, @@ -90,17 +98,19 @@ def generate_initial_experiments( row_constraints=row_constraints, max_abs_corr=max_abs_corr, max_attempts=max_attempts, - verbose=verbose + verbose=verbose, ) - + # Save to CSV + output_parent = os.path.dirname(os.path.abspath(save_path)) + os.makedirs(output_parent, exist_ok=True) lhs_df.to_csv(save_path, index=False) - + if verbose: print(f"Generated {len(lhs_df)} initial experiments") print(f"Saved to: {save_path}") print("=" * 50) - + return { "status": "success", "n_samples": len(lhs_df), @@ -108,23 +118,90 @@ def generate_initial_experiments( "save_path": save_path, "seed": seed, "variables": space.names, - "constraints_applied": len(row_constraints) > 0 if row_constraints else False + "constraints_applied": len(row_constraints) > 0 if row_constraints else False, } +def _validate_candidate_batch( + batch_result: Dict[str, Any], + design: Design, + observed_inputs: pd.DataFrame, + batch_size: int, +) -> np.ndarray: + """Fail closed on incomplete, duplicate, observed, or off-grid batches.""" + + if not isinstance(batch_result, dict) or "X_phys" not in batch_result: + raise ValueError("Candidate proposal did not return an 'X_phys' array.") + try: + candidates = np.asarray(batch_result["X_phys"], dtype=float) + except (TypeError, ValueError) as exc: + raise ValueError("Candidate physical inputs must be numeric.") from exc + + expected_shape = (batch_size, len(design.names)) + if candidates.shape != expected_shape: + raise ValueError( + "Candidate proposal returned an incomplete or malformed batch: " + f"expected shape {expected_shape}, got {candidates.shape}." + ) + if not np.isfinite(candidates).all(): + raise ValueError("Candidate proposal contains non-finite physical inputs.") + + tolerances = np.maximum(np.abs(design.steps) * 1e-9, 1e-10) + grid_indices = np.empty(expected_shape, dtype=np.int64) + for column_index, (name, grid) in enumerate(zip(design.names, design.var_list)): + distances = np.abs(candidates[:, column_index, None] - grid[None, :]) + nearest_indices = np.argmin(distances, axis=1) + nearest_distances = distances[ + np.arange(batch_size, dtype=np.int64), nearest_indices + ] + if np.any(nearest_distances > tolerances[column_index]): + bad_rows = np.flatnonzero( + nearest_distances > tolerances[column_index] + ).tolist() + raise ValueError( + f"Candidate input '{name}' is off-grid at batch rows {bad_rows}." + ) + grid_indices[:, column_index] = nearest_indices + + if np.unique(grid_indices, axis=0).shape[0] != batch_size: + raise ValueError("Candidate proposal contains duplicate snapped recipes.") + + observed = observed_inputs.loc[:, design.names].to_numpy(dtype=float) + if observed.size: + matches_observed = np.all( + np.isclose( + candidates[:, None, :], + observed[None, :, :], + rtol=0.0, + atol=tolerances, + ), + axis=2, + ) + repeated_rows = np.flatnonzero(matches_observed.any(axis=1)).tolist() + if repeated_rows: + raise ValueError( + "Candidate proposal repeats observed recipes at batch rows " + f"{repeated_rows}." + ) + + return candidates + + def run_mobo_experiment( csv_path: str, - save_dir: str = "results/experiment", + save_dir: str = "local_outputs/experiment", config_path: Optional[str] = None, seed: int = 42, device: str = "auto", verbose: bool = True, batch_size: int = 5, - propose_candidates: bool = True + propose_candidates: bool = False, + reference_point: Optional[Sequence[float]] = None, + num_restarts: int = 20, ) -> Dict[str, Any]: """ Run a complete MOBO experiment from CSV data. - + Args: csv_path: Path to CSV file with experimental data save_dir: Directory to save results @@ -133,83 +210,118 @@ def run_mobo_experiment( device: Device to use ("auto", "cpu", "cuda") verbose: Whether to print progress information batch_size: Number of candidates to propose for next batch - propose_candidates: Whether to propose new candidates for next experiments - + propose_candidates: Request campaign candidates. Step 2A always blocks + this legacy path; a reviewed Step 2B campaign adapter is required. + reference_point: Explicit hypervolume reference point in the exact same + transformed objective space as ``Y``. Required when proposing. + num_restarts: Number of acquisition-optimization restarts. + Returns: Dictionary with experiment results and metadata """ if verbose: print("MOBO-Kit: Multi-objective Bayesian Optimization Toolkit") print("=" * 60) - + # Set random seeds set_seeds(seed) - + # Select device if device == "auto": device_obj = select_device("cuda") else: device_obj = select_device(device) - + if verbose: print(f"Using device: {device_obj}") print(f"Loading data from: {csv_path}") - - # Load and process data - df = load_csv(csv_path) - - # Load or generate configuration - if config_path and os.path.exists(config_path): - import yaml - with open(config_path, 'r') as f: + + # Load the explicit configuration first when supplied, then parse the CSV + # exactly once against the expected objective names. + if config_path is not None: + if not os.path.isfile(config_path): + raise FileNotFoundError(f"Configuration file not found: {config_path}") + with open(config_path, "r") as f: config = yaml.safe_load(f) + if propose_candidates: + # This executes before campaign CSV parsing, model fitting, or any + # output-directory creation. Unapproved configs receive complete + # gate errors; approved configs still cannot enter the legacy + # raw-objective proposal implementation. + block_legacy_campaign_proposal(config) + parsed_csv = parse_campaign_csv( + csv_path, expected_objectives=get_objective_names(config) + ) if verbose: print(f"Loaded config from: {config_path}") else: - config = csv_to_config(csv_path) + if propose_candidates: + raise ProductionApprovalError( + [ + "Campaign candidate proposal requires an explicit resolved " + "production configuration; CSV auto-configuration is not " + "scientific approval." + ] + ) + parsed_csv = parse_campaign_csv(csv_path) + config = parsed_csv.config if verbose: print("Auto-generated config from CSV metadata") - - # Build design space - specs = build_input_spec_list(config["inputs"]) - space = build_design(specs) - + + df = parsed_csv.data + + # Build the design through the same validated path used by LHS generation. + space = build_design_from_config(config) + # Split data into inputs and objectives X, Y = split_XY(df, space, config) - + + if propose_candidates and reference_point is None: + raise ValueError( + "Candidate proposal requires an explicit reference_point in the " + "same transformed objective space as the model outputs. MOBO-Kit " + "will not invent a campaign reference point." + ) + if verbose: - print(f"Loaded {len(X)} samples with {X.shape[1]} inputs and {Y.shape[1]} objectives") + print( + f"Loaded {len(X)} samples with {X.shape[1]} inputs and {Y.shape[1]} objectives" + ) print(f"Objective names: {get_objective_names(config)}") - + # Normalize inputs to [0,1] range X_norm = x_normalizer_np(X, space) - + # Convert to torch tensors (Y is used directly, not standardized) X_t = torch.tensor(X_norm, dtype=torch.float64, device=device_obj) Y_t = torch.tensor(Y.values, dtype=torch.float64, device=device_obj) - + if verbose: print("Fitting Gaussian Process models...") - + # Fit GP models model = fit_gp_models(X_t, Y_t) - + if verbose: print("Generating predictions...") - + # Generate predictions (already in original units due to internal standardization) pred_mean, pred_std = posterior_report(model, X_t) - + # Create output directory os.makedirs(save_dir, exist_ok=True) - + # Predictions are generated but not saved to separate CSV - + # Generate plots try: if verbose: print("Generating plots...") - + + # The runner writes diagnostics to disk and must also work on lab PCs, + # CI workers, and managed Python installs without a Tcl/Tk GUI runtime. + plt.switch_backend("Agg") + # Create parity plots parity_path = os.path.join(save_dir, "parity_plots.png") fig, metrics_df = plot_parity_np( @@ -218,35 +330,43 @@ def run_mobo_experiment( pred_std=pred_std, objective_names=get_objective_names(config), save=parity_path, - show_plot=False + show_plot=False, ) plt.close(fig) # Close the figure to free memory - + if verbose: print(f"Parity plots saved to: {parity_path}") - + except Exception as e: if verbose: print(f"Warning: Could not generate plots: {e}") - + # Propose new candidates for next batch candidates = None if propose_candidates: try: if verbose: print("Proposing new candidates for next batch...") - + + ref_point_np = np.asarray(reference_point, dtype=float) + if ref_point_np.shape != (Y.shape[1],): + raise ValueError( + "reference_point must contain exactly one value per objective " + f"({Y.shape[1]} expected, got shape {ref_point_np.shape})." + ) + # Compute reference point and hypervolume - _, pareto_Y_t, hv_val = compute_ref_pareto_hv(Y_t) - ref_point_t = torch.tensor([-0.01] * Y.shape[1], dtype=X_t.dtype, device=device_obj) - + ref_point_t, pareto_Y_t, hv_val = compute_ref_pareto_hv( + Y_t, ref_point_np=ref_point_np + ) + if verbose: print(f"Current hypervolume: {hv_val:.4f}") print(f"Pareto points: {pareto_Y_t.shape[0]}") - + # Get constraints row_constraints = constraints_from_config(config, space) - + # Propose batch batch_result = propose_batch( design=space, @@ -255,41 +375,49 @@ def run_mobo_experiment( ref_point_t=ref_point_t, batch_size=batch_size, row_constraints=row_constraints, - verbose=verbose + num_restarts=num_restarts, + verbose=verbose, ) - + + candidate_array = _validate_candidate_batch( + batch_result=batch_result, + design=space, + observed_inputs=X, + batch_size=batch_size, + ) + batch_result = dict(batch_result) + batch_result["X_phys"] = candidate_array + candidates = batch_result candidates_path = os.path.join(save_dir, "next_batch.csv") - - # Create next_batch.csv in the same format as input CSV with metadata - # Load the original CSV to get metadata structure - original_df = load_csv(csv_path) - - # Create new candidates DataFrame with same structure - new_candidates_df = pd.DataFrame( - batch_result['X_phys'], - columns=space.names - ) - + + # Retain the legacy flat-table export for the explicit qNEHVI path. + # It is not a metadata-style campaign CSV and therefore is not a + # supported round-trip input to parse_campaign_csv. + new_candidates_df = pd.DataFrame(candidate_array, columns=space.names) + # Add empty objective columns to match original format objective_names = get_objective_names(config) for obj_name in objective_names: new_candidates_df[obj_name] = "" - + # Combine original data with new candidates - combined_df = pd.concat([original_df, new_candidates_df], ignore_index=True) - + combined_df = pd.concat([df, new_candidates_df], ignore_index=True) + # Save the combined CSV combined_df.to_csv(candidates_path, index=False) - + if verbose: print(f"Proposed {batch_size} candidates for next batch") print(f"Candidates saved to: {candidates_path}") - + except Exception as e: if verbose: - print(f"Warning: Could not propose candidates: {e}") - + print(f"Candidate proposal failed: {e}") + raise RuntimeError( + "Candidate proposal failed; no batch was accepted." + ) from e + # Prepare results summary results = { "status": "success", @@ -301,40 +429,38 @@ def run_mobo_experiment( "seed": seed, "save_dir": save_dir, "config": config, - "candidates": candidates + "candidates": candidates, } - + if verbose: print(f"Experiment completed successfully!") print(f"Results saved to: {save_dir}") - + return results def main(): """ Main entry point for running MOBO-Kit with default settings. - + This function runs a complete MOBO experiment using the default CSV file and saves results to the default output directory. """ # Default paths csv_path = "data/processed/configCSV_example.csv" - save_dir = "results/demo" - + save_dir = "local_outputs/demo" + # Check if default CSV exists if not os.path.exists(csv_path): print(f"Error: Default data file not found: {csv_path}") print("Please provide a valid CSV file path or ensure the default file exists.") sys.exit(1) - + try: results = run_mobo_experiment( - csv_path=csv_path, - save_dir=save_dir, - verbose=True + csv_path=csv_path, save_dir=save_dir, verbose=True ) - + print("\n" + "=" * 60) print("EXPERIMENT SUMMARY:") print(f" Samples: {results['n_samples']}") @@ -343,11 +469,12 @@ def main(): print(f" Objectives: {', '.join(results['objective_names'])}") print(f" Results: {results['save_dir']}") print("=" * 60) - + except Exception as e: print(f"Error running MOBO experiment: {e}") if "--verbose" in sys.argv: import traceback + traceback.print_exc() sys.exit(1) diff --git a/src/mobo_kit/model_validation.py b/src/mobo_kit/model_validation.py new file mode 100644 index 0000000..6040064 --- /dev/null +++ b/src/mobo_kit/model_validation.py @@ -0,0 +1,1009 @@ +"""Strict GP fitting and exact leave-one-out validation for robustness studies. + +This module intentionally does not use the historical model-selection helper in +``models.py``. That helper prints fitting failures and continues with fallback +hyperparameters, which is appropriate for its exploratory notebook use but not +for an auditable robustness study. Every fit here either returns a structured +record or raises :class:`ModelFitError` without silently changing its contract. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +import hashlib +import json +from math import log, pi +from numbers import Real +from time import perf_counter +from typing import Any, Hashable, Iterable, Mapping, Sequence +import warnings + +import gpytorch +import numpy as np +import pandas as pd +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize +from gpytorch.constraints import GreaterThan +from gpytorch.kernels import MaternKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood +from scipy.stats import spearmanr +import torch + + +DEFAULT_CURRENT_NAME = "default_current" +CONSERVATIVE_NAME = "conservative" +DEFAULT_CURRENT_MIN_NOISE = 1.0e-3 +CONSERVATIVE_MIN_NOISE = 0.01 +CONSERVATIVE_MIN_LENGTHSCALE = 0.05 +GAUSSIAN_95_Z = 1.959963984540054 +VERY_SMALL_NORMALIZED_LENGTHSCALE = 0.05 +EXTREMELY_LARGE_NORMALIZED_LENGTHSCALE = 10.0 + + +def _finite_positive(value: Any, *, field_name: str) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise ValueError(f"{field_name} must be a real non-boolean number.") + result = float(value) + if not np.isfinite(result) or result <= 0.0: + raise ValueError(f"{field_name} must be finite and strictly positive.") + return result + + +def _seed(value: Any) -> int: + if ( + isinstance(value, (bool, np.bool_)) + or not isinstance(value, (int, np.integer)) + or int(value) < 0 + ): + raise ValueError("seed must be a non-negative integer.") + return int(value) + + +@dataclass(frozen=True) +class ModelVariantSpec: + """One explicit GP model contract used by Step 2C.""" + + name: str + min_noise: float + min_lengthscale: float | None + kernel_name: str = "matern_2.5_ard" + + def __post_init__(self) -> None: + if self.name not in {DEFAULT_CURRENT_NAME, CONSERVATIVE_NAME}: + raise ValueError( + "Model variant name must be 'default_current' or 'conservative'." + ) + noise = _finite_positive(self.min_noise, field_name="min_noise") + lengthscale = ( + None + if self.min_lengthscale is None + else _finite_positive(self.min_lengthscale, field_name="min_lengthscale") + ) + if self.kernel_name != "matern_2.5_ard": + raise ValueError("Only the audited matern_2.5_ard kernel is supported.") + if self.name == DEFAULT_CURRENT_NAME and ( + noise != DEFAULT_CURRENT_MIN_NOISE or lengthscale is not None + ): + raise ValueError( + "default_current must preserve the Step 2B noise floor of 1e-3 " + "and must not add a lengthscale floor." + ) + if self.name == CONSERVATIVE_NAME and ( + noise != CONSERVATIVE_MIN_NOISE + or lengthscale != CONSERVATIVE_MIN_LENGTHSCALE + ): + raise ValueError( + "conservative must use min_noise=0.01 and min_lengthscale=0.05." + ) + object.__setattr__(self, "min_noise", noise) + object.__setattr__(self, "min_lengthscale", lengthscale) + + +DEFAULT_CURRENT = ModelVariantSpec( + DEFAULT_CURRENT_NAME, + min_noise=DEFAULT_CURRENT_MIN_NOISE, + min_lengthscale=None, +) +CONSERVATIVE = ModelVariantSpec( + CONSERVATIVE_NAME, + min_noise=CONSERVATIVE_MIN_NOISE, + min_lengthscale=CONSERVATIVE_MIN_LENGTHSCALE, +) + + +def model_variant_spec(name: str) -> ModelVariantSpec: + """Return one of the two fixed Step 2C model contracts.""" + if name == DEFAULT_CURRENT_NAME: + return DEFAULT_CURRENT + if name == CONSERVATIVE_NAME: + return CONSERVATIVE + raise ValueError(f"Unsupported model variant {name!r}.") + + +@dataclass(frozen=True) +class ModelFitWarning: + variant_name: str + fit_key: str + omitted_sample_id: Hashable | None + objective_index: int + objective_name: str + stage: str + warning_category: str + message: str + + +class ModelFitError(RuntimeError): + """A strict model-fit failure with all warnings observed before failure.""" + + def __init__( + self, + *, + variant_name: str, + fit_key: str, + omitted_sample_id: Hashable | None, + objective_index: int, + objective_name: str, + stage: str, + cause: BaseException, + fit_warnings: Sequence[ModelFitWarning], + ) -> None: + self.variant_name = variant_name + self.fit_key = fit_key + self.omitted_sample_id = omitted_sample_id + self.objective_index = objective_index + self.objective_name = objective_name + self.stage = stage + self.cause = cause + self.fit_warnings = tuple(fit_warnings) + super().__init__( + "Strict GP fit failed: " + f"variant={variant_name!r}, fit_key={fit_key!r}, " + f"objective={objective_name!r}, stage={stage!r}, " + f"cause={type(cause).__name__}: {cause}" + ) + + +@dataclass(frozen=True) +class ModelFitCacheKey: + variant_name: str + cohort_fingerprint: str + omitted_sample_key: str + objective_names: tuple[str, ...] + seed: int + + +@dataclass(frozen=True) +class FittedModelRecord: + variant: ModelVariantSpec + model: ModelListGP + train_X: torch.Tensor + train_Y: torch.Tensor + sample_ids: tuple[Hashable, ...] + objective_names: tuple[str, ...] + fit_key: str + omitted_sample_id: Hashable | None + seed: int + cohort_fingerprint: str + training_fingerprint: str + fit_runtime_seconds: float + warnings: tuple[ModelFitWarning, ...] + + +@dataclass +class ModelFitCache: + """In-memory fit cache shared by LOOCV and observation influence.""" + + records: dict[ModelFitCacheKey, FittedModelRecord] = field(default_factory=dict) + hits: int = 0 + misses: int = 0 + + def get(self, key: ModelFitCacheKey) -> FittedModelRecord | None: + record = self.records.get(key) + if record is None: + self.misses += 1 + else: + self.hits += 1 + return record + + def store(self, key: ModelFitCacheKey, record: FittedModelRecord) -> None: + existing = self.records.get(key) + if existing is not None and existing is not record: + raise ValueError(f"A different fit already exists for cache key {key}.") + self.records[key] = record + + +@dataclass(frozen=True) +class LOOCVPrediction: + variant_name: str + omitted_sample_id: Hashable + row_role: str + is_control: bool + objective_index: int + objective_name: str + observed: float + predicted_mean: float + latent_std: float + predictive_std: float + prediction_error: float + residual: float + standardized_residual: float + within_68_percent_interval: bool + within_95_percent_interval: bool + gaussian_nlpd: float + fold_fit_key: str + fold_fit_warning_count: int + + +@dataclass(frozen=True) +class PredictionMetricRecord: + variant_name: str + objective_index: int + objective_name: str + prediction_count: int + mae: float + rmse: float + r_squared: float + r_squared_warning: str + spearman_rank_correlation: float + mean_signed_error: float + median_absolute_error: float + coverage_68_percent: float + coverage_95_percent: float + mean_standardized_residual: float + maximum_absolute_standardized_residual: float + mean_gaussian_nlpd: float + + +@dataclass(frozen=True) +class HyperparameterRecord: + variant_name: str + fit_key: str + omitted_sample_id: Hashable | None + objective_index: int + objective_name: str + kernel_type: str + likelihood_noise: float + outputscale: float + ard_lengthscales: tuple[float, ...] + input_names: tuple[str, ...] + configured_min_noise: float + configured_min_lengthscale: float | None + noise_constraint_lower_bound: float + lengthscale_constraint_lower_bound: float + noise_near_floor: bool + lengthscales_near_floor: tuple[bool, ...] + lengthscales_very_small_normalized_domain: tuple[bool, ...] + lengthscales_extremely_large_flat: tuple[bool, ...] + very_small_lengthscale_threshold: float = VERY_SMALL_NORMALIZED_LENGTHSCALE + extremely_large_lengthscale_threshold: float = ( + EXTREMELY_LARGE_NORMALIZED_LENGTHSCALE + ) + input_parameter_space: str = "normalized_0_1" + outcome_parameter_space: str = "standardized_internal" + + def as_flat_dict(self) -> dict[str, Any]: + result = asdict(self) + result.pop("ard_lengthscales") + result.pop("input_names") + result.pop("lengthscales_near_floor") + result.pop("lengthscales_very_small_normalized_domain") + result.pop("lengthscales_extremely_large_flat") + result["any_lengthscale_very_small_normalized_domain"] = any( + self.lengthscales_very_small_normalized_domain + ) + result["any_lengthscale_extremely_large_flat"] = any( + self.lengthscales_extremely_large_flat + ) + for input_name, value, near_floor, very_small, extremely_large in zip( + self.input_names, + self.ard_lengthscales, + self.lengthscales_near_floor, + self.lengthscales_very_small_normalized_domain, + self.lengthscales_extremely_large_flat, + ): + result[f"ard_lengthscale_{input_name}"] = value + result[f"ard_lengthscale_{input_name}_near_floor"] = near_floor + result[f"ard_lengthscale_{input_name}_very_small_normalized_domain"] = ( + very_small + ) + result[f"ard_lengthscale_{input_name}_extremely_large_flat"] = ( + extremely_large + ) + return result + + +@dataclass(frozen=True) +class ExactLOOCVResult: + variant: ModelVariantSpec + predictions: tuple[LOOCVPrediction, ...] + metrics: tuple[PredictionMetricRecord, ...] + fold_records: Mapping[Hashable, FittedModelRecord] + cohort_fingerprint: str + cache: ModelFitCache + + def predictions_frame(self) -> pd.DataFrame: + return pd.DataFrame(asdict(row) for row in self.predictions) + + def metrics_frame(self) -> pd.DataFrame: + return pd.DataFrame(asdict(row) for row in self.metrics) + + +@dataclass(frozen=True) +class ModelValidationResult: + variant: ModelVariantSpec + full_fit: FittedModelRecord + loocv: ExactLOOCVResult + hyperparameters: tuple[HyperparameterRecord, ...] + + def hyperparameters_frame(self) -> pd.DataFrame: + return pd.DataFrame(row.as_flat_dict() for row in self.hyperparameters) + + def warnings_frame(self) -> pd.DataFrame: + records = [self.full_fit, *self.loocv.fold_records.values()] + return fit_warnings_frame(records) + + +def _sample_key(value: Hashable | None) -> str: + if value is None: + return "" + return f"{type(value).__name__}:{value!r}" + + +def _tensor_bytes(value: torch.Tensor) -> bytes: + tensor = value.detach().cpu().contiguous() + return tensor.numpy().tobytes() + + +def dataset_fingerprint( + X: torch.Tensor, + Y: torch.Tensor, + sample_ids: Sequence[Hashable], +) -> str: + """Return a deterministic fingerprint for cache and provenance checks.""" + digest = hashlib.sha256() + for tensor in (X, Y): + digest.update(str(tensor.dtype).encode("utf-8")) + digest.update(json.dumps(tuple(tensor.shape)).encode("utf-8")) + digest.update(_tensor_bytes(tensor)) + serialized_ids = [f"{type(value).__name__}:{value!r}" for value in sample_ids] + digest.update(json.dumps(serialized_ids, separators=(",", ":")).encode("utf-8")) + return digest.hexdigest() + + +def _validate_dataset( + X: torch.Tensor, + Y: torch.Tensor, + sample_ids: Sequence[Hashable], + objective_names: Sequence[str], + *, + minimum_rows: int, +) -> tuple[tuple[Hashable, ...], tuple[str, ...]]: + if not isinstance(X, torch.Tensor) or X.ndim != 2: + raise ValueError("X must be a torch tensor with shape (N, D).") + if not isinstance(Y, torch.Tensor) or Y.ndim != 2: + raise ValueError("Y must be a torch tensor with shape (N, M).") + if not X.is_floating_point() or not Y.is_floating_point(): + raise TypeError("X and Y must use floating dtypes.") + if X.device != Y.device or X.dtype != Y.dtype: + raise ValueError("X and Y must share dtype and device.") + if X.shape[0] != Y.shape[0] or X.shape[0] < minimum_rows: + raise ValueError( + f"X and Y must share at least {minimum_rows} rows; " + f"got {X.shape[0]} and {Y.shape[0]}." + ) + if X.shape[1] == 0 or Y.shape[1] == 0: + raise ValueError("X and Y must each contain at least one column.") + if not torch.isfinite(X).all() or not torch.isfinite(Y).all(): + raise ValueError("X and Y must contain only finite values.") + if torch.any(X < 0.0) or torch.any(X > 1.0): + raise ValueError("X must be normalized to [0, 1].") + ids = tuple(sample_ids) + if len(ids) != X.shape[0] or len(set(ids)) != len(ids): + raise ValueError("sample_ids must be unique and aligned with X/Y rows.") + names = tuple(objective_names) + if ( + len(names) != Y.shape[1] + or any(not isinstance(name, str) or not name.strip() for name in names) + or len(set(names)) != len(names) + ): + raise ValueError( + "objective_names must be unique non-empty strings aligned with Y columns." + ) + return ids, tuple(name.strip() for name in names) + + +def _warning_rows( + caught: Sequence[warnings.WarningMessage], + *, + variant: ModelVariantSpec, + fit_key: str, + omitted_sample_id: Hashable | None, + objective_index: int, + objective_name: str, + stage: str, +) -> list[ModelFitWarning]: + return [ + ModelFitWarning( + variant_name=variant.name, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage=stage, + warning_category=warning.category.__name__, + message=str(warning.message), + ) + for warning in caught + ] + + +def _build_single_task_gp( + X: torch.Tensor, + y: torch.Tensor, + variant: ModelVariantSpec, +) -> SingleTaskGP: + lengthscale_constraint = ( + None + if variant.min_lengthscale is None + else GreaterThan(variant.min_lengthscale) + ) + kernel_kwargs: dict[str, Any] = { + "nu": 2.5, + "ard_num_dims": X.shape[1], + } + if lengthscale_constraint is not None: + kernel_kwargs["lengthscale_constraint"] = lengthscale_constraint + base_kernel = MaternKernel(**kernel_kwargs) + covar_module = ScaleKernel(base_kernel) + likelihood = GaussianLikelihood(noise_constraint=GreaterThan(variant.min_noise)) + return SingleTaskGP( + X, + y, + covar_module=covar_module, + likelihood=likelihood, + outcome_transform=Standardize(m=1), + ) + + +def fit_model_variant( + X: torch.Tensor, + Y: torch.Tensor, + *, + sample_ids: Sequence[Hashable], + objective_names: Sequence[str], + variant: ModelVariantSpec, + seed: int = 73, + fit_key: str = "full", + omitted_sample_id: Hashable | None = None, + cohort_fingerprint: str | None = None, + cache: ModelFitCache | None = None, +) -> FittedModelRecord: + """Fit one strict independent GP per objective and return an audit record.""" + ids, names = _validate_dataset(X, Y, sample_ids, objective_names, minimum_rows=2) + if not isinstance(variant, ModelVariantSpec): + raise TypeError("variant must be a ModelVariantSpec.") + resolved_seed = _seed(seed) + if not isinstance(fit_key, str) or not fit_key.strip(): + raise ValueError("fit_key must be a non-empty string.") + fit_key = fit_key.strip() + training_hash = dataset_fingerprint(X, Y, ids) + cohort_hash = training_hash if cohort_fingerprint is None else cohort_fingerprint + if not isinstance(cohort_hash, str) or not cohort_hash.strip(): + raise ValueError("cohort_fingerprint must be None or a non-empty string.") + cache_key = ModelFitCacheKey( + variant_name=variant.name, + cohort_fingerprint=cohort_hash, + omitted_sample_key=_sample_key(omitted_sample_id), + objective_names=names, + seed=resolved_seed, + ) + if cache is not None: + cached = cache.get(cache_key) + if cached is not None: + if cached.training_fingerprint != training_hash: + raise ValueError( + "Cached model training fingerprint does not match supplied data." + ) + return cached + + np.random.seed(resolved_seed) + torch.manual_seed(resolved_seed) + if torch.cuda.is_available(): + torch.cuda.manual_seed_all(resolved_seed) + + started = perf_counter() + models: list[SingleTaskGP] = [] + fit_warning_rows: list[ModelFitWarning] = [] + for objective_index, objective_name in enumerate(names): + caught: list[warnings.WarningMessage] = [] + try: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + gp = _build_single_task_gp( + X, Y[:, objective_index : objective_index + 1], variant + ) + fit_warning_rows.extend( + _warning_rows( + caught, + variant=variant, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="construct", + ) + ) + except Exception as exc: + fit_warning_rows.extend( + _warning_rows( + caught, + variant=variant, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="construct", + ) + ) + raise ModelFitError( + variant_name=variant.name, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="construct", + cause=exc, + fit_warnings=fit_warning_rows, + ) from exc + + mll = ExactMarginalLogLikelihood(gp.likelihood, gp) + try: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + fit_gpytorch_mll(mll) + fit_warning_rows.extend( + _warning_rows( + caught, + variant=variant, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="optimize", + ) + ) + except Exception as exc: + fit_warning_rows.extend( + _warning_rows( + caught, + variant=variant, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="optimize", + ) + ) + raise ModelFitError( + variant_name=variant.name, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="optimize", + cause=exc, + fit_warnings=fit_warning_rows, + ) from exc + gp.eval() + gp.likelihood.eval() + models.append(gp) + + record = FittedModelRecord( + variant=variant, + model=ModelListGP(*models), + train_X=X.detach().clone(), + train_Y=Y.detach().clone(), + sample_ids=ids, + objective_names=names, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + seed=resolved_seed, + cohort_fingerprint=cohort_hash, + training_fingerprint=training_hash, + fit_runtime_seconds=perf_counter() - started, + warnings=tuple(fit_warning_rows), + ) + record.model.eval() + if cache is not None: + cache.store(cache_key, record) + return record + + +def _posterior_prediction( + record: FittedModelRecord, + X: torch.Tensor, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + with torch.no_grad(): + latent = record.model.posterior(X, observation_noise=False) + predictive = record.model.posterior(X, observation_noise=True) + mean = latent.mean.detach().cpu().double().numpy() + latent_std = latent.variance.clamp_min(0.0).sqrt().detach().cpu().double().numpy() + predictive_std = ( + predictive.variance.clamp_min(0.0).sqrt().detach().cpu().double().numpy() + ) + if mean.shape != latent_std.shape or mean.shape != predictive_std.shape: + raise RuntimeError("Posterior mean and uncertainty shapes do not match.") + if not ( + np.all(np.isfinite(mean)) + and np.all(np.isfinite(latent_std)) + and np.all(np.isfinite(predictive_std)) + and np.all(predictive_std > 0.0) + ): + raise RuntimeError("Posterior predictions must be finite with positive noise.") + if np.any(predictive_std + 1.0e-12 < latent_std): + raise RuntimeError("Predictive uncertainty cannot be below latent uncertainty.") + return mean, latent_std, predictive_std + + +def compute_prediction_metrics( + observed: Sequence[float] | np.ndarray, + predicted_mean: Sequence[float] | np.ndarray, + predictive_std: Sequence[float] | np.ndarray, + *, + variant_name: str = "unspecified", + objective_index: int = 0, + objective_name: str = "objective", + small_n_warning_threshold: int = 20, +) -> PredictionMetricRecord: + """Compute declared predictive metrics from original-unit predictions.""" + actual = np.asarray(observed, dtype=float) + predicted = np.asarray(predicted_mean, dtype=float) + uncertainty = np.asarray(predictive_std, dtype=float) + if ( + actual.ndim != 1 + or predicted.shape != actual.shape + or uncertainty.shape != actual.shape + ): + raise ValueError("observed, predicted_mean, and predictive_std must align.") + if actual.size == 0: + raise ValueError("At least one prediction is required.") + if not ( + np.all(np.isfinite(actual)) + and np.all(np.isfinite(predicted)) + and np.all(np.isfinite(uncertainty)) + ): + raise ValueError("Prediction metrics require finite inputs.") + if np.any(uncertainty <= 0.0): + raise ValueError("predictive_std must be strictly positive.") + if ( + isinstance(small_n_warning_threshold, bool) + or not isinstance(small_n_warning_threshold, (int, np.integer)) + or int(small_n_warning_threshold) < 2 + ): + raise ValueError("small_n_warning_threshold must be an integer >= 2.") + + errors = predicted - actual + residuals = -errors + standardized = residuals / uncertainty + squared_error = errors**2 + centered = actual - actual.mean() + denominator = float(np.sum(centered**2)) + if actual.size < 2 or denominator <= 0.0: + r_squared = np.nan + r_squared_warning = ( + "R² is undefined for fewer than two or constant observations." + ) + else: + r_squared = 1.0 - float(np.sum(squared_error)) / denominator + r_squared_warning = ( + f"R² is unstable with small N={actual.size}." + if actual.size < int(small_n_warning_threshold) + else "" + ) + if actual.size < 2 or np.unique(actual).size < 2 or np.unique(predicted).size < 2: + spearman = np.nan + else: + spearman = float(spearmanr(actual, predicted).statistic) + nlpd = 0.5 * np.log(2.0 * pi * uncertainty**2) + 0.5 * standardized**2 + return PredictionMetricRecord( + variant_name=str(variant_name), + objective_index=int(objective_index), + objective_name=str(objective_name), + prediction_count=int(actual.size), + mae=float(np.mean(np.abs(errors))), + rmse=float(np.sqrt(np.mean(squared_error))), + r_squared=float(r_squared), + r_squared_warning=r_squared_warning, + spearman_rank_correlation=float(spearman), + mean_signed_error=float(np.mean(errors)), + median_absolute_error=float(np.median(np.abs(errors))), + coverage_68_percent=float(np.mean(np.abs(standardized) <= 1.0)), + coverage_95_percent=float(np.mean(np.abs(standardized) <= GAUSSIAN_95_Z)), + mean_standardized_residual=float(np.mean(standardized)), + maximum_absolute_standardized_residual=float(np.max(np.abs(standardized))), + mean_gaussian_nlpd=float(np.mean(nlpd)), + ) + + +def _summarize_predictions( + predictions: Sequence[LOOCVPrediction], + variant: ModelVariantSpec, + objective_names: Sequence[str], +) -> tuple[PredictionMetricRecord, ...]: + rows: list[PredictionMetricRecord] = [] + for objective_index, objective_name in enumerate(objective_names): + selected = [ + row for row in predictions if row.objective_index == objective_index + ] + rows.append( + compute_prediction_metrics( + [row.observed for row in selected], + [row.predicted_mean for row in selected], + [row.predictive_std for row in selected], + variant_name=variant.name, + objective_index=objective_index, + objective_name=objective_name, + ) + ) + return tuple(rows) + + +def run_exact_loocv( + X: torch.Tensor, + Y: torch.Tensor, + *, + sample_ids: Sequence[Hashable], + objective_names: Sequence[str], + variant: ModelVariantSpec, + seed: int = 73, + row_roles: Sequence[str] | None = None, + control_sample_ids: Iterable[Hashable] = (), + cache: ModelFitCache | None = None, +) -> ExactLOOCVResult: + """Fit exactly one N-1 model per row and predict every held-out objective.""" + ids, names = _validate_dataset(X, Y, sample_ids, objective_names, minimum_rows=3) + roles = tuple("observation" for _ in ids) if row_roles is None else tuple(row_roles) + if len(roles) != len(ids) or any( + not isinstance(role, str) or not role.strip() for role in roles + ): + raise ValueError("row_roles must contain one non-empty string per row.") + control_ids = set(control_sample_ids) + resolved_cache = ModelFitCache() if cache is None else cache + cohort_hash = dataset_fingerprint(X, Y, ids) + fold_records: dict[Hashable, FittedModelRecord] = {} + prediction_rows: list[LOOCVPrediction] = [] + for omitted_index, omitted_id in enumerate(ids): + mask = torch.ones(X.shape[0], dtype=torch.bool, device=X.device) + mask[omitted_index] = False + fold_ids = tuple( + sample_id for index, sample_id in enumerate(ids) if index != omitted_index + ) + fit_key = f"omit:{_sample_key(omitted_id)}" + record = fit_model_variant( + X[mask], + Y[mask], + sample_ids=fold_ids, + objective_names=names, + variant=variant, + seed=seed, + fit_key=fit_key, + omitted_sample_id=omitted_id, + cohort_fingerprint=cohort_hash, + cache=resolved_cache, + ) + fold_records[omitted_id] = record + mean, latent_std, predictive_std = _posterior_prediction( + record, X[omitted_index : omitted_index + 1] + ) + for objective_index, objective_name in enumerate(names): + observed = float(Y[omitted_index, objective_index].item()) + predicted = float(mean[0, objective_index]) + latent_uncertainty = float(latent_std[0, objective_index]) + predictive_uncertainty = float(predictive_std[0, objective_index]) + error = predicted - observed + residual = -error + standardized = residual / predictive_uncertainty + nlpd = 0.5 * log(2.0 * pi * predictive_uncertainty**2) + 0.5 * ( + standardized**2 + ) + prediction_rows.append( + LOOCVPrediction( + variant_name=variant.name, + omitted_sample_id=omitted_id, + row_role=roles[omitted_index].strip(), + is_control=omitted_id in control_ids, + objective_index=objective_index, + objective_name=objective_name, + observed=observed, + predicted_mean=predicted, + latent_std=latent_uncertainty, + predictive_std=predictive_uncertainty, + prediction_error=error, + residual=residual, + standardized_residual=standardized, + within_68_percent_interval=abs(standardized) <= 1.0, + within_95_percent_interval=(abs(standardized) <= GAUSSIAN_95_Z), + gaussian_nlpd=nlpd, + fold_fit_key=record.fit_key, + fold_fit_warning_count=len(record.warnings), + ) + ) + expected_count = X.shape[0] * Y.shape[1] + if len(prediction_rows) != expected_count: + raise RuntimeError( + f"LOOCV produced {len(prediction_rows)} rows; expected {expected_count}." + ) + metrics = _summarize_predictions(prediction_rows, variant, names) + return ExactLOOCVResult( + variant=variant, + predictions=tuple(prediction_rows), + metrics=metrics, + fold_records=fold_records, + cohort_fingerprint=cohort_hash, + cache=resolved_cache, + ) + + +def _constraint_lower_bound(constraint: Any) -> float: + value = constraint.lower_bound.detach().cpu().double().reshape(-1) + return float(value[0].item()) + + +def extract_model_hyperparameters( + record: FittedModelRecord, + *, + input_names: Sequence[str], +) -> tuple[HyperparameterRecord, ...]: + """Extract comparable full/fold hyperparameters from a fitted model list.""" + names = tuple(input_names) + if len(names) != record.train_X.shape[1] or any( + not isinstance(name, str) or not name.strip() for name in names + ): + raise ValueError("input_names must align with the fitted input dimension.") + rows: list[HyperparameterRecord] = [] + for objective_index, (objective_name, gp) in enumerate( + zip(record.objective_names, record.model.models) + ): + base_kernel = gp.covar_module.base_kernel + lengthscales = tuple( + float(value) + for value in base_kernel.lengthscale.detach() + .cpu() + .double() + .reshape(-1) + .tolist() + ) + if len(lengthscales) != len(names): + raise RuntimeError("ARD lengthscales do not align with input names.") + noise = float(gp.likelihood.noise.detach().cpu().double().reshape(-1)[0].item()) + outputscale = float( + gp.covar_module.outputscale.detach().cpu().double().reshape(-1)[0].item() + ) + noise_floor = _constraint_lower_bound( + gp.likelihood.noise_covar.raw_noise_constraint + ) + lengthscale_floor = _constraint_lower_bound( + base_kernel.raw_lengthscale_constraint + ) + noise_near = noise <= noise_floor * 1.05 + 1.0e-12 + lengthscale_near = tuple( + value <= lengthscale_floor * 1.05 + 1.0e-12 for value in lengthscales + ) + lengthscale_very_small = tuple( + value <= VERY_SMALL_NORMALIZED_LENGTHSCALE for value in lengthscales + ) + lengthscale_extremely_large = tuple( + value >= EXTREMELY_LARGE_NORMALIZED_LENGTHSCALE for value in lengthscales + ) + rows.append( + HyperparameterRecord( + variant_name=record.variant.name, + fit_key=record.fit_key, + omitted_sample_id=record.omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + kernel_type=type(base_kernel).__name__, + likelihood_noise=noise, + outputscale=outputscale, + ard_lengthscales=lengthscales, + input_names=tuple(name.strip() for name in names), + configured_min_noise=record.variant.min_noise, + configured_min_lengthscale=record.variant.min_lengthscale, + noise_constraint_lower_bound=noise_floor, + lengthscale_constraint_lower_bound=lengthscale_floor, + noise_near_floor=noise_near, + lengthscales_near_floor=lengthscale_near, + lengthscales_very_small_normalized_domain=lengthscale_very_small, + lengthscales_extremely_large_flat=lengthscale_extremely_large, + ) + ) + return tuple(rows) + + +def fit_warnings_frame(records: Iterable[FittedModelRecord]) -> pd.DataFrame: + columns = [field.name for field in ModelFitWarning.__dataclass_fields__.values()] + rows = [asdict(warning) for record in records for warning in record.warnings] + return pd.DataFrame(rows, columns=columns) + + +def validate_model_variant( + X: torch.Tensor, + Y: torch.Tensor, + *, + sample_ids: Sequence[Hashable], + input_names: Sequence[str], + objective_names: Sequence[str], + variant: ModelVariantSpec, + seed: int = 73, + row_roles: Sequence[str] | None = None, + control_sample_ids: Iterable[Hashable] = (), + cache: ModelFitCache | None = None, +) -> ModelValidationResult: + """Fit the full model, run exact LOOCV, and extract every hyperparameter.""" + ids, names = _validate_dataset(X, Y, sample_ids, objective_names, minimum_rows=3) + resolved_cache = ModelFitCache() if cache is None else cache + cohort_hash = dataset_fingerprint(X, Y, ids) + full = fit_model_variant( + X, + Y, + sample_ids=ids, + objective_names=names, + variant=variant, + seed=seed, + fit_key="full", + omitted_sample_id=None, + cohort_fingerprint=cohort_hash, + cache=resolved_cache, + ) + loocv = run_exact_loocv( + X, + Y, + sample_ids=ids, + objective_names=names, + variant=variant, + seed=seed, + row_roles=row_roles, + control_sample_ids=control_sample_ids, + cache=resolved_cache, + ) + all_records = [full, *loocv.fold_records.values()] + hyperparameters = tuple( + hyperparameter + for record in all_records + for hyperparameter in extract_model_hyperparameters( + record, input_names=input_names + ) + ) + return ModelValidationResult( + variant=variant, + full_fit=full, + loocv=loocv, + hyperparameters=hyperparameters, + ) + + +__all__ = [ + "CONSERVATIVE", + "DEFAULT_CURRENT", + "ExactLOOCVResult", + "FittedModelRecord", + "HyperparameterRecord", + "LOOCVPrediction", + "ModelFitCache", + "ModelFitError", + "ModelFitWarning", + "ModelValidationResult", + "ModelVariantSpec", + "PredictionMetricRecord", + "compute_prediction_metrics", + "dataset_fingerprint", + "extract_model_hyperparameters", + "fit_model_variant", + "fit_warnings_frame", + "model_variant_spec", + "run_exact_loocv", + "validate_model_variant", +] diff --git a/src/mobo_kit/objectives.py b/src/mobo_kit/objectives.py new file mode 100644 index 0000000..5847b21 --- /dev/null +++ b/src/mobo_kit/objectives.py @@ -0,0 +1,351 @@ +"""Versioned, all-maximize objective transformations for MOBO acquisition.""" + +from __future__ import annotations + +from dataclasses import dataclass +from numbers import Real +from typing import Literal, Sequence + +import numpy as np +import torch +from botorch.acquisition.multi_objective.objective import MCMultiOutputObjective + + +Goal = Literal["maximize", "minimize", "target"] +TransformName = Literal[ + "identity", "affine", "gaussian_target", "negative_absolute_target" +] +UtilityBound = tuple[float | None, float | None] + + +def _finite_optional(value: float | None, *, field: str, name: str) -> float | None: + if value is None: + return None + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise ValueError( + f"Objective {name!r} field {field!r} must be a real non-boolean number." + ) + number = float(value) + if not np.isfinite(number): + raise ValueError(f"Objective {name!r} field {field!r} must be finite.") + return number + + +@dataclass(frozen=True) +class ObjectiveSpec: + """Immutable definition of one raw-output-to-utility transformation.""" + + name: str + goal: Goal + transform: TransformName + source_column: str | None = None + lower_anchor: float | None = None + upper_anchor: float | None = None + target: float | None = None + sigma: float | None = None + scale: float | None = None + clip: bool = False + + def __post_init__(self) -> None: + if not isinstance(self.name, str) or not self.name.strip(): + raise ValueError("Objective name must be a non-empty string.") + object.__setattr__(self, "name", self.name.strip()) + if self.goal not in {"maximize", "minimize", "target"}: + raise ValueError( + f"Objective {self.name!r} has unsupported goal {self.goal!r}." + ) + supported = { + "identity", + "affine", + "gaussian_target", + "negative_absolute_target", + } + if self.transform not in supported: + raise ValueError( + f"Objective {self.name!r} has unsupported transform " + f"{self.transform!r}." + ) + if self.source_column is not None and ( + not isinstance(self.source_column, str) or not self.source_column.strip() + ): + raise ValueError("source_column must be None or a non-empty string.") + if self.source_column is not None: + object.__setattr__(self, "source_column", self.source_column.strip()) + if not isinstance(self.clip, bool): + raise ValueError("clip must be a boolean.") + + numeric = { + field: _finite_optional(getattr(self, field), field=field, name=self.name) + for field in ( + "lower_anchor", + "upper_anchor", + "target", + "sigma", + "scale", + ) + } + for field, value in numeric.items(): + object.__setattr__(self, field, value) + + if self.transform == "identity": + if self.goal != "maximize": + raise ValueError("identity is supported only for maximize utilities.") + self._require_unused( + "lower_anchor", "upper_anchor", "target", "sigma", "scale" + ) + if self.clip: + raise ValueError( + "identity utilities cannot enable clip; approve their scale " + "upstream or use an explicit affine transform." + ) + elif self.transform == "affine": + if self.goal not in {"maximize", "minimize"}: + raise ValueError("affine requires goal 'maximize' or 'minimize'.") + if self.lower_anchor is None or self.upper_anchor is None: + raise ValueError("affine requires lower_anchor and upper_anchor.") + if self.lower_anchor >= self.upper_anchor: + raise ValueError("affine requires lower_anchor < upper_anchor.") + self._require_unused("target", "sigma", "scale") + elif self.transform == "gaussian_target": + if self.goal != "target": + raise ValueError("gaussian_target requires goal 'target'.") + if self.target is None or self.sigma is None: + raise ValueError("gaussian_target requires target and sigma.") + if self.sigma <= 0: + raise ValueError("gaussian_target sigma must be strictly positive.") + self._require_unused("lower_anchor", "upper_anchor", "scale") + if self.clip: + raise ValueError( + "gaussian_target is naturally bounded; clip is invalid." + ) + else: + if self.goal != "target": + raise ValueError("negative_absolute_target requires goal 'target'.") + if self.target is None or self.scale is None: + raise ValueError( + "negative_absolute_target requires target and explicit scale." + ) + if self.scale <= 0: + raise ValueError( + "negative_absolute_target scale must be strictly positive." + ) + self._require_unused("lower_anchor", "upper_anchor", "sigma") + if self.clip: + raise ValueError("clip is not supported for negative_absolute_target.") + + def _require_unused(self, *fields: str) -> None: + used = [field for field in fields if getattr(self, field) is not None] + if used: + raise ValueError( + f"Objective {self.name!r} transform {self.transform!r} does not " + f"accept parameter(s): {', '.join(used)}." + ) + + +class ObjectiveTransform: + """Apply an ordered objective contract to floating tensors ``[..., M]``.""" + + def __init__(self, specs: Sequence[ObjectiveSpec], *, version: str) -> None: + if not isinstance(version, str) or not version.strip(): + raise ValueError("Objective contract version must be a non-empty string.") + if not specs: + raise ValueError("At least one ObjectiveSpec is required.") + validated = tuple(specs) + if not all(isinstance(spec, ObjectiveSpec) for spec in validated): + raise TypeError("Every objective specification must be an ObjectiveSpec.") + names = [spec.name for spec in validated] + duplicates = sorted({name for name in names if names.count(name) > 1}) + if duplicates: + raise ValueError(f"Objective names must be unique; got {duplicates}.") + self.specs = validated + self.version = version.strip() + + @property + def objective_count(self) -> int: + return len(self.specs) + + @property + def names(self) -> tuple[str, ...]: + return tuple(spec.name for spec in self.specs) + + def transform(self, Y: torch.Tensor) -> torch.Tensor: + """Transform raw/model outputs while preserving shape, dtype, and device.""" + if not isinstance(Y, torch.Tensor): + raise TypeError("Y must be a torch.Tensor.") + if not Y.is_floating_point(): + raise TypeError("Y must use a floating dtype.") + if Y.ndim < 1 or Y.shape[-1] != self.objective_count: + raise ValueError( + f"Y final dimension must be {self.objective_count}; " + f"got shape {tuple(Y.shape)}." + ) + if not torch.isfinite(Y).all(): + raise ValueError("Y must contain only finite values.") + outputs: list[torch.Tensor] = [] + for index, spec in enumerate(self.specs): + raw = Y[..., index] + if spec.transform == "identity": + utility = raw + elif spec.transform == "affine": + lower = raw.new_tensor(spec.lower_anchor) + upper = raw.new_tensor(spec.upper_anchor) + if spec.goal == "maximize": + utility = (raw - lower) / (upper - lower) + else: + utility = (upper - raw) / (upper - lower) + if spec.clip: + utility = utility.clamp(0.0, 1.0) + elif spec.transform == "gaussian_target": + target = raw.new_tensor(spec.target) + sigma = raw.new_tensor(spec.sigma) + utility = torch.exp(-0.5 * ((raw - target) / sigma).square()) + else: + target = raw.new_tensor(spec.target) + scale = raw.new_tensor(spec.scale) + utility = -(raw - target).abs() / scale + outputs.append(utility) + transformed = torch.stack(outputs, dim=-1) + if transformed.shape != Y.shape: + raise RuntimeError("Internal objective transform shape error.") + return transformed + + __call__ = transform + + +class ConfiguredMCMultiOutputObjective(MCMultiOutputObjective): + """BoTorch MC objective backed by the exact same `ObjectiveTransform`.""" + + def __init__(self, objective_transform: ObjectiveTransform) -> None: + super().__init__() + if not isinstance(objective_transform, ObjectiveTransform): + raise TypeError("objective_transform must be an ObjectiveTransform.") + self.objective_transform = objective_transform + + def forward( + self, samples: torch.Tensor, X: torch.Tensor | None = None + ) -> torch.Tensor: + del X + return self.objective_transform.transform(samples) + + +def _validate_posterior_sample_bounds( + bounds: Sequence[UtilityBound], objective_transform: ObjectiveTransform +) -> tuple[UtilityBound, ...]: + if isinstance(bounds, (str, bytes)): + raise TypeError("bounds must be an ordered sequence of (lower, upper) pairs.") + try: + raw_bounds = tuple(bounds) + except TypeError as exc: + raise TypeError( + "bounds must be an ordered sequence of (lower, upper) pairs." + ) from exc + if len(raw_bounds) != objective_transform.objective_count: + raise ValueError( + "bounds must contain one (lower, upper) pair per objective; " + f"expected {objective_transform.objective_count}, got {len(raw_bounds)}." + ) + + validated: list[UtilityBound] = [] + bounded_count = 0 + for index, (raw_bound, spec) in enumerate( + zip(raw_bounds, objective_transform.specs) + ): + if isinstance(raw_bound, (str, bytes)): + raise TypeError(f"bounds[{index}] must be a (lower, upper) pair.") + try: + pair = tuple(raw_bound) + except TypeError as exc: + raise TypeError(f"bounds[{index}] must be a (lower, upper) pair.") from exc + if len(pair) != 2: + raise ValueError(f"bounds[{index}] must contain exactly two values.") + lower = _finite_optional( + pair[0], field="posterior_sample_lower_bound", name=spec.name + ) + upper = _finite_optional( + pair[1], field="posterior_sample_upper_bound", name=spec.name + ) + if lower is not None and upper is not None and lower > upper: + raise ValueError( + f"Objective {spec.name!r} posterior-sample lower bound must not " + "exceed its upper bound." + ) + if lower is not None or upper is not None: + bounded_count += 1 + if spec.transform != "identity" or spec.goal != "maximize": + raise ValueError( + "Posterior-sample bounds are supported only for explicit " + f"identity/maximize utilities; objective {spec.name!r} uses " + f"{spec.transform!r}/{spec.goal!r}." + ) + validated.append((lower, upper)) + if bounded_count == 0: + raise ValueError("At least one posterior-sample utility bound is required.") + return tuple(validated) + + +class BoundedPosteriorSampleTransform: + """Clamp declared identity utilities only after transforming MC samples. + + This acquisition-only wrapper never changes observed training targets or the + underlying versioned objective contract. It is intended for explicit + synthetic qLogNEHVI policy comparisons. + """ + + def __init__( + self, + objective_transform: ObjectiveTransform, + bounds: Sequence[UtilityBound], + ) -> None: + if not isinstance(objective_transform, ObjectiveTransform): + raise TypeError("objective_transform must be an ObjectiveTransform.") + self.objective_transform = objective_transform + self.bounds = _validate_posterior_sample_bounds(bounds, objective_transform) + self.version = f"{objective_transform.version}+posterior-sample-bounds-v1" + + def transform(self, samples: torch.Tensor) -> torch.Tensor: + utilities = self.objective_transform.transform(samples) + bounded_columns: list[torch.Tensor] = [] + for index, (lower, upper) in enumerate(self.bounds): + utility = utilities[..., index] + if lower is not None or upper is not None: + utility = torch.clamp(utility, min=lower, max=upper) + bounded_columns.append(utility) + bounded = torch.stack(bounded_columns, dim=-1) + if bounded.shape != utilities.shape: + raise RuntimeError("Internal posterior-sample bounds shape error.") + return bounded + + __call__ = transform + + +class BoundedMCMultiOutputObjective(MCMultiOutputObjective): + """BoTorch objective applying explicit bounds to posterior utility samples.""" + + def __init__( + self, + objective_transform: ObjectiveTransform, + bounds: Sequence[UtilityBound], + ) -> None: + super().__init__() + self.posterior_sample_transform = BoundedPosteriorSampleTransform( + objective_transform, bounds + ) + self.objective_transform = objective_transform + self.bounds = self.posterior_sample_transform.bounds + self.version = self.posterior_sample_transform.version + + def forward( + self, samples: torch.Tensor, X: torch.Tensor | None = None + ) -> torch.Tensor: + del X + return self.posterior_sample_transform.transform(samples) + + +__all__ = [ + "BoundedMCMultiOutputObjective", + "BoundedPosteriorSampleTransform", + "ConfiguredMCMultiOutputObjective", + "ObjectiveSpec", + "ObjectiveTransform", + "UtilityBound", +] diff --git a/src/mobo_kit/observation_influence.py b/src/mobo_kit/observation_influence.py new file mode 100644 index 0000000..b1418b3 --- /dev/null +++ b/src/mobo_kit/observation_influence.py @@ -0,0 +1,874 @@ +"""Pure all-observation influence metrics for Step 2C robustness studies. + +The module consumes proposals, common-pool acquisition scores, posterior reports, +Pareto membership, and hyperparameters that were computed elsewhere. It never +fits a model and never changes row roles or primary include-in-model policy. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +import json +from numbers import Real +from types import MappingProxyType +from typing import Any, Hashable, Mapping, Sequence + +import numpy as np +import pandas as pd +from scipy.stats import rankdata, spearmanr + +from .batch_comparison import ( + DEFAULT_REGIONAL_THRESHOLDS, + RegionalBatchComparison, + regional_match_batches, +) + + +INFLUENCE_COMPONENT_NAMES = ( + "batch_displacement", + "prediction_change", + "acquisition_change", + "pareto_change", + "hyperparameter_change", +) + + +def _sample_sort_key(value: Hashable) -> tuple[int, str, Any]: + """Order common scalar Sample IDs naturally and all other IDs stably.""" + if isinstance(value, (int, np.integer)) and not isinstance(value, (bool, np.bool_)): + return 0, "", int(value) + if isinstance(value, (float, np.floating)) and np.isfinite(value): + return 1, "", float(value) + if isinstance(value, str): + return 2, "", value + return 3, type(value).__name__, repr(value) + + +def _readonly_array( + value: Any, + *, + name: str, + dimensions: int, + finite: bool = True, +) -> np.ndarray: + result = np.asarray(value, dtype=float).copy() + if result.ndim != dimensions: + raise ValueError( + f"{name} must be {dimensions}-dimensional; got {result.shape}." + ) + if any(size == 0 for size in result.shape): + raise ValueError(f"{name} must not contain an empty dimension.") + if finite and not np.all(np.isfinite(result)): + raise ValueError(f"{name} must contain only finite values.") + result.setflags(write=False) + return result + + +def _sha256(value: str, *, name: str) -> str: + if not isinstance(value, str): + raise ValueError(f"{name} must be a 64-character SHA-256 hex string.") + normalized = value.strip().lower() + if len(normalized) != 64 or any( + character not in "0123456789abcdef" for character in normalized + ): + raise ValueError(f"{name} must be a 64-character SHA-256 hex string.") + return normalized + + +def _positive_scales(value: Sequence[float], *, count: int) -> np.ndarray: + scales = np.asarray(value, dtype=float).copy() + if scales.shape != (count,) or not np.all(np.isfinite(scales)): + raise ValueError(f"objective_scales must contain {count} finite values.") + if np.any(scales <= 0.0): + raise ValueError("objective_scales must be strictly positive.") + scales.setflags(write=False) + return scales + + +def _immutable_numeric_mapping( + value: Mapping[str, float], *, name: str +) -> Mapping[str, float]: + if not isinstance(value, Mapping) or not value: + raise ValueError(f"{name} must be a non-empty mapping.") + result: dict[str, float] = {} + for key, raw in value.items(): + if not isinstance(key, str) or not key.strip(): + raise ValueError(f"{name} keys must be non-empty strings.") + if isinstance(raw, (bool, np.bool_)) or not isinstance(raw, Real): + raise ValueError(f"{name} values must be real non-boolean numbers.") + number = float(raw) + if not np.isfinite(number) or number <= 0.0: + raise ValueError(f"{name} values must be finite and strictly positive.") + result[key.strip()] = number + if len(result) != len(value): + raise ValueError(f"{name} keys must be unique after trimming.") + return MappingProxyType(result) + + +@dataclass(frozen=True) +class InfluenceRunInput: + """Already-computed arrays for one full or leave-one-out proposal run.""" + + run_label: str + omitted_sample_id: Hashable | None + common_pool_sha256: str + selected_X_norm: np.ndarray + pool_acquisition_scores: np.ndarray + prediction_mean_at_full_candidates: np.ndarray + prediction_std_at_full_candidates: np.ndarray + pareto_sample_ids: tuple[Hashable, ...] + hyperparameters: Mapping[str, float] + prediction_uncertainty_kind: str = "latent" + + def __post_init__(self) -> None: + if not isinstance(self.run_label, str) or not self.run_label.strip(): + raise ValueError("run_label must be a non-empty string.") + uncertainty_kind = self.prediction_uncertainty_kind + if uncertainty_kind not in {"latent", "predictive"}: + raise ValueError( + "prediction_uncertainty_kind must be 'latent' or 'predictive'." + ) + selected = _readonly_array( + self.selected_X_norm, name="selected_X_norm", dimensions=2 + ) + if np.any(selected < 0.0) or np.any(selected > 1.0): + raise ValueError("selected_X_norm must lie within [0, 1].") + if np.unique(selected, axis=0).shape[0] != selected.shape[0]: + raise ValueError("selected_X_norm must contain unique candidate rows.") + scores = _readonly_array( + self.pool_acquisition_scores, + name="pool_acquisition_scores", + dimensions=1, + ) + means = _readonly_array( + self.prediction_mean_at_full_candidates, + name="prediction_mean_at_full_candidates", + dimensions=2, + ) + standard_deviations = _readonly_array( + self.prediction_std_at_full_candidates, + name="prediction_std_at_full_candidates", + dimensions=2, + ) + if means.shape != standard_deviations.shape: + raise ValueError( + "Prediction mean and standard deviation shapes must match." + ) + if means.shape[0] != selected.shape[0]: + raise ValueError( + "Prediction rows must align with the full-candidate batch size." + ) + if np.any(standard_deviations < 0.0): + raise ValueError("Prediction standard deviations cannot be negative.") + pareto_ids = tuple(self.pareto_sample_ids) + if len(set(pareto_ids)) != len(pareto_ids): + raise ValueError("pareto_sample_ids must be unique.") + object.__setattr__(self, "run_label", self.run_label.strip()) + object.__setattr__( + self, + "common_pool_sha256", + _sha256(self.common_pool_sha256, name="common_pool_sha256"), + ) + object.__setattr__(self, "selected_X_norm", selected) + object.__setattr__(self, "pool_acquisition_scores", scores) + object.__setattr__(self, "prediction_mean_at_full_candidates", means) + object.__setattr__( + self, "prediction_std_at_full_candidates", standard_deviations + ) + object.__setattr__(self, "pareto_sample_ids", pareto_ids) + object.__setattr__( + self, + "hyperparameters", + _immutable_numeric_mapping(self.hyperparameters, name="hyperparameters"), + ) + + +@dataclass(frozen=True) +class PredictionChangeRecord: + omitted_sample_id: Hashable + candidate_index: int + objective_index: int + objective_name: str + full_mean: float + omitted_mean: float + mean_delta: float + absolute_mean_delta: float + full_std: float + omitted_std: float + std_delta: float + absolute_std_delta: float + objective_scale: float + normalized_absolute_mean_delta: float + normalized_absolute_std_delta: float + + +@dataclass(frozen=True) +class PredictionChangeMetrics: + mean_absolute_mean_change: float + maximum_absolute_mean_change: float + mean_absolute_std_change: float + maximum_absolute_std_change: float + mean_normalized_absolute_mean_change: float + mean_normalized_absolute_std_change: float + component_value: float + + +@dataclass(frozen=True) +class AcquisitionRankMetrics: + pool_size: int + top_k: int + spearman_rank_correlation: float + top_k_overlap_count: int + top_k_jaccard: float + mean_absolute_rank_change: float + maximum_absolute_rank_change: float + normalized_mean_absolute_rank_change: float + mean_absolute_score_change: float + maximum_absolute_score_change: float + component_value: float + + +@dataclass(frozen=True) +class ParetoMembershipChange: + full_count: int + omitted_count: int + intersection_count: int + union_count: int + jaccard: float + added_sample_ids: tuple[Hashable, ...] + removed_sample_ids: tuple[Hashable, ...] + symmetric_difference_count: int + component_value: float + + +@dataclass(frozen=True) +class HyperparameterDisplacement: + parameter_count: int + mean_absolute_log_ratio: float + maximum_absolute_log_ratio: float + absolute_log_ratio_by_parameter: Mapping[str, float] + component_value: float + + +@dataclass(frozen=True) +class InfluenceRunMetrics: + omitted_sample_id: Hashable + omitted_row_role: str + omitted_primary_include_in_model: bool + batch: RegionalBatchComparison + prediction: PredictionChangeMetrics + acquisition: AcquisitionRankMetrics + pareto: ParetoMembershipChange + hyperparameter: HyperparameterDisplacement + raw_components: Mapping[str, float] + normalized_components: Mapping[str, float] + composite_score: float + influence_rank: int + influence_percentile: float + + +@dataclass(frozen=True) +class ObservationInfluenceStudyResult: + common_pool_sha256: str + objective_names: tuple[str, ...] + objective_scales: np.ndarray + regional_thresholds: tuple[float, ...] + top_k: int + component_weights: Mapping[str, float] + row_roles: Mapping[Hashable, str] + primary_include_policy: Mapping[Hashable, bool] + runs: tuple[InfluenceRunMetrics, ...] + prediction_changes: tuple[PredictionChangeRecord, ...] + + def rank_for_sample(self, sample_id: Hashable) -> InfluenceRunMetrics: + for run in self.runs: + if run.omitted_sample_id == sample_id: + return run + raise KeyError(f"No omission run exists for Sample ID {sample_id!r}.") + + def sample_1_rank(self) -> tuple[int, float]: + record = self.rank_for_sample(1) + return record.influence_rank, record.influence_percentile + + def summary_frame(self) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + for run in self.runs: + row: dict[str, Any] = { + "omitted_sample_id": run.omitted_sample_id, + "omitted_row_role": run.omitted_row_role, + "omitted_primary_include_in_model": ( + run.omitted_primary_include_in_model + ), + "common_pool_sha256": self.common_pool_sha256, + "exact_overlap_count": run.batch.exact_overlap_count, + "jaccard_overlap": run.batch.jaccard_overlap, + "matched_pair_distances": json.dumps( + run.batch.matched_pair_distances.tolist() + ), + "mean_matched_distance": run.batch.mean_matched_distance, + "maximum_matched_distance": run.batch.maximum_matched_distance, + "symmetric_chamfer_distance": (run.batch.symmetric_chamfer_distance), + "hausdorff_distance": run.batch.hausdorff_distance, + "mean_absolute_prediction_mean_change": ( + run.prediction.mean_absolute_mean_change + ), + "maximum_absolute_prediction_mean_change": ( + run.prediction.maximum_absolute_mean_change + ), + "mean_absolute_prediction_std_change": ( + run.prediction.mean_absolute_std_change + ), + "maximum_absolute_prediction_std_change": ( + run.prediction.maximum_absolute_std_change + ), + "acquisition_spearman_rank_correlation": ( + run.acquisition.spearman_rank_correlation + ), + "acquisition_top_k": run.acquisition.top_k, + "acquisition_top_k_overlap_count": ( + run.acquisition.top_k_overlap_count + ), + "acquisition_top_k_jaccard": run.acquisition.top_k_jaccard, + "acquisition_mean_absolute_rank_change": ( + run.acquisition.mean_absolute_rank_change + ), + "acquisition_normalized_mean_absolute_rank_change": ( + run.acquisition.normalized_mean_absolute_rank_change + ), + "full_pareto_count": run.pareto.full_count, + "omitted_pareto_count": run.pareto.omitted_count, + "pareto_jaccard": run.pareto.jaccard, + "pareto_added_sample_ids": json.dumps( + list(run.pareto.added_sample_ids) + ), + "pareto_removed_sample_ids": json.dumps( + list(run.pareto.removed_sample_ids) + ), + "hyperparameter_mean_absolute_log_ratio": ( + run.hyperparameter.mean_absolute_log_ratio + ), + "hyperparameter_maximum_absolute_log_ratio": ( + run.hyperparameter.maximum_absolute_log_ratio + ), + "hyperparameter_absolute_log_ratios": json.dumps( + dict(run.hyperparameter.absolute_log_ratio_by_parameter), + sort_keys=True, + ), + "composite_influence_score": run.composite_score, + "influence_rank": run.influence_rank, + "influence_percentile": run.influence_percentile, + } + for threshold, count in run.batch.regional_match_counts.items(): + suffix = f"{threshold:.2f}".replace(".", "_") + row[f"regional_matches_within_{suffix}"] = count + for component in INFLUENCE_COMPONENT_NAMES: + row[f"{component}_raw_component"] = run.raw_components[component] + row[f"{component}_normalized_component"] = run.normalized_components[ + component + ] + row[f"{component}_weight"] = self.component_weights[component] + rows.append(row) + return pd.DataFrame(rows) + + def prediction_changes_frame(self) -> pd.DataFrame: + return pd.DataFrame(asdict(row) for row in self.prediction_changes) + + +def prediction_change_metrics( + full_mean: np.ndarray, + full_std: np.ndarray, + omitted_mean: np.ndarray, + omitted_std: np.ndarray, + *, + omitted_sample_id: Hashable, + objective_names: Sequence[str], + objective_scales: Sequence[float], +) -> tuple[PredictionChangeMetrics, tuple[PredictionChangeRecord, ...]]: + """Compare posterior reports evaluated at the same full-model candidates.""" + baseline_mean = _readonly_array(full_mean, name="full_mean", dimensions=2) + baseline_std = _readonly_array(full_std, name="full_std", dimensions=2) + comparison_mean = _readonly_array(omitted_mean, name="omitted_mean", dimensions=2) + comparison_std = _readonly_array(omitted_std, name="omitted_std", dimensions=2) + if not ( + baseline_mean.shape + == baseline_std.shape + == comparison_mean.shape + == comparison_std.shape + ): + raise ValueError("All prediction matrices must have identical shapes.") + if np.any(baseline_std < 0.0) or np.any(comparison_std < 0.0): + raise ValueError("Prediction standard deviations cannot be negative.") + names = tuple( + name.strip() if isinstance(name, str) else name for name in objective_names + ) + if len(names) != baseline_mean.shape[1] or any( + not isinstance(name, str) or not name for name in names + ): + raise ValueError("objective_names must align with prediction columns.") + if len(set(names)) != len(names): + raise ValueError("objective_names must be unique.") + scales = _positive_scales(objective_scales, count=len(names)) + mean_delta = comparison_mean - baseline_mean + std_delta = comparison_std - baseline_std + normalized_mean = np.abs(mean_delta) / scales[None, :] + normalized_std = np.abs(std_delta) / scales[None, :] + records: list[PredictionChangeRecord] = [] + for candidate_index in range(baseline_mean.shape[0]): + for objective_index, objective_name in enumerate(names): + records.append( + PredictionChangeRecord( + omitted_sample_id=omitted_sample_id, + candidate_index=candidate_index, + objective_index=objective_index, + objective_name=objective_name, + full_mean=float(baseline_mean[candidate_index, objective_index]), + omitted_mean=float( + comparison_mean[candidate_index, objective_index] + ), + mean_delta=float(mean_delta[candidate_index, objective_index]), + absolute_mean_delta=float( + abs(mean_delta[candidate_index, objective_index]) + ), + full_std=float(baseline_std[candidate_index, objective_index]), + omitted_std=float(comparison_std[candidate_index, objective_index]), + std_delta=float(std_delta[candidate_index, objective_index]), + absolute_std_delta=float( + abs(std_delta[candidate_index, objective_index]) + ), + objective_scale=float(scales[objective_index]), + normalized_absolute_mean_delta=float( + normalized_mean[candidate_index, objective_index] + ), + normalized_absolute_std_delta=float( + normalized_std[candidate_index, objective_index] + ), + ) + ) + mean_normalized = float(normalized_mean.mean()) + std_normalized = float(normalized_std.mean()) + metrics = PredictionChangeMetrics( + mean_absolute_mean_change=float(np.abs(mean_delta).mean()), + maximum_absolute_mean_change=float(np.abs(mean_delta).max()), + mean_absolute_std_change=float(np.abs(std_delta).mean()), + maximum_absolute_std_change=float(np.abs(std_delta).max()), + mean_normalized_absolute_mean_change=mean_normalized, + mean_normalized_absolute_std_change=std_normalized, + component_value=0.5 * (mean_normalized + std_normalized), + ) + return metrics, tuple(records) + + +def acquisition_rank_metrics( + full_scores: np.ndarray, + omitted_scores: np.ndarray, + *, + top_k: int, +) -> AcquisitionRankMetrics: + """Compare acquisition ranks on one hash-verified ordered common pool.""" + baseline = _readonly_array(full_scores, name="full_scores", dimensions=1) + comparison = _readonly_array(omitted_scores, name="omitted_scores", dimensions=1) + if baseline.shape != comparison.shape: + raise ValueError("full_scores and omitted_scores must have identical shape.") + if ( + isinstance(top_k, (bool, np.bool_)) + or not isinstance(top_k, (int, np.integer)) + or int(top_k) <= 0 + ): + raise ValueError("top_k must be a positive integer.") + effective_top_k = min(int(top_k), baseline.size) + baseline_rank = rankdata(-baseline, method="average") + comparison_rank = rankdata(-comparison, method="average") + rank_delta = np.abs(comparison_rank - baseline_rank) + if np.array_equal(baseline, comparison): + correlation = 1.0 + elif np.unique(baseline).size < 2 or np.unique(comparison).size < 2: + correlation = np.nan + else: + correlation = float(spearmanr(baseline_rank, comparison_rank).statistic) + baseline_top = set(np.argsort(-baseline, kind="stable")[:effective_top_k].tolist()) + comparison_top = set( + np.argsort(-comparison, kind="stable")[:effective_top_k].tolist() + ) + overlap = len(baseline_top & comparison_top) + union = len(baseline_top | comparison_top) + top_k_jaccard = float(overlap / union) + denominator = max(1, baseline.size - 1) + normalized_rank_change = float(rank_delta.mean() / denominator) + component = 0.5 * (normalized_rank_change + (1.0 - top_k_jaccard)) + score_delta = np.abs(comparison - baseline) + return AcquisitionRankMetrics( + pool_size=int(baseline.size), + top_k=effective_top_k, + spearman_rank_correlation=correlation, + top_k_overlap_count=overlap, + top_k_jaccard=top_k_jaccard, + mean_absolute_rank_change=float(rank_delta.mean()), + maximum_absolute_rank_change=float(rank_delta.max()), + normalized_mean_absolute_rank_change=normalized_rank_change, + mean_absolute_score_change=float(score_delta.mean()), + maximum_absolute_score_change=float(score_delta.max()), + component_value=component, + ) + + +def pareto_membership_change( + full_sample_ids: Sequence[Hashable], + omitted_sample_ids: Sequence[Hashable], +) -> ParetoMembershipChange: + """Return transparent set changes for observed Pareto membership.""" + full_ids = tuple(full_sample_ids) + comparison_ids = tuple(omitted_sample_ids) + full = set(full_ids) + comparison = set(comparison_ids) + if len(full) != len(full_ids) or len(comparison) != len(comparison_ids): + raise ValueError("Pareto sample ID sequences must be unique.") + intersection = full & comparison + union = full | comparison + jaccard = 1.0 if not union else float(len(intersection) / len(union)) + added = tuple(sorted(comparison - full, key=_sample_sort_key)) + removed = tuple(sorted(full - comparison, key=_sample_sort_key)) + return ParetoMembershipChange( + full_count=len(full), + omitted_count=len(comparison), + intersection_count=len(intersection), + union_count=len(union), + jaccard=jaccard, + added_sample_ids=added, + removed_sample_ids=removed, + symmetric_difference_count=len(full ^ comparison), + component_value=1.0 - jaccard, + ) + + +def hyperparameter_displacement( + full_parameters: Mapping[str, float], + omitted_parameters: Mapping[str, float], +) -> HyperparameterDisplacement: + """Compare positive hyperparameters with scale-free absolute log ratios.""" + baseline = _immutable_numeric_mapping(full_parameters, name="full_parameters") + comparison = _immutable_numeric_mapping( + omitted_parameters, name="omitted_parameters" + ) + if set(baseline) != set(comparison): + missing = sorted(set(baseline) - set(comparison)) + extra = sorted(set(comparison) - set(baseline)) + raise ValueError( + "Hyperparameter keys must match exactly; " + f"missing={missing}, extra={extra}." + ) + changes = { + key: float(abs(np.log(comparison[key]) - np.log(baseline[key]))) + for key in sorted(baseline) + } + values = np.asarray(list(changes.values()), dtype=float) + mean_change = float(values.mean()) + return HyperparameterDisplacement( + parameter_count=len(changes), + mean_absolute_log_ratio=mean_change, + maximum_absolute_log_ratio=float(values.max()), + absolute_log_ratio_by_parameter=MappingProxyType(changes), + component_value=mean_change, + ) + + +def _component_weights( + value: Mapping[str, float] | None, +) -> Mapping[str, float]: + raw = ( + {component: 1.0 for component in INFLUENCE_COMPONENT_NAMES} + if value is None + else dict(value) + ) + if set(raw) != set(INFLUENCE_COMPONENT_NAMES): + raise ValueError( + "component_weights must contain exactly " + f"{list(INFLUENCE_COMPONENT_NAMES)}." + ) + parsed: dict[str, float] = {} + for component in INFLUENCE_COMPONENT_NAMES: + weight = raw[component] + if isinstance(weight, (bool, np.bool_)) or not isinstance(weight, Real): + raise ValueError("Influence component weights must be real numbers.") + number = float(weight) + if not np.isfinite(number) or number < 0.0: + raise ValueError( + "Influence component weights must be finite and non-negative." + ) + parsed[component] = number + total = sum(parsed.values()) + if total <= 0.0: + raise ValueError("At least one influence component weight must be positive.") + return MappingProxyType( + {component: parsed[component] / total for component in parsed} + ) + + +def _normalize_component_rows( + rows: Sequence[Mapping[str, float]], +) -> list[dict[str, float]]: + normalized = [dict() for _ in rows] + for component in INFLUENCE_COMPONENT_NAMES: + values = np.asarray([row[component] for row in rows], dtype=float) + if not np.all(np.isfinite(values)) or np.any(values < 0.0): + raise ValueError( + "Raw influence components must be finite and non-negative." + ) + lower = float(values.min()) + upper = float(values.max()) + if upper - lower <= 1.0e-15: + scaled = np.zeros(values.shape, dtype=float) + else: + scaled = (values - lower) / (upper - lower) + for index, value in enumerate(scaled): + normalized[index][component] = float(value) + return normalized + + +def _policy_mapping( + row_roles: Mapping[Hashable, str], + include_policy: Mapping[Hashable, bool], +) -> tuple[Mapping[Hashable, str], Mapping[Hashable, bool]]: + if not isinstance(row_roles, Mapping) or not row_roles: + raise ValueError("row_roles must be a non-empty mapping.") + if not isinstance(include_policy, Mapping) or set(include_policy) != set(row_roles): + raise ValueError("primary_include_policy keys must exactly match row_roles.") + roles: dict[Hashable, str] = {} + includes: dict[Hashable, bool] = {} + for sample_id, role in row_roles.items(): + if not isinstance(role, str) or not role.strip(): + raise ValueError("Every row role must be a non-empty string.") + include = include_policy[sample_id] + if not isinstance(include, (bool, np.bool_)): + raise ValueError("Every include-in-model policy value must be boolean.") + roles[sample_id] = role.strip() + includes[sample_id] = bool(include) + return MappingProxyType(roles), MappingProxyType(includes) + + +def run_observation_influence_study( + full_run: InfluenceRunInput, + omission_runs: Sequence[InfluenceRunInput], + *, + expected_common_pool_sha256: str, + objective_names: Sequence[str], + objective_scales: Sequence[float], + row_roles: Mapping[Hashable, str], + primary_include_policy: Mapping[Hashable, bool], + regional_thresholds: Sequence[float] = DEFAULT_REGIONAL_THRESHOLDS, + top_k: int = 100, + component_weights: Mapping[str, float] | None = None, + require_complete_coverage: bool = True, +) -> ObservationInfluenceStudyResult: + """Compare full and all omission runs without fitting or mutating policy. + + Each raw component is min-max normalized over the omission cohort, then the + normalized components are combined with the reported weights. Rank 1 is the + largest composite score; its percentile is 100 (ties share a rank). + """ + if ( + not isinstance(full_run, InfluenceRunInput) + or full_run.omitted_sample_id is not None + ): + raise ValueError( + "full_run must be an InfluenceRunInput with no omitted sample." + ) + runs = tuple(omission_runs) + if not runs or not all(isinstance(run, InfluenceRunInput) for run in runs): + raise ValueError("omission_runs must be a non-empty sequence of run inputs.") + omitted_ids = tuple(run.omitted_sample_id for run in runs) + if any(sample_id is None for sample_id in omitted_ids) or len( + set(omitted_ids) + ) != len(omitted_ids): + raise ValueError("Every omission run must identify one unique omitted sample.") + roles, include_policy = _policy_mapping(row_roles, primary_include_policy) + if not isinstance(require_complete_coverage, bool): + raise ValueError("require_complete_coverage must be a boolean.") + omitted_set = set(omitted_ids) + if require_complete_coverage and omitted_set != set(roles): + raise ValueError( + "Omission runs must cover every fixed row-role Sample ID exactly once." + ) + if not require_complete_coverage and not omitted_set <= set(roles): + raise ValueError("Omission runs contain a Sample ID absent from row_roles.") + expected_hash = _sha256( + expected_common_pool_sha256, name="expected_common_pool_sha256" + ) + if full_run.common_pool_sha256 != expected_hash or any( + run.common_pool_sha256 != expected_hash for run in runs + ): + raise ValueError("Every influence run must use the verified common pool hash.") + if any( + run.pool_acquisition_scores.shape != full_run.pool_acquisition_scores.shape + for run in runs + ): + raise ValueError("Common-pool acquisition score arrays must have equal length.") + if any(run.selected_X_norm.shape != full_run.selected_X_norm.shape for run in runs): + raise ValueError( + "Every influence proposal batch must have the full batch shape." + ) + if any( + run.prediction_mean_at_full_candidates.shape + != full_run.prediction_mean_at_full_candidates.shape + or run.prediction_std_at_full_candidates.shape + != full_run.prediction_std_at_full_candidates.shape + for run in runs + ): + raise ValueError( + "Every prediction report must use the full candidate locations." + ) + if any( + run.prediction_uncertainty_kind != full_run.prediction_uncertainty_kind + for run in runs + ): + raise ValueError("All runs must report the same uncertainty kind.") + names = tuple( + name.strip() if isinstance(name, str) else name for name in objective_names + ) + objective_count = full_run.prediction_mean_at_full_candidates.shape[1] + if len(names) != objective_count or any( + not isinstance(name, str) or not name for name in names + ): + raise ValueError("objective_names must align with prediction columns.") + if len(set(names)) != len(names): + raise ValueError("objective_names must be unique.") + scales = _positive_scales(objective_scales, count=objective_count) + weights = _component_weights(component_weights) + thresholds = regional_match_batches( + full_run.selected_X_norm, + full_run.selected_X_norm, + thresholds=regional_thresholds, + ).thresholds + known_sample_ids = set(roles) + if not set(full_run.pareto_sample_ids) <= known_sample_ids: + raise ValueError("Full-run Pareto membership contains an unknown Sample ID.") + + provisional: list[dict[str, Any]] = [] + all_prediction_rows: list[PredictionChangeRecord] = [] + for run in sorted(runs, key=lambda item: _sample_sort_key(item.omitted_sample_id)): + omitted_id = run.omitted_sample_id + if omitted_id in run.pareto_sample_ids: + raise ValueError( + f"Omission run {omitted_id!r} still lists its omitted row as Pareto." + ) + if not set(run.pareto_sample_ids) <= known_sample_ids - {omitted_id}: + raise ValueError( + "Omission Pareto membership contains an unknown Sample ID." + ) + batch = regional_match_batches( + full_run.selected_X_norm, + run.selected_X_norm, + thresholds=thresholds, + ) + prediction, prediction_rows = prediction_change_metrics( + full_run.prediction_mean_at_full_candidates, + full_run.prediction_std_at_full_candidates, + run.prediction_mean_at_full_candidates, + run.prediction_std_at_full_candidates, + omitted_sample_id=omitted_id, + objective_names=names, + objective_scales=scales, + ) + all_prediction_rows.extend(prediction_rows) + acquisition = acquisition_rank_metrics( + full_run.pool_acquisition_scores, + run.pool_acquisition_scores, + top_k=top_k, + ) + pareto = pareto_membership_change( + full_run.pareto_sample_ids, run.pareto_sample_ids + ) + hyperparameter = hyperparameter_displacement( + full_run.hyperparameters, run.hyperparameters + ) + raw_components = { + "batch_displacement": batch.mean_matched_distance + / np.sqrt(full_run.selected_X_norm.shape[1]), + "prediction_change": prediction.component_value, + "acquisition_change": acquisition.component_value, + "pareto_change": pareto.component_value, + "hyperparameter_change": hyperparameter.component_value, + } + provisional.append( + { + "omitted_sample_id": omitted_id, + "batch": batch, + "prediction": prediction, + "acquisition": acquisition, + "pareto": pareto, + "hyperparameter": hyperparameter, + "raw_components": raw_components, + } + ) + + normalized_rows = _normalize_component_rows( + [row["raw_components"] for row in provisional] + ) + composite_scores = [ + float( + sum( + normalized[component] * weights[component] + for component in INFLUENCE_COMPONENT_NAMES + ) + ) + for normalized in normalized_rows + ] + count = len(composite_scores) + completed: list[InfluenceRunMetrics] = [] + for index, row in enumerate(provisional): + score = composite_scores[index] + rank = 1 + sum(other > score + 1.0e-15 for other in composite_scores) + percentile = 100.0 if count == 1 else 100.0 * (count - rank) / (count - 1) + omitted_id = row["omitted_sample_id"] + completed.append( + InfluenceRunMetrics( + omitted_sample_id=omitted_id, + omitted_row_role=roles[omitted_id], + omitted_primary_include_in_model=include_policy[omitted_id], + batch=row["batch"], + prediction=row["prediction"], + acquisition=row["acquisition"], + pareto=row["pareto"], + hyperparameter=row["hyperparameter"], + raw_components=MappingProxyType(dict(row["raw_components"])), + normalized_components=MappingProxyType(normalized_rows[index]), + composite_score=score, + influence_rank=rank, + influence_percentile=percentile, + ) + ) + completed.sort( + key=lambda row: ( + -row.composite_score, + _sample_sort_key(row.omitted_sample_id), + ) + ) + return ObservationInfluenceStudyResult( + common_pool_sha256=expected_hash, + objective_names=names, + objective_scales=scales, + regional_thresholds=thresholds, + top_k=min(int(top_k), full_run.pool_acquisition_scores.size), + component_weights=weights, + row_roles=roles, + primary_include_policy=include_policy, + runs=tuple(completed), + prediction_changes=tuple(all_prediction_rows), + ) + + +__all__ = [ + "AcquisitionRankMetrics", + "HyperparameterDisplacement", + "INFLUENCE_COMPONENT_NAMES", + "InfluenceRunInput", + "InfluenceRunMetrics", + "ObservationInfluenceStudyResult", + "ParetoMembershipChange", + "PredictionChangeMetrics", + "PredictionChangeRecord", + "acquisition_rank_metrics", + "hyperparameter_displacement", + "pareto_membership_change", + "prediction_change_metrics", + "run_observation_influence_study", +] diff --git a/src/mobo_kit/production_gate.py b/src/mobo_kit/production_gate.py new file mode 100644 index 0000000..54f1bf2 --- /dev/null +++ b/src/mobo_kit/production_gate.py @@ -0,0 +1,528 @@ +"""Fail-closed approval validation for campaign-facing candidate generation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from hashlib import sha256 +import json +from numbers import Real +from typing import Any, Mapping, Sequence + +import numpy as np + + +_PLACEHOLDER_MARKERS = ("pending", "provisional", "tbd", "todo", "replace_me") +_ALLOWED_NULL_PATHS = {"local_penalization.dimension_weights"} + + +class ProductionApprovalError(ValueError): + """Raised with every discovered reason a campaign is not production-ready.""" + + def __init__(self, errors: Sequence[str]) -> None: + self.errors = tuple(errors) + super().__init__( + "Production candidate generation is blocked:\n- " + "\n- ".join(self.errors) + ) + + +class CampaignProposalDisabledError(RuntimeError): + """Raised when approved configuration reaches an unwired legacy proposal path.""" + + +@dataclass(frozen=True) +class ProductionApprovalReceipt: + """Validated approval provenance to record with a future campaign run.""" + + schema_version: str + campaign_id: str + workbook_profile: str + input_contract_version: str + objective_count: int + approved_by: str + approved_at: str + decision_record: str + resolved_config_sha256: str + + +def _value_at(config: Mapping[str, Any], path: str) -> Any: + value: Any = config + for part in path.split("."): + if not isinstance(value, Mapping) or part not in value: + return _MISSING + value = value[part] + return value + + +class _Missing: + pass + + +_MISSING = _Missing() + + +def _is_placeholder(value: Any) -> bool: + if value is None or value is _MISSING: + return True + if isinstance(value, str): + normalized = value.strip().casefold() + return not normalized or any( + marker in normalized for marker in _PLACEHOLDER_MARKERS + ) + return False + + +def _contains_unresolved(value: Any) -> bool: + if _is_placeholder(value): + return True + if isinstance(value, Mapping): + return not value or any(_contains_unresolved(item) for item in value.values()) + if isinstance(value, (list, tuple)): + return not value or any(_contains_unresolved(item) for item in value) + return False + + +def _find_unresolved( + value: Any, *, path: str = "", allow_empty_constraints: bool = True +) -> list[str]: + findings: list[str] = [] + if value is None: + if path not in _ALLOWED_NULL_PATHS: + findings.append(path or "") + return findings + if isinstance(value, str): + if _is_placeholder(value): + findings.append(path or "") + return findings + if isinstance(value, Mapping): + if not value: + findings.append(path or "") + return findings + for key, item in value.items(): + child = f"{path}.{key}" if path else str(key) + findings.extend( + _find_unresolved( + item, + path=child, + allow_empty_constraints=allow_empty_constraints, + ) + ) + return findings + if isinstance(value, (list, tuple)): + if not value: + if not (allow_empty_constraints and path == "constraints"): + findings.append(path or "") + return findings + for index, item in enumerate(value): + findings.extend( + _find_unresolved( + item, + path=f"{path}[{index}]", + allow_empty_constraints=allow_empty_constraints, + ) + ) + return findings + + +def _require_resolved(config: Mapping[str, Any], path: str, errors: list[str]) -> Any: + value = _value_at(config, path) + if value is _MISSING: + errors.append(f"Missing required field {path!r}.") + return None + if _contains_unresolved(value): + errors.append(f"Field {path!r} contains an unresolved placeholder.") + return None + return value + + +def _require_string( + config: Mapping[str, Any], path: str, errors: list[str] +) -> str | None: + value = _require_resolved(config, path, errors) + if value is None: + return None + if not isinstance(value, str): + errors.append(f"Field {path!r} must be a resolved string.") + return None + return value.strip() + + +def _finite_number( + value: Any, *, path: str, errors: list[str], positive: bool +) -> float | None: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + errors.append(f"Field {path!r} must be a real non-boolean number.") + return None + number = float(value) + if not np.isfinite(number) or (number <= 0 if positive else number < 0): + qualifier = "strictly positive" if positive else "non-negative" + errors.append(f"Field {path!r} must be finite and {qualifier}.") + return None + return number + + +def _finite_real(value: Any, *, path: str, errors: list[str]) -> float | None: + """Validate a finite real without imposing a sign restriction.""" + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + errors.append(f"Field {path!r} must be a finite real non-boolean number.") + return None + number = float(value) + if not np.isfinite(number): + errors.append(f"Field {path!r} must be a finite real number.") + return None + return number + + +def _positive_integer(value: Any, *, path: str, errors: list[str]) -> int | None: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, (int, np.integer)): + errors.append(f"Field {path!r} must be a positive integer.") + return None + number = int(value) + if number <= 0: + errors.append(f"Field {path!r} must be a positive integer.") + return None + return number + + +def _canonical_hash(config: Mapping[str, Any]) -> str: + try: + payload = json.dumps( + config, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + allow_nan=False, + ).encode("utf-8") + except (TypeError, ValueError) as exc: + raise ProductionApprovalError( + [f"Resolved configuration is not canonically JSON-serializable: {exc}."] + ) from exc + return sha256(payload).hexdigest() + + +def validate_production_config( + config: Mapping[str, Any], +) -> ProductionApprovalReceipt: + """Validate a fully resolved campaign contract or block candidate generation. + + This gate validates authorization and provenance, not scientific correctness. + Scientific approvers remain responsible for the supplied formulas, scales, + reference point, QC rules, and constraints. + """ + if not isinstance(config, Mapping): + raise ProductionApprovalError(["Configuration must be a mapping."]) + errors: list[str] = [] + unresolved_paths = _find_unresolved(config) + if unresolved_paths: + errors.append( + "Resolved configuration still contains null/blank/placeholder values " + f"at: {', '.join(unresolved_paths)}." + ) + schema_version = _require_string(config, "schema_version", errors) + campaign_id = _require_string(config, "campaign_id", errors) + approved = _value_at(config, "approved_for_production") + if approved is not True: + errors.append("'approved_for_production' must be explicitly true.") + approved_by = _require_string(config, "approval.approved_by", errors) + approved_at = _require_string(config, "approval.approved_at", errors) + decision_record = _require_string(config, "approval.decision_record", errors) + input_contract_version = _require_string(config, "input_contract_version", errors) + if approved_at is not None: + try: + timestamp = datetime.fromisoformat(approved_at) + if timestamp.tzinfo is None: + raise ValueError("timezone missing") + except ValueError: + errors.append( + "approval.approved_at must be an ISO-8601 timestamp with timezone." + ) + + if _value_at(config, "objective_mapping_status") != "approved": + errors.append("objective_mapping_status must be explicitly 'approved'.") + if _value_at(config, "constraints_status") != "approved": + errors.append("constraints_status must be explicitly 'approved'.") + + workbook_profile = _value_at(config, "workbook_profile") + if workbook_profile is _MISSING: + workbook_profile = _value_at(config, "workbook.profile") + if _contains_unresolved(workbook_profile): + errors.append("A resolved workbook_profile (or workbook.profile) is required.") + elif not isinstance(workbook_profile, str): + errors.append("workbook_profile must be a resolved string.") + elif workbook_profile != "d2d_summary_v2": + errors.append("D2D production requires workbook profile 'd2d_summary_v2'.") + + objectives = _value_at(config, "objectives") + if not isinstance(objectives, list) or len(objectives) != 3: + errors.append("'objectives' must be a list containing exactly three entries.") + objectives = [] + objective_names: set[str] = set() + objective_fields = ( + "name", + "model_source_column", + "utility_transform", + "transform_version", + "formula_version", + "direction", + "scaling", + ) + for index, objective in enumerate(objectives): + prefix = f"objectives[{index}]" + if not isinstance(objective, Mapping): + errors.append(f"{prefix} must be a mapping.") + continue + for field in objective_fields: + value = objective.get(field, _MISSING) + if _contains_unresolved(value): + errors.append(f"{prefix}.{field} must be present and resolved.") + for field in objective_fields[:-1]: + value = objective.get(field) + if value is not None and not isinstance(value, str): + errors.append(f"{prefix}.{field} must be a string.") + name = objective.get("name") + if isinstance(name, str) and name.strip(): + if name in objective_names: + errors.append(f"Objective name {name!r} is duplicated.") + objective_names.add(name) + if objective.get("direction") not in {"maximize", "minimize", "target"}: + errors.append(f"{prefix}.direction must be maximize, minimize, or target.") + scaling = objective.get("scaling") + if not isinstance(scaling, Mapping): + errors.append(f"{prefix}.scaling must be a resolved mapping.") + else: + mode = scaling.get("mode") + version = scaling.get("version") + if not isinstance(version, str) or _is_placeholder(version): + errors.append(f"{prefix}.scaling.version must be resolved.") + if mode == "already_normalized": + allowed_keys = {"mode", "version"} + unexpected = sorted( + (key for key in scaling if key not in allowed_keys), key=str + ) + if unexpected: + errors.append( + f"{prefix}.scaling already_normalized has unsupported " + f"field(s): {unexpected}." + ) + elif mode == "fixed_affine": + allowed_keys = { + "mode", + "version", + "lower_anchor", + "upper_anchor", + } + unexpected = sorted( + (key for key in scaling if key not in allowed_keys), key=str + ) + if unexpected: + errors.append( + f"{prefix}.scaling fixed_affine has unsupported field(s): " + f"{unexpected}." + ) + lower = scaling.get("lower_anchor") + upper = scaling.get("upper_anchor") + lower_value = _finite_real( + lower, + path=f"{prefix}.scaling.lower_anchor", + errors=errors, + ) + upper_value = _finite_real( + upper, + path=f"{prefix}.scaling.upper_anchor", + errors=errors, + ) + if ( + lower_value is not None + and upper_value is not None + and lower_value >= upper_value + ): + errors.append( + f"{prefix}.scaling requires lower_anchor < upper_anchor." + ) + else: + errors.append( + f"{prefix}.scaling.mode must be 'already_normalized' or " + "'fixed_affine'; observed/data-derived scaling is forbidden." + ) + reference = _value_at(config, "reference_point_utility") + if not isinstance(reference, list) or len(reference) != len(objectives): + errors.append( + "'reference_point_utility' must be a fixed list matching the objective count." + ) + elif any( + isinstance(value, (bool, np.bool_)) + or not isinstance(value, Real) + or not np.isfinite(float(value)) + for value in reference + ): + errors.append("'reference_point_utility' must contain only finite numbers.") + + for field in ( + "complete_case_rule", + "failed_measurement_rule", + "outlier_rule", + "control_rule", + "replicate_rule", + ): + _require_string(config, f"qc_policy.{field}", errors) + + constraints = _value_at(config, "constraints") + if not isinstance(constraints, list): + errors.append("'constraints' must be an explicit list; [] is acceptable.") + elif any( + not isinstance(item, Mapping) or _contains_unresolved(item) + for item in constraints + ): + errors.append("Every configured constraint must be a resolved mapping.") + + if _value_at(config, "r1.method") != "ucb_hvi": + errors.append("r1.method must be 'ucb_hvi'.") + r1_batch_size = _value_at(config, "r1.batch_size") + if ( + isinstance(r1_batch_size, (bool, np.bool_)) + or not isinstance(r1_batch_size, (int, np.integer)) + or int(r1_batch_size) != 5 + ): + errors.append("r1.batch_size must be explicitly set to 5.") + beta = _require_resolved(config, "r1.beta", errors) + if beta is not None: + _finite_number(beta, path="r1.beta", errors=errors, positive=False) + for field in ("posterior_samples", "candidate_pool_size"): + value = _require_resolved(config, f"r1.{field}", errors) + if value is not None: + _positive_integer(value, path=f"r1.{field}", errors=errors) + + if _value_at(config, "r2.method") != "qlognehvi": + errors.append("r2.method must be 'qlognehvi'.") + r2_batch_size = _value_at(config, "r2.batch_size") + if ( + isinstance(r2_batch_size, (bool, np.bool_)) + or not isinstance(r2_batch_size, (int, np.integer)) + or int(r2_batch_size) != 3 + ): + errors.append("r2.batch_size must be explicitly set to 3.") + if _value_at(config, "r2.sequential_pending") is not True: + errors.append("r2.sequential_pending must be explicitly true.") + for field in ("mc_samples", "candidate_pool_size"): + value = _require_resolved(config, f"r2.{field}", errors) + if value is not None: + _positive_integer(value, path=f"r2.{field}", errors=errors) + + metric = _require_resolved(config, "local_penalization.distance_metric", errors) + if metric is not None and metric not in { + "normalized_euclidean", + "weighted_normalized_euclidean", + }: + errors.append("local_penalization.distance_metric is unsupported.") + dimension_weights = _value_at(config, "local_penalization.dimension_weights") + if metric == "weighted_normalized_euclidean": + configured_inputs = config.get("inputs") + expected_dimension = ( + len(configured_inputs) if isinstance(configured_inputs, list) else 0 + ) + if expected_dimension <= 0: + errors.append( + "Weighted distance requires a non-empty inputs list to validate weights." + ) + if ( + not isinstance(dimension_weights, list) + or len(dimension_weights) != expected_dimension + or any( + isinstance(value, (bool, np.bool_)) + or not isinstance(value, Real) + or not np.isfinite(float(value)) + or float(value) <= 0 + for value in dimension_weights + ) + ): + errors.append( + "Weighted distance requires one finite positive dimension weight " + "per configured input." + ) + elif ( + metric == "normalized_euclidean" + and dimension_weights is not _MISSING + and dimension_weights is not None + ): + errors.append( + "Ordinary normalized_euclidean distance requires dimension_weights: null." + ) + for field, positive in ( + ("radius", True), + ("min_batch_distance", False), + ("min_observed_distance", False), + ): + value = _require_resolved(config, f"local_penalization.{field}", errors) + if value is not None: + _finite_number( + value, + path=f"local_penalization.{field}", + errors=errors, + positive=positive, + ) + if ( + _value_at(config, "local_penalization.allow_hard_distance_relaxation") + is not False + ): + errors.append( + "local_penalization.allow_hard_distance_relaxation must be false." + ) + + seed = _require_resolved(config, "reproducibility.seed", errors) + if seed is not None and ( + isinstance(seed, (bool, np.bool_)) + or not isinstance(seed, (int, np.integer)) + or int(seed) < 0 + ): + errors.append("reproducibility.seed must be a non-negative integer.") + for field in ( + "record_git_commit", + "record_environment_versions", + "record_resolved_config_hash", + ): + if _value_at(config, f"reproducibility.{field}") is not True: + errors.append(f"reproducibility.{field} must be explicitly true.") + + if errors: + raise ProductionApprovalError(errors) + return ProductionApprovalReceipt( + schema_version=str(schema_version), + campaign_id=str(campaign_id), + workbook_profile=str(workbook_profile), + input_contract_version=str(input_contract_version), + objective_count=len(objectives), + approved_by=str(approved_by), + approved_at=str(approved_at), + decision_record=str(decision_record), + resolved_config_sha256=_canonical_hash(config), + ) + + +assert_production_approved = validate_production_config + + +def block_legacy_campaign_proposal(config: Mapping[str, Any]) -> None: + """Validate approval, then block the legacy runner until the Step 2B adapter. + + Validation is intentionally performed first so an unresolved campaign + receives the complete approval errors. Even an approved configuration must + not enter the old raw-objective qNEHVI path, which does not implement the + Step 2A objective contract, discrete pool, or shared batch selector. + """ + validate_production_config(config) + raise CampaignProposalDisabledError( + "Campaign candidate proposal is disabled in Step 2A: the legacy qNEHVI " + "runner is not wired to the versioned objective contract, discrete " + "candidate pool, and shared local-penalized selector. Implement and " + "review the Step 2B campaign adapter before generating a real batch." + ) + + +__all__ = [ + "CampaignProposalDisabledError", + "ProductionApprovalError", + "ProductionApprovalReceipt", + "assert_production_approved", + "block_legacy_campaign_proposal", + "validate_production_config", +] diff --git a/src/mobo_kit/qlognehvi_batch.py b/src/mobo_kit/qlognehvi_batch.py new file mode 100644 index 0000000..e6ca39b --- /dev/null +++ b/src/mobo_kit/qlognehvi_batch.py @@ -0,0 +1,389 @@ +"""Discrete singleton-pool qLogNEHVI scoring and sequential batch proposals.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable, Sequence + +import numpy as np +import torch +from botorch.acquisition.multi_objective.logei import ( + qLogNoisyExpectedHypervolumeImprovement, +) +from botorch.sampling.normal import SobolQMCNormalSampler + + +@dataclass(frozen=True) +class QLogNEHVIPoolScoreResult: + """Singleton qLogNEHVI values and the pending context used to compute them.""" + + base_log_score: np.ndarray + evaluated_shape: tuple[int, int, int] + pending_count: int + mc_samples: int + seed: int + reference_point_utility: np.ndarray + objective_contract_version: str + method: str = "qlognehvi" + method_version: str = "step2a-v1" + + +@dataclass(frozen=True) +class QLogNEHVIBatchProposal: + """Sequentially selected qLogNEHVI batch and per-step score histories.""" + + selection: Any + score_history: tuple[QLogNEHVIPoolScoreResult, ...] + preexisting_pending_count: int + metadata: dict[str, Any] + + +def _positive_integer(value: int, *, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, (int, np.integer)): + raise ValueError(f"{name} must be a positive integer; got {value!r}.") + result = int(value) + if result <= 0: + raise ValueError(f"{name} must be a positive integer; got {value!r}.") + return result + + +def _input_matrix(value: torch.Tensor, *, name: str) -> torch.Tensor: + if not isinstance(value, torch.Tensor) or value.ndim != 2: + shape = getattr(value, "shape", None) + raise ValueError(f"{name} must be a tensor with shape (N, D); got {shape}.") + if not value.is_floating_point(): + raise TypeError(f"{name} must use a floating dtype.") + if not torch.isfinite(value).all(): + raise ValueError(f"{name} must contain only finite values.") + return value + + +def _reference( + value: np.ndarray | torch.Tensor | None, + *, + dtype: torch.dtype, + device: torch.device, +) -> torch.Tensor: + if value is None: + raise ValueError( + "reference_point_utility is required and is never derived from data." + ) + reference = torch.as_tensor(value, dtype=dtype, device=device) + if reference.ndim != 1 or reference.numel() == 0: + raise ValueError( + "reference_point_utility must have shape (M,) with at least one objective." + ) + if not torch.isfinite(reference).all(): + raise ValueError("reference_point_utility must contain only finite values.") + return reference + + +def _build_qlognehvi( + *, + model: Any, + train_X: torch.Tensor, + reference_point: torch.Tensor, + objective: Any, + mc_samples: int, + seed: int, + X_pending: torch.Tensor | None, + constraints: Sequence[Callable[[torch.Tensor], torch.Tensor]] | None, + eta: float | torch.Tensor, + prune_baseline: bool, +) -> qLogNoisyExpectedHypervolumeImprovement: + sampler = SobolQMCNormalSampler( + sample_shape=torch.Size([mc_samples]), seed=int(seed) + ) + return qLogNoisyExpectedHypervolumeImprovement( + model=model, + ref_point=reference_point, + X_baseline=train_X, + sampler=sampler, + objective=objective, + constraints=None if constraints is None else list(constraints), + eta=eta, + X_pending=X_pending, + prune_baseline=prune_baseline, + ) + + +def score_qlognehvi_singletons( + model: Any, + train_X_norm: torch.Tensor, + X_pool_norm: torch.Tensor, + objective: Any, + reference_point_utility: np.ndarray | torch.Tensor | None, + *, + mc_samples: int = 128, + seed: int = 0, + chunk_size: int = 512, + X_pending_norm: torch.Tensor | None = None, + constraints: Sequence[Callable[[torch.Tensor], torch.Tensor]] | None = None, + eta: float | torch.Tensor = 0.001, + prune_baseline: bool = False, +) -> QLogNEHVIPoolScoreResult: + """Evaluate qLogNEHVI on a normalized pool with explicit ``N x 1 x D`` shape.""" + from .objectives import ( + BoundedMCMultiOutputObjective, + ConfiguredMCMultiOutputObjective, + ) + + train_X = _input_matrix(train_X_norm, name="train_X_norm") + pool = _input_matrix(X_pool_norm, name="X_pool_norm") + if pool.shape[0] == 0: + raise ValueError("X_pool_norm must contain at least one candidate.") + if pool.shape[1] != train_X.shape[1]: + raise ValueError("train_X_norm and X_pool_norm dimensions must match.") + sample_count = _positive_integer(mc_samples, name="mc_samples") + chunk = _positive_integer(chunk_size, name="chunk_size") + if ( + isinstance(seed, (bool, np.bool_)) + or not isinstance(seed, (int, np.integer)) + or int(seed) < 0 + ): + raise ValueError("seed must be a non-negative integer.") + approved_objective_types = ( + ConfiguredMCMultiOutputObjective, + BoundedMCMultiOutputObjective, + ) + if not isinstance(objective, approved_objective_types): + raise TypeError( + "objective must be a configured or bounded configured multi-output " + "objective so raw outcomes cannot silently bypass the approved utility " + "transform." + ) + pending: torch.Tensor | None = None + if X_pending_norm is not None: + pending = _input_matrix(X_pending_norm, name="X_pending_norm") + if pending.shape[1] != train_X.shape[1]: + raise ValueError("X_pending_norm and train_X_norm dimensions must match.") + pending = pending.to(dtype=train_X.dtype, device=train_X.device) + reference = _reference( + reference_point_utility, dtype=train_X.dtype, device=train_X.device + ) + objective_count = objective.objective_transform.objective_count + if reference.numel() != objective_count: + raise ValueError( + "reference_point_utility dimension must match the configured objective " + f"count ({objective_count}); got {reference.numel()}." + ) + model_output_count = getattr(model, "num_outputs", None) + if model_output_count is not None and int(model_output_count) != objective_count: + raise ValueError( + f"Model has {int(model_output_count)} outputs but objective contract " + f"has {objective_count}." + ) + objective_version = getattr( + objective, "version", objective.objective_transform.version + ) + acquisition = _build_qlognehvi( + model=model, + train_X=train_X, + reference_point=reference, + objective=objective, + mc_samples=sample_count, + seed=int(seed), + X_pending=pending, + constraints=constraints, + eta=eta, + prune_baseline=bool(prune_baseline), + ) + + values: list[torch.Tensor] = [] + with torch.no_grad(): + for start in range(0, pool.shape[0], chunk): + singleton_batch = ( + pool[start : start + chunk] + .to(dtype=train_X.dtype, device=train_X.device) + .unsqueeze(-2) + ) + chunk_values = acquisition(singleton_batch) + if chunk_values.shape != (singleton_batch.shape[0],): + raise RuntimeError( + "qLogNEHVI singleton evaluation returned unexpected shape " + f"{tuple(chunk_values.shape)} for input " + f"{tuple(singleton_batch.shape)}." + ) + values.append(chunk_values.detach().cpu().double()) + scores = torch.cat(values).numpy() + if np.any(np.isnan(scores)) or np.any(np.isposinf(scores)): + raise RuntimeError("qLogNEHVI returned NaN or positive-infinite scores.") + return QLogNEHVIPoolScoreResult( + base_log_score=scores, + evaluated_shape=(int(pool.shape[0]), 1, int(pool.shape[1])), + pending_count=0 if pending is None else int(pending.shape[0]), + mc_samples=sample_count, + seed=int(seed), + reference_point_utility=reference.detach().cpu().double().numpy(), + objective_contract_version=objective_version, + ) + + +def _assert_no_reference_overlap( + pool_norm: np.ndarray, + reference_norm: np.ndarray | None, + *, + name: str, + atol: float = 1e-12, +) -> None: + if reference_norm is None: + return + reference = np.asarray(reference_norm, dtype=float) + if reference.ndim != 2 or reference.shape[1] != pool_norm.shape[1]: + raise ValueError( + f"{name} must have shape (N, {pool_norm.shape[1]}); got {reference.shape}." + ) + if not np.all(np.isfinite(reference)): + raise ValueError(f"{name} must contain only finite values.") + if reference.shape[0] == 0: + return + overlap = np.all( + np.isclose( + pool_norm[:, None, :], + reference[None, :, :], + rtol=0.0, + atol=atol, + ), + axis=-1, + ) + if np.any(overlap): + pool_indices = np.flatnonzero(np.any(overlap, axis=1)).tolist() + raise ValueError( + f"Candidate pool overlaps {name} at pool indices {pool_indices}; " + "resample with those recipes in the avoid set." + ) + + +def propose_qlognehvi_penalized_batch( + candidate_pool: Any, + model: Any, + train_X_norm: torch.Tensor, + objective: Any, + reference_point_utility: np.ndarray | torch.Tensor | None, + *, + q: int, + local_penalization_config: Any, + X_pending_norm: torch.Tensor | None = None, + mc_samples: int = 128, + seed: int = 0, + chunk_size: int = 512, + constraints: Sequence[Callable[[torch.Tensor], torch.Tensor]] | None = None, + eta: float | torch.Tensor = 0.001, + prune_baseline: bool = False, +) -> QLogNEHVIBatchProposal: + """Select a discrete batch, rebuilding qLogNEHVI pending state each step.""" + from .batch_selection import BaseScoreResult, select_local_penalized_batch + + train_X = _input_matrix(train_X_norm, name="train_X_norm") + pool_norm = np.asarray(candidate_pool.X_norm, dtype=float) + if pool_norm.ndim != 2 or pool_norm.shape[1] != train_X.shape[1]: + raise ValueError( + "candidate_pool.X_norm and train_X_norm must share input dimension." + ) + train_numpy = train_X.detach().cpu().double().numpy() + _assert_no_reference_overlap(pool_norm, train_numpy, name="observed train_X_norm") + + pending_tensor: torch.Tensor | None = None + pending_numpy: np.ndarray | None = None + if X_pending_norm is not None: + pending_tensor = _input_matrix(X_pending_norm, name="X_pending_norm").to( + dtype=train_X.dtype, device=train_X.device + ) + pending_numpy = pending_tensor.detach().cpu().double().numpy() + _assert_no_reference_overlap( + pool_norm, pending_numpy, name="pre-existing X_pending_norm" + ) + + histories: list[QLogNEHVIPoolScoreResult] = [] + + def score_remaining( + remaining_indices: np.ndarray, selected_indices: np.ndarray + ) -> Any: + selected_tensor = torch.as_tensor( + pool_norm[selected_indices], dtype=train_X.dtype, device=train_X.device + ) + if pending_tensor is None: + current_pending = selected_tensor if selected_indices.size else None + elif selected_indices.size: + current_pending = torch.cat([pending_tensor, selected_tensor], dim=0) + else: + current_pending = pending_tensor + result = score_qlognehvi_singletons( + model, + train_X, + torch.as_tensor( + pool_norm[remaining_indices], + dtype=train_X.dtype, + device=train_X.device, + ), + objective, + reference_point_utility, + mc_samples=mc_samples, + seed=seed, + chunk_size=chunk_size, + X_pending_norm=current_pending, + constraints=constraints, + eta=eta, + prune_baseline=prune_baseline, + ) + histories.append(result) + raw_base_score = np.exp(np.clip(result.base_log_score, -745.0, 709.0)) + raw_base_score[np.isneginf(result.base_log_score)] = 0.0 + return BaseScoreResult( + base_log_score=result.base_log_score, + base_score=raw_base_score, + diagnostics={ + "pending_count": result.pending_count, + "evaluated_shape": result.evaluated_shape, + "remaining_pool_indices": remaining_indices.copy(), + }, + ) + + observed_pending = ( + train_numpy + if pending_numpy is None + else np.vstack([train_numpy, pending_numpy]) + ) + selection = select_local_penalized_batch( + candidate_pool, + q, + score_remaining, + local_penalization_config, + observed_pending_norm=observed_pending, + ) + return QLogNEHVIBatchProposal( + selection=selection, + score_history=tuple(histories), + preexisting_pending_count=( + 0 if pending_tensor is None else int(pending_tensor.shape[0]) + ), + metadata={ + "method": "qlognehvi", + "method_version": "step2a-v1", + "objective_contract_version": getattr( + objective, "version", objective.objective_transform.version + ), + "reference_point_utility": np.asarray( + reference_point_utility, dtype=float + ).copy(), + "pool_seed": candidate_pool.seed, + "pool_size": candidate_pool.size, + "pool_draws": candidate_pool.draws, + "pool_rejected_duplicate": candidate_pool.rejected_duplicate, + "pool_rejected_avoid": candidate_pool.rejected_avoid, + "pool_rejected_constraint": candidate_pool.rejected_constraint, + "mc_seed": int(seed), + "mc_samples": int(mc_samples), + "preexisting_pending_count": ( + 0 if pending_tensor is None else int(pending_tensor.shape[0]) + ), + "local_penalization": { + "radius": local_penalization_config.radius, + "min_batch_distance": local_penalization_config.min_batch_distance, + "min_observed_distance": local_penalization_config.min_observed_distance, + "dimension_weights": local_penalization_config.dimension_weights, + "epsilon": local_penalization_config.epsilon, + }, + "selected_pool_indices": selection.selected_pool_indices.copy(), + }, + ) diff --git a/src/mobo_kit/robust_regions.py b/src/mobo_kit/robust_regions.py new file mode 100644 index 0000000..3601618 --- /dev/null +++ b/src/mobo_kit/robust_regions.py @@ -0,0 +1,752 @@ +"""Persistent candidate-region clustering and debug-only consensus gating.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Mapping, Sequence + +import numpy as np +import pandas as pd +from scipy.cluster.hierarchy import fcluster, linkage +from scipy.spatial.distance import pdist, squareform + + +DEBUG_WATERMARK = "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" + + +# The Step 2C primary baseline is the reference level for every one-factor study. +# Keeping that relationship explicit prevents the larger nested and penalty families +# from dominating persistence merely because they contain more runs. +_STUDY_FAMILY_ALIASES: dict[str, frozenset[str]] = { + "model": frozenset({"model", "model_variant"}), + "nested_pool": frozenset({"nested", "nested_pool", "pool"}), + "sobol_scramble": frozenset({"scramble", "sobol_scramble"}), + "bound_policy": frozenset({"bound", "bound_policy", "bounded_utility"}), + "beta": frozenset({"beta"}), + "local_penalty": frozenset({"local_penalty", "penalty"}), +} + + +@dataclass(frozen=True) +class RobustRegionResult: + regions: pd.DataFrame + membership: pd.DataFrame + threshold: float + region_count: int + + +@dataclass(frozen=True) +class ConsensusCriteriaResult: + passed: bool + checks: dict[str, bool] + observed: dict[str, float | int | bool] + reasons: tuple[str, ...] + + +def _candidate_frame(frame: pd.DataFrame) -> tuple[pd.DataFrame, list[str], list[str]]: + if not isinstance(frame, pd.DataFrame) or frame.empty: + raise ValueError("candidate_records must be a non-empty DataFrame.") + required = {"run_id", "run_family", "acquisition_score"} + missing = sorted(required - set(frame.columns)) + if missing: + raise ValueError(f"candidate_records is missing required columns: {missing}.") + norm_columns = sorted( + ( + column + for column in frame.columns + if column.startswith("norm_") and column[5:].isdigit() + ), + key=lambda value: int(value.split("_", 1)[1]), + ) + grid_columns = sorted( + ( + column + for column in frame.columns + if column.startswith("grid_") and column[5:].isdigit() + ), + key=lambda value: int(value.split("_", 1)[1]), + ) + if not norm_columns or len(norm_columns) != len(grid_columns): + raise ValueError( + "candidate_records must contain aligned norm_0..D and grid_0..D columns." + ) + result = frame.copy(deep=True).reset_index(drop=True) + result["run_id"] = result["run_id"].astype(str) + result["run_family"] = result["run_family"].astype(str) + if ( + result["run_id"].str.strip().eq("").any() + or result["run_family"].str.strip().eq("").any() + ): + raise ValueError("run_id and run_family values must be nonblank.") + norm = result.loc[:, norm_columns].to_numpy(dtype=float) + if not np.all(np.isfinite(norm)) or np.any(norm < 0) or np.any(norm > 1): + raise ValueError("Normalized candidate coordinates must be finite in [0, 1].") + grid = result.loc[:, grid_columns].to_numpy() + if not np.all(np.isfinite(grid)) or not np.all(grid == np.floor(grid)): + raise ValueError("Grid candidate coordinates must be finite integers.") + acquisition = pd.to_numeric(result["acquisition_score"], errors="coerce").to_numpy() + if not np.all(np.isfinite(acquisition)) or np.any(acquisition < 0): + raise ValueError("acquisition_score values must be finite and non-negative.") + result["acquisition_score"] = acquisition + base_hvi_column = "base_hvi" if "base_hvi" in result else "acquisition_score" + base_hvi = pd.to_numeric(result[base_hvi_column], errors="coerce").to_numpy() + if not np.all(np.isfinite(base_hvi)) or np.any(base_hvi < 0): + raise ValueError("base HVI values must be finite and non-negative.") + result["base_hvi"] = base_hvi + run_maximum = result.groupby("run_id", sort=False)["base_hvi"].transform("max") + result["run_maximum_base_hvi"] = run_maximum + result["base_hvi_normalized_within_run"] = np.divide( + base_hvi, + run_maximum.to_numpy(dtype=float), + out=np.zeros_like(base_hvi, dtype=float), + where=run_maximum.to_numpy(dtype=float) > 0, + ) + if "selection_order" in result: + selection_order = pd.to_numeric(result["selection_order"], errors="coerce") + if ( + selection_order.isna().any() + or (~np.isfinite(selection_order.to_numpy(dtype=float))).any() + or (selection_order <= 0).any() + ): + raise ValueError("selection_order values must be finite and positive.") + result["selection_order"] = selection_order + return result, norm_columns, grid_columns + + +def _canonical_order(frame: pd.DataFrame, grid_columns: Sequence[str]) -> np.ndarray: + tie_breakers = [ + column for column in ("selection_order", "candidate_id") if column in frame + ] + return frame.sort_values( + [*grid_columns, "run_family", "run_id", *tie_breakers], kind="stable" + ).index.to_numpy(dtype=int) + + +def _study_family_runs( + registry: Mapping[str, str], +) -> tuple[dict[str, set[str]], set[str]]: + normalized = { + str(run_id): str(family).strip().lower() for run_id, family in registry.items() + } + baseline_runs = { + run_id for run_id, family in normalized.items() if family == "baseline" + } + study_runs: dict[str, set[str]] = {} + for name, aliases in _STUDY_FAMILY_ALIASES.items(): + study_runs[name] = baseline_runs | { + run_id for run_id, family in normalized.items() if family in aliases + } + return study_runs, baseline_runs + + +def _coverage( + represented_runs: set[str], family_runs: set[str] +) -> tuple[int, int, float]: + total = len(family_runs) + represented = len(represented_runs & family_runs) + return represented, total, represented / total if total else np.nan + + +def _control_run_groups( + frame: pd.DataFrame, + *, + control_sample_id: int | None, +) -> tuple[set[str], set[str], bool]: + control_column = next( + ( + column + for column in ("control_included", "include_control") + if column in frame + ), + None, + ) + omitted_column = "omitted_sample_id" if "omitted_sample_id" in frame else None + if control_column is None and omitted_column is None: + return set(), set(), False + + if control_column is not None: + values = frame[control_column] + if not values.map(lambda value: isinstance(value, (bool, np.bool_))).all(): + raise ValueError(f"{control_column} values must be boolean.") + control_included = values.astype(bool) + control_omitted = ~control_included + elif omitted_column is not None: + if isinstance(control_sample_id, bool) or not isinstance( + control_sample_id, (int, np.integer) + ): + raise ValueError( + "control_sample_id is required when only omitted_sample_id is " + "available." + ) + omitted = pd.to_numeric(frame[omitted_column], errors="coerce") + control_omitted = omitted.eq(int(control_sample_id)) + control_included = ~control_omitted + else: # pragma: no cover - guarded by the early return above + raise RuntimeError("Control-run grouping reached an invalid state.") + + control_runs = set(frame.loc[control_included, "run_id"].astype(str)) + omitted_control_runs = set(frame.loc[control_omitted, "run_id"].astype(str)) + return ( + control_runs, + omitted_control_runs, + bool(control_runs and omitted_control_runs), + ) + + +def _finite_median(frame: pd.DataFrame, column: str) -> float: + if column not in frame: + return np.nan + values = pd.to_numeric(frame[column], errors="coerce").to_numpy(dtype=float) + values = values[np.isfinite(values)] + return float(np.median(values)) if values.size else np.nan + + +def _medoid_index(norm: np.ndarray, grid: np.ndarray) -> int: + if norm.shape[0] == 1: + return 0 + distances = squareform(pdist(norm, metric="euclidean")) + means = distances.mean(axis=1) + minimum = float(means.min()) + tied = np.flatnonzero(np.abs(means - minimum) <= 1.0e-15) + if tied.size == 1: + return int(tied[0]) + keys = tuple(grid[tied, column] for column in reversed(range(grid.shape[1]))) + return int(tied[np.lexsort(keys)[0]]) + + +def _diameter(norm: np.ndarray) -> float: + if norm.shape[0] < 2: + return 0.0 + return float(np.max(pdist(norm, metric="euclidean"))) + + +def cluster_candidate_regions( + candidate_records: pd.DataFrame, + *, + distance_threshold: float = 0.15, + core_run_registry: Mapping[str, str] | None = None, + control_sample_id: int | None = None, +) -> RobustRegionResult: + """Cluster candidate selections by deterministic complete linkage.""" + threshold = float(distance_threshold) + if not np.isfinite(threshold) or threshold <= 0: + raise ValueError("distance_threshold must be finite and positive.") + frame, norm_columns, grid_columns = _candidate_frame(candidate_records) + order = _canonical_order(frame, grid_columns) + sorted_frame = ( + frame.iloc[order] + .reset_index(drop=False) + .rename(columns={"index": "source_row"}) + ) + norm = sorted_frame.loc[:, norm_columns].to_numpy(dtype=float) + grid = sorted_frame.loc[:, grid_columns].to_numpy(dtype=np.int64) + if norm.shape[0] == 1: + raw_labels = np.ones(1, dtype=int) + else: + hierarchy = linkage( + norm, method="complete", metric="euclidean", optimal_ordering=True + ) + raw_labels = fcluster(hierarchy, t=threshold, criterion="distance") + + registry = ( + {str(run): str(family) for run, family in core_run_registry.items()} + if core_run_registry is not None + else dict( + sorted_frame.loc[:, ["run_id", "run_family"]] + .drop_duplicates() + .itertuples(index=False, name=None) + ) + ) + if not registry: + raise ValueError("core_run_registry must contain at least one run.") + for run_id, family in registry.items(): + observed = sorted_frame.loc[sorted_frame["run_id"] == run_id, "run_family"] + if not observed.empty and not observed.eq(family).all(): + raise ValueError(f"Run {run_id!r} has inconsistent family metadata.") + family_runs: dict[str, set[str]] = {} + for run_id, family in registry.items(): + family_runs.setdefault(family, set()).add(run_id) + study_family_runs, baseline_runs = _study_family_runs(registry) + ( + control_runs, + omitted_control_runs, + control_correspondence_available, + ) = _control_run_groups(sorted_frame, control_sample_id=control_sample_id) + + raw_region_rows: list[dict[str, object]] = [] + membership_parts: list[pd.DataFrame] = [] + for raw_label in sorted(set(int(value) for value in raw_labels)): + positions = np.flatnonzero(raw_labels == raw_label) + member_frame = sorted_frame.iloc[positions].copy() + member_norm = norm[positions] + member_grid = grid[positions] + medoid_local = _medoid_index(member_norm, member_grid) + medoid_row = member_frame.iloc[medoid_local] + medoid_norm = member_norm[medoid_local] + member_distances = np.linalg.norm(member_norm - medoid_norm, axis=1) + represented_runs = set(member_frame["run_id"].astype(str)) & set(registry) + coverage_by_family: dict[str, float] = {} + for family, runs in sorted(family_runs.items()): + covered = len(runs & represented_runs) + coverage_by_family[family] = covered / len(runs) + raw_family_persistence = float(np.mean(list(coverage_by_family.values()))) + study_family_coverage: dict[str, float] = {} + study_family_counts: dict[str, tuple[int, int]] = {} + for family in _STUDY_FAMILY_ALIASES: + covered, total, coverage = _coverage( + represented_runs, study_family_runs[family] + ) + study_family_counts[family] = (covered, total) + study_family_coverage[family] = coverage + available_study_coverage = [ + value for value in study_family_coverage.values() if np.isfinite(value) + ] + study_family_persistence = ( + float(np.mean(available_study_coverage)) + if available_study_coverage + else np.nan + ) + # A Step 2C registry has an explicit shared baseline. Equal weighting of + # its six study families is then the primary persistence measure. Generic + # callers without that convention retain the historical raw-family score. + persistence = ( + study_family_persistence if baseline_runs else raw_family_persistence + ) + represented_families = { + registry[run_id] for run_id in represented_runs if run_id in registry + } + represented_nonbaseline_families = { + registry[run_id] + for run_id in represented_runs + if run_id in registry and run_id not in baseline_runs + } + acquisition = member_frame["acquisition_score"].to_numpy(dtype=float) + normalized_hvi = member_frame["base_hvi_normalized_within_run"].to_numpy( + dtype=float + ) + hvi_quartiles = np.quantile(normalized_hvi, [0.25, 0.75]) + boundary_flags = np.isclose( + member_norm, 0.0, rtol=0.0, atol=1.0e-12 + ) | np.isclose(member_norm, 1.0, rtol=0.0, atol=1.0e-12) + boundary_frequency = boundary_flags.mean(axis=0) + control_covered, control_total, control_coverage = _coverage( + set(member_frame["run_id"].astype(str)), control_runs + ) + omit_covered, omit_total, omit_coverage = _coverage( + set(member_frame["run_id"].astype(str)), omitted_control_runs + ) + # The lesser cohort coverage is a conservative, symmetric regional + # correspondence measure: it is high only when both control policies + # repeatedly select candidates in this same complete-link region. + control_correspondence = ( + float(min(control_coverage, omit_coverage)) + if control_correspondence_available + else np.nan + ) + row: dict[str, object] = { + "raw_region_label": raw_label, + "member_count": int(len(positions)), + "distinct_run_count": int(member_frame["run_id"].nunique()), + "distinct_core_run_count": int(len(represented_runs)), + "distinct_family_count": int(len(represented_families)), + "distinct_nonbaseline_family_count": int( + len(represented_nonbaseline_families) + ), + "run_ids": "|".join(sorted(set(member_frame["run_id"].astype(str)))), + "run_families": "|".join( + sorted(set(member_frame["run_family"].astype(str))) + ), + "family_weighted_persistence": persistence, + "registry_family_weighted_persistence": raw_family_persistence, + "study_family_weighted_persistence": study_family_persistence, + "persistence_basis": ( + "six_equal_weight_step2c_study_families" + if baseline_runs + else "registry_run_families" + ), + "family_coverage": "|".join( + f"{family}:{coverage:.6f}" + for family, coverage in sorted(coverage_by_family.items()) + ), + "study_family_coverage": "|".join( + f"{family}:{coverage:.6f}" + for family, coverage in study_family_coverage.items() + if np.isfinite(coverage) + ), + "median_acquisition_score": float(np.median(acquisition)), + "maximum_acquisition_score": float(np.max(acquisition)), + "median_selection_order": _finite_median(member_frame, "selection_order"), + "base_hvi_normalization": "divide_by_run_maximum", + "median_run_normalized_base_hvi": float(np.median(normalized_hvi)), + "q1_run_normalized_base_hvi": float(hvi_quartiles[0]), + "q3_run_normalized_base_hvi": float(hvi_quartiles[1]), + "iqr_run_normalized_base_hvi": float(hvi_quartiles[1] - hvi_quartiles[0]), + "mean_distance_to_medoid": float(member_distances.mean()), + "maximum_distance_to_medoid": float(member_distances.max()), + "cluster_diameter": _diameter(member_norm), + "maximum_within_region_distance": _diameter(member_norm), + "medoid_source_row": int(medoid_row["source_row"]), + "all_grid_valid": ( + bool(member_frame.get("grid_valid", True).astype(bool).all()) + if "grid_valid" in member_frame + else True + ), + "all_hard_distance_valid": ( + bool(member_frame.get("hard_distance_valid", True).astype(bool).all()) + if "hard_distance_valid" in member_frame + else True + ), + "minimum_nearest_control_distance": ( + float(member_frame["nearest_control_distance"].min()) + if "nearest_control_distance" in member_frame + else np.nan + ), + "mean_boundary_coordinate_count": ( + float(member_frame["boundary_coordinate_count"].mean()) + if "boundary_coordinate_count" in member_frame + else np.nan + ), + "boundary_dimension_frequency": "|".join( + f"{column}:{frequency:.6f}" + for column, frequency in zip(norm_columns, boundary_frequency) + ), + "median_nearest_observed_distance": _finite_median( + member_frame, "nearest_observed_distance" + ), + "control_omission_correspondence_available": ( + control_correspondence_available + ), + "control_omission_correspondence_definition": ( + "minimum_of_control_included_and_control_omitted_run_coverage" + ), + "control_included_represented_run_count": control_covered, + "control_included_total_run_count": control_total, + "control_included_run_coverage": control_coverage, + "control_omitted_represented_run_count": omit_covered, + "control_omitted_total_run_count": omit_total, + "control_omitted_run_coverage": omit_coverage, + "control_included_vs_control_omitted_correspondence": ( + control_correspondence + ), + } + for family, (covered, total) in study_family_counts.items(): + row[f"{family}_represented_run_count"] = covered + row[f"{family}_total_run_count"] = total + row[f"{family}_coverage"] = study_family_coverage[family] + for short_name, family in { + "nested": "nested_pool", + "scramble": "sobol_scramble", + "bound": "bound_policy", + "penalty": "local_penalty", + }.items(): + row[f"{short_name}_coverage"] = study_family_coverage[family] + for dimension, frequency in enumerate(boundary_frequency): + row[f"boundary_dimension_{dimension}_frequency"] = float(frequency) + if "boundary_dimensions" in member_frame: + boundary_names = [ + token.strip() + for value in member_frame["boundary_dimensions"].fillna("").astype(str) + for token in value.split("|") + if token.strip() + ] + row["boundary_dimension_name_frequency"] = "|".join( + f"{name}:{boundary_names.count(name) / len(member_frame):.6f}" + for name in sorted(set(boundary_names)) + ) + for column in grid_columns: + row[f"medoid_{column}"] = int(medoid_row[column]) + for column in norm_columns: + row[f"medoid_{column}"] = float(medoid_row[column]) + for column in member_frame.columns: + if column.startswith("phys_"): + row[f"medoid_{column}"] = float(medoid_row[column]) + elif column.startswith("pred_mean_") or column.startswith("pred_std_"): + numeric = pd.to_numeric(member_frame[column], errors="coerce") + if numeric.notna().all(): + row[f"medoid_{column}"] = float(medoid_row[column]) + row[f"mean_{column}"] = float(numeric.mean()) + row[f"median_{column}"] = float(numeric.median()) + raw_region_rows.append(row) + member_frame["raw_region_label"] = raw_label + member_frame["distance_to_region_medoid"] = member_distances + membership_parts.append(member_frame) + + regions = pd.DataFrame(raw_region_rows) + medoid_grid_columns = [f"medoid_{column}" for column in grid_columns] + ranking = regions.sort_values( + [ + "family_weighted_persistence", + "distinct_family_count", + "distinct_core_run_count", + "median_run_normalized_base_hvi", + "median_acquisition_score", + "member_count", + *medoid_grid_columns, + ], + ascending=[ + False, + False, + False, + False, + False, + False, + *([True] * len(grid_columns)), + ], + kind="stable", + ).reset_index(drop=True) + # Canonical region IDs follow robustness rank, with medoid grid tuple as the + # deterministic final tie break. + ranking["region_id"] = [ + f"REGION-{index:03d}" for index in range(1, len(ranking) + 1) + ] + label_to_id = dict(zip(ranking["raw_region_label"], ranking["region_id"])) + membership = pd.concat(membership_parts, ignore_index=True) + membership["region_id"] = membership["raw_region_label"].map(label_to_id) + membership = membership.sort_values( + ["region_id", "run_family", "run_id", *grid_columns], kind="stable" + ).reset_index(drop=True) + ranking["debug_only"] = True + ranking["approved_for_experiment"] = False + ranking["approved_for_production"] = False + ranking["candidate_status"] = DEBUG_WATERMARK + membership["debug_only"] = True + membership["approved_for_experiment"] = False + membership["approved_for_production"] = False + membership["candidate_status"] = DEBUG_WATERMARK + return RobustRegionResult( + regions=ranking, + membership=membership, + threshold=threshold, + region_count=int(ranking.shape[0]), + ) + + +def select_robust_shortlist( + region_result: RobustRegionResult, + *, + minimum_count: int = 8, + maximum_count: int = 12, + minimum_normalized_distance: float = 0.15, +) -> pd.DataFrame: + """Choose diverse region medoids; return fewer only when geometry requires it.""" + if not isinstance(region_result, RobustRegionResult): + raise TypeError("region_result must be a RobustRegionResult.") + if not (1 <= int(minimum_count) <= int(maximum_count)): + raise ValueError("Require 1 <= minimum_count <= maximum_count.") + distance = float(minimum_normalized_distance) + if not np.isfinite(distance) or distance < 0: + raise ValueError("minimum_normalized_distance must be finite and non-negative.") + regions = region_result.regions.copy() + medoid_norm_columns = sorted( + (column for column in regions if column.startswith("medoid_norm_")), + key=lambda value: int(value.rsplit("_", 1)[1]), + ) + if not medoid_norm_columns: + raise ValueError("Region table has no medoid normalized coordinates.") + selected_rows: list[pd.Series] = [] + selected_norm: list[np.ndarray] = [] + for _, row in regions.iterrows(): + candidate = row.loc[medoid_norm_columns].to_numpy(dtype=float) + if selected_norm: + nearest = float( + np.linalg.norm( + np.asarray(selected_norm) - candidate[None, :], axis=1 + ).min() + ) + if nearest < distance: + continue + selected_rows.append(row) + selected_norm.append(candidate) + if len(selected_rows) == int(maximum_count): + break + if not selected_rows: + raise RuntimeError("No robust-region medoid passed the shortlist rules.") + shortlist = pd.DataFrame(selected_rows).reset_index(drop=True) + shortlist.insert( + 0, + "shortlist_id", + [f"R1-RS{index:02d}" for index in range(1, len(shortlist) + 1)], + ) + shortlist["shortlist_target_minimum_met"] = len(shortlist) >= int(minimum_count) + shortlist["shortlist_count"] = len(shortlist) + shortlist["minimum_normalized_distance_required"] = distance + shortlist["debug_only"] = True + shortlist["approved_for_experiment"] = False + shortlist["approved_for_production"] = False + shortlist["candidate_status"] = DEBUG_WATERMARK + return shortlist + + +def evaluate_consensus_criteria( + regions: pd.DataFrame, + consensus_candidates: pd.DataFrame, + *, + largest_two_regional_matches_within_0_15: int, + largest_two_mean_matched_distance: float, + nested_match_minimum: int = 4, + mean_distance_maximum: float = 0.10, + minimum_family_coverage: int = 3, + consensus_batch_size: int = 5, + required_minimum_distance: float = 0.15, + full_mode_eligible: bool = True, +) -> ConsensusCriteriaResult: + """Evaluate every gate on the exact medoids eligible for publication.""" + if not isinstance(regions, pd.DataFrame) or not isinstance( + consensus_candidates, pd.DataFrame + ): + raise TypeError("regions and consensus_candidates must be DataFrames.") + minimum_distance = float(required_minimum_distance) + if not np.isfinite(minimum_distance) or minimum_distance < 0.0: + raise ValueError("required_minimum_distance must be finite and non-negative.") + if not isinstance(full_mode_eligible, (bool, np.bool_)): + raise TypeError("full_mode_eligible must be a boolean.") + family_count_column = ( + "distinct_nonbaseline_family_count" + if "distinct_nonbaseline_family_count" in regions + else "distinct_family_count" + ) + robust_region_count = int( + ( + pd.to_numeric(regions.get(family_count_column), errors="coerce") + >= int(minimum_family_coverage) + ).sum() + ) + exact_count = int(consensus_candidates.shape[0]) == int(consensus_batch_size) + family_qualified = bool( + exact_count + and ( + pd.to_numeric( + consensus_candidates.get( + family_count_column, + pd.Series(np.nan, index=consensus_candidates.index), + ), + errors="coerce", + ) + >= int(minimum_family_coverage) + ).all() + ) + grid_valid = bool( + exact_count + and consensus_candidates.get("all_grid_valid", pd.Series([False])) + .astype(bool) + .all() + ) + norm_columns = sorted( + ( + column + for column in consensus_candidates + if column.startswith("medoid_norm_") + ), + key=lambda value: int(value.rsplit("_", 1)[1]), + ) + grid_columns = sorted( + ( + column + for column in consensus_candidates + if column.startswith("medoid_grid_") + ), + key=lambda value: int(value.rsplit("_", 1)[1]), + ) + coordinates = ( + consensus_candidates.loc[:, norm_columns].to_numpy(dtype=float) + if exact_count and norm_columns + else np.empty((0, 0), dtype=float) + ) + finite_bounded = bool( + exact_count + and coordinates.shape[1] > 0 + and np.all(np.isfinite(coordinates)) + and np.all(coordinates >= 0.0) + and np.all(coordinates <= 1.0) + ) + unique = bool( + exact_count + and len(grid_columns) == len(norm_columns) + and np.unique( + consensus_candidates.loc[:, grid_columns].to_numpy(dtype=np.int64), + axis=0, + ).shape[0] + == int(consensus_batch_size) + ) + if exact_count and coordinates.shape[0] > 1: + pairwise_minimum = float(np.min(pdist(coordinates, metric="euclidean"))) + else: + pairwise_minimum = 0.0 + hard_valid = bool( + exact_count + and pairwise_minimum + 1.0e-12 >= minimum_distance + and consensus_candidates.get("all_hard_distance_valid", pd.Series([False])) + .astype(bool) + .all() + ) + checks = { + "full_mode_eligible_for_consensus": bool(full_mode_eligible), + "largest_two_nested_regional_matches": int( + largest_two_regional_matches_within_0_15 + ) + >= int(nested_match_minimum), + "largest_two_nested_mean_matched_distance": float( + largest_two_mean_matched_distance + ) + <= float(mean_distance_maximum), + "five_regions_cover_three_core_families": robust_region_count + >= int(consensus_batch_size), + "exact_five_candidates": exact_count, + "chosen_regions_cover_three_core_families": family_qualified, + "finite_and_bounded": finite_bounded, + "unique_and_on_grid": unique and grid_valid, + "chosen_pairwise_hard_distance_valid": hard_valid, + "debug_only": bool( + exact_count + and consensus_candidates.get("debug_only", pd.Series([False])) + .astype(bool) + .all() + ), + "experimental_approval_false": bool( + exact_count + and ( + ~consensus_candidates.get( + "approved_for_experiment", pd.Series([True]) + ).astype(bool) + ).all() + ), + "production_approval_false": bool( + exact_count + and ( + ~consensus_candidates.get( + "approved_for_production", pd.Series([True]) + ).astype(bool) + ).all() + ), + } + reasons = tuple(name for name, passed in checks.items() if not passed) + observed: dict[str, float | int | bool] = { + "largest_two_regional_matches_within_0_15": int( + largest_two_regional_matches_within_0_15 + ), + "largest_two_mean_matched_distance": float(largest_two_mean_matched_distance), + "regions_with_minimum_family_coverage": robust_region_count, + "consensus_candidate_count": int(consensus_candidates.shape[0]), + "chosen_regions_family_qualified": family_qualified, + "chosen_candidates_finite_and_bounded": finite_bounded, + "chosen_candidates_unique_and_on_grid": unique and grid_valid, + "chosen_pairwise_minimum_distance": pairwise_minimum, + "required_pairwise_minimum_distance": minimum_distance, + "chosen_hard_distance_valid": hard_valid, + "full_mode_eligible_for_consensus": bool(full_mode_eligible), + } + return ConsensusCriteriaResult( + passed=all(checks.values()), + checks=checks, + observed=observed, + reasons=reasons, + ) + + +__all__ = [ + "ConsensusCriteriaResult", + "RobustRegionResult", + "cluster_candidate_regions", + "evaluate_consensus_criteria", + "select_robust_shortlist", +] diff --git a/src/mobo_kit/robustness_plots.py b/src/mobo_kit/robustness_plots.py new file mode 100644 index 0000000..8ba3394 --- /dev/null +++ b/src/mobo_kit/robustness_plots.py @@ -0,0 +1,1322 @@ +"""Headless, watermarked plotting helpers for Step 2C robustness diagnostics.""" + +from __future__ import annotations + +from pathlib import Path +import re +from typing import Any, Mapping, Sequence + +import matplotlib +import numpy as np +import pandas as pd +from scipy.spatial.distance import cdist +from sklearn.decomposition import PCA + +matplotlib.use("Agg") +from matplotlib import pyplot as plt # noqa: E402 +from scipy.optimize import linear_sum_assignment + + +DEBUG_WATERMARK = "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" + + +def _output_path(output_path: str | Path) -> Path: + path = Path(output_path) + if path.suffix.lower() != ".png": + raise ValueError("output_path must use a .png suffix.") + path.parent.mkdir(parents=True, exist_ok=True) + return path + + +def _save_debug_figure(fig: Any, output_path: str | Path) -> Path: + try: + path = _output_path(output_path) + fig.text( + 0.5, + 0.012, + DEBUG_WATERMARK, + ha="center", + va="bottom", + color="firebrick", + fontsize=9, + fontweight="bold", + bbox={ + "facecolor": "white", + "edgecolor": "firebrick", + "alpha": 0.92, + }, + ) + fig.tight_layout(rect=(0.0, 0.065, 1.0, 1.0)) + fig.savefig( + path, + dpi=160, + metadata={"Description": DEBUG_WATERMARK}, + ) + finally: + plt.close(fig) + return path + + +def _frame( + value: pd.DataFrame, + *, + name: str, + required_columns: Sequence[str], +) -> pd.DataFrame: + if not isinstance(value, pd.DataFrame): + raise TypeError(f"{name} must be a pandas DataFrame.") + if value.empty: + raise ValueError(f"{name} must not be empty.") + missing = [column for column in required_columns if column not in value.columns] + if missing: + raise ValueError(f"{name} is missing required columns: {missing}.") + return value.copy() + + +def _numeric(frame: pd.DataFrame, columns: Sequence[str], *, name: str) -> None: + for column in columns: + converted = pd.to_numeric(frame[column], errors="coerce") + values = converted.to_numpy(dtype=float) + if not np.all(np.isfinite(values)): + raise ValueError(f"{name} column {column!r} must be complete and finite.") + frame[column] = values + + +def _labels(frame: pd.DataFrame, column: str, *, name: str) -> None: + if frame[column].isna().any(): + raise ValueError(f"{name} column {column!r} must be complete.") + labels = frame[column].astype(str).str.strip() + if labels.eq("").any(): + raise ValueError(f"{name} column {column!r} must contain nonblank labels.") + frame[column] = labels + + +def _matrix(value: np.ndarray, *, name: str) -> np.ndarray: + matrix = np.asarray(value, dtype=float) + if matrix.ndim != 2 or matrix.shape[0] == 0 or matrix.shape[1] < 2: + raise ValueError(f"{name} must have non-empty shape (N, D) with D >= 2.") + if not np.all(np.isfinite(matrix)): + raise ValueError(f"{name} must contain only finite values.") + if np.any(matrix < 0.0) or np.any(matrix > 1.0): + raise ValueError(f"{name} must contain normalized coordinates in [0, 1].") + return matrix + + +def _normalized_columns( + frame: pd.DataFrame, + *, + prefix: str, + name: str, +) -> list[str]: + columns = sorted( + ( + column + for column in frame.columns + if column.startswith(prefix) and column[len(prefix) :].isdigit() + ), + key=lambda column: int(column[len(prefix) :]), + ) + expected = [f"{prefix}{index}" for index in range(len(columns))] + if len(columns) < 2 or columns != expected: + raise ValueError( + f"{name} must contain contiguous {prefix}0..D columns with D >= 1." + ) + return columns + + +def _natural_label_key(value: object) -> tuple[tuple[int, object], ...]: + parts = re.split(r"(\d+)", str(value)) + return tuple( + (0, int(part)) if part.isdigit() else (1, part.casefold()) + for part in parts + if part + ) + + +def _validate_aligned_labels( + labels: Sequence[object] | None, + *, + count: int, + name: str, + default_prefix: str, +) -> np.ndarray: + if labels is None: + return np.asarray( + [f"{default_prefix}{index + 1}" for index in range(count)], dtype=str + ) + values = np.asarray(tuple(labels), dtype=object) + if values.shape != (count,): + raise ValueError(f"{name} must contain one label per candidate row.") + if any(value is None or not str(value).strip() for value in values): + raise ValueError(f"{name} must contain complete nonblank labels.") + return np.asarray([str(value).strip() for value in values], dtype=str) + + +def _heatmap( + values: np.ndarray, + *, + row_labels: Sequence[str], + column_labels: Sequence[str], + title: str, + colorbar_label: str, + output_path: str | Path, + cmap: str = "Blues", +) -> Path: + matrix = np.asarray(values, dtype=float) + if ( + matrix.ndim != 2 + or matrix.shape != (len(row_labels), len(column_labels)) + or matrix.shape[0] == 0 + or matrix.shape[1] == 0 + ): + raise ValueError("Heatmap values must align with non-empty row/column labels.") + if not np.all(np.isfinite(matrix)): + raise ValueError("Heatmap values must be complete and finite.") + width = max(7.5, 0.55 * len(column_labels) + 3.0) + height = max(4.5, 0.38 * len(row_labels) + 2.2) + fig, axis = plt.subplots(figsize=(width, height)) + image = axis.imshow(matrix, aspect="auto", interpolation="nearest", cmap=cmap) + axis.set_xticks( + np.arange(len(column_labels)), column_labels, rotation=40, ha="right" + ) + axis.set_yticks(np.arange(len(row_labels)), row_labels) + axis.set(title=title) + colorbar = fig.colorbar(image, ax=axis, pad=0.02) + colorbar.set_label(colorbar_label) + if matrix.size <= 225: + threshold = float(matrix.min() + (matrix.max() - matrix.min()) / 2.0) + for row in range(matrix.shape[0]): + for column in range(matrix.shape[1]): + value = matrix[row, column] + axis.text( + column, + row, + f"{value:.2g}", + ha="center", + va="center", + fontsize=7, + color="white" if value > threshold else "black", + ) + return _save_debug_figure(fig, output_path) + + +def _safe_axis_range(*values: np.ndarray) -> tuple[float, float]: + combined = np.concatenate([np.asarray(value, dtype=float) for value in values]) + lower = float(combined.min()) + upper = float(combined.max()) + if lower == upper: + padding = max(1.0, abs(lower) * 0.05) + else: + padding = (upper - lower) * 0.05 + return lower - padding, upper + padding + + +def plot_loocv_diagnostics( + predictions: pd.DataFrame, + output_path: str | Path, + *, + objective_column: str = "objective", + observed_column: str = "observed", + predicted_column: str = "predicted_mean", + std_column: str = "predicted_std", +) -> Path: + """Plot LOOCV parity, standardized residuals, and interval coverage.""" + + frame = _frame( + predictions, + name="predictions", + required_columns=( + objective_column, + observed_column, + predicted_column, + std_column, + ), + ) + _labels(frame, objective_column, name="predictions") + _numeric( + frame, + (observed_column, predicted_column, std_column), + name="predictions", + ) + if (frame[std_column] <= 0.0).any(): + raise ValueError("predicted standard deviations must be strictly positive.") + frame["_standardized_residual"] = ( + frame[observed_column] - frame[predicted_column] + ) / frame[std_column] + frame = frame.sort_values( + [objective_column, observed_column, predicted_column], kind="mergesort" + ).reset_index(drop=True) + objectives = sorted(frame[objective_column].unique()) + + fig, axes = plt.subplots(1, 3, figsize=(15, 4.5)) + parity, residual, coverage = axes + for objective in objectives: + group = frame[frame[objective_column] == objective] + parity.scatter( + group[observed_column], + group[predicted_column], + label=objective, + alpha=0.8, + ) + limits = _safe_axis_range( + frame[observed_column].to_numpy(), frame[predicted_column].to_numpy() + ) + parity.plot(limits, limits, linestyle="--", color="black", linewidth=1) + parity.set( + xlim=limits, + ylim=limits, + xlabel="Observed objective", + ylabel="LOOCV predicted mean", + title="LOOCV observed versus predicted", + ) + parity.legend(fontsize=8) + + positions = np.arange(1, len(frame) + 1) + residual.scatter(positions, frame["_standardized_residual"], s=28) + residual.axhline(0.0, color="black", linewidth=1) + residual.axhline(1.96, color="grey", linestyle="--", linewidth=1) + residual.axhline(-1.96, color="grey", linestyle="--", linewidth=1) + residual.set( + xlabel="Held-out prediction (stable order)", + ylabel="Standardized residual", + title="LOOCV standardized residuals", + ) + + x_positions = np.arange(len(objectives), dtype=float) + coverage_68 = [] + coverage_95 = [] + for objective in objectives: + absolute = frame.loc[ + frame[objective_column] == objective, "_standardized_residual" + ].abs() + coverage_68.append(float((absolute <= 1.0).mean())) + coverage_95.append(float((absolute <= 1.96).mean())) + width = 0.36 + coverage.bar(x_positions - width / 2, coverage_68, width, label="68% interval") + coverage.bar(x_positions + width / 2, coverage_95, width, label="95% interval") + coverage.axhline(0.68, color="C0", linestyle=":", linewidth=1) + coverage.axhline(0.95, color="C1", linestyle=":", linewidth=1) + coverage.set_xticks(x_positions, objectives, rotation=30, ha="right") + coverage.set( + ylim=(0.0, 1.05), + ylabel="Empirical coverage", + title="LOOCV predictive interval coverage", + ) + coverage.legend(fontsize=8) + return _save_debug_figure(fig, output_path) + + +def plot_nested_search_convergence( + convergence: pd.DataFrame, + output_path: str | Path, + *, + pool_size_column: str = "pool_size", + mean_distance_column: str = "mean_matched_distance", + max_distance_column: str = "maximum_matched_distance", + regional_columns: Mapping[float, str] | None = None, +) -> Path: + """Plot nested-pool regional matches and optimal matched distances.""" + + region_columns = ( + { + 0.10: "regional_matches_0_10", + 0.15: "regional_matches_0_15", + 0.20: "regional_matches_0_20", + } + if regional_columns is None + else {float(key): value for key, value in regional_columns.items()} + ) + if not region_columns or any( + not np.isfinite(threshold) or threshold < 0.0 for threshold in region_columns + ): + raise ValueError("regional_columns must map finite non-negative thresholds.") + required = ( + pool_size_column, + mean_distance_column, + max_distance_column, + *region_columns.values(), + ) + frame = _frame(convergence, name="convergence", required_columns=required) + _numeric(frame, required, name="convergence") + if (frame[pool_size_column] <= 0.0).any(): + raise ValueError("pool sizes must be strictly positive.") + if frame[pool_size_column].duplicated().any(): + raise ValueError("pool sizes must be unique.") + if (frame[[mean_distance_column, max_distance_column]] < 0.0).any().any(): + raise ValueError("matched distances must be non-negative.") + if (frame[list(region_columns.values())] < 0.0).any().any(): + raise ValueError("regional match counts must be non-negative.") + frame = frame.sort_values(pool_size_column, kind="mergesort") + + fig, axes = plt.subplots(1, 2, figsize=(11, 4.5)) + for threshold, column in sorted(region_columns.items()): + axes[0].plot( + frame[pool_size_column], + frame[column], + marker="o", + label=f"within {threshold:.2f}", + ) + axes[0].set( + xlabel="Accepted unique Sobol pool size", + ylabel="Regionally matched candidates", + title="Nested-search regional convergence", + ) + axes[0].legend(fontsize=8) + axes[0].ticklabel_format(axis="x", style="plain") + + axes[1].plot( + frame[pool_size_column], + frame[mean_distance_column], + marker="o", + label="Mean matched distance", + ) + axes[1].plot( + frame[pool_size_column], + frame[max_distance_column], + marker="s", + label="Maximum matched distance", + ) + axes[1].set( + xlabel="Accepted unique Sobol pool size", + ylabel="Normalized Euclidean distance", + title="Nested-search matched distances", + ) + axes[1].legend(fontsize=8) + axes[1].ticklabel_format(axis="x", style="plain") + return _save_debug_figure(fig, output_path) + + +def plot_bounded_utility_comparison( + comparison: pd.DataFrame, + output_path: str | Path, + *, + policy_column: str = "policy", + raw_value_column: str = "raw_value", + bounded_value_column: str = "bounded_value", +) -> Path: + """Compare raw and bounded utility coordinates without exposing recipes.""" + + frame = _frame( + comparison, + name="comparison", + required_columns=(policy_column, raw_value_column, bounded_value_column), + ) + _labels(frame, policy_column, name="comparison") + _numeric(frame, (raw_value_column, bounded_value_column), name="comparison") + summary = ( + frame.groupby(policy_column, sort=True)[ + [raw_value_column, bounded_value_column] + ] + .mean() + .sort_index() + ) + delta = summary[bounded_value_column] - summary[raw_value_column] + positions = np.arange(len(summary), dtype=float) + width = 0.36 + + fig, axes = plt.subplots(1, 2, figsize=(10.5, 4.5)) + axes[0].bar( + positions - width / 2, + summary[raw_value_column], + width, + label="Raw utility", + ) + axes[0].bar( + positions + width / 2, + summary[bounded_value_column], + width, + label="Bounded utility", + ) + axes[0].set_xticks(positions, summary.index, rotation=30, ha="right") + axes[0].set( + ylabel="Mean utility coordinate", + title="Bounded-utility policy comparison", + ) + axes[0].legend(fontsize=8) + axes[1].bar(positions, delta, color="C2") + axes[1].axhline(0.0, color="black", linewidth=1) + axes[1].set_xticks(positions, summary.index, rotation=30, ha="right") + axes[1].set( + ylabel="Mean bounded minus raw utility", + title="Utility-bound effect", + ) + return _save_debug_figure(fig, output_path) + + +def plot_local_penalty_tradeoff( + tradeoff: pd.DataFrame, + output_path: str | Path, + *, + variant_column: str = "variant", + acquisition_column: str = "sum_base_hvi", + diversity_column: str = "minimum_within_batch_distance", +) -> Path: + """Plot acquisition quality against within-batch diversity by policy.""" + + frame = _frame( + tradeoff, + name="tradeoff", + required_columns=(variant_column, acquisition_column, diversity_column), + ) + _labels(frame, variant_column, name="tradeoff") + _numeric(frame, (acquisition_column, diversity_column), name="tradeoff") + if (frame[diversity_column] < 0.0).any(): + raise ValueError("diversity distances must be non-negative.") + summary = ( + frame.groupby(variant_column, sort=True)[[acquisition_column, diversity_column]] + .mean() + .sort_index() + ) + fig, axis = plt.subplots(figsize=(7, 5)) + axis.scatter( + summary[diversity_column], summary[acquisition_column], s=70, color="C3" + ) + for label, row in summary.iterrows(): + axis.annotate( + label, + (row[diversity_column], row[acquisition_column]), + xytext=(5, 4), + textcoords="offset points", + fontsize=8, + ) + axis.set( + xlabel="Minimum normalized within-batch distance", + ylabel="Mean summed base HVI", + title="Local-penalty acquisition versus diversity", + ) + return _save_debug_figure(fig, output_path) + + +def plot_observation_influence_ranking( + influence: pd.DataFrame, + output_path: str | Path, + *, + sample_column: str = "sample_id", + score_column: str = "influence_score", +) -> Path: + """Plot a deterministic all-observation influence ranking.""" + + frame = _frame( + influence, + name="influence", + required_columns=(sample_column, score_column), + ) + _labels(frame, sample_column, name="influence") + _numeric(frame, (score_column,), name="influence") + if frame[sample_column].duplicated().any(): + raise ValueError("sample identifiers must be unique.") + frame = frame.sort_values( + [score_column, sample_column], ascending=[True, True], kind="mergesort" + ) + fig, axis = plt.subplots(figsize=(7.5, max(4.5, 0.3 * len(frame)))) + axis.barh(frame[sample_column], frame[score_column], color="C4") + axis.set( + xlabel="Composite influence score", + ylabel="Omitted sample", + title="Observation-influence ranking", + ) + return _save_debug_figure(fig, output_path) + + +def plot_boundary_enrichment( + enrichment: pd.DataFrame, + output_path: str | Path, + *, + input_column: str = "input_name", + pool_lower_column: str = "pool_lower_boundary_frequency", + top_lower_column: str = "top_lower_boundary_frequency", + selected_lower_column: str = "selected_lower_boundary_frequency", + pool_upper_column: str = "pool_upper_boundary_frequency", + top_upper_column: str = "top_upper_boundary_frequency", + selected_upper_column: str = "selected_upper_boundary_frequency", +) -> Path: + """Compare lower/upper boundary seeking in pool, high-HVI, and selections.""" + + side_columns = { + "Configured minimum": ( + pool_lower_column, + top_lower_column, + selected_lower_column, + ), + "Configured maximum": ( + pool_upper_column, + top_upper_column, + selected_upper_column, + ), + } + required = ( + input_column, + *(column for columns in side_columns.values() for column in columns), + ) + frame = _frame(enrichment, name="enrichment", required_columns=required) + _labels(frame, input_column, name="enrichment") + _numeric(frame, required[1:], name="enrichment") + if frame[input_column].duplicated().any(): + raise ValueError("input names must be unique.") + frequencies = frame.loc[:, list(required[1:])] + if ((frequencies < 0.0) | (frequencies > 1.0)).any().any(): + raise ValueError("boundary frequencies must remain in [0, 1].") + frame = frame.sort_values(input_column, kind="mergesort") + positions = np.arange(len(frame), dtype=float) + width = 0.27 + fig, axes = plt.subplots( + 2, + 1, + figsize=(max(8, 0.7 * len(frame)), 8.0), + sharex=True, + constrained_layout=True, + ) + for axis, (side_label, columns) in zip(axes, side_columns.items()): + pool_column, top_column, selected_column = columns + axis.bar(positions - width, frame[pool_column], width, label="Full pool") + axis.bar(positions, frame[top_column], width, label="Top acquisition") + axis.bar(positions + width, frame[selected_column], width, label="Selected") + axis.set( + ylim=(0.0, 1.05), + ylabel="Boundary frequency", + title=side_label, + ) + axis.legend(fontsize=8) + axes[-1].set_xticks(positions, frame[input_column], rotation=35, ha="right") + fig.suptitle("Lower versus upper boundary enrichment by input dimension") + return _save_debug_figure(fig, output_path) + + +def _canonical_region_order(points: np.ndarray, labels: np.ndarray) -> np.ndarray: + label_strings = labels.astype(str) + keys: list[np.ndarray] = [ + points[:, column] for column in reversed(range(points.shape[1])) + ] + keys.append(label_strings) + return np.lexsort(tuple(keys)) + + +def _region_medoids( + points: np.ndarray, + labels: np.ndarray, +) -> tuple[list[str], np.ndarray]: + region_names = sorted(set(labels.astype(str))) + medoids: list[np.ndarray] = [] + for region in region_names: + members = points[labels.astype(str) == region] + distances = cdist(members, members, metric="euclidean") + totals = distances.sum(axis=1) + minimum = totals.min() + tied = members[np.isclose(totals, minimum, rtol=0.0, atol=1e-12)] + keys = tuple(tied[:, column] for column in reversed(range(tied.shape[1]))) + medoids.append(tied[np.lexsort(keys)[0]]) + return region_names, np.asarray(medoids, dtype=float) + + +def plot_robust_region_overview( + region_points_norm: np.ndarray, + region_labels: Sequence[object], + output_path: str | Path, + *, + persistence: Sequence[float] | None = None, + input_names: Sequence[str] | None = None, +) -> Path: + """Plot robust-region members in PCA space and medoids in normalized space.""" + + points = _matrix(region_points_norm, name="region_points_norm") + if points.shape[0] < 2: + raise ValueError("region_points_norm must contain at least two rows for PCA.") + labels = np.asarray(tuple(region_labels), dtype=object) + if labels.shape != (points.shape[0],): + raise ValueError("region_labels must contain one label per normalized row.") + if any(value is None or not str(value).strip() for value in labels): + raise ValueError("region_labels must contain complete nonblank labels.") + labels = np.asarray([str(value).strip() for value in labels], dtype=str) + if persistence is None: + counts = { + label: int(np.count_nonzero(labels == label)) for label in set(labels) + } + persistence_values = np.asarray( + [counts[label] for label in labels], dtype=float + ) + else: + persistence_values = np.asarray(tuple(persistence), dtype=float) + if persistence_values.shape != (points.shape[0],): + raise ValueError("persistence must contain one value per normalized row.") + if not np.all(np.isfinite(persistence_values)) or np.any( + persistence_values <= 0.0 + ): + raise ValueError("persistence values must be finite and strictly positive.") + if input_names is None: + names = [f"X{index + 1}" for index in range(points.shape[1])] + else: + names = [str(value).strip() for value in input_names] + if len(names) != points.shape[1] or any(not value for value in names): + raise ValueError( + "input_names must contain one nonblank name per input dimension." + ) + + order = _canonical_region_order(points, labels) + points = points[order] + labels = labels[order] + persistence_values = persistence_values[order] + projected = PCA(n_components=2).fit_transform(points) + region_names, medoids = _region_medoids(points, labels) + + fig, axes = plt.subplots(1, 2, figsize=(13, 5)) + colors = plt.get_cmap("tab10") + for index, region in enumerate(region_names): + mask = labels == region + sizes = 30.0 + 20.0 * persistence_values[mask] / persistence_values.max() + axes[0].scatter( + projected[mask, 0], + projected[mask, 1], + s=sizes, + alpha=0.75, + color=colors(index % 10), + label=f"Region {region}", + ) + axes[0].set( + xlabel="PC1", + ylabel="PC2", + title="Robust-region PCA overview", + ) + axes[0].legend(fontsize=8) + + positions = np.arange(points.shape[1]) + for index, (region, medoid) in enumerate(zip(region_names, medoids)): + axes[1].plot( + positions, + medoid, + marker="o", + color=colors(index % 10), + label=f"Region {region}", + ) + axes[1].set_xticks(positions, names, rotation=35, ha="right") + axes[1].set( + ylim=(-0.03, 1.03), + ylabel="Normalized input coordinate", + title="Robust-region medoids in normalized space", + ) + axes[1].legend(fontsize=8) + return _save_debug_figure(fig, output_path) + + +def plot_ard_lengthscale_comparison( + hyperparameters: pd.DataFrame, + output_path: str | Path, + *, + variant_column: str = "variant_name", + objective_column: str = "objective_name", + lengthscale_columns: Mapping[str, str] | None = None, +) -> Path: + """Compare ARD lengthscales across model variants and objectives.""" + + frame = _frame( + hyperparameters, + name="hyperparameters", + required_columns=(variant_column, objective_column), + ) + _labels(frame, variant_column, name="hyperparameters") + _labels(frame, objective_column, name="hyperparameters") + if lengthscale_columns is None: + detected = { + column.removeprefix("ard_lengthscale_"): column + for column in frame.columns + if column.startswith("ard_lengthscale_") + and not column.endswith( + ( + "_near_floor", + "_very_small_normalized_domain", + "_extremely_large_flat", + ) + ) + } + columns = detected + else: + columns = { + str(input_name).strip(): str(column).strip() + for input_name, column in lengthscale_columns.items() + } + if not columns or any(not name or not column for name, column in columns.items()): + raise ValueError( + "lengthscale_columns must identify at least one nonblank ARD column." + ) + missing = sorted(set(columns.values()) - set(frame.columns)) + if missing: + raise ValueError(f"hyperparameters is missing ARD columns: {missing}.") + _numeric(frame, tuple(columns.values()), name="hyperparameters") + if (frame[list(columns.values())] <= 0.0).any().any(): + raise ValueError("ARD lengthscales must be strictly positive.") + + variants = sorted(frame[variant_column].unique(), key=_natural_label_key) + objectives = sorted(frame[objective_column].unique(), key=_natural_label_key) + observed_pairs = set( + frame[[variant_column, objective_column]].itertuples(index=False, name=None) + ) + expected_pairs = { + (variant, objective) for variant in variants for objective in objectives + } + if observed_pairs != expected_pairs: + raise ValueError( + "hyperparameters must cover every model-variant/objective pair." + ) + + figure_width = max(6.5, 5.2 * len(objectives)) + fig, axes_value = plt.subplots( + 1, len(objectives), figsize=(figure_width, 4.8), squeeze=False + ) + axes = axes_value.ravel() + positions = np.arange(len(columns), dtype=float) + colors = plt.get_cmap("tab10") + for objective_index, objective in enumerate(objectives): + axis = axes[objective_index] + for variant_index, variant in enumerate(variants): + group = frame[ + (frame[variant_column] == variant) + & (frame[objective_column] == objective) + ] + values = group.loc[:, list(columns.values())].to_numpy(dtype=float) + medians = np.median(values, axis=0) + lower = np.quantile(values, 0.25, axis=0) + upper = np.quantile(values, 0.75, axis=0) + axis.errorbar( + positions, + medians, + yerr=np.vstack((medians - lower, upper - medians)), + marker="o", + capsize=3, + color=colors(variant_index % 10), + label=variant, + ) + axis.set_xticks(positions, list(columns), rotation=35, ha="right") + axis.set_yscale("log") + axis.set( + xlabel="Normalized input dimension", + ylabel="ARD lengthscale (log scale)", + title=f"{objective}: ARD comparison", + ) + axis.grid(axis="y", which="both", alpha=0.25) + axes[0].legend(fontsize=8) + return _save_debug_figure(fig, output_path) + + +def plot_control_omission_candidate_region_comparison( + full_candidates_norm: np.ndarray, + control_omitted_candidates_norm: np.ndarray, + output_path: str | Path, + *, + full_region_labels: Sequence[object] | None = None, + omit_region_labels: Sequence[object] | None = None, +) -> Path: + """Compare full-fit and configured-control-omission candidates with PCA.""" + + full = _matrix(full_candidates_norm, name="full_candidates_norm") + omitted = _matrix( + control_omitted_candidates_norm, name="control_omitted_candidates_norm" + ) + if full.shape[1] != omitted.shape[1]: + raise ValueError("Full and control-omitted candidates must share dimensions.") + full_labels = ( + np.repeat("unassigned", full.shape[0]) + if full_region_labels is None + else _validate_aligned_labels( + full_region_labels, + count=full.shape[0], + name="full_region_labels", + default_prefix="full-", + ) + ) + omitted_labels = ( + np.repeat("unassigned", omitted.shape[0]) + if omit_region_labels is None + else _validate_aligned_labels( + omit_region_labels, + count=omitted.shape[0], + name="omit_region_labels", + default_prefix="omit-", + ) + ) + + full_order = np.lexsort( + tuple(full[:, column] for column in reversed(range(full.shape[1]))) + ) + omitted_order = np.lexsort( + tuple(omitted[:, column] for column in reversed(range(omitted.shape[1]))) + ) + full = full[full_order] + omitted = omitted[omitted_order] + full_labels = full_labels[full_order] + omitted_labels = omitted_labels[omitted_order] + combined = np.vstack((full, omitted)) + if combined.shape[0] < 2: + raise ValueError("At least two total candidate rows are required for PCA.") + pca = PCA(n_components=2) + projected = pca.fit_transform(combined) + for component in range(2): + loading = pca.components_[component] + anchor = int(np.argmax(np.abs(loading))) + if loading[anchor] < 0.0: + projected[:, component] *= -1.0 + full_projected = projected[: full.shape[0]] + omitted_projected = projected[full.shape[0] :] + full_match, omitted_match = linear_sum_assignment(cdist(full, omitted)) + + fig, axis = plt.subplots(figsize=(8.2, 5.6)) + for full_index, omitted_index in zip(full_match, omitted_match): + axis.plot( + [full_projected[full_index, 0], omitted_projected[omitted_index, 0]], + [full_projected[full_index, 1], omitted_projected[omitted_index, 1]], + color="0.7", + linewidth=1, + zorder=1, + ) + region_names = sorted( + set(full_labels) | set(omitted_labels), key=_natural_label_key + ) + color_lookup = { + region: plt.get_cmap("tab10")(index % 10) + for index, region in enumerate(region_names) + } + for values, labels, marker, scenario in ( + (full_projected, full_labels, "o", "Full model"), + (omitted_projected, omitted_labels, "^", "Omit configured control"), + ): + axis.scatter( + values[:, 0], + values[:, 1], + c=[color_lookup[label] for label in labels], + marker=marker, + s=75, + edgecolor="black", + linewidth=0.5, + label=scenario, + zorder=2, + ) + if region_names != ["unassigned"] and len(region_names) <= 10: + for region in region_names: + axis.scatter( + [], + [], + color=color_lookup[region], + marker="s", + s=45, + label=region, + ) + axis.set( + xlabel="Common PCA coordinate 1", + ylabel="Common PCA coordinate 2", + title="Full versus configured-control-omission candidate regions", + ) + axis.legend(fontsize=8) + return _save_debug_figure(fig, output_path) + + +# Backward-compatible callable name; plot text and semantics are control-generic. +plot_sample1_candidate_region_comparison = ( + plot_control_omission_candidate_region_comparison +) + + +def plot_candidate_predictions_vs_observed_ranges( + predictions: pd.DataFrame, + output_path: str | Path, + *, + candidate_column: str = "candidate_id", + model_column: str = "model_variant", + objective_column: str = "objective_name", + mean_column: str = "predicted_mean", + std_column: str = "predicted_std", + observed_min_column: str = "observed_minimum", + observed_max_column: str = "observed_maximum", +) -> Path: + """Show candidate posterior predictions relative to observed score ranges.""" + + required = ( + candidate_column, + model_column, + objective_column, + mean_column, + std_column, + observed_min_column, + observed_max_column, + ) + frame = _frame(predictions, name="predictions", required_columns=required) + for column in (candidate_column, model_column, objective_column): + _labels(frame, column, name="predictions") + _numeric( + frame, + (mean_column, std_column, observed_min_column, observed_max_column), + name="predictions", + ) + if (frame[std_column] <= 0.0).any(): + raise ValueError("candidate predictive standard deviations must be positive.") + if (frame[observed_min_column] > frame[observed_max_column]).any(): + raise ValueError("observed minimum must not exceed observed maximum.") + identity_columns = [candidate_column, model_column, objective_column] + if frame.duplicated(identity_columns).any(): + raise ValueError("candidate/model/objective prediction rows must be unique.") + range_counts = frame.groupby(objective_column, sort=False)[ + [observed_min_column, observed_max_column] + ].nunique(dropna=False) + if (range_counts > 1).any().any(): + raise ValueError("each objective must use one consistent observed range.") + + objectives = sorted(frame[objective_column].unique(), key=_natural_label_key) + maximum_rows = int(frame.groupby(objective_column).size().max()) + fig, axes_value = plt.subplots( + 1, + len(objectives), + figsize=(max(7.0, 5.3 * len(objectives)), max(4.8, 0.3 * maximum_rows + 2.5)), + squeeze=False, + ) + axes = axes_value.ravel() + colors = plt.get_cmap("tab10") + models = sorted(frame[model_column].unique(), key=_natural_label_key) + model_colors = {model: colors(index % 10) for index, model in enumerate(models)} + for objective_index, objective in enumerate(objectives): + axis = axes[objective_index] + group = frame[frame[objective_column] == objective].sort_values( + [candidate_column, model_column], + key=lambda values: values.map(_natural_label_key), + kind="mergesort", + ) + positions = np.arange(len(group), dtype=float) + observed_min = float(group[observed_min_column].iloc[0]) + observed_max = float(group[observed_max_column].iloc[0]) + axis.axvspan( + observed_min, + observed_max, + color="0.88", + alpha=0.8, + label="Observed range", + ) + for model in models: + mask = group[model_column] == model + if mask.any(): + axis.errorbar( + group.loc[mask, mean_column], + positions[mask.to_numpy()], + xerr=group.loc[mask, std_column], + fmt="o", + capsize=3, + color=model_colors[model], + label=model, + ) + labels = [ + f"{candidate} | {model}" + for candidate, model in group[[candidate_column, model_column]].itertuples( + index=False, name=None + ) + ] + axis.set_yticks(positions, labels, fontsize=7) + axis.set( + xlabel="Posterior mean ± one standard deviation", + title=f"{objective}: prediction vs observed range", + ) + axis.invert_yaxis() + axes[0].legend(fontsize=7) + return _save_debug_figure(fig, output_path) + + +def plot_shortlist_medoid_parallel_coordinates( + shortlist: pd.DataFrame, + output_path: str | Path, + *, + region_column: str = "region_id", + persistence_column: str = "family_weighted_persistence", + input_names: Sequence[str] | None = None, +) -> Path: + """Plot normalized shortlist medoids as deterministic parallel coordinates.""" + + frame = _frame(shortlist, name="shortlist", required_columns=(region_column,)) + _labels(frame, region_column, name="shortlist") + if frame[region_column].duplicated().any(): + raise ValueError("shortlist region identifiers must be unique.") + norm_columns = _normalized_columns(frame, prefix="medoid_norm_", name="shortlist") + _numeric(frame, norm_columns, name="shortlist") + normalized = frame[norm_columns] + if ((normalized < 0.0) | (normalized > 1.0)).any().any(): + raise ValueError("shortlist medoid coordinates must remain in [0, 1].") + if persistence_column not in frame.columns: + raise ValueError( + f"shortlist is missing required column: {persistence_column!r}." + ) + _numeric(frame, (persistence_column,), name="shortlist") + if ((frame[persistence_column] < 0.0) | (frame[persistence_column] > 1.0)).any(): + raise ValueError("shortlist persistence values must remain in [0, 1].") + if input_names is None: + names = [f"X{index + 1}" for index in range(len(norm_columns))] + else: + names = [str(value).strip() for value in input_names] + if len(names) != len(norm_columns) or any(not value for value in names): + raise ValueError( + "input_names must contain one nonblank name per medoid dimension." + ) + frame = frame.sort_values( + region_column, + key=lambda values: values.map(_natural_label_key), + kind="mergesort", + ) + positions = np.arange(len(norm_columns), dtype=float) + fig, axis = plt.subplots(figsize=(max(8.5, 0.8 * len(norm_columns)), 5.2)) + colors = plt.get_cmap("tab20") + for index, row in frame.reset_index(drop=True).iterrows(): + persistence = float(row[persistence_column]) + axis.plot( + positions, + row[norm_columns].to_numpy(dtype=float), + marker="o", + linewidth=1.0 + 2.0 * persistence, + alpha=0.4 + 0.55 * persistence, + color=colors(index % 20), + label=str(row[region_column]), + ) + axis.set_xticks(positions, names, rotation=35, ha="right") + axis.set( + ylim=(-0.03, 1.03), + ylabel="Normalized medoid coordinate", + title="Robust-shortlist medoid parallel coordinates", + ) + if len(frame) <= 12: + axis.legend(fontsize=7, ncol=2) + return _save_debug_figure(fig, output_path) + + +def plot_run_region_persistence_heatmap( + membership: pd.DataFrame, + output_path: str | Path, + *, + run_column: str = "run_id", + region_column: str = "region_id", + family_column: str = "run_family", + value_column: str | None = None, +) -> Path: + """Plot run-by-region selection persistence as a deterministic heatmap.""" + + required = [run_column, region_column, family_column] + if value_column is not None: + required.append(value_column) + frame = _frame(membership, name="membership", required_columns=required) + for column in (run_column, region_column, family_column): + _labels(frame, column, name="membership") + family_counts = frame.groupby(run_column, sort=False)[family_column].nunique() + if (family_counts != 1).any(): + raise ValueError("each run must map to exactly one run family.") + run_metadata = frame[[run_column, family_column]].drop_duplicates() + run_metadata = run_metadata.sort_values( + [family_column, run_column], + key=lambda values: values.map(_natural_label_key), + kind="mergesort", + ) + runs = run_metadata[run_column].tolist() + regions = sorted(frame[region_column].unique(), key=_natural_label_key) + if value_column is None: + values = frame[[run_column, region_column]].drop_duplicates().assign(_value=1.0) + value_name = "_value" + colorbar_label = "Region represented (0/1)" + else: + _numeric(frame, (value_column,), name="membership") + if (frame[value_column] < 0.0).any(): + raise ValueError("persistence heatmap values must be non-negative.") + values = frame[[run_column, region_column, value_column]].copy() + value_name = value_column + colorbar_label = value_column.replace("_", " ") + pivot = values.pivot_table( + index=run_column, + columns=region_column, + values=value_name, + aggfunc="sum", + fill_value=0.0, + ).reindex(index=runs, columns=regions, fill_value=0.0) + family_lookup = dict( + run_metadata[[run_column, family_column]].itertuples(index=False, name=None) + ) + row_labels = [f"{family_lookup[run]} | {run}" for run in runs] + return _heatmap( + pivot.to_numpy(dtype=float), + row_labels=row_labels, + column_labels=regions, + title="Core run-by-region persistence", + colorbar_label=colorbar_label, + output_path=output_path, + ) + + +def plot_model_policy_region_correspondence( + correspondence: pd.DataFrame, + output_path: str | Path, + *, + model_column: str = "model_variant", + policy_column: str = "bound_policy", + region_column: str = "region_id", + value_column: str | None = None, +) -> Path: + """Plot candidate-region correspondence by model and utility policy.""" + + required = [model_column, policy_column, region_column] + if value_column is not None: + required.append(value_column) + frame = _frame(correspondence, name="correspondence", required_columns=required) + for column in (model_column, policy_column, region_column): + _labels(frame, column, name="correspondence") + frame["_model_policy"] = ( + frame[model_column].astype(str) + " | " + frame[policy_column].astype(str) + ) + row_labels = sorted(frame["_model_policy"].unique(), key=_natural_label_key) + regions = sorted(frame[region_column].unique(), key=_natural_label_key) + if value_column is None: + values = frame.assign(_value=1.0) + value_name = "_value" + colorbar_label = "Selected candidate count" + else: + _numeric(frame, (value_column,), name="correspondence") + if (frame[value_column] < 0.0).any(): + raise ValueError("correspondence values must be non-negative.") + values = frame + value_name = value_column + colorbar_label = value_column.replace("_", " ") + pivot = values.pivot_table( + index="_model_policy", + columns=region_column, + values=value_name, + aggfunc="sum", + fill_value=0.0, + ).reindex(index=row_labels, columns=regions, fill_value=0.0) + return _heatmap( + pivot.to_numpy(dtype=float), + row_labels=row_labels, + column_labels=regions, + title="Model/policy candidate-region correspondence", + colorbar_label=colorbar_label, + output_path=output_path, + cmap="Purples", + ) + + +def plot_acquisition_quality_vs_persistence( + regions: pd.DataFrame, + output_path: str | Path, + *, + region_column: str = "region_id", + persistence_column: str = "family_weighted_persistence", + acquisition_column: str = "median_acquisition_score", + family_count_column: str = "distinct_family_count", +) -> Path: + """Plot regional acquisition quality against family-weighted persistence.""" + + required = ( + region_column, + persistence_column, + acquisition_column, + family_count_column, + ) + frame = _frame(regions, name="regions", required_columns=required) + _labels(frame, region_column, name="regions") + _numeric( + frame, + (persistence_column, acquisition_column, family_count_column), + name="regions", + ) + if frame[region_column].duplicated().any(): + raise ValueError("region identifiers must be unique.") + if ((frame[persistence_column] < 0.0) | (frame[persistence_column] > 1.0)).any(): + raise ValueError("region persistence values must remain in [0, 1].") + if (frame[acquisition_column] < 0.0).any(): + raise ValueError("regional acquisition quality must be non-negative.") + if (frame[family_count_column] <= 0.0).any(): + raise ValueError("distinct family counts must be strictly positive.") + frame = frame.sort_values( + region_column, + key=lambda values: values.map(_natural_label_key), + kind="mergesort", + ) + sizes = 40.0 + 35.0 * frame[family_count_column].to_numpy(dtype=float) + fig, axis = plt.subplots(figsize=(7.5, 5.2)) + axis.scatter( + frame[persistence_column], + frame[acquisition_column], + s=sizes, + c=frame[family_count_column], + cmap="viridis", + alpha=0.8, + edgecolor="black", + linewidth=0.5, + ) + for _, row in frame.iterrows(): + axis.annotate( + str(row[region_column]), + ( + float(row[persistence_column]), + float(row[acquisition_column]), + ), + xytext=(4, 4), + textcoords="offset points", + fontsize=7, + ) + axis.set( + xlim=(-0.03, 1.03), + xlabel="Family-weighted persistence", + ylabel="Median base HVI", + title="Acquisition quality versus region persistence", + ) + return _save_debug_figure(fig, output_path) + + +def plot_shortlist_region_influence_sensitivity( + sensitivity: pd.DataFrame, + output_path: str | Path, + *, + region_column: str = "region_id", + omitted_sample_column: str = "omitted_sample_id", + sensitivity_column: str = "prediction_change", +) -> Path: + """Plot omission sensitivity for each shortlisted robust region.""" + + frame = _frame( + sensitivity, + name="sensitivity", + required_columns=(region_column, omitted_sample_column, sensitivity_column), + ) + _labels(frame, region_column, name="sensitivity") + _labels(frame, omitted_sample_column, name="sensitivity") + _numeric(frame, (sensitivity_column,), name="sensitivity") + if (frame[sensitivity_column] < 0.0).any(): + raise ValueError("shortlist-region sensitivity must be non-negative.") + regions = sorted(frame[region_column].unique(), key=_natural_label_key) + samples = sorted(frame[omitted_sample_column].unique(), key=_natural_label_key) + pivot = frame.pivot_table( + index=region_column, + columns=omitted_sample_column, + values=sensitivity_column, + aggfunc="mean", + ).reindex(index=regions, columns=samples) + if pivot.isna().any().any(): + raise ValueError( + "sensitivity must cover every shortlist-region/omitted-sample pair." + ) + return _heatmap( + pivot.to_numpy(dtype=float), + row_labels=regions, + column_labels=[f"Omit {sample}" for sample in samples], + title="Shortlist-region observation-influence sensitivity", + colorbar_label=sensitivity_column.replace("_", " "), + output_path=output_path, + cmap="Oranges", + ) + + +__all__ = [ + "DEBUG_WATERMARK", + "plot_acquisition_quality_vs_persistence", + "plot_ard_lengthscale_comparison", + "plot_boundary_enrichment", + "plot_bounded_utility_comparison", + "plot_candidate_predictions_vs_observed_ranges", + "plot_control_omission_candidate_region_comparison", + "plot_local_penalty_tradeoff", + "plot_loocv_diagnostics", + "plot_model_policy_region_correspondence", + "plot_nested_search_convergence", + "plot_observation_influence_ranking", + "plot_robust_region_overview", + "plot_run_region_persistence_heatmap", + "plot_sample1_candidate_region_comparison", + "plot_shortlist_medoid_parallel_coordinates", + "plot_shortlist_region_influence_sensitivity", +] diff --git a/src/mobo_kit/sobol_pool.py b/src/mobo_kit/sobol_pool.py new file mode 100644 index 0000000..172dd2f --- /dev/null +++ b/src/mobo_kit/sobol_pool.py @@ -0,0 +1,399 @@ +"""Deterministic nested Sobol prefixes for finite discrete designs.""" + +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +from math import prod +from typing import Sequence +import warnings + +import numpy as np +import scipy +from scipy.stats import qmc + +from .candidate_pool import ( + CandidatePool, + CandidatePoolSamplingError, + physical_rows_to_grid_indices, +) +from .constraints import RowConstraint, apply_row_constraints +from .design import Design + + +@dataclass(frozen=True) +class NestedSobolPoolResult: + """Accepted-unique Sobol prefixes and their reproducibility metadata.""" + + pools: dict[int, CandidatePool] + prefix_hashes: dict[int, str] + accepted_sizes: tuple[int, ...] + scramble_seed: int + raw_sobol_draws: int + rejected_duplicate: int + rejected_avoid: int + rejected_constraint: int + ignored_off_grid_observed: int + scipy_version: str + + @property + def largest_pool(self) -> CandidatePool: + """Return the largest accepted prefix.""" + + return self.pools[self.accepted_sizes[-1]] + + @property + def accepted_count(self) -> int: + """Return the accepted count in the final master prefix.""" + + return self.largest_pool.size + + @property + def pools_by_size(self) -> dict[int, CandidatePool]: + """Alias spelling useful to report-building callers.""" + + return self.pools + + +@dataclass(frozen=True) +class _PrefixSnapshot: + draws: int + rejected_duplicate: int + rejected_avoid: int + rejected_constraint: int + + +def _validated_grids(design: Design) -> tuple[np.ndarray, ...]: + if not isinstance(design, Design): + raise TypeError("design must be a Design.") + grids = tuple(np.asarray(grid, dtype=float) for grid in design.var_array) + if not grids or len(grids) != len(design.names): + raise ValueError("design must contain one non-empty grid per input name.") + for name, grid in zip(design.names, grids): + if grid.ndim != 1 or grid.size == 0: + raise ValueError(f"Design grid {name!r} must be a non-empty vector.") + if not np.all(np.isfinite(grid)) or np.unique(grid).size != grid.size: + raise ValueError(f"Design grid {name!r} must be finite and unique.") + return grids + + +def _positive_sizes(values: Sequence[int]) -> tuple[int, ...]: + if isinstance(values, (str, bytes)): + raise TypeError("accepted_sizes must be a non-empty sequence of integers.") + try: + raw = tuple(values) + except TypeError as exc: + raise TypeError( + "accepted_sizes must be a non-empty sequence of integers." + ) from exc + if not raw: + raise ValueError("accepted_sizes must not be empty.") + if any( + isinstance(value, (bool, np.bool_)) + or not isinstance(value, (int, np.integer)) + or int(value) <= 0 + for value in raw + ): + raise ValueError("accepted_sizes must contain only positive integers.") + normalized = tuple(int(value) for value in raw) + if len(set(normalized)) != len(normalized): + raise ValueError("accepted_sizes must not contain duplicates.") + return tuple(sorted(normalized)) + + +def _non_negative_seed(value: int) -> int: + if ( + isinstance(value, (bool, np.bool_)) + or not isinstance(value, (int, np.integer)) + or int(value) < 0 + ): + raise ValueError("scramble_seed must be a non-negative integer.") + return int(value) + + +def _physical_rows( + value: np.ndarray | None, *, name: str, dimension: int +) -> np.ndarray: + if value is None: + return np.empty((0, dimension), dtype=float) + array = np.asarray(value, dtype=float) + if array.ndim != 2 or array.shape[1] != dimension: + raise ValueError(f"{name} must have shape (N, {dimension}); got {array.shape}.") + if not np.all(np.isfinite(array)): + raise ValueError(f"{name} must contain only finite values.") + return array + + +def map_unit_points_to_grid_indices( + unit_points: np.ndarray, + design: Design, +) -> np.ndarray: + """Map points in ``[0, 1)`` to exact discrete grid indices.""" + + grids = _validated_grids(design) + points = np.asarray(unit_points, dtype=float) + if points.ndim != 2 or points.shape[1] != len(grids): + raise ValueError( + "unit_points must have shape (N, n_design_inputs); " + f"got {points.shape} for {len(grids)} inputs." + ) + if not np.all(np.isfinite(points)): + raise ValueError("unit_points must contain only finite values.") + if np.any(points < 0.0) or np.any(points >= 1.0): + raise ValueError("unit_points must lie in the half-open interval [0, 1).") + axis_sizes = np.asarray([grid.size for grid in grids], dtype=np.int64) + indices = np.floor(points * axis_sizes[None, :]).astype(np.int64) + return np.minimum(indices, axis_sizes[None, :] - 1) + + +def _indices_to_physical( + grid_indices: np.ndarray, + grids: tuple[np.ndarray, ...], +) -> np.ndarray: + physical = np.empty(grid_indices.shape, dtype=float) + for column, grid in enumerate(grids): + physical[:, column] = grid[grid_indices[:, column]] + return physical + + +def _normalize_physical(X_phys: np.ndarray, design: Design) -> np.ndarray: + lower = np.asarray(design.lowers, dtype=float) + upper = np.asarray(design.uppers, dtype=float) + spans = upper - lower + normalized = np.zeros_like(X_phys, dtype=float) + changing = spans > 0.0 + normalized[:, changing] = (X_phys[:, changing] - lower[changing]) / spans[changing] + return normalized + + +def hash_grid_index_prefix(grid_indices: np.ndarray) -> str: + """Return a platform-stable SHA-256 for one integer-index prefix.""" + + indices = np.asarray(grid_indices) + if indices.ndim != 2: + raise ValueError("grid_indices must be a two-dimensional integer matrix.") + if not np.issubdtype(indices.dtype, np.integer): + raise TypeError("grid_indices must have an integer dtype.") + canonical = np.ascontiguousarray(indices, dtype=" np.ndarray: + if rows.shape[0] == 0: + return np.empty((0, len(design.names)), dtype=np.int64) + return physical_rows_to_grid_indices(rows, design) + + +def _observed_exclusion_indices( + rows: np.ndarray, + design: Design, +) -> tuple[np.ndarray, int]: + """Partition observed rows, deliberately skipping exact off-grid controls.""" + + accepted: list[np.ndarray] = [] + ignored = 0 + lower = np.asarray(design.lowers, dtype=float) + upper = np.asarray(design.uppers, dtype=float) + for row in rows: + if np.any(row < lower) or np.any(row > upper): + raise ValueError( + "observed_phys rows must remain within the design bounds even " + "when an observed recipe is off-grid." + ) + try: + accepted.append(physical_rows_to_grid_indices(row[None, :], design)[0]) + except ValueError as exc: + if "off-grid" not in str(exc): + raise + ignored += 1 + if not accepted: + return np.empty((0, len(design.names)), dtype=np.int64), ignored + return np.asarray(accepted, dtype=np.int64), ignored + + +def _sobol_prefix(dimension: int, draws: int, seed: int) -> np.ndarray: + engine = qmc.Sobol(d=dimension, scramble=True, seed=seed) + if draws > 0 and draws & (draws - 1) == 0: + return engine.random_base2(int(draws.bit_length() - 1)) + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The balance properties of Sobol.*", + category=UserWarning, + ) + return engine.random(draws) + + +def build_nested_sobol_discrete_pool( + design: Design, + accepted_sizes: Sequence[int], + *, + scramble_seed: int, + observed_phys: np.ndarray | None = None, + pending_phys: np.ndarray | None = None, + avoid_phys: np.ndarray | None = None, + row_constraints: Sequence[RowConstraint] | None = None, + max_raw_draws: int | None = None, +) -> NestedSobolPoolResult: + """Build exact accepted-unique nested prefixes from one Sobol scramble. + + Observed rows that are not exact grid members are intentionally omitted from + index exclusion (the D2D control is such a row). Pending and explicit avoid + rows must be exact grid members and fail closed otherwise. + """ + + grids = _validated_grids(design) + sizes = _positive_sizes(accepted_sizes) + seed = _non_negative_seed(scramble_seed) + largest = sizes[-1] + if max_raw_draws is None: + draw_limit = max(1024, largest * 50) + elif ( + isinstance(max_raw_draws, (bool, np.bool_)) + or not isinstance(max_raw_draws, (int, np.integer)) + or int(max_raw_draws) <= 0 + ): + raise ValueError("max_raw_draws must be a positive integer.") + else: + draw_limit = int(max_raw_draws) + if draw_limit < largest: + raise CandidatePoolSamplingError( + requested=largest, + accepted=0, + draws=0, + max_draws=draw_limit, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + reason="max_raw_draws is smaller than the requested accepted prefix.", + ) + + dimension = len(grids) + observed = _physical_rows(observed_phys, name="observed_phys", dimension=dimension) + pending = _physical_rows(pending_phys, name="pending_phys", dimension=dimension) + explicit_avoid = _physical_rows(avoid_phys, name="avoid_phys", dimension=dimension) + observed_indices, ignored_off_grid = _observed_exclusion_indices(observed, design) + pending_indices = _strict_exclusion_indices(pending, design) + avoid_indices = _strict_exclusion_indices(explicit_avoid, design) + exclusions = np.vstack([observed_indices, pending_indices, avoid_indices]) + avoid_set = {tuple(int(value) for value in row) for row in exclusions} + + total_grid_size = prod(int(grid.size) for grid in grids) + available = total_grid_size - len(avoid_set) + if largest > available: + raise CandidatePoolSamplingError( + requested=largest, + accepted=0, + draws=0, + max_draws=draw_limit, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + reason=( + "The request exceeds the number of grid tuples remaining after " + f"exact exclusions ({available})." + ), + ) + + seen: set[tuple[int, ...]] = set() + accepted: list[tuple[int, ...]] = [] + snapshots: dict[int, _PrefixSnapshot] = {} + draws = rejected_duplicate = rejected_avoid = rejected_constraint = 0 + prefix_draws = 1 << max(0, (largest - 1).bit_length()) + + while len(accepted) < largest and draws < draw_limit: + target_draws = min(prefix_draws, draw_limit) + points = _sobol_prefix(dimension, target_draws, seed) + new_indices = map_unit_points_to_grid_indices( + points[draws:target_draws], design + ) + for row in new_indices: + draws += 1 + key = tuple(int(value) for value in row) + if key in seen: + rejected_duplicate += 1 + continue + seen.add(key) + if key in avoid_set: + rejected_avoid += 1 + continue + if row_constraints: + physical = _indices_to_physical(row[None, :], grids) + if not bool( + apply_row_constraints(physical, design, row_constraints)[0] + ): + rejected_constraint += 1 + continue + accepted.append(key) + accepted_count = len(accepted) + if accepted_count in sizes: + snapshots[accepted_count] = _PrefixSnapshot( + draws=draws, + rejected_duplicate=rejected_duplicate, + rejected_avoid=rejected_avoid, + rejected_constraint=rejected_constraint, + ) + if accepted_count == largest: + break + if target_draws == draw_limit: + break + prefix_draws *= 2 + + if len(accepted) != largest: + raise CandidatePoolSamplingError( + requested=largest, + accepted=len(accepted), + draws=draws, + max_draws=draw_limit, + rejected_duplicate=rejected_duplicate, + rejected_avoid=rejected_avoid, + rejected_constraint=rejected_constraint, + reason=( + "Maximum raw Sobol draws reached; exact exclusions and constraints " + "were not relaxed." + ), + ) + + all_indices = np.asarray(accepted, dtype=np.int64) + pools: dict[int, CandidatePool] = {} + hashes: dict[int, str] = {} + for size in sizes: + indices = all_indices[:size].copy() + physical = _indices_to_physical(indices, grids) + snapshot = snapshots[size] + pools[size] = CandidatePool( + grid_indices=indices, + X_phys=physical, + X_norm=_normalize_physical(physical, design), + seed=seed, + draws=snapshot.draws, + rejected_duplicate=snapshot.rejected_duplicate, + rejected_avoid=snapshot.rejected_avoid, + rejected_constraint=snapshot.rejected_constraint, + ) + hashes[size] = hash_grid_index_prefix(indices) + + return NestedSobolPoolResult( + pools=pools, + prefix_hashes=hashes, + accepted_sizes=sizes, + scramble_seed=seed, + raw_sobol_draws=draws, + rejected_duplicate=rejected_duplicate, + rejected_avoid=rejected_avoid, + rejected_constraint=rejected_constraint, + ignored_off_grid_observed=ignored_off_grid, + scipy_version=scipy.__version__, + ) + + +__all__ = [ + "NestedSobolPoolResult", + "build_nested_sobol_discrete_pool", + "hash_grid_index_prefix", + "map_unit_points_to_grid_indices", +] diff --git a/src/mobo_kit/step2c_artifacts.py b/src/mobo_kit/step2c_artifacts.py new file mode 100644 index 0000000..bdbb076 --- /dev/null +++ b/src/mobo_kit/step2c_artifacts.py @@ -0,0 +1,2272 @@ +"""Read-only validation for completed D2D Step 2C artifact bundles.""" + +from __future__ import annotations + +import csv +from dataclasses import dataclass +import getpass +import hashlib +import io +import json +import math +import os +from pathlib import Path, PurePosixPath +import re +import subprocess +from typing import Any, Mapping, Sequence +import zipfile + +from PIL import Image, UnidentifiedImageError +import yaml + + +DEBUG_WATERMARK = "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" + +PUBLIC_SUMMARY_ARCHIVE_ROOT = "MOBO_Kit_Step2C_Public_Summary" +PUBLIC_SUMMARY_SCHEMA_VERSION = "d2d-step2c-public-summary-v1" +PUBLIC_SUMMARY_README_FILE = "PUBLIC_SUMMARY_README.txt" +PUBLIC_SUMMARY_MANIFEST_FILE = "PUBLIC_SUMMARY_MANIFEST.json" +PUBLIC_SUMMARY_DEBUG_MARKER_FILE = "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt" + +# Public export is deliberately allowlisted. These tables contain only aggregate +# model/search diagnostics; row-level measurements, candidate coordinates, +# recipes, private-workbook proof, and plots of recipe coordinates stay in the +# ignored local evidence bundle. +PUBLIC_SUMMARY_CSV_FILES = ( + "model_validation_summary.csv", + "model_hyperparameter_stability_summary.csv", + "nested_pool_convergence_summary.csv", + "sobol_scramble_comparison.csv", + "bounded_utility_comparison.csv", + "local_penalty_tradeoff.csv", + "beta_robustness.csv", + "boundary_enrichment.csv", + "study_run_summary.csv", +) + +PUBLIC_SUMMARY_FILES = ( + PUBLIC_SUMMARY_DEBUG_MARKER_FILE, + PUBLIC_SUMMARY_README_FILE, + PUBLIC_SUMMARY_MANIFEST_FILE, + *PUBLIC_SUMMARY_CSV_FILES, +) + +REQUIRED_TOP_LEVEL_FILES = ( + "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt", + "workbook_audit.json", + "score_validation.csv", + "training_row_manifest.csv", + "resolved_debug_config.yaml", + "run_manifest.json", + "model_validation_summary.csv", + "loocv_predictions_long.csv", + "model_hyperparameters.csv", + "model_fit_warnings.csv", + "nested_pool_manifest.csv", + "nested_pool_convergence_summary.csv", + "nested_pool_candidates_long.csv", + "sobol_scramble_comparison.csv", + "local_refinement_trace.csv", + "bounded_utility_comparison.csv", + "local_penalty_tradeoff.csv", + "beta_robustness.csv", + "boundary_enrichment.csv", + "observation_influence_summary.csv", + "observation_influence_candidates_long.csv", + "observation_influence_predictions.csv", + "robust_regions.csv", + "r1_robust_shortlist_debug.csv", +) + +CONSENSUS_BATCH_FILE = "r1_consensus_debug_batch.csv" +NO_STABLE_BATCH_REASON_FILE = "r1_no_stable_batch_reason.json" + +# One PNG may cover multiple closely related topics. For example, the LOOCV +# diagnostic combines parity, standardized residual, and interval-coverage +# panels, while the robust-region overview combines PCA and medoid panels. +REQUIRED_PLOT_FILES = ( + "plots/model_validation/loocv_diagnostics.png", + "plots/model_validation/ard_lengthscale_comparison.png", + "plots/model_validation/candidate_predictions_vs_observed_ranges.png", + "plots/search_convergence/nested_pool_convergence.png", + "plots/search_convergence/boundary_enrichment.png", + "plots/bounded_utility/bounded_utility_comparison.png", + "plots/local_penalty/local_penalty_tradeoff.png", + "plots/influence/observation_influence_ranking.png", + "plots/influence/full_vs_omit_control_candidates.png", + "plots/influence/shortlist_region_influence_sensitivity.png", + "plots/robust_regions/robust_region_overview.png", + "plots/robust_regions/shortlist_parallel_coordinates.png", + "plots/robust_regions/run_region_persistence_heatmap.png", + "plots/robust_regions/model_policy_region_correspondence.png", + "plots/robust_regions/acquisition_quality_vs_persistence.png", +) + +REQUIRED_PLOT_DIRECTORIES = tuple( + dict.fromkeys( + str(Path(name).parent).replace("\\", "/") for name in REQUIRED_PLOT_FILES + ) +) + +_CSV_STAMP_COLUMNS = ( + "debug_only", + "approved_for_experiment", + "approved_for_production", + "candidate_status", +) +_SHA256_PATTERN = re.compile(r"[0-9a-fA-F]{64}") +_GIT_COMMIT_PATTERN = re.compile(r"[0-9a-fA-F]{40}") +_D2D_INPUT_COLUMNS = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", +) +_D2D_OBJECTIVE_NAMES = ( + "uniformity_score", + "optoelectronic_score", + "thickness_score", +) +_GRID_COLUMNS = tuple(f"grid_{index}" for index in range(10)) +_NORM_COLUMNS = tuple(f"norm_{index}" for index in range(10)) +_MEDOID_GRID_COLUMNS = tuple(f"medoid_grid_{index}" for index in range(10)) +_MEDOID_NORM_COLUMNS = tuple(f"medoid_norm_{index}" for index in range(10)) + +_PUBLIC_PRIVATE_FILE_NAMES = frozenset( + { + "workbook_audit.json", + "score_validation.csv", + "training_row_manifest.csv", + "resolved_debug_config.yaml", + "run_manifest.json", + "loocv_predictions_long.csv", + "model_hyperparameters.csv", + "model_fit_warnings.csv", + "nested_pool_manifest.csv", + "nested_pool_candidates_long.csv", + "local_refinement_trace.csv", + "bounded_utility_candidates_long.csv", + "observation_influence_summary.csv", + "observation_influence_candidates_long.csv", + "observation_influence_predictions.csv", + "robust_regions.csv", + "robust_region_membership.csv", + "r1_robust_shortlist_debug.csv", + "r1_consensus_debug_batch.csv", + "study_candidates_long.csv", + } +) +_PUBLIC_BANNED_HEADER_NAMES = frozenset( + { + *_D2D_INPUT_COLUMNS, + "sample_id", + "omitted_sample_id", + "row_position", + "row_label", + "row_role", + "measurement_provenance", + "candidate_id", + "consensus_candidate_id", + "shortlist_id", + "selection_order", + "location_id", + } +) +_PUBLIC_BANNED_HEADER_PREFIXES = ( + "grid_", + "norm_", + "phys_", + "medoid_", + "pred_mean_", + "pred_std_", + "ucb_raw_", + "ucb_effective_", +) +_PUBLIC_WINDOWS_ABSOLUTE_PATH = re.compile(r"(?i)(? Step2CArtifactContractError: + return Step2CArtifactContractError(message) + + +def _relative(path: Path, output_dir: Path) -> str: + return path.relative_to(output_dir).as_posix() + + +def _read_yaml_mapping(path: Path, *, label: str) -> dict[str, Any]: + try: + value = yaml.safe_load(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, yaml.YAMLError) as exc: + raise _error(f"{label} must be readable valid YAML: {path}.") from exc + if not isinstance(value, dict): + raise _error(f"{label} must contain a top-level mapping: {path}.") + return value + + +def _configured_output_root(config: Mapping[str, Any], repository_root: Path) -> Path: + outputs = config.get("outputs") + if not isinstance(outputs, Mapping): + raise _error("resolved_debug_config.yaml must contain an outputs mapping.") + raw_root = outputs.get("root") + if not isinstance(raw_root, str) or not raw_root.strip(): + raise _error("resolved_debug_config.yaml outputs.root must be nonblank.") + configured = Path(raw_root.strip()) + if configured.is_absolute(): + resolved = configured.resolve() + else: + resolved = (repository_root / configured).resolve() + if resolved == repository_root or repository_root not in resolved.parents: + raise _error( + "resolved_debug_config.yaml outputs.root must be a strict descendant " + "of repository_root." + ) + return resolved + + +def _validate_config_stamps(config: Mapping[str, Any]) -> None: + if config.get("run_mode") != "debug": + raise _error("resolved_debug_config.yaml run_mode must be exactly 'debug'.") + if config.get("approved_for_experiment") is not False: + raise _error( + "resolved_debug_config.yaml approved_for_experiment must be false." + ) + if config.get("approved_for_production") is not False: + raise _error( + "resolved_debug_config.yaml approved_for_production must be false." + ) + if config.get("debug_watermark") != DEBUG_WATERMARK: + raise _error( + "resolved_debug_config.yaml debug_watermark must equal the exact " + "Step 2C watermark." + ) + + +def _validate_csv_stamp(path: Path, output_dir: Path) -> tuple[tuple[str, ...], int]: + relative = _relative(path, output_dir) + try: + with path.open("r", encoding="utf-8-sig", newline="") as handle: + reader = csv.reader(handle) + try: + header = next(reader) + except StopIteration as exc: + raise _error( + f"CSV artifact {relative} is empty and has no header." + ) from exc + if not header or any(not name.strip() for name in header): + raise _error(f"CSV artifact {relative} has a blank column name.") + if len(set(header)) != len(header): + raise _error(f"CSV artifact {relative} has duplicate column names.") + missing = [name for name in _CSV_STAMP_COLUMNS if name not in header] + if missing: + raise _error( + f"CSV artifact {relative} is missing debug stamp columns: {missing}." + ) + positions = {name: header.index(name) for name in _CSV_STAMP_COLUMNS} + row_count = 0 + for row_number, row in enumerate(reader, start=2): + row_count += 1 + if len(row) != len(header): + raise _error( + f"CSV artifact {relative} row {row_number} has " + f"{len(row)} fields; expected {len(header)}." + ) + if row[positions["debug_only"]].strip().casefold() != "true": + raise _error( + f"CSV artifact {relative} row {row_number} must set " + "debug_only=true." + ) + for approval in ( + "approved_for_experiment", + "approved_for_production", + ): + if row[positions[approval]].strip().casefold() != "false": + raise _error( + f"CSV artifact {relative} row {row_number} must set " + f"{approval}=false." + ) + if row[positions["candidate_status"]] != DEBUG_WATERMARK: + raise _error( + f"CSV artifact {relative} row {row_number} must carry " + "the exact debug watermark." + ) + return tuple(header), row_count + except Step2CArtifactContractError: + raise + except (OSError, UnicodeError, csv.Error) as exc: + raise _error(f"CSV artifact {relative} must be readable valid CSV.") from exc + + +def _validate_required_csv_schema( + relative: str, + header: tuple[str, ...], + row_count: int, +) -> None: + required = REQUIRED_CSV_COLUMNS.get(relative) + if required is None: + return + missing = sorted(set(required) - set(header)) + if missing: + raise _error( + f"CSV artifact {relative} is missing required semantic columns: {missing}." + ) + if row_count == 0 and relative not in _EMPTY_CSV_ALLOWED: + raise _error(f"CSV artifact {relative} must contain at least one data row.") + + +def _read_and_validate_json_stamp(path: Path, output_dir: Path) -> dict[str, Any]: + relative = _relative(path, output_dir) + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise _error(f"JSON artifact {relative} must be readable valid JSON.") from exc + if not isinstance(value, dict): + raise _error(f"JSON artifact {relative} must contain a top-level object.") + if value.get("debug_only") is not True: + raise _error(f"JSON artifact {relative} must set debug_only=true.") + if value.get("approved_for_experiment") is not False: + raise _error( + f"JSON artifact {relative} must set approved_for_experiment=false." + ) + if value.get("approved_for_production") is not False: + raise _error( + f"JSON artifact {relative} must set approved_for_production=false." + ) + if value.get("candidate_status") != DEBUG_WATERMARK: + raise _error(f"JSON artifact {relative} must carry the exact debug watermark.") + return value + + +def _validate_png_watermark(path: Path, output_dir: Path) -> None: + relative = _relative(path, output_dir) + try: + with Image.open(path) as image: + image_format = image.format + description = image.info.get("Description") + dimensions = image.size + image.verify() + except (OSError, UnidentifiedImageError) as exc: + raise _error(f"Plot artifact {relative} must be a readable PNG.") from exc + if image_format != "PNG" or dimensions[0] <= 0 or dimensions[1] <= 0: + raise _error(f"Plot artifact {relative} must be a non-empty PNG image.") + if description != DEBUG_WATERMARK: + raise _error( + f"Plot artifact {relative} PNG Description must equal the exact " + "debug watermark." + ) + + +def _validated_sha256(value: Any, *, field: str) -> str: + if not isinstance(value, str) or _SHA256_PATTERN.fullmatch(value) is None: + raise _error(f"run_manifest.json {field} must be a SHA-256 hex digest.") + return value.upper() + + +def _validated_mtime(value: Any, *, field: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise _error( + f"run_manifest.json {field} must be a non-negative integer timestamp." + ) + return value + + +def _sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest().upper() + + +def _sha256_bytes(payload: bytes) -> str: + return hashlib.sha256(payload).hexdigest().upper() + + +def _public_profile_identities(additional: Sequence[str]) -> tuple[str, ...]: + identities = { + value.strip() + for value in ( + getpass.getuser(), + Path.home().name, + os.environ.get("USERNAME", ""), + os.environ.get("USER", ""), + os.environ.get("LOGNAME", ""), + *additional, + ) + if isinstance(value, str) + and len(value.strip()) >= 4 + and value.strip().casefold() not in _IGNORED_PROFILE_IDENTITIES + } + return tuple(sorted(identities, key=str.casefold)) + + +def _scan_public_text( + relative: str, + text: str, + *, + forbidden_profile_strings: Sequence[str], +) -> None: + patterns = ( + (_PUBLIC_WINDOWS_ABSOLUTE_PATH, "a Windows absolute path"), + (_PUBLIC_UNC_PATH, "a UNC path"), + (_PUBLIC_POSIX_PROFILE_PATH, "a macOS/Linux profile path"), + (_PUBLIC_COMMON_POSIX_PATH, "a common POSIX absolute path"), + (_PUBLIC_GENERIC_POSIX_PATH, "a POSIX absolute path"), + (_PUBLIC_WORKBOOK_NAME, "a workbook filename"), + ) + for pattern, description in patterns: + if pattern.search(text) is not None: + raise _error( + f"Public summary entry {relative} contains {description}; " + "the export must be path- and workbook-free." + ) + lowered = text.casefold() + for private_name in _PUBLIC_PRIVATE_FILE_NAMES: + if private_name in lowered: + raise _error( + f"Public summary entry {relative} names private artifact " + f"{private_name}." + ) + for identity in _public_profile_identities(forbidden_profile_strings): + identity_pattern = re.compile( + rf"(?i)(? None: + try: + reader = csv.reader(io.StringIO(text)) + header = next(reader) + except (StopIteration, csv.Error) as exc: + raise _error(f"Public summary CSV {relative} has no valid header.") from exc + if not header or any(not name.strip() for name in header): + raise _error(f"Public summary CSV {relative} has a blank column name.") + if len(header) != len(set(header)): + raise _error(f"Public summary CSV {relative} has duplicate columns.") + normalized_header = tuple(name.strip().casefold() for name in header) + banned = sorted( + name + for name in normalized_header + if name in _PUBLIC_BANNED_HEADER_NAMES + or any(name.startswith(prefix) for prefix in _PUBLIC_BANNED_HEADER_PREFIXES) + ) + if banned: + raise _error( + f"Public summary CSV {relative} contains recipe/sample-level " + f"column(s): {banned}." + ) + missing_stamps = sorted(set(_CSV_STAMP_COLUMNS) - set(header)) + if missing_stamps: + raise _error( + f"Public summary CSV {relative} is missing debug stamp columns: " + f"{missing_stamps}." + ) + positions = {name: header.index(name) for name in _CSV_STAMP_COLUMNS} + row_count = 0 + try: + for row_number, row in enumerate(reader, start=2): + row_count += 1 + if len(row) != len(header): + raise _error( + f"Public summary CSV {relative} row {row_number} has " + f"{len(row)} fields; expected {len(header)}." + ) + if row[positions["debug_only"]].strip().casefold() != "true": + raise _error( + f"Public summary CSV {relative} row {row_number} must set " + "debug_only=true." + ) + for approval in ( + "approved_for_experiment", + "approved_for_production", + ): + if row[positions[approval]].strip().casefold() != "false": + raise _error( + f"Public summary CSV {relative} row {row_number} must set " + f"{approval}=false." + ) + if row[positions["candidate_status"]] != DEBUG_WATERMARK: + raise _error( + f"Public summary CSV {relative} row {row_number} must carry " + "the exact debug watermark." + ) + except csv.Error as exc: + raise _error(f"Public summary CSV {relative} is malformed.") from exc + if row_count == 0: + raise _error(f"Public summary CSV {relative} must contain aggregate rows.") + + +def _validate_public_manifest( + manifest: Mapping[str, Any], payloads: Mapping[str, bytes] +) -> None: + if set(manifest) != _PUBLIC_MANIFEST_FIELDS: + missing = sorted(_PUBLIC_MANIFEST_FIELDS - set(manifest)) + extra = sorted(set(manifest) - _PUBLIC_MANIFEST_FIELDS) + raise _error( + "PUBLIC_SUMMARY_MANIFEST.json must use the strict public schema; " + f"missing={missing}, extra={extra}." + ) + if manifest.get("schema_version") != PUBLIC_SUMMARY_SCHEMA_VERSION: + raise _error("PUBLIC_SUMMARY_MANIFEST.json has the wrong schema version.") + if manifest.get("archive_root") != PUBLIC_SUMMARY_ARCHIVE_ROOT: + raise _error("PUBLIC_SUMMARY_MANIFEST.json has the wrong archive root.") + if manifest.get("mode") not in {"fast", "full"}: + raise _error("PUBLIC_SUMMARY_MANIFEST.json mode must be fast or full.") + if manifest.get("input_data_kind") not in { + "private_campaign_dataset_redacted", + "generated_synthetic_dataset", + } or manifest.get("source_dataset") != manifest.get("input_data_kind"): + raise _error( + "PUBLIC_SUMMARY_MANIFEST.json must use a redacted/generic data label." + ) + required_true = ("public_export", "debug_only") + required_false = ( + "approved_for_experiment", + "approved_for_production", + "contains_candidate_recipes", + "contains_sample_level_data", + "contains_local_paths", + "full_private_evidence_included", + ) + for field in required_true: + if manifest.get(field) is not True: + raise _error(f"PUBLIC_SUMMARY_MANIFEST.json {field} must be true.") + for field in required_false: + if manifest.get(field) is not False: + raise _error(f"PUBLIC_SUMMARY_MANIFEST.json {field} must be false.") + if manifest.get("candidate_status") != DEBUG_WATERMARK: + raise _error("PUBLIC_SUMMARY_MANIFEST.json has the wrong debug watermark.") + commit = manifest.get("git_commit") + if not isinstance(commit, str) or _GIT_COMMIT_PATTERN.fullmatch(commit) is None: + raise _error("PUBLIC_SUMMARY_MANIFEST.json git_commit is invalid.") + if tuple(manifest.get("objective_order", ())) != _D2D_OBJECTIVE_NAMES: + raise _error("PUBLIC_SUMMARY_MANIFEST.json objective order is invalid.") + if manifest.get("moment_method") != "analytic_identity": + raise _error("PUBLIC_SUMMARY_MANIFEST.json moment method is invalid.") + if not isinstance(manifest.get("consensus_passed"), bool) or not all( + isinstance(manifest.get(field), Mapping) + for field in ("consensus_checks", "consensus_observed", "runtime_versions") + ): + raise _error("PUBLIC_SUMMARY_MANIFEST.json aggregate evidence is invalid.") + for field in ("robust_region_count", "shortlist_count"): + value = manifest.get(field) + if isinstance(value, bool) or not isinstance(value, int) or value < 0: + raise _error(f"PUBLIC_SUMMARY_MANIFEST.json {field} is invalid.") + + hashes = manifest.get("included_files") + expected_hash_names = set(PUBLIC_SUMMARY_FILES) - {PUBLIC_SUMMARY_MANIFEST_FILE} + if not isinstance(hashes, Mapping) or set(hashes) != expected_hash_names: + raise _error( + "PUBLIC_SUMMARY_MANIFEST.json included_files must exactly match the " + "allowlisted non-manifest entries." + ) + for relative in sorted(expected_hash_names): + expected_hash = hashes.get(relative) + if ( + not isinstance(expected_hash, str) + or _SHA256_PATTERN.fullmatch(expected_hash) is None + or expected_hash.upper() != _sha256_bytes(payloads[relative]) + ): + raise _error(f"PUBLIC_SUMMARY_MANIFEST.json hash mismatch for {relative}.") + + +def validate_step2c_public_summary_archive( + archive_path: str | Path, + *, + forbidden_profile_strings: Sequence[str] = (), +) -> Step2CPublicSummaryValidation: + """Validate the strict aggregate-only public Step 2C ZIP without mutation.""" + + archive_file = Path(archive_path).resolve() + if not archive_file.is_file(): + raise _error(f"Public summary archive does not exist: {archive_file}.") + expected_entries = { + f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{relative}" for relative in PUBLIC_SUMMARY_FILES + } + payloads: dict[str, bytes] = {} + try: + with zipfile.ZipFile(archive_file, mode="r") as archive: + infos = archive.infolist() + names = [info.filename for info in infos] + if len(names) != len(set(names)): + raise _error("Public summary archive contains duplicate entry names.") + for info in infos: + pure = PurePosixPath(info.filename) + if ( + info.is_dir() + or pure.is_absolute() + or "\\" in info.filename + or ".." in pure.parts + or len(pure.parts) != 2 + ): + raise _error( + f"Unsafe public summary archive entry: {info.filename!r}." + ) + unix_mode = (info.external_attr >> 16) & 0o170000 + if unix_mode == 0o120000: + raise _error( + f"Public summary archive entry is a symbolic link: " + f"{info.filename!r}." + ) + if pure.name.casefold() in _PUBLIC_PRIVATE_FILE_NAMES: + raise _error( + f"Public summary archive includes private artifact " + f"{pure.name}." + ) + if info.file_size > 32 * 1024 * 1024: + raise _error( + f"Public summary entry {info.filename} exceeds 32 MiB." + ) + actual_entries = set(names) + if actual_entries != expected_entries: + missing = sorted(expected_entries - actual_entries) + extra = sorted(actual_entries - expected_entries) + raise _error( + "Public summary archive must contain exactly the allowlisted " + f"surface; missing={missing}, extra={extra}." + ) + for info in infos: + relative = PurePosixPath(info.filename).name + payloads[relative] = archive.read(info) + except Step2CArtifactContractError: + raise + except (OSError, zipfile.BadZipFile, RuntimeError) as exc: + raise _error("Public summary archive must be a readable valid ZIP.") from exc + + texts: dict[str, str] = {} + for relative, payload in payloads.items(): + try: + text = payload.decode("utf-8-sig") + except UnicodeDecodeError as exc: + raise _error( + f"Public summary entry {relative} must be UTF-8 text." + ) from exc + texts[relative] = text + _scan_public_text( + relative, + text, + forbidden_profile_strings=forbidden_profile_strings, + ) + if texts[PUBLIC_SUMMARY_DEBUG_MARKER_FILE].strip() != DEBUG_WATERMARK: + raise _error("Public summary archive has the wrong debug marker.") + for relative in PUBLIC_SUMMARY_CSV_FILES: + _validate_public_csv_payload(relative, texts[relative]) + try: + manifest = json.loads(texts[PUBLIC_SUMMARY_MANIFEST_FILE]) + except json.JSONDecodeError as exc: + raise _error("PUBLIC_SUMMARY_MANIFEST.json must be valid JSON.") from exc + if not isinstance(manifest, Mapping): + raise _error("PUBLIC_SUMMARY_MANIFEST.json must contain an object.") + _validate_public_manifest(manifest, payloads) + + return Step2CPublicSummaryValidation( + archive_path=archive_file, + archive_root=PUBLIC_SUMMARY_ARCHIVE_ROOT, + entries_checked=tuple(sorted(expected_entries)), + entry_sha256={ + relative: _sha256_bytes(payload) + for relative, payload in sorted(payloads.items()) + }, + ) + + +def _canonical_mapping_sha256(value: Mapping[str, Any]) -> str: + payload = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest().upper() + + +def _validate_workbook_proof( + manifest: Mapping[str, Any], repository_root: Path, output_dir: Path +) -> tuple[Path, str, int]: + raw_path = manifest.get("workbook_path") + if not isinstance(raw_path, str) or not raw_path.strip(): + raise _error("run_manifest.json workbook_path must be a nonblank path.") + workbook_path = Path(raw_path.strip()) + if not workbook_path.is_absolute(): + workbook_path = repository_root / workbook_path + workbook_path = workbook_path.resolve() + if not workbook_path.is_file(): + raise _error( + f"run_manifest.json source workbook does not exist: {workbook_path}." + ) + if workbook_path == output_dir or output_dir in workbook_path.parents: + raise _error("The source workbook must remain outside the output bundle.") + + before_hash = _validated_sha256( + manifest.get("workbook_sha256_before"), field="workbook_sha256_before" + ) + after_hash = _validated_sha256( + manifest.get("workbook_sha256_after"), field="workbook_sha256_after" + ) + before_mtime = _validated_mtime( + manifest.get("workbook_mtime_ns_before"), field="workbook_mtime_ns_before" + ) + after_mtime = _validated_mtime( + manifest.get("workbook_mtime_ns_after"), field="workbook_mtime_ns_after" + ) + if manifest.get("source_workbook_modified") is not False: + raise _error( + "run_manifest.json source_workbook_modified must be exactly false." + ) + if before_hash != after_hash: + raise _error("Source workbook SHA-256 changed between before and after proof.") + if before_mtime != after_mtime: + raise _error("Source workbook mtime changed between before and after proof.") + + stat_before = workbook_path.stat() + current_hash = _sha256_file(workbook_path) + stat_after = workbook_path.stat() + if ( + stat_before.st_mtime_ns != stat_after.st_mtime_ns + or stat_before.st_size != stat_after.st_size + ): + raise _error("Source workbook changed while its proof was being validated.") + if current_hash != after_hash: + raise _error( + "The live source workbook SHA-256 does not match run_manifest.json." + ) + if stat_after.st_mtime_ns != after_mtime: + raise _error("The live source workbook mtime does not match run_manifest.json.") + return workbook_path, current_hash, stat_after.st_mtime_ns + + +def _require_mapping_fields( + value: Mapping[str, Any], fields: tuple[str, ...], *, label: str +) -> None: + missing = [field for field in fields if field not in value] + if missing: + raise _error(f"{label} is missing required provenance fields: {missing}.") + + +def _is_finite_number(value: Any) -> bool: + return ( + not isinstance(value, bool) + and isinstance(value, (int, float)) + and math.isfinite(float(value)) + ) + + +def _validate_manifest_provenance( + manifest: Mapping[str, Any], + repository_root: Path, + ignored_output_root: Path, + resolved_config: Mapping[str, Any], +) -> None: + required = ( + "schema_version", + "method_version", + "mode", + "debug_only", + "approved_for_experiment", + "approved_for_production", + "candidate_status", + "real_r2_proposal_generated", + "workbook_writeback_performed", + "git_commit", + "git_dirty", + "git_status_at_start", + "step2b_checkpoint", + "config_path", + "config_sha256", + "resolved_config_hash", + "objective_order", + "objective_source_columns", + "reference_point", + "objective_bounds", + "moment_method", + "analytic_mc_comparison", + "sobol_seeds", + "nested_pool_sizes", + "pool_prefix_hashes", + "local_refinement", + "beta_values", + "bound_policies", + "local_penalty_variants", + "model_variants", + "influence_common_pool_hash", + "influence_common_pool_size", + "influence_omitted_sample_ids", + "robust_region_clustering", + "robust_region_threshold", + "robust_region_sensitivity_counts", + "robust_region_count", + "shortlist_count", + "stability_criteria", + "consensus_passed", + "consensus_checks", + "consensus_observed", + "runtime_versions", + "hardware", + "phase_runtime_seconds", + "runtime_seconds_total", + "known_uniformity_score_mismatch", + "uniformity_warning_count", + "control_assumption", + "off_grid_control_exception", + "output_directory", + "public_summary_archive_requested", + "private_evidence_archive_requested", + ) + _require_mapping_fields(manifest, required, label="run_manifest.json") + if manifest.get("schema_version") != "d2d-step2c-robustness-run-v1": + raise _error("run_manifest.json schema_version is not the Step 2C schema.") + if manifest.get("mode") not in {"fast", "full"}: + raise _error("run_manifest.json mode must be 'fast' or 'full'.") + if manifest.get("debug_only") is not True: + raise _error("run_manifest.json debug_only must be exactly true.") + for field in ( + "approved_for_experiment", + "approved_for_production", + "real_r2_proposal_generated", + "workbook_writeback_performed", + ): + if manifest.get(field) is not False: + raise _error(f"run_manifest.json {field} must be exactly false.") + if manifest.get("candidate_status") != DEBUG_WATERMARK: + raise _error("run_manifest.json must carry the exact debug watermark.") + if not isinstance(manifest.get("git_dirty"), bool) or not isinstance( + manifest.get("git_status_at_start"), list + ): + raise _error("run_manifest.json Git dirty/status provenance is invalid.") + for field in ("git_commit", "step2b_checkpoint"): + value = manifest.get(field) + if not isinstance(value, str) or _GIT_COMMIT_PATTERN.fullmatch(value) is None: + raise _error(f"run_manifest.json {field} must be a 40-character commit.") + + config_path = Path(str(manifest["config_path"])) + if not config_path.is_absolute(): + config_path = repository_root / config_path + config_path = config_path.resolve() + if not config_path.is_file() or ( + config_path != repository_root and repository_root not in config_path.parents + ): + raise _error("run_manifest.json config_path must be a repository file.") + config_hash = _validated_sha256(manifest["config_sha256"], field="config_sha256") + if _sha256_file(config_path) != config_hash: + raise _error("The live Step 2C config SHA-256 does not match the manifest.") + resolved_hash = _validated_sha256( + manifest["resolved_config_hash"], field="resolved_config_hash" + ) + if resolved_hash != _canonical_mapping_sha256(resolved_config): + raise _error( + "The canonical resolved_debug_config.yaml SHA-256 does not match " + "run_manifest.json." + ) + source_config = _read_yaml_mapping(config_path, label="live Step 2C config") + if resolved_hash != _canonical_mapping_sha256(source_config): + raise _error( + "resolved_debug_config.yaml does not canonically match the live Step 2C " + "source config." + ) + + if tuple(manifest["objective_order"]) != _D2D_OBJECTIVE_NAMES: + raise _error("run_manifest.json objective_order changed from Z/AA/AB order.") + reference = manifest["reference_point"] + if ( + not isinstance(reference, list) + or len(reference) != 3 + or not all(_is_finite_number(value) for value in reference) + ): + raise _error( + "run_manifest.json reference_point must contain three finite values." + ) + bounds = manifest["objective_bounds"] + if not isinstance(bounds, list) or len(bounds) != 3: + raise _error("run_manifest.json objective_bounds must contain three pairs.") + if manifest.get("moment_method") != "analytic_identity": + raise _error("run_manifest.json moment_method must be analytic_identity.") + if not isinstance(manifest["analytic_mc_comparison"], Mapping): + raise _error("run_manifest.json analytic_mc_comparison must be a mapping.") + + seeds = manifest["sobol_seeds"] + sizes = manifest["nested_pool_sizes"] + if ( + not isinstance(seeds, list) + or len(seeds) != 3 + or any(isinstance(value, bool) or not isinstance(value, int) for value in seeds) + ): + raise _error("run_manifest.json sobol_seeds must contain three integers.") + if ( + not isinstance(sizes, list) + or len(sizes) != 4 + or any( + isinstance(value, bool) or not isinstance(value, int) or value <= 0 + for value in sizes + ) + or sizes != sorted(set(sizes)) + ): + raise _error( + "run_manifest.json nested_pool_sizes must be four increasing sizes." + ) + prefix_hashes = manifest["pool_prefix_hashes"] + if not isinstance(prefix_hashes, Mapping): + raise _error("run_manifest.json pool_prefix_hashes must be a mapping.") + for seed_index, seed in enumerate(seeds): + by_size = prefix_hashes.get(str(seed)) + if not isinstance(by_size, Mapping): + raise _error( + f"run_manifest.json has no prefix hashes for Sobol seed {seed}." + ) + expected_sizes = sizes if seed_index == 0 else [sizes[-1]] + for size in expected_sizes: + _validated_sha256( + by_size.get(str(size), by_size.get(size)), + field=f"pool_prefix_hashes[{seed}][{size}]", + ) + + for field in ("local_refinement", "hardware", "phase_runtime_seconds"): + if not isinstance(manifest[field], Mapping) or not manifest[field]: + raise _error(f"run_manifest.json {field} must be a non-empty mapping.") + if any( + not _is_finite_number(value) or float(value) < 0.0 + for value in manifest["phase_runtime_seconds"].values() + ): + raise _error( + "run_manifest.json phase runtimes must be finite and non-negative." + ) + if ( + not _is_finite_number(manifest["runtime_seconds_total"]) + or float(manifest["runtime_seconds_total"]) < 0.0 + ): + raise _error("run_manifest.json runtime_seconds_total must be non-negative.") + if tuple(float(value) for value in manifest["beta_values"]) != (1.0, 4.0, 9.0): + raise _error("run_manifest.json beta_values must preserve the 1/4/9 study.") + if set(manifest["bound_policies"]) != {"none", "clip_ucb"}: + raise _error("run_manifest.json bound_policies are incomplete.") + if ( + not isinstance(manifest["local_penalty_variants"], list) + or len(manifest["local_penalty_variants"]) != 5 + ): + raise _error("run_manifest.json must record all five penalty variants.") + model_variants = manifest["model_variants"] + if not isinstance(model_variants, list) or { + item.get("name") for item in model_variants if isinstance(item, Mapping) + } != {"default_current", "conservative"}: + raise _error("run_manifest.json must record both GP model variants.") + _validated_sha256( + manifest["influence_common_pool_hash"], field="influence_common_pool_hash" + ) + if ( + isinstance(manifest["influence_common_pool_size"], bool) + or not isinstance(manifest["influence_common_pool_size"], int) + or manifest["influence_common_pool_size"] <= 0 + or not isinstance(manifest["influence_omitted_sample_ids"], list) + or not manifest["influence_omitted_sample_ids"] + ): + raise _error("run_manifest.json influence-study provenance is invalid.") + if manifest["robust_region_clustering"] != "agglomerative_complete_link" or not ( + _is_finite_number(manifest["robust_region_threshold"]) + and float(manifest["robust_region_threshold"]) > 0.0 + ): + raise _error("run_manifest.json robust-region method/threshold is invalid.") + for field in ("robust_region_count", "shortlist_count"): + if ( + isinstance(manifest[field], bool) + or not isinstance(manifest[field], int) + or manifest[field] <= 0 + ): + raise _error(f"run_manifest.json {field} must be a positive integer.") + + criteria = manifest["stability_criteria"] + checks = manifest["consensus_checks"] + observed = manifest["consensus_observed"] + if not all(isinstance(value, Mapping) for value in (criteria, checks, observed)): + raise _error( + "run_manifest.json consensus criteria/checks/observed must be mappings." + ) + _require_mapping_fields( + criteria, + ( + "consensus_batch_size", + "regional_match_threshold", + "largest_two_nested_regional_match_minimum", + "largest_two_nested_mean_matched_distance_maximum", + "minimum_nonbaseline_core_family_coverage", + "required_pairwise_minimum_distance", + ), + label="run_manifest.json stability_criteria", + ) + if criteria["consensus_batch_size"] != 5: + raise _error( + "run_manifest.json stability criteria must require five candidates." + ) + for field in ( + "require_finite_and_bounded", + "require_unique_and_on_grid", + "require_debug_only_and_approval_false", + ): + if criteria.get(field) is not True: + raise _error( + f"run_manifest.json stability criteria must keep {field}=true." + ) + if not isinstance(manifest["consensus_passed"], bool): + raise _error("run_manifest.json consensus_passed must be boolean.") + + versions = manifest["runtime_versions"] + if not isinstance(versions, Mapping) or not { + "python", + "numpy", + "pandas", + "scipy", + "scikit_learn", + "matplotlib", + "torch", + "botorch", + "gpytorch", + } <= set(versions): + raise _error("run_manifest.json runtime_versions are incomplete.") + if manifest["known_uniformity_score_mismatch"] is not True: + raise _error("run_manifest.json must retain the known Uniformity mismatch.") + if ( + isinstance(manifest["uniformity_warning_count"], bool) + or not isinstance(manifest["uniformity_warning_count"], int) + or manifest["uniformity_warning_count"] <= 0 + ): + raise _error("run_manifest.json must record Uniformity warnings.") + if ( + not isinstance(manifest["control_assumption"], str) + or not manifest["control_assumption"].strip() + ): + raise _error("run_manifest.json control_assumption must be nonblank.") + if ( + not isinstance(manifest["off_grid_control_exception"], list) + or not manifest["off_grid_control_exception"] + ): + raise _error("run_manifest.json must retain the off-grid control exception.") + for field in ( + "public_summary_archive_requested", + "private_evidence_archive_requested", + ): + if not isinstance(manifest[field], bool): + raise _error(f"run_manifest.json {field} must be boolean.") + output_directory = Path(str(manifest["output_directory"])) + if not output_directory.is_absolute(): + output_directory = repository_root / output_directory + output_directory = output_directory.resolve() + if ( + output_directory == ignored_output_root + or ignored_output_root not in output_directory.parents + ): + raise _error("run_manifest.json output_directory escapes the configured root.") + + +def _resolved_input_specs( + config: Mapping[str, Any], repository_root: Path +) -> tuple[dict[str, Any], ...]: + raw_base = config.get("base_step2b_config") + if not isinstance(raw_base, str) or not raw_base.strip(): + raise _error("resolved_debug_config.yaml must name base_step2b_config.") + base_path = Path(raw_base.strip()) + if not base_path.is_absolute(): + base_path = repository_root / base_path + base_path = base_path.resolve() + if not base_path.is_file() or repository_root not in base_path.parents: + raise _error("base_step2b_config must resolve to a repository file.") + base = _read_yaml_mapping(base_path, label="base_step2b_config") + inputs = base.get("inputs") + if not isinstance(inputs, list) or len(inputs) != len(_D2D_INPUT_COLUMNS): + raise _error("base_step2b_config must contain the ten D2D input grids.") + if ( + tuple(item.get("name") for item in inputs if isinstance(item, Mapping)) + != _D2D_INPUT_COLUMNS + ): + raise _error("base_step2b_config D2D input order changed.") + return tuple(dict(item) for item in inputs) + + +def _parse_finite_csv_number(value: str, *, field: str) -> float: + try: + number = float(value) + except (TypeError, ValueError) as exc: + raise _error(f"Consensus field {field} must be numeric.") from exc + if not math.isfinite(number): + raise _error(f"Consensus field {field} must be finite.") + return number + + +def _parse_csv_boolean(value: str, *, field: str) -> bool: + normalized = value.strip().casefold() + if normalized == "true": + return True + if normalized == "false": + return False + raise _error(f"Consensus field {field} must be true or false.") + + +def _read_csv_rows(path: Path, *, label: str) -> list[dict[str, str]]: + try: + with path.open("r", encoding="utf-8-sig", newline="") as handle: + reader = csv.DictReader(handle) + rows = list(reader) + except (OSError, UnicodeError, csv.Error) as exc: + raise _error(f"{label} must be readable valid CSV.") from exc + if any(None in row for row in rows): + raise _error(f"{label} contains a malformed CSV row.") + return rows + + +def _strict_integer(value: Any, *, field: str, minimum: int = 0) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise _error(f"{field} must be an integer >= {minimum}.") + return value + + +def _strict_number(value: Any, *, field: str, minimum: float = 0.0) -> float: + if not _is_finite_number(value) or float(value) < minimum: + raise _error(f"{field} must be finite and >= {minimum}.") + return float(value) + + +def _strict_integer_list(value: Any, *, field: str) -> tuple[int, ...]: + if not isinstance(value, list) or not value: + raise _error(f"{field} must be a non-empty integer list.") + return tuple( + _strict_integer(item, field=f"{field}[{index}]", minimum=1) + for index, item in enumerate(value) + ) + + +def _config_mapping( + value: Mapping[str, Any], key: str, *, field: str +) -> Mapping[str, Any]: + nested = value.get(key) + if not isinstance(nested, Mapping): + raise _error(f"resolved_debug_config.yaml {field} must be a mapping.") + return nested + + +def _values_match(actual: Any, expected: Any) -> bool: + if isinstance(expected, bool): + return actual is expected + if isinstance(expected, int): + return ( + not isinstance(actual, bool) + and isinstance(actual, int) + and actual == expected + ) + return _is_finite_number(actual) and math.isclose( + float(actual), float(expected), rel_tol=0.0, abs_tol=1.0e-12 + ) + + +def _validate_consensus_evidence( + output_dir: Path, + *, + manifest: Mapping[str, Any], + config: Mapping[str, Any], +) -> tuple[tuple[int, ...], ...]: + """Recompute every consensus gate from resolved config and bundled tables.""" + + regions_config = _config_mapping(config, "robust_regions", field="robust_regions") + required_config = _config_mapping( + regions_config, + "consensus_required_criteria", + field="robust_regions.consensus_required_criteria", + ) + search_config = _config_mapping( + config, "candidate_search", field="candidate_search" + ) + influence_config = _config_mapping( + config, "observation_influence", field="observation_influence" + ) + modes_config = _config_mapping(config, "execution_modes", field="execution_modes") + mode = str(manifest["mode"]) + mode_config = _config_mapping(modes_config, mode, field=f"execution_modes.{mode}") + + full_sizes = _strict_integer_list( + search_config.get("nested_unique_sizes"), + field="resolved_debug_config.yaml candidate_search.nested_unique_sizes", + ) + full_omissions = _strict_integer_list( + influence_config.get("omitted_sample_ids"), + field="resolved_debug_config.yaml observation_influence.omitted_sample_ids", + ) + mode_sizes = _strict_integer_list( + mode_config.get("nested_unique_sizes"), + field=f"resolved_debug_config.yaml execution_modes.{mode}.nested_unique_sizes", + ) + mode_omissions = _strict_integer_list( + mode_config.get("omitted_sample_ids"), + field=f"resolved_debug_config.yaml execution_modes.{mode}.omitted_sample_ids", + ) + if tuple(manifest["nested_pool_sizes"]) != mode_sizes: + raise _error( + "run_manifest.json nested_pool_sizes do not match the resolved execution " + "mode." + ) + if tuple(manifest["influence_omitted_sample_ids"]) != mode_omissions: + raise _error( + "run_manifest.json influence omissions do not match the resolved " + "execution mode." + ) + + batch_size = _strict_integer( + regions_config.get("consensus_batch_size"), + field="resolved_debug_config.yaml robust_regions.consensus_batch_size", + minimum=1, + ) + region_threshold = _strict_number( + regions_config.get("primary_distance_threshold"), + field="resolved_debug_config.yaml robust_regions.primary_distance_threshold", + ) + if regions_config.get("clustering") != manifest["robust_region_clustering"]: + raise _error( + "run_manifest.json robust-region clustering does not match the resolved " + "debug config." + ) + if not _values_match(manifest["robust_region_threshold"], region_threshold): + raise _error( + "run_manifest.json robust-region threshold does not match the resolved " + "debug config." + ) + nested_match_minimum = _strict_integer( + required_config.get("largest_two_nested_regional_matches_within_0_15"), + field=( + "resolved_debug_config.yaml consensus largest-two regional-match minimum" + ), + minimum=1, + ) + mean_distance_maximum = _strict_number( + required_config.get("largest_two_nested_mean_matched_distance_max"), + field=( + "resolved_debug_config.yaml consensus largest-two mean-distance maximum" + ), + ) + family_minimum = _strict_integer( + required_config.get("minimum_region_family_coverage"), + field="resolved_debug_config.yaml consensus family-coverage minimum", + minimum=1, + ) + for field in ("require_grid_valid", "require_hard_distance_valid"): + if required_config.get(field) is not True: + raise _error( + f"resolved_debug_config.yaml consensus criterion {field} must be true." + ) + + criteria = manifest["stability_criteria"] + expected_criteria: dict[str, int | float | bool] = { + "consensus_batch_size": batch_size, + "regional_match_threshold": region_threshold, + "largest_two_nested_regional_match_minimum": nested_match_minimum, + "largest_two_nested_mean_matched_distance_maximum": (mean_distance_maximum), + "minimum_nonbaseline_core_family_coverage": family_minimum, + "required_pairwise_minimum_distance": region_threshold, + "require_finite_and_bounded": True, + "require_unique_and_on_grid": True, + "require_debug_only_and_approval_false": True, + } + for field, expected in expected_criteria.items(): + if not _values_match(criteria.get(field), expected): + raise _error( + f"run_manifest.json stability criterion {field} does not match the " + "resolved debug config." + ) + + full_mode_eligible = bool( + mode == "full" and mode_sizes == full_sizes and mode_omissions == full_omissions + ) + largest_size, second_largest_size = mode_sizes[-1], mode_sizes[-2] + convergence_rows = _read_csv_rows( + output_dir / "nested_pool_convergence_summary.csv", + label="nested_pool_convergence_summary.csv", + ) + largest_two_rows = [] + for row_number, row in enumerate(convergence_rows, start=2): + reference = _parse_finite_csv_number( + row["reference_pool_size"], + field=f"nested convergence row {row_number} reference_pool_size", + ) + comparison = _parse_finite_csv_number( + row["comparison_pool_size"], + field=f"nested convergence row {row_number} comparison_pool_size", + ) + if ( + row.get("comparison_type") == "prefix_vs_largest" + and reference == largest_size + and comparison == second_largest_size + ): + largest_two_rows.append((row_number, row)) + if len(largest_two_rows) != 1: + raise _error( + "nested_pool_convergence_summary.csv must contain exactly one largest-two " + "pool comparison row." + ) + row_number, largest_two = largest_two_rows[0] + largest_two_matches = _parse_finite_csv_number( + largest_two["regional_matches_within_0.15"], + field=f"nested convergence row {row_number} regional matches", + ) + if not largest_two_matches.is_integer(): + raise _error("Largest-two regional-match count must be an integer.") + regional_match_count = int(largest_two_matches) + mean_matched_distance = _parse_finite_csv_number( + largest_two["mean_matched_distance"], + field=f"nested convergence row {row_number} mean_matched_distance", + ) + + region_rows = _read_csv_rows( + output_dir / "robust_regions.csv", label="robust_regions.csv" + ) + if manifest["robust_region_count"] != len(region_rows): + raise _error( + "run_manifest.json robust_region_count disagrees with robust_regions.csv." + ) + qualifying_region_count = 0 + region_evidence: dict[ + str, + tuple[ + int, + bool, + bool, + tuple[int, ...], + tuple[float, ...], + ], + ] = {} + for row_number, row in enumerate(region_rows, start=2): + region_id = row["region_id"].strip() + if not region_id or region_id in region_evidence: + raise _error( + "robust_regions.csv region_id values must be unique and nonblank." + ) + family_count = _parse_finite_csv_number( + row["distinct_nonbaseline_family_count"], + field=f"robust region row {row_number} family count", + ) + if not family_count.is_integer(): + raise _error("Robust-region family counts must be integers.") + qualifying_region_count += int(family_count) >= family_minimum + grid_valid = _parse_csv_boolean( + row["all_grid_valid"], + field=f"robust region row {row_number} all_grid_valid", + ) + hard_valid = _parse_csv_boolean( + row["all_hard_distance_valid"], + field=f"robust region row {row_number} all_hard_distance_valid", + ) + grid_values = [] + for column in _MEDOID_GRID_COLUMNS: + value = _parse_finite_csv_number( + row[column], field=f"robust region row {row_number} {column}" + ) + if not value.is_integer(): + raise _error("Robust-region medoid grid indices must be integers.") + grid_values.append(int(value)) + normalized = tuple( + _parse_finite_csv_number( + row[column], field=f"robust region row {row_number} {column}" + ) + for column in _MEDOID_NORM_COLUMNS + ) + if any(value < 0.0 or value > 1.0 for value in normalized): + raise _error("Robust-region medoid coordinates must lie in [0, 1].") + region_evidence[region_id] = ( + int(family_count), + grid_valid, + hard_valid, + tuple(grid_values), + normalized, + ) + + shortlist_rows = _read_csv_rows( + output_dir / "r1_robust_shortlist_debug.csv", + label="r1_robust_shortlist_debug.csv", + ) + if manifest["shortlist_count"] != len(shortlist_rows): + raise _error( + "run_manifest.json shortlist_count disagrees with " + "r1_robust_shortlist_debug.csv." + ) + eligible_rows: list[tuple[int, dict[str, str]]] = [] + seen_shortlist_regions: set[str] = set() + for row_number, row in enumerate(shortlist_rows, start=2): + region_id = row["region_id"].strip() + if not region_id or region_id in seen_shortlist_regions: + raise _error("Shortlist region_id values must be unique and nonblank.") + seen_shortlist_regions.add(region_id) + source_evidence = region_evidence.get(region_id) + if source_evidence is None: + raise _error("Every shortlist region_id must reference robust_regions.csv.") + family_count = _parse_finite_csv_number( + row["distinct_nonbaseline_family_count"], + field=f"shortlist row {row_number} family count", + ) + if not family_count.is_integer(): + raise _error("Shortlist family counts must be integers.") + grid_valid = _parse_csv_boolean( + row["all_grid_valid"], + field=f"shortlist row {row_number} all_grid_valid", + ) + hard_valid = _parse_csv_boolean( + row["all_hard_distance_valid"], + field=f"shortlist row {row_number} all_hard_distance_valid", + ) + shortlist_grid = [] + for column in _MEDOID_GRID_COLUMNS: + value = _parse_finite_csv_number( + row[column], field=f"shortlist row {row_number} {column}" + ) + if not value.is_integer(): + raise _error("Shortlist medoid grid indices must be integers.") + shortlist_grid.append(int(value)) + shortlist_norm = tuple( + _parse_finite_csv_number( + row[column], field=f"shortlist row {row_number} {column}" + ) + for column in _MEDOID_NORM_COLUMNS + ) + ( + source_family_count, + source_grid_valid, + source_hard_valid, + source_grid, + source_norm, + ) = source_evidence + if ( + int(family_count) != source_family_count + or grid_valid is not source_grid_valid + or hard_valid is not source_hard_valid + or tuple(shortlist_grid) != source_grid + or any( + not math.isclose(left, right, rel_tol=0.0, abs_tol=1.0e-12) + for left, right in zip(shortlist_norm, source_norm) + ) + ): + raise _error("Shortlist region evidence does not match robust_regions.csv.") + if int(family_count) >= family_minimum and grid_valid and hard_valid: + eligible_rows.append((row_number, row)) + chosen_rows = eligible_rows[:batch_size] + exact_count = len(chosen_rows) == batch_size + coordinates: list[tuple[float, ...]] = [] + grids: list[tuple[int, ...]] = [] + chosen_grid_valid = True + chosen_hard_valid = True + chosen_debug = True + chosen_experiment_false = True + chosen_production_false = True + for row_number, row in chosen_rows: + normalized = tuple( + _parse_finite_csv_number( + row[column], field=f"shortlist row {row_number} {column}" + ) + for column in _MEDOID_NORM_COLUMNS + ) + grid_values = [] + for column in _MEDOID_GRID_COLUMNS: + value = _parse_finite_csv_number( + row[column], field=f"shortlist row {row_number} {column}" + ) + if not value.is_integer(): + raise _error("Shortlist grid indices must be integers.") + grid_values.append(int(value)) + coordinates.append(normalized) + grids.append(tuple(grid_values)) + chosen_grid_valid = chosen_grid_valid and _parse_csv_boolean( + row["all_grid_valid"], + field=f"shortlist row {row_number} all_grid_valid", + ) + chosen_hard_valid = chosen_hard_valid and _parse_csv_boolean( + row["all_hard_distance_valid"], + field=f"shortlist row {row_number} all_hard_distance_valid", + ) + chosen_debug = chosen_debug and _parse_csv_boolean( + row["debug_only"], field=f"shortlist row {row_number} debug_only" + ) + chosen_experiment_false = chosen_experiment_false and not _parse_csv_boolean( + row["approved_for_experiment"], + field=f"shortlist row {row_number} approved_for_experiment", + ) + chosen_production_false = chosen_production_false and not _parse_csv_boolean( + row["approved_for_production"], + field=f"shortlist row {row_number} approved_for_production", + ) + + finite_and_bounded = bool( + exact_count + and all( + len(row) == len(_MEDOID_NORM_COLUMNS) + and all(0.0 <= value <= 1.0 for value in row) + for row in coordinates + ) + ) + unique_and_on_grid = bool( + exact_count and chosen_grid_valid and len(set(grids)) == batch_size + ) + pairwise_minimum = 0.0 + if exact_count and len(coordinates) > 1: + pairwise_minimum = min( + math.dist(coordinates[left], coordinates[right]) + for left in range(len(coordinates)) + for right in range(left + 1, len(coordinates)) + ) + hard_distance_valid = bool( + exact_count + and chosen_hard_valid + and pairwise_minimum + 1.0e-12 >= region_threshold + ) + family_qualified = exact_count + + expected_checks = { + "full_mode_eligible_for_consensus": full_mode_eligible, + "largest_two_nested_regional_matches": ( + regional_match_count >= nested_match_minimum + ), + "largest_two_nested_mean_matched_distance": ( + mean_matched_distance <= mean_distance_maximum + ), + "five_regions_cover_three_core_families": ( + qualifying_region_count >= batch_size + ), + "exact_five_candidates": exact_count, + "chosen_regions_cover_three_core_families": family_qualified, + "finite_and_bounded": finite_and_bounded, + "unique_and_on_grid": unique_and_on_grid, + "chosen_pairwise_hard_distance_valid": hard_distance_valid, + "debug_only": bool(exact_count and chosen_debug), + "experimental_approval_false": bool(exact_count and chosen_experiment_false), + "production_approval_false": bool(exact_count and chosen_production_false), + } + checks = manifest["consensus_checks"] + if set(checks) != set(expected_checks): + raise _error( + "run_manifest.json consensus_checks do not contain the exact supported " + "gate set." + ) + for field, expected in expected_checks.items(): + if checks[field] is not expected: + raise _error( + f"run_manifest.json consensus check {field} disagrees with bundled " + "evidence." + ) + + expected_observed: dict[str, int | float | bool] = { + "largest_two_regional_matches_within_0_15": regional_match_count, + "largest_two_mean_matched_distance": mean_matched_distance, + "regions_with_minimum_family_coverage": qualifying_region_count, + "consensus_candidate_count": len(chosen_rows), + "chosen_regions_family_qualified": family_qualified, + "chosen_candidates_finite_and_bounded": finite_and_bounded, + "chosen_candidates_unique_and_on_grid": unique_and_on_grid, + "chosen_pairwise_minimum_distance": pairwise_minimum, + "required_pairwise_minimum_distance": region_threshold, + "chosen_hard_distance_valid": hard_distance_valid, + "full_mode_eligible_for_consensus": full_mode_eligible, + } + observed = manifest["consensus_observed"] + if set(observed) != set(expected_observed): + raise _error( + "run_manifest.json consensus_observed does not contain the exact " + "supported evidence set." + ) + for field, expected in expected_observed.items(): + if not _values_match(observed[field], expected): + raise _error( + f"run_manifest.json consensus observed value {field} disagrees with " + "bundled evidence." + ) + if manifest["consensus_passed"] is not all(expected_checks.values()): + raise _error( + "run_manifest.json consensus_passed disagrees with recomputed gates." + ) + return tuple(grids) + + +def _validate_consensus_batch( + path: Path, + *, + manifest: Mapping[str, Any], + config: Mapping[str, Any], + repository_root: Path, + expected_grid_tuples: tuple[tuple[int, ...], ...], +) -> None: + try: + with path.open("r", encoding="utf-8-sig", newline="") as handle: + reader = csv.DictReader(handle) + rows = list(reader) + header = tuple(reader.fieldnames or ()) + except (OSError, UnicodeError, csv.Error) as exc: + raise _error( + "r1_consensus_debug_batch.csv must be readable valid CSV." + ) from exc + required = { + "consensus_candidate_id", + "selection_order", + "distinct_nonbaseline_family_count", + "all_grid_valid", + "all_hard_distance_valid", + *_D2D_INPUT_COLUMNS, + *_MEDOID_GRID_COLUMNS, + *_MEDOID_NORM_COLUMNS, + *_CSV_STAMP_COLUMNS, + } + missing = sorted(required - set(header)) + if missing: + raise _error( + "r1_consensus_debug_batch.csv is missing safety/recipe columns: " + f"{missing}." + ) + if len(rows) != 5: + raise _error("r1_consensus_debug_batch.csv must contain exactly five rows.") + specs = _resolved_input_specs(config, repository_root) + grid_tuples: list[tuple[int, ...]] = [] + normalized_rows: list[tuple[float, ...]] = [] + orders: list[int] = [] + family_minimum = int( + manifest["stability_criteria"]["minimum_nonbaseline_core_family_coverage"] + ) + for row_number, row in enumerate(rows, start=2): + order_value = _parse_finite_csv_number( + row["selection_order"], field=f"row {row_number} selection_order" + ) + if not order_value.is_integer(): + raise _error("Consensus selection_order values must be integers.") + orders.append(int(order_value)) + if ( + row["all_grid_valid"].strip().casefold() != "true" + or row["all_hard_distance_valid"].strip().casefold() != "true" + ): + raise _error("Every consensus row must pass grid and hard-distance flags.") + family_count = _parse_finite_csv_number( + row["distinct_nonbaseline_family_count"], + field=f"row {row_number} family coverage", + ) + if family_count < family_minimum: + raise _error("Every consensus row must meet the family-coverage criterion.") + grid_values: list[int] = [] + norm_values: list[float] = [] + for dimension, (name, spec) in enumerate(zip(_D2D_INPUT_COLUMNS, specs)): + grid_value = _parse_finite_csv_number( + row[f"medoid_grid_{dimension}"], + field=f"row {row_number} medoid_grid_{dimension}", + ) + if not grid_value.is_integer(): + raise _error("Consensus grid indices must be integers.") + grid_index = int(grid_value) + start = float(spec["start"]) + stop = float(spec["stop"]) + step = float(spec["step"]) + maximum_index = int(round((stop - start) / step)) + if grid_index < 0 or grid_index > maximum_index: + raise _error("Consensus grid index lies outside an allowed D2D grid.") + physical = _parse_finite_csv_number( + row[name], field=f"row {row_number} {name}" + ) + expected_physical = start + grid_index * step + if not math.isclose( + physical, expected_physical, rel_tol=0.0, abs_tol=1.0e-10 + ): + raise _error( + "Consensus physical values do not match their grid indices." + ) + normalized = _parse_finite_csv_number( + row[f"medoid_norm_{dimension}"], + field=f"row {row_number} medoid_norm_{dimension}", + ) + expected_normalized = ( + 0.0 if stop == start else (physical - start) / (stop - start) + ) + if not 0.0 <= normalized <= 1.0 or not math.isclose( + normalized, expected_normalized, rel_tol=0.0, abs_tol=1.0e-12 + ): + raise _error( + "Consensus normalized values are invalid for the D2D grid." + ) + grid_values.append(grid_index) + norm_values.append(normalized) + grid_tuples.append(tuple(grid_values)) + normalized_rows.append(tuple(norm_values)) + if orders != [1, 2, 3, 4, 5]: + raise _error( + "Consensus rows and selection_order must be exactly ordered 1 through 5." + ) + if len(set(grid_tuples)) != 5: + raise _error("Consensus candidates must be unique exact grid recipes.") + if tuple(grid_tuples) != expected_grid_tuples: + raise _error( + "Consensus grid recipes do not match the eligible shortlist head in " + "selection order." + ) + minimum_required = float( + manifest["stability_criteria"]["required_pairwise_minimum_distance"] + ) + pairwise_minimum = math.inf + for left in range(5): + for right in range(left + 1, 5): + distance = math.sqrt( + sum( + ( + normalized_rows[left][dimension] + - normalized_rows[right][dimension] + ) + ** 2 + for dimension in range(len(_D2D_INPUT_COLUMNS)) + ) + ) + pairwise_minimum = min(pairwise_minimum, distance) + if pairwise_minimum + 1.0e-12 < minimum_required: + raise _error( + "Consensus candidates violate the required hard pairwise distance." + ) + checks = manifest["consensus_checks"] + if not checks or any(value is not True for value in checks.values()): + raise _error( + "A consensus artifact requires every manifest consensus check to pass." + ) + if manifest["consensus_observed"].get("consensus_candidate_count") != 5: + raise _error("Manifest consensus_candidate_count must equal five.") + + +def _validate_no_stable_reason( + payload: Mapping[str, Any], manifest: Mapping[str, Any] +) -> None: + _require_mapping_fields( + payload, + ("message", "checks", "failed_checks", "observed", "consensus_passed"), + label=NO_STABLE_BATCH_REASON_FILE, + ) + failed = payload["failed_checks"] + checks = payload["checks"] + expected_failed = [ + name for name, passed in manifest["consensus_checks"].items() if passed is False + ] + if ( + payload["consensus_passed"] is not False + or not isinstance(failed, list) + or not failed + or not isinstance(checks, Mapping) + or any(not isinstance(item, str) for item in failed) + or len(failed) != len(expected_failed) + or set(failed) != set(expected_failed) + ): + raise _error("The no-stable-batch reason must name every failed false check.") + if dict(checks) != dict(manifest["consensus_checks"]): + raise _error("No-stable-batch checks must match run_manifest.json.") + if ( + not isinstance(payload["observed"], Mapping) + or not isinstance(payload["message"], str) + or not payload["message"].strip() + ): + raise _error("The no-stable-batch reason needs observed values and a message.") + if dict(payload["observed"]) != dict(manifest["consensus_observed"]): + raise _error("No-stable-batch observed values must match run_manifest.json.") + + +def _validate_file_surface(files: tuple[Path, ...], output_dir: Path) -> None: + allowed_suffixes = {".csv", ".json", ".png", ".txt", ".yaml"} + for path in files: + relative = _relative(path, output_dir) + if path.suffix.casefold() not in allowed_suffixes: + raise _error( + f"Unsupported artifact format {relative}; recipe-bearing formats " + "outside stamped CSV/JSON/PNG are forbidden." + ) + if path.suffix.casefold() == ".txt": + if ( + relative != "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt" + or path.read_text(encoding="utf-8").strip() != DEBUG_WATERMARK + ): + raise _error( + "The only permitted text artifact is the exact debug marker." + ) + if ( + path.suffix.casefold() == ".yaml" + and relative != "resolved_debug_config.yaml" + ): + raise _error("Only resolved_debug_config.yaml is permitted in the bundle.") + + +def _all_regular_files(output_dir: Path) -> tuple[Path, ...]: + paths: list[Path] = [] + for path in output_dir.rglob("*"): + if path.is_symlink(): + raise _error( + f"Artifact bundles may not contain symbolic links: " + f"{_relative(path, output_dir)}." + ) + if path.is_file(): + resolved = path.resolve() + if output_dir not in resolved.parents: + raise _error( + f"Artifact resolves outside the output bundle: " + f"{_relative(path, output_dir)}." + ) + paths.append(path) + return tuple(sorted(paths, key=lambda item: _relative(item, output_dir))) + + +def _validate_replicate_worklists( + files: tuple[Path, ...], output_dir: Path, *, consensus_exists: bool +) -> None: + for path in files: + lowered = path.name.casefold() + if "replicate" not in lowered and "worklist" not in lowered: + continue + relative = _relative(path, output_dir) + if not consensus_exists: + raise _error( + f"Replicate worklist artifact {relative} is forbidden without " + "r1_consensus_debug_batch.csv." + ) + if "debug_preview" not in lowered and "debug-preview" not in lowered: + raise _error( + f"Replicate worklist artifact {relative} must be explicitly named " + "as a debug preview." + ) + + +def validate_step2c_artifact_bundle( + output_dir: str | Path, + *, + repository_root: str | Path, +) -> Step2CArtifactValidation: + """Validate one completed local Step 2C output directory without mutation. + + The trusted ``repository_root`` anchors the relative ``outputs.root`` in the + resolved configuration and any relative source-workbook path in the run + manifest. The function performs only reads and returns a content-hash map for + every artifact after all contract checks pass. + """ + + repository = Path(repository_root).resolve() + destination = Path(output_dir).resolve() + if not repository.is_dir(): + raise _error(f"repository_root must be an existing directory: {repository}.") + if not destination.is_dir(): + raise _error(f"output_dir must be an existing directory: {destination}.") + + config_path = destination / "resolved_debug_config.yaml" + if not config_path.is_file(): + raise _error("Missing required top-level artifact: resolved_debug_config.yaml.") + config = _read_yaml_mapping(config_path, label="resolved_debug_config.yaml") + _validate_config_stamps(config) + ignored_root = _configured_output_root(config, repository) + if destination == ignored_root or ignored_root not in destination.parents: + raise _error( + f"output_dir must be a strict descendant of configured ignored root " + f"{ignored_root}." + ) + if (repository / ".git").exists(): + try: + ignored = subprocess.run( + ["git", "check-ignore", "-q", str(destination)], + cwd=repository, + check=False, + timeout=15, + ) + except (OSError, subprocess.SubprocessError) as exc: + raise _error( + "Unable to verify that the Step 2C output is Git-ignored." + ) from exc + if ignored.returncode != 0: + raise _error("The configured Step 2C output directory must be Git-ignored.") + + missing_top_level = [ + name for name in REQUIRED_TOP_LEVEL_FILES if not (destination / name).is_file() + ] + if missing_top_level: + raise _error( + "Missing required top-level Step 2C artifact(s): " f"{missing_top_level}." + ) + + consensus_path = destination / CONSENSUS_BATCH_FILE + no_stable_path = destination / NO_STABLE_BATCH_REASON_FILE + consensus_exists = consensus_path.is_file() + no_stable_exists = no_stable_path.is_file() + if consensus_exists == no_stable_exists: + raise _error( + "Exactly one conditional artifact is required: " + f"{CONSENSUS_BATCH_FILE} or {NO_STABLE_BATCH_REASON_FILE}." + ) + conditional_artifact = ( + CONSENSUS_BATCH_FILE if consensus_exists else NO_STABLE_BATCH_REASON_FILE + ) + + for directory in REQUIRED_PLOT_DIRECTORIES: + path = destination / directory + if not path.is_dir() or path.is_symlink(): + raise _error(f"Missing required plot-topic directory: {directory}.") + missing_plots = [ + name for name in REQUIRED_PLOT_FILES if not (destination / name).is_file() + ] + if missing_plots: + raise _error(f"Missing required plot-topic artifact(s): {missing_plots}.") + + files = _all_regular_files(destination) + _validate_file_surface(files, destination) + _validate_replicate_worklists(files, destination, consensus_exists=consensus_exists) + csv_files = tuple( + _relative(path, destination) + for path in files + if path.suffix.casefold() == ".csv" + ) + json_files = tuple( + _relative(path, destination) + for path in files + if path.suffix.casefold() == ".json" + ) + png_files = tuple( + _relative(path, destination) + for path in files + if path.suffix.casefold() == ".png" + ) + for relative in csv_files: + header, row_count = _validate_csv_stamp(destination / relative, destination) + _validate_required_csv_schema(relative, header, row_count) + json_payloads: dict[str, dict[str, Any]] = {} + for relative in json_files: + json_payloads[relative] = _read_and_validate_json_stamp( + destination / relative, destination + ) + for relative in png_files: + _validate_png_watermark(destination / relative, destination) + + manifest = json_payloads.get("run_manifest.json") + if manifest is None: + raise _error("run_manifest.json was not validated as a top-level JSON object.") + _validate_manifest_provenance(manifest, repository, ignored_root, config) + if consensus_exists: + if manifest.get("consensus_passed") is not True: + raise _error( + "r1_consensus_debug_batch.csv requires consensus_passed=true in " + "run_manifest.json." + ) + if manifest.get("mode") != "full": + raise _error( + "r1_consensus_debug_batch.csv is forbidden outside a full run." + ) + else: + if manifest.get("consensus_passed") is not False: + raise _error( + "r1_no_stable_batch_reason.json requires consensus_passed=false in " + "run_manifest.json." + ) + expected_consensus_grids = _validate_consensus_evidence( + destination, manifest=manifest, config=config + ) + if consensus_exists: + _validate_consensus_batch( + consensus_path, + manifest=manifest, + config=config, + repository_root=repository, + expected_grid_tuples=expected_consensus_grids, + ) + else: + reason_payload = json_payloads.get(NO_STABLE_BATCH_REASON_FILE) + if reason_payload is None: + raise _error("The no-stable-batch reason was not validated as JSON.") + _validate_no_stable_reason(reason_payload, manifest) + workbook_path, workbook_hash, workbook_mtime = _validate_workbook_proof( + manifest, repository, destination + ) + + artifact_sha256 = { + _relative(path, destination): _sha256_file(path) for path in files + } + return Step2CArtifactValidation( + output_dir=destination, + ignored_output_root=ignored_root, + conditional_artifact=conditional_artifact, + workbook_path=workbook_path, + workbook_sha256=workbook_hash, + workbook_mtime_ns=workbook_mtime, + csv_files_checked=csv_files, + json_files_checked=json_files, + png_files_checked=png_files, + artifact_sha256=artifact_sha256, + ) + + +__all__ = [ + "CONSENSUS_BATCH_FILE", + "DEBUG_WATERMARK", + "NO_STABLE_BATCH_REASON_FILE", + "PUBLIC_SUMMARY_ARCHIVE_ROOT", + "PUBLIC_SUMMARY_CSV_FILES", + "PUBLIC_SUMMARY_DEBUG_MARKER_FILE", + "PUBLIC_SUMMARY_FILES", + "PUBLIC_SUMMARY_MANIFEST_FILE", + "PUBLIC_SUMMARY_README_FILE", + "PUBLIC_SUMMARY_SCHEMA_VERSION", + "REQUIRED_CSV_COLUMNS", + "REQUIRED_PLOT_DIRECTORIES", + "REQUIRED_PLOT_FILES", + "REQUIRED_TOP_LEVEL_FILES", + "Step2CArtifactContractError", + "Step2CArtifactValidation", + "Step2CPublicSummaryValidation", + "validate_step2c_artifact_bundle", + "validate_step2c_public_summary_archive", +] diff --git a/src/mobo_kit/ucb_hvi.py b/src/mobo_kit/ucb_hvi.py new file mode 100644 index 0000000..be7ecf3 --- /dev/null +++ b/src/mobo_kit/ucb_hvi.py @@ -0,0 +1,860 @@ +"""Discrete multi-objective UCB-HVI scoring in transformed utility space. + +The module deliberately separates posterior sampling from deterministic +hypervolume arithmetic. Nonlinear objective transforms are applied to every +posterior Monte Carlo sample before utility moments are formed. All objective +dimensions are maximized after transformation, and the reference point is +always supplied explicitly by the caller. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from math import sqrt +from numbers import Real +from typing import Any, Callable, Literal, Sequence + +import numpy as np +import torch +from botorch.sampling.normal import SobolQMCNormalSampler +from botorch.utils.multi_objective.hypervolume import Hypervolume +from botorch.utils.multi_objective.pareto import is_non_dominated + + +TensorTransform = Callable[[torch.Tensor], torch.Tensor] +MomentMethod = Literal["monte_carlo", "analytic_identity"] +UCBBoundPolicy = Literal["none", "clip_ucb"] +UtilityBound = tuple[float | None, float | None] + + +@dataclass(frozen=True) +class PosteriorIdentityMoments: + """Exact posterior moments for an all-identity maximize contract.""" + + utility_mean: torch.Tensor + utility_std: torch.Tensor + observation_noise: bool + objective_contract_version: str + moment_method: str = "analytic_identity" + + +@dataclass(frozen=True) +class BoundedUCBResult: + """Raw and policy-effective UCB vectors plus non-negative clip amounts.""" + + utility_ucb_raw: np.ndarray + utility_ucb_effective: np.ndarray + utility_ucb_clip_amount: np.ndarray + policy: str + bounds: tuple[UtilityBound, ...] + + +@dataclass(frozen=True) +class PosteriorUtilityMoments: + """Monte Carlo moments in all-maximize transformed utility space.""" + + utility_mean: np.ndarray + utility_std: np.ndarray + mc_samples: int + seed: int + observation_noise: bool + standard_deviation_correction: int = 0 + moment_method: str = "monte_carlo" + + +@dataclass(frozen=True) +class UCBHVIScoreResult: + """Candidate-wise optimistic utilities and hypervolume improvements.""" + + base_score: np.ndarray + base_log_score: np.ndarray + utility_mean: np.ndarray + utility_std: np.ndarray + utility_ucb: np.ndarray + baseline_hypervolume: float + pareto_utility: np.ndarray + reference_point_utility: np.ndarray + beta: float + kappa: float + mc_samples: int | None + seed: int | None + observation_noise: bool + objective_contract_version: str + moment_method: str = "monte_carlo" + bound_policy: str = "none" + utility_bounds: tuple[UtilityBound, ...] | None = None + utility_ucb_raw: np.ndarray | None = None + utility_ucb_effective: np.ndarray | None = None + utility_ucb_clip_amount: np.ndarray | None = None + method: str = "ucb_hvi" + method_version: str = "step2a-v1" + + def __post_init__(self) -> None: + effective = ( + np.asarray(self.utility_ucb, dtype=float) + if self.utility_ucb_effective is None + else np.asarray(self.utility_ucb_effective, dtype=float) + ) + raw = ( + effective.copy() + if self.utility_ucb_raw is None + else np.asarray(self.utility_ucb_raw, dtype=float) + ) + clip_amount = ( + np.abs(raw - effective) + if self.utility_ucb_clip_amount is None + else np.asarray(self.utility_ucb_clip_amount, dtype=float) + ) + expected_shape = np.asarray(self.utility_mean).shape + if ( + effective.shape != expected_shape + or raw.shape != expected_shape + or clip_amount.shape != expected_shape + ): + raise ValueError( + "Raw/effective UCB and clip amounts must align with utility_mean." + ) + object.__setattr__(self, "utility_ucb", effective) + object.__setattr__(self, "utility_ucb_raw", raw) + object.__setattr__(self, "utility_ucb_effective", effective) + object.__setattr__(self, "utility_ucb_clip_amount", clip_amount) + + +@dataclass(frozen=True) +class UCBHVIBatchProposal: + """A locally penalized batch plus its complete static UCB-HVI score table.""" + + selection: Any + scoring: UCBHVIScoreResult + positive_score_tolerance: float + metadata: dict[str, Any] + + +def _finite_nonnegative(value: float, *, name: str) -> float: + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise ValueError(f"{name} must be a real non-boolean number; got {value!r}.") + number = float(value) + if not np.isfinite(number) or number < 0: + raise ValueError(f"{name} must be finite and non-negative; got {value!r}.") + return number + + +def _positive_integer(value: int, *, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, (int, np.integer)): + raise ValueError(f"{name} must be a positive integer; got {value!r}.") + result = int(value) + if result <= 0: + raise ValueError(f"{name} must be a positive integer; got {value!r}.") + return result + + +def _apply_objective_transform(transform: Any, samples: torch.Tensor) -> torch.Tensor: + if hasattr(transform, "transform"): + utility = transform.transform(samples) + elif callable(transform): + utility = transform(samples) + else: + raise TypeError("objective_transform must be callable or expose transform().") + if not isinstance(utility, torch.Tensor): + raise TypeError("objective_transform must return a torch.Tensor.") + if utility.shape != samples.shape: + raise ValueError( + "objective_transform must preserve shape; " + f"got {tuple(samples.shape)} -> {tuple(utility.shape)}." + ) + if not torch.isfinite(utility).all(): + raise ValueError("objective_transform produced non-finite utility values.") + return utility + + +def _posterior(model: Any, X: torch.Tensor, observation_noise: bool) -> Any: + try: + return model.posterior(X, observation_noise=observation_noise) + except TypeError as exc: + raise TypeError( + "model.posterior must accept the explicit observation_noise keyword." + ) from exc + + +def _identity_objective_contract(objective_transform: Any) -> Any: + if hasattr(objective_transform, "bounds"): + raise ValueError( + "analytic identity moments cannot be used with a posterior-sample " + "bounds wrapper; apply an explicit UCB bound policy instead." + ) + contract = getattr(objective_transform, "objective_transform", objective_transform) + specs = getattr(contract, "specs", None) + if not specs: + raise ValueError( + "analytic identity moments require an explicit versioned objective " + "contract with objective specifications." + ) + if any( + getattr(spec, "transform", None) != "identity" + or getattr(spec, "goal", None) != "maximize" + or bool(getattr(spec, "clip", False)) + for spec in specs + ): + raise ValueError( + "analytic identity moments require every objective to use the " + "identity transform with maximize direction and no clipping." + ) + version = getattr(contract, "version", None) + if not isinstance(version, str) or not version.strip(): + raise ValueError( + "analytic identity moments require a non-empty objective contract version." + ) + return contract + + +def posterior_identity_moments( + model: Any, + X_pool_norm: torch.Tensor, + objective_transform: Any, + *, + chunk_size: int = 512, + observation_noise: bool = False, +) -> PosteriorIdentityMoments: + """Return exact posterior mean and standard deviation for identity utilities. + + Tensors remain on the posterior's device and retain its dtype and any model + batch dimensions. Candidate rows are concatenated along the posterior q + dimension, making the result invariant to the requested evaluation chunk. + """ + if not isinstance(X_pool_norm, torch.Tensor) or X_pool_norm.ndim != 2: + shape = getattr(X_pool_norm, "shape", None) + raise ValueError( + f"X_pool_norm must be a tensor with shape (N, D); got {shape}." + ) + if not X_pool_norm.is_floating_point(): + raise TypeError("X_pool_norm must use a floating dtype.") + if X_pool_norm.shape[0] == 0: + raise ValueError("X_pool_norm must contain at least one candidate.") + if not torch.isfinite(X_pool_norm).all(): + raise ValueError("X_pool_norm must contain only finite values.") + chunk = _positive_integer(chunk_size, name="chunk_size") + if not isinstance(observation_noise, bool): + raise ValueError("observation_noise must be a boolean.") + contract = _identity_objective_contract(objective_transform) + objective_count = len(contract.specs) + + mean_parts: list[torch.Tensor] = [] + std_parts: list[torch.Tensor] = [] + with torch.no_grad(): + for start in range(0, X_pool_norm.shape[0], chunk): + X_chunk = X_pool_norm[start : start + chunk] + posterior = _posterior(model, X_chunk, observation_noise) + mean = posterior.mean + variance = posterior.variance + if not isinstance(mean, torch.Tensor) or not isinstance( + variance, torch.Tensor + ): + raise TypeError("model.posterior mean and variance must be tensors.") + if mean.shape != variance.shape: + raise ValueError("Posterior mean and variance shapes must match.") + if mean.ndim < 2 or mean.shape[-2] != X_chunk.shape[0]: + raise ValueError( + "Posterior candidate dimension must align with the input chunk; " + f"got mean shape {tuple(mean.shape)} for {X_chunk.shape[0]} rows." + ) + if mean.shape[-1] != objective_count: + raise ValueError( + "Posterior objective dimension does not match the identity " + f"contract; expected {objective_count}, got {mean.shape[-1]}." + ) + if not mean.is_floating_point() or not variance.is_floating_point(): + raise TypeError("Posterior mean and variance must use floating dtypes.") + if mean.dtype != variance.dtype or mean.device != variance.device: + raise ValueError( + "Posterior mean and variance must share dtype and device." + ) + if not torch.isfinite(mean).all() or not torch.isfinite(variance).all(): + raise ValueError("Posterior mean and variance must be finite.") + tolerance = 100.0 * torch.finfo(variance.dtype).eps + if torch.any(variance < -tolerance): + raise ValueError("Posterior variance cannot be materially negative.") + mean_parts.append(mean) + std_parts.append(variance.clamp_min(0.0).sqrt()) + + try: + means = torch.cat(mean_parts, dim=-2) + standard_deviations = torch.cat(std_parts, dim=-2) + except RuntimeError as exc: + raise ValueError( + "Posterior batch shape, dtype, and device must remain stable across chunks." + ) from exc + return PosteriorIdentityMoments( + utility_mean=means, + utility_std=standard_deviations, + observation_noise=observation_noise, + objective_contract_version=contract.version.strip(), + ) + + +def posterior_utility_moments( + model: Any, + X_pool_norm: torch.Tensor, + objective_transform: TensorTransform | Any, + *, + mc_samples: int = 128, + seed: int = 0, + chunk_size: int = 512, + observation_noise: bool = False, +) -> PosteriorUtilityMoments: + """Estimate utility moments for a normalized ``(N, D)`` candidate pool. + + Candidates are represented as independent singleton q-batches ``N x 1 x D``. + A seeded Sobol sampler is rebuilt for each chunk. BoTorch collapses the + singleton batch dimensions in its base-sample shape, so every candidate sees + the same reproducible QMC normal draws and results are invariant to chunking. + Population standard deviation (``correction=0``) is reported. + """ + if not isinstance(X_pool_norm, torch.Tensor) or X_pool_norm.ndim != 2: + shape = getattr(X_pool_norm, "shape", None) + raise ValueError( + f"X_pool_norm must be a tensor with shape (N, D); got {shape}." + ) + if not X_pool_norm.is_floating_point(): + raise TypeError("X_pool_norm must use a floating dtype.") + if not torch.isfinite(X_pool_norm).all(): + raise ValueError("X_pool_norm must contain only finite values.") + sample_count = _positive_integer(mc_samples, name="mc_samples") + chunk = _positive_integer(chunk_size, name="chunk_size") + if ( + isinstance(seed, (bool, np.bool_)) + or not isinstance(seed, (int, np.integer)) + or int(seed) < 0 + ): + raise ValueError("seed must be a non-negative integer.") + if not isinstance(observation_noise, bool): + raise ValueError("observation_noise must be a boolean.") + + mean_parts: list[torch.Tensor] = [] + std_parts: list[torch.Tensor] = [] + with torch.no_grad(): + for start in range(0, X_pool_norm.shape[0], chunk): + X_chunk = X_pool_norm[start : start + chunk].unsqueeze(-2) + posterior = _posterior(model, X_chunk, observation_noise) + sampler = SobolQMCNormalSampler( + sample_shape=torch.Size([sample_count]), seed=int(seed) + ) + raw_samples = sampler(posterior) + utility_samples = _apply_objective_transform( + objective_transform, raw_samples + ) + if utility_samples.shape[-2] != 1: + raise ValueError( + "Singleton posterior evaluation must retain q=1 in the " + f"penultimate dimension; got {tuple(utility_samples.shape)}." + ) + utility_samples = utility_samples.squeeze(-2) + mean_parts.append(utility_samples.mean(dim=0)) + std_parts.append(utility_samples.std(dim=0, correction=0)) + + if mean_parts: + means = torch.cat(mean_parts, dim=0) + standard_deviations = torch.cat(std_parts, dim=0) + else: + # The output dimension cannot be discovered safely without a posterior. + raise ValueError("X_pool_norm must contain at least one candidate.") + return PosteriorUtilityMoments( + utility_mean=means.detach().cpu().double().numpy(), + utility_std=standard_deviations.detach().cpu().double().numpy(), + mc_samples=sample_count, + seed=int(seed), + observation_noise=bool(observation_noise), + ) + + +def _utility_matrix(value: np.ndarray | torch.Tensor, *, name: str) -> np.ndarray: + if isinstance(value, torch.Tensor): + array = value.detach().cpu().double().numpy() + else: + array = np.asarray(value, dtype=float) + if array.ndim != 2: + raise ValueError(f"{name} must have shape (N, M); got {array.shape}.") + if array.shape[1] == 0: + raise ValueError(f"{name} must include at least one objective.") + if not np.all(np.isfinite(array)): + raise ValueError(f"{name} must contain only finite values.") + return array + + +def _validated_utility_bounds( + bounds: Sequence[UtilityBound] | None, + objective_count: int, + *, + require_bounded: bool, +) -> tuple[UtilityBound, ...]: + if bounds is None: + if require_bounded: + raise ValueError("clip_ucb requires explicit per-objective utility bounds.") + return tuple((None, None) for _ in range(objective_count)) + if isinstance(bounds, (str, bytes)): + raise TypeError("bounds must be an ordered sequence of (lower, upper) pairs.") + try: + raw_bounds = tuple(bounds) + except TypeError as exc: + raise TypeError( + "bounds must be an ordered sequence of (lower, upper) pairs." + ) from exc + if len(raw_bounds) != objective_count: + raise ValueError( + "bounds must contain one (lower, upper) pair per objective; " + f"expected {objective_count}, got {len(raw_bounds)}." + ) + validated: list[UtilityBound] = [] + bounded_count = 0 + for index, raw_bound in enumerate(raw_bounds): + if isinstance(raw_bound, (str, bytes)): + raise TypeError(f"bounds[{index}] must be a (lower, upper) pair.") + try: + pair = tuple(raw_bound) + except TypeError as exc: + raise TypeError(f"bounds[{index}] must be a (lower, upper) pair.") from exc + if len(pair) != 2: + raise ValueError(f"bounds[{index}] must contain exactly two values.") + converted: list[float | None] = [] + for label, value in zip(("lower", "upper"), pair): + if value is None: + converted.append(None) + continue + if isinstance(value, (bool, np.bool_)) or not isinstance(value, Real): + raise ValueError( + f"bounds[{index}] {label} must be finite, real, and non-boolean." + ) + number = float(value) + if not np.isfinite(number): + raise ValueError(f"bounds[{index}] {label} must be finite.") + converted.append(number) + lower, upper = converted + if lower is not None and upper is not None and lower > upper: + raise ValueError( + f"bounds[{index}] lower value must not exceed its upper value." + ) + if lower is not None or upper is not None: + bounded_count += 1 + validated.append((lower, upper)) + if require_bounded and bounded_count == 0: + raise ValueError("clip_ucb requires at least one finite utility bound.") + return tuple(validated) + + +def apply_ucb_bound_policy( + raw_ucb: np.ndarray | torch.Tensor, + bounds: Sequence[UtilityBound] | None, + policy: UCBBoundPolicy | str, +) -> BoundedUCBResult: + """Apply an explicit optimistic-utility policy without changing targets.""" + raw = _utility_matrix(raw_ucb, name="raw_ucb").copy() + if policy not in {"none", "clip_ucb"}: + raise ValueError("policy must be exactly 'none' or 'clip_ucb'.") + validated = _validated_utility_bounds( + bounds, raw.shape[1], require_bounded=policy == "clip_ucb" + ) + effective = raw.copy() + if policy == "clip_ucb": + for index, (lower, upper) in enumerate(validated): + effective[:, index] = np.clip( + effective[:, index], + -np.inf if lower is None else lower, + np.inf if upper is None else upper, + ) + return BoundedUCBResult( + utility_ucb_raw=raw, + utility_ucb_effective=effective, + utility_ucb_clip_amount=np.abs(raw - effective), + policy=str(policy), + bounds=validated, + ) + + +def _reference_point( + value: np.ndarray | torch.Tensor | None, objective_count: int +) -> np.ndarray: + if value is None: + raise ValueError( + "reference_point_utility is required and is never derived from data." + ) + if isinstance(value, torch.Tensor): + reference = value.detach().cpu().double().numpy() + else: + reference = np.asarray(value, dtype=float) + if reference.shape != (objective_count,): + raise ValueError( + "reference_point_utility must have shape " + f"({objective_count},); got {reference.shape}." + ) + if not np.all(np.isfinite(reference)): + raise ValueError("reference_point_utility must contain only finite values.") + return reference + + +def pareto_utility_above_reference( + observed_utility: np.ndarray | torch.Tensor, + reference_point_utility: np.ndarray | torch.Tensor, +) -> np.ndarray: + """Return finite non-dominated utilities that strictly dominate the reference.""" + observed = _utility_matrix(observed_utility, name="observed_utility") + reference = _reference_point(reference_point_utility, observed.shape[1]) + contributing = observed[np.all(observed > reference, axis=1)] + if contributing.shape[0] == 0: + return np.empty((0, observed.shape[1]), dtype=float) + tensor = torch.as_tensor(contributing, dtype=torch.double) + return tensor[is_non_dominated(tensor)].numpy() + + +def hypervolume_improvement_scores( + optimistic_utility: np.ndarray | torch.Tensor, + observed_utility: np.ndarray | torch.Tensor, + reference_point_utility: np.ndarray | torch.Tensor | None, + *, + numeric_tolerance: float = 1e-12, + chunk_size: int = 1024, +) -> tuple[np.ndarray, float, np.ndarray, np.ndarray]: + """Compute exact singleton HVI for optimistic all-maximize utility vectors.""" + candidates = _utility_matrix(optimistic_utility, name="optimistic_utility") + observed = _utility_matrix(observed_utility, name="observed_utility") + if candidates.shape[1] != observed.shape[1]: + raise ValueError("Candidate and observed utility dimensions must match.") + reference = _reference_point(reference_point_utility, observed.shape[1]) + tolerance = _finite_nonnegative(numeric_tolerance, name="numeric_tolerance") + chunk = _positive_integer(chunk_size, name="chunk_size") + pareto = pareto_utility_above_reference(observed, reference) + ref_tensor = torch.as_tensor(reference, dtype=torch.double) + hypervolume = Hypervolume(ref_point=ref_tensor) + baseline = ( + 0.0 + if pareto.shape[0] == 0 + else float(hypervolume.compute(torch.as_tensor(pareto, dtype=torch.double))) + ) + + scores = np.zeros(candidates.shape[0], dtype=float) + for start in range(0, candidates.shape[0], chunk): + stop = min(start + chunk, candidates.shape[0]) + for index in range(start, stop): + candidate = candidates[index] + if not np.all(candidate > reference): + continue + if pareto.shape[0] and np.any( + np.all(pareto >= candidate - tolerance, axis=1) + ): + continue + augmented = np.vstack([pareto, candidate[None, :]]) + augmented_tensor = torch.as_tensor(augmented, dtype=torch.double) + augmented_pareto = augmented_tensor[is_non_dominated(augmented_tensor)] + improvement = float(hypervolume.compute(augmented_pareto)) - baseline + if improvement < -tolerance: + raise RuntimeError( + "Hypervolume improvement was negative beyond numeric " + f"tolerance: candidate_index={index}, improvement={improvement}, " + f"tolerance={tolerance}." + ) + if improvement > tolerance: + scores[index] = improvement + return scores, baseline, pareto, reference + + +def score_ucb_hvi_from_moments( + utility_mean: np.ndarray, + utility_std: np.ndarray, + observed_utility: np.ndarray, + reference_point_utility: np.ndarray | None, + *, + beta: float, + numeric_tolerance: float = 1e-12, + chunk_size: int = 1024, + log_epsilon: float = 1e-12, + mc_samples: int | None = None, + seed: int | None = None, + observation_noise: bool = False, + objective_contract_version: str = "direct-moments", + moment_method: str = "direct_moments", + bound_policy: UCBBoundPolicy | str = "none", + utility_bounds: Sequence[UtilityBound] | None = None, +) -> UCBHVIScoreResult: + """Form utility UCB vectors and score their singleton hypervolume gain.""" + means = _utility_matrix(utility_mean, name="utility_mean") + standard_deviations = _utility_matrix(utility_std, name="utility_std") + if standard_deviations.shape != means.shape: + raise ValueError("utility_std must have the same shape as utility_mean.") + if np.any(standard_deviations < 0): + raise ValueError("utility_std cannot contain negative values.") + beta_value = _finite_nonnegative(beta, name="beta") + if isinstance(log_epsilon, (bool, np.bool_)): + raise ValueError("log_epsilon must be a real non-boolean number.") + epsilon = float(log_epsilon) + if not np.isfinite(epsilon) or epsilon <= 0: + raise ValueError("log_epsilon must be finite and strictly positive.") + if ( + not isinstance(objective_contract_version, str) + or not objective_contract_version.strip() + ): + raise ValueError("objective_contract_version must be a non-empty string.") + if not isinstance(moment_method, str) or not moment_method.strip(): + raise ValueError("moment_method must be a non-empty string.") + kappa = sqrt(beta_value) + optimistic_raw = means + kappa * standard_deviations + bounded = apply_ucb_bound_policy(optimistic_raw, utility_bounds, bound_policy) + scores, baseline, pareto, reference = hypervolume_improvement_scores( + bounded.utility_ucb_effective, + observed_utility, + reference_point_utility, + numeric_tolerance=numeric_tolerance, + chunk_size=chunk_size, + ) + return UCBHVIScoreResult( + base_score=scores, + base_log_score=np.log(np.maximum(scores, epsilon)), + utility_mean=means, + utility_std=standard_deviations, + utility_ucb=bounded.utility_ucb_effective, + baseline_hypervolume=baseline, + pareto_utility=pareto, + reference_point_utility=reference, + beta=beta_value, + kappa=kappa, + mc_samples=mc_samples, + seed=seed, + observation_noise=bool(observation_noise), + objective_contract_version=objective_contract_version.strip(), + moment_method=moment_method.strip(), + bound_policy=bounded.policy, + utility_bounds=bounded.bounds, + utility_ucb_raw=bounded.utility_ucb_raw, + utility_ucb_effective=bounded.utility_ucb_effective, + utility_ucb_clip_amount=bounded.utility_ucb_clip_amount, + ) + + +def score_ucb_hvi_pool( + model: Any, + X_pool_norm: torch.Tensor, + observed_raw: np.ndarray | torch.Tensor, + objective_transform: TensorTransform | Any, + reference_point_utility: np.ndarray | torch.Tensor | None, + *, + beta: float, + mc_samples: int = 128, + seed: int = 0, + posterior_chunk_size: int = 512, + hvi_chunk_size: int = 1024, + observation_noise: bool = False, + numeric_tolerance: float = 1e-12, + log_epsilon: float = 1e-12, + moment_method: MomentMethod | str = "monte_carlo", + bound_policy: UCBBoundPolicy | str = "none", + utility_bounds: Sequence[UtilityBound] | None = None, +) -> UCBHVIScoreResult: + """Score a normalized discrete pool from raw-output model posteriors.""" + if moment_method == "monte_carlo": + moments = posterior_utility_moments( + model, + X_pool_norm, + objective_transform, + mc_samples=mc_samples, + seed=seed, + chunk_size=posterior_chunk_size, + observation_noise=observation_noise, + ) + utility_mean = moments.utility_mean + utility_std = moments.utility_std + resolved_mc_samples: int | None = moments.mc_samples + resolved_seed: int | None = moments.seed + resolved_method = moments.moment_method + resolved_observation_noise = moments.observation_noise + elif moment_method == "analytic_identity": + analytic = posterior_identity_moments( + model, + X_pool_norm, + objective_transform, + chunk_size=posterior_chunk_size, + observation_noise=observation_noise, + ) + if analytic.utility_mean.ndim != 2 or analytic.utility_std.ndim != 2: + raise ValueError( + "UCB-HVI pool scoring requires unbatched analytic posterior moments " + "with shape (N, M)." + ) + utility_mean = analytic.utility_mean.detach().cpu().numpy() + utility_std = analytic.utility_std.detach().cpu().numpy() + resolved_mc_samples = None + resolved_seed = None + resolved_method = analytic.moment_method + resolved_observation_noise = analytic.observation_noise + else: + raise ValueError( + "moment_method must be exactly 'monte_carlo' or 'analytic_identity'." + ) + raw = torch.as_tensor( + observed_raw, + dtype=X_pool_norm.dtype, + device=X_pool_norm.device, + ) + if raw.ndim != 2: + raise ValueError( + f"observed_raw must have shape (N, M); got {tuple(raw.shape)}." + ) + observed_utility_tensor = _apply_objective_transform(objective_transform, raw) + contract_version = getattr(objective_transform, "version", None) + if contract_version is None and hasattr(objective_transform, "objective_transform"): + contract_version = getattr( + objective_transform.objective_transform, "version", None + ) + if not isinstance(contract_version, str) or not contract_version.strip(): + raise ValueError( + "objective_transform must expose a non-empty objective contract version." + ) + return score_ucb_hvi_from_moments( + utility_mean, + utility_std, + observed_utility_tensor, + reference_point_utility, + beta=beta, + numeric_tolerance=numeric_tolerance, + chunk_size=hvi_chunk_size, + log_epsilon=log_epsilon, + mc_samples=resolved_mc_samples, + seed=resolved_seed, + observation_noise=resolved_observation_noise, + objective_contract_version=contract_version, + moment_method=resolved_method, + bound_policy=bound_policy, + utility_bounds=utility_bounds, + ) + + +def propose_ucb_hvi_batch( + candidate_pool: Any, + model: Any, + observed_raw: np.ndarray | torch.Tensor, + objective_transform: TensorTransform | Any, + reference_point_utility: np.ndarray | torch.Tensor | None, + *, + q: int, + beta: float, + local_penalization_config: Any, + observed_pending_norm: np.ndarray | None = None, + positive_score_tolerance: float = 1e-12, + mc_samples: int = 128, + seed: int = 0, + posterior_chunk_size: int = 512, + hvi_chunk_size: int = 1024, + observation_noise: bool = False, + numeric_tolerance: float = 1e-12, + log_epsilon: float = 1e-12, + moment_method: MomentMethod | str = "monte_carlo", + bound_policy: UCBBoundPolicy | str = "none", + utility_bounds: Sequence[UtilityBound] | None = None, +) -> UCBHVIBatchProposal: + """Select exactly ``q`` locally penalized positive-HVI pool candidates. + + The UCB-HVI score is static for the pool, while the shared selector updates + the soft local penalty and hard distance masks after every selection. A + candidate whose true raw HVI is not greater than + ``positive_score_tolerance`` is ineligible (log score ``-inf``). + """ + from .batch_selection import BaseScoreResult, select_local_penalized_batch + + if isinstance(positive_score_tolerance, (bool, np.bool_)): + raise ValueError("positive_score_tolerance must be a real non-boolean number.") + tolerance = float(positive_score_tolerance) + if not np.isfinite(tolerance) or tolerance <= 0: + raise ValueError( + "positive_score_tolerance must be finite and strictly positive." + ) + try: + model_parameter = next(model.parameters()) + model_dtype = model_parameter.dtype + model_device = model_parameter.device + except (AttributeError, StopIteration): + model_dtype = torch.double + model_device = torch.device("cpu") + X_pool = torch.as_tensor( + candidate_pool.X_norm, dtype=model_dtype, device=model_device + ) + scoring = score_ucb_hvi_pool( + model, + X_pool, + observed_raw, + objective_transform, + reference_point_utility, + beta=beta, + mc_samples=mc_samples, + seed=seed, + posterior_chunk_size=posterior_chunk_size, + hvi_chunk_size=hvi_chunk_size, + observation_noise=observation_noise, + numeric_tolerance=numeric_tolerance, + log_epsilon=log_epsilon, + moment_method=moment_method, + bound_policy=bound_policy, + utility_bounds=utility_bounds, + ) + + def score_remaining( + remaining_indices: np.ndarray, selected_indices: np.ndarray + ) -> Any: + del selected_indices + raw_scores = scoring.base_score[remaining_indices] + log_scores = scoring.base_log_score[remaining_indices].copy() + log_scores[raw_scores <= tolerance] = -np.inf + return BaseScoreResult( + base_log_score=log_scores, + base_score=raw_scores, + diagnostics={ + "utility_mean": scoring.utility_mean[remaining_indices], + "utility_std": scoring.utility_std[remaining_indices], + "utility_ucb": scoring.utility_ucb[remaining_indices], + "utility_ucb_raw": scoring.utility_ucb_raw[remaining_indices], + "utility_ucb_effective": scoring.utility_ucb_effective[ + remaining_indices + ], + "utility_ucb_clip_amount": scoring.utility_ucb_clip_amount[ + remaining_indices + ], + "eligible_positive_hvi": raw_scores > tolerance, + }, + ) + + selection = select_local_penalized_batch( + candidate_pool, + q, + score_remaining, + local_penalization_config, + observed_pending_norm=observed_pending_norm, + ) + return UCBHVIBatchProposal( + selection=selection, + scoring=scoring, + positive_score_tolerance=tolerance, + metadata={ + "method": scoring.method, + "method_version": scoring.method_version, + "objective_contract_version": scoring.objective_contract_version, + "reference_point_utility": scoring.reference_point_utility.copy(), + "pool_seed": candidate_pool.seed, + "pool_size": candidate_pool.size, + "pool_draws": candidate_pool.draws, + "pool_rejected_duplicate": candidate_pool.rejected_duplicate, + "pool_rejected_avoid": candidate_pool.rejected_avoid, + "pool_rejected_constraint": candidate_pool.rejected_constraint, + "posterior_seed": scoring.seed, + "mc_samples": scoring.mc_samples, + "moment_method": scoring.moment_method, + "bound_policy": scoring.bound_policy, + "utility_bounds": scoring.utility_bounds, + "beta": scoring.beta, + "kappa": scoring.kappa, + "observation_noise": scoring.observation_noise, + "positive_score_tolerance": tolerance, + "local_penalization": { + "radius": local_penalization_config.radius, + "min_batch_distance": local_penalization_config.min_batch_distance, + "min_observed_distance": local_penalization_config.min_observed_distance, + "dimension_weights": local_penalization_config.dimension_weights, + "epsilon": local_penalization_config.epsilon, + }, + "selected_pool_indices": selection.selected_pool_indices.copy(), + }, + ) diff --git a/src/mobo_kit/utils.py b/src/mobo_kit/utils.py index 2c0fd73..aabe2ec 100644 --- a/src/mobo_kit/utils.py +++ b/src/mobo_kit/utils.py @@ -1,6 +1,11 @@ # src/utils.py from __future__ import annotations -from typing import List, Tuple, Any +from dataclasses import dataclass +import csv +import io +from pathlib import Path +from typing import Any, List, Sequence, Tuple + import pandas as pd import numpy as np import torch @@ -8,76 +13,481 @@ from .design import Design + +_METADATA_LABELS = ("units", "start", "stop", "step") +_CSV_ENCODINGS = ("utf-8-sig", "cp1252", "latin-1") + + +@dataclass +class ParsedCampaignCSV: + """Parsed metadata-style campaign CSV and its inferred data contract. + + ``metadata_row_count`` counts rows between the header and the first + experimental row, including an optional blank separator. Header positions + in ``duplicate_headers`` are one-based CSV/Excel column positions. + """ + + config: dict[str, Any] + data: pd.DataFrame + input_columns: list[str] + objective_columns: list[str] + metadata_row_count: int + raw_headers: list[str] + duplicate_headers: dict[str, list[int]] + encoding: str + + +def _read_raw_csv(path: str | Path) -> tuple[Path, list[list[str]], str]: + csv_path = Path(path) + if not csv_path.is_file(): + raise FileNotFoundError(f"Campaign CSV not found: {csv_path}") + + payload = csv_path.read_bytes() + if not payload: + raise ValueError(f"Campaign CSV is empty: {csv_path}") + + decode_errors: list[str] = [] + for encoding in _CSV_ENCODINGS: + try: + text = payload.decode(encoding) + except UnicodeDecodeError as exc: + decode_errors.append(f"{encoding}: {exc}") + continue + + try: + rows = list(csv.reader(io.StringIO(text, newline=""), strict=True)) + except csv.Error as exc: + raise ValueError(f"Malformed CSV syntax in {csv_path}: {exc}") from exc + + if not rows or not any(cell.strip() for cell in rows[0]): + raise ValueError(f"Campaign CSV has no usable header row: {csv_path}") + return csv_path, rows, encoding + + detail = "; ".join(decode_errors) + raise UnicodeError( + f"Could not decode campaign CSV {csv_path} using " + f"{', '.join(_CSV_ENCODINGS)}. {detail}" + ) + + +def _normalize_row_widths( + rows: list[list[str]], width: int, csv_path: Path +) -> list[list[str]]: + normalized: list[list[str]] = [] + for line_number, row in enumerate(rows, start=1): + if len(row) > width and any(cell.strip() for cell in row[width:]): + raise ValueError( + f"CSV row {line_number} in {csv_path} has {len(row)} fields but " + f"the header has {width}; extra nonblank fields are not allowed." + ) + normalized.append((row[:width] + [""] * width)[:width]) + return normalized + + +def _duplicate_header_positions(headers: Sequence[str]) -> dict[str, list[int]]: + positions: dict[str, list[int]] = {} + for position, raw_header in enumerate(headers, start=1): + header = raw_header.strip() + if header: + positions.setdefault(header, []).append(position) + return {name: found for name, found in positions.items() if len(found) > 1} + + +def _find_metadata_rows( + rows: Sequence[Sequence[str]], csv_path: Path +) -> tuple[dict[str, int], int]: + found: dict[str, int] = {} + label_columns: set[int] = set() + + for row_index, row in enumerate(rows[1:], start=1): + matches = [ + (column_index, cell.strip().lower()) + for column_index, cell in enumerate(row) + if cell.strip().lower() in _METADATA_LABELS + ] + if len(matches) > 1: + raise ValueError( + f"Metadata row {row_index + 1} in {csv_path} contains more than " + f"one metadata label: {[label for _, label in matches]}" + ) + if not matches: + continue + + column_index, label = matches[0] + if label in found: + raise ValueError( + f"Duplicate '{label}' metadata row in {csv_path} " + f"(rows {found[label] + 1} and {row_index + 1})." + ) + found[label] = row_index + label_columns.add(column_index) + if len(found) == len(_METADATA_LABELS): + break + + missing = [label for label in _METADATA_LABELS if label not in found] + if missing: + raise ValueError( + "Plain data CSVs are not supported by parse_campaign_csv; expected " + "labeled units/start/stop/step metadata rows. " + f"Missing metadata rows: {missing}." + ) + if len(label_columns) != 1: + raise ValueError( + f"Metadata labels in {csv_path} must use one consistent label column; " + f"found columns {[index + 1 for index in sorted(label_columns)]}." + ) + + return found, next(iter(label_columns)) + + +def _parse_metadata_number( + value: str, *, label: str, header: str, position: int +) -> float: + try: + number = float(value) + except (TypeError, ValueError) as exc: + raise ValueError( + f"Malformed {label} metadata for input '{header}' at column " + f"{position}: {value!r} is not numeric." + ) from exc + if not np.isfinite(number): + raise ValueError( + f"Malformed {label} metadata for input '{header}' at column " + f"{position}: values must be finite." + ) + return number + + +def parse_campaign_csv( + path: str | Path, + expected_objectives: Sequence[str] | None = None, +) -> ParsedCampaignCSV: + """Parse a metadata-style campaign CSV without fixed row offsets. + + Inputs are named columns with complete numeric ``start``, ``stop``, and + ``step`` metadata. If ``expected_objectives`` is omitted, objectives are + inferred from named non-input columns after an optional blank separator. + Plain data-only CSVs and duplicate named headers are rejected explicitly. + Experimental values are preserved here and validated numerically by + :func:`split_XY` at the model boundary. + """ + + csv_path, raw_rows, encoding = _read_raw_csv(path) + raw_headers = list(raw_rows[0]) + rows = _normalize_row_widths(raw_rows, len(raw_headers), csv_path) + headers = [header.strip() for header in raw_headers] + + duplicate_headers = _duplicate_header_positions(raw_headers) + if duplicate_headers: + rendered = ", ".join( + f"{name!r} at columns {positions}" + for name, positions in duplicate_headers.items() + ) + raise ValueError( + "Duplicate CSV headers detected before pandas column renaming: " + f"{rendered}." + ) + + metadata_rows, metadata_label_column = _find_metadata_rows(rows, csv_path) + last_metadata_row = max(metadata_rows.values()) + data_start = last_metadata_row + 1 + while data_start < len(rows) and not any(cell.strip() for cell in rows[data_start]): + data_start += 1 + if data_start >= len(rows): + raise ValueError(f"Campaign CSV has an empty experimental section: {csv_path}") + + input_columns: list[str] = [] + input_positions: list[int] = [] + input_specs: list[dict[str, Any]] = [] + + for column_index, header in enumerate(headers): + if not header or column_index == metadata_label_column: + continue + + numeric_values = { + label: rows[row_index][column_index].strip() + for label, row_index in metadata_rows.items() + if label in {"start", "stop", "step"} + } + present = {label: bool(value) for label, value in numeric_values.items()} + if not any(present.values()): + continue + if not all(present.values()): + missing = [label for label, is_present in present.items() if not is_present] + raise ValueError( + f"Incomplete numeric metadata for column '{header}': missing " + f"{missing}. Input columns require start, stop, and step." + ) + + start = _parse_metadata_number( + numeric_values["start"], + label="start", + header=header, + position=column_index + 1, + ) + stop = _parse_metadata_number( + numeric_values["stop"], + label="stop", + header=header, + position=column_index + 1, + ) + step = _parse_metadata_number( + numeric_values["step"], + label="step", + header=header, + position=column_index + 1, + ) + if stop < start: + raise ValueError( + f"Invalid metadata for input '{header}': stop ({stop}) is below " + f"start ({start})." + ) + if step <= 0: + raise ValueError( + f"Invalid metadata for input '{header}': step must be positive." + ) + + unit = rows[metadata_rows["units"]][column_index].strip() or None + input_columns.append(header) + input_positions.append(column_index) + input_specs.append( + { + "name": header, + "unit": unit, + "start": start, + "stop": stop, + "step": step, + } + ) + + if not input_columns: + raise ValueError( + "Campaign CSV contains no input columns with complete numeric " + f"metadata: {csv_path}" + ) + + named_non_input_positions = [ + column_index + for column_index, header in enumerate(headers) + if header + and column_index != metadata_label_column + and column_index not in input_positions + ] + + if expected_objectives is not None: + if isinstance(expected_objectives, str): + objective_columns = [expected_objectives.strip()] + else: + objective_columns = [str(name).strip() for name in expected_objectives] + if not objective_columns or any(not name for name in objective_columns): + raise ValueError( + "expected_objectives must contain at least one nonblank name." + ) + if len(set(objective_columns)) != len(objective_columns): + raise ValueError("expected_objectives contains duplicate names.") + missing_objectives = [name for name in objective_columns if name not in headers] + if missing_objectives: + raise ValueError( + f"Missing objective columns in campaign CSV: {missing_objectives}." + ) + input_objectives = [name for name in objective_columns if name in input_columns] + if input_objectives: + raise ValueError( + f"Columns cannot be both inputs and objectives: {input_objectives}." + ) + else: + objective_positions = [ + position + for position in named_non_input_positions + if position > max(input_positions) + ] + separator_positions = [ + column_index + for column_index, header in enumerate(headers) + if not header and column_index > max(input_positions) + ] + if separator_positions: + objective_positions = [ + position + for position in objective_positions + if position > separator_positions[0] + ] + objective_columns = [headers[position] for position in objective_positions] + + if not objective_columns: + raise ValueError( + "Campaign CSV has no named objective columns. Provide objective headers " + "or pass expected_objectives." + ) + + unnamed_positions = [ + column_index for column_index, header in enumerate(headers) if not header + ] + experimental_rows: list[list[str]] = [] + for source_row, row in enumerate(rows[data_start:], start=data_start + 1): + if not any(cell.strip() for cell in row): + continue + populated_unnamed = [ + column_index + 1 + for column_index in unnamed_positions + if row[column_index].strip() + ] + if populated_unnamed: + raise ValueError( + f"Experimental row {source_row} contains values in unnamed columns " + f"{populated_unnamed}; add explicit headers before parsing." + ) + experimental_rows.append(row) + + if not experimental_rows: + raise ValueError(f"Campaign CSV has an empty experimental section: {csv_path}") + + data_positions = [ + column_index + for column_index, header in enumerate(headers) + if header and column_index != metadata_label_column + ] + data = pd.DataFrame( + [ + [ + row[column_index] if row[column_index].strip() else pd.NA + for column_index in data_positions + ] + for row in experimental_rows + ], + columns=[headers[column_index] for column_index in data_positions], + ) + + config = { + "inputs": input_specs, + "objectives": {"names": objective_columns}, + "constraints": [], + } + return ParsedCampaignCSV( + config=config, + data=data, + input_columns=input_columns, + objective_columns=objective_columns, + metadata_row_count=data_start - 1, + raw_headers=raw_headers, + duplicate_headers=duplicate_headers, + encoding=encoding, + ) + + def get_objective_names(cfg: dict) -> List[str]: names = cfg.get("objectives", {}).get("names", []) if not names: raise ValueError("Config must have objectives.names as a non-empty list.") return list(names) -def load_csv(path: str) -> pd.DataFrame: - return pd.read_csv(path) -def split_XY(df: pd.DataFrame, design: Design, config: dict) -> Tuple[np.ndarray, np.ndarray]: - """ - Split a DataFrame into input features (X) and objectives (Y) using the new config/design structure. - - This function works with CSV files like configCSV_example.csv that contain: - - Rows 0-4: Configuration metadata (column names, units, start, stop, step) - - Row 5: Empty row - - Rows 6+: Experimental data - - Args: - df: DataFrame containing the data (including metadata rows) - design: Design object with input parameter names - config: Configuration dictionary with objectives.names - - Returns: - Tuple of (X, Y) arrays where: - - X: (N, D) array of input features - - Y: (N, M) array of objectives - - Raises: - KeyError: If required columns are missing from the DataFrame +def load_csv( + path: str | Path, + expected_objectives: Sequence[str] | None = None, +) -> pd.DataFrame: + """Load only experimental rows from a metadata-style campaign CSV.""" + + return parse_campaign_csv(path, expected_objectives=expected_objectives).data + + +def _blank_mask(frame: pd.DataFrame) -> pd.DataFrame: + return frame.isna() | frame.apply( + lambda column: column.map( + lambda value: isinstance(value, str) and not value.strip() + ) + ) + + +def _numeric_model_frame(frame: pd.DataFrame, role: str) -> pd.DataFrame: + blank = _blank_mask(frame) + converted = frame.apply(pd.to_numeric, errors="coerce") + invalid = converted.isna() | ~np.isfinite(converted.astype(float)) + if invalid.any().any(): + locations = [ + f"row {frame.index[row]!r}, column {frame.columns[column]!r}" + for row, column in zip(*np.where(invalid.to_numpy())) + ] + detail = ", ".join(locations[:8]) + if len(locations) > 8: + detail += f", and {len(locations) - 8} more" + kind = "blank" if (invalid & blank).any().any() else "nonnumeric" + raise ValueError( + f"{role} model data contains {kind} or non-finite values at {detail}." + ) + return converted.astype(float) + + +def split_XY( + df: pd.DataFrame, design: Design, config: dict +) -> Tuple[pd.DataFrame, pd.DataFrame]: + """Select and validate named numeric model inputs and objectives. + + Rows are never filled with zero or silently discarded. Any incomplete row + must be completed or removed explicitly by a campaign/QC policy before this + model-boundary function is called. """ - # Get input column names from design + + if not isinstance(df, pd.DataFrame): + raise TypeError("split_XY expects a pandas.DataFrame.") + if df.empty: + raise ValueError("Experimental data is empty; no model rows are available.") + if df.columns.duplicated().any(): + duplicates = list(dict.fromkeys(df.columns[df.columns.duplicated()].tolist())) + raise ValueError(f"Experimental DataFrame has duplicate columns: {duplicates}.") + x_cols = list(design.names) - - # Get objective column names from config y_cols = get_objective_names(config) - - # Check for missing columns - miss_x = [c for c in x_cols if c not in df.columns] - miss_y = [c for c in y_cols if c not in df.columns] - + miss_x = [column for column in x_cols if column not in df.columns] + miss_y = [column for column in y_cols if column not in df.columns] if miss_x or miss_y: parts = [] - if miss_x: + if miss_x: parts.append(f"missing inputs: {miss_x}") - if miss_y: + if miss_y: parts.append(f"missing objectives: {miss_y}") raise KeyError("CSV column check failed: " + "; ".join(parts)) - - # Extract data rows (skip the first 6 rows: metadata + empty row) - data_df = df.iloc[6:].copy() - - # Remove rows with all NaN values (empty rows) - data_df = data_df.dropna(how='all') - - # Extract X and Y arrays - X = data_df[x_cols].astype(float) - Y = data_df[y_cols].astype(float) - + + X_raw = df.loc[:, x_cols].copy() + Y_raw = df.loc[:, y_cols].copy() + objective_blanks = _blank_mask(Y_raw) + if objective_blanks.all().all(): + raise ValueError( + "All objective values are blank; no completed model rows exist." + ) + + all_blank_rows = objective_blanks.all(axis=1) + if all_blank_rows.any(): + raise ValueError( + "Objective values are blank for rows " + f"{Y_raw.index[all_blank_rows].tolist()}; rows are not dropped silently." + ) + partial_rows = objective_blanks.any(axis=1) + if partial_rows.any(): + raise ValueError( + "Partially completed objective rows found at indices " + f"{Y_raw.index[partial_rows].tolist()}; complete every objective " + "before modeling." + ) + + X = _numeric_model_frame(X_raw, "Input") + Y = _numeric_model_frame(Y_raw, "Objective") return X, Y + def select_device(prefer: str = "cuda") -> torch.device: - return torch.device("cuda" if prefer == "cuda" and torch.cuda.is_available() else "cpu") + return torch.device( + "cuda" if prefer == "cuda" and torch.cuda.is_available() else "cpu" + ) + def set_seeds(seed: int) -> None: np.random.seed(seed) torch.manual_seed(seed) + def np_to_torch( - *arrays: np.ndarray, + *arrays: np.ndarray | pd.DataFrame, device: torch.device | None = None, dtype: torch.dtype = torch.float64, return_device: bool = False, @@ -92,7 +502,10 @@ def np_to_torch( """ if device is None: device = select_device("cuda") # uses your existing helper - tensors = tuple(torch.as_tensor(a, dtype=dtype, device=device) for a in arrays) + tensors = tuple( + torch.as_tensor(np.asarray(array), dtype=dtype, device=device) + for array in arrays + ) out = tensors[0] if len(tensors) == 1 else tensors return (out, device) if return_device else out @@ -109,123 +522,30 @@ def torch_to_np(*tensors: torch.Tensor): return arrays[0] if len(arrays) == 1 else arrays -def csv_to_config(csv_path: str, output_path: str = None) -> str: - """ - Convert a CSV configuration file to a YAML config file. - - Expected CSV format: - - Row 0: Column names (input parameters + objectives) - - Row 1: Units for each column - - Row 2: Start values for input parameters - - Row 3: Stop values for input parameters - - Row 4: Step values for input parameters - - Row 5: Empty row - - Row 6+: Experimental data - - Args: - csv_path: Path to the CSV configuration file - output_path: Path for the output YAML file. If None, generates a default name. - - Returns: - Generated config dictionary +def csv_to_config( + csv_path: str | Path, + output_path: str | Path | None = None, + expected_objectives: Sequence[str] | None = None, +) -> dict[str, Any]: + """Build a configuration from a metadata-style campaign CSV. + + This wrapper has no filesystem side effect unless ``output_path`` is + supplied explicitly. Generic campaign conversion always defaults to no + constraints. """ - # Read CSV file - df = pd.read_csv(csv_path) - - # Extract metadata from first few rows - column_names = df.columns.tolist() - units = df.iloc[0].tolist() - starts = df.iloc[1].tolist() - stops = df.iloc[2].tolist() - steps = df.iloc[3].tolist() - - # Identify input parameters and objectives by finding the empty column separator - # Skip the first unnamed column, then find where empty columns start - input_params = [] - objective_params = [] - - # Start from column 1 (skip first unnamed column) - i = 1 - while i < len(column_names): - col_name = column_names[i] - # Check if this is an empty column (NaN, empty string, or pandas unnamed column) - if (pd.isna(col_name) or - str(col_name).strip() == "" or - str(col_name).startswith("Unnamed:")): - # Found the separator - everything after this is objectives - objective_params = [col for col in column_names[i+1:] - if not (pd.isna(col) or str(col).strip() == "" or str(col).startswith("Unnamed:"))] - break - else: - input_params.append(col_name) - i += 1 - - # If no separator found, assume all remaining columns are objectives - if not objective_params: - objective_params = [col for col in column_names[len(input_params)+1:] if not (pd.isna(col) or str(col).strip() == "")] - - # Build the config dictionary - config = { - "inputs": [], - "objectives": {"names": objective_params}, - "constraints": [ - { - "clausius_clapeyron": True, - "ah_col": "absolute_humidity", - "temp_c_col": "temperature_c" - } - ] - } - - # Add input parameters - for i, param in enumerate(input_params): - # Skip empty column names - if pd.isna(param) or str(param).strip() == "": - continue - - # Find the column index in the original column_names list - try: - metadata_idx = column_names.index(param) - except ValueError: - # Fallback: use position-based indexing - metadata_idx = i + 1 # +1 because we skipped the first column - - unit = units[metadata_idx] if metadata_idx < len(units) else "" - start = starts[metadata_idx] if metadata_idx < len(starts) else 0.0 - stop = stops[metadata_idx] if metadata_idx < len(stops) else 1.0 - step = steps[metadata_idx] if metadata_idx < len(steps) else 0.01 - - # Convert to appropriate types - try: - start = float(start) if pd.notna(start) else 0.0 - stop = float(stop) if pd.notna(stop) else 1.0 - step = float(step) if pd.notna(step) else 0.01 - except (ValueError, TypeError): - # Use defaults if conversion fails - start, stop, step = 0.0, 1.0, 0.01 - - input_spec = { - "name": str(param).strip(), - "unit": unit, - "start": start, - "stop": stop, - "step": step - } - - config["inputs"].append(input_spec) - - # Generate output path if not provided - if output_path is None: - import os - csv_basename = os.path.splitext(os.path.basename(csv_path))[0] - output_path = f"configs/{csv_basename}_config.yaml" - - # Ensure output directory exists - import os - os.makedirs(os.path.dirname(output_path), exist_ok=True) - - # Write YAML file - with open(output_path, 'w', encoding='utf-8') as f: - yaml.dump(config, f, default_flow_style=False, sort_keys=False, indent=2) - - return config \ No newline at end of file + + config = parse_campaign_csv( + csv_path, expected_objectives=expected_objectives + ).config + if output_path is not None: + destination = Path(output_path) + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("w", encoding="utf-8", newline="\n") as stream: + yaml.safe_dump( + config, + stream, + default_flow_style=False, + sort_keys=False, + indent=2, + ) + return config diff --git a/src/mobo_kit/workbook_schema.py b/src/mobo_kit/workbook_schema.py new file mode 100644 index 0000000..d087713 --- /dev/null +++ b/src/mobo_kit/workbook_schema.py @@ -0,0 +1,829 @@ +"""Read-only structural audits for historical and updated D2D workbooks.""" + +from __future__ import annotations + +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import DefaultDict, Sequence + +import numpy as np +from openpyxl import load_workbook +from openpyxl.utils import get_column_letter + +from .candidate_pool import physical_rows_to_grid_indices +from .design import InputSpec, build_design + + +_V2_INPUT_HEADERS = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol (uL)", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", +) +_V2_CANONICAL_INPUTS = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", +) +_V2_HEADERS = ( + "Sample number", + *_V2_INPUT_HEADERS, + "Coverage", + "Uniformity", + "1 - Uniformity", + "Phase purity", + "PL - Implied Voc (Max)", + "PL - Implied Voc (Max) Normalized", + "Photoconductance (Max)", + "Photoconductance (Max) Normalized", + "Uniformity score", + "Optoelectronic score", + "Thickness (avg)", + "Normalized thickness (sigma = 250)", + "Total combination - addition", + None, + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", +) +_V3_HEADERS = ( + "Sample number", + *_V2_INPUT_HEADERS, + "Coverage", + "Uniformity", + "1 - Uniformity", + "Phase purity", + "PL - Implied Voc (Max)", + "Photoconductance (Max)", + "Log10 (Photoconductance (Max) x PL - Implied Voc (Max))", + "T1", + "T2", + "T3", + "T4", + "T anom", + "Thickness (avg)", + "Normalized thickness (sigma = 250)", + "Uniformity score", + "Optoelectronic score", + "Thickness score", + "Stability score?", + "Total combination - addition", + "Total combination - multiplied", + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", +) +_V3_OBJECTIVE_HEADERS = ( + "Uniformity score", + "Optoelectronic score", + "Thickness score", +) +_V3_CANONICAL_OBJECTIVES = ( + "uniformity_score", + "optoelectronic_score", + "thickness_score", +) +_V3_IGNORED_MODEL_HEADERS = ( + "Stability score?", + "Total combination - addition", + "Total combination - multiplied", + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", +) + + +@dataclass(frozen=True) +class WorkbookInputException: + """A versioned, observed-only input exception accepted by one profile.""" + + sample_id: int + row: int + input_name: str + observed_value: float + reason: str + + +@dataclass(frozen=True) +class WorkbookInputExceptionRule: + """Explicit runtime rule for one observed-only off-grid input value.""" + + sample_id: int + input_name: str + observed_value: float + reason: str + + +@dataclass(frozen=True) +class WorkbookNote: + cell: str + row: int + column: int + value: str + + +@dataclass(frozen=True) +class WorkbookAudit: + """Raw schema, recognized profile, and non-mutating validation findings.""" + + workbook_path: Path + sheet_names: list[str] + active_sheet: str + used_range: str + raw_headers: list[str | None] + duplicate_headers: dict[str, list[int]] + blank_header_columns: list[int] + sample_row_count: int + warnings: list[str] + profile: str = "unknown" + header_positions: dict[str, list[int]] | None = None + canonical_input_mapping: dict[str, str] | None = None + canonical_input_positions: dict[str, int] | None = None + sample_rows: list[int] | None = None + notes: list[WorkbookNote] | None = None + input_rows_valid: bool | None = None + input_validation_errors: list[str] | None = None + objective_mapping_approved: bool = False + formula_cell_count: int = 0 + canonical_objective_mapping: dict[str, str] | None = None + canonical_objective_positions: dict[str, int] | None = None + ignored_model_positions: dict[str, int] | None = None + objective_mapping_resolved_for_debug: bool = False + input_exceptions: list[WorkbookInputException] | None = None + + @property + def column_count(self) -> int: + return len(self.raw_headers) + + +def _is_nonblank(value: object) -> bool: + if value is None: + return False + if isinstance(value, str): + return bool(value.replace("\u00a0", " ").strip()) + return True + + +def _header_text(value: object) -> str | None: + if not _is_nonblank(value): + return None + return value if isinstance(value, str) else str(value) + + +def _normalized_header(value: str) -> str: + return "".join(character for character in value.casefold() if character.isalnum()) + + +def _sample_identifier_columns(header_cells: dict[int, object]) -> list[int]: + aliases = {"sample", "sampleid", "samplenumber"} + return [ + column + for column, value in header_cells.items() + if (header := _header_text(value)) is not None + and _normalized_header(header) in aliases + ] + + +def _numeric_sample_identifier(value: object) -> int | None: + if isinstance(value, (bool, np.bool_)) or not isinstance( + value, (int, float, np.integer, np.floating) + ): + return None + numeric_value = float(value) + if not np.isfinite(numeric_value) or not numeric_value.is_integer(): + return None + return int(numeric_value) + + +def _numeric_sample_rows( + row_values: dict[int, dict[int, object]], *, sample_column: int +) -> list[int]: + return [ + row + for row, values in sorted(row_values.items()) + if _numeric_sample_identifier(values.get(sample_column)) is not None + ] + + +def _recognize_profile( + raw_headers: list[str | None], + *, + active_sheet: str, + header_row: int, + min_column: int, +) -> str: + if ( + active_sheet == "Sheet1" + and header_row == 1 + and min_column == 1 + and tuple(raw_headers) == _V3_HEADERS + ): + return "d2d_summary_v3_scores" + if ( + active_sheet == "Sheet1" + and header_row == 1 + and min_column == 1 + and tuple(raw_headers) == _V2_HEADERS + ): + return "d2d_summary_v2" + if ( + len(raw_headers) == 29 + and "Anneal Temp" in raw_headers + and raw_headers.count("Uniformity score") == 2 + ): + return "d2d_summary_step1_historical" + return "unknown" + + +def _d2d_design(): + return build_design( + [ + InputSpec("speed_1", 1000, 6000, 500), + InputSpec("time_1", 5, 50, 5), + InputSpec("speed_2", 0, 5000, 500), + InputSpec("time_2", 10, 60, 5), + InputSpec("precur_conc", 1.0, 2.0, 0.05), + InputSpec("precur_vol", 40, 200, 10), + InputSpec("anneal_temp", 100, 185, 5), + InputSpec("anneal_time", 10, 60, 5), + InputSpec("anti_vol", 100, 200, 5), + InputSpec("anti_time", 9, 25, 2), + ] + ) + + +def _validate_v2_inputs( + row_values: dict[int, dict[int, object]], sample_rows: list[int] +) -> tuple[bool, list[str]]: + errors: list[str] = [] + expected_rows = list(range(2, 17)) + if sample_rows != expected_rows: + errors.append( + "The v2 profile requires sample rows exactly at Excel rows 2-16; " + f"found {sample_rows}." + ) + if row_values.get(17): + errors.append("The v2 profile requires Excel row 17 to be blank.") + + observed_identifiers: list[int | None] = [] + for row in expected_rows: + raw_identifier = row_values.get(row, {}).get(1) + if isinstance(raw_identifier, (bool, np.bool_)) or not isinstance( + raw_identifier, (int, float, np.integer, np.floating) + ): + observed_identifiers.append(None) + continue + numeric_identifier = float(raw_identifier) + if not np.isfinite(numeric_identifier) or not numeric_identifier.is_integer(): + observed_identifiers.append(None) + continue + observed_identifiers.append(int(numeric_identifier)) + numeric_identifiers = [ + identifier for identifier in observed_identifiers if identifier is not None + ] + if len(numeric_identifiers) != len(expected_rows) or len( + set(numeric_identifiers) + ) != len(numeric_identifiers): + errors.append( + "The v2 profile requires 15 unique numeric sample identifiers in Excel " + f"rows 2-16; found {observed_identifiers}." + ) + + physical_rows: list[tuple[int, list[float]]] = [] + for row in sample_rows: + values: list[float] = [] + for column, name in zip(range(2, 12), _V2_CANONICAL_INPUTS): + raw = row_values.get(row, {}).get(column) + if isinstance(raw, (bool, np.bool_)): + errors.append(f"Row {row} input {name!r} is boolean, not numeric.") + continue + try: + number = float(raw) + except (TypeError, ValueError): + errors.append(f"Row {row} input {name!r} is missing or nonnumeric.") + continue + if not np.isfinite(number): + errors.append(f"Row {row} input {name!r} is non-finite.") + continue + values.append(number) + if len(values) == 10: + physical_rows.append((row, values)) + valid_indices: list[np.ndarray] = [] + valid_excel_rows: list[int] = [] + for excel_row, physical_row in physical_rows: + try: + valid_indices.append( + physical_rows_to_grid_indices( + np.asarray(physical_row, dtype=float)[None, :], _d2d_design() + )[0] + ) + valid_excel_rows.append(excel_row) + except ValueError as exc: + errors.append(f"Excel row {excel_row}: {exc}") + if valid_indices: + index_array = np.asarray(valid_indices) + unique_indices, inverse, counts = np.unique( + index_array, axis=0, return_inverse=True, return_counts=True + ) + del unique_indices + duplicate_groups = [ + [ + valid_excel_rows[position] + for position in np.flatnonzero(inverse == group) + ] + for group, count in enumerate(counts) + if count > 1 + ] + if duplicate_groups: + errors.append( + "The v2 recipe rows contain duplicate input grid tuples at Excel " + f"row group(s) {duplicate_groups}." + ) + return not errors, errors + + +def _validate_v3_inputs( + row_values: dict[int, dict[int, object]], + sample_rows: list[int], + allowed_input_exceptions: Sequence[WorkbookInputExceptionRule], +) -> tuple[bool, list[str], list[WorkbookInputException]]: + """Validate v3 observations against explicit runtime exception rules.""" + + errors: list[str] = [] + exceptions: list[WorkbookInputException] = [] + expected_rows = list(range(2, 17)) + if sample_rows != expected_rows: + errors.append( + "The v3 profile requires numeric sample rows exactly at Excel rows " + f"2-16; found {sample_rows}." + ) + + observed_identifiers = [ + _numeric_sample_identifier(row_values.get(row, {}).get(1)) + for row in expected_rows + ] + numeric_identifiers = [ + identifier for identifier in observed_identifiers if identifier is not None + ] + if len(numeric_identifiers) != len(expected_rows) or len( + set(numeric_identifiers) + ) != len(numeric_identifiers): + errors.append( + "The v3 profile requires 15 unique numeric sample identifiers in Excel " + f"rows 2-16; found {observed_identifiers}." + ) + + design = _d2d_design() + rules_by_key: dict[tuple[int, str], WorkbookInputExceptionRule] = {} + for rule in allowed_input_exceptions: + if not isinstance(rule, WorkbookInputExceptionRule): + raise TypeError( + "allowed_input_exceptions must contain WorkbookInputExceptionRule values." + ) + key = (rule.sample_id, rule.input_name) + if key in rules_by_key: + raise ValueError(f"Duplicate workbook input exception rule for {key!r}.") + if rule.input_name not in design.names: + raise ValueError( + f"Unknown workbook input exception field {rule.input_name!r}." + ) + dimension = design.names.index(rule.input_name) + observed = float(rule.observed_value) + if not np.isfinite(observed): + raise ValueError("Workbook input exception values must be finite.") + if observed < design.lowers[dimension] or observed > design.uppers[dimension]: + raise ValueError("Workbook input exception values must remain in bounds.") + if np.any( + np.isclose( + observed, + design.var_array[dimension], + rtol=0.0, + atol=1e-9, + ) + ): + raise ValueError("Workbook input exception values must be off-grid.") + if not str(rule.reason).strip(): + raise ValueError("Workbook input exception rules require a reason.") + rules_by_key[key] = rule + + valid_grid_indices: list[np.ndarray] = [] + valid_excel_rows: list[int] = [] + physical_rows: list[tuple[int, int | None, list[float]]] = [] + encountered_rule_keys: set[tuple[int, str]] = set() + for row in sample_rows: + sample_id = _numeric_sample_identifier(row_values.get(row, {}).get(1)) + values: list[float] = [] + for column, name in zip(range(2, 12), _V2_CANONICAL_INPUTS): + raw = row_values.get(row, {}).get(column) + if isinstance(raw, (bool, np.bool_)): + errors.append(f"Row {row} input {name!r} is boolean, not numeric.") + continue + try: + number = float(raw) + except (TypeError, ValueError): + errors.append(f"Row {row} input {name!r} is missing or nonnumeric.") + continue + if not np.isfinite(number): + errors.append(f"Row {row} input {name!r} is non-finite.") + continue + values.append(number) + if len(values) != len(_V2_CANONICAL_INPUTS): + continue + + physical_rows.append((row, sample_id, values)) + physical_row = np.asarray(values, dtype=float) + out_of_bounds = np.flatnonzero( + (physical_row < design.lowers) | (physical_row > design.uppers) + ) + if out_of_bounds.size: + details = ", ".join( + f"{design.names[index]}={physical_row[index]:g} outside " + f"[{design.lowers[index]:g}, {design.uppers[index]:g}]" + for index in out_of_bounds + ) + errors.append(f"Excel row {row} has out-of-bounds input(s): {details}.") + continue + + grid_probe = physical_row.copy() + row_exceptions: list[WorkbookInputException] = [] + for dimension, input_name in enumerate(design.names): + observed = float(physical_row[dimension]) + if np.any( + np.isclose( + observed, + design.var_array[dimension], + rtol=0.0, + atol=1e-9, + ) + ): + continue + rule = ( + None + if sample_id is None + else rules_by_key.get((int(sample_id), input_name)) + ) + if rule is None or not np.isclose( + observed, rule.observed_value, rtol=0.0, atol=1e-12 + ): + continue + grid_probe[dimension] = float(design.var_array[dimension][0]) + encountered_rule_keys.add((rule.sample_id, rule.input_name)) + row_exceptions.append( + WorkbookInputException( + sample_id=rule.sample_id, + row=row, + input_name=rule.input_name, + observed_value=rule.observed_value, + reason=rule.reason, + ) + ) + try: + grid_index = physical_rows_to_grid_indices(grid_probe[None, :], design)[0] + except ValueError as exc: + errors.append(f"Excel row {row}: {exc}") + continue + + if row_exceptions: + exceptions.extend(row_exceptions) + else: + valid_grid_indices.append(grid_index) + valid_excel_rows.append(row) + + missing_rules = sorted(set(rules_by_key) - encountered_rule_keys) + if missing_rules: + errors.append( + "Configured workbook input exception rule(s) were not found exactly: " + f"{missing_rules}." + ) + + if valid_grid_indices: + index_array = np.asarray(valid_grid_indices) + _, inverse, counts = np.unique( + index_array, axis=0, return_inverse=True, return_counts=True + ) + duplicate_groups = [ + [ + valid_excel_rows[position] + for position in np.flatnonzero(inverse == group) + ] + for group, count in enumerate(counts) + if count > 1 + ] + if duplicate_groups: + errors.append( + "The v3 recipe rows contain duplicate on-grid input tuples at " + f"Excel row group(s) {duplicate_groups}." + ) + + if physical_rows: + tuples_to_rows: DefaultDict[tuple[float, ...], list[int]] = defaultdict(list) + for row, _, values in physical_rows: + tuples_to_rows[tuple(values)].append(row) + duplicate_observed_groups = [ + rows for rows in tuples_to_rows.values() if len(rows) > 1 + ] + if duplicate_observed_groups and not any( + "duplicate on-grid input tuples" in error for error in errors + ): + errors.append( + "The v3 recipe rows contain duplicate observed input tuples at " + f"Excel row group(s) {duplicate_observed_groups}." + ) + + return not errors, errors, exceptions + + +def audit_campaign_workbook( + path: str | Path, + *, + allowed_input_exceptions: Sequence[WorkbookInputExceptionRule] = (), +) -> WorkbookAudit: + """Inspect the active worksheet in read-only mode without saving it.""" + workbook_path = Path(path) + if not workbook_path.is_file(): + raise FileNotFoundError(f"Workbook not found: {workbook_path}") + workbook = load_workbook( + filename=workbook_path, + read_only=True, + data_only=False, + ) + try: + sheet_names = list(workbook.sheetnames) + worksheet = workbook.active + active_sheet = worksheet.title + reset_dimensions = getattr(worksheet, "reset_dimensions", None) + if callable(reset_dimensions): + reset_dimensions() + + min_row: int | None = None + min_column: int | None = None + max_row: int | None = None + max_column: int | None = None + header_row: int | None = None + header_cells: dict[int, object] = {} + sample_columns: list[int] = [] + sample_rows: list[int] = [] + row_values: dict[int, dict[int, object]] = {} + formula_count = 0 + + for row in worksheet.iter_rows(): + nonblank_cells: list[tuple[int, int, object]] = [] + for cell in row: + value = cell.value + if not _is_nonblank(value): + continue + cell_row = int(cell.row) + cell_column = int(cell.column) + nonblank_cells.append((cell_row, cell_column, value)) + row_values.setdefault(cell_row, {})[cell_column] = value + if isinstance(value, str) and value.startswith("="): + formula_count += 1 + min_row = cell_row if min_row is None else min(min_row, cell_row) + min_column = ( + cell_column if min_column is None else min(min_column, cell_column) + ) + max_row = cell_row if max_row is None else max(max_row, cell_row) + max_column = ( + cell_column if max_column is None else max(max_column, cell_column) + ) + if not nonblank_cells: + continue + row_number = nonblank_cells[0][0] + if header_row is None: + header_row = row_number + header_cells = {column: value for _, column, value in nonblank_cells} + sample_columns = _sample_identifier_columns(header_cells) + continue + if sample_columns and any( + column == sample_columns[0] for _, column, _ in nonblank_cells + ): + sample_rows.append(row_number) + + if ( + min_row is None + or min_column is None + or max_row is None + or max_column is None + or header_row is None + ): + raise ValueError( + f"Active worksheet {active_sheet!r} contains no nonblank cell values." + ) + + raw_headers = [ + _header_text(header_cells.get(column)) + for column in range(min_column, max_column + 1) + ] + positions_by_header: DefaultDict[str, list[int]] = defaultdict(list) + for column, header in zip(range(min_column, max_column + 1), raw_headers): + if header is not None: + positions_by_header[header].append(column) + header_positions = dict(positions_by_header) + duplicate_headers = { + header: positions + for header, positions in header_positions.items() + if len(positions) > 1 + } + blank_header_columns = [ + column + for column, header in zip(range(min_column, max_column + 1), raw_headers) + if header is None + ] + + warnings: list[str] = [] + for header, positions in duplicate_headers.items(): + warnings.append( + f"Duplicate header {header!r} appears at 1-based columns " + f"{positions}; columns remain separate." + ) + for column in blank_header_columns: + previous_header = ( + raw_headers[column - min_column - 1] if column > min_column else None + ) + after_text = ( + f" after {previous_header!r}" if previous_header is not None else "" + ) + warnings.append( + f"Blank header at 1-based column {column} " + f"({get_column_letter(column)}){after_text} within the used range." + ) + normalized_groups: DefaultDict[str, list[tuple[str, int]]] = defaultdict(list) + for column, header in zip(range(min_column, max_column + 1), raw_headers): + if header is not None: + normalized_groups[_normalized_header(header)].append((header, column)) + for normalized, entries in normalized_groups.items(): + raw_names = {header for header, _ in entries} + if len(raw_names) > 1: + details = ", ".join( + f"{header!r} (column {column})" for header, column in entries + ) + warnings.append( + f"Ambiguous related headers normalize to {normalized!r}: {details}." + ) + if not sample_columns: + warnings.append( + "No sample identifier header was found; sample_row_count is 0." + ) + elif len(sample_columns) > 1: + warnings.append( + "Multiple sample identifier headers were found at 1-based columns " + f"{sample_columns}; sample rows were counted from column " + f"{sample_columns[0]}." + ) + + used_range = ( + f"{get_column_letter(min_column)}{min_row}:" + f"{get_column_letter(max_column)}{max_row}" + ) + profile = _recognize_profile( + raw_headers, + active_sheet=active_sheet, + header_row=header_row, + min_column=min_column, + ) + if profile == "d2d_summary_v3_scores": + # Rows 17-20 contain explanatory notes in the v3 workbook. Only + # integer-valued Sample number cells identify observations. + sample_rows = _numeric_sample_rows(row_values, sample_column=1) + + canonical_mapping: dict[str, str] = {} + canonical_positions: dict[str, int] = {} + canonical_objective_mapping: dict[str, str] = {} + canonical_objective_positions: dict[str, int] = {} + ignored_model_positions: dict[str, int] = {} + objective_mapping_resolved_for_debug = False + input_exceptions: list[WorkbookInputException] = [] + input_valid: bool | None = None + input_errors: list[str] = [] + if profile == "d2d_summary_v2": + canonical_mapping = dict(zip(_V2_INPUT_HEADERS, _V2_CANONICAL_INPUTS)) + canonical_positions = { + canonical: column + for column, canonical in zip(range(2, 12), _V2_CANONICAL_INPUTS) + } + input_valid, input_errors = _validate_v2_inputs(row_values, sample_rows) + if used_range != "A1:AC18": + input_errors.append( + "The v2 profile requires content range A1:AC18; " + f"found {used_range}." + ) + input_valid = False + warnings.append( + "Objective mapping is provisional and not approved for production." + ) + if ( + row_values.get(18, {}).get(17) + and row_values.get(18, {}).get(19) + and not row_values.get(18, {}).get(16) + and not row_values.get(18, {}).get(18) + ): + warnings.append( + "Supplied workbook notes are at Q18/S18 (normalized columns); " + "the Step 2A pack's P18/R18 note locations do not match the file." + ) + elif profile == "d2d_summary_v3_scores": + canonical_mapping = dict(zip(_V2_INPUT_HEADERS, _V2_CANONICAL_INPUTS)) + canonical_positions = { + canonical: column + for column, canonical in zip(range(2, 12), _V2_CANONICAL_INPUTS) + } + canonical_objective_mapping = dict( + zip(_V3_OBJECTIVE_HEADERS, _V3_CANONICAL_OBJECTIVES) + ) + canonical_objective_positions = { + canonical: column + for column, canonical in zip(range(26, 29), _V3_CANONICAL_OBJECTIVES) + } + ignored_model_positions = { + header: column + for column, header in zip(range(29, 36), _V3_IGNORED_MODEL_HEADERS) + } + objective_mapping_resolved_for_debug = True + input_valid, input_errors, input_exceptions = _validate_v3_inputs( + row_values, sample_rows, allowed_input_exceptions + ) + if used_range != "A1:AI20": + input_errors.append( + "The v3 profile requires content range A1:AI20; " + f"found {used_range}." + ) + input_valid = False + warnings.append( + "Objective mapping Z/AA/AB is resolved for debug only and is " + "not approved for production." + ) + for exception in input_exceptions: + warnings.append( + f"Observed-only input exception: Sample {exception.sample_id} " + f"at Excel row {exception.row} retains " + f"{exception.input_name}={exception.observed_value:g}; " + "new candidates must remain on the approved grid." + ) + + sample_row_set = set(sample_rows) + notes = [ + WorkbookNote( + cell=f"{get_column_letter(column)}{row}", + row=row, + column=column, + value=str(value), + ) + for row, values in sorted(row_values.items()) + if row != header_row and row not in sample_row_set + for column, value in sorted(values.items()) + if _is_nonblank(value) + ] + return WorkbookAudit( + workbook_path=workbook_path, + sheet_names=sheet_names, + active_sheet=active_sheet, + used_range=used_range, + raw_headers=raw_headers, + duplicate_headers=duplicate_headers, + blank_header_columns=blank_header_columns, + sample_row_count=len(sample_rows), + warnings=warnings, + profile=profile, + header_positions=header_positions, + canonical_input_mapping=canonical_mapping, + canonical_input_positions=canonical_positions, + sample_rows=sample_rows, + notes=notes, + input_rows_valid=input_valid, + input_validation_errors=input_errors, + objective_mapping_approved=False, + formula_cell_count=formula_count, + canonical_objective_mapping=canonical_objective_mapping, + canonical_objective_positions=canonical_objective_positions, + ignored_model_positions=ignored_model_positions, + objective_mapping_resolved_for_debug=(objective_mapping_resolved_for_debug), + input_exceptions=input_exceptions, + ) + finally: + workbook.close() + + +__all__ = [ + "WorkbookAudit", + "WorkbookInputException", + "WorkbookInputExceptionRule", + "WorkbookNote", + "audit_campaign_workbook", +] diff --git a/tests/__pycache__/smoke_test.cpython-310-pytest-8.4.1.pyc b/tests/__pycache__/smoke_test.cpython-310-pytest-8.4.1.pyc deleted file mode 100644 index 600682cddb41fcfe425e312b585316050b259366..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2398 zcmZ8iTW=f372YekE(Z$#2-#JSjgRK+v9HsC4# z{;wH<*>`I3=3~L)BN*aaAjBYMVjxq$EoNJWX)p)5W-V!;rj~ff)9)5)!`n)H)9v!rv30c0HqZv!M4OuDCS$gRw)DJ_++a7+ zO+9ZWx7aq?W^bXlSbzfHdFUskg>I8J@&9U|x9K~i^OeP))3?uIuf%++$E14!r!gK) z8kL`9A;D3WPUE9$ctj=6o*&al#-|jgA)})5Y7KE(4Md*A67L`2DbE;=rbm^Z=2&L% z=h3Wc2{0Fw5;3u=wdeCVAvjL+IWTh?;UpHa>eOsPMSPS_?8;wY7N;zHfz!NN1)6ad zCh?N;OQ(Yun9w)4X+ANlwJ2lYmtsz*I1f36JCj$<2(~G=UG$@vs}guLgFww!L{b{MtvpXE`1TQJfZbL#O@+w$?|6;l;K@v zOogE2?&m*$|MPpnbe<#ur(&K+ap&G{Bu*oB%U91BCLI{}VTiw(h9Myr=cZ{$Q(EU{ zX-K;Ow2Tdyo6;#jR$9_6)Y_H}V#{V>9ed~I*9LK5ZtI!*jjMgMWE=Dx=@(96fsK7= zm2TMpy9Vfj4p=q60UL_Ood9k5y{R*qMTZqXy%m#_luo{l#JsR^R_Pz;VoWOZzSui>SywF2T5eKI0zFP(BI*I_?qIg%U25cV-5>tlm#z;9E>8I!Hp zBwNLZ+<4iB6R(x)#oDXQ*T$98~RL}5c#IO`RZ1&sbZ2_ui>Psik=#i z?MD}Xg0xf(4Og*je);&bKL#J~E&ZV8>;wh|D)k6jA-$NaJ@?`00!KcsmmiF$irprcf7Ry<{0a(Ve7d+8L5#(8%N^$XL zU{0LM)n$R2l814M_bXqQk5a)BbwmwxF$;5gq1;@!ia_5m&@Cu2zQ9?EFN-CVF)YqP zp2q3X7n3&XsN$Kf6eAVEd--f3Vz}JNZtx)HLaLN} zaN$6R$u@7QaH^MA-u(xk;3vQR;#2M^UP}>M3EZ5+MO1b|)5_JYk1Jy^c>1h1P}jlL z=YlsB@fEo~2k*hMvY-=o)x_1;$XAq3Uy*_0LT+UUozjl-IRu%re8E*us4VDyF3;vE znT$aSoz+!6Qy{QVof5SGdY28^B2(n-L%&4Mfui04u0e&|1Jz9RD|~_MeaNb_p8~ZH zuzX{ysul4qAgF!WJVA~cvr~KucW*-`k$2S|k*_XU_hry&sVsw%nqKo!V;`&A|F^;K zfzu!EM;{#iK~OFZsfcE0haYFroIzs6;lszjdki3=gv0kmukJi}{O}O+2SULz=*?=r zn=dMd0Y-=5PJ9T&Gd;Uw0yP7e_05sFYI|lI);)9GY}-9Gv$xGYa4oYB631-+*mv9J zmdSq!4k`~r?+4W5$xka!eS~D3imLNbzQEV3G0qn{eh6)yD^%`sdd6c(tJXEEYF_c$ zV64<}$ocyagYN)AuKpOJMh@2&ipt6fY6%|g&SjhkjX{k!4KA)BrO~7i3MBxFb;kb( zPvmJXd~#~=Y1}E)AYTa>RAw9r4ZGSw{qfU)L@P=Kk~WV;tPb>lh~&4`zTZ{k>XsDr tG1i%UgZu(iD(^#ZJx}OwRCf^DKyKGLu~bGn@Ee(q>zJP9?LY?p^Iw#ytuz1t diff --git a/tests/__pycache__/test_acquisition.cpython-310-pytest-8.4.1.pyc b/tests/__pycache__/test_acquisition.cpython-310-pytest-8.4.1.pyc deleted file mode 100644 index e693d6a738bddb86214ccdeff13005956b62b1fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33216 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 W8jop~I4dmul&G4PZW&gwYW@c&euLWp diff --git a/tests/__pycache__/test_design.cpython-311-pytest-7.4.0.pyc b/tests/__pycache__/test_design.cpython-311-pytest-7.4.0.pyc deleted file mode 100644 index 3014c55dd2a97310c0f8848663af26ce4ca3579e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12651 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 bymY;S%3B;Zx%nxjIjMFa8;Y5L1PcQIgvKaN diff --git a/tests/__pycache__/test_main.cpython-310-pytest-8.4.1.pyc b/tests/__pycache__/test_main.cpython-310-pytest-8.4.1.pyc deleted file mode 100644 index e98cc5f38df40bde58dafbd56b29585cde35f179..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4019 zcmai$O^h2!6@a^{-R*YUV|#uxvl+ky7GkuqX7V!$C^K4iCqbay$%?S6tTn4vkE_RH zr@QS`m6MEVOF*(BNSgzf6%s+R_m&$MBrZsZ0|&&JLtQz|Vb4e_5WH7y&v*twY}Kn* zud3_+ef2ssGp+%@PyQGmd~O@Y-?1?L!`e3?iO)bVgOSjv6P?q@qy{mfOx+@Bja|2O zO}3s5?6xz^F(b%A?rNFWvJlLS2%{r9A|pEfu2?VW@5=RA$jxvrny=4C3-twEp9vSE zrTWsi?Ya6nW-!3q;y$)W;0SnEe-9e8C}}-2B09IA&+-d<$r#uYM=2)uh&&-bgTv1l0~1naU@-%> ze0Jvuw#PWat8zq9ETF)RJNhz+2G&k~SZ_g^ha^@(G+}c-atGuY-XY^-nvi|EV*HXm zpwKnhd1<_)iq_T?X}u(h+~axp=8Lx)-`Ra%1YGO}qH*wO_f8ya^VY*)ce~T*ML{gZ z?z?y1x}(LV+jrjGm4N^oN-y$Tajko(+?!!b$VMmXZq49iaAvDUe?vE#zBis9vr`wR zb~EfW{7`IxGU=lE{PW)p|MBPFZo#d}__7oBm$#4anlD?OxDvHQfQ3Wn)UY4(WIBIED}vsfNF2A*9v7T7RpZdOf7rjpwkPP$Ad2Snqg

6Xi2^VEDp@5ib;(~TuR-w?MhakXAiWJq z{C)}}%p93R%D~-*CIgs^D4@b!vb>>8vvF=`#v23jO5I)sIl`A z)R&|SsMrIoj2fWAW%=V4F}Bb!5!eDXXpz@CfJx^F6|6(36kb>Wl`NEJ8NLNYz+5TG zwg^}qIS2On#5s!^z>N*KDYMz9fXjAHqctZNk{qnbOyT(u5Po<2qH0DQQznc@Hlq0BId zm8C1I7GHw#&}Y~A@>sjZ&x6*#a92B51>UvFl}amSK{o&q%gXoOtE64`XHVg09mt|G z(AC<1aXJG%3QVto_$hMQfqMnFiotXC0lfVWsD5*)%7Pa?@;TdqO~@Yj!k3a)DMzj< z5~aR{?vnae?q)NH!ISw`1#AL`p+8wR`j@rOoqCeJP7IH80b^ai z0if!|;z2J6`hl_mSAj2qlG1P7SEgvi%4~Gtyl{QEGG(XR&aYsX6|~HH@Op$AfcX6Q zJm43w@@pu*j^cF`Uq$f^6yHQ~6GXL;GM#+LuR?JeXP2i7SNR)fsyFy0v}h)$$Xj3M z-#XLi$}|$Mojta%yyAH9@ep`3S80YUz<-I*f66d?4^1wd8@f@81v3dgXC6bc9b{)vTNE zy?v{myDR;@R@jn<^_d@PcNrHE7L88Wi(=8A+r!Xk4V*&sLMdtu@$fd(TZpj78oUtB zm%11obQ*yWdT5p7J>&xf??4-5a*E@19G&zpA%Xu#ETyBG6T||ZmGhw|z%c`M3IZF@ zSO4Ojp6vGIgfKdcg8~FbmCk-e`n(y)3L>gsJ_ViYNbc_8_3DzMU8Zu|(Bw9UU?+{V zYzVPfCsG;A`Ilg%x-B~%ti4_qyivo|O?I9C2$%Oh2$g9;ET!y+e%K2H#}Hd{h2KW) z)g-Bwlp~|A*Y)LrqHMpO8#{IwoD4GN%ptF0ryrm|;%H>>@1yn(h-qGIOc!pPxl(HN z|6L)Rv%pJNqmEf5WpkMpi390ZlsCX!J+GWeAgLFqR=hOytff&N1d25tJmM`0bUrqz z?AVrK*A$0BQDAdMuiOTlLkzC6{vKc*wj$H+0KK!U#dSF9c;0?13_MR+aWCo~j*qMh zN97BC4_BYVL+Z&^C{E(zx;E-Nsrk=0p{0%sH30Q`rZ(r~RdjB`N4+rE!YD)g?ghgk R7hxY9QYHoBpq48X{{yR1KF9z7 diff --git a/tests/__pycache__/test_main.cpython-311-pytest-7.4.0.pyc b/tests/__pycache__/test_main.cpython-311-pytest-7.4.0.pyc deleted file mode 100644 index 8f6532254800d8775e2a9133c52437c13ca3f264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8365 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 b#aF}?!9~|Z#52Hg(aoLW7;qj=$q4-y(B^Xk diff --git a/tests/__pycache__/test_models.cpython-310-pytest-8.4.1.pyc b/tests/__pycache__/test_models.cpython-310-pytest-8.4.1.pyc deleted file mode 100644 index 468c9f201932a5d36c86bb7a010c090c3a1426b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11843 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 Rj310Y5q~m1t;*pa{5R-_&*1<7 diff --git a/tests/__pycache__/test_plotting.cpython-310-pytest-8.4.1.pyc b/tests/__pycache__/test_plotting.cpython-310-pytest-8.4.1.pyc deleted file mode 100644 index bd59c0fc40771332694f4d4f8c45355ef1d9a71f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2493 zcmZuyOK%%D5MHiUyQ`IC`5`~##P-%rnh0nVwCO1b+N5^f91D#AuTq%A{uE-&r}-Avxs?Wzb#>nSTH(@mKCj#>N&#t zxqgn$b>&xx=Hi-HQ*$Glj~Bd!c+p!_d@)*z>t0>W&1gAZ@mAthZ#7=?*5ZcOh}XS! zr9;sb?+UDKMOWhuZzJCHHbHKCTe#%8*umvrH188!!BrTy$)|YkXPri5`waGv(SiCm zYwzHM+J3ILT1_O@j(TB)eN0$*ocLXu#(pPFy5VtC7pu_;^Krm~E)8Phrx_39Feg}4 zFDM|&D)=x?gD~OjLXe`E%c7L~SwKVH_mfPlDZZ0ZN}_;=Y2u%dfX6{57L*2u5GXwA zNtLjb=;_V_Q5}Z~shq&(>?jSe-(fF=&9W%u{^9fd`e^Iy_GsKW_{k@)f;fxF_)x8W z5J&knd8XSLO*@1!fh6s4jgRM0{xQI<`y)F!OklIRW^AcDUfQJ#RoIzoLz_ZZShsLl7a)*}( z5aEr6%P=d-*>9tW8mYoc zzcHr!yP~X?!T+j?T>;MxDI1srdr%rwy2gRl+}zLS4|qU%m>j#DFy2ytH6KCU3C-7c z6ii(S1A<-F>&R+$dr{QS8@me5uEaLKpn50o`SBg~{q+sN2qj)64qo}e5d(}+nr}UN zc;HHaxW^^ip8Fu%>&9n3azLT)yP;bV<0v?^jy>cqP#1n^U99qt(GJ zf))b@djahvNIH#`2A)W4B9} zZ?1@9Mxnt8LpBj%NZ<)0%?PxqoxShUw4I$y8;~w^xQL`!+#-c2`WDhxZ=N5^p59boxrDr^&;+Ba-JTYD%DrNYxFv- z@b6ydZu>_@C~FheIeFRMO*=h_8`ggO<@IHX`Vwyv(5>7*o7K5-vF zPC<$;$p|HB3-5Bo6rdH1>56n&lSF2JnkKnH-=E2<#0^!cweQ6#?nUHlSsCU6*@mLu aL=}A=RiM;16xz@YWb5`VyWk+>zyARnYS)|q diff --git a/tests/__pycache__/test_plotting.cpython-310.pyc b/tests/__pycache__/test_plotting.cpython-310.pyc deleted file mode 100644 index ccf54c7b4e5f373becdb3eb54cea57d32f4a79f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1351 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 wT8}4OM7{Hve&Lk93P= 10: - # print("\n--- Testing LOOCV ---") - # try: - # kernel_options = [ - # lambda d: RBFKernel(ard_num_dims=d), - # lambda d: MaternKernel(nu=1.5, ard_num_dims=d) - # ] - # noise_options = [None, LogNormalPrior(-4.0, 0.5)] - - # model_cv, cv_results = loocv_select_models( - # X_t, Y_t, - # objective_names=['Inverted Quadratic', 'Sinusoidal'], - # matern_options=kernel_options, - # noise_options=noise_options - # ) - - # print("LOOCV Results:") - # print(cv_results) - - # except Exception as e: - # print(f"LOOCV failed: {e}") - - # 8. Create parity plots - print("\n--- Parity Plots ---") - try: - fig_parity, metrics_parity = plot_parity_np( - Y_train, pred_mean, pred_std, - objective_names=['Inverted Quadratic', 'Sinusoidal'], - save='synthetic_parity_plot.png' - ) - print("Parity plot metrics:") - print(metrics_parity) - except Exception as e: - print(f"Parity plot failed: {e}") - - print("\n=== GP MODELING TEST COMPLETED ===") - return { - 'X_train': X_train, 'Y_train': Y_train, 'Y_true': Y_true, - 'model': model_mixed, 'Y_mean': Y_mean, 'Y_scale': Y_scale, - 'pred_mean': pred_mean, 'pred_std': pred_std, - 'r2_scores': r2_scores, 'rmse_scores': rmse_scores - } - - -def test_mobo_loop(n_initial=20, n_batches=5, batch_size=10, seed=42): - """ - Test the complete MOBO loop with maximization objectives - - Args: - n_initial: Number of initial training points - n_batches: Number of MOBO iterations - batch_size: Number of points to propose per batch - seed: Random seed for reproducibility - """ - print("\n=== MOBO LOOP TEST (MAXIMIZATION) ===") - - # Set up design space - input_specs = [ - InputSpec(name='x1', unit=None, start=0.0, stop=1.0, step=0.01, decimals=2), - InputSpec(name='x2', unit=None, start=0.0, stop=1.0, step=0.01, decimals=2) - ] - design = build_design(input_specs) - print(f"Design space: {len(input_specs)} dimensions") - - # Generate initial training data - np.random.seed(seed) - X_all, Y_all, _ = generate_training_data(n_points=n_initial, noise_std=0.02, seed=seed) - - print(f"Initial training data: {n_initial} points") - print(f"Y_initial range: {Y_all.min(axis=0)} to {Y_all.max(axis=0)}") - - # Track MOBO progression - batch_info = [] - hypervolumes = [] - - # Reference point will be computed automatically by compute_ref_pareto_hv - ref_point_np = np.array([0.02-1e-2, -1e-2]) # Will be set after first hypervolume computation - ref_point_t = None # Will be set during first hypervolume computation - - for batch in range(n_batches + 1): # +1 to include initial evaluation - print(f"\n--- Batch {batch} (N={len(X_all)}) ---") - - # Standardize and prepare data - #Y_std, Y_mean, Y_scale = y_standardize_np(Y_all) - (X_t, Y_t), device = np_to_torch(X_all, Y_all, device='cuda', return_device=True) - - # Fit GP models - try: - # Use different kernels for different objectives - kernels = [ - lambda d: RBFKernel(ard_num_dims=d), # Smooth for inverted quadratic - lambda d: PeriodicKernel(ard_num_dims=d) # Flexible for sinusoidal - ] - noise_priors = None #[LogNormalPrior(-4.0, 0.5), LogNormalPrior(-3.5, 0.5)] - - model = fit_gp_models(X_t, Y_t)#, kernel_fn=kernels, noise_priors=noise_priors) - plot_gp_predictions(model, X_all, Y_all, f"(N={len(X_all)})") - #plot_shap(design, X_all, model) - print("✓ GP models fitted a") - for i, gp in enumerate(model.models): - print("Lengthscales:", gp.covar_module.base_kernel.lengthscale.detach().cpu().numpy().flatten()) - print("Outputscale:", gp.covar_module.outputscale.item()) - print("Noise:", gp.likelihood.noise.item()) - - except Exception as e: - print(f"⚠️ GP fitting failed: {e}") - # Fall back to default model - model = fit_gp_models(X_t, Y_t) - plot_gp_predictions(model, X_all, Y_all, f"(N={len(X_all)})") - print("✓ Fallback to default GP model") - - # Compute hypervolume - try: - # Convert to tensor for compute_ref_pareto_hv - use same device as model - Y_tensor = torch.tensor(Y_all, dtype=torch.float64, device=device) - ref_point_t, pareto_Y, hv = compute_ref_pareto_hv(Y_tensor, ref_point_np) - hypervolumes.append(hv) - print(f"Hypervolume: {hv:.6f}") - - # Pareto front info from the function - n_pareto = len(pareto_Y) - Y_pareto_np = pareto_Y.detach().cpu().numpy() - - print(f"Pareto points: {n_pareto}/{len(Y_all)}") - print(f"Pareto front range: {Y_pareto_np.min(axis=0)} to {Y_pareto_np.max(axis=0)}") - print(f"Reference point: {ref_point_t.detach().cpu().numpy()}") - - except Exception as e: - print(f"⚠️ Hypervolume computation failed: {e}") - hv = 0.0 - hypervolumes.append(hv) - n_pareto = 0 - # Set a fallback reference point if hypervolume computation fails - if ref_point_t is None: - ref_point_t = torch.tensor([0.1-1e-2, -1e-2], dtype=torch.float64, device=device) - - # Store batch information - batch_info.append({ - 'batch': batch, - 'n_points': len(X_all), - 'n_pareto': n_pareto, - 'Y_batch': Y_all.copy(), - 'hypervolume': hv - }) - - # Stop if this is the last evaluation - if batch >= n_batches: - break - - # Propose next batch - print(f"Proposing {batch_size} new points...") - try: - # Use the reference point computed from hypervolume calculation - #ref_point_device = ref_point_t.to(device=device, dtype=torch.float64) - - # Call propose_batch with correct signature - result = propose_batch( - design=design, - model=model, - train_X=X_t, # Normalized training inputs - ref_point_t=ref_point_t, - batch_size=batch_size, - sample_shape=64, - verbose=True - ) - - # Extract physical coordinates (already snapped) - X_next = result['X_phys'] - print(f"✓ Proposed points: {X_next.shape}") - - # Evaluate new points - Y_next = synthetic_objectives(X_next) - print(f"New objectives range: {Y_next.min(axis=0)} to {Y_next.max(axis=0)}") - - # Add to dataset - X_all = np.vstack([X_all, X_next]) - Y_all = np.vstack([Y_all, Y_next]) - - except Exception as e: - print(f"⚠️ Batch proposal failed: {e}") - break - - print(f"\n=== MOBO LOOP COMPLETED ===") - print(f"Total points collected: {len(X_all)}") - print(f"Final hypervolume: {hypervolumes[-1]:.6f}") - print(f"Hypervolume improvement: {hypervolumes[-1] - hypervolumes[0]:.6f}") - - # Create progression plots - print("\nCreating MOBO progression plots...") - try: - plot_mobo_progression(batch_info, hypervolumes) - except Exception as e: - print(f"⚠️ Plotting failed: {e}") - - # Plot final objective space - try: - Y_batches = [info['Y_batch'] for info in batch_info] # Every batch - labels = [f"Batch {info['batch']}" for info in batch_info] - plot_objective_space(Y_batches, labels, "MOBO Objective Space Evolution") - except Exception as e: - print(f"⚠️ Objective space plot failed: {e}") - - return { - 'X_final': X_all, - 'Y_final': Y_all, - 'batch_info': batch_info, - 'hypervolumes': hypervolumes, - 'model': model, - } - - -def compute_pareto_front(Y, minimize=False): - """ - Compute Pareto front for maximization (default) or minimization - - Args: - Y: Objective values (N, M) - minimize: If True, find Pareto front for minimization - - Returns: - pareto_mask: Boolean mask of Pareto optimal points - """ - Y_work = -Y if not minimize else Y # Convert to minimization - - n_points, n_obj = Y_work.shape - pareto_mask = np.ones(n_points, dtype=bool) - - for i in range(n_points): - if not pareto_mask[i]: - continue - - # Check if point i is dominated by any other point - for j in range(n_points): - if i == j or not pareto_mask[j]: - continue - - # j dominates i if j is better in all objectives - if np.all(Y_work[j] <= Y_work[i]) and np.any(Y_work[j] < Y_work[i]): - pareto_mask[i] = False - break - - return pareto_mask - - -if __name__ == "__main__": - # Run GP modeling test - print("Starting synthetic test with maximization objectives...") - gp_results = test_gp_pipeline() - - # Run MOBO loop test - print("\n" + "="*60) - mobo_results = test_mobo_loop(n_initial=20, n_batches=5, batch_size=5) - - print(f"\n🎯 SYNTHETIC TEST SUMMARY:") - print(f"GP modeling: ✓ Completed") - print(f"MOBO loop: ✓ Completed ({len(mobo_results['X_final'])} total points)") - print(f"Hypervolume improvement: {mobo_results['hypervolumes'][-1] - mobo_results['hypervolumes'][0]:.6f}") - print(f"Final Pareto points: {mobo_results['batch_info'][-1]['n_pareto']}") - print("All plots saved to current directory ✓") diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 8d207b9..6bd0f51 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -1,736 +1,197 @@ -# tests/test_acquisition.py -import sys -import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +"""Fast smoke tests for the active acquisition API.""" + +from __future__ import annotations -import yaml -import torch import numpy as np -import pandas as pd -from src.design import build_design_from_config -from src.utils import load_csv, split_XY, np_to_torch, get_objective_names -from src.models import fit_gp_models, loocv_select_models -from src.data import y_minmax_np, y_standardize_np -from src.acquisition import ( - outcome_ge, outcome_le, - _unit_bounds, _make_snap_postproc, build_qnehvi, optimize_acq_qnehvi, propose_batch +import torch +from botorch.acquisition.multi_objective.logei import ( + qLogNoisyExpectedHypervolumeImprovement, +) +from botorch.models import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize + +import mobo_kit.acquisition as acquisition +from mobo_kit.acquisition import ( + _make_snap_postproc, + _unit_bounds, + build_qnehvi, + optimize_acq_qnehvi, + outcome_ge, + outcome_ge_standardized, + outcome_le, + outcome_le_standardized, + propose_batch, ) +from mobo_kit.design import InputSpec, build_design -CFG_PATH = "configs/configCSV_example_config.yaml" -CSV_PATH = "data/processed/configCSV_example.csv" - -def test_outcome_constraint_builders(): - """Test outcome constraint builders with various shapes and values.""" - print("Testing outcome constraint builders...") - - # Test different tensor shapes - shapes = [(64, 2, 4, 3), (10, 5), (100,)] - - for shape in shapes: - Y = torch.zeros(shape) - - # Test outcome_ge: feasible when Y[..., 1] >= 0.8 - c_ge = outcome_ge(obj_idx=1, thresh=0.8) - val_ge = c_ge(Y) - - assert val_ge.shape == shape[:-1], f"Shape mismatch for {shape}" - assert torch.all(val_ge > 0), f"All zeros should be infeasible for {shape}" - - # Make feasible - Y[..., 1] = 0.9 - assert torch.all(c_ge(Y) <= 0), f"Should be feasible for {shape}" - - # Test outcome_le: feasible when Y[..., 2] <= 0.15 - Y = torch.ones(shape) - c_le = outcome_le(obj_idx=2, thresh=0.15) - val_le = c_le(Y) - - assert val_le.shape == shape[:-1], f"Shape mismatch for {shape}" - assert torch.all(val_le > 0), f"All ones should be infeasible for {shape}" - - # Make feasible - Y[..., 2] = 0.10 - assert torch.all(c_le(Y) <= 0), f"Should be feasible for {shape}" - - print("✓ Outcome constraint builders work with various shapes") - -def test_unit_bounds(): - """Test unit bounds helper function.""" - print("Testing unit bounds helper...") - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - dtype = torch.float64 - - for d in [1, 3, 8]: - bounds = _unit_bounds(d, device, dtype) - - assert bounds.shape == (2, d), f"Expected shape (2, {d}), got {bounds.shape}" - assert bounds[0].allclose(torch.zeros(d, device=device, dtype=dtype)), "Lower bounds should be 0" - assert bounds[1].allclose(torch.ones(d, device=device, dtype=dtype)), "Upper bounds should be 1" - assert bounds.device.type == device.type, "Device type mismatch" - assert bounds.dtype == dtype, "Dtype mismatch" - - print("✓ Unit bounds helper works correctly") - -def test_snap_postproc_factory(): - """Test snap post-processing factory function.""" - print("Testing snap post-processing factory...") - - # Load config and design - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - - # Create post-processing function - postproc = _make_snap_postproc(design) - - # Test with different shapes - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - test_shapes = [(5, 8), (1, 3, 8), (10, 2, 8)] - - for shape in test_shapes: - # Create random normalized input - Z = torch.rand(shape, device=device, dtype=torch.float64) - - # Apply post-processing - Z_snapped = postproc(Z) - - # Check output shape matches input - assert Z_snapped.shape == Z.shape, f"Shape mismatch for {shape}" - assert Z_snapped.device == Z.device, "Device mismatch" - assert Z_snapped.dtype == Z.dtype, "Dtype mismatch" - - # Check values are in [0, 1] range - assert torch.all(Z_snapped >= 0) and torch.all(Z_snapped <= 1), "Values out of [0,1] range" - - print("✓ Snap post-processing factory works correctly") - -def test_build_qnehvi(): - """Test qNEHVI acquisition function builder.""" - print("Testing qNEHVI builder...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - # Convert to torch tensors - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - # Normalize Y data - Y_scaled, Y_min, Y_max = y_minmax_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - # Fit model - model = fit_gp_models(X_t, Y_scaled_t) - - # Build qNEHVI - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - acq_func = build_qnehvi( - model=model, - train_X=X_t, - ref_point_t=ref_point, - sample_shape=64, # Smaller for testing - use_lognehvi=True + +def _design(): + return build_design( + [ + InputSpec("temperature", 10.0, 20.0, 5.0, unit="C"), + InputSpec("ratio", 0.0, 1.0, 0.25), + ] ) - - # Verify acquisition function properties - assert hasattr(acq_func, 'model'), "Should have model attribute" - assert hasattr(acq_func, 'ref_point'), "Should have ref_point attribute" - assert hasattr(acq_func, 'X_baseline'), "Should have X_baseline attribute" - - print("✓ qNEHVI builder works correctly") - -def test_optimize_acq_qnehvi(): - """Test acquisition function optimization wrapper.""" - print("Testing acquisition optimization wrapper...") - - # Load real data and fit model - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - Y_scaled, Y_min, Y_max = y_minmax_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - model = fit_gp_models(X_t, Y_scaled_t) - - # Build acquisition function - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - acq_func = build_qnehvi( - model=model, - train_X=X_t, - ref_point_t=ref_point, - sample_shape=32, # Small for testing - use_lognehvi=True + + +def _unfitted_two_objective_model(train_x: torch.Tensor) -> ModelListGP: + y_1 = (train_x[:, :1] + 0.5 * train_x[:, 1:2]).square() + y_2 = 1.0 - 0.25 * train_x[:, :1] + train_x[:, 1:2] + models = [ + SingleTaskGP(train_x, y, outcome_transform=Standardize(m=1)) for y in (y_1, y_2) + ] + return ModelListGP(*models) + + +def test_unit_bounds_and_outcome_constraint_signs(): + bounds = _unit_bounds(3, torch.device("cpu"), torch.float64) + + assert bounds.shape == (2, 3) + assert bounds.device.type == "cpu" + assert bounds.dtype == torch.float64 + torch.testing.assert_close(bounds[0], torch.zeros(3, dtype=torch.float64)) + torch.testing.assert_close(bounds[1], torch.ones(3, dtype=torch.float64)) + + outcomes = torch.tensor([[0.4, 1.2], [0.8, 0.5]], dtype=torch.float64) + torch.testing.assert_close( + outcome_ge(0, 0.5)(outcomes), + torch.tensor([0.1, -0.3], dtype=torch.float64), ) - - # Test optimization - d = X_t.shape[1] - q = 2 # Small batch for testing - - candidates, acq_values = optimize_acq_qnehvi( - acq_function=acq_func, - d=d, - q=q, - num_restarts=5, # Small for testing - raw_samples=100, # Small for testing - device=device, - dtype=torch.float64 + torch.testing.assert_close( + outcome_le(1, 1.0)(outcomes), + torch.tensor([0.2, -0.5], dtype=torch.float64), ) - - # Verify output shapes - assert candidates.shape == (q, d), f"Expected shape ({q}, {d}), got {candidates.shape}" - assert acq_values.shape == (q,), f"Expected shape ({q},), got {acq_values.shape}" - - # Verify values are in [0, 1] range (normalized) - assert torch.all(candidates >= 0) and torch.all(candidates <= 1), "Candidates out of [0,1] range" - - print("✓ Acquisition optimization wrapper works correctly") - -def test_propose_batch_basic(): - """Test basic propose_batch functionality without constraints.""" - print("Testing basic propose_batch...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - Y_scaled, Y_min, Y_max = y_minmax_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - model = fit_gp_models(X_t, Y_scaled_t) - - # Test basic proposal - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - batch_size = 3 - - result = propose_batch( - design=design, - model=model, - train_X=X_t, - ref_point_t=ref_point, - batch_size=batch_size, - num_restarts=5, # Small for testing - raw_samples=100, # Small for testing - sample_shape=32, # Small for testing - verbose=True + + standardized = torch.tensor([[1.0], [2.5]], dtype=torch.float64) + torch.testing.assert_close( + outcome_ge_standardized(0, 14.0, 10.0, 2.0)(standardized), + torch.tensor([1.0, -0.5], dtype=torch.float64), ) - - # Verify result structure - assert isinstance(result, dict), "Should return dictionary" - assert 'X_phys' in result, "Should have X_phys key" - assert 'X_norm' in result, "Should have X_norm key" - assert 'attempts' in result, "Should have attempts key" - assert 'acq_val' in result, "Should have acq_val key" - - # Verify shapes - if result['X_phys'].shape[0] > 0: # If we got valid candidates - assert result['X_phys'].shape[1] == len(design.names), "Physical dimensions should match design" - assert result['X_norm'].shape[1] == len(design.names), "Normalized dimensions should match design" - assert result['X_phys'].shape[0] <= batch_size, "Should not exceed batch size" - assert result['X_norm'].shape[0] <= batch_size, "Should not exceed batch size" - assert result['acq_val'].shape[0] <= batch_size, "Should not exceed batch size" - - print("✓ Basic propose_batch works correctly") - -def test_propose_batch_with_custom_acq(): - """Test propose_batch with custom acquisition function.""" - print("Testing propose_batch with custom acquisition function...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - Y_scaled, Y_mean, Y_std = y_standardize_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - model = fit_gp_models(X_t, Y_scaled_t) - - # Define custom acquisition function builder - def custom_acq_builder(): - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - return build_qnehvi( - model=model, - train_X=X_t, - ref_point_t=ref_point, - sample_shape=32, - use_lognehvi=False # Use regular NEHVI instead of log - ) - - # Test with custom acquisition function - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - batch_size = 2 - - result = propose_batch( - design=design, - model=model, - train_X=X_t, - ref_point_t=ref_point, - batch_size=batch_size, - acq=custom_acq_builder, # Use custom acquisition function - num_restarts=3, # Small for testing - raw_samples=50, # Small for testing - sample_shape=16, # Small for testing - verbose=True + torch.testing.assert_close( + outcome_le_standardized(0, 14.0, 10.0, 2.0)(standardized), + torch.tensor([-1.0, 0.5], dtype=torch.float64), ) - - # Verify result structure - assert isinstance(result, dict), "Should return dictionary" - assert all(key in result for key in ['X_phys', 'X_norm', 'attempts', 'acq_val']), "Missing expected keys" - - print("✓ Custom acquisition function works correctly") - -def test_propose_batch_with_constraints(): - """Test propose_batch with both row constraints and outcome constraints.""" - print("Testing propose_batch with constraints...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - device = torch.device('cuda')#'cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - Y_scaled, Y_mean, Y_std = y_standardize_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - from src.data import x_normalizer_torch - Xn_t = x_normalizer_torch(X_t, design) - - objective_names = get_objective_names(config) - model_cv, results_df = loocv_select_models(Xn_t, Y_scaled_t, objective_names=objective_names, device=device) - #model = fit_gp_models(Xn_t, Y_scaled_t) - - # 🔍 DEBUG: Analyze model uncertainty and performance - print(f"\n🔍 Model Analysis:") - print(f" Dataset size: {Xn_t.shape[0]} samples, {Xn_t.shape[1]} inputs, {Y_scaled_t.shape[1]} objectives") - print(f" LOOCV results columns: {list(results_df.columns)}") - print(f" LOOCV results:") - - # Show LOOCV performance for each objective - for obj_name in objective_names: - obj_results = results_df[results_df['Objective'] == obj_name] - if len(obj_results) > 0: - best_idx = obj_results['RMSE'].idxmin() - best_kernel = obj_results.loc[best_idx, 'Kernel'] - best_noise = obj_results.loc[best_idx, 'NoisePrior'] - best_r2 = obj_results.loc[best_idx, 'R2'] - best_rmse = obj_results.loc[best_idx, 'RMSE'] - print(f" {obj_name}: Best R²={best_r2}, RMSE={best_rmse} (Kernel: {best_kernel}, Noise: {best_noise})") - - # Use posterior_report for comprehensive model analysis - from src.models import posterior_report - from src.metrics import compute_metrics - print(f"\n📊 Model Performance on Training Data:") - try: - pred_mean, pred_std = posterior_report( - model_cv, Xn_t, Y_mean, Y_std - ) - metrics_df = compute_metrics(Y, pred_mean, pred_std, objective_names=objective_names, add_residuals=True, add_zscores=True) - print(f" Training performance metrics:") - for _, row in metrics_df.iterrows(): - print(f" {row['Objective']}: R²={row['R2']}, RMSE={row['RMSE']}") - - except Exception as e: - print(f" Could not run posterior_report: {e}") - - from src.plotting import plot_parity_np - from src.utils import torch_to_np - - # Extract predictions for each objective from report_df - # pred_columns = [f"Pred[{name}]" for name in objective_names] - # pred_mean = report_df[pred_columns].values # Shape: (N, M) - # #print(pred_mean) - - # # Extract standard deviations if available - # std_columns = [f"Std[{name}]" for name in objective_names] - # pred_std = report_df[std_columns].values if all(col in report_df.columns for col in std_columns) else None - - fig, parity_df = plot_parity_np(Y, pred_mean=pred_mean, pred_std=pred_std, objective_names=objective_names) - - # Define row constraints (temperature + humidity constraint) - from src.constraints import constraints_from_config - row_constraints_list = constraints_from_config(config, design) - - # Define outcome constraints using original units (more intuitive) - from src.acquisition import outcome_ge_standardized, outcome_le_standardized - - # Example: PCE >= 15% and Repeatability <= 0.1 (adjust these values based on your actual data!) - # You should replace these with your actual desired thresholds in original units - pce_threshold = 15.0 # Example: 15% PCE minimum - repeatability_threshold = 0.1 # Example: 0.1 maximum repeatability - - outcome_constraints = [ - outcome_ge_standardized(obj_idx=0, thresh_original=pce_threshold, - Y_mean=Y_mean[0], Y_std=Y_std[0]), # PCE >= 15% - outcome_le_standardized(obj_idx=2, thresh_original=repeatability_threshold, - Y_mean=Y_mean[2], Y_std=Y_std[2]), # Repeatability <= 0.1 - ] - - print(f"📋 Outcome constraints:") - print(f" PCE >= {pce_threshold} (standardized: {(pce_threshold - Y_mean[0]) / Y_std[0]:.3f})") - print(f" Repeatability <= {repeatability_threshold} (standardized: {(repeatability_threshold - Y_mean[2]) / Y_std[2]:.3f})") - - # Test with both types of constraints - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - - # 🔍 DEBUG: Analyze reference point and hypervolume - print(f"\n📊 Hypervolume Analysis:") - print(f" Reference point: {ref_point.tolist()}") - print(f" Training data bounds: min={Y_scaled_t.min(dim=0).values.tolist()}") - print(f" Training data bounds: max={Y_scaled_t.max(dim=0).values.tolist()}") - - # Calculate current hypervolume using metrics.py functions - from src.metrics import compute_ref_pareto_hv - - # Get Pareto front and hypervolume - ref_point_auto, pareto_front, current_hv = compute_ref_pareto_hv(Y_scaled_t) - print(f" Auto reference point: {ref_point_auto.tolist()}") - print(f" Pareto front size: {pareto_front.shape[0]} points") - print(f" Current hypervolume: {current_hv:.6f}") - - # Compare with our manual reference point - print(f" Manual vs Auto ref point difference: {torch.norm(ref_point - ref_point_auto):.6f}") - - batch_size = 8 - - result = propose_batch( - design=design, - model=model_cv, - train_X=Xn_t, - ref_point_t=ref_point_auto, - batch_size=batch_size, - row_constraints=row_constraints_list, # Physical space constraints - #constraints=outcome_constraints, # Outcome space constraints - eta=0.05, # Constraint violation penalty - num_restarts=10, # Small for testing - raw_samples=512, # Increased for better MC estimation - sample_shape=256, # Increased for more candidates - max_attempts=5, # More attempts due to constraints - verbose=True, - #use_lognehvi=False # Use regular NEHVI to see raw EI values + + +def test_snap_postprocessor_preserves_shape_dtype_and_grid(): + postprocess = _make_snap_postproc(_design()) + candidates = torch.tensor([[[0.15, 0.15], [0.84, 0.90]]], dtype=torch.float64) + + snapped = postprocess(candidates) + + assert snapped.shape == candidates.shape + assert snapped.dtype == candidates.dtype + assert snapped.device == candidates.device + torch.testing.assert_close( + snapped, + torch.tensor([[[0.0, 0.25], [1.0, 1.0]]], dtype=torch.float64), ) - from src.plotting import plot_bar - Xn_new = result['X_norm'] - Xn_new_t = np_to_torch(Xn_new, device=device) - X_new = result['X_phys'] - pred_mean_new, pred_std_new = posterior_report( - model_cv, Xn_new_t, Y_mean, Y_std, - ) - fig_bar = plot_bar(pred_mean_new, pred_std_new, labels=objective_names) - - # Verify result structure - assert isinstance(result, dict), "Should return dictionary" - assert all(key in result for key in ['X_phys', 'X_norm', 'attempts', 'acq_val']), "Missing expected keys" - - # If we got valid candidates, verify they satisfy row constraints - if result['X_phys'].shape[0] > 0: - print(f"Generated {result['X_phys'].shape[0]} candidates in {result['attempts']} attempts") - - # 🔍 DEBUG: Analyze acquisition values and model predictions - print(f"\n📈 Acquisition Value Analysis:") - print(f" Raw acquisition values (log(EHVI)): {[f'{v:.6f}' for v in result['acq_val']]}") - - # Convert from log(EHVI) to EHVI since use_lognehvi=True (default) - ehvi_values = [np.exp(v) for v in result['acq_val']] - print(f" Actual EHVI values: {[f'{v:.6f}' for v in ehvi_values]}") - - # Print candidate details - print(f"\n🔍 Generated Candidates (Physical Units):") - for i, (phys_vals, norm_vals, acq_val) in enumerate(zip(result['X_phys'], result['X_norm'], result['acq_val'])): - print(f" Candidate {i+1}:") - print(f" Log(EHVI): {acq_val:.6f}") - print(f" EHVI: {np.exp(acq_val):.6f}") - print(f" Physical Values:") - for j, name in enumerate(design.names): - print(f" {name}: {phys_vals[j]:.4f}") - print(f" Normalized Values:") - for j, name in enumerate(design.names): - print(f" {name}: {norm_vals[j]:.4f}") - print() - - # Verify physical constraints are satisfied - from src.constraints import apply_row_constraints - constraint_mask = apply_row_constraints(result['X_phys'], design, row_constraints_list) - print(f"🔒 Row Constraint Satisfaction:") - print(f" All candidates satisfy row constraints: {np.all(constraint_mask)}") - - # Verify dimensions match design - assert result['X_phys'].shape[1] == len(design.names), "Physical dimensions should match design" - assert result['X_norm'].shape[1] == len(design.names), "Normalized dimensions should match design" - - # Check that physical values are within design bounds - for i, name in enumerate(design.names): - phys_vals = result['X_phys'][:, i] - design_min = design.lowers[i] - design_max = design.uppers[i] - assert np.all(phys_vals >= design_min) and np.all(phys_vals <= design_max), \ - f"Values for {name} out of bounds [{design_min}, {design_max}]" - - # Check that normalized values are in [0, 1] - assert np.all(result['X_norm'] >= 0) and np.all(result['X_norm'] <= 1), \ - "Normalized values should be in [0, 1]" - - print(f"✓ All {result['X_phys'].shape[0]} candidates satisfy constraints") - - # 🔍 DEBUG: Explain acquisition values - print(f"\n💡 Acquisition Value Interpretation (Log-NEHVI):") - print(f" Log(EHVI) values: {[f'{v:.6f}' for v in result['acq_val']]}") - print(f" Actual EHVI values: {[f'{v:.6f}' for v in ehvi_values]}") - print(f" Higher EHVI = Greater expected improvement in hypervolume") - print(f" Lower EHVI = Minimal improvement expected") - - else: - print("No valid candidates found - constraints may be too strict") - assert result['attempts'] > 0, "Should have made at least one attempt" - - print("✓ Constraint handling works correctly") - -def test_propose_batch_constraint_stress_test(): - """Stress test with very strict constraints to test failure handling.""" - print("Testing propose_batch with very strict constraints...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - Y_scaled, Y_min, Y_max = y_minmax_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - # 🔍 DEBUG: Try different model fitting approaches - print(f"\n🔍 Model Fitting Comparison:") - - # Option 1: Basic GP (likely overfitting) - print(f" Testing basic GP fit...") - model_basic = fit_gp_models(X_t, Y_scaled_t) - - # Option 2: LOOCV-selected models (better regularization) - print(f" Testing LOOCV-selected models...") - objective_names = get_objective_names(config) - model_loocv, results_df = loocv_select_models(X_t, Y_scaled_t, objective_names=objective_names, device=device) - - # Compare model uncertainties - print(f"\n📊 Model Uncertainty Comparison:") - for model_name, model in [("Basic GP", model_basic), ("LOOCV GP", model_loocv)]: - with torch.no_grad(): - posterior = model[0].posterior(X_t) - mean = posterior.mean - variance = posterior.variance - uncertainty = torch.sqrt(variance) - print(f" {model_name}:") - for i, obj_name in enumerate(objective_names): - print(f" {obj_name}: μ={mean[0, i].item():.4f}, σ={uncertainty[0, i].item():.4f}") - - # Use LOOCV model for better performance - model = model_loocv - - # Define very strict constraints that are nearly impossible to satisfy - def strict_row_constraint(X_phys, design): - """Very strict constraint - only allows very specific temperature/humidity combinations.""" - temp_idx = design.names.index('temperature_c') - humidity_idx = design.names.index('absolute_humidity') - - # Only allow temperature between 25-26°C AND humidity between 5-7 g/m³ - temp_ok = (X_phys[:, temp_idx] >= 25.0) & (X_phys[:, temp_idx] <= 26.0) - humidity_ok = (X_phys[:, humidity_idx] >= 5.0) & (X_phys[:, humidity_idx] <= 7.0) - - return temp_ok & humidity_ok - - # Very strict outcome constraints - from src.acquisition import outcome_ge - strict_outcome_constraints = [ - outcome_ge(obj_idx=0, thresh=0.9), # PCE >= 0.9 (very high) - outcome_ge(obj_idx=1, thresh=0.9), # Stability >= 0.9 (very high) - ] - - ref_point = Y_scaled_t.min(dim=0).values - 0.01 - - # 🔍 DEBUG: Test different acquisition function settings - print(f"\n🧪 Testing Different Acquisition Settings:") - - # Test 1: Regular NEHVI - print(f" Test 1: Regular NEHVI") - result1 = propose_batch( - design=design, - model=model, - train_X=X_t, - ref_point_t=ref_point, - batch_size=2, - row_constraints=[strict_row_constraint], - constraints=strict_outcome_constraints, - eta=0.01, # Strict penalty - num_restarts=2, # Small for testing - raw_samples=64, # Small for testing - sample_shape=16, # Small for testing - max_attempts=3, # Limited attempts - verbose=False, - use_lognehvi=False # Regular NEHVI + + +def test_build_qnehvi_constructs_active_botorch_acquisition(): + train_x = torch.tensor( + [ + [0.0, 0.0], + [0.2, 0.8], + [0.4, 0.3], + [0.7, 1.0], + [1.0, 0.5], + ], + dtype=torch.float64, ) - - # Test 2: LogNEHVI (your default) - print(f" Test 2: LogNEHVI (default)") - result2 = propose_batch( - design=design, + model = _unfitted_two_objective_model(train_x) + + acq = build_qnehvi( model=model, - train_X=X_t, - ref_point_t=ref_point, + train_X=train_x, + ref_point_t=torch.tensor([-0.5, -0.5], dtype=torch.float64), + sample_shape=8, + prune_baseline=False, + ) + + assert isinstance(acq, qLogNoisyExpectedHypervolumeImprovement) + assert acq.sampler.sample_shape == torch.Size([8]) + + +def test_optimize_wrapper_passes_cpu_bounds_and_options(monkeypatch): + received = {} + + def fake_optimize_acqf(**kwargs): + received.update(kwargs) + bounds = kwargs["bounds"] + q = kwargs["q"] + candidates = bounds.mean(dim=0).repeat(q, 1) + values = torch.arange(q, device=bounds.device, dtype=bounds.dtype) + return candidates, values + + monkeypatch.setattr(acquisition, "optimize_acqf", fake_optimize_acqf) + options = {"maxiter": 2} + + candidates, values = optimize_acq_qnehvi( + acq_function=object(), + d=2, + q=3, + num_restarts=4, + raw_samples=16, + device=torch.device("cpu"), + dtype=torch.float64, + options=options, + sequential=False, + ) + + assert candidates.shape == (3, 2) + assert values.shape == (3,) + assert received["num_restarts"] == 4 + assert received["raw_samples"] == 16 + assert received["options"] is options + assert received["sequential"] is False + torch.testing.assert_close( + received["bounds"], + torch.tensor([[0.0, 0.0], [1.0, 1.0]], dtype=torch.float64), + ) + + +def test_propose_batch_returns_snapped_physical_and_normalized_arrays(monkeypatch): + class DummyAcquisition: + def __init__(self): + self.pending_calls = [] + + def set_X_pending(self, value): + self.pending_calls.append(value) + + dummy_acquisition = DummyAcquisition() + + def fake_optimize(**kwargs): + raw = torch.tensor( + [[0.15, 0.15], [0.84, 0.90]], + dtype=kwargs["dtype"], + device=kwargs["device"], + ) + candidates = kwargs["post_processing_func"](raw) + values = torch.tensor( + [1.5, 1.0], dtype=kwargs["dtype"], device=kwargs["device"] + ) + return candidates, values + + monkeypatch.setattr(acquisition, "optimize_acq_qnehvi", fake_optimize) + train_x = torch.tensor([[0.0, 0.0], [1.0, 1.0]], dtype=torch.float64) + + result = propose_batch( + design=_design(), + model=None, + train_X=train_x, + ref_point_t=torch.tensor([-1.0, -1.0], dtype=torch.float64), batch_size=2, - row_constraints=[strict_row_constraint], - constraints=strict_outcome_constraints, - eta=0.01, # Strict penalty - num_restarts=2, # Small for testing - raw_samples=64, # Small for testing - sample_shape=16, # Small for testing - max_attempts=3, # Limited attempts - verbose=False, - use_lognehvi=True # LogNEHVI + acq=lambda: dummy_acquisition, + max_attempts=1, + device=torch.device("cpu"), + dtype=torch.float64, ) - - # Compare results - print(f"\n📊 Acquisition Function Comparison:") - print(f" Regular NEHVI: {len(result1['acq_val'])} candidates, acq_vals: {[f'{v:.6f}' for v in result1['acq_val']]}") - print(f" LogNEHVI: {len(result2['acq_val'])} candidates, acq_vals: {[f'{v:.6f}' for v in result2['acq_val']]}") - - # Use the result with more candidates for detailed analysis - result = result1 if len(result1['acq_val']) > 0 else result2 - - # Verify graceful handling of strict constraints - assert isinstance(result, dict), "Should return dictionary even with strict constraints" - assert all(key in result for key in ['X_phys', 'X_norm', 'attempts', 'acq_val']), "Missing expected keys" - assert result['attempts'] > 0, "Should have made at least one attempt" - - print(f"\n📊 Stress Test Results:") - print(f" Attempts made: {result['attempts']}") - print(f" Candidates found: {result['X_phys'].shape[0]}") - - if result['X_phys'].shape[0] == 0: - print("✓ Gracefully handled impossible constraints (returned empty batch)") - else: - print(f"✓ Found {result['X_phys'].shape[0]} candidates despite strict constraints") - - # 🔍 DEBUG: Analyze why these candidates were selected - print(f"\n🔍 Candidate Analysis:") - X_candidates = torch.tensor(result['X_norm'], dtype=torch.float64, device=device) - with torch.no_grad(): - candidate_posterior = model[0].posterior(X_candidates) - candidate_mean = candidate_posterior.mean - candidate_std = torch.sqrt(candidate_posterior.variance) - - print(f" Model predictions for candidates:") - for i, (acq_val, mean, std) in enumerate(zip(result['acq_val'], candidate_mean, candidate_std)): - print(f" Candidate {i+1}: acq={acq_val:.6f}") - for j, obj_name in enumerate(objective_names): - print(f" {obj_name}: μ={mean[j].item():.4f}, σ={std[j].item():.4f}") - - # Print detailed candidate information - print(f"\n🔍 Generated Candidates (Physical Units):") - for i, (phys_vals, norm_vals, acq_val) in enumerate(zip(result['X_phys'], result['X_norm'], result['acq_val'])): - print(f" Candidate {i+1}:") - print(f" Acquisition Value: {acq_val:.6f}") - print(f" Physical Values:") - for j, name in enumerate(design.names): - print(f" {name}: {phys_vals[j]:.4f}") - print(f" Normalized Values:") - for j, name in enumerate(design.names): - print(f" {name}: {norm_vals[j]:.4f}") - print() - - # Verify constraints are satisfied - constraint_mask = strict_row_constraint(result['X_phys'], design) - print(f"🔒 Constraint Satisfaction Check:") - print(f" Row constraint satisfied: {np.all(constraint_mask)}") - - # Check specific constraint values - temp_idx = design.names.index('temperature_c') - humidity_idx = design.names.index('absolute_humidity') - - print(f" Temperature constraints (25-26°C):") - for i, temp in enumerate(result['X_phys'][:, temp_idx]): - in_range = 25.0 <= temp <= 26.0 - print(f" Candidate {i+1}: {temp:.2f}°C {'✓' if in_range else '✗'}") - - print(f" Humidity constraints (5-7 g/m³):") - for i, humidity in enumerate(result['X_phys'][:, humidity_idx]): - in_range = 5.0 <= humidity <= 7.0 - print(f" Candidate {i+1}: {humidity:.2f} g/m³ {'✓' if in_range else '✗'}") - - print("✓ Stress test completed successfully") - -def main(): - """Run comprehensive acquisition function tests.""" - print("Running comprehensive acquisition.py tests...\n") - - try: - # Test 1: Outcome constraint builders - print("="*60) - test_outcome_constraint_builders() - - # Test 2: Unit bounds helper - print("="*60) - test_unit_bounds() - - # Test 3: Snap post-processing factory - print("="*60) - test_snap_postproc_factory() - - # Test 4: qNEHVI builder - print("="*60) - test_build_qnehvi() - - # Test 5: Acquisition optimization wrapper - print("="*60) - test_optimize_acq_qnehvi() - - # Test 6: Basic propose_batch - print("="*60) - test_propose_batch_basic() - - # Test 7: Custom acquisition function - print("="*60) - test_propose_batch_with_custom_acq() - - # Test 8: Constraint handling - print("="*60) - test_propose_batch_with_constraints() - - # Test 9: Constraint stress test - print("="*60) - test_propose_batch_constraint_stress_test() - - print("="*60) - print("\n🎉 All acquisition tests passed successfully!") - print("🎯 Tested outcome constraints, helpers, builders, and main functions") - print("🎯 Validated with real configCSV_example.csv data") - print("🎯 Confirmed custom acquisition function support") - print("🎯 Tested comprehensive constraint handling (row + outcome constraints)") - print("🎯 Validated graceful failure handling with strict constraints") - - except Exception as e: - print(f"\n❌ Test failed: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - main() + + assert set(result) == {"X_phys", "X_norm", "acq_val", "attempts"} + np.testing.assert_allclose(result["X_norm"], [[0.0, 0.25], [1.0, 1.0]]) + np.testing.assert_allclose(result["X_phys"], [[10.0, 0.25], [20.0, 1.0]]) + np.testing.assert_allclose(result["acq_val"], [1.5, 1.0]) + assert result["attempts"] == 1 + assert dummy_acquisition.pending_calls == [None] diff --git a/tests/test_batch_comparison.py b/tests/test_batch_comparison.py new file mode 100644 index 0000000..9f3628c --- /dev/null +++ b/tests/test_batch_comparison.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from mobo_kit.batch_comparison import ( + DEFAULT_REGIONAL_THRESHOLDS, + compare_candidate_batches, + regional_match_batches, +) + + +def test_known_exact_regional_chamfer_and_hausdorff_metrics(): + reference = np.array([[0.0, 0.0], [0.5, 0.5], [1.0, 1.0]]) + comparison = np.array([[0.0, 0.0], [0.5, 0.6], [0.8, 1.0]]) + result = regional_match_batches(reference, comparison) + + assert result.exact_overlap_count == 1 + assert result.exact_overlap == 1 + assert result.jaccard_overlap == pytest.approx(1.0 / 5.0) + np.testing.assert_allclose(result.matched_pair_distances, [0.0, 0.1, 0.2]) + np.testing.assert_allclose(result.matched_distances, [0.0, 0.1, 0.2]) + assert result.mean_matched_distance == pytest.approx(0.1) + assert result.maximum_matched_distance == pytest.approx(0.2) + assert result.max_matched_distance == pytest.approx(0.2) + assert dict(result.regional_match_counts) == {0.10: 2, 0.15: 2, 0.20: 3} + assert result.regional_match_count(0.15) == 2 + assert result.symmetric_chamfer_distance == pytest.approx(0.1) + assert result.chamfer_distance == pytest.approx(0.1) + assert result.hausdorff_distance == pytest.approx(0.2) + assert result.thresholds == DEFAULT_REGIONAL_THRESHOLDS + + +def test_hungarian_assignment_enforces_optimal_one_to_one_matching(): + reference = np.array([[0.0], [0.1]]) + comparison = np.array([[0.09], [1.0]]) + result = compare_candidate_batches(reference, comparison) + + np.testing.assert_allclose(result.matched_pair_distances, [0.09, 0.9]) + np.testing.assert_allclose(result.matched_reference_rows, [[0.0], [0.1]]) + np.testing.assert_allclose(result.matched_comparison_rows, [[0.09], [1.0]]) + assert result.mean_matched_distance == pytest.approx(0.495) + assert result.maximum_matched_distance == pytest.approx(0.9) + assert dict(result.regional_match_counts) == {0.10: 1, 0.15: 1, 0.20: 1} + + +def test_all_metrics_and_canonical_pairs_are_invariant_to_row_reordering(): + reference = np.array([[0.1, 0.9], [0.8, 0.2], [0.3, 0.4], [0.6, 0.7], [0.0, 0.0]]) + comparison = np.array( + [[0.31, 0.39], [0.62, 0.72], [0.82, 0.18], [0.0, 0.0], [0.2, 0.8]] + ) + baseline = regional_match_batches(reference, comparison) + reordered = regional_match_batches( + reference[[3, 0, 4, 1, 2]], comparison[[2, 4, 1, 0, 3]] + ) + + assert reordered.exact_overlap_count == baseline.exact_overlap_count + assert reordered.jaccard_overlap == baseline.jaccard_overlap + assert dict(reordered.regional_match_counts) == dict(baseline.regional_match_counts) + assert reordered.mean_matched_distance == baseline.mean_matched_distance + assert reordered.maximum_matched_distance == baseline.maximum_matched_distance + assert reordered.symmetric_chamfer_distance == baseline.symmetric_chamfer_distance + assert reordered.hausdorff_distance == baseline.hausdorff_distance + np.testing.assert_array_equal( + reordered.matched_reference_rows, baseline.matched_reference_rows + ) + np.testing.assert_array_equal( + reordered.matched_comparison_rows, baseline.matched_comparison_rows + ) + np.testing.assert_array_equal( + reordered.matched_pair_distances, baseline.matched_pair_distances + ) + + +def test_rectangular_batches_use_minimum_cardinality_assignment_and_full_chamfer(): + reference = np.array([[0.0], [0.5], [1.0]]) + comparison = np.array([[0.1], [0.9]]) + result = regional_match_batches(reference, comparison) + + assert result.matched_pair_distances.shape == (2,) + np.testing.assert_allclose(result.matched_pair_distances, [0.1, 0.1]) + assert result.mean_matched_distance == pytest.approx(0.1) + assert result.symmetric_chamfer_distance == pytest.approx( + 0.5 * ((0.1 + 0.4 + 0.1) / 3.0 + (0.1 + 0.1) / 2.0) + ) + assert result.hausdorff_distance == pytest.approx(0.4) + + +def test_exact_overlap_uses_set_jaccard_even_with_repeated_rows(): + reference = np.array([[0.0], [0.0], [0.5]]) + comparison = np.array([[0.0], [1.0], [1.0]]) + result = regional_match_batches(reference, comparison) + assert result.exact_overlap_count == 1 + assert result.jaccard_overlap == pytest.approx(1.0 / 3.0) + + +def test_custom_thresholds_are_sorted_and_require_exact_lookup(): + result = regional_match_batches( + np.array([[0.0], [1.0]]), + np.array([[0.05], [0.8]]), + thresholds=(0.2, 0.05, 0.1), + ) + assert result.thresholds == (0.05, 0.1, 0.2) + assert dict(result.regional_match_counts) == {0.05: 1, 0.1: 1, 0.2: 2} + with pytest.raises(KeyError, match="not evaluated"): + result.regional_match_count(0.15) + + +@pytest.mark.parametrize( + "reference, comparison, thresholds, message", + [ + (np.empty((0, 2)), np.ones((1, 2)), (0.1,), "non-empty"), + (np.ones((1, 2)), np.ones((1, 3)), (0.1,), "same input dimension"), + (np.array([[np.nan]]), np.ones((1, 1)), (0.1,), "finite"), + (np.array([[1.01]]), np.ones((1, 1)), (0.1,), "normalized"), + (np.ones((1, 1)), np.ones((1, 1)), (), "must not be empty"), + (np.ones((1, 1)), np.ones((1, 1)), (0.1, 0.1), "duplicates"), + (np.ones((1, 1)), np.ones((1, 1)), (-0.1,), "non-negative"), + ], +) +def test_batch_comparison_validation(reference, comparison, thresholds, message): + with pytest.raises(ValueError, match=message): + regional_match_batches(reference, comparison, thresholds=thresholds) diff --git a/tests/test_batch_selection.py b/tests/test_batch_selection.py new file mode 100644 index 0000000..786237e --- /dev/null +++ b/tests/test_batch_selection.py @@ -0,0 +1,295 @@ +import numpy as np +import pytest + +from mobo_kit.batch_selection import ( + BaseScoreResult, + LocalPenalizationConfig, + UndersizedBatchError, + select_local_penalized_batch, + soft_local_penalty, +) +from mobo_kit.candidate_pool import CandidatePool + + +def _pool(values): + X = np.asarray(values, dtype=float) + if X.ndim == 1: + X = X[:, None] + grid_indices = np.zeros(X.shape, dtype=int) + grid_indices[:, 0] = np.arange(X.shape[0]) + return CandidatePool( + grid_indices=grid_indices, + X_phys=X.copy(), + X_norm=X.copy(), + seed=1, + draws=X.shape[0], + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + + +def _static_callback(scores, calls=None): + scores = np.asarray(scores, dtype=float) + + def callback(remaining, selected): + if calls is not None: + calls.append((remaining.copy(), selected.copy())) + return BaseScoreResult( + base_log_score=scores[remaining], + base_score=np.exp(scores[remaining]), + diagnostics={"selected_before": selected.copy()}, + ) + + return callback + + +def test_soft_penalty_zero_and_far_distance_limits(): + factors, logs = soft_local_penalty(np.array([0.0, 10.0]), radius=0.2, epsilon=1e-9) + assert factors[0] == pytest.approx(0.0) + assert logs[0] == pytest.approx(np.log(1e-9)) + assert factors[1] == pytest.approx(1.0) + with pytest.raises(ValueError, match="radius"): + soft_local_penalty(np.array([1.0]), radius=0) + with pytest.raises(ValueError, match="non-boolean"): + LocalPenalizationConfig(radius=True, min_batch_distance=0.0) + + +def test_stable_tie_break_and_callback_receives_selected_indices(): + calls = [] + result = select_local_penalized_batch( + _pool([0.0, 0.5, 1.0]), + 2, + _static_callback([0.0, 0.0, 0.0], calls), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0.0), + ) + assert result.selected_pool_indices.tolist() == [0, 2] + assert calls[0][1].tolist() == [] + assert calls[1][1].tolist() == [0] + assert [step.order for step in result.steps] == [1, 2] + + +def test_local_penalty_increases_diversity_over_unpenalized_top_q(): + pool = _pool([0.0, 0.01, 0.02, 0.6, 1.0]) + base_logs = np.log([1.0, 0.99, 0.98, 0.8, 0.7]) + unpenalized_top = pool.X_norm[np.argsort(-base_logs, kind="stable")[:3], 0] + result = select_local_penalized_batch( + pool, + 3, + _static_callback(base_logs), + LocalPenalizationConfig(radius=0.2, min_batch_distance=0.0), + ) + selected = np.sort(result.X_norm[:, 0]) + assert np.min(np.diff(selected)) > np.min(np.diff(np.sort(unpenalized_top))) + assert result.steps[1].penalty_factor < 1.0 + + +def test_none_radius_disables_only_the_soft_penalty(): + pool = _pool([0.0, 0.01, 0.5, 1.0]) + result = select_local_penalized_batch( + pool, + 3, + _static_callback([0.0, -0.1, -0.2, -0.3]), + LocalPenalizationConfig(radius=None, min_batch_distance=0.0), + ) + assert result.selected_pool_indices.tolist() == [0, 1, 2] + assert result.distance_diagnostics["radius"] is None + assert all(step.penalty_factor == pytest.approx(1.0) for step in result.steps) + assert all(step.log_penalty == pytest.approx(0.0) for step in result.steps) + assert all( + step.penalized_log_score == pytest.approx(step.base_log_score) + for step in result.steps + ) + + +def test_none_radius_preserves_hard_batch_and_observed_rules(): + result = select_local_penalized_batch( + _pool([0.0, 0.1, 0.5, 0.9]), + 2, + _static_callback([0.0, -0.01, -0.2, -0.3]), + LocalPenalizationConfig( + radius=None, + min_batch_distance=0.4, + min_observed_distance=0.15, + ), + observed_pending_norm=np.array([[0.9]]), + ) + # The second-highest score is too close to the first selection, and the + # final point is an exact observed recipe. The hard rules therefore choose + # the third-ranked point even though no soft penalty is active. + assert result.selected_pool_indices.tolist() == [0, 2] + assert result.distance_diagnostics["minimum_within_batch_distance"] >= 0.4 + assert all(step.penalty_factor == pytest.approx(1.0) for step in result.steps) + + +def test_hard_batch_and_observed_distances_are_enforced(): + pool = _pool([0.0, 0.1, 0.3, 0.55, 0.9]) + result = select_local_penalized_batch( + pool, + 3, + _static_callback([0.0, -0.1, -0.2, -0.3, -0.4]), + LocalPenalizationConfig( + radius=0.1, min_batch_distance=0.25, min_observed_distance=0.15 + ), + observed_pending_norm=np.array([[0.3]]), + ) + matrix = result.distance_diagnostics["pairwise_distance_matrix"] + triangle = matrix[np.triu_indices(3, k=1)] + assert np.all(triangle >= 0.25 - 1e-12) + assert np.all(np.abs(result.X_norm[:, 0] - 0.3) >= 0.15 - 1e-12) + + +def test_exact_observed_duplicate_is_excluded_even_with_zero_threshold(): + result = select_local_penalized_batch( + _pool([0.0, 0.5, 1.0]), + 1, + _static_callback([1.0, 0.0, -1.0]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0.0), + observed_pending_norm=np.array([[0.0]]), + ) + assert result.selected_pool_indices.tolist() == [1] + + +def test_impossible_spacing_and_all_ineligible_fail_structurally(): + pool = _pool([0.0, 0.1, 0.2]) + with pytest.raises(UndersizedBatchError) as captured: + select_local_penalized_batch( + pool, + 2, + _static_callback([0.0, -0.1, -0.2]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0.5), + ) + assert captured.value.selected_size == 1 + assert captured.value.requested_size == 2 + + with pytest.raises(UndersizedBatchError) as all_zero: + select_local_penalized_batch( + pool, + 1, + _static_callback([-np.inf, -np.inf, -np.inf]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0.0), + ) + assert all_zero.value.selected_size == 0 + assert all_zero.value.remaining_candidate_count == 0 + assert all_zero.value.hard_valid_candidate_count == 3 + + +def test_log_epsilon_never_relaxes_hard_distance(): + pool = _pool([0.0, 0.4999999999995]) + with pytest.raises(UndersizedBatchError): + select_local_penalized_batch( + pool, + 2, + _static_callback([0.0, -0.1]), + LocalPenalizationConfig( + radius=0.1, + min_batch_distance=0.5, + epsilon=0.5, + ), + ) + + +def test_dimension_weight_validation_and_effect(): + pool = _pool([[0.0, 0.0], [0.2, 0.0], [0.0, 0.2]]) + config = LocalPenalizationConfig( + radius=0.1, + min_batch_distance=0, + dimension_weights=np.array([4.0, 1.0]), + ) + result = select_local_penalized_batch( + pool, 2, _static_callback([0.0, 0.0, 0.0]), config + ) + assert result.selected_pool_indices.tolist() == [0, 1] + with pytest.raises(ValueError, match="shape"): + select_local_penalized_batch( + pool, + 1, + _static_callback([0.0, 0.0, 0.0]), + LocalPenalizationConfig( + radius=0.1, + min_batch_distance=0, + dimension_weights=np.ones(3), + ), + ) + with pytest.raises(ValueError, match="strictly positive"): + LocalPenalizationConfig( + radius=0.1, + min_batch_distance=0, + dimension_weights=np.array([1.0, 0.0]), + ) + with pytest.raises(ValueError, match="non-boolean"): + LocalPenalizationConfig( + radius=0.1, + min_batch_distance=0, + dimension_weights=np.array([True, True]), + ) + + +def test_duplicate_pool_and_invalid_scores_are_rejected(): + duplicate_pool = CandidatePool( + grid_indices=np.array([[0], [0]]), + X_phys=np.array([[0.0], [0.0]]), + X_norm=np.array([[0.0], [0.0]]), + seed=1, + draws=2, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + with pytest.raises(ValueError, match="duplicate"): + select_local_penalized_batch( + duplicate_pool, + 1, + _static_callback([0.0, 0.0]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0), + ) + + duplicate_coordinates = CandidatePool( + grid_indices=np.array([[0], [1]]), + X_phys=np.array([[0.0], [0.0]]), + X_norm=np.array([[0.0], [0.0]]), + seed=1, + draws=2, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + with pytest.raises(ValueError, match="duplicate normalized"): + select_local_penalized_batch( + duplicate_coordinates, + 1, + _static_callback([0.0, 0.0]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0), + ) + + def bad_callback(remaining, selected): + del selected + return BaseScoreResult(np.full(remaining.size, np.nan)) + + with pytest.raises(ValueError, match="NaN"): + select_local_penalized_batch( + _pool([0.0, 1.0]), + 1, + bad_callback, + LocalPenalizationConfig(radius=0.1, min_batch_distance=0), + ) + + +def test_selector_rejects_out_of_range_references_and_nonfinite_physical_rows(): + with pytest.raises(ValueError, match=r"within \[0, 1\]"): + select_local_penalized_batch( + _pool([0.0, 1.0]), + 1, + _static_callback([0.0, -1.0]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0), + observed_pending_norm=np.array([[2.0]]), + ) + invalid = _pool([0.0, 1.0]) + object.__setattr__(invalid, "X_phys", np.array([[np.nan], [1.0]])) + with pytest.raises(ValueError, match="physical rows"): + select_local_penalized_batch( + invalid, + 1, + _static_callback([0.0, -1.0]), + LocalPenalizationConfig(radius=0.1, min_batch_distance=0), + ) diff --git a/tests/test_candidate_diagnostics.py b/tests/test_candidate_diagnostics.py new file mode 100644 index 0000000..4b184ec --- /dev/null +++ b/tests/test_candidate_diagnostics.py @@ -0,0 +1,132 @@ +from pathlib import Path + +import numpy as np +import pytest +from PIL import Image + +from mobo_kit.candidate_diagnostics import ( + boundary_flags, + grid_membership_mask, + pairwise_normalized_distances, + plot_candidate_pca, + plot_distance_heatmap, + plot_parallel_coordinates, + plot_selection_scores, + summarize_candidate_batch, +) +from mobo_kit.design import InputSpec, build_design + + +def _design(): + return build_design( + [ + InputSpec("a", 0.0, 2.0, 1.0), + InputSpec("b", 10.0, 20.0, 5.0), + ] + ) + + +def test_pairwise_normalized_distances_and_summary(): + selected = np.array([[0.0, 0.0], [0.3, 0.4], [1.0, 0.0]]) + matrix = pairwise_normalized_distances(selected) + assert matrix.shape == (3, 3) + assert matrix[0, 1] == pytest.approx(0.5) + assert np.allclose(matrix, matrix.T) + assert np.allclose(np.diag(matrix), 0.0) + + summary = summarize_candidate_batch( + selected, + observed_pending_norm=np.array([[0.0, 0.1]]), + X_phys=np.array([[0.0, 10.0], [1.0, 15.0], [2.0, 10.0]]), + design=_design(), + metadata={"method": "TEST_ONLY"}, + ) + triangle = matrix[np.triu_indices(3, k=1)] + assert summary.minimum_within_batch_distance == pytest.approx(triangle.min()) + assert summary.mean_within_batch_distance == pytest.approx(triangle.mean()) + assert summary.maximum_within_batch_distance == pytest.approx(triangle.max()) + assert summary.nearest_observed_pending_distance[0] == pytest.approx(0.1) + assert summary.duplicate_row_pairs == () + assert summary.grid_valid_rows.tolist() == [True, True, True] + assert summary.metadata == {"method": "TEST_ONLY"} + + +def test_duplicate_grid_and_boundary_checks(): + selected = np.array([[0.0, 0.0], [0.0, 0.0], [0.5, 1.0]]) + summary = summarize_candidate_batch(selected) + assert summary.duplicate_row_pairs == ((0, 1),) + assert boundary_flags(selected).tolist() == [ + [True, True], + [True, True], + [False, True], + ] + valid = grid_membership_mask( + np.array([[0.0, 10.0], [1.5, 15.0], [2.0, 19.0]]), _design() + ) + assert valid.tolist() == [True, False, False] + + +def test_weight_validation_and_weighted_distance(): + X = np.array([[0.0, 0.0], [1.0, 1.0]]) + matrix = pairwise_normalized_distances(X, dimension_weights=np.array([1.0, 4.0])) + assert matrix[0, 1] == pytest.approx(np.sqrt(5.0)) + with pytest.raises(ValueError, match="strictly positive"): + pairwise_normalized_distances(X, dimension_weights=np.array([1.0, 0.0])) + with pytest.raises(ValueError, match="shape"): + pairwise_normalized_distances(X, dimension_weights=np.ones(3)) + + +def test_singleton_summary_has_explicit_empty_within_batch_statistics(): + summary = summarize_candidate_batch(np.array([[0.2, 0.8]])) + assert summary.minimum_within_batch_distance is None + assert summary.mean_within_batch_distance is None + assert summary.maximum_within_batch_distance is None + assert np.isnan(summary.nearest_observed_pending_distance[0]) + + +def test_plotting_helpers_write_headless_pngs(tmp_path: Path): + observed = np.array([[0.0, 0.0], [0.5, 0.3], [0.9, 1.0]]) + pool = np.linspace(0.0, 1.0, 40).reshape(20, 2) + selected = np.array([[0.1, 0.8], [0.8, 0.2], [0.5, 0.5]]) + watermark = "DEBUG ONLY - TEST PLOT" + paths = [ + plot_candidate_pca( + observed, + selected, + tmp_path / "pca.png", + pool_norm=pool, + watermark=watermark, + ), + plot_parallel_coordinates( + selected, + ["a", "b"], + tmp_path / "parallel.png", + watermark=watermark, + ), + plot_distance_heatmap(selected, tmp_path / "distance.png", watermark=watermark), + plot_selection_scores( + [1, 2, 3], + [-1.0, -1.2, -1.4], + [-1.0, -1.8, -2.1], + tmp_path / "scores.png", + watermark=watermark, + ), + ] + for path in paths: + assert path.exists() + assert path.stat().st_size > 1000 + with Image.open(path) as image: + assert image.info["Description"] == watermark + + +def test_diagnostics_reject_shape_and_partial_grid_context(): + with pytest.raises(ValueError, match="shape"): + pairwise_normalized_distances(np.ones(3)) + with pytest.raises(ValueError, match="supplied together"): + summarize_candidate_batch(np.ones((2, 2)), X_phys=np.ones((2, 2)), design=None) + with pytest.raises(ValueError, match="same row count"): + summarize_candidate_batch( + np.ones((2, 2)), X_phys=np.ones((1, 2)), design=_design() + ) + with pytest.raises(ValueError, match="duplicate_atol"): + summarize_candidate_batch(np.ones((2, 2)), duplicate_atol=-1) diff --git a/tests/test_candidate_pool.py b/tests/test_candidate_pool.py new file mode 100644 index 0000000..18ba27e --- /dev/null +++ b/tests/test_candidate_pool.py @@ -0,0 +1,150 @@ +import numpy as np +import pytest + +from mobo_kit.candidate_pool import ( + CandidatePoolSamplingError, + physical_rows_to_grid_indices, + sample_discrete_candidate_pool, +) +from mobo_kit.design import InputSpec, build_design + + +def _design(): + return build_design( + [ + InputSpec("a", 0.0, 4.0, 1.0), + InputSpec("b", 10.0, 20.0, 5.0), + InputSpec("c", -1.0, 1.0, 1.0), + ] + ) + + +def test_exact_size_seeded_order_grid_membership_and_normalization(): + design = _design() + first = sample_discrete_candidate_pool(design, 20, seed=12) + repeat = sample_discrete_candidate_pool(design, 20, seed=12) + different = sample_discrete_candidate_pool(design, 20, seed=13) + assert first.size == 20 + assert np.array_equal(first.grid_indices, repeat.grid_indices) + assert np.array_equal(first.X_phys, repeat.X_phys) + assert np.array_equal(first.X_norm, repeat.X_norm) + assert not np.array_equal(first.grid_indices, different.grid_indices) + assert np.unique(first.grid_indices, axis=0).shape[0] == 20 + assert np.all(first.grid_indices >= 0) + assert np.all(first.grid_indices < np.array([5, 3, 3])) + assert np.all((first.X_norm >= 0) & (first.X_norm <= 1)) + assert np.array_equal( + physical_rows_to_grid_indices(first.X_phys, design), first.grid_indices + ) + + +def test_observed_pending_and_explicit_avoid_are_excluded(): + design = _design() + exclusions = np.array([[0.0, 10.0, -1.0], [1.0, 15.0, 0.0], [2.0, 20.0, 1.0]]) + pool = sample_discrete_candidate_pool( + design, + 25, + seed=4, + observed_phys=exclusions[:1], + pending_phys=exclusions[1:2], + avoid_phys=exclusions[2:], + ) + excluded_indices = physical_rows_to_grid_indices(exclusions, design) + pool_set = {tuple(row) for row in pool.grid_indices} + assert not any(tuple(row) in pool_set for row in excluded_indices) + + +def test_constraints_run_in_physical_space_and_are_fail_closed(): + design = _design() + calls = [] + + def require_even_first_input(X_phys, supplied_design): + calls.append(X_phys.copy()) + assert supplied_design is design + return (X_phys[:, 0] % 2) == 0 + + pool = sample_discrete_candidate_pool( + design, + 15, + seed=3, + row_constraints=[require_even_first_input], + max_draws=500, + ) + assert calls + assert np.all(pool.X_phys[:, 0] % 2 == 0) + assert pool.rejected_constraint > 0 + + +def test_impossible_request_raises_structured_error(): + design = build_design([InputSpec("x", 0, 1, 1)]) + with pytest.raises(CandidatePoolSamplingError) as captured: + sample_discrete_candidate_pool( + design, 2, seed=1, observed_phys=np.array([[0.0]]) + ) + error = captured.value + assert error.requested == 2 + assert error.accepted == 0 + assert "exceeds" in error.reason + + +def test_max_draws_failure_reports_rejections(): + design = build_design([InputSpec("x", 0, 4, 1)]) + + def reject_all(X_phys, supplied_design): + del supplied_design + return np.zeros(X_phys.shape[0], dtype=bool) + + with pytest.raises(CandidatePoolSamplingError) as captured: + sample_discrete_candidate_pool( + design, + 1, + seed=2, + row_constraints=[reject_all], + max_draws=5, + ) + assert captured.value.draws == 5 + assert captured.value.rejected_constraint > 0 + + +def test_draw_statistics_on_known_two_point_grid(): + design = build_design([InputSpec("x", 0, 1, 1)]) + pool = sample_discrete_candidate_pool(design, 2, seed=0, max_draws=10) + # NumPy Generator seed 0 draws 1, 1, 1, 0 for the first four integers. + assert pool.grid_indices[:, 0].tolist() == [1, 0] + assert pool.draws == 4 + assert pool.rejected_duplicate == 2 + assert pool.rejected_avoid == 0 + assert pool.rejected_constraint == 0 + + excluded = sample_discrete_candidate_pool( + design, + 1, + seed=0, + observed_phys=np.array([[1.0]]), + max_draws=10, + ) + assert excluded.grid_indices[:, 0].tolist() == [0] + assert excluded.draws == 4 + assert excluded.rejected_duplicate == 2 + assert excluded.rejected_avoid == 1 + assert excluded.rejected_constraint == 0 + + +def test_off_grid_exclusions_fail_instead_of_using_fuzzy_matching(): + with pytest.raises(ValueError, match="off-grid"): + sample_discrete_candidate_pool( + _design(), 2, seed=1, avoid_phys=np.array([[0.25, 10.0, 0.0]]) + ) + with pytest.raises(ValueError, match="off-grid"): + physical_rows_to_grid_indices(np.array([[1e-9, 10.0, 0.0]]), _design()) + + +def test_sampler_never_calls_cartesian_product_allocators(monkeypatch): + def forbidden(*args, **kwargs): + del args, kwargs + raise AssertionError("full Cartesian allocation was attempted") + + monkeypatch.setattr(np, "meshgrid", forbidden) + monkeypatch.setattr(np, "indices", forbidden) + pool = sample_discrete_candidate_pool(_design(), 10, seed=9) + assert pool.size == 10 diff --git a/tests/test_csv_parser.py b/tests/test_csv_parser.py new file mode 100644 index 0000000..c379510 --- /dev/null +++ b/tests/test_csv_parser.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +import pandas as pd +import pytest +import yaml + +from mobo_kit.utils import ( + ParsedCampaignCSV, + csv_to_config, + load_csv, + parse_campaign_csv, + split_XY, +) + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +EXAMPLE_CAMPAIGN_CSV = REPOSITORY_ROOT / "data" / "processed" / "configCSV_example.csv" + + +def _campaign_text(*, blank_separator: bool = True) -> str: + separator_header = "," if blank_separator else "" + separator_cell = "," if blank_separator else "" + blank_row = ",,,,,\n" if blank_separator else "" + return ( + f",x_speed,x_time{separator_header},yield,stability\n" + f"units,rpm,s{separator_cell},%,h\n" + f"start,1000,5{separator_cell},,\n" + f"stop,2000,15{separator_cell},,\n" + f"step,500,5{separator_cell},,\n" + f"{blank_row}" + f",1000,5{separator_cell},1.5,10\n" + f",1500,10{separator_cell},2.5,11\n" + ) + + +def _write_text(path: Path, text: str, encoding: str = "utf-8") -> Path: + path.write_bytes(text.encode(encoding)) + return path + + +def test_parser_detects_metadata_and_preserves_first_experiment(tmp_path: Path) -> None: + path = _write_text(tmp_path / "campaign.csv", _campaign_text()) + + parsed = parse_campaign_csv(path) + + assert isinstance(parsed, ParsedCampaignCSV) + assert parsed.input_columns == ["x_speed", "x_time"] + assert parsed.objective_columns == ["yield", "stability"] + assert parsed.metadata_row_count == 5 + assert parsed.duplicate_headers == {} + assert parsed.config["constraints"] == [] + assert parsed.data.iloc[0].to_dict() == { + "x_speed": "1000", + "x_time": "5", + "yield": "1.5", + "stability": "10", + } + assert len(parsed.data) == 2 + + +def test_repository_example_retains_its_first_experiment() -> None: + parsed = parse_campaign_csv(EXAMPLE_CAMPAIGN_CSV) + + assert parsed.metadata_row_count == 5 + assert parsed.input_columns == [ + "speed_inorg", + "speed_org", + "inkfl_inorg", + "inkfl_org", + "conc_inorg", + "conc_org", + "temperature_c", + "absolute_humidity", + ] + assert parsed.objective_columns == ["PCE", "Stability", "Repeatability"] + assert len(parsed.data) == 12 + assert parsed.data.iloc[0]["speed_inorg"] == "0.58" + + +def test_parser_accepts_no_blank_separator_row_or_column(tmp_path: Path) -> None: + path = _write_text( + tmp_path / "no_separator.csv", _campaign_text(blank_separator=False) + ) + + parsed = parse_campaign_csv(path) + + assert parsed.metadata_row_count == 4 + assert parsed.objective_columns == ["yield", "stability"] + assert parsed.data.iloc[0]["x_speed"] == "1000" + + +def test_load_csv_returns_only_experimental_rows(tmp_path: Path) -> None: + path = _write_text(tmp_path / "campaign.csv", _campaign_text()) + + data = load_csv(path) + + assert list(data.columns) == ["x_speed", "x_time", "yield", "stability"] + assert len(data) == 2 + assert "units" not in data.astype(str).to_numpy() + + +def test_utf8_bom_is_supported(tmp_path: Path) -> None: + path = tmp_path / "bom.csv" + path.write_bytes(b"\xef\xbb\xbf" + _campaign_text().encode("utf-8")) + + parsed = parse_campaign_csv(path) + + assert parsed.encoding == "utf-8-sig" + assert parsed.input_columns[0] == "x_speed" + + +def test_cp1252_is_supported(tmp_path: Path) -> None: + text = _campaign_text().replace("rpm", "\N{DEGREE SIGN}C") + path = _write_text(tmp_path / "cp1252.csv", text, encoding="cp1252") + + parsed = parse_campaign_csv(path) + + assert parsed.encoding == "cp1252" + assert parsed.config["inputs"][0]["unit"] == "\N{DEGREE SIGN}C" + + +def test_latin1_is_used_when_cp1252_cannot_decode(tmp_path: Path) -> None: + payload = _campaign_text().replace("rpm", "UNIT_MARKER").encode("ascii") + path = tmp_path / "latin1.csv" + path.write_bytes(payload.replace(b"UNIT_MARKER", b"\x81")) + + parsed = parse_campaign_csv(path) + + assert parsed.encoding == "latin-1" + assert parsed.config["inputs"][0]["unit"] == "\x81" + + +def test_duplicate_headers_are_rejected_before_pandas_mangling(tmp_path: Path) -> None: + text = _campaign_text().replace(",yield,stability", ",yield,yield", 1) + path = _write_text(tmp_path / "duplicates.csv", text) + + with pytest.raises(ValueError, match=r"Duplicate CSV headers.*columns \[5, 6\]"): + parse_campaign_csv(path) + + +def test_plain_data_csv_is_rejected_explicitly(tmp_path: Path) -> None: + path = _write_text(tmp_path / "plain.csv", "x,y\n1,2\n") + + with pytest.raises(ValueError, match="Plain data CSVs are not supported"): + parse_campaign_csv(path) + + +@pytest.mark.parametrize( + ("old", "new", "message"), + [ + ("start,1000,5", "start,not-a-number,5", "Malformed start metadata"), + ("step,500,5", "step,,5", "Incomplete numeric metadata"), + ], +) +def test_malformed_or_missing_numeric_metadata_is_rejected( + tmp_path: Path, old: str, new: str, message: str +) -> None: + path = _write_text( + tmp_path / "bad_metadata.csv", _campaign_text().replace(old, new) + ) + + with pytest.raises(ValueError, match=message): + parse_campaign_csv(path) + + +def test_expected_objectives_are_validated_and_ordered(tmp_path: Path) -> None: + path = _write_text(tmp_path / "campaign.csv", _campaign_text()) + + parsed = parse_campaign_csv(path, expected_objectives=["stability", "yield"]) + assert parsed.objective_columns == ["stability", "yield"] + + with pytest.raises(ValueError, match="Missing objective columns.*missing_score"): + parse_campaign_csv(path, expected_objectives=["yield", "missing_score"]) + + +def test_missing_objectives_and_empty_experiment_section_are_clear( + tmp_path: Path, +) -> None: + no_objective = ",x\nunits,rpm\nstart,0\nstop,1\nstep,0.5\n\n,0\n" + path = _write_text(tmp_path / "no_objective.csv", no_objective) + with pytest.raises(ValueError, match="no named objective columns"): + parse_campaign_csv(path) + + empty = ( + ",x_speed,x_time,,yield,stability\n" + "units,rpm,s,,%,h\n" + "start,1000,5,,,\n" + "stop,2000,15,,,\n" + "step,500,5,,,\n" + ",,,,,\n" + ) + empty_path = _write_text(tmp_path / "empty.csv", empty) + with pytest.raises(ValueError, match="empty experimental section"): + parse_campaign_csv(empty_path) + + +def test_csv_to_config_is_opt_in_for_output_and_constraints( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + path = _write_text(tmp_path / "campaign.csv", _campaign_text()) + monkeypatch.chdir(tmp_path) + + config = csv_to_config(path) + + assert config["constraints"] == [] + assert not (tmp_path / "configs").exists() + + output_path = tmp_path / "generated" / "campaign.yaml" + written = csv_to_config(path, output_path) + assert yaml.safe_load(output_path.read_text(encoding="utf-8")) == written + + +def _design() -> SimpleNamespace: + return SimpleNamespace(names=["x_speed", "x_time"]) + + +def _model_config() -> dict: + return {"objectives": {"names": ["yield", "stability"]}} + + +def test_split_xy_returns_named_numeric_dataframes() -> None: + data = pd.DataFrame( + { + "x_speed": ["1000", "1500"], + "x_time": ["5", "10"], + "yield": ["1.5", "2.5"], + "stability": ["10", "11"], + }, + index=["sample-a", "sample-b"], + ) + + X, Y = split_XY(data, _design(), _model_config()) + + assert isinstance(X, pd.DataFrame) + assert isinstance(Y, pd.DataFrame) + assert X.columns.tolist() == ["x_speed", "x_time"] + assert Y.columns.tolist() == ["yield", "stability"] + assert X.index.tolist() == ["sample-a", "sample-b"] + assert X.dtypes.tolist() == ["float64", "float64"] + assert Y.iloc[0].tolist() == [1.5, 10.0] + + +def test_split_xy_rejects_missing_columns() -> None: + data = pd.DataFrame({"x_speed": [1], "yield": [2], "stability": [3]}) + + with pytest.raises(KeyError, match="missing inputs.*x_time"): + split_XY(data, _design(), _model_config()) + + +@pytest.mark.parametrize( + ("data", "message"), + [ + ( + pd.DataFrame( + { + "x_speed": [1000], + "x_time": [5], + "yield": [pd.NA], + "stability": [pd.NA], + } + ), + "All objective values are blank", + ), + ( + pd.DataFrame( + { + "x_speed": [1000, 1500], + "x_time": [5, 10], + "yield": [1.5, pd.NA], + "stability": [10, pd.NA], + } + ), + "Objective values are blank for rows", + ), + ( + pd.DataFrame( + { + "x_speed": [1000], + "x_time": [5], + "yield": [1.5], + "stability": [pd.NA], + } + ), + "Partially completed objective rows", + ), + ( + pd.DataFrame( + { + "x_speed": ["invalid"], + "x_time": [5], + "yield": [1.5], + "stability": [10], + } + ), + "Input model data contains", + ), + ( + pd.DataFrame( + { + "x_speed": [1000], + "x_time": [5], + "yield": ["invalid"], + "stability": [10], + } + ), + "Objective model data contains", + ), + ], +) +def test_split_xy_rejects_incomplete_or_nonnumeric_model_rows( + data: pd.DataFrame, message: str +) -> None: + with pytest.raises(ValueError, match=message): + split_XY(data, _design(), _model_config()) diff --git a/tests/test_d2d_baseline.py b/tests/test_d2d_baseline.py new file mode 100644 index 0000000..27bc503 --- /dev/null +++ b/tests/test_d2d_baseline.py @@ -0,0 +1,338 @@ +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +import torch +import yaml +import botorch +import gpytorch + +import mobo_kit +from mobo_kit.cli import main as cli_main +from mobo_kit.constraints import apply_row_constraints, constraints_from_config +from mobo_kit.design import InputSpec, build_design, build_design_from_config +from mobo_kit.lhs import lhs_dataframe_optimized +from mobo_kit.main import ( + _validate_candidate_batch, + generate_initial_experiments, + run_mobo_experiment, +) +from mobo_kit.utils import select_device + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +CANONICAL_D2D_CONFIG = REPOSITORY_ROOT / "configs" / "FA0.9CS0.1PbI3_260407_Config.yaml" + +EXPECTED_INPUTS = [ + ("speed_1", "rpm", 1000.0, 6000.0, 500.0, 11), + ("time_1", "s", 5.0, 50.0, 5.0, 10), + ("speed_2", "rpm", 0.0, 5000.0, 500.0, 11), + ("time_2", "s", 10.0, 60.0, 5.0, 11), + ("precur_conc", "M", 1.0, 2.0, 0.05, 21), + ("precur_vol", "uL", 40.0, 200.0, 10.0, 17), + ("anneal_temp", "C", 100.0, 185.0, 5.0, 18), + ("anneal_time", "min", 10.0, 60.0, 5.0, 11), + ("anti_vol", "uL", 100.0, 200.0, 5.0, 21), + ("anti_time", "s", 9.0, 25.0, 2.0, 9), +] + + +def _load_canonical_config() -> dict: + with CANONICAL_D2D_CONFIG.open("r", encoding="utf-8") as config_file: + return yaml.safe_load(config_file) + + +def _assert_exact_grid_membership(frame: pd.DataFrame, design) -> None: + for column, grid in zip(design.names, design.var_list): + assert np.all(np.isin(frame[column].to_numpy(), grid)) + + +def test_canonical_d2d_yaml_has_exact_input_contract(): + config = _load_canonical_config() + design = build_design_from_config(config) + + assert config["campaign"] == { + "name": "D2D_FA0.9Cs0.1PbI3", + "status": "baseline_only", + } + assert config["objectives"]["names"] == [] + assert config["constraints"] == [] + assert design.names == [item[0] for item in EXPECTED_INPUTS] + assert design.units == [item[1] for item in EXPECTED_INPUTS] + assert np.array_equal( + design.lowers, + np.asarray([item[2] for item in EXPECTED_INPUTS]), + ) + assert np.array_equal( + design.uppers, + np.asarray([item[3] for item in EXPECTED_INPUTS]), + ) + assert np.array_equal( + design.steps, + np.asarray([item[4] for item in EXPECTED_INPUTS]), + ) + + +def test_canonical_d2d_grids_have_exact_membership_and_cardinality(): + design = build_design_from_config(_load_canonical_config()) + + for grid, expected in zip(design.var_list, EXPECTED_INPUTS): + _, _, start, stop, step, cardinality = expected + expected_grid = np.round( + start + step * np.arange(cardinality, dtype=float), + 6, + ) + assert len(grid) == cardinality + assert np.array_equal(grid, expected_grid) + assert grid[0] == start + assert grid[-1] == stop + + assert int(np.prod([len(grid) for grid in design.var_list], dtype=np.int64)) == ( + 177_816_994_740 + ) + + +def test_canonical_d2d_lhs_is_deterministic_unique_and_ten_dimensional(): + design = build_design_from_config(_load_canonical_config()) + kwargs = { + "design": design, + "n": 20, + "seed": 42, + "max_abs_corr": 0.32, + "max_attempts": 10, + "samples_per_attempt": 100, + "subset_tries": 2000, + } + + first = lhs_dataframe_optimized(**kwargs) + second = lhs_dataframe_optimized(**kwargs) + + pd.testing.assert_frame_equal(first, second) + assert first.shape == (20, 10) + assert list(first.columns) == design.names + assert not first.duplicated().any() + _assert_exact_grid_membership(first, design) + + correlations = np.abs(np.corrcoef(first.to_numpy(), rowvar=False)) + np.fill_diagonal(correlations, 0.0) + assert float(np.max(correlations)) <= 0.32 + + +def test_canonical_constraints_default_to_empty(): + config = _load_canonical_config() + design = build_design_from_config(config) + + assert constraints_from_config(config, design) == [] + + +def test_one_explicit_supported_constraint_builds_and_applies(): + config = { + "inputs": [ + { + "name": "absolute_humidity", + "unit": "g/m^3", + "start": 0, + "stop": 30, + "step": 1, + }, + { + "name": "temperature_c", + "unit": "C", + "start": 20, + "stop": 30, + "step": 1, + }, + ], + "constraints": [ + { + "clausius_clapeyron": True, + "ah_col": "absolute_humidity", + "temp_c_col": "temperature_c", + } + ], + } + design = build_design_from_config(config) + constraints = constraints_from_config(config, design) + rows = np.asarray([[10.0, 20.0], [30.0, 20.0]]) + + assert len(constraints) == 1 + assert np.array_equal( + apply_row_constraints(rows, design, constraints), + np.asarray([True, False]), + ) + + +@pytest.mark.parametrize( + ("ah_col", "temp_col", "missing"), + [ + ("absolute_humidity", "anneal_temp", "absolute_humidity"), + ("anti_vol", "temperature_c", "temperature_c"), + ], +) +def test_constraint_references_to_missing_columns_fail_clearly( + ah_col: str, + temp_col: str, + missing: str, +): + config = _load_canonical_config() + config["constraints"] = [ + { + "clausius_clapeyron": True, + "ah_col": ah_col, + "temp_c_col": temp_col, + } + ] + design = build_design_from_config(config) + + with pytest.raises(KeyError, match=re.escape(f"column '{missing}'")): + constraints_from_config(config, design) + + +@pytest.mark.parametrize( + ("arguments", "expected_tokens"), + [ + (["--help"], ["generate", "run"]), + ( + ["generate", "--help"], + ["--config", "--n-samples", "--out", "--max-corr"], + ), + ( + ["run", "--help"], + ["--csv", "--device", "--propose-candidates", "--reference-point"], + ), + ], +) +def test_cli_help_surfaces_are_importable( + arguments, + expected_tokens, + monkeypatch, + capsys, +): + monkeypatch.setattr(sys, "argv", ["mobo-kit", *arguments]) + with pytest.raises(SystemExit) as exit_info: + cli_main() + captured = capsys.readouterr() + output = captured.out + captured.err + + assert exit_info.value.code == 0 + assert "Traceback" not in output + for token in expected_tokens: + assert token in output + + +def test_package_import_and_cpu_smoke(): + assert Path(mobo_kit.__file__).resolve().is_file() + assert torch.__version__ + assert gpytorch.__version__ + assert botorch.__version__ + cpu = select_device("cpu") + tensor = torch.tensor([1.0, 2.0], dtype=torch.float64, device=cpu) + + assert cpu.type == "cpu" + assert tensor.device.type == "cpu" + assert tensor.sum().item() == 3.0 + + +def test_production_source_and_notebooks_have_no_personal_absolute_paths(): + forbidden_fragments = ( + "/Users/", + "C:\\Users\\", + "C:\\\\Users\\\\", + "Dropbox/Buonassisi-Group", + "Dropbox\\Buonassisi-Group", + "Dropbox\\\\Buonassisi-Group", + ) + inspected_paths = [ + *sorted((REPOSITORY_ROOT / "src").rglob("*.py")), + *sorted((REPOSITORY_ROOT / "notebooks").rglob("*.ipynb")), + ] + + findings = [] + for path in inspected_paths: + text = path.read_text(encoding="utf-8", errors="replace") + for fragment in forbidden_fragments: + if fragment in text: + findings.append(f"{path.relative_to(REPOSITORY_ROOT)}: {fragment}") + + assert findings == [] + + +def test_candidate_batch_validation_fails_closed(): + design = build_design([InputSpec("x", 0, 1, 1), InputSpec("y", 0, 1, 1)]) + observed = pd.DataFrame([[0.0, 0.0]], columns=design.names) + + valid = _validate_candidate_batch( + {"X_phys": [[0.0, 1.0], [1.0, 0.0]]}, + design, + observed, + batch_size=2, + ) + np.testing.assert_array_equal(valid, [[0.0, 1.0], [1.0, 0.0]]) + + with pytest.raises(ValueError, match="expected shape"): + _validate_candidate_batch( + {"X_phys": [[0.0, 1.0]]}, design, observed, batch_size=2 + ) + with pytest.raises(ValueError, match="duplicate snapped recipes"): + _validate_candidate_batch( + {"X_phys": [[0.0, 1.0], [0.0, 1.0]]}, + design, + observed, + batch_size=2, + ) + with pytest.raises(ValueError, match="repeats observed recipes"): + _validate_candidate_batch( + {"X_phys": [[0.0, 0.0], [1.0, 1.0]]}, + design, + observed, + batch_size=2, + ) + with pytest.raises(ValueError, match="off-grid"): + _validate_candidate_batch( + {"X_phys": [[0.0, 0.5], [1.0, 1.0]]}, + design, + observed, + batch_size=2, + ) + + +def test_explicit_missing_config_path_fails_instead_of_inference(tmp_path): + missing_config = tmp_path / "missing.yaml" + + with pytest.raises(FileNotFoundError, match="Configuration file not found"): + run_mobo_experiment( + csv_path="not-read-before-config-validation.csv", + config_path=str(missing_config), + verbose=False, + device="cpu", + ) + + +def test_generate_baseline_is_safe_and_does_not_propose_candidates(tmp_path): + output_path = tmp_path / "nested" / "d2d_round_0.csv" + + result = generate_initial_experiments( + config_path=str(CANONICAL_D2D_CONFIG), + n_samples=20, + save_path=str(output_path), + seed=42, + verbose=False, + max_abs_corr=0.32, + max_attempts=10, + ) + generated = pd.read_csv(output_path) + design = build_design_from_config(_load_canonical_config()) + + assert result["status"] == "success" + assert result["n_samples"] == 20 + assert result["variables"] == design.names + assert result["constraints_applied"] is False + assert generated.shape == (20, 10) + assert list(generated.columns) == design.names + assert not generated.duplicated().any() + _assert_exact_grid_membership(generated, design) diff --git a/tests/test_d2d_campaign.py b/tests/test_d2d_campaign.py new file mode 100644 index 0000000..f8f7846 --- /dev/null +++ b/tests/test_d2d_campaign.py @@ -0,0 +1,595 @@ +from __future__ import annotations + +from copy import deepcopy +import hashlib +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +import pytest +import torch +import yaml + +from mobo_kit.candidate_pool import physical_rows_to_grid_indices +from mobo_kit.d2d_campaign import ( + D2D_DEBUG_WATERMARK, + D2D_INPUT_COLUMNS, + D2D_OBJECTIVE_COLUMNS, + D2D_OBJECTIVE_NAMES, + D2D_REFERENCE_POINT_UTILITY, + D2DDebugConfigError, + ReplicateAggregationResult, + aggregate_replicate_objectives, + build_d2d_objective_transform, + combine_r0_and_aggregated_r1, + expand_candidates_to_replicates, + load_d2d_debug_config, + prepare_d2d_training_data, +) + + +REPO_ROOT = Path(__file__).resolve().parents[1] +DEBUG_CONFIG_PATH = REPO_ROOT / "configs" / "d2d_step2b_debug.yaml" +WORKBOOK_INPUT_COLUMNS = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol (uL)", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", +) + + +def _load_raw_config(*, materialize: bool = True) -> dict[str, Any]: + raw = yaml.safe_load(DEBUG_CONFIG_PATH.read_text(encoding="utf-8")) + assert isinstance(raw, dict) + if materialize: + raw["template_only"] = False + raw["workbook"]["expected_sha256"] = hashlib.sha256( + b"public synthetic workbook identity" + ).hexdigest() + return raw + + +def _write_config( + tmp_path: Path, raw: dict[str, Any], *, name: str = "sanitized_debug.yaml" +) -> Path: + path = tmp_path / name + path.write_text(yaml.safe_dump(raw, sort_keys=False), encoding="utf-8") + return path + + +@pytest.fixture +def config(tmp_path: Path): + return load_d2d_debug_config(_write_config(tmp_path, _load_raw_config())) + + +def _synthetic_r0_frame(config) -> pd.DataFrame: + rows: list[dict[str, float | int]] = [] + workbook_columns = dict(zip(D2D_INPUT_COLUMNS, WORKBOOK_INPUT_COLUMNS)) + exceptions = { + (exception.sample_id, exception.input_name): exception.observed_value + for exception in config.off_grid_exceptions + } + for sample_index, sample_id in enumerate(config.expected_sample_ids): + row: dict[str, float | int] = {"Sample number": sample_id} + for dimension, (column, grid) in enumerate( + zip(WORKBOOK_INPUT_COLUMNS, config.design.var_array) + ): + grid_index = (sample_index * (dimension + 1) + dimension) % len(grid) + row[column] = float(grid[grid_index]) + for input_name in D2D_INPUT_COLUMNS: + key = (sample_id, input_name) + if key in exceptions: + row[workbook_columns[input_name]] = exceptions[key] + row["Uniformity score"] = 0.25 + 0.02 * sample_index + row["Optoelectronic score"] = -2.0 + 0.05 * sample_index + row["Thickness score"] = 0.35 + 0.015 * sample_index + rows.append(row) + return pd.DataFrame(rows) + + +def _five_synthetic_candidates(config) -> pd.DataFrame: + rows: list[dict[str, Any]] = [] + for candidate_index in range(5): + row: dict[str, Any] = {"campaign_id": config.raw["campaign_id"]} + for dimension, (name, grid) in enumerate( + zip(D2D_INPUT_COLUMNS, config.design.var_array) + ): + if dimension == 0: + grid_index = candidate_index + else: + grid_index = len(grid) - 1 + row[name] = float(grid[grid_index]) + rows.append(row) + return pd.DataFrame(rows) + + +def _measured_replicates(config) -> tuple[pd.DataFrame, pd.DataFrame]: + candidates = _five_synthetic_candidates(config) + records = expand_candidates_to_replicates(candidates) + for candidate_index, candidate_id in enumerate( + records["candidate_id"].drop_duplicates(), start=1 + ): + mask = records["candidate_id"] == candidate_id + records.loc[mask, "Uniformity score"] = ( + np.asarray([0.10, 0.15, 0.20]) + candidate_index / 10.0 + ) + records.loc[mask, "Optoelectronic score"] = ( + np.asarray([-2.0, -1.0, 0.0]) + candidate_index + ) + records.loc[mask, "Thickness score"] = ( + np.asarray([0.30, 0.40, 0.50]) + candidate_index / 20.0 + ) + return candidates, records + + +def test_debug_config_resolves_materialized_public_contract(config) -> None: + assert config.raw["run_mode"] == "debug" + assert config.raw["debug_run_authorized"] is True + assert config.raw["approved_for_production"] is False + assert config.raw["approved_for_experiment"] is False + assert config.workbook_profile == "d2d_summary_v3_scores" + assert config.workbook_sheet == "Sheet1" + assert config.expected_content_range == "A1:AI20" + assert config.expected_sample_ids == tuple(range(1001, 1016)) + assert tuple(config.design.names) == D2D_INPUT_COLUMNS + assert config.control_sample_ids == (1001,) + assert config.include_control_in_debug_model is True + assert config.r1_batch_size == 5 + assert config.replicates_per_condition == 3 + assert config.debug_watermark == D2D_DEBUG_WATERMARK + assert config.output_root == "local_outputs/d2d_step2b_debug" + np.testing.assert_array_equal( + config.reference_point_utility, D2D_REFERENCE_POINT_UTILITY + ) + + +def test_tracked_debug_config_is_a_nonrunnable_public_template( + tmp_path: Path, +) -> None: + raw = _load_raw_config(materialize=False) + with pytest.raises(D2DDebugConfigError, match="public template"): + load_d2d_debug_config(_write_config(tmp_path, raw)) + + +def test_public_template_requires_explicit_synthetic_resolution() -> None: + config = load_d2d_debug_config(DEBUG_CONFIG_PATH, allow_public_template=True) + + assert len(config.expected_workbook_sha256) == 64 + assert config.expected_workbook_sha256 != ("REQUIRED_IN_IGNORED_PRIVATE_CONFIG") + exception = config.off_grid_exceptions[0] + dimension = config.design.names.index(exception.input_name) + grid = config.design.var_array[dimension] + assert exception.observed_value == pytest.approx((grid[0] + grid[1]) / 2.0) + + +@pytest.mark.parametrize( + ("field", "invalid_value"), + [ + ("run_mode", "production"), + ("debug_run_authorized", False), + ("debug_run_authorized", 1), + ("approved_for_production", True), + ("approved_for_production", 0), + ("approved_for_experiment", True), + ("approved_for_experiment", 0), + ], +) +def test_debug_config_rejects_weakened_or_nonboolean_safety_flags( + tmp_path: Path, field: str, invalid_value: Any +) -> None: + raw = _load_raw_config() + raw[field] = invalid_value + + with pytest.raises(D2DDebugConfigError, match=field): + load_d2d_debug_config(_write_config(tmp_path, raw)) + + +def test_debug_config_rejects_contract_drift(tmp_path: Path) -> None: + mutations = [ + ("schema version", lambda raw: raw.update(schema_version="production-v1")), + ("campaign id", lambda raw: raw.update(campaign_id="")), + ( + "content range", + lambda raw: raw["workbook"].update(expected_content_range="A1:AB16"), + ), + ( + "objective direction", + lambda raw: raw["objectives"][0].update(direction="minimize"), + ), + ( + "objective transform", + lambda raw: raw["objectives"][1].update(utility_transform="affine"), + ), + ( + "objective source", + lambda raw: raw["objectives"][2].update(excel_column="AC"), + ), + ( + "approved input grid", + lambda raw: raw["inputs"][0].update(stop=6500), + ), + ( + "support formula", + lambda raw: raw["objectives"][0].update(support_formula="L * N"), + ), + ( + "thickness target", + lambda raw: raw["objectives"][2].update(target_nm=700.0), + ), + ( + "score validation policy", + lambda raw: raw["qc_policy"].update(thickness_mismatch="warn"), + ), + ( + "safe output root", + lambda raw: raw["outputs"].update(root="docs/private_candidates"), + ), + ("baseline beta", lambda raw: raw["r1"].update(beta=9.0)), + ( + "local baseline", + lambda raw: raw["local_penalization"].update(radius=0.35), + ), + ("reproducibility seed", lambda raw: raw["reproducibility"].update(seed=137)), + ( + "reference point", + lambda raw: raw.update(reference_point_utility=[0.0, -10.0, -0.01]), + ), + ("constraints", lambda raw: raw.update(constraints=["synthetic constraint"])), + ] + for index, (label, mutate) in enumerate(mutations): + raw = deepcopy(_load_raw_config()) + mutate(raw) + with pytest.raises((D2DDebugConfigError, ValueError)): + load_d2d_debug_config( + _write_config(tmp_path, raw, name=f"invalid-{index}-{label}.yaml") + ) + + +def test_objective_contract_is_named_ordered_identity_and_maximize(config) -> None: + transform = build_d2d_objective_transform() + + assert transform.names == D2D_OBJECTIVE_NAMES + assert ( + tuple(spec.source_column for spec in transform.specs) == D2D_OBJECTIVE_COLUMNS + ) + assert tuple(spec.goal for spec in transform.specs) == ("maximize",) * 3 + assert tuple(spec.transform for spec in transform.specs) == ("identity",) * 3 + values = torch.tensor([[0.7, -1.25, 0.9]], dtype=torch.float64) + transformed = transform(values) + assert torch.equal(transformed, values) + np.testing.assert_array_equal( + config.reference_point_utility, np.asarray([-0.01, -10.0, -0.01]) + ) + + +def test_training_data_includes_control_and_partitions_approved_exception( + config, +) -> None: + frame = _synthetic_r0_frame(config) + training = prepare_d2d_training_data(frame, config) + + assert training.X_phys_all.shape == (15, 10) + assert training.X_norm_all.shape == (15, 10) + assert training.Y_objectives.shape == (15, 3) + assert training.include_in_model.tolist() == [True] * 15 + assert training.row_roles == ("control",) + ("r0_lhs",) * 14 + exception = config.off_grid_exceptions[0] + assert ( + training.X_phys_all[0, D2D_INPUT_COLUMNS.index(exception.input_name)] + == exception.observed_value + ) + assert training.on_grid_mask.tolist() == [False] + [True] * 14 + assert training.on_grid_sample_ids.tolist() == list(config.expected_sample_ids[1:]) + assert training.on_grid_grid_indices.shape == (14, 10) + assert training.off_grid_exceptions == config.off_grid_exceptions + assert all(np.isfinite(training.X_phys_all.ravel())) + assert np.all((training.X_norm_all >= 0.0) & (training.X_norm_all <= 1.0)) + assert np.all(training.Y_objectives > config.reference_point_utility) + + expected_indices = physical_rows_to_grid_indices( + training.X_phys_all[1:], config.design + ) + np.testing.assert_array_equal(training.on_grid_grid_indices, expected_indices) + with pytest.raises(ValueError, match="off-grid"): + physical_rows_to_grid_indices(training.X_phys_all, config.design) + + ablated = prepare_d2d_training_data(frame, config, include_control=False) + assert ablated.include_in_model.tolist() == [False] + [True] * 14 + + +def test_training_data_rejects_unapproved_off_grid_observation(config) -> None: + frame = _synthetic_r0_frame(config) + exception = config.off_grid_exceptions[0] + workbook_column = WORKBOOK_INPUT_COLUMNS[ + D2D_INPUT_COLUMNS.index(exception.input_name) + ] + frame.loc[1, workbook_column] = exception.observed_value + + with pytest.raises(ValueError, match="has an unapproved off-grid value"): + prepare_d2d_training_data(frame, config) + + +def test_training_data_rejects_duplicate_observed_recipes(config) -> None: + frame = _synthetic_r0_frame(config) + frame.loc[2, list(WORKBOOK_INPUT_COLUMNS)] = frame.loc[ + 1, list(WORKBOOK_INPUT_COLUMNS) + ].to_numpy() + + with pytest.raises(ValueError, match="training recipes must be unique"): + prepare_d2d_training_data(frame, config) + + +@pytest.mark.parametrize( + ("objective", "invalid_score"), + [ + ("Uniformity score", -0.01), + ("Uniformity score", 1.01), + ("Thickness score", 1.01), + ], +) +def test_training_data_rejects_uniformity_outside_unit_interval( + config, objective: str, invalid_score: float +) -> None: + frame = _synthetic_r0_frame(config) + frame.loc[4, objective] = invalid_score + + with pytest.raises(ValueError, match=rf"{objective}.*\[0, 1\]"): + prepare_d2d_training_data(frame, config) + + +@pytest.mark.parametrize( + ("column", "value", "message"), + [ + ("speed_1", 999.0, "within configured bounds"), + ("time_1", np.inf, "input values must be complete and finite"), + ("Uniformity score", np.nan, "objective values must be complete and finite"), + ( + "Optoelectronic score", + -10.0, + "strictly dominate the fixed reference point", + ), + ], +) +def test_training_data_rejects_bounds_finite_and_dominance_failures( + config, column: str, value: float, message: str +) -> None: + frame = _synthetic_r0_frame(config) + frame.loc[7, column] = value + + with pytest.raises(ValueError, match=message): + prepare_d2d_training_data(frame, config) + + +def test_training_data_preserves_named_objective_order(config) -> None: + frame = _synthetic_r0_frame(config) + frame.loc[0, list(D2D_OBJECTIVE_COLUMNS)] = [0.61, -3.25, 0.87] + + training = prepare_d2d_training_data(frame, config) + + np.testing.assert_array_equal(training.Y_objectives[0], [0.61, -3.25, 0.87]) + manifest = training.manifest_frame() + required_metadata = { + "campaign_id", + "round", + "sample_id", + "candidate_id", + "row_role", + "replicate_group", + "replicate_number", + "candidate_status", + "include_in_model", + "measurement_provenance", + "off_grid_exception", + "exclusion_reason", + } + assert required_metadata <= set(manifest.columns) + assert manifest["campaign_id"].eq(config.raw["campaign_id"]).all() + assert manifest["round"].eq("R0").all() + assert manifest["candidate_id"].is_unique + assert manifest.loc[0, "off_grid_exception"] + assert manifest.loc[0, "off_grid_exception_reason"] + assert list(manifest.columns[-3:]) == list(D2D_OBJECTIVE_COLUMNS) + np.testing.assert_array_equal( + manifest.loc[0, list(D2D_OBJECTIVE_COLUMNS)].to_numpy(dtype=float), + [0.61, -3.25, 0.87], + ) + + +def test_five_candidates_expand_to_three_replicates_each(config) -> None: + candidates = _five_synthetic_candidates(config) + + expanded = expand_candidates_to_replicates(candidates) + + assert expanded.shape[0] == 15 + assert expanded["candidate_id"].drop_duplicates().tolist() == [ + "R1-C01", + "R1-C02", + "R1-C03", + "R1-C04", + "R1-C05", + ] + assert expanded["execution_id"].is_unique + assert expanded["measurement_provenance"].eq("pending_measurement").all() + assert not expanded["off_grid_exception"].any() + assert expanded.groupby("candidate_id", sort=False).size().tolist() == [3] * 5 + assert ( + expanded.groupby("candidate_id", sort=False)["replicate_number"] + .agg(list) + .tolist() + == [[1, 2, 3]] * 5 + ) + assert expanded["replicate_group"].equals(expanded["candidate_id"]) + assert expanded["candidate_status"].eq(D2D_DEBUG_WATERMARK).all() + assert expanded["campaign_id"].eq(config.raw["campaign_id"]).all() + assert expanded["debug_only"].eq(True).all() # noqa: E712 + assert expanded["approved_for_experiment"].eq(False).all() # noqa: E712 + assert expanded.loc[:, list(D2D_OBJECTIVE_COLUMNS)].isna().all().all() + for candidate_id, group in expanded.groupby("candidate_id", sort=False): + source = candidates.iloc[int(candidate_id[-2:]) - 1] + expected = np.repeat( + source.loc[list(D2D_INPUT_COLUMNS)].to_numpy(dtype=float)[None, :], + 3, + axis=0, + ) + np.testing.assert_array_equal( + group.loc[:, list(D2D_INPUT_COLUMNS)].to_numpy(dtype=float), expected + ) + + +def test_candidate_expansion_rejects_duplicate_candidate_ids(config) -> None: + candidates = _five_synthetic_candidates(config) + candidates["candidate_id"] = [ + "R1-C01", + "R1-C01", + "R1-C03", + "R1-C04", + "R1-C05", + ] + + with pytest.raises(ValueError, match="candidate_id values must be unique"): + expand_candidates_to_replicates(candidates) + + +@pytest.mark.parametrize("invalid_count", [True, 3.5, 0, -1]) +def test_replicate_helpers_reject_nonpositive_or_noninteger_counts( + config, invalid_count: Any +) -> None: + candidates, records = _measured_replicates(config) + + with pytest.raises(ValueError, match="replicates_per_condition"): + expand_candidates_to_replicates( + candidates, replicates_per_condition=invalid_count + ) + with pytest.raises(ValueError, match="expected_replicates"): + aggregate_replicate_objectives(records, expected_replicates=invalid_count) + + +def test_replicate_aggregation_reports_statistics_counts_and_sources(config) -> None: + _, records = _measured_replicates(config) + + result = aggregate_replicate_objectives(records) + + assert result.warnings == () + assert result.frame.shape[0] == 5 + assert result.frame["replicate_group"].tolist() == [ + "R1-C01", + "R1-C02", + "R1-C03", + "R1-C04", + "R1-C05", + ] + assert result.frame["complete_replicate_set"].eq(True).all() # noqa: E712 + assert result.frame["include_in_next_model"].eq(True).all() # noqa: E712 + for candidate_index, row in result.frame.iterrows(): + source_index = candidate_index + 1 + assert row["source_sample_ids"] == "|".join( + f"R1-C{source_index:02d}-R{replicate}" for replicate in (1, 2, 3) + ) + expected_values = { + "Uniformity score": np.asarray([0.10, 0.15, 0.20]) + source_index / 10.0, + "Optoelectronic score": np.asarray([-2.0, -1.0, 0.0]) + source_index, + "Thickness score": np.asarray([0.30, 0.40, 0.50]) + source_index / 20.0, + } + for objective, values in expected_values.items(): + assert row[f"{objective}_mean"] == pytest.approx(np.mean(values)) + assert row[f"{objective}_sample_std"] == pytest.approx( + np.std(values, ddof=1) + ) + assert row[f"{objective}_standard_error"] == pytest.approx( + np.std(values, ddof=1) / np.sqrt(3) + ) + assert row[f"{objective}_count"] == 3 + + +def test_replicate_aggregation_rejects_input_mismatch(config) -> None: + _, records = _measured_replicates(config) + records.loc[1, "speed_1"] = float(config.design.var_array[0][6]) + + with pytest.raises(ValueError, match="contains different requested input"): + aggregate_replicate_objectives(records) + + +@pytest.mark.parametrize("objective", ["Uniformity score", "Thickness score"]) +def test_replicate_aggregation_rejects_bounded_score_out_of_range( + config, objective: str +) -> None: + _, records = _measured_replicates(config) + records.loc[0, objective] = 1.01 + + with pytest.raises(ValueError, match=rf"{objective!r} must remain in \[0, 1\]"): + aggregate_replicate_objectives(records) + + +@pytest.mark.parametrize("duplicate_field", ["execution_id", "replicate_number"]) +def test_replicate_aggregation_rejects_duplicate_execution_metadata( + config, duplicate_field: str +) -> None: + _, records = _measured_replicates(config) + records.loc[1, duplicate_field] = records.loc[0, duplicate_field] + + with pytest.raises( + ValueError, + match=f"{duplicate_field} values must be unique|duplicate {duplicate_field}", + ): + aggregate_replicate_objectives(records) + + +def test_incomplete_single_replicate_has_nan_sample_std_and_sem(config) -> None: + _, records = _measured_replicates(config) + single = records.iloc[[0]].copy() + + result = aggregate_replicate_objectives(single) + + assert len(result.warnings) == 1 + assert "incomplete" in result.warnings[0] + row = result.frame.iloc[0] + assert bool(row["complete_replicate_set"]) is False + assert bool(row["include_in_next_model"]) is False + assert row["source_sample_ids"] == "R1-C01-R1" + for objective in D2D_OBJECTIVE_COLUMNS: + assert row[f"{objective}_count"] == 1 + assert row[f"{objective}_mean"] == pytest.approx(single.iloc[0][objective]) + assert np.isnan(row[f"{objective}_sample_std"]) + assert np.isnan(row[f"{objective}_standard_error"]) + + +def test_r0_and_aggregated_r1_combine_as_twenty_conditions(config) -> None: + training = prepare_d2d_training_data(_synthetic_r0_frame(config), config) + candidates, records = _measured_replicates(config) + aggregated = aggregate_replicate_objectives(records) + + X, Y = combine_r0_and_aggregated_r1(training, aggregated) + + assert X.shape == (20, 10) + assert Y.shape == (20, 3) + assert np.unique(X, axis=0).shape[0] == 20 + np.testing.assert_array_equal(X[:15], training.X_phys_all) + np.testing.assert_array_equal( + X[15:], candidates.loc[:, list(D2D_INPUT_COLUMNS)].to_numpy(dtype=float) + ) + mean_columns = [f"{objective}_mean" for objective in D2D_OBJECTIVE_COLUMNS] + np.testing.assert_allclose( + Y[15:], aggregated.frame.loc[:, mean_columns].to_numpy(dtype=float) + ) + + +def test_combine_rejects_duplicate_condition_between_rounds(config) -> None: + training = prepare_d2d_training_data(_synthetic_r0_frame(config), config) + duplicate = training.X_phys_all[1] + row: dict[str, Any] = { + name: float(value) for name, value in zip(D2D_INPUT_COLUMNS, duplicate) + } + for objective, value in zip(D2D_OBJECTIVE_COLUMNS, [0.7, -1.0, 0.8]): + row[f"{objective}_mean"] = value + row["include_in_next_model"] = True + aggregated = ReplicateAggregationResult(pd.DataFrame([row]), ()) + + with pytest.raises(ValueError, match="duplicate recipes"): + combine_r0_and_aggregated_r1(training, aggregated) diff --git a/tests/test_d2d_notebook.py b/tests/test_d2d_notebook.py new file mode 100644 index 0000000..343c699 --- /dev/null +++ b/tests/test_d2d_notebook.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pandas as pd +import pytest + +from mobo_kit.d2d_scores import compute_thickness_average, compute_thickness_score + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +NOTEBOOK_PATH = ( + REPOSITORY_ROOT + / "notebooks" + / "D2D_MOBO_TEST Global Distance Candidate generation.ipynb" +) + + +def _source(cell: dict) -> str: + value = cell.get("source", "") + return "".join(value) if isinstance(value, list) else str(value) + + +def test_d2d_notebook_contains_guarded_score_section_in_order() -> None: + notebook = json.loads(NOTEBOOK_PATH.read_text(encoding="utf-8")) + cells = notebook["cells"] + sources = [_source(cell) for cell in cells] + section_2 = next( + index + for index, text in enumerate(sources) + if text.startswith("## 2. Normalize Data") + ) + section_25 = next( + index + for index, text in enumerate(sources) + if text.startswith("## 2.5 Calculate and Validate D2D Scores") + ) + section_3 = next( + index + for index, text in enumerate(sources) + if text.startswith("## 3. Fit Gaussian Process") + ) + assert section_2 < section_25 < section_3 + + section_text = "\n".join(sources[section_25:section_3]) + assert "exp(-((mean_t - 650.0) / 250.0) ** 2)" in section_text + assert '"Uniformity score"' in section_text + assert '"Optoelectronic score"' in section_text + assert '"Thickness score"' in section_text + assert "T1" in section_text and "T4" in section_text + assert "T anom" in section_text + assert "validate_supplied_d2d_scores" in section_text + assert "from mobo_kit.d2d_scores import" in section_text + + section_code = "\n".join( + _source(cell) + for cell in cells[section_25:section_3] + if cell["cell_type"] == "code" + ) + assert "-0.5" not in section_code + assert "0.5 *" not in section_code + + +def test_d2d_notebook_is_portable_cleared_and_uses_active_apis() -> None: + notebook = json.loads(NOTEBOOK_PATH.read_text(encoding="utf-8")) + all_source = "\n".join(_source(cell) for cell in notebook["cells"]) + assert "/Users/" not in all_source + assert "C:\\Users\\" not in all_source + for retired_name in ( + "MixedMCMultiOutputObjective", + "fit_gp_models_baybe", + "propose_batch_discrete", + "random_discrete_choices", + ): + assert retired_name not in all_source + assert "run_d2d_step2b_debug" in all_source + assert "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" in all_source + assert "diagnostics_path.mkdir(parents=True, exist_ok=True)" in all_source + assert "save_path.mkdir" not in all_source + for cell in notebook["cells"]: + if cell["cell_type"] == "code": + assert cell.get("execution_count") is None + assert cell.get("outputs") == [] + + +def test_d2d_notebook_narrative_enforces_resolved_step2b_contract() -> None: + notebook = json.loads(NOTEBOOK_PATH.read_text(encoding="utf-8")) + markdown_source = "\n".join( + _source(cell) for cell in notebook["cells"] if cell["cell_type"] == "markdown" + ) + all_source = "\n".join(_source(cell) for cell in notebook["cells"]) + + for required_contract_text in ( + "Excel Z `Uniformity score`", + "AA `Optoelectronic score`", + "AB `Thickness score`", + "R1 UCB-HVI", + "**max/max/max**", + "`[-0.01, -10.0, -0.01]`", + "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT", + ): + assert required_contract_text in markdown_source + + for contradictory_legacy_text in ( + "PCE, Stability, Repeatability", + 'out["X_phys"]', + 'out["X_norm"]', + 'out["acq_val"]', + "use_lognehvi", + "log nEHVI", + "Before running the batch in the lab", + "hand off to execution", + "slightly worse than the worst feasible values", + "lab_worklist", + ): + assert contradictory_legacy_text not in all_source + + +def test_notebook_thickness_helper_fixture_uses_no_half_factor() -> None: + expected = pytest.approx(0.9607894391523232) + assert compute_thickness_score([600.0, 600.0, None, "\u00a0"]) == expected + + +def test_notebook_thickness_calculation_cell_executes_against_package_api() -> None: + notebook = json.loads(NOTEBOOK_PATH.read_text(encoding="utf-8")) + cell_source = next( + _source(cell) + for cell in notebook["cells"] + if "thickness_means = normalized_thickness.apply" in _source(cell) + ) + normalized_thickness = pd.DataFrame( + [[600.0, 600.0, None, "\u00a0"]], columns=["T1", "T2", "T3", "T4"] + ) + namespace = { + "compute_thickness_average": compute_thickness_average, + "compute_thickness_score": compute_thickness_score, + "data_rows": pd.DataFrame( + {"Sample number": [1], "Thickness score": [0.9607894391523232]} + ), + "display": lambda _value: None, + "normalized_thickness": normalized_thickness, + "pd": pd, + } + + exec(compile(cell_source, str(NOTEBOOK_PATH), "exec"), namespace) + + assert namespace["thickness_means"].iloc[0] == pytest.approx(600.0) + assert namespace["calculated_thickness_scores"].iloc[0] == pytest.approx( + 0.9607894391523232 + ) diff --git a/tests/test_d2d_r2_test.py b/tests/test_d2d_r2_test.py new file mode 100644 index 0000000..d61b842 --- /dev/null +++ b/tests/test_d2d_r2_test.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from dataclasses import replace +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +import torch + +from mobo_kit.candidate_pool import sample_discrete_candidate_pool +from mobo_kit.d2d_campaign import ( + D2D_INPUT_COLUMNS, + D2D_OBJECTIVE_COLUMNS, + D2D_WORKBOOK_INPUT_COLUMNS, + aggregate_replicate_objectives, + combine_r0_and_aggregated_r1, + load_d2d_debug_config, + prepare_d2d_training_data, +) +from mobo_kit.d2d_r2_test import ( + D2D_R2_TEST_WATERMARK, + propose_d2d_qlognehvi_test_batch, +) +from mobo_kit.data import x_normalizer_np +from mobo_kit.models import fit_gp_models + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +PUBLIC_CONFIG_PATH = REPOSITORY_ROOT / "configs" / "d2d_step2b_debug.yaml" + + +def _grid_rows(design, start: int, count: int) -> np.ndarray: + rows = [] + for index in range(start, start + count): + rows.append( + [ + float(grid[(index * (dimension + 1) + 2 * dimension) % len(grid)]) + for dimension, grid in enumerate(design.var_array) + ] + ) + result = np.asarray(rows, dtype=float) + assert np.unique(result, axis=0).shape[0] == count + return result + + +def _synthetic_scores(X_norm: np.ndarray) -> np.ndarray: + uniformity = 0.10 + 0.75 * (0.65 * X_norm[:, 0] + 0.35 * (1.0 - X_norm[:, 1])) + optoelectronic = -9.4 + 1.8 * (0.55 * X_norm[:, 2] + 0.45 * X_norm[:, 4]) + thickness = 0.05 + 0.90 * np.exp(-(((X_norm[:, 5] - 0.55) / 0.32) ** 2)) + return np.column_stack([uniformity, optoelectronic, thickness]) + + +def test_future_r2_uses_20_conditions_and_returns_three_synthetic_points() -> None: + config = load_d2d_debug_config(PUBLIC_CONFIG_PATH, allow_public_template=True) + X_r0 = _grid_rows(config.design, 0, 15) + exception = config.off_grid_exceptions[0] + X_r0[0, D2D_INPUT_COLUMNS.index(exception.input_name)] = exception.observed_value + Y_r0 = _synthetic_scores(x_normalizer_np(X_r0, config.design)) + frame = pd.DataFrame({"Sample number": config.expected_sample_ids}) + for index, column in enumerate(D2D_WORKBOOK_INPUT_COLUMNS): + frame[column] = X_r0[:, index] + for index, column in enumerate(D2D_OBJECTIVE_COLUMNS): + frame[column] = Y_r0[:, index] + r0_training = prepare_d2d_training_data(frame, config) + + X_r1 = _grid_rows(config.design, 40, 5) + Y_r1 = _synthetic_scores(x_normalizer_np(X_r1, config.design)) + records = [] + for candidate_index, (inputs, objectives) in enumerate(zip(X_r1, Y_r1), start=1): + for replicate, offset in enumerate((-0.01, 0.0, 0.01), start=1): + row = { + "execution_id": f"R1-C{candidate_index:02d}-R{replicate}", + "replicate_group": f"R1-C{candidate_index:02d}", + "replicate_number": replicate, + } + row.update(dict(zip(D2D_INPUT_COLUMNS, inputs))) + row.update( + { + objective: float(value + offset * (objective_index + 1) / 10) + for objective_index, (objective, value) in enumerate( + zip(D2D_OBJECTIVE_COLUMNS, objectives) + ) + } + ) + records.append(row) + aggregated = aggregate_replicate_objectives(pd.DataFrame(records)) + assert aggregated.frame.shape[0] == 5 + assert aggregated.frame["complete_replicate_set"].all() + + X_phys, Y = combine_r0_and_aggregated_r1(r0_training, aggregated) + assert X_phys.shape == (20, 10) + assert Y.shape == (20, 3) + X_norm = x_normalizer_np(X_phys, config.design) + train_X = torch.as_tensor(X_norm, dtype=torch.double) + train_Y = torch.as_tensor(Y, dtype=torch.double) + torch.manual_seed(config.seed) + model = fit_gp_models(train_X, train_Y) + + pool = sample_discrete_candidate_pool( + config.design, + 64, + seed=911, + observed_phys=X_phys[1:], + row_constraints=[], + ) + with pytest.raises(ValueError, match="synthetic test-only"): + propose_d2d_qlognehvi_test_batch( + pool, model, train_X, config, test_only=False, mc_samples=16 + ) + for invalid_samples in (True, 3.5, 0, -1): + with pytest.raises(ValueError, match="mc_samples must be a positive"): + propose_d2d_qlognehvi_test_batch( + pool, + model, + train_X, + config, + test_only=True, + mc_samples=invalid_samples, + ) + for invalid_seed in (True, 3.5, -1): + with pytest.raises(ValueError, match="seed must be a non-negative"): + propose_d2d_qlognehvi_test_batch( + pool, + model, + train_X, + config, + test_only=True, + mc_samples=16, + seed=invalid_seed, + ) + with pytest.raises(ValueError, match="off-grid"): + propose_d2d_qlognehvi_test_batch( + replace(pool, X_phys=pool.X_phys + 1e-6), + model, + train_X, + config, + test_only=True, + mc_samples=16, + ) + inconsistent_indices = pool.grid_indices.copy() + inconsistent_indices[0, 0] = (inconsistent_indices[0, 0] + 1) % len( + config.design.var_array[0] + ) + with pytest.raises(ValueError, match="grid_indices do not match"): + propose_d2d_qlognehvi_test_batch( + replace(pool, grid_indices=inconsistent_indices), + model, + train_X, + config, + test_only=True, + mc_samples=16, + ) + inconsistent_norm = pool.X_norm.copy() + inconsistent_norm[0, 0] += 1e-6 + with pytest.raises(ValueError, match="normalized rows do not match"): + propose_d2d_qlognehvi_test_batch( + replace(pool, X_norm=inconsistent_norm), + model, + train_X, + config, + test_only=True, + mc_samples=16, + ) + result = propose_d2d_qlognehvi_test_batch( + pool, + model, + train_X, + config, + test_only=True, + mc_samples=16, + seed=73, + chunk_size=64, + ) + assert result.selection.X_phys.shape == (3, 10) + assert np.unique(result.selection.X_phys, axis=0).shape[0] == 3 + assert result.metadata["objective_contract_version"] == ( + "d2d-step2b-debug-objectives-v1" + ) + assert result.metadata["test_only"] is True + assert result.metadata["synthetic_only"] is True + assert result.metadata["approved_for_experiment"] is False + assert result.metadata["candidate_status"] == D2D_R2_TEST_WATERMARK + assert result.selection.method_diagnostics["test_only"] is True diff --git a/tests/test_d2d_scores.py b/tests/test_d2d_scores.py new file mode 100644 index 0000000..7d7f64c --- /dev/null +++ b/tests/test_d2d_scores.py @@ -0,0 +1,299 @@ +import math + +import numpy as np +import pandas as pd +import pytest + +from mobo_kit.d2d_scores import ( + D2DScoreTolerances, + D2DScoreValidationError, + OPTOELECTRONIC_CHECK_COLUMN, + THICKNESS_CHECK_COLUMN, + compute_optoelectronic_score, + compute_thickness_average, + compute_thickness_score, + compute_uniformity_score, + raise_for_errors, + validate_supplied_d2d_scores, +) + + +def _valid_frame() -> pd.DataFrame: + implied_voc = 0.9 + photoconductance = 1000.0 + optoelectronic = compute_optoelectronic_score(implied_voc, photoconductance) + thicknesses = (600.0, 650.0, 700.0, None) + thickness = compute_thickness_score(thicknesses) + return pd.DataFrame( + { + "Sample number": [101], + "Coverage": [0.8], + "1 - Uniformity": [0.5], + "Phase purity": [0.75], + "PL - Implied Voc (Max)": [implied_voc], + "Photoconductance (Max)": [photoconductance], + OPTOELECTRONIC_CHECK_COLUMN: [optoelectronic], + "T1": [thicknesses[0]], + "T2": [thicknesses[1]], + "T3": [thicknesses[2]], + "T4": [thicknesses[3]], + "T anom": [1_000_000.0], + "Thickness (avg)": [-123.0], + THICKNESS_CHECK_COLUMN: [thickness], + "Uniformity score": [0.3], + "Optoelectronic score": [optoelectronic], + "Thickness score": [thickness], + }, + index=pd.Index(["synthetic-row"], name="source_row"), + ) + + +def test_uniformity_support_equation_and_finite_validation(): + assert compute_uniformity_score(0.8, 0.5, 0.75) == pytest.approx(0.3) + with pytest.raises(ValueError, match="finite"): + compute_uniformity_score(np.nan, 0.5, 0.75) + with pytest.raises(TypeError, match="non-boolean"): + compute_uniformity_score(True, 0.5, 0.75) + + +def test_optoelectronic_equation_and_extreme_products(): + assert compute_optoelectronic_score(0.9, 1000.0) == pytest.approx( + math.log10(0.9 * 1000.0) + ) + # Algebraic evaluation remains finite even when the intermediate product + # would overflow double precision. + assert compute_optoelectronic_score(1e300, 1e300) == pytest.approx(600.0) + + +@pytest.mark.parametrize( + "implied_voc, photoconductance", + [(0.0, 1.0), (-1.0, 1.0), (1.0, 0.0), (1.0, -1.0)], +) +def test_optoelectronic_rejects_nonpositive_components(implied_voc, photoconductance): + with pytest.raises(ValueError, match="strictly positive"): + compute_optoelectronic_score(implied_voc, photoconductance) + + +@pytest.mark.parametrize("invalid", [np.nan, np.inf, -np.inf]) +def test_optoelectronic_rejects_nonfinite_components(invalid): + with pytest.raises(ValueError, match="finite"): + compute_optoelectronic_score(invalid, 1.0) + + +def test_thickness_average_ignores_all_documented_blank_forms(): + assert compute_thickness_average(600.0, None, " \t", "\u00a0\u00a0") == 600.0 + assert compute_thickness_average(600.0, np.nan, "", 700.0) == 650.0 + with pytest.raises(ValueError, match="At least one valid"): + compute_thickness_average(None, "", " ", "\u00a0") + + +def test_thickness_average_rejects_nonblank_nonnumeric_measurement(): + with pytest.raises(TypeError, match="T2"): + compute_thickness_average(600.0, "not measured", None, None) + + +def test_thickness_score_uses_unrounded_mean_and_no_half_factor(): + values = (600.4, 600.4, 600.4, 601.6) + unrounded_mean = 600.7 + assert compute_thickness_average(*values) == pytest.approx(unrounded_mean) + expected = math.exp(-(((unrounded_mean - 650.0) / 250.0) ** 2)) + assert compute_thickness_score(values) == pytest.approx(expected) + + one_scale_from_target = compute_thickness_score((900.0, None, None, None)) + assert one_scale_from_target == pytest.approx(math.exp(-1.0)) + assert one_scale_from_target != pytest.approx(math.exp(-0.5)) + assert compute_thickness_score(values, 650.0, 250.0) == pytest.approx(expected) + + +def test_thickness_api_accepts_exactly_t1_through_t4_and_excludes_t_anom(): + expected = compute_thickness_score((600.0, 650.0, 700.0, None)) + assert expected == pytest.approx(1.0) + with pytest.raises(ValueError, match="exactly T1:T4"): + compute_thickness_score((600.0, 650.0, 700.0, None, 1_000_000.0)) + with pytest.raises(ValueError, match="strictly positive"): + compute_thickness_score((650.0, None, None, None), scale=0.0) + + +def test_clean_validation_is_exportable_and_does_not_mutate_authoritative_scores(): + frame = _valid_frame() + original = frame.copy(deep=True) + + result = validate_supplied_d2d_scores(frame) + + assert result.has_errors is False + assert result.warnings == () + assert result.raise_for_errors() is result + assert raise_for_errors(result) is result + assert len(result.rows) == 1 + row = result.rows[0] + assert row.uniformity_score_authoritative == pytest.approx(0.3) + assert row.uniformity_score_calculated == pytest.approx(0.3) + assert row.thickness_average_calculated == pytest.approx(650.0) + assert row.thickness_score_calculated == pytest.approx(1.0) + assert row.optoelectronic_check_matches is True + assert row.optoelectronic_objective_matches is True + assert row.thickness_check_matches is True + assert row.thickness_objective_matches is True + assert result.frame.loc[0, "row_label"] == "synthetic-row" + assert result.findings_frame().empty + pd.testing.assert_frame_equal(frame, original) + + +def test_uniformity_mismatch_warns_and_keeps_supplied_score_authoritative(): + frame = _valid_frame() + frame.loc["synthetic-row", "Uniformity score"] = 0.91 + original = frame.copy(deep=True) + + result = validate_supplied_d2d_scores(frame) + + assert result.has_errors is False + assert result.known_uniformity_score_mismatch is True + assert result.uniformity_warning_count == 1 + assert [finding.code for finding in result.warnings] == ["uniformity_mismatch"] + assert result.rows[0].uniformity_score_authoritative == pytest.approx(0.91) + assert result.rows[0].uniformity_score_calculated == pytest.approx(0.3) + assert result.rows[0].uniformity_matches is False + result.raise_for_errors() + pd.testing.assert_frame_equal(frame, original) + + +@pytest.mark.parametrize("invalid_score", [-0.01, 1.01, 1.5]) +def test_authoritative_uniformity_score_must_remain_in_unit_interval( + invalid_score, +): + frame = _valid_frame() + frame.loc["synthetic-row", "Uniformity score"] = invalid_score + + result = validate_supplied_d2d_scores(frame) + + assert "uniformity_score_out_of_range" in { + finding.code for finding in result.errors + } + with pytest.raises(D2DScoreValidationError, match=r"must remain in \[0, 1\]"): + result.raise_for_errors() + + +@pytest.mark.parametrize( + "column, code", + [ + (OPTOELECTRONIC_CHECK_COLUMN, "optoelectronic_check_mismatch"), + ("Optoelectronic score", "optoelectronic_objective_mismatch"), + (THICKNESS_CHECK_COLUMN, "thickness_check_mismatch"), + ("Thickness score", "thickness_objective_mismatch"), + ], +) +def test_unexpected_optoelectronic_and_thickness_mismatches_are_errors(column, code): + frame = _valid_frame() + frame.loc["synthetic-row", column] = -20.0 + + result = validate_supplied_d2d_scores(frame) + + assert code in [finding.code for finding in result.errors] + with pytest.raises(D2DScoreValidationError, match="score validation failed"): + result.raise_for_errors() + + +@pytest.mark.parametrize( + "column, invalid", + [ + ("Uniformity score", None), + ("Optoelectronic score", np.nan), + ("Thickness score", np.inf), + ], +) +def test_missing_or_nonfinite_authoritative_scores_are_errors(column, invalid): + frame = _valid_frame() + frame.loc["synthetic-row", column] = invalid + + result = validate_supplied_d2d_scores(frame) + + assert result.has_errors + assert any( + finding.column == column + and finding.code + in {"authoritative_score_missing", "authoritative_score_invalid"} + for finding in result.errors + ) + + +def test_invalid_raw_optoelectronic_and_empty_thickness_are_errors(): + frame = _valid_frame() + frame = frame.astype({column: object for column in ("T1", "T2", "T3", "T4")}) + frame.loc["synthetic-row", "PL - Implied Voc (Max)"] = 0.0 + frame.loc["synthetic-row", ["T1", "T2", "T3", "T4"]] = [ + None, + "", + " ", + "\u00a0", + ] + + result = validate_supplied_d2d_scores(frame) + codes = {finding.code for finding in result.errors} + + assert "optoelectronic_support_invalid" in codes + assert "thickness_support_invalid" in codes + + +def test_optional_check_fields_are_compared_only_when_available(): + frame = _valid_frame().drop( + columns=[OPTOELECTRONIC_CHECK_COLUMN, THICKNESS_CHECK_COLUMN] + ) + + result = validate_supplied_d2d_scores(frame) + + assert result.has_errors is False + assert result.rows[0].optoelectronic_check_matches is None + assert result.rows[0].thickness_check_matches is None + + +def test_raw_t1_t4_override_neither_t_anom_nor_displayed_rounded_average(): + frame = _valid_frame() + values = (600.4, 600.4, 600.4, 601.6) + score = compute_thickness_score(values) + frame.loc["synthetic-row", ["T1", "T2", "T3", "T4"]] = values + frame.loc["synthetic-row", "T anom"] = 650.0 + frame.loc["synthetic-row", "Thickness (avg)"] = 650.0 + frame.loc["synthetic-row", THICKNESS_CHECK_COLUMN] = score + frame.loc["synthetic-row", "Thickness score"] = score + + result = validate_supplied_d2d_scores(frame).raise_for_errors() + + assert result.rows[0].thickness_average_calculated == pytest.approx(600.7) + assert result.rows[0].thickness_score_calculated == pytest.approx(score) + + +def test_default_tolerance_is_two_decimal_rounding_aware_and_configurable(): + frame = _valid_frame() + for column in (OPTOELECTRONIC_CHECK_COLUMN, "Optoelectronic score"): + frame.loc["synthetic-row", column] = round( + frame.loc["synthetic-row", column], 2 + ) + for column in (THICKNESS_CHECK_COLUMN, "Thickness score"): + frame.loc["synthetic-row", column] = round( + frame.loc["synthetic-row", column], 2 + ) + + assert validate_supplied_d2d_scores(frame).has_errors is False + strict = D2DScoreTolerances( + uniformity_atol=0.0, + optoelectronic_atol=0.0, + thickness_atol=0.0, + rounding_slack=0.0, + ) + strict_result = validate_supplied_d2d_scores(frame, strict) + assert "optoelectronic_check_mismatch" in { + finding.code for finding in strict_result.errors + } + + +def test_tolerance_and_dataframe_contract_validation(): + with pytest.raises(ValueError, match="non-negative"): + D2DScoreTolerances(thickness_atol=-1.0) + with pytest.raises(TypeError, match="D2DScoreTolerances"): + validate_supplied_d2d_scores(_valid_frame(), {"thickness_atol": 0.1}) + with pytest.raises(ValueError, match="Missing required"): + validate_supplied_d2d_scores(_valid_frame().drop(columns=["T4"])) + + duplicate = pd.concat([_valid_frame(), _valid_frame()[["Thickness score"]]], axis=1) + with pytest.raises(ValueError, match="Ambiguous duplicate"): + validate_supplied_d2d_scores(duplicate) diff --git a/tests/test_d2d_step2b_debug.py b/tests/test_d2d_step2b_debug.py new file mode 100644 index 0000000..e8ad0f1 --- /dev/null +++ b/tests/test_d2d_step2b_debug.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +from types import SimpleNamespace + +import numpy as np +import pandas as pd +import pytest +import torch +import yaml + +import mobo_kit.d2d_step2b_debug as debug_module +from mobo_kit.d2d_campaign import D2D_DEBUG_WATERMARK, D2D_INPUT_COLUMNS +from mobo_kit.d2d_step2b_debug import run_d2d_step2b_debug + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +PUBLIC_CONFIG = REPOSITORY_ROOT / "configs" / "d2d_step2b_debug.yaml" +PRIVATE_WORKBOOK = os.environ.get("MOBO_KIT_D2D_PRIVATE_WORKBOOK") +PRIVATE_CONFIG = os.environ.get("MOBO_KIT_D2D_PRIVATE_CONFIG") + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest().upper() + + +def _materialized_public_config(tmp_path: Path, expected_sha256: str) -> Path: + config = yaml.safe_load(PUBLIC_CONFIG.read_text(encoding="utf-8")) + config["template_only"] = False + config["workbook"]["expected_sha256"] = expected_sha256 + path = tmp_path / "materialized-public-debug.yaml" + path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8") + return path + + +def test_debug_adapter_creates_no_output_before_workbook_hash_validation( + tmp_path: Path, +) -> None: + config_path = _materialized_public_config(tmp_path, "0" * 64) + workbook = tmp_path / "source" / "not_the_campaign.xlsx" + workbook.parent.mkdir() + workbook.write_bytes(b"not an xlsx") + output = tmp_path / "must_not_exist" + + with pytest.raises(ValueError, match="Workbook hash does not match"): + run_d2d_step2b_debug(workbook, config_path, output, run_sensitivity=False) + assert not output.exists() + + +def test_debug_adapter_rejects_output_inside_source_workbook_directory( + tmp_path: Path, +) -> None: + workbook = tmp_path / "source.xlsx" + workbook.write_bytes(b"placeholder") + + with pytest.raises(ValueError, match="source workbook or its directory"): + run_d2d_step2b_debug( + workbook, + PUBLIC_CONFIG, + tmp_path / "debug_output", + run_sensitivity=False, + ) + + +def test_debug_adapter_rejects_repository_docs_output_before_writing( + tmp_path: Path, +) -> None: + workbook = tmp_path / "source.xlsx" + workbook.write_bytes(b"placeholder") + copied_config = _materialized_public_config(tmp_path, "0" * 64) + forbidden = REPOSITORY_ROOT / "docs" / "step2b-forbidden-test-output" + assert not forbidden.exists() + + with pytest.raises(ValueError, match="configured debug output root"): + run_d2d_step2b_debug( + workbook, + copied_config, + forbidden, + run_sensitivity=False, + ) + assert not forbidden.exists() + + +def test_compute_proposal_uses_independent_pool_and_mc_seeds( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, int] = {} + dimensions = len(D2D_INPUT_COLUMNS) + pool = SimpleNamespace( + X_norm=np.zeros((5, dimensions)), + X_phys=np.zeros((5, dimensions)), + ) + selection = SimpleNamespace(X_phys=np.zeros((5, dimensions))) + + def fake_pool(*args: object, seed: int, **kwargs: object) -> object: + captured["pool_seed"] = seed + return pool + + def fake_proposal(*args: object, seed: int, **kwargs: object) -> object: + captured["mc_seed"] = seed + return SimpleNamespace(selection=selection) + + monkeypatch.setattr(debug_module, "sample_discrete_candidate_pool", fake_pool) + monkeypatch.setattr(debug_module, "propose_ucb_hvi_batch", fake_proposal) + config = SimpleNamespace( + design=object(), + min_observed_distance=0.0, + dimension_weights=None, + reference_point_utility=np.array([-0.01, -10.0, -0.01]), + r1_batch_size=5, + score_chunk_size=32, + ) + training = SimpleNamespace( + X_phys_all=np.zeros((1, dimensions)), + on_grid_mask=np.ones(1, dtype=bool), + X_norm_all=np.zeros((1, dimensions)), + ) + + debug_module._compute_proposal( + config, + training, + object(), + torch.zeros((1, dimensions), dtype=torch.double), + torch.zeros((1, 3), dtype=torch.double), + pool_size=5, + pool_seed=137, + mc_seed=73, + beta=4.0, + posterior_samples=16, + radius=0.25, + min_batch_distance=0.15, + ) + + assert captured == {"pool_seed": 137, "mc_seed": 73} + + +@pytest.mark.skipif( + not PRIVATE_WORKBOOK or not PRIVATE_CONFIG, + reason=( + "set MOBO_KIT_D2D_PRIVATE_WORKBOOK and MOBO_KIT_D2D_PRIVATE_CONFIG " + "to opt into the ignored private integration test" + ), +) +def test_local_debug_adapter_is_deterministic_complete_and_read_only( + tmp_path: Path, +) -> None: + local_workbook = Path(str(PRIVATE_WORKBOOK)).expanduser().resolve() + private_config = Path(str(PRIVATE_CONFIG)).expanduser().resolve() + private_raw = yaml.safe_load(private_config.read_text(encoding="utf-8")) + before_hash = _sha256(local_workbook) + before_mtime = local_workbook.stat().st_mtime_ns + assert before_hash == str(private_raw["workbook"]["expected_sha256"]).upper() + + first = run_d2d_step2b_debug( + local_workbook, + private_config, + tmp_path / "run_1", + run_sensitivity=False, + ) + second = run_d2d_step2b_debug( + local_workbook, + private_config, + tmp_path / "run_2", + run_sensitivity=False, + ) + + assert first.candidates_unique.shape[0] == 5 + assert first.replicate_worklist.shape[0] == 15 + assert first.candidates_unique["candidate_id"].tolist() == [ + "R1-C01", + "R1-C02", + "R1-C03", + "R1-C04", + "R1-C05", + ] + assert first.candidates_unique["debug_only"].all() + assert not first.candidates_unique["approved_for_experiment"].any() + assert first.candidates_unique["candidate_status"].eq(D2D_DEBUG_WATERMARK).all() + assert first.candidates_unique["known_uniformity_score_mismatch"].all() + assert { + "campaign_id", + "round", + "sample_id", + "candidate_id", + "row_role", + "replicate_group", + "replicate_number", + "candidate_status", + "include_in_model", + "measurement_provenance", + "off_grid_exception", + "exclusion_reason", + } <= set(first.candidates_unique.columns) + assert first.replicate_worklist["known_uniformity_score_mismatch"].all() + assert first.candidates_unique["grid_valid"].all() + assert first.candidates_unique["bounds_valid"].all() + assert (first.candidates_unique["base_ucb_hvi"] > 0).all() + assert ( + not first.candidates_unique.loc[:, list(D2D_INPUT_COLUMNS)].duplicated().any() + ) + np.testing.assert_array_equal( + first.candidates_unique.loc[:, list(D2D_INPUT_COLUMNS)].to_numpy(), + second.candidates_unique.loc[:, list(D2D_INPUT_COLUMNS)].to_numpy(), + ) + assert first.run_manifest["training_row_count"] == 15 + assert first.run_manifest["uniformity_warning_count"] == 11 + assert first.run_manifest["reference_point_utility"] == [-0.01, -10.0, -0.01] + assert first.run_manifest["objective_order"] == [ + "Uniformity score", + "Optoelectronic score", + "Thickness score", + ] + baseline_sensitivity = first.sensitivity_summary.iloc[0] + assert baseline_sensitivity["exact_overlap_with_baseline"] == 5 + assert baseline_sensitivity["jaccard_overlap_with_baseline"] == 1.0 + assert baseline_sensitivity["mean_nearest_batch_distance_to_baseline"] == 0.0 + assert baseline_sensitivity["pool_seed"] == 73 + assert baseline_sensitivity["mc_seed"] == 73 + assert bool(baseline_sensitivity["debug_only"]) + assert not bool(baseline_sensitivity["approved_for_experiment"]) + assert baseline_sensitivity["candidate_status"] == D2D_DEBUG_WATERMARK + assert bool(baseline_sensitivity["known_uniformity_score_mismatch"]) + assert "boundary_coordinate_count" in first.sensitivity_summary.columns + assert "boundary_dimension_count" not in first.sensitivity_summary.columns + assert "ordered_batch_sha256" in first.sensitivity_summary.columns + assert "candidate_set_sha256" not in first.sensitivity_summary.columns + assert baseline_sensitivity["observed_pareto_count"] == 6 + assert json.loads(baseline_sensitivity["observed_pareto_sample_ids"]) + + sensitivity_candidates = first.sensitivity_candidates + assert sensitivity_candidates.shape[0] == 5 + assert set(sensitivity_candidates["record_type"]) == {"selected_candidate"} + assert set(sensitivity_candidates["run_label"]) == {"baseline"} + assert set(sensitivity_candidates["pool_seed"]) == {73} + assert set(sensitivity_candidates["mc_seed"]) == {73} + assert set(sensitivity_candidates["candidate_status"]) == {D2D_DEBUG_WATERMARK} + assert sensitivity_candidates["known_uniformity_score_mismatch"].all() + assert { + *D2D_INPUT_COLUMNS, + "base_raw_score", + "base_log_score", + "final_penalized_score", + "penalized_log_score", + "penalty_factor", + "pairwise_distance_row", + "nearest_observed_distance", + "boundary_coordinates", + "boundary_coordinate_count", + "total_fit_proposal_runtime_seconds", + "status", + "warning", + } <= set(sensitivity_candidates.columns) + np.testing.assert_allclose( + sensitivity_candidates["final_penalized_score"], + sensitivity_candidates["base_raw_score"] + * sensitivity_candidates["penalty_factor"], + ) + assert all( + len(json.loads(value)) == 5 + for value in sensitivity_candidates["pairwise_distance_row"] + ) + assert first.control_ablation.empty + assert { + "baseline_predicted_uniformity_score_mean", + "control_excluded_predicted_uniformity_score_mean", + "delta_uniformity_score_mean", + "selected_exact_overlap_count", + "total_fit_proposal_runtime_seconds", + "status", + "candidate_status", + "known_uniformity_score_mismatch", + } <= set(first.control_ablation.columns) + + required = { + "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt", + "workbook_audit.json", + "score_validation.csv", + "training_row_manifest.csv", + "model_diagnostics.csv", + "r1_debug_candidates_unique.csv", + "r1_debug_replicate_worklist.csv", + "candidate_diagnostics.csv", + "sensitivity_summary.csv", + "sensitivity_candidates_long.csv", + "control_ablation.csv", + "run_manifest.json", + } + assert required <= {path.name for path in first.output_dir.iterdir()} + plot_paths = list((first.output_dir / "plots").glob("*.png")) + assert len(plot_paths) == 4 + assert all(D2D_DEBUG_WATERMARK.encode() in path.read_bytes() for path in plot_paths) + written_candidates = pd.read_csv( + first.output_dir / "sensitivity_candidates_long.csv" + ) + assert written_candidates.shape[0] == 5 + written_control = pd.read_csv(first.output_dir / "control_ablation.csv") + assert written_control.empty + written_diagnostics = pd.read_csv(first.output_dir / "candidate_diagnostics.csv") + assert written_diagnostics["candidate_status"].eq(D2D_DEBUG_WATERMARK).all() + assert written_diagnostics["known_uniformity_score_mismatch"].all() + written_training = pd.read_csv(first.output_dir / "training_row_manifest.csv") + assert written_training["candidate_status"].eq(D2D_DEBUG_WATERMARK).all() + assert written_training["known_uniformity_score_mismatch"].all() + assert _sha256(local_workbook) == before_hash + assert local_workbook.stat().st_mtime_ns == before_mtime diff --git a/tests/test_d2d_step2c_config.py b/tests/test_d2d_step2c_config.py new file mode 100644 index 0000000..09abbc3 --- /dev/null +++ b/tests/test_d2d_step2c_config.py @@ -0,0 +1,156 @@ +from pathlib import Path + +import pytest +import yaml + +from mobo_kit.d2d_campaign import D2D_DEBUG_WATERMARK, D2D_INPUT_COLUMNS +from mobo_kit.d2d_step2c_config import ( + STEP2C_NESTED_POOL_SIZES, + STEP2C_OUTPUT_ROOT, + STEP2C_RUNTIME_GENERATED, + STEP2C_SYNTHETIC_SOURCE_KIND, + Step2CConfigError, + load_step2c_config, +) + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +CONFIG_PATH = REPOSITORY_ROOT / "configs" / "d2d_step2c_debug.yaml" + + +def _copy_with_change(tmp_path: Path, mutator) -> Path: + raw = yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8")) + mutator(raw) + path = tmp_path / "changed_step2c.yaml" + path.write_text(yaml.safe_dump(raw, sort_keys=False), encoding="utf-8") + return path + + +def test_step2c_config_resolves_public_safe_debug_contract() -> None: + config = load_step2c_config(CONFIG_PATH) + + assert config.config_path == CONFIG_PATH.resolve() + assert len(config.config_sha256) == 64 + assert len(config.resolved_config_hash) == 64 + assert tuple(config.design.names) == D2D_INPUT_COLUMNS + assert len(config.expected_sample_ids) == 15 + assert config.workbook_source_kind == STEP2C_SYNTHETIC_SOURCE_KIND + assert config.workbook_relative_path == STEP2C_RUNTIME_GENERATED + assert config.workbook_expected_sha256 == STEP2C_RUNTIME_GENERATED.upper() + assert config.raw["workbook"] == {"source_kind": STEP2C_SYNTHETIC_SOURCE_KIND} + assert config.raw["r0"] == {"inherit_from_base": True} + assert config.nested_pool_sizes == STEP2C_NESTED_POOL_SIZES + assert config.beta_values == (1.0, 4.0, 9.0) + assert config.bound_policies == ("none", "clip_ucb") + assert config.primary_bound_policy == "clip_ucb" + assert config.primary_penalty_variant == "radius_0_25" + assert config.model_variant_names == ("default_current", "conservative") + assert config.influence_sample_ids == config.expected_sample_ids + assert config.shortlist_min == 8 + assert config.shortlist_max == 12 + assert config.output_root == STEP2C_OUTPUT_ROOT + assert config.raw["debug_watermark"] == D2D_DEBUG_WATERMARK + assert config.raw["approved_for_experiment"] is False + assert config.raw["approved_for_production"] is False + assert config.mode("full").nested_unique_sizes == STEP2C_NESTED_POOL_SIZES + assert config.mode("fast").omitted_sample_ids == config.expected_sample_ids[:3] + + +@pytest.mark.parametrize( + "mutator,match", + [ + ( + lambda raw: raw.__setitem__("approved_for_experiment", True), + "approved_for_experiment", + ), + ( + lambda raw: raw["workbook"].__setitem__( + "objective_columns", ["Y", "Z", "AA"] + ), + "workbook", + ), + ( + lambda raw: raw["objectives"].__setitem__("reference_point", [0, 0, 0]), + "reference_point", + ), + ( + lambda raw: raw["r0"].__setitem__( + "include_control_in_primary_model", False + ), + "r0", + ), + ( + lambda raw: raw["candidate_search"].__setitem__( + "preserve_accepted_prefix_nesting", False + ), + "nesting", + ), + ( + lambda raw: raw["local_penalty_study"].__setitem__( + "hard_distance_relaxation", True + ), + "relaxation", + ), + ( + lambda raw: raw["ucb_hvi"].__setitem__("primary_beta", 9.0), + "primary UCB-HVI", + ), + ( + lambda raw: raw["ucb_hvi"].__setitem__("primary_bound_policy", "none"), + "clipping", + ), + ( + lambda raw: raw["observation_influence"].__setitem__( + "common_pool_size", 65536 + ), + "common pool", + ), + ( + lambda raw: raw["robust_regions"].__setitem__( + "primary_distance_threshold", 0.20 + ), + "Robust-region", + ), + ( + lambda raw: raw["execution_modes"]["fast"].__setitem__( + "anchors_per_selection_step", 4 + ), + "execution_modes", + ), + ( + lambda raw: raw["outputs"].__setitem__("tracked_private_recipes", True), + "outputs", + ), + ], +) +def test_step2c_config_fails_closed_on_safety_and_scientific_changes( + tmp_path: Path, mutator, match: str +) -> None: + changed = _copy_with_change(tmp_path, mutator) + with pytest.raises(Step2CConfigError, match=match): + load_step2c_config(changed) + + +def test_step2c_config_rejects_unknown_execution_mode() -> None: + config = load_step2c_config(CONFIG_PATH) + with pytest.raises(Step2CConfigError, match="fast.*full"): + config.mode("production") + + +def test_step2c_config_accepts_runtime_synthetic_identity(tmp_path: Path) -> None: + raw = yaml.safe_load(CONFIG_PATH.read_text(encoding="utf-8")) + raw["workbook"] = { + "source_kind": STEP2C_SYNTHETIC_SOURCE_KIND, + "path": ( + "local_outputs/d2d_step2c_robustness/" + "synthetic_ci_sources/public_fixture.xlsx" + ), + "expected_sha256": "a" * 64, + } + path = tmp_path / "resolved_synthetic_step2c.yaml" + path.write_text(yaml.safe_dump(raw, sort_keys=False), encoding="utf-8") + + config = load_step2c_config(path) + + assert config.workbook_source_kind == STEP2C_SYNTHETIC_SOURCE_KIND + assert config.workbook_expected_sha256 == "A" * 64 diff --git a/tests/test_d2d_step2c_robustness_helpers.py b/tests/test_d2d_step2c_robustness_helpers.py new file mode 100644 index 0000000..227fc2d --- /dev/null +++ b/tests/test_d2d_step2c_robustness_helpers.py @@ -0,0 +1,624 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +import zipfile + +import numpy as np +import pandas as pd +import pytest + +from mobo_kit.candidate_pool import CandidatePool +from mobo_kit.d2d_campaign import D2D_INPUT_COLUMNS +from mobo_kit.d2d_step2c_robustness import ( + StudyBatch, + _augment_model_summary_with_candidate_diagnostics, + _boundary_enrichment_table, + _build_public_summary_zip_staging, + _declared_bound_flags, + _penalty_tradeoff_table, + _publish_validated_bundle_transaction, +) +from mobo_kit.step2c_artifacts import ( + DEBUG_WATERMARK, + PUBLIC_SUMMARY_ARCHIVE_ROOT, + PUBLIC_SUMMARY_CSV_FILES, + PUBLIC_SUMMARY_FILES, + PUBLIC_SUMMARY_MANIFEST_FILE, + Step2CArtifactContractError, + validate_step2c_public_summary_archive, +) + + +def _study_batch( + penalty_label: str, + *, + score_scale: float = 1.0, + penalty_factor: float = 1.0, + changed_rows: int = 0, +) -> StudyBatch: + dimension = len(D2D_INPUT_COLUMNS) + normalized = np.full((5, dimension), 0.5, dtype=float) + normalized[:, 0] = np.linspace(0.0, 1.0, 5) + normalized[:changed_rows, 1] = 0.8 + grid = np.zeros((5, dimension), dtype=np.int64) + grid[:, 0] = np.arange(5) + scores = score_scale * np.linspace(5.0, 1.0, 5) + return StudyBatch( + run_id=penalty_label, + run_family="penalty", + core_run=True, + grid_indices=grid, + X_phys=normalized.copy(), + X_norm=normalized, + base_scores=scores, + penalized_scores=scores * penalty_factor, + model_variant="default_current", + pool_seed=73, + pool_size=32, + pool_hash="A" * 64, + beta=4.0, + bound_policy="clip_ucb", + penalty_label=penalty_label, + refinement_enabled=True, + refinement_runtime_seconds=0.1, + proposal_runtime_seconds=0.2, + ) + + +def test_declared_bound_flags_keep_unbounded_objective_unflagged() -> None: + values = np.asarray([-0.1, 0.5, 1.1]) + + below, above, outside = _declared_bound_flags(values, (0.0, 1.0)) + np.testing.assert_array_equal(below, [True, False, False]) + np.testing.assert_array_equal(above, [False, False, True]) + np.testing.assert_array_equal(outside, [True, False, True]) + + below, above, outside = _declared_bound_flags(values, (None, None)) + assert not below.any() + assert not above.any() + assert not outside.any() + + +def test_boundary_enrichment_separates_lower_and_upper_endpoints() -> None: + dimension = len(D2D_INPUT_COLUMNS) + pool_norm = np.full((4, dimension), 0.5, dtype=float) + pool_norm[:, 0] = [0.0, 1.0, 0.5, 0.0] + pool = CandidatePool( + grid_indices=np.zeros((4, dimension), dtype=np.int64), + X_phys=pool_norm.copy(), + X_norm=pool_norm, + seed=73, + draws=4, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + selected = _study_batch("no_soft_no_hard") + + table = _boundary_enrichment_table( + pool, + np.asarray([4.0, 3.0, 2.0, 1.0]), + selected, + comparison_batches=(), + ) + pool_row = table[ + (table["input_name"] == D2D_INPUT_COLUMNS[0]) & (table["group"] == "pool") + ].iloc[0] + selected_row = table[ + (table["input_name"] == D2D_INPUT_COLUMNS[0]) + & (table["group"] == "selected_batch") + ].iloc[0] + + assert pool_row["lower_boundary_count"] == 2 + assert pool_row["upper_boundary_count"] == 1 + assert pool_row["boundary_count"] == 3 + assert pool_row["lower_boundary_rate"] == pytest.approx(0.5) + assert pool_row["upper_boundary_rate"] == pytest.approx(0.25) + assert selected_row["lower_boundary_count"] == 1 + assert selected_row["upper_boundary_count"] == 1 + assert selected_row["lower_enrichment_ratio_vs_pool"] == pytest.approx(0.4) + assert selected_row["upper_enrichment_ratio_vs_pool"] == pytest.approx(0.8) + + +def test_candidate_model_summary_counts_declared_support_extrapolation() -> None: + summary = pd.DataFrame( + { + "variant_name": ["default_current"] * 3, + "objective_index": [0, 1, 2], + "objective_name": ["uniformity", "optoelectronic", "thickness"], + } + ) + candidate_rows = pd.DataFrame( + { + "run_id": ["baseline"] * 5, + "model_variant": ["default_current"] * 5, + "pred_mean_0": [-0.1, 0.2, 0.4, 0.8, 1.1], + "pred_mean_1": [-20.0, -5.0, 0.0, 5.0, 20.0], + "pred_mean_2": [0.1, 0.3, 0.5, 0.7, 0.9], + } + ) + training_y = np.asarray( + [ + [0.1, -10.0, 0.2], + [0.3, -8.0, 0.4], + [0.5, -6.0, 0.6], + [0.7, -4.0, 0.8], + [0.9, -2.0, 1.0], + ] + ) + + result = _augment_model_summary_with_candidate_diagnostics( + summary, + candidate_rows, + training_y, + ((0.0, 1.0), (None, None), (0.0, 1.0)), + ) + + counts = result.set_index("objective_index")[ + "selected_prediction_outside_declared_bounds_count" + ] + assert counts.to_dict() == {0: 2.0, 1: 0.0, 2: 0.0} + + +def test_penalty_tradeoff_covers_all_variants_without_hard_relaxation() -> None: + batches = ( + _study_batch("no_soft_no_hard"), + _study_batch("no_soft_hard_0_15", score_scale=0.99), + _study_batch("radius_0_15", score_scale=0.98, penalty_factor=0.9995), + _study_batch( + "radius_0_25", + score_scale=0.96, + penalty_factor=0.95, + changed_rows=1, + ), + _study_batch( + "radius_0_35", + score_scale=0.90, + penalty_factor=0.80, + changed_rows=2, + ), + ) + observed = np.full((2, len(D2D_INPUT_COLUMNS)), 0.5) + + table = _penalty_tradeoff_table(batches, observed_norm=observed) + + assert table["penalty_label"].tolist() == [ + "no_soft_no_hard", + "no_soft_hard_0_15", + "radius_0_15", + "radius_0_25", + "radius_0_35", + ] + assert not table["hard_distance_relaxed"].any() + reference_labels = table.set_index("penalty_label")[ + "comparison_reference_penalty_label" + ] + assert reference_labels["no_soft_hard_0_15"] == "no_soft_no_hard" + assert reference_labels["radius_0_25"] == "no_soft_hard_0_15" + classifications = table.set_index("penalty_label")[ + "penalty_activity_classification" + ] + assert classifications["radius_0_15"].startswith("implemented but effectively") + assert classifications["radius_0_25"] == ( + "active but only modestly changes diversity" + ) + assert classifications["radius_0_35"] == ("active and materially changes diversity") + + +def _publication_paths(tmp_path): + destination = tmp_path / "published" + staging = tmp_path / ".published.staging" + backup = tmp_path / ".published.backup" + zip_path = tmp_path / "published.zip" + zip_staging = tmp_path / ".published.zip.staging" + zip_backup = tmp_path / ".published.zip.backup" + return destination, staging, backup, zip_path, zip_staging, zip_backup + + +def test_atomic_publish_restores_previous_bundle_when_staging_rename_fails( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + destination, staging, backup, zip_path, zip_staging, zip_backup = ( + _publication_paths(tmp_path) + ) + destination.mkdir() + staging.mkdir() + (destination / "original.txt").write_text("original", encoding="utf-8") + (staging / "replacement.txt").write_text("replacement", encoding="utf-8") + zip_path.write_text("original zip", encoding="utf-8") + zip_staging.write_text("replacement zip", encoding="utf-8") + real_rename = type(staging).rename + + def fail_staging_rename(self, target): + if self == staging: + raise OSError("injected staging rename failure") + return real_rename(self, target) + + monkeypatch.setattr(type(staging), "rename", fail_staging_rename) + + with pytest.raises(OSError, match="injected"): + _publish_validated_bundle_transaction( + staging, + destination, + backup, + zip_staging=zip_staging, + zip_path=zip_path, + zip_backup=zip_backup, + overwrite=True, + publish_zip=True, + validate_published=lambda: None, + ) + + assert (destination / "original.txt").read_text(encoding="utf-8") == "original" + assert zip_path.read_text(encoding="utf-8") == "original zip" + assert not backup.exists() + assert not zip_backup.exists() + assert (staging / "replacement.txt").is_file() + assert not zip_staging.exists() + + +def test_atomic_publish_restores_directory_and_zip_when_zip_replace_fails( + tmp_path, monkeypatch: pytest.MonkeyPatch +) -> None: + destination, staging, backup, zip_path, zip_staging, zip_backup = ( + _publication_paths(tmp_path) + ) + destination.mkdir() + staging.mkdir() + (destination / "original.txt").write_text("original", encoding="utf-8") + (staging / "replacement.txt").write_text("replacement", encoding="utf-8") + zip_path.write_text("original zip", encoding="utf-8") + zip_staging.write_text("replacement zip", encoding="utf-8") + + def fail_zip_replace(source, target): + if source == zip_staging and target == zip_path: + raise OSError("injected ZIP publication failure") + raise AssertionError("Unexpected os.replace call") + + monkeypatch.setattr("mobo_kit.d2d_step2c_robustness.os.replace", fail_zip_replace) + + with pytest.raises(OSError, match="ZIP publication"): + _publish_validated_bundle_transaction( + staging, + destination, + backup, + zip_staging=zip_staging, + zip_path=zip_path, + zip_backup=zip_backup, + overwrite=True, + publish_zip=True, + validate_published=lambda: None, + ) + + assert (destination / "original.txt").read_text(encoding="utf-8") == "original" + assert zip_path.read_text(encoding="utf-8") == "original zip" + assert not backup.exists() + assert not zip_backup.exists() + assert not staging.exists() + assert not zip_staging.exists() + + +def test_atomic_publish_restores_both_originals_when_final_validation_fails( + tmp_path, +) -> None: + destination, staging, backup, zip_path, zip_staging, zip_backup = ( + _publication_paths(tmp_path) + ) + destination.mkdir() + staging.mkdir() + (destination / "original.txt").write_text("original", encoding="utf-8") + (staging / "replacement.txt").write_text("replacement", encoding="utf-8") + zip_path.write_text("original zip", encoding="utf-8") + zip_staging.write_text("replacement zip", encoding="utf-8") + + def fail_final_validation(): + assert (destination / "replacement.txt").is_file() + assert zip_path.read_text(encoding="utf-8") == "replacement zip" + raise RuntimeError("injected final validation failure") + + with pytest.raises(RuntimeError, match="final validation"): + _publish_validated_bundle_transaction( + staging, + destination, + backup, + zip_staging=zip_staging, + zip_path=zip_path, + zip_backup=zip_backup, + overwrite=True, + publish_zip=True, + validate_published=fail_final_validation, + ) + + assert (destination / "original.txt").read_text(encoding="utf-8") == "original" + assert zip_path.read_text(encoding="utf-8") == "original zip" + assert not backup.exists() + assert not zip_backup.exists() + assert not staging.exists() + assert not zip_staging.exists() + + +def test_atomic_publish_without_zip_removes_stale_zip_only_after_validation( + tmp_path, +) -> None: + destination, staging, backup, zip_path, zip_staging, zip_backup = ( + _publication_paths(tmp_path) + ) + destination.mkdir() + staging.mkdir() + (destination / "original.txt").write_text("original", encoding="utf-8") + (staging / "replacement.txt").write_text("replacement", encoding="utf-8") + zip_path.write_text("stale zip", encoding="utf-8") + + def validate(): + assert (destination / "replacement.txt").is_file() + assert not zip_path.exists() + assert zip_backup.read_text(encoding="utf-8") == "stale zip" + return "validated" + + result = _publish_validated_bundle_transaction( + staging, + destination, + backup, + zip_staging=zip_staging, + zip_path=zip_path, + zip_backup=zip_backup, + overwrite=True, + publish_zip=False, + validate_published=validate, + ) + + assert result == "validated" + assert (destination / "replacement.txt").is_file() + assert not zip_path.exists() + assert not backup.exists() + assert not zip_backup.exists() + + +def _public_summary_source(tmp_path: Path) -> tuple[Path, dict]: + source = tmp_path / "full_private_bundle" + source.mkdir() + for relative in PUBLIC_SUMMARY_CSV_FILES: + pd.DataFrame( + { + "metric_name": ["aggregate"], + "metric_value": [1.0], + "debug_only": [True], + "approved_for_experiment": [False], + "approved_for_production": [False], + "candidate_status": [DEBUG_WATERMARK], + } + ).to_csv(source / relative, index=False) + private_manifest = { + "method_version": "synthetic-public-export-test-v1", + "mode": "full", + "input_data_kind": "private_pinned_workbook", + "git_commit": "a" * 40, + "objective_order": [ + "uniformity_score", + "optoelectronic_score", + "thickness_score", + ], + "reference_point": [-0.01, -10.0, -0.01], + "objective_bounds": [[0.0, 1.0], [None, None], [0.0, 1.0]], + "moment_method": "analytic_identity", + "robust_region_count": 7, + "shortlist_count": 5, + "consensus_passed": False, + "consensus_checks": {"stable": False}, + "consensus_observed": {"region_count": 7}, + "runtime_versions": {"python": "test"}, + # The public builder must ignore all of these private-only values. + "workbook_path": r"C:\Users\ExampleUser\private_campaign_input.xlsx", + "config_path": r"C:\Users\ExampleUser\repo\private.yaml", + "git_status_at_start": ["?? local_inputs/private_campaign_input.xlsx"], + "hardware": {"profile": "ExampleUser"}, + } + return source, private_manifest + + +def _rewrite_public_archive( + archive_path: Path, + update, +) -> None: + with zipfile.ZipFile(archive_path, mode="r") as archive: + payloads = {info.filename: archive.read(info) for info in archive.infolist()} + update(payloads) + with zipfile.ZipFile( + archive_path, mode="w", compression=zipfile.ZIP_DEFLATED + ) as archive: + for name, payload in sorted(payloads.items()): + archive.writestr(name, payload) + + +def _replace_public_csv_and_hash( + payloads: dict[str, bytes], relative: str, replacement: bytes +) -> None: + csv_entry = f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{relative}" + payloads[csv_entry] = replacement + manifest_entry = f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{PUBLIC_SUMMARY_MANIFEST_FILE}" + manifest = json.loads(payloads[manifest_entry].decode("utf-8")) + manifest["included_files"][relative] = ( + hashlib.sha256(replacement).hexdigest().upper() + ) + payloads[manifest_entry] = json.dumps(manifest, indent=2, sort_keys=True).encode( + "utf-8" + ) + + +def test_public_summary_builder_strips_private_provenance_and_recipes( + tmp_path: Path, +) -> None: + source, private_manifest = _public_summary_source(tmp_path) + archive_path = tmp_path / "public_summary.zip" + + _build_public_summary_zip_staging( + source, + archive_path, + private_manifest=private_manifest, + overwrite=False, + ) + validated = validate_step2c_public_summary_archive( + archive_path, forbidden_profile_strings=("ExampleUser",) + ) + + assert set(validated.entry_sha256) == set(PUBLIC_SUMMARY_FILES) + with zipfile.ZipFile(archive_path, mode="r") as archive: + names = archive.namelist() + combined_text = "\n".join( + archive.read(name).decode("utf-8") for name in names + ).casefold() + assert set(names) == { + f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{relative}" for relative in PUBLIC_SUMMARY_FILES + } + for forbidden in ( + "c:\\users\\", + "/users/", + "/home/", + "exampleuser", + "private_campaign_input.xlsx", + "workbook_path", + "config_path", + "git_status_at_start", + "hardware", + "r1_robust_shortlist_debug.csv", + "study_candidates_long.csv", + ): + assert forbidden not in combined_text + + +@pytest.mark.parametrize( + ("injected", "forbidden_profiles", "message"), + [ + (r"C:\Users\ExampleUser\private.csv", (), "Windows absolute path"), + ("/Users/example-user/private.csv", (), "profile path"), + ("/home/example-user/private.csv", (), "profile path"), + ("/private/location/data.csv", (), "POSIX absolute path"), + ("private_campaign_input.xlsx", (), "workbook filename"), + ("ExampleUser", ("ExampleUser",), "profile identifier"), + ], +) +def test_public_summary_validator_rejects_paths_profiles_and_workbook_names( + tmp_path: Path, + injected: str, + forbidden_profiles: tuple[str, ...], + message: str, +) -> None: + source, private_manifest = _public_summary_source(tmp_path) + archive_path = tmp_path / "public_summary.zip" + _build_public_summary_zip_staging( + source, + archive_path, + private_manifest=private_manifest, + overwrite=False, + ) + relative = PUBLIC_SUMMARY_CSV_FILES[0] + + def inject(payloads: dict[str, bytes]) -> None: + csv_entry = f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{relative}" + replacement = ( + payloads[csv_entry] + .decode("utf-8") + .replace("aggregate", injected) + .encode("utf-8") + ) + _replace_public_csv_and_hash(payloads, relative, replacement) + + _rewrite_public_archive(archive_path, inject) + + with pytest.raises(Step2CArtifactContractError, match=message): + validate_step2c_public_summary_archive( + archive_path, forbidden_profile_strings=forbidden_profiles + ) + + +def test_public_summary_validator_rejects_recipe_header_and_private_file( + tmp_path: Path, +) -> None: + source, private_manifest = _public_summary_source(tmp_path) + archive_path = tmp_path / "public_summary.zip" + _build_public_summary_zip_staging( + source, + archive_path, + private_manifest=private_manifest, + overwrite=False, + ) + relative = PUBLIC_SUMMARY_CSV_FILES[0] + + def add_recipe_header(payloads: dict[str, bytes]) -> None: + csv_entry = f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/{relative}" + lines = payloads[csv_entry].decode("utf-8").splitlines() + replacement = "\n".join( + [f"speed_1,{lines[0]}", *[f"12,{line}" for line in lines[1:]]] + ).encode("utf-8") + _replace_public_csv_and_hash(payloads, relative, replacement) + + _rewrite_public_archive(archive_path, add_recipe_header) + with pytest.raises(Step2CArtifactContractError, match="recipe/sample-level"): + validate_step2c_public_summary_archive(archive_path) + + _build_public_summary_zip_staging( + source, + archive_path, + private_manifest=private_manifest, + overwrite=True, + ) + + def add_private_file(payloads: dict[str, bytes]) -> None: + payloads[f"{PUBLIC_SUMMARY_ARCHIVE_ROOT}/r1_consensus_debug_batch.csv"] = ( + b"speed_1\n12\n" + ) + + _rewrite_public_archive(archive_path, add_private_file) + with pytest.raises(Step2CArtifactContractError, match="private artifact"): + validate_step2c_public_summary_archive(archive_path) + + +def test_atomic_publish_restores_public_and_private_zips_when_public_scan_fails( + tmp_path: Path, +) -> None: + destination, staging, backup, zip_path, zip_staging, zip_backup = ( + _publication_paths(tmp_path) + ) + private_zip_path = tmp_path / "published_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip" + private_zip_staging = tmp_path / ".published_PRIVATE.zip.staging" + private_zip_backup = tmp_path / ".published_PRIVATE.zip.backup" + destination.mkdir() + staging.mkdir() + (destination / "original.txt").write_text("original", encoding="utf-8") + (staging / "replacement.txt").write_text("replacement", encoding="utf-8") + zip_path.write_text("original public", encoding="utf-8") + zip_staging.write_text("replacement public", encoding="utf-8") + private_zip_path.write_text("original private", encoding="utf-8") + private_zip_staging.write_text("replacement private", encoding="utf-8") + + def fail_public_scan() -> None: + assert zip_path.read_text(encoding="utf-8") == "replacement public" + assert private_zip_path.read_text(encoding="utf-8") == "replacement private" + raise RuntimeError("injected public privacy scan failure") + + with pytest.raises(RuntimeError, match="privacy scan"): + _publish_validated_bundle_transaction( + staging, + destination, + backup, + zip_staging=zip_staging, + zip_path=zip_path, + zip_backup=zip_backup, + overwrite=True, + publish_zip=True, + validate_published=lambda: None, + private_zip_staging=private_zip_staging, + private_zip_path=private_zip_path, + private_zip_backup=private_zip_backup, + publish_private_zip=True, + validate_public_zip=fail_public_scan, + ) + + assert (destination / "original.txt").read_text(encoding="utf-8") == "original" + assert zip_path.read_text(encoding="utf-8") == "original public" + assert private_zip_path.read_text(encoding="utf-8") == "original private" + assert not backup.exists() + assert not zip_backup.exists() + assert not private_zip_backup.exists() diff --git a/tests/test_d2d_step2c_synthetic.py b/tests/test_d2d_step2c_synthetic.py new file mode 100644 index 0000000..1f010bf --- /dev/null +++ b/tests/test_d2d_step2c_synthetic.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path +import shutil +from uuid import uuid4 + +import numpy as np + +from mobo_kit.d2d_campaign import OffGridObservedException, load_d2d_workbook_frame +from mobo_kit.d2d_step2c_config import load_step2c_config +from mobo_kit.d2d_step2c_synthetic import ( + run_synthetic_step2c_fast_ci, + write_sanitized_step2c_workbook, +) +from mobo_kit.step2c_artifacts import validate_step2c_artifact_bundle + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +CONFIG_PATH = REPOSITORY_ROOT / "configs" / "d2d_step2c_debug.yaml" +OUTPUT_ROOT = REPOSITORY_ROOT / "local_outputs" / "d2d_step2c_robustness" + + +def _proof(path: Path) -> tuple[str, int, int]: + return ( + hashlib.sha256(path.read_bytes()).hexdigest().upper(), + path.stat().st_mtime_ns, + path.stat().st_size, + ) + + +def test_sanitized_workbook_uses_real_v3_read_only_adapter(tmp_path: Path) -> None: + config = load_step2c_config(CONFIG_PATH) + workbook = write_sanitized_step2c_workbook( + tmp_path / "sanitized_step2c.xlsx", config + ) + before = _proof(workbook) + inherited = config.off_grid_exceptions[0] + dimension = tuple(config.design.names).index(inherited.input_name) + grid = np.asarray(config.design.var_array[dimension], dtype=float) + synthetic_exception = OffGridObservedException( + sample_id=inherited.sample_id, + input_name=inherited.input_name, + observed_value=float(inherited.observed_value), + reason="sanitized synthetic off-grid fixture", + ) + + frame, audit = load_d2d_workbook_frame( + workbook, + expected_sample_ids=config.expected_sample_ids, + allowed_input_exceptions=(synthetic_exception,), + ) + + assert frame["Sample number"].tolist() == list(config.expected_sample_ids) + assert audit.profile == config.base.workbook_profile + assert audit.active_sheet == config.base.workbook_sheet + assert audit.used_range == config.base.expected_content_range + assert audit.input_rows_valid is True + assert before == _proof(workbook) + observed = float( + frame.loc[ + frame["Sample number"].eq(synthetic_exception.sample_id), + synthetic_exception.input_name, + ].iloc[0] + ) + assert not np.any(np.isclose(observed, grid, rtol=0.0, atol=1.0e-12)) + assert observed == inherited.observed_value + + +def test_synthetic_fast_ci_runs_full_orchestration_and_atomic_publication() -> None: + run_name = f"pytest_synthetic_ci_{uuid4().hex}" + output = OUTPUT_ROOT / run_name + source = OUTPUT_ROOT / "synthetic_ci_sources" / f"{run_name}.xlsx" + resolved_source = ( + OUTPUT_ROOT / "synthetic_ci_sources" / f"{run_name}_resolved_config.yaml" + ) + archive = output.with_suffix(".zip") + try: + result = run_synthetic_step2c_fast_ci( + CONFIG_PATH, + output, + create_portable_zip=True, + nested_unique_sizes=(32, 64, 128, 256), + anchors_per_selection_step=1, + omitted_sample_ids=None, + mc_comparison_samples=64, + ) + validated = validate_step2c_artifact_bundle( + output, repository_root=REPOSITORY_ROOT + ) + + assert result.mode == "fast" + assert result.consensus.passed is False + assert result.run_manifest["input_data_kind"] == "sanitized_synthetic_ci" + assert result.run_manifest["workbook_writeback_performed"] is False + assert result.run_manifest["real_r2_proposal_generated"] is False + assert validated.workbook_path == source.resolve() + assert validated.artifact_sha256 == result.artifact_hashes + assert archive.is_file() + assert source.is_file() + assert resolved_source.is_file() + finally: + if output.is_dir(): + shutil.rmtree(output) + if archive.is_file(): + archive.unlink() + if source.is_file(): + source.unlink() + if resolved_source.is_file(): + resolved_source.unlink() + source_parent = source.parent + if source_parent.is_dir() and not any(source_parent.iterdir()): + source_parent.rmdir() diff --git a/tests/test_design.py b/tests/test_design.py index 3898803..33d7f2a 100644 --- a/tests/test_design.py +++ b/tests/test_design.py @@ -1,38 +1,114 @@ -import sys -import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - import numpy as np -from src.design import ( +import pytest + +from mobo_kit.design import ( + InputSpec, + build_design, + build_design_from_config, + build_input_spec_list, make_linspace, - get_variable_space, - get_parameter_space, - generate_initial_design, ) -from src.utils import get_closest_array -def test_make_linspace(): - arr = make_linspace(0, 1, 0.2) - assert np.allclose(arr, [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]), "Incorrect linspace result" -def test_variable_space_shape(): - var_array = get_variable_space() - assert isinstance(var_array, list) and len(var_array) == 8 - assert all(isinstance(v, np.ndarray) or isinstance(v, list) for v in var_array) +def _valid_inputs(): + return [ + { + "name": "speed", + "unit": "rpm", + "start": 1000, + "stop": 2000, + "step": 250, + }, + { + "name": "time", + "unit": "s", + "start": 5, + "stop": 20, + "step": 5, + }, + ] + + +def test_make_linspace_preserves_requested_step_and_endpoints(): + grid = make_linspace(0.0, 1.0, 0.2) + + assert np.array_equal(grid, np.array([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])) + assert np.allclose(np.diff(grid), 0.2) + + +def test_make_linspace_allows_a_single_fixed_value(): + assert np.array_equal(make_linspace(3.5, 3.5, 0.25), np.array([3.5])) + + +@pytest.mark.parametrize( + ("start", "stop", "step", "message"), + [ + (0.0, 1.0, 0.3, "not aligned"), + (1.0, 0.0, 0.1, "stop must be"), + (0.0, 1.0, 0.0, "step must be > 0"), + (0.0, 1.0, -0.1, "step must be > 0"), + (0.0, np.inf, 0.1, "must be finite"), + ], +) +def test_make_linspace_rejects_ambiguous_grids(start, stop, step, message): + with pytest.raises(ValueError, match=message): + make_linspace(start, stop, step) + + +def test_build_design_from_config_constructs_exact_grids(): + design = build_design_from_config({"inputs": _valid_inputs()}) + + assert design.names == ["speed", "time"] + assert design.units == ["rpm", "s"] + assert np.array_equal( + design.var_list[0], np.array([1000.0, 1250.0, 1500.0, 1750.0, 2000.0]) + ) + assert np.array_equal(design.var_list[1], np.array([5.0, 10.0, 15.0, 20.0])) + assert np.array_equal(design.lowers, np.array([1000.0, 5.0])) + assert np.array_equal(design.uppers, np.array([2000.0, 20.0])) + + +@pytest.mark.parametrize( + ("inputs", "message"), + [ + ([], "non-empty list"), + ([{"name": "", "start": 0, "stop": 1, "step": 1}], "non-empty string"), + ([{"name": "x", "start": 0, "stop": 1}], "missing required"), + ([{"name": "x", "start": "bad", "stop": 1, "step": 1}], "finite number"), + ([{"name": "x", "start": 0, "stop": np.nan, "step": 1}], "must be finite"), + ([{"name": "x", "start": 1, "stop": 0, "step": 1}], "stop >= start"), + ([{"name": "x", "start": 0, "stop": 1, "step": -1}], "step.*> 0"), + ([{"name": "x", "start": 0, "stop": 1, "step": 0.3}], "not aligned"), + ( + [ + {"name": " x ", "start": 0, "stop": 1, "step": 1}, + {"name": "x", "start": 0, "stop": 1, "step": 1}, + ], + "duplicate name", + ), + ], +) +def test_input_schema_validation_is_explicit(inputs, message): + with pytest.raises(ValueError, match=message): + build_input_spec_list(inputs) + + +def test_rounding_that_collapses_grid_points_is_rejected(): + with pytest.raises(ValueError, match="duplicate values"): + InputSpec(name="x", start=0.0, stop=0.02, step=0.005, decimals=2) + -def test_parameter_space_format(): - space = get_parameter_space() - assert len(space.parameters) == 8 - names = [p.name for p in space.parameters] - assert "temp" in names and "humidity" in names +def test_build_design_revalidates_mutated_specs_and_unique_names(): + first = InputSpec("x", 0, 1, 1) + second = InputSpec("y", 0, 1, 1) + second.name = "x" -def test_design_sampling_and_snapping(): - raw_samples = generate_initial_design(n_samples=5) - assert raw_samples.shape == (5, 8), "Design sample shape incorrect" + with pytest.raises(ValueError, match="duplicate name"): + build_design([first, second]) - var_array = get_variable_space() - snapped = get_closest_array(raw_samples, var_array) - assert snapped.shape == (5, 8) - for i in range(8): - assert np.all(np.isin(snapped[:, i], var_array[i])), f"Column {i} has invalid snapped values" \ No newline at end of file +def test_build_design_requires_input_specs(): + with pytest.raises(ValueError, match="At least one"): + build_design([]) + with pytest.raises(TypeError, match="InputSpec"): + build_design([{"name": "x"}]) diff --git a/tests/test_discrete_refinement.py b/tests/test_discrete_refinement.py new file mode 100644 index 0000000..81a1aa7 --- /dev/null +++ b/tests/test_discrete_refinement.py @@ -0,0 +1,258 @@ +import numpy as np + +from mobo_kit.candidate_pool import CandidatePool +from mobo_kit.design import InputSpec, build_design +from mobo_kit.discrete_refinement import ( + CachedGridScorer, + RefinementConfig, + grid_indices_to_physical_and_normalized, + propose_refined_discrete_batch, + refine_discrete_acquisition_anchors, +) + + +def _design(): + return build_design( + [ + InputSpec("x", 0, 4, 1), + InputSpec("y", 0, 3, 1), + ] + ) + + +def _pool(design) -> CandidatePool: + indices = np.asarray([[x, y] for x in range(5) for y in range(4)], dtype=np.int64) + physical, normalized = grid_indices_to_physical_and_normalized(indices, design) + return CandidatePool( + grid_indices=indices, + X_phys=physical, + X_norm=normalized, + seed=73, + draws=len(indices), + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + + +def test_refinement_reaches_coordinate_local_optimum_and_is_monotone() -> None: + design = _design() + + def score(rows: np.ndarray) -> np.ndarray: + return 100.0 - (rows[:, 0] - 3) ** 2 - 2.0 * (rows[:, 1] - 2) ** 2 + + refined, trace = refine_discrete_acquisition_anchors( + design, + np.asarray([[0, 0], [4, 3]], dtype=np.int64), + score, + config=RefinementConfig( + anchors_per_selection_step=2, + max_sweeps=10, + improvement_tolerance=1e-12, + radius=None, + min_batch_distance=0.0, + ), + selection_step=1, + ) + + assert {item.refined_grid_index for item in refined} == {(3, 2)} + assert all(item.termination_reason == "no_improvement" for item in refined) + assert all(row.score_after >= row.score_before for row in trace) + assert any(row.accepted_move for row in trace) + assert all(np.isfinite(row.base_score_before) for row in trace) + assert all(np.isfinite(row.base_score_after) for row in trace) + assert all(np.isfinite(row.penalized_log_score_before) for row in trace) + assert all(np.isfinite(row.penalized_log_score_after) for row in trace) + assert all(row.termination_reason in {None, "no_improvement"} for row in trace) + + +def test_refinement_tie_break_and_trace_are_deterministic() -> None: + design = _design() + config = RefinementConfig( + anchors_per_selection_step=1, + max_sweeps=3, + radius=None, + min_batch_distance=0.0, + ) + + def flat(rows: np.ndarray) -> np.ndarray: + return np.ones(rows.shape[0]) + + first = refine_discrete_acquisition_anchors( + design, + np.asarray([[2, 2]], dtype=np.int64), + flat, + config=config, + selection_step=1, + ) + second = refine_discrete_acquisition_anchors( + design, + np.asarray([[2, 2]], dtype=np.int64), + flat, + config=config, + selection_step=1, + ) + + assert first == second + assert first[0][0].refined_grid_index == (2, 2) + assert first[0][0].termination_reason == "no_improvement" + + +def test_grid_normalization_matches_physical_bound_canonicalization_bitwise() -> None: + design = build_design([InputSpec("decimal_axis", 1.0, 2.0, 0.05, decimals=2)]) + indices = np.arange(design.var_array[0].size, dtype=np.int64)[:, None] + + physical, normalized = grid_indices_to_physical_and_normalized(indices, design) + expected = (physical - design.lowers) / (design.uppers - design.lowers) + index_fraction = indices / float(design.var_array[0].size - 1) + + np.testing.assert_array_equal(normalized, expected) + assert np.any(normalized != index_fraction) + + +def test_refined_batch_enforces_observed_exclusion_and_hard_distance() -> None: + design = _design() + pool = _pool(design) + observed_grid = np.asarray([[4, 3]], dtype=np.int64) + _, observed_norm = grid_indices_to_physical_and_normalized(observed_grid, design) + + def score(rows: np.ndarray) -> np.ndarray: + # The forbidden observed point is the nominal maximum. + return 100.0 + 10.0 * rows[:, 0] + rows[:, 1] + + result = propose_refined_discrete_batch( + pool, + design, + score, + q=2, + config=RefinementConfig( + anchors_per_selection_step=6, + max_sweeps=5, + radius=None, + min_batch_distance=0.75, + ), + observed_grid_indices=observed_grid, + observed_norm=observed_norm, + ) + + assert result.grid_indices.shape == (2, 2) + assert not np.any(np.all(result.grid_indices == observed_grid[0], axis=1)) + assert np.unique(result.grid_indices, axis=0).shape[0] == 2 + assert np.linalg.norm(result.X_norm[0] - result.X_norm[1]) >= 0.75 + assert all(value > 0 for value in result.base_scores) + assert result.distinct_converged_optima >= 2 + + +def test_refined_batch_forwards_pending_rows_through_avoid_grid_indices() -> None: + design = _design() + pool = _pool(design) + pending_grid = np.asarray([[4, 3]], dtype=np.int64) + + def score(rows: np.ndarray) -> np.ndarray: + return 100.0 + 10.0 * rows[:, 0] + rows[:, 1] + + result = propose_refined_discrete_batch( + pool, + design, + score, + q=1, + config=RefinementConfig( + anchors_per_selection_step=6, + max_sweeps=5, + radius=None, + min_batch_distance=0.0, + ), + avoid_grid_indices=pending_grid, + ) + + assert result.grid_indices.shape == (1, 2) + assert not np.array_equal(result.grid_indices[0], pending_grid[0]) + + +def test_refinement_reports_max_sweeps_without_off_grid_moves() -> None: + design = _design() + + def score(rows: np.ndarray) -> np.ndarray: + return 10.0 + rows[:, 0] + rows[:, 0] * rows[:, 1] + + refined, trace = refine_discrete_acquisition_anchors( + design, + np.asarray([[0, 0]], dtype=np.int64), + score, + config=RefinementConfig( + anchors_per_selection_step=1, + max_sweeps=1, + radius=None, + min_batch_distance=0.0, + ), + selection_step=1, + ) + + assert refined[0].termination_reason == "max_sweeps" + assert trace[-1].termination_reason == "max_sweeps" + assert np.all(np.asarray(refined[0].refined_grid_index) >= 0) + assert refined[0].refined_grid_index[0] < 5 + assert refined[0].refined_grid_index[1] < 4 + + +def test_cached_grid_scorer_deduplicates_vectorized_requests() -> None: + calls: list[np.ndarray] = [] + + def score(rows: np.ndarray) -> np.ndarray: + calls.append(rows.copy()) + return rows.sum(axis=1).astype(float) + + cached = CachedGridScorer(score, dimension=2) + requested = np.asarray([[1, 2], [1, 2], [2, 3]], dtype=np.int64) + np.testing.assert_array_equal(cached(requested), [3.0, 3.0, 5.0]) + np.testing.assert_array_equal(cached(requested[::-1]), [5.0, 3.0, 3.0]) + + assert cached.cache_size == 2 + assert len(calls) == 1 + assert calls[0].shape == (2, 2) + + +def test_later_steps_readd_eligible_previously_refined_optima() -> None: + design = build_design([InputSpec("x", 0, 2, 1), InputSpec("y", 0, 2, 1)]) + pool_indices = np.asarray([[1, 0], [1, 2]], dtype=np.int64) + physical, normalized = grid_indices_to_physical_and_normalized(pool_indices, design) + pool = CandidatePool( + grid_indices=pool_indices, + X_phys=physical, + X_norm=normalized, + seed=73, + draws=2, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + table = np.ones((3, 3), dtype=float) + table[0, 0] = 9.0 + table[1, 0] = 5.0 + table[1, 2] = 8.0 + table[2, 2] = 10.0 + + def score(rows: np.ndarray) -> np.ndarray: + return table[rows[:, 0], rows[:, 1]] + + result = propose_refined_discrete_batch( + pool, + design, + score, + q=2, + config=RefinementConfig( + anchors_per_selection_step=2, + max_sweeps=5, + radius=None, + min_batch_distance=0.3, + ), + ) + + assert any( + anchor.selection_step == 2 and anchor.anchor_pool_index is None + for anchor in result.anchors + ) + assert all(anchor.accepted_move_count >= 0 for anchor in result.anchors) + assert all( + isinstance(anchor.changed_dimensions, tuple) for anchor in result.anchors + ) diff --git a/tests/test_lhs.py b/tests/test_lhs.py new file mode 100644 index 0000000..6ddd594 --- /dev/null +++ b/tests/test_lhs.py @@ -0,0 +1,165 @@ +import numpy as np +import pandas as pd +import pytest + +from mobo_kit.design import InputSpec, build_design +from mobo_kit.lhs import lhs_dataframe, lhs_dataframe_optimized + + +def _design(): + return build_design( + [ + InputSpec("speed", 0.0, 1.0, 0.25, unit="m/min"), + InputSpec("temperature", 20.0, 24.0, 1.0, unit="C"), + InputSpec("time", 5.0, 15.0, 5.0, unit="s"), + ] + ) + + +def _assert_exact_grid_membership(frame: pd.DataFrame, design) -> None: + for column, grid in zip(design.names, design.var_list): + assert np.all(np.isin(frame[column].to_numpy(), grid)) + + +def test_seeded_lhs_is_deterministic_unique_and_exactly_sized(): + design = _design() + kwargs = dict( + design=design, + n=12, + seed=123, + samples_per_attempt=40, + max_attempts=5, + ) + + first = lhs_dataframe_optimized(**kwargs) + second = lhs_dataframe_optimized(**kwargs) + + pd.testing.assert_frame_equal(first, second) + assert first.shape == (12, 3) + assert list(first.columns) == design.names + assert len(first.drop_duplicates()) == 12 + _assert_exact_grid_membership(first, design) + + +def test_different_seed_changes_at_least_one_condition(): + design = _design() + first = lhs_dataframe_optimized( + design=design, + n=12, + seed=123, + samples_per_attempt=40, + max_attempts=5, + ) + second = lhs_dataframe_optimized( + design=design, + n=12, + seed=124, + samples_per_attempt=40, + max_attempts=5, + ) + + assert not first.equals(second) + + +def test_compatibility_wrapper_has_the_same_strict_contract(): + design = _design() + frame = lhs_dataframe( + design, + n=8, + seed=77, + samples_per_attempt=24, + max_attempts=5, + ) + + assert frame.shape == (8, 3) + assert len(frame.drop_duplicates()) == 8 + _assert_exact_grid_membership(frame, design) + + +def test_constraints_are_applied_after_grid_snapping(): + design = _design() + calls = [] + + def snapped_constraint(X, constrained_design): + for column, grid in enumerate(constrained_design.var_list): + assert np.all(np.isin(X[:, column], grid)) + calls.append(X.copy()) + speed_index = constrained_design.names.index("speed") + return X[:, speed_index] >= 0.75 + + frame = lhs_dataframe_optimized( + design, + n=5, + seed=5, + row_constraints=[snapped_constraint], + samples_per_attempt=30, + max_attempts=5, + ) + + assert calls + assert frame.shape == (5, 3) + assert np.all(frame["speed"] >= 0.75) + _assert_exact_grid_membership(frame, design) + + +def test_impossible_constraint_raises_without_unconstrained_fallback(): + design = _design() + + def reject_everything(X, _design): + return np.zeros(X.shape[0], dtype=bool) + + with pytest.raises(RuntimeError, match="Unable to generate exactly n=3"): + lhs_dataframe_optimized( + design, + n=3, + seed=9, + row_constraints=reject_everything, + samples_per_attempt=20, + max_attempts=2, + ) + + +def test_request_larger_than_grid_raises_before_sampling(): + design = build_design([InputSpec("x", 0, 1, 1), InputSpec("y", 0, 1, 1)]) + + with pytest.raises(ValueError, match="only 4 unique grid combinations"): + lhs_dataframe_optimized(design, n=5, seed=1) + + +def test_max_abs_corr_is_a_hard_requirement(): + design = build_design([InputSpec("x", 0, 1, 1), InputSpec("y", 0, 1, 1)]) + + # A two-row Latin design on two binary dimensions varies in both columns; + # their absolute Pearson correlation is necessarily 1. + with pytest.raises(RuntimeError, match=r"required <= 0\.500000"): + lhs_dataframe_optimized( + design, + n=2, + seed=42, + max_abs_corr=0.5, + samples_per_attempt=2, + batch_size=2, + subset_tries=10, + max_attempts=1, + ) + + +def test_returned_design_satisfies_configured_correlation_limit(): + design = build_design([InputSpec("x", 0, 1, 1), InputSpec("y", 0, 1, 1)]) + + frame = lhs_dataframe_optimized( + design, + n=4, + seed=3, + max_abs_corr=0.0, + samples_per_attempt=4, + max_attempts=10, + ) + + correlation = np.corrcoef(frame.to_numpy(), rowvar=False)[0, 1] + assert abs(correlation) <= 0.0 + + +def test_continuous_unsnapped_output_is_rejected_for_campaign_use(): + with pytest.raises(ValueError, match="requires snap_to_grids=True"): + lhs_dataframe_optimized(_design(), n=3, seed=1, snap_to_grids=False) diff --git a/tests/test_model_validation.py b/tests/test_model_validation.py new file mode 100644 index 0000000..e9302f7 --- /dev/null +++ b/tests/test_model_validation.py @@ -0,0 +1,419 @@ +from __future__ import annotations + +import warnings + +import numpy as np +import pandas as pd +import pytest +import torch + +import mobo_kit.model_validation as validation_module +import mobo_kit.models as models_module +from mobo_kit.model_validation import ( + CONSERVATIVE, + DEFAULT_CURRENT, + ModelFitCache, + ModelFitError, + ModelVariantSpec, + compute_prediction_metrics, + extract_model_hyperparameters, + fit_model_variant, + fit_warnings_frame, + model_variant_spec, + run_exact_loocv, + validate_model_variant, +) +from mobo_kit.models import fit_gp_models + + +def _training_data() -> tuple[torch.Tensor, torch.Tensor, tuple[int, ...]]: + X = torch.tensor( + [ + [0.0, 0.0], + [0.2, 0.8], + [0.5, 0.3], + [0.8, 0.9], + [1.0, 0.1], + ], + dtype=torch.double, + ) + Y = torch.stack( + ( + 0.2 + 0.7 * X[:, 0] + 0.1 * X[:, 1], + -1.0 + 0.3 * X[:, 0] - 0.5 * X[:, 1], + ), + dim=1, + ) + return X, Y, (1, 2, 3, 4, 5) + + +def _skip_optimization(mll: object) -> object: + return mll + + +def test_model_variant_contracts_are_fixed() -> None: + assert model_variant_spec("default_current") is DEFAULT_CURRENT + assert DEFAULT_CURRENT.min_noise == pytest.approx(1.0e-3) + assert DEFAULT_CURRENT.min_lengthscale is None + assert model_variant_spec("conservative") is CONSERVATIVE + assert CONSERVATIVE.min_noise == pytest.approx(0.01) + assert CONSERVATIVE.min_lengthscale == pytest.approx(0.05) + + with pytest.raises(ValueError, match="default_current"): + ModelVariantSpec("default_current", min_noise=0.01, min_lengthscale=None) + with pytest.raises(ValueError, match="conservative"): + ModelVariantSpec("conservative", min_noise=0.01, min_lengthscale=0.01) + with pytest.raises(ValueError, match="Unsupported"): + model_variant_spec("mystery") + + +def test_default_current_matches_step2b_model_construction( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", _skip_optimization) + monkeypatch.setattr(models_module, "fit_gpytorch_mll", _skip_optimization) + X, Y, sample_ids = _training_data() + + torch.manual_seed(73) + strict = fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + seed=73, + ) + torch.manual_seed(73) + historical = fit_gp_models(X, Y) + query = X[:3] + with torch.no_grad(): + strict_posterior = strict.model.posterior(query) + historical_posterior = historical.posterior(query) + + assert all( + type(strict_gp.covar_module.base_kernel) + is type(historical_gp.covar_module.base_kernel) + for strict_gp, historical_gp in zip(strict.model.models, historical.models) + ) + torch.testing.assert_close(strict_posterior.mean, historical_posterior.mean) + torch.testing.assert_close(strict_posterior.variance, historical_posterior.variance) + + +def test_conservative_constraints_and_hyperparameter_extraction( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", _skip_optimization) + X, Y, sample_ids = _training_data() + record = fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=CONSERVATIVE, + ) + + for gp in record.model.models: + noise_floor = gp.likelihood.noise_covar.raw_noise_constraint.lower_bound + lengthscale_floor = ( + gp.covar_module.base_kernel.raw_lengthscale_constraint.lower_bound + ) + assert float(noise_floor) == pytest.approx(0.01) + assert float(lengthscale_floor) == pytest.approx(0.05) + assert float(gp.likelihood.noise.detach()) > 0.01 + assert torch.all(gp.covar_module.base_kernel.lengthscale > 0.05) + + parameters = extract_model_hyperparameters( + record, input_names=("input_a", "input_b") + ) + assert len(parameters) == 2 + assert all(row.configured_min_noise == 0.01 for row in parameters) + assert all(row.configured_min_lengthscale == 0.05 for row in parameters) + assert all(len(row.ard_lengthscales) == 2 for row in parameters) + assert all(row.input_parameter_space == "normalized_0_1" for row in parameters) + assert all( + row.outcome_parameter_space == "standardized_internal" for row in parameters + ) + flattened = pd.DataFrame(row.as_flat_dict() for row in parameters) + assert { + "ard_lengthscale_input_a", + "ard_lengthscale_input_b", + "likelihood_noise", + "outputscale", + } <= set(flattened.columns) + + +def test_lengthscale_flags_are_relative_to_normalized_domain( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", _skip_optimization) + X, Y, sample_ids = _training_data() + record = fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + ) + for gp in record.model.models: + gp.covar_module.base_kernel.lengthscale = torch.tensor( + [[0.01, 20.0]], dtype=torch.double + ) + + parameters = extract_model_hyperparameters( + record, input_names=("input_a", "input_b") + ) + + assert all( + row.lengthscales_very_small_normalized_domain == (True, False) + for row in parameters + ) + assert all( + row.lengthscales_extremely_large_flat == (False, True) for row in parameters + ) + flattened = pd.DataFrame(row.as_flat_dict() for row in parameters) + assert flattened["any_lengthscale_very_small_normalized_domain"].all() + assert flattened["any_lengthscale_extremely_large_flat"].all() + assert flattened["ard_lengthscale_input_a_very_small_normalized_domain"].all() + assert flattened["ard_lengthscale_input_b_extremely_large_flat"].all() + + +def test_fit_failure_is_structured_and_never_printed_or_retried( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + calls = 0 + + def fail_strictly(mll: object) -> object: + nonlocal calls + calls += 1 + warnings.warn("optimizer diagnostic", RuntimeWarning, stacklevel=2) + raise RuntimeError("optimizer stopped") + + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", fail_strictly) + X, Y, sample_ids = _training_data() + + with pytest.raises(ModelFitError) as captured: + fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + ) + + error = captured.value + assert calls == 1 + assert error.stage == "optimize" + assert error.objective_index == 0 + assert isinstance(error.cause, RuntimeError) + assert any( + row.warning_category == "RuntimeWarning" + and row.message == "optimizer diagnostic" + for row in error.fit_warnings + ) + captured_output = capsys.readouterr() + assert captured_output.out == "" + assert captured_output.err == "" + + +def test_constructor_failure_retains_structured_warnings( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def fail_during_construction(*args: object, **kwargs: object) -> object: + del args, kwargs + warnings.warn("constructor diagnostic", UserWarning, stacklevel=2) + raise RuntimeError("constructor stopped") + + monkeypatch.setattr( + validation_module, "_build_single_task_gp", fail_during_construction + ) + X, Y, sample_ids = _training_data() + with pytest.raises(ModelFitError) as captured: + fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + ) + + assert captured.value.stage == "construct" + assert any( + row.message == "constructor diagnostic" + and row.warning_category == "UserWarning" + for row in captured.value.fit_warnings + ) + + +def test_successful_fit_warnings_are_structured( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def warn_and_succeed(mll: object) -> object: + warnings.warn("fit reached a bound", UserWarning, stacklevel=2) + return mll + + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", warn_and_succeed) + X, Y, sample_ids = _training_data() + record = fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + fit_key="warning-test", + ) + + optimizer_warnings = [row for row in record.warnings if row.stage == "optimize"] + assert len(optimizer_warnings) == 2 + assert all(row.warning_category == "UserWarning" for row in optimizer_warnings) + assert all(row.fit_key == "warning-test" for row in optimizer_warnings) + frame = fit_warnings_frame([record]) + assert frame.shape[0] >= 2 + assert {"stage", "warning_category", "message"} <= set(frame.columns) + + +def test_exact_loocv_retains_folds_uncertainty_roles_and_cache( + monkeypatch: pytest.MonkeyPatch, +) -> None: + fit_calls = 0 + + def count_fit(mll: object) -> object: + nonlocal fit_calls + fit_calls += 1 + return mll + + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", count_fit) + X, Y, sample_ids = _training_data() + cache = ModelFitCache() + first = run_exact_loocv( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + seed=19, + row_roles=("control", "r0", "r0", "r0", "r0"), + control_sample_ids=(1,), + cache=cache, + ) + first_fit_calls = fit_calls + repeat = run_exact_loocv( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + seed=19, + row_roles=("control", "r0", "r0", "r0", "r0"), + control_sample_ids=(1,), + cache=cache, + ) + + assert first_fit_calls == len(sample_ids) * Y.shape[1] + assert fit_calls == first_fit_calls + assert cache.hits == len(sample_ids) + assert len(first.predictions) == len(sample_ids) * Y.shape[1] + assert len(first.fold_records) == len(sample_ids) + assert [row.omitted_sample_id for row in first.predictions] == [ + sample_id for sample_id in sample_ids for _ in range(Y.shape[1]) + ] + assert sum(row.is_control for row in first.predictions) == Y.shape[1] + assert {row.row_role for row in first.predictions if row.is_control} == {"control"} + assert all(row.predictive_std >= row.latent_std for row in first.predictions) + assert any(row.predictive_std > row.latent_std for row in first.predictions) + assert all(np.isfinite(row.gaussian_nlpd) for row in first.predictions) + for omitted_id, record in first.fold_records.items(): + assert omitted_id not in record.sample_ids + assert len(record.sample_ids) == len(sample_ids) - 1 + pd.testing.assert_frame_equal( + first.predictions_frame(), repeat.predictions_frame(), check_exact=False + ) + assert len(first.metrics) == Y.shape[1] + assert all(metric.prediction_count == len(sample_ids) for metric in first.metrics) + + run_exact_loocv( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DEFAULT_CURRENT, + seed=20, + cache=cache, + ) + assert fit_calls == first_fit_calls * 2 + + +def test_validate_model_variant_extracts_full_and_fold_hyperparameters( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", _skip_optimization) + X, Y, sample_ids = _training_data() + result = validate_model_variant( + X, + Y, + sample_ids=sample_ids, + input_names=("input_a", "input_b"), + objective_names=("one", "two"), + variant=CONSERVATIVE, + seed=73, + control_sample_ids=(1,), + ) + + assert result.full_fit.omitted_sample_id is None + assert len(result.loocv.fold_records) == len(sample_ids) + assert len(result.hyperparameters) == (len(sample_ids) + 1) * Y.shape[1] + frame = result.hyperparameters_frame() + assert set(frame["fit_key"]) == { + "full", + *{f"omit:int:{sample_id!r}" for sample_id in sample_ids}, + } + assert np.allclose(frame["noise_constraint_lower_bound"], 0.01) + assert np.allclose(frame["lengthscale_constraint_lower_bound"], 0.05) + assert set(result.loocv.fold_records) == set(sample_ids) + assert result.loocv.cache is not None + + +def test_prediction_metrics_match_hand_calculation() -> None: + observed = np.array([0.0, 1.0]) + predicted = np.array([0.1, 0.9]) + uncertainty = np.array([0.2, 0.2]) + + result = compute_prediction_metrics( + observed, + predicted, + uncertainty, + variant_name="hand", + objective_index=2, + objective_name="score", + ) + + expected_nlpd = 0.5 * np.log(2.0 * np.pi * 0.2**2) + 0.5 * 0.5**2 + assert result.prediction_count == 2 + assert result.mae == pytest.approx(0.1) + assert result.rmse == pytest.approx(0.1) + assert result.r_squared == pytest.approx(0.96) + assert result.spearman_rank_correlation == pytest.approx(1.0) + assert result.mean_signed_error == pytest.approx(0.0, abs=1e-15) + assert result.median_absolute_error == pytest.approx(0.1) + assert result.coverage_68_percent == 1.0 + assert result.coverage_95_percent == 1.0 + assert result.mean_standardized_residual == pytest.approx(0.0, abs=1e-15) + assert result.maximum_absolute_standardized_residual == pytest.approx(0.5) + assert result.mean_gaussian_nlpd == pytest.approx(expected_nlpd) + assert "small N=2" in result.r_squared_warning + + +def test_prediction_metrics_report_undefined_r_squared_and_validate_uncertainty() -> ( + None +): + constant = compute_prediction_metrics( + [1.0, 1.0, 1.0], + [0.9, 1.0, 1.1], + [0.2, 0.2, 0.2], + ) + assert np.isnan(constant.r_squared) + assert np.isnan(constant.spearman_rank_correlation) + assert "undefined" in constant.r_squared_warning + + with pytest.raises(ValueError, match="strictly positive"): + compute_prediction_metrics([0.0], [0.0], [0.0]) diff --git a/tests/test_models.py b/tests/test_models.py index 32cff3b..af37941 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,264 +1,89 @@ -import sys -import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +"""CPU-fast regression tests for the active GP model helpers.""" + +from __future__ import annotations -import yaml -import torch import numpy as np -import pandas as pd -from src.design import build_design_from_config -from src.utils import load_csv, split_XY, np_to_torch, get_objective_names -from src.models import fit_gp_models, default_noise_options, loocv_select_models, posterior_report -from src.data import y_minmax_np +import pytest +import torch +from botorch.models.model_list_gp_regression import ModelListGP +from gpytorch.kernels import Kernel +from gpytorch.priors import Prior -import gpytorch +import mobo_kit.models as models_module +from mobo_kit.models import ( + default_kernel_options, + default_noise_options, + fit_gp_models, + posterior_report, +) -CFG_PATH = "configs/configCSV_example_config.yaml" -CSV_PATH = "data/processed/configCSV_example.csv" -def test_basic_gp_fitting(): - """Test basic GP model fitting with real data.""" - print("Testing GP model fitting...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - - print(f"Data loaded: X shape {X.shape}, Y shape {Y.shape}") - - # Convert to torch tensors (test with CUDA if available) - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - print(f"Converted to torch: X {X_t.shape}, Y {Y_t.shape} on {X_t.device}") - - # Test noise options - print("\nTesting noise options...") - noise_opts = default_noise_options(device=device) - print(f"Available noise options: {list(noise_opts.keys())}") - - # Test that we can create likelihoods with these priors - for name, prior in noise_opts.items(): - if prior is not None: - try: - likelihood = gpytorch.likelihoods.GaussianLikelihood(noise_prior=prior) - print(f"✓ {name}: Successfully created likelihood") - except Exception as e: - print(f"✗ {name}: Failed to create likelihood - {e}") - else: - print(f"✓ {name}: No prior (default likelihood)") - - # Fit GP models - print("\nFitting GP models...") - model = fit_gp_models(X_t, Y_t) - - # Verify model structure - assert hasattr(model, 'models'), "Should return ModelListGP" - assert len(model.models) == Y.shape[1], f"Should have {Y.shape[1]} models" - print(f"✓ Successfully fitted {len(model.models)} GP models") - - # Test prediction - with torch.no_grad(): - posterior = model.posterior(X_t) - pred_mean = posterior.mean - pred_var = posterior.variance - - assert pred_mean.shape == Y_t.shape, "Prediction mean shape mismatch" - assert pred_var.shape == Y_t.shape, "Prediction variance shape mismatch" - print(f"✓ Predictions have correct shape: {pred_mean.shape}") - - return model, X_t, Y_t +def _training_data(): + train_x = torch.tensor( + [ + [0.0, 0.0], + [0.2, 0.8], + [0.4, 0.3], + [0.6, 0.9], + [0.8, 0.2], + [1.0, 1.0], + ], + dtype=torch.float64, + ) + train_y = torch.stack( + ( + train_x[:, 0] + 0.5 * train_x[:, 1], + 1.0 - train_x[:, 0].square() + train_x[:, 1], + ), + dim=1, + ) + return train_x, train_y -def test_loocv_model_selection(): - """Test LOOCV model selection with real data.""" - print("\nTesting LOOCV model selection...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - objective_names = get_objective_names(config) - - # Convert to torch tensors (test with CUDA if available) - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - print(f"Running LOOCV on {len(X_t)} samples with {len(objective_names)} objectives") - - # Run LOOCV model selection (simplified for speed) - best_model, results_df = loocv_select_models( - X_t, Y_t, - objective_names=objective_names, - device=device - ) - - # Verify results - assert hasattr(best_model, 'models'), "Should return ModelListGP" - assert len(best_model.models) == len(objective_names), "Should have model for each objective" - assert isinstance(results_df, pd.DataFrame), "Should return DataFrame" - - print(f"✓ LOOCV completed with {len(results_df)} combinations tested") - print(f"✓ Results columns: {list(results_df.columns)}") - print(f"✓ Best model has {len(best_model.models)} objectives") - - # Check results structure - expected_cols = {"Kernel", "NoisePrior", "Objective", "R2", "RMSE"} - assert expected_cols.issubset(set(results_df.columns)), "Missing expected columns" - - # Show sample results - print("\nSample LOOCV results:") - print(results_df.head(10)) - - return best_model, results_df, X_t, Y_t, objective_names +def test_default_model_options_build_expected_active_types(): + kernel_factories = default_kernel_options() + kernels = [factory(2) for factory in kernel_factories] + noise_options = default_noise_options(torch.device("cpu")) + + assert len(kernels) == 4 + assert all(isinstance(kernel, Kernel) for kernel in kernels) + assert all(kernel.ard_num_dims == 2 for kernel in kernels) + assert noise_options[0] is None + assert all(option is None or isinstance(option, Prior) for option in noise_options) + + +def test_fit_gp_models_and_posterior_report_have_multioutput_shapes(monkeypatch): + fit_calls = [] + def skip_hyperparameter_optimization(mll): + fit_calls.append(mll) + return mll -def test_posterior_report(): - """Test posterior reporting with unnormalization.""" - print("\nTesting posterior report...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - objective_names = get_objective_names(config) - - # Convert to torch tensors (test with CUDA if available) - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - # Normalize Y data (as would be done in real pipeline) - Y_scaled, Y_min, Y_max = y_minmax_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - print(f"Y normalization - Min: {Y_min}, Max: {Y_max}") - - # Fit model on scaled data - model = fit_gp_models(X_t, Y_scaled_t) - - # Generate posterior report - report_df, metrics_df = posterior_report( - model, X_t, Y_scaled_t, Y_min, Y_max, - objective_names=objective_names, - add_residuals=True, - add_zscores=True + monkeypatch.setattr( + models_module, "fit_gpytorch_mll", skip_hyperparameter_optimization ) - - # Verify report structure - assert isinstance(report_df, pd.DataFrame), "Should return DataFrame" - assert isinstance(metrics_df, pd.DataFrame), "Should return metrics DataFrame" - - print(f"✓ Report generated with {len(report_df)} rows") - print(f"✓ Report columns: {list(report_df.columns)}") - print(f"✓ Metrics columns: {list(metrics_df.columns)}") - - # Check for expected columns - for obj_name in objective_names: - assert f"True[{obj_name}]" in report_df.columns, f"Missing True column for {obj_name}" - assert f"Pred[{obj_name}]" in report_df.columns, f"Missing Pred column for {obj_name}" - assert f"Std[{obj_name}]" in report_df.columns, f"Missing Std column for {obj_name}" - assert f"Residual[{obj_name}]" in report_df.columns, f"Missing Residual column for {obj_name}" - assert f"Z[{obj_name}]" in report_df.columns, f"Missing Z-score column for {obj_name}" - - print("\nMetrics summary:") - print(metrics_df) - - print("\nSample report data:") - print(report_df.head()) - - return report_df, metrics_df + train_x, train_y = _training_data() + model = fit_gp_models(train_x, train_y) + pred_mean, pred_std = posterior_report(model, train_x[:3]) -def test_posterior_report_simple(): - """Simple test for posterior report to debug issues.""" - print("\nTesting posterior report (simple)...") - - # Load real data - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - design = build_design_from_config(config) - df = load_csv(CSV_PATH) - X, Y = split_XY(df, design, config) - objective_names = get_objective_names(config) - - # Convert to torch tensors (test with CUDA if available) - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - X_t, Y_t = np_to_torch(X.values, Y.values, device=device) - - # Normalize Y data (as would be done in real pipeline) - Y_scaled, Y_min, Y_max = y_minmax_np(Y.values) - Y_scaled_t = torch.tensor(Y_scaled, dtype=torch.float64, device=device) - - print(f"Y normalization - Min: {Y_min}, Max: {Y_max}") - print(f"Y_scaled_t shape: {Y_scaled_t.shape}, device: {Y_scaled_t.device}") - - # Fit model on scaled data - print("Fitting model...") - model = fit_gp_models(X_t, Y_scaled_t) - print(f"Model fitted with {len(model.models)} objectives") - - # Test posterior step by step - print("Testing posterior step by step...") - - # Step 1: Get posterior - print("Step 1: Getting posterior...") - post = model.posterior(X_t) - pred_mean_t = post.mean - pred_std_t = torch.sqrt(post.variance) - print(f"Posterior shapes - mean: {pred_mean_t.shape}, std: {pred_std_t.shape}") - - # Step 2: Convert to numpy - print("Step 2: Converting to numpy...") - from src.utils import torch_to_np - pred_mean, pred_std, true_scaled = torch_to_np(pred_mean_t, pred_std_t, Y_scaled_t) - print(f"Numpy shapes - mean: {pred_mean.shape}, std: {pred_std.shape}, true: {true_scaled.shape}") - - # Step 3: Unnormalize - print("Step 3: Unnormalizing...") - Y_min = np.asarray(Y_min, dtype=float) - Y_max = np.asarray(Y_max, dtype=float) - scale = (Y_max - Y_min).astype(float) - print(f"Scale factors: {scale}") - - pred_mean_unnorm = pred_mean * scale + Y_min - pred_std_unnorm = pred_std * scale - true_Y = true_scaled * scale + Y_min - print(f"Unnormalized shapes - mean: {pred_mean_unnorm.shape}, std: {pred_std_unnorm.shape}, true: {true_Y.shape}") - - print("✓ All steps completed successfully!") - return model, X_t, Y_scaled_t, Y_min, Y_max, objective_names + assert isinstance(model, ModelListGP) + assert len(model.models) == train_y.shape[1] + assert len(fit_calls) == train_y.shape[1] + assert pred_mean.shape == (3, 2) + assert pred_std.shape == (3, 2) + assert np.isfinite(pred_mean).all() + assert np.isfinite(pred_std).all() + assert (pred_std >= 0.0).all() + assert all(next(gp.parameters()).device.type == "cpu" for gp in model.models) -def main(): - """Run comprehensive model tests.""" - print("Running comprehensive models.py tests...\n") - - try: - # Test 1: Basic GP fitting - print("="*60) - model, X_t, Y_t = test_basic_gp_fitting() - - # Test 2: LOOCV model selection - print("="*60) - best_model, results_df, X_t, Y_t, objective_names = test_loocv_model_selection() - - # Test 3: Posterior reporting (simple) - print("="*60) - model, X_t, Y_scaled_t, Y_min, Y_max, objective_names = test_posterior_report_simple() - - print("="*60) - print("\n🎉 All model tests passed successfully!") - print(f"🎯 Tested with {len(X_t)} samples across {X_t.shape[1]} input dimensions") - print(f"🎯 Validated {len(objective_names)} objectives: {objective_names}") - print(f"🎯 LOOCV tested {len(results_df)} kernel/noise combinations") - print(f"🎯 Generated comprehensive posterior report with metrics") - - except Exception as e: - print(f"\n❌ Test failed: {e}") - import traceback - traceback.print_exc() +def test_fit_gp_models_rejects_mismatched_per_objective_options(monkeypatch): + monkeypatch.setattr(models_module, "fit_gpytorch_mll", lambda mll: mll) + train_x, train_y = _training_data() + with pytest.raises(ValueError, match="kernel_fn list length"): + fit_gp_models(train_x, train_y, kernel_fn=[default_kernel_options()[0]]) -if __name__ == "__main__": - main() + with pytest.raises(ValueError, match="noise_priors list length"): + fit_gp_models(train_x, train_y, noise_priors=[None]) diff --git a/tests/test_objectives.py b/tests/test_objectives.py new file mode 100644 index 0000000..2c3a58f --- /dev/null +++ b/tests/test_objectives.py @@ -0,0 +1,328 @@ +import pytest +import torch + +from mobo_kit.objectives import ( + BoundedMCMultiOutputObjective, + BoundedPosteriorSampleTransform, + ConfiguredMCMultiOutputObjective, + ObjectiveSpec, + ObjectiveTransform, +) + + +def _mixed_transform(): + return ObjectiveTransform( + [ + ObjectiveSpec("already_utility", "maximize", "identity"), + ObjectiveSpec( + "maximize_raw", + "maximize", + "affine", + lower_anchor=10.0, + upper_anchor=20.0, + ), + ObjectiveSpec( + "minimize_raw", + "minimize", + "affine", + lower_anchor=0.0, + upper_anchor=4.0, + ), + ObjectiveSpec( + "target_raw", + "target", + "gaussian_target", + target=650.0, + sigma=100.0, + ), + ], + version="TEST_ONLY-v1", + ) + + +def _identity_transform(): + return ObjectiveTransform( + [ + ObjectiveSpec("uniformity", "maximize", "identity"), + ObjectiveSpec("optoelectronic", "maximize", "identity"), + ObjectiveSpec("thickness", "maximize", "identity"), + ], + version="TEST_IDENTITY-v1", + ) + + +def test_identity_affine_and_arbitrary_leading_dimensions(): + Y = torch.tensor( + [ + [[[0.2, 10.0, 0.0, 650.0], [0.8, 20.0, 4.0, 750.0]]], + [[[0.4, 15.0, 2.0, 550.0], [0.1, 25.0, -2.0, 650.0]]], + ], + dtype=torch.double, + ) + result = _mixed_transform()(Y) + assert result.shape == Y.shape + assert result.dtype == Y.dtype + assert result.device == Y.device + assert torch.allclose(result[..., 0], Y[..., 0]) + assert result[0, 0, 0, 1].item() == pytest.approx(0.0) + assert result[0, 0, 1, 1].item() == pytest.approx(1.0) + assert result[0, 0, 0, 2].item() == pytest.approx(1.0) + assert result[0, 0, 1, 2].item() == pytest.approx(0.0) + + +def test_affine_clip_is_explicit(): + transform = ObjectiveTransform( + [ + ObjectiveSpec( + "clipped", + "maximize", + "affine", + lower_anchor=0, + upper_anchor=1, + clip=True, + ) + ], + version="TEST_ONLY-v1", + ) + result = transform(torch.tensor([[-1.0], [0.4], [2.0]])) + assert result[:, 0].tolist() == pytest.approx([0.0, 0.4, 1.0]) + with pytest.raises(ValueError, match="cannot enable clip"): + ObjectiveSpec("identity", "maximize", "identity", clip=True) + + +def test_gaussian_target_value_symmetry_and_monotonicity(): + transform = ObjectiveTransform( + [ + ObjectiveSpec( + "thickness", + "target", + "gaussian_target", + target=650, + sigma=100, + ) + ], + version="TEST_ONLY-v1", + ) + result = transform( + torch.tensor([[650.0], [600.0], [700.0], [450.0]], dtype=torch.double) + )[:, 0] + assert result[0].item() == pytest.approx(1.0) + assert result[1].item() == pytest.approx(result[2].item()) + assert result[1] < result[0] + assert result[3] < result[1] + + +def test_negative_absolute_target_hand_calculation(): + transform = ObjectiveTransform( + [ + ObjectiveSpec( + "target", + "target", + "negative_absolute_target", + target=10, + scale=2, + ) + ], + version="TEST_ONLY-v1", + ) + assert transform(torch.tensor([[8.0], [10.0], [13.0]]))[:, 0].tolist() == [ + -1.0, + -0.0, + -1.5, + ] + + +def test_nonlinear_transform_is_applied_before_sample_mean(): + transform = ObjectiveTransform( + [ + ObjectiveSpec( + "target", + "target", + "gaussian_target", + target=0, + sigma=1, + ) + ], + version="TEST_ONLY-v1", + ) + posterior_samples = torch.tensor([[[-1.0]], [[1.0]]]) + mean_after_transform = transform(posterior_samples).mean(dim=0) + transform_of_mean = transform(posterior_samples.mean(dim=0)) + assert mean_after_transform.item() == pytest.approx( + torch.exp(torch.tensor(-0.5)).item() + ) + assert transform_of_mean.item() == pytest.approx(1.0) + assert not torch.allclose(mean_after_transform, transform_of_mean) + + +def test_botorch_objective_matches_direct_transform(): + transform = _mixed_transform() + objective = ConfiguredMCMultiOutputObjective(transform) + samples = torch.tensor( + [[[[0.2, 15.0, 2.0, 650.0], [0.8, 20.0, 0.0, 750.0]]]], + dtype=torch.double, + ) + assert torch.equal(objective(samples), transform(samples)) + + +def test_bounded_posterior_sample_transform_is_explicit_and_non_mutating(): + transform = _identity_transform() + bounded = BoundedPosteriorSampleTransform( + transform, + [(0.0, 1.0), (None, None), (0.0, 1.0)], + ) + samples = torch.tensor( + [ + [[[-0.2, -3.5, 1.2], [0.4, 2.1, 0.7]]], + [[[1.4, 8.0, -0.1], [0.9, -1.2, 2.0]]], + ], + dtype=torch.double, + ) + samples_before = samples.clone() + utilities = bounded(samples) + + assert utilities.shape == samples.shape + assert utilities.dtype == samples.dtype + assert utilities.device == samples.device + assert torch.equal(samples, samples_before) + assert torch.all((utilities[..., 0] >= 0.0) & (utilities[..., 0] <= 1.0)) + assert torch.equal(utilities[..., 1], samples[..., 1]) + assert torch.all((utilities[..., 2] >= 0.0) & (utilities[..., 2] <= 1.0)) + assert bounded.bounds == ((0.0, 1.0), (None, None), (0.0, 1.0)) + assert bounded.version == "TEST_IDENTITY-v1+posterior-sample-bounds-v1" + + # The base contract remains unchanged for observed/training targets. Bounds + # apply only when the acquisition-specific wrapper is explicitly invoked. + training_targets = samples_before[0, 0].clone() + training_targets_before = training_targets.clone() + assert torch.equal(transform(training_targets), training_targets_before) + assert torch.equal(training_targets, training_targets_before) + + +def test_bounded_botorch_objective_matches_wrapper_without_touching_reference(): + transform = _identity_transform() + bounds = [(0.0, 1.0), (None, None), (0.0, 1.0)] + objective = BoundedMCMultiOutputObjective(transform, bounds) + samples = torch.tensor([[[-1.0, 2.5, 3.0]]], dtype=torch.float32) + reference_point = torch.tensor([-0.1, -4.0, -0.2], dtype=torch.float32) + reference_before = reference_point.clone() + + expected = BoundedPosteriorSampleTransform(transform, bounds)(samples) + assert torch.equal(objective(samples), expected) + assert torch.equal(reference_point, reference_before) + assert objective.bounds == tuple(bounds) + + +@pytest.mark.parametrize( + "bounds, match", + [ + ([(0.0, 1.0)], "one .* pair per objective"), + ([(1.0, 0.0), (None, None), (0.0, 1.0)], "must not exceed"), + ([(None, None), (None, None), (None, None)], "At least one"), + ([(False, 1.0), (None, None), (0.0, 1.0)], "non-boolean"), + ([(0.0, float("inf")), (None, None), (0.0, 1.0)], "finite"), + ], +) +def test_bounded_posterior_sample_contract_validation(bounds, match): + with pytest.raises(ValueError, match=match): + BoundedPosteriorSampleTransform(_identity_transform(), bounds) + + +def test_posterior_sample_bounds_reject_nonidentity_objectives(): + transform = ObjectiveTransform( + [ + ObjectiveSpec( + "scaled", + "maximize", + "affine", + lower_anchor=0.0, + upper_anchor=1.0, + ) + ], + version="TEST_AFFINE-v1", + ) + with pytest.raises(ValueError, match="identity/maximize"): + BoundedPosteriorSampleTransform(transform, [(0.0, 1.0)]) + + +@pytest.mark.parametrize( + "kwargs, match", + [ + ({"name": "", "goal": "maximize", "transform": "identity"}, "name"), + ({"name": "x", "goal": "minimize", "transform": "identity"}, "only"), + ({"name": "x", "goal": "target", "transform": "affine"}, "requires"), + ( + { + "name": "x", + "goal": "maximize", + "transform": "affine", + "lower_anchor": 1, + "upper_anchor": 1, + }, + "lower_anchor < upper_anchor", + ), + ( + { + "name": "x", + "goal": "target", + "transform": "gaussian_target", + "target": 0, + "sigma": 0, + }, + "strictly positive", + ), + ( + { + "name": "x", + "goal": "maximize", + "transform": "affine", + "lower_anchor": False, + "upper_anchor": 1, + }, + "non-boolean", + ), + ( + { + "name": "x", + "goal": "maximize", + "transform": "affine", + "lower_anchor": "0", + "upper_anchor": 1, + }, + "non-boolean", + ), + ( + { + "name": "x", + "goal": "target", + "transform": "negative_absolute_target", + "target": 0, + "scale": -1, + }, + "strictly positive", + ), + ], +) +def test_invalid_objective_specs_fail(kwargs, match): + with pytest.raises(ValueError, match=match): + ObjectiveSpec(**kwargs) + + +def test_wrong_dimension_integer_nonfinite_and_duplicate_names_fail(): + transform = ObjectiveTransform( + [ObjectiveSpec("x", "maximize", "identity")], version="TEST_ONLY-v1" + ) + with pytest.raises(ValueError, match="final dimension"): + transform(torch.ones((2, 2))) + with pytest.raises(TypeError, match="floating"): + transform(torch.ones((2, 1), dtype=torch.int64)) + with pytest.raises(ValueError, match="finite"): + transform(torch.tensor([[float("nan")]])) + with pytest.raises(ValueError, match="unique"): + ObjectiveTransform( + [ + ObjectiveSpec("x", "maximize", "identity"), + ObjectiveSpec("x", "maximize", "identity"), + ], + version="TEST_ONLY-v1", + ) diff --git a/tests/test_observation_influence.py b/tests/test_observation_influence.py new file mode 100644 index 0000000..f6b0bd8 --- /dev/null +++ b/tests/test_observation_influence.py @@ -0,0 +1,319 @@ +from __future__ import annotations + +import json + +import numpy as np +import pandas as pd +import pytest + +from mobo_kit.observation_influence import ( + InfluenceRunInput, + acquisition_rank_metrics, + hyperparameter_displacement, + pareto_membership_change, + prediction_change_metrics, + run_observation_influence_study, +) + + +POOL_HASH = "a" * 64 +OBJECTIVES = ("uniformity", "optoelectronic", "thickness") +SCALES = (1.0, 2.0, 1.0) + + +def _run( + label: str, + omitted_sample_id: int | None, + *, + selected: np.ndarray, + scores: np.ndarray, + means: np.ndarray, + stds: np.ndarray, + pareto: tuple[int, ...], + hyperparameters: dict[str, float], + pool_hash: str = POOL_HASH, +) -> InfluenceRunInput: + return InfluenceRunInput( + run_label=label, + omitted_sample_id=omitted_sample_id, + common_pool_sha256=pool_hash, + selected_X_norm=selected, + pool_acquisition_scores=scores, + prediction_mean_at_full_candidates=means, + prediction_std_at_full_candidates=stds, + pareto_sample_ids=pareto, + hyperparameters=hyperparameters, + ) + + +def _study_inputs() -> tuple[InfluenceRunInput, tuple[InfluenceRunInput, ...]]: + selected = np.array([[0.0, 0.0], [1.0, 1.0]]) + means = np.array([[0.4, -8.0, 0.7], [0.6, -7.5, 0.8]]) + stds = np.full((2, 3), 0.1) + full = _run( + "full", + None, + selected=selected, + scores=np.array([4.0, 3.0, 2.0, 1.0]), + means=means, + stds=stds, + pareto=(1, 2), + hyperparameters={"noise": 0.1, "lengthscale": 0.5}, + ) + omit_1 = _run( + "omit-1", + 1, + selected=np.array([[0.7, 0.0], [0.2, 1.0]]), + scores=np.array([1.0, 2.0, 3.0, 4.0]), + means=means + np.array([[0.4, 1.0, 0.3], [0.4, 1.0, 0.3]]), + stds=stds + 0.2, + pareto=(2,), + hyperparameters={"noise": 0.4, "lengthscale": 1.5}, + ) + omit_2 = _run( + "omit-2", + 2, + selected=np.array([[0.05, 0.0], [1.0, 0.95]]), + scores=np.array([4.0, 2.9, 2.1, 1.0]), + means=means + 0.02, + stds=stds + 0.01, + pareto=(1,), + hyperparameters={"noise": 0.11, "lengthscale": 0.52}, + ) + omit_3 = _run( + "omit-3", + 3, + selected=selected.copy(), + scores=np.array([4.0, 3.0, 2.0, 1.0]), + means=means.copy(), + stds=stds.copy(), + pareto=(1, 2), + hyperparameters={"noise": 0.1, "lengthscale": 0.5}, + ) + return full, (omit_1, omit_2, omit_3) + + +def test_run_input_defensively_copies_arrays_and_hyperparameters() -> None: + selected = np.array([[0.0, 0.0], [1.0, 1.0]]) + scores = np.array([2.0, 1.0]) + means = np.ones((2, 3)) + stds = np.full((2, 3), 0.2) + hyperparameters = {"noise": 0.1} + run = _run( + "full", + None, + selected=selected, + scores=scores, + means=means, + stds=stds, + pareto=(1,), + hyperparameters=hyperparameters, + ) + selected[0, 0] = 0.8 + scores[0] = 0.0 + means[0, 0] = 9.0 + stds[0, 0] = 9.0 + hyperparameters["noise"] = 9.0 + + assert run.selected_X_norm[0, 0] == 0.0 + assert run.pool_acquisition_scores[0] == 2.0 + assert run.prediction_mean_at_full_candidates[0, 0] == 1.0 + assert run.prediction_std_at_full_candidates[0, 0] == 0.2 + assert run.hyperparameters["noise"] == 0.1 + with pytest.raises(ValueError): + run.selected_X_norm[0, 0] = 1.0 + with pytest.raises(TypeError): + run.hyperparameters["noise"] = 1.0 + + +def test_prediction_change_metrics_are_objective_scaled_and_long_form() -> None: + full_mean = np.array([[0.0, 0.0], [1.0, 2.0]]) + omitted_mean = np.array([[0.1, 0.4], [0.8, 2.4]]) + full_std = np.full((2, 2), 0.1) + omitted_std = np.array([[0.2, 0.3], [0.1, 0.5]]) + metrics, rows = prediction_change_metrics( + full_mean, + full_std, + omitted_mean, + omitted_std, + omitted_sample_id=7, + objective_names=("a", "b"), + objective_scales=(1.0, 2.0), + ) + + assert len(rows) == 4 + assert rows[1].mean_delta == pytest.approx(0.4) + assert rows[1].normalized_absolute_mean_delta == pytest.approx(0.2) + assert metrics.mean_absolute_mean_change == pytest.approx(0.275) + assert metrics.maximum_absolute_mean_change == pytest.approx(0.4) + assert metrics.mean_normalized_absolute_mean_change == pytest.approx(0.175) + assert metrics.mean_normalized_absolute_std_change == pytest.approx(0.1) + assert metrics.component_value == pytest.approx(0.1375) + + +def test_acquisition_rank_and_top_k_changes_match_hand_values() -> None: + metrics = acquisition_rank_metrics( + np.array([4.0, 3.0, 2.0, 1.0]), + np.array([4.0, 2.0, 3.0, 1.0]), + top_k=2, + ) + + assert metrics.pool_size == 4 + assert metrics.top_k == 2 + assert metrics.top_k_overlap_count == 1 + assert metrics.top_k_jaccard == pytest.approx(1.0 / 3.0) + assert metrics.mean_absolute_rank_change == pytest.approx(0.5) + assert metrics.maximum_absolute_rank_change == pytest.approx(1.0) + assert metrics.normalized_mean_absolute_rank_change == pytest.approx(1.0 / 6.0) + assert metrics.spearman_rank_correlation == pytest.approx(0.8) + assert metrics.component_value == pytest.approx(5.0 / 12.0) + + identical = acquisition_rank_metrics(np.ones(3), np.ones(3), top_k=10) + assert identical.top_k == 3 + assert identical.spearman_rank_correlation == 1.0 + assert identical.component_value == 0.0 + + +def test_pareto_and_hyperparameter_changes_are_transparent() -> None: + pareto = pareto_membership_change((1, 2), (2, 3)) + assert pareto.intersection_count == 1 + assert pareto.jaccard == pytest.approx(1.0 / 3.0) + assert pareto.added_sample_ids == (3,) + assert pareto.removed_sample_ids == (1,) + assert pareto.component_value == pytest.approx(2.0 / 3.0) + + displacement = hyperparameter_displacement( + {"lengthscale": 2.0, "noise": 1.0}, + {"lengthscale": 1.0, "noise": 2.0}, + ) + assert displacement.parameter_count == 2 + assert displacement.mean_absolute_log_ratio == pytest.approx(np.log(2.0)) + assert displacement.maximum_absolute_log_ratio == pytest.approx(np.log(2.0)) + assert dict(displacement.absolute_log_ratio_by_parameter) == { + "lengthscale": pytest.approx(np.log(2.0)), + "noise": pytest.approx(np.log(2.0)), + } + + +def test_full_study_ranks_sample_1_and_preserves_fixed_policy() -> None: + full, omissions = _study_inputs() + row_roles = {1: "control", 2: "r0_lhs", 3: "r0_lhs"} + include_policy = {1: True, 2: True, 3: True} + row_roles_before = dict(row_roles) + include_before = dict(include_policy) + + result = run_observation_influence_study( + full, + omissions, + expected_common_pool_sha256=POOL_HASH, + objective_names=OBJECTIVES, + objective_scales=SCALES, + row_roles=row_roles, + primary_include_policy=include_policy, + regional_thresholds=(0.10, 0.15, 0.20), + top_k=2, + ) + + assert row_roles == row_roles_before + assert include_policy == include_before + assert dict(result.row_roles) == row_roles_before + assert dict(result.primary_include_policy) == include_before + with pytest.raises(TypeError): + result.row_roles[1] = "changed" + sample_1 = result.rank_for_sample(1) + assert sample_1.influence_rank == 1 + assert sample_1.influence_percentile == 100.0 + assert result.sample_1_rank() == (1, 100.0) + assert sample_1.omitted_row_role == "control" + assert sample_1.omitted_primary_include_in_model is True + assert len(result.prediction_changes) == len(omissions) * 2 * 3 + assert set(sample_1.raw_components) == { + "batch_displacement", + "prediction_change", + "acquisition_change", + "pareto_change", + "hyperparameter_change", + } + assert set(sample_1.normalized_components) == set(sample_1.raw_components) + assert sum(result.component_weights.values()) == pytest.approx(1.0) + + summary = result.summary_frame() + assert summary.shape[0] == 3 + assert summary.iloc[0]["omitted_sample_id"] == 1 + assert summary.iloc[0]["influence_rank"] == 1 + assert { + "regional_matches_within_0_10", + "regional_matches_within_0_15", + "regional_matches_within_0_20", + "batch_displacement_raw_component", + "batch_displacement_normalized_component", + "composite_influence_score", + } <= set(summary.columns) + assert json.loads(summary.iloc[0]["pareto_removed_sample_ids"]) == [1] + changes = result.prediction_changes_frame() + assert changes.shape[0] == 18 + assert set(changes["objective_name"]) == set(OBJECTIVES) + + +def test_study_is_deterministic_under_omission_input_reordering() -> None: + full, omissions = _study_inputs() + kwargs = { + "expected_common_pool_sha256": POOL_HASH, + "objective_names": OBJECTIVES, + "objective_scales": SCALES, + "row_roles": {1: "control", 2: "r0_lhs", 3: "r0_lhs"}, + "primary_include_policy": {1: True, 2: True, 3: True}, + "top_k": 2, + } + first = run_observation_influence_study(full, omissions, **kwargs) + reordered = run_observation_influence_study( + full, tuple(reversed(omissions)), **kwargs + ) + + pd.testing.assert_frame_equal(first.summary_frame(), reordered.summary_frame()) + pd.testing.assert_frame_equal( + first.prediction_changes_frame(), reordered.prediction_changes_frame() + ) + + +def test_study_rejects_pool_hash_shape_pareto_and_coverage_errors() -> None: + full, omissions = _study_inputs() + bad_hash_run = _run( + "omit-1", + 1, + selected=omissions[0].selected_X_norm, + scores=omissions[0].pool_acquisition_scores, + means=omissions[0].prediction_mean_at_full_candidates, + stds=omissions[0].prediction_std_at_full_candidates, + pareto=(2,), + hyperparameters=dict(omissions[0].hyperparameters), + pool_hash="b" * 64, + ) + base_kwargs = { + "expected_common_pool_sha256": POOL_HASH, + "objective_names": OBJECTIVES, + "objective_scales": SCALES, + "row_roles": {1: "control", 2: "r0_lhs", 3: "r0_lhs"}, + "primary_include_policy": {1: True, 2: True, 3: True}, + } + with pytest.raises(ValueError, match="verified common pool"): + run_observation_influence_study( + full, (bad_hash_run, omissions[1], omissions[2]), **base_kwargs + ) + with pytest.raises(ValueError, match="cover every"): + run_observation_influence_study(full, omissions[:2], **base_kwargs) + + invalid_pareto = _run( + "omit-1", + 1, + selected=omissions[0].selected_X_norm, + scores=omissions[0].pool_acquisition_scores, + means=omissions[0].prediction_mean_at_full_candidates, + stds=omissions[0].prediction_std_at_full_candidates, + pareto=(1, 2), + hyperparameters=dict(omissions[0].hyperparameters), + ) + with pytest.raises(ValueError, match="still lists"): + run_observation_influence_study( + full, (invalid_pareto, omissions[1], omissions[2]), **base_kwargs + ) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index ade17b9..2d68e3d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,64 +1,99 @@ -import sys -import os -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from src.design import build_design_from_config -from src.lhs import lhs_dataframe_optimized, lhs_dataframe -from src.constraints import constraints_from_config -from src.plotting import plot_parity_np, plot_correlation_heatmap, plot_distribution, plot_PCA, plot_pairplot -import yaml -from src.utils import load_csv, split_XY - -CFG_PATH = "configs/configCSV_example_config.yaml" -CSV_PATH = "data/processed/configCSV_example.csv" - -def test_plots(): - print("Starting test...") - config = yaml.load(open(CFG_PATH), Loader=yaml.FullLoader) - print("Config loaded successfully") - design = build_design_from_config(config) - print("Design built successfully") - lhs_df = lhs_dataframe_optimized(design, n=10, max_abs_corr=0.3) - print("LHS data generated successfully") - df = load_csv(CSV_PATH) - print("CSV loaded successfully") - X, Y = split_XY(df, design, config) - print(f"Data split successfully - X type: {type(X)}, Y type: {type(Y)}") - print(f"X columns: {X.columns.tolist()}") - print(f"X dtypes: {X.dtypes}") - print(f"X shape: {X.shape}") - print(X) - - # Test correlation heatmap - print("\nTesting correlation heatmap...") - fig1, corr_mat = plot_correlation_heatmap(X) - print("Correlation heatmap created successfully") - - # Test distribution plots - print("\nTesting distribution plots...") - fig2 = plot_distribution(X, title="Experimental Data Distributions") - print("Distribution plots created successfully") - - # Test PCA plots - print("\nTesting PCA plots...") - fig3, pca_result, pca_obj = plot_PCA(X, title="Experimental Data PCA") - print("PCA plots created successfully") - print(f"Explained variance: {pca_obj.explained_variance_ratio_[:2].sum():.3f}") - - # Test pairplot - print("\nTesting pairplot...") - fig4 = plot_pairplot(X, title="Experimental Data Pairwise Relationships") - print("Pairplot created successfully") - - #fig1.savefig("tests/test_correlation.png") - #fig2.savefig("tests/test_distributions.png") - #fig3.savefig("tests/test_pca.png") - #fig4.savefig("tests/test_pairplot.png") - - return fig1, fig2, fig3, fig4 - -def main(): - test_plots() - -if __name__ == "__main__": - main() \ No newline at end of file +"""Headless smoke tests for campaign diagnostic plots.""" + +from __future__ import annotations + +import matplotlib + +matplotlib.use("Agg", force=True) + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import pytest + +from mobo_kit.plotting import ( + plot_PCA, + plot_correlation_heatmap, + plot_distribution, + plot_parity_np, +) + + +def _feature_frame(): + return pd.DataFrame( + { + "temperature": [80.0, 85.0, 90.0, 95.0, 100.0, 105.0], + "speed": [1.0, 1.5, 1.2, 2.0, 1.8, 2.4], + "ratio": [0.1, 0.3, 0.2, 0.6, 0.7, 0.9], + "sample": ["A", "B", "C", "D", "E", "F"], + } + ) + + +def test_parity_plot_returns_metrics_and_saves_png(tmp_path): + true_y = np.array([[0.2, 1.0], [0.4, 1.5], [0.7, 2.0], [0.9, 2.5]], dtype=float) + pred_y = true_y + np.array( + [[0.02, -0.10], [-0.03, 0.05], [0.01, 0.08], [0.04, -0.04]] + ) + pred_std = np.full_like(true_y, 0.05) + output_path = tmp_path / "parity.png" + + fig, metrics = plot_parity_np( + true_y, + pred_y, + pred_std=pred_std, + objective_names=["efficiency", "stability"], + save=str(output_path), + show_plot=False, + ) + + assert output_path.is_file() + assert len(fig.axes) == 2 + assert list(metrics.columns) == ["Objective", "R2", "RMSE"] + assert metrics["Objective"].tolist() == ["efficiency", "stability"] + assert np.isfinite(metrics[["R2", "RMSE"]].to_numpy()).all() + plt.close(fig) + + +def test_tabular_diagnostic_plots_run_headlessly_and_preserve_shapes(tmp_path): + frame = _feature_frame() + numeric_columns = ["temperature", "speed", "ratio"] + + corr_fig, corr = plot_correlation_heatmap( + frame, + columns=numeric_columns, + save=str(tmp_path / "correlation.png"), + show_plot=False, + ) + distribution_fig = plot_distribution( + frame, + columns=numeric_columns, + n_cols=2, + save=str(tmp_path / "distributions.png"), + show_plot=False, + ) + pca_fig, transformed, pca = plot_PCA( + frame, + columns=numeric_columns, + n_components=2, + save=str(tmp_path / "pca.png"), + show_plot=False, + ) + + assert corr.shape == (3, 3) + assert transformed.shape == (len(frame), 2) + assert pca.n_components_ == 2 + assert (tmp_path / "correlation.png").is_file() + assert (tmp_path / "distributions.png").is_file() + assert (tmp_path / "pca.png").is_file() + + for fig in (corr_fig, distribution_fig, pca_fig): + assert fig.axes + plt.close(fig) + + +def test_plotting_helpers_reject_missing_numeric_data(): + frame = pd.DataFrame({"sample": ["A", "B"]}) + + with pytest.raises(ValueError, match="No numeric columns"): + plot_correlation_heatmap(frame, show_plot=False) diff --git a/tests/test_production_gate.py b/tests/test_production_gate.py new file mode 100644 index 0000000..72eb52d --- /dev/null +++ b/tests/test_production_gate.py @@ -0,0 +1,281 @@ +from copy import deepcopy +from pathlib import Path + +import pytest +import yaml + +from mobo_kit.production_gate import ( + CampaignProposalDisabledError, + ProductionApprovalError, + validate_production_config, +) +from mobo_kit.main import run_mobo_experiment + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + + +def _valid_config(): + objectives = [] + for name, source, direction in ( + ("uniformity", "APPROVED_UNIFORMITY", "maximize"), + ("optoelectronic", "APPROVED_OPTO", "maximize"), + ("thickness", "APPROVED_THICKNESS", "target"), + ): + objectives.append( + { + "name": name, + "model_source_column": source, + "utility_transform": "approved_transform", + "transform_version": "transform-v1", + "formula_version": "formula-v1", + "direction": direction, + "scaling": { + "mode": "already_normalized", + "version": "fixed-scale-v1", + }, + } + ) + return { + "schema_version": "d2d-production-v1", + "campaign_id": "D2D-001", + "approved_for_production": True, + "approval": { + "approved_by": "Scientific and computational owners", + "approved_at": "2026-07-25T15:00:00-04:00", + "decision_record": "docs/D2D_MEETING_DECISIONS.md", + }, + "workbook_profile": "d2d_summary_v2", + "input_contract_version": "d2d-input-v1", + "objective_mapping_status": "approved", + "constraints_status": "approved", + "objectives": objectives, + "reference_point_utility": [-0.1, -0.1, -0.1], + "qc_policy": { + "complete_case_rule": "all_three_required-v1", + "failed_measurement_rule": "exclude_with_reason-v1", + "outlier_rule": "owner_review-v1", + "control_rule": "declared-control-policy-v1", + "replicate_rule": "declared-replicate-policy-v1", + }, + "constraints": [], + "r1": { + "method": "ucb_hvi", + "batch_size": 5, + "beta": 1.0, + "posterior_samples": 128, + "candidate_pool_size": 10000, + }, + "r2": { + "method": "qlognehvi", + "batch_size": 3, + "mc_samples": 128, + "candidate_pool_size": 10000, + "sequential_pending": True, + }, + "local_penalization": { + "distance_metric": "normalized_euclidean", + "radius": 0.2, + "min_batch_distance": 0.1, + "min_observed_distance": 0.05, + "dimension_weights": None, + "allow_hard_distance_relaxation": False, + }, + "reproducibility": { + "seed": 20260725, + "record_git_commit": True, + "record_environment_versions": True, + "record_resolved_config_hash": True, + }, + } + + +def test_fully_resolved_config_returns_stable_receipt(): + config = _valid_config() + first = validate_production_config(config) + second = validate_production_config(deepcopy(config)) + assert first.objective_count == 3 + assert first.workbook_profile == "d2d_summary_v2" + assert first.resolved_config_sha256 == second.resolved_config_sha256 + assert len(first.resolved_config_sha256) == 64 + + +@pytest.mark.parametrize( + "mutation, expected", + [ + (lambda config: config.pop("approved_for_production"), "explicitly true"), + ( + lambda config: config.update(approved_for_production=False), + "explicitly true", + ), + (lambda config: config.pop("objectives"), "exactly three"), + ( + lambda config: config["objectives"][0].pop("formula_version"), + "formula_version", + ), + ( + lambda config: config.pop("reference_point_utility"), + "reference_point_utility", + ), + (lambda config: config.pop("qc_policy"), "qc_policy"), + (lambda config: config.pop("constraints"), "explicit list"), + (lambda config: config["r1"].pop("beta"), "r1.beta"), + ( + lambda config: config["local_penalization"].pop("radius"), + "local_penalization.radius", + ), + ], +) +def test_required_production_fields_fail_closed(mutation, expected): + config = _valid_config() + mutation(config) + with pytest.raises(ProductionApprovalError, match=expected): + validate_production_config(config) + + +def test_placeholder_values_are_rejected(): + config = _valid_config() + config["objectives"][1]["utility_transform"] = "PENDING_AFTER_MEETING" + config["approval"]["approved_by"] = "TBD" + with pytest.raises(ProductionApprovalError) as captured: + validate_production_config(config) + message = str(captured.value) + assert "utility_transform" in message + assert "approval.approved_by" in message + + nested = _valid_config() + nested["objectives"][0]["scaling"] = {"anchors": {"lower": "TBD"}} + with pytest.raises(ProductionApprovalError, match="scaling"): + validate_production_config(nested) + + +def test_placeholders_anywhere_in_resolved_config_are_rejected_with_paths(): + config = _valid_config() + config["approval"]["review_metadata"] = {"scientific_review": {"status": "TBD"}} + config["qc_policy"]["extra_rule"] = "PENDING_AFTER_MEETING" + with pytest.raises(ProductionApprovalError) as captured: + validate_production_config(config) + message = str(captured.value) + assert "approval.review_metadata.scientific_review.status" in message + assert "qc_policy.extra_rule" in message + + +def test_scaling_must_be_fixed_and_cannot_reference_observed_data(): + dynamic = _valid_config() + dynamic["objectives"][0]["scaling"] = { + "mode": "observed_minmax", + "version": "round-specific-v1", + } + with pytest.raises(ProductionApprovalError, match="data-derived scaling"): + validate_production_config(dynamic) + + hidden_dynamic = _valid_config() + hidden_dynamic["objectives"][0]["scaling"]["anchor_source"] = "observed_data" + with pytest.raises(ProductionApprovalError, match="unsupported field"): + validate_production_config(hidden_dynamic) + + fixed = _valid_config() + fixed["objectives"][0]["scaling"] = { + "mode": "fixed_affine", + "version": "fixed-negative-anchor-v1", + "lower_anchor": -2.0, + "upper_anchor": 3.0, + } + assert validate_production_config(fixed).objective_count == 3 + + +@pytest.mark.parametrize( + "path, value", + [ + (("r1", "batch_size"), 5.0), + (("r1", "posterior_samples"), 12.5), + (("r1", "candidate_pool_size"), 100.25), + (("r2", "batch_size"), 3.0), + (("r2", "mc_samples"), 16.5), + (("r2", "candidate_pool_size"), 200.5), + ], +) +def test_sample_pool_and_batch_counts_require_integer_types(path, value): + config = _valid_config() + config[path[0]][path[1]] = value + with pytest.raises(ProductionApprovalError, match=path[1]): + validate_production_config(config) + + +def test_distance_weights_are_explicit_and_metric_consistent(): + ordinary = _valid_config() + ordinary["local_penalization"]["dimension_weights"] = [1.0] + with pytest.raises(ProductionApprovalError, match="requires dimension_weights"): + validate_production_config(ordinary) + + weighted = _valid_config() + weighted["inputs"] = [{"name": "x1"}, {"name": "x2"}] + weighted["local_penalization"]["distance_metric"] = "weighted_normalized_euclidean" + weighted["local_penalization"]["dimension_weights"] = [1.0, 0.0] + with pytest.raises(ProductionApprovalError, match="finite positive"): + validate_production_config(weighted) + weighted["local_penalization"]["dimension_weights"] = [1.0, 2.0] + assert validate_production_config(weighted).objective_count == 3 + + +def test_nonfinite_extra_configuration_cannot_receive_a_receipt(): + config = _valid_config() + config["approval"]["extra_numeric_provenance"] = float("nan") + with pytest.raises(ProductionApprovalError, match="JSON-serializable"): + validate_production_config(config) + + +def test_supplied_provisional_template_cannot_pass_gate(): + path = REPOSITORY_ROOT / "configs" / "d2d_step2a_provisional.yaml" + with path.open("r", encoding="utf-8") as stream: + provisional = yaml.safe_load(stream) + assert provisional["approved_for_production"] is False + with pytest.raises(ProductionApprovalError) as captured: + validate_production_config(provisional) + assert "explicitly true" in str(captured.value) + assert len(captured.value.errors) >= 10 + + +def test_campaign_runner_blocks_provisional_config_before_csv_or_output(tmp_path): + output_dir = tmp_path / "must_not_exist" + provisional_path = REPOSITORY_ROOT / "configs" / "d2d_step2a_provisional.yaml" + with pytest.raises(ProductionApprovalError): + run_mobo_experiment( + csv_path=str(tmp_path / "missing.csv"), + save_dir=str(output_dir), + config_path=str(provisional_path), + device="cpu", + verbose=False, + propose_candidates=True, + ) + assert not output_dir.exists() + + +def test_campaign_runner_blocks_even_approved_config_from_legacy_path(tmp_path): + config_path = tmp_path / "approved.yaml" + config_path.write_text(yaml.safe_dump(_valid_config()), encoding="utf-8") + output_dir = tmp_path / "must_not_exist" + with pytest.raises(CampaignProposalDisabledError, match="Step 2B campaign adapter"): + run_mobo_experiment( + csv_path=str(tmp_path / "missing.csv"), + save_dir=str(output_dir), + config_path=str(config_path), + device="cpu", + verbose=False, + propose_candidates=True, + ) + assert not output_dir.exists() + + +def test_campaign_runner_rejects_auto_config_for_proposals_before_csv_parse(tmp_path): + output_dir = tmp_path / "must_not_exist" + with pytest.raises(ProductionApprovalError, match="explicit resolved"): + run_mobo_experiment( + csv_path=str(tmp_path / "missing.csv"), + save_dir=str(output_dir), + config_path=None, + device="cpu", + verbose=False, + propose_candidates=True, + ) + assert not output_dir.exists() diff --git a/tests/test_qlognehvi_batch.py b/tests/test_qlognehvi_batch.py new file mode 100644 index 0000000..a8e1dd4 --- /dev/null +++ b/tests/test_qlognehvi_batch.py @@ -0,0 +1,314 @@ +import numpy as np +import pytest +import torch +from botorch.models import SingleTaskGP +from botorch.models.model_list_gp_regression import ModelListGP +from botorch.models.transforms.outcome import Standardize + +import mobo_kit.qlognehvi_batch as module +from mobo_kit.batch_selection import LocalPenalizationConfig, UndersizedBatchError +from mobo_kit.candidate_pool import CandidatePool +from mobo_kit.objectives import ( + BoundedMCMultiOutputObjective, + ConfiguredMCMultiOutputObjective, + ObjectiveSpec, + ObjectiveTransform, +) + + +def _objective(count=2): + transform = ObjectiveTransform( + [ + ObjectiveSpec(f"utility_{index}", "maximize", "identity") + for index in range(count) + ], + version="TEST_ONLY-qlog-v1", + ) + return ConfiguredMCMultiOutputObjective(transform) + + +class FakeAcquisition: + def __init__(self, pending_count: int): + self.pending_count = pending_count + self.shapes = [] + + def __call__(self, X: torch.Tensor) -> torch.Tensor: + self.shapes.append(tuple(X.shape)) + # A deterministic log score with a visible pending-point effect. + return X[..., 0, :].sum(dim=-1) - self.pending_count + + +def test_singleton_shape_chunking_and_pending_metadata(monkeypatch): + acquisitions = [] + + def fake_builder(**kwargs): + pending = kwargs["X_pending"] + acquisition = FakeAcquisition(0 if pending is None else pending.shape[0]) + acquisitions.append(acquisition) + return acquisition + + monkeypatch.setattr(module, "_build_qlognehvi", fake_builder) + train = torch.zeros((3, 2), dtype=torch.double) + pool = torch.tensor([[0.1, 0.2], [0.3, 0.4], [0.5, 0.6]], dtype=torch.double) + pending = torch.tensor([[0.0, 1.0]], dtype=torch.double) + result = module.score_qlognehvi_singletons( + object(), + train, + pool, + _objective(), + np.array([-1.0, -1.0]), + mc_samples=16, + seed=8, + chunk_size=2, + X_pending_norm=pending, + ) + assert result.evaluated_shape == (3, 1, 2) + assert result.pending_count == 1 + assert result.mc_samples == 16 + assert result.seed == 8 + assert np.allclose(result.base_log_score, [-0.7, -0.3, 0.1]) + assert acquisitions[0].shapes == [(2, 1, 2), (1, 1, 2)] + + +def test_chunked_and_unchunked_fake_scores_are_equal(monkeypatch): + monkeypatch.setattr( + module, + "_build_qlognehvi", + lambda **kwargs: FakeAcquisition(0), + ) + train = torch.zeros((2, 2), dtype=torch.double) + pool = torch.rand((7, 2), generator=torch.Generator().manual_seed(2)) + kwargs = (object(), train, pool, _objective(), np.array([-1.0, -1.0])) + chunked = module.score_qlognehvi_singletons(*kwargs, chunk_size=2) + whole = module.score_qlognehvi_singletons(*kwargs, chunk_size=100) + assert np.array_equal(chunked.base_log_score, whole.base_log_score) + + +@pytest.mark.parametrize( + "reference, match", + [(None, "required"), (np.array([]), "shape"), (np.array([np.nan]), "finite")], +) +def test_reference_validation(monkeypatch, reference, match): + monkeypatch.setattr( + module, + "_build_qlognehvi", + lambda **kwargs: FakeAcquisition(0), + ) + with pytest.raises(ValueError, match=match): + module.score_qlognehvi_singletons( + object(), + torch.zeros((2, 1)), + torch.zeros((1, 1)), + _objective(1), + reference, + ) + + +def test_seed_is_passed_to_qmc_builder(monkeypatch): + captured = {} + + def fake_builder(**kwargs): + captured.update(kwargs) + return FakeAcquisition(0) + + monkeypatch.setattr(module, "_build_qlognehvi", fake_builder) + module.score_qlognehvi_singletons( + object(), + torch.zeros((2, 1)), + torch.zeros((1, 1)), + _objective(1), + np.array([-1.0]), + mc_samples=32, + seed=41, + ) + assert captured["mc_samples"] == 32 + assert captured["seed"] == 41 + + +def test_configured_objective_and_reference_dimensions_are_required(monkeypatch): + monkeypatch.setattr( + module, + "_build_qlognehvi", + lambda **kwargs: FakeAcquisition(0), + ) + train = torch.zeros((2, 1)) + pool = torch.zeros((1, 1)) + with pytest.raises(TypeError, match="configured or bounded configured"): + module.score_qlognehvi_singletons(object(), train, pool, None, np.array([-1.0])) + with pytest.raises(ValueError, match="configured objective count"): + module.score_qlognehvi_singletons( + object(), train, pool, _objective(2), np.array([-1.0]) + ) + + +def test_real_qlognehvi_accepts_bounded_objective_and_returns_reproducible_scores(): + train_X = torch.tensor([[0.0], [0.25], [0.5], [0.75], [1.0]], dtype=torch.double) + train_Y = torch.tensor( + [ + [0.15, -3.0, 0.20], + [0.55, -2.2, 0.45], + [0.90, -1.5, 0.85], + [0.65, -1.9, 0.60], + [0.25, -2.8, 0.30], + ], + dtype=torch.double, + ) + model = ModelListGP( + *[ + SingleTaskGP( + train_X, + train_Y[:, index : index + 1], + outcome_transform=Standardize(m=1), + ) + for index in range(train_Y.shape[1]) + ] + ) + transform = ObjectiveTransform( + [ + ObjectiveSpec("uniformity", "maximize", "identity"), + ObjectiveSpec("optoelectronic", "maximize", "identity"), + ObjectiveSpec("thickness", "maximize", "identity"), + ], + version="TEST_ONLY-bounded-qlog-v1", + ) + objective = BoundedMCMultiOutputObjective( + transform, + bounds=((0.0, 1.0), (None, None), (0.0, 1.0)), + ) + pool = torch.tensor([[0.1], [0.4], [0.7], [0.9]], dtype=torch.double) + reference = np.array([-0.01, -4.0, -0.01]) + + first = module.score_qlognehvi_singletons( + model, + train_X, + pool, + objective, + reference, + mc_samples=16, + seed=73, + chunk_size=2, + prune_baseline=False, + ) + second = module.score_qlognehvi_singletons( + model, + train_X, + pool, + objective, + reference, + mc_samples=16, + seed=73, + chunk_size=2, + prune_baseline=False, + ) + + assert first.evaluated_shape == (4, 1, 1) + assert first.objective_contract_version == objective.version + assert np.array_equal(first.base_log_score, second.base_log_score) + assert not np.any(np.isnan(first.base_log_score)) + assert not np.any(np.isposinf(first.base_log_score)) + assert np.any(np.isfinite(first.base_log_score)) + assert np.array_equal(first.reference_point_utility, reference) + + +def _proposal_pool(): + X = np.array([[0.3], [0.45], [0.6], [0.75], [0.9]]) + return CandidatePool( + grid_indices=np.arange(5)[:, None], + X_phys=X.copy(), + X_norm=X.copy(), + seed=3, + draws=5, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + + +def test_sequential_proposal_updates_pending_and_returns_exact_three(monkeypatch): + pending_counts = [] + seen_objectives = [] + + def fake_score(model, train_X, X_pool, objective, reference, **kwargs): + del model, train_X + pending = kwargs.get("X_pending_norm") + pending_count = 0 if pending is None else pending.shape[0] + pending_counts.append(pending_count) + seen_objectives.append(objective) + return module.QLogNEHVIPoolScoreResult( + base_log_score=X_pool[:, 0].detach().cpu().numpy(), + evaluated_shape=(X_pool.shape[0], 1, X_pool.shape[1]), + pending_count=pending_count, + mc_samples=kwargs["mc_samples"], + seed=kwargs["seed"], + reference_point_utility=np.asarray(reference), + objective_contract_version=objective.objective_transform.version, + ) + + monkeypatch.setattr(module, "score_qlognehvi_singletons", fake_score) + objective = _objective(1) + proposal = module.propose_qlognehvi_penalized_batch( + _proposal_pool(), + object(), + torch.tensor([[0.05], [0.15]], dtype=torch.double), + objective, + np.array([-1.0]), + q=3, + local_penalization_config=LocalPenalizationConfig( + radius=0.12, min_batch_distance=0.1 + ), + X_pending_norm=torch.tensor([[0.2]], dtype=torch.double), + mc_samples=16, + seed=7, + ) + assert proposal.selection.selected_pool_indices.size == 3 + assert np.unique(proposal.selection.selected_pool_indices).size == 3 + assert pending_counts == [1, 2, 3] + assert seen_objectives == [objective, objective, objective] + assert [history.pending_count for history in proposal.score_history] == [1, 2, 3] + assert all(step.base_score is not None for step in proposal.selection.steps) + assert proposal.metadata["pool_seed"] == 3 + assert proposal.metadata["mc_seed"] == 7 + assert proposal.metadata["objective_contract_version"] == "TEST_ONLY-qlog-v1" + + +def test_proposal_rejects_observed_or_pending_pool_overlap(): + with pytest.raises(ValueError, match="overlaps observed"): + module.propose_qlognehvi_penalized_batch( + _proposal_pool(), + object(), + torch.tensor([[0.3]], dtype=torch.double), + _objective(1), + np.array([-1.0]), + q=1, + local_penalization_config=LocalPenalizationConfig( + radius=0.1, min_batch_distance=0 + ), + ) + + +def test_qlog_proposal_hard_distance_failure_is_explicit(monkeypatch): + def fake_score(model, train_X, X_pool, objective, reference, **kwargs): + del model, train_X + return module.QLogNEHVIPoolScoreResult( + base_log_score=np.zeros(X_pool.shape[0]), + evaluated_shape=(X_pool.shape[0], 1, X_pool.shape[1]), + pending_count=0, + mc_samples=kwargs["mc_samples"], + seed=kwargs["seed"], + reference_point_utility=np.asarray(reference), + objective_contract_version=objective.objective_transform.version, + ) + + monkeypatch.setattr(module, "score_qlognehvi_singletons", fake_score) + with pytest.raises(UndersizedBatchError): + module.propose_qlognehvi_penalized_batch( + _proposal_pool(), + object(), + torch.tensor([[0.05]], dtype=torch.double), + _objective(1), + np.array([-1.0]), + q=3, + local_penalization_config=LocalPenalizationConfig( + radius=0.1, min_batch_distance=0.7 + ), + ) diff --git a/tests/test_robust_regions.py b/tests/test_robust_regions.py new file mode 100644 index 0000000..103ce6b --- /dev/null +++ b/tests/test_robust_regions.py @@ -0,0 +1,313 @@ +import numpy as np +import pandas as pd + +from mobo_kit.robust_regions import ( + DEBUG_WATERMARK, + cluster_candidate_regions, + evaluate_consensus_criteria, + select_robust_shortlist, +) + + +def _records() -> pd.DataFrame: + rows = [ + ("pool_large", "pool", 0, 0, 0.00, 0.00, 10.0), + ("model_default", "model", 0, 1, 0.00, 0.05, 9.0), + ("beta_four", "beta", 1, 0, 0.05, 0.00, 8.0), + ("pool_small", "pool", 8, 8, 0.80, 0.80, 7.0), + ("model_default", "model", 8, 9, 0.80, 0.90, 6.0), + ("beta_four", "beta", 9, 8, 0.90, 0.80, 5.0), + ("pool_large", "pool", 4, 9, 0.40, 0.90, 4.0), + ] + return pd.DataFrame( + rows, + columns=[ + "run_id", + "run_family", + "grid_0", + "grid_1", + "norm_0", + "norm_1", + "acquisition_score", + ], + ).assign( + phys_0=lambda frame: frame["grid_0"] * 10.0, + phys_1=lambda frame: frame["grid_1"] * 5.0, + grid_valid=True, + hard_distance_valid=True, + boundary_coordinate_count=0, + nearest_control_distance=0.5, + pred_mean_0=0.6, + pred_std_0=0.1, + ) + + +def _registry() -> dict[str, str]: + return { + "pool_large": "pool", + "pool_small": "pool", + "model_default": "model", + "beta_four": "beta", + } + + +def test_complete_link_regions_are_deterministic_and_family_weighted() -> None: + records = _records() + first = cluster_candidate_regions( + records, distance_threshold=0.15, core_run_registry=_registry() + ) + second = cluster_candidate_regions( + records.sample(frac=1.0, random_state=91).reset_index(drop=True), + distance_threshold=0.15, + core_run_registry=_registry(), + ) + + assert first.region_count == 3 + comparable = [ + "region_id", + "member_count", + "distinct_run_count", + "distinct_family_count", + "family_weighted_persistence", + "cluster_diameter", + "medoid_grid_0", + "medoid_grid_1", + ] + pd.testing.assert_frame_equal( + first.regions[comparable], second.regions[comparable], check_exact=True + ) + assert (first.regions["cluster_diameter"] <= 0.15 + 1e-12).all() + assert first.regions.iloc[0]["distinct_family_count"] == 3 + assert np.isclose( + first.regions.iloc[0]["family_weighted_persistence"], + np.mean([0.5, 1.0, 1.0]), + ) + assert first.regions["candidate_status"].eq(DEBUG_WATERMARK).all() + assert (~first.regions["approved_for_experiment"]).all() + + +def test_threshold_sensitivity_and_diverse_shortlist() -> None: + records = _records() + strict = cluster_candidate_regions( + records, distance_threshold=0.04, core_run_registry=_registry() + ) + primary = cluster_candidate_regions( + records, distance_threshold=0.15, core_run_registry=_registry() + ) + loose = cluster_candidate_regions( + records, distance_threshold=0.25, core_run_registry=_registry() + ) + + assert strict.region_count > primary.region_count + assert loose.region_count <= primary.region_count + shortlist = select_robust_shortlist( + primary, minimum_count=2, maximum_count=3, minimum_normalized_distance=0.15 + ) + assert 2 <= len(shortlist) <= 3 + coords = shortlist[["medoid_norm_0", "medoid_norm_1"]].to_numpy() + if len(coords) > 1: + distances = np.linalg.norm(coords[:, None, :] - coords[None, :, :], axis=-1) + assert distances[np.triu_indices(len(coords), k=1)].min() >= 0.15 + assert shortlist["shortlist_id"].is_unique + assert shortlist["candidate_status"].eq(DEBUG_WATERMARK).all() + + +def _consensus_inputs(pass_all: bool) -> tuple[pd.DataFrame, pd.DataFrame]: + regions = pd.DataFrame( + { + "distinct_family_count": [3, 4, 3, 3, 5], + "distinct_nonbaseline_family_count": [3, 4, 3, 3, 5], + "all_grid_valid": [True] * 5, + "all_hard_distance_valid": [True] * 5, + } + ) + shortlist = pd.DataFrame( + { + "distinct_nonbaseline_family_count": [3, 4, 3, 3, 5], + "medoid_grid_0": [0, 0, 1, 2, 4], + "medoid_grid_1": [0, 4, 0, 2, 4], + "medoid_norm_0": [0.0, 0.0, 0.25, 0.5, 1.0], + "medoid_norm_1": [0.0, 1.0, 0.0, 0.5, 1.0], + "all_grid_valid": [True] * 5, + "all_hard_distance_valid": [True] * 5, + "debug_only": [True] * 5, + "approved_for_experiment": [False] * 5, + "approved_for_production": [False] * 5, + } + ) + if not pass_all: + shortlist.loc[0, "all_hard_distance_valid"] = False + return regions, shortlist + + +def test_consensus_is_created_only_when_every_declared_gate_passes() -> None: + regions, shortlist = _consensus_inputs(pass_all=True) + passed = evaluate_consensus_criteria( + regions, + shortlist, + largest_two_regional_matches_within_0_15=4, + largest_two_mean_matched_distance=0.08, + ) + assert passed.passed is True + assert passed.reasons == () + + regions, shortlist = _consensus_inputs(pass_all=False) + failed = evaluate_consensus_criteria( + regions, + shortlist, + largest_two_regional_matches_within_0_15=3, + largest_two_mean_matched_distance=0.11, + ) + assert failed.passed is False + assert set(failed.reasons) >= { + "largest_two_nested_regional_matches", + "largest_two_nested_mean_matched_distance", + "chosen_pairwise_hard_distance_valid", + } + + +def test_family_count_uses_distinct_categories_not_run_frequency() -> None: + records = pd.concat([_records()] * 4, ignore_index=True) + result = cluster_candidate_regions( + records, distance_threshold=0.15, core_run_registry=_registry() + ) + assert result.regions["distinct_family_count"].max() == 3 + assert result.regions["distinct_core_run_count"].max() <= 4 + + +def test_region_table_exposes_relative_hvi_prediction_distance_and_boundary_metrics() -> ( + None +): + records = _records() + records["selection_order"] = records.groupby("run_id").cumcount() + 1 + records["nearest_observed_distance"] = [0.2, 0.3, 0.4, 0.8, 0.9, 1.0, 0.7] + records["pred_mean_0"] = [0.5, 0.6, 0.7, 0.1, 0.2, 0.3, 0.4] + records["pred_std_0"] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] + records["boundary_dimensions"] = [ + "input_a|input_b", + "input_a", + "input_b", + "", + "", + "", + "", + ] + + result = cluster_candidate_regions( + records, distance_threshold=0.15, core_run_registry=_registry() + ) + region = result.regions.loc[ + (result.regions["medoid_grid_0"] == 0) + & (result.regions["medoid_grid_1"].isin([0, 1])) + ].iloc[0] + + assert region["median_selection_order"] == 1 + assert region["median_run_normalized_base_hvi"] == 1.0 + assert region["iqr_run_normalized_base_hvi"] == 0.0 + assert np.isclose(region["median_pred_mean_0"], 0.6) + assert np.isclose(region["median_pred_std_0"], 0.2) + assert np.isclose(region["median_nearest_observed_distance"], 0.3) + assert np.isclose(region["boundary_dimension_0_frequency"], 2 / 3) + assert np.isclose(region["boundary_dimension_1_frequency"], 2 / 3) + assert region["boundary_dimension_name_frequency"] == ( + "input_a:0.666667|input_b:0.666667" + ) + assert np.isclose( + region["maximum_within_region_distance"], region["cluster_diameter"] + ) + assert np.isclose(region["nested_coverage"], 0.5) + assert region["model_coverage"] == 1.0 + assert region["beta_coverage"] == 1.0 + assert pd.isna(region["scramble_coverage"]) + assert not region["control_omission_correspondence_available"] + assert pd.isna(region["control_included_vs_control_omitted_correspondence"]) + assert "base_hvi_normalized_within_run" in result.membership + + +def test_step2c_baseline_is_shared_but_six_family_coverage_is_equal_weighted() -> None: + rows = [ + ("baseline", "baseline", 0, 0, 0.0, 0.0, 10.0), + ("nested_1", "nested_pool", 8, 8, 0.8, 0.8, 9.0), + ("scramble_1", "sobol_scramble", 8, 8, 0.8, 0.8, 8.0), + ("model_1", "model_variant", 8, 8, 0.8, 0.8, 7.0), + ("bound_1", "bounded_utility", 8, 8, 0.8, 0.8, 6.0), + ("beta_1", "beta", 8, 8, 0.8, 0.8, 5.0), + ("penalty_1", "local_penalty", 8, 8, 0.8, 0.8, 4.0), + ] + records = pd.DataFrame( + rows, + columns=[ + "run_id", + "run_family", + "grid_0", + "grid_1", + "norm_0", + "norm_1", + "acquisition_score", + ], + ) + registry = dict( + records[["run_id", "run_family"]].itertuples(index=False, name=None) + ) + + result = cluster_candidate_regions( + records, distance_threshold=0.15, core_run_registry=registry + ) + baseline_region = result.regions.loc[result.regions["medoid_grid_0"] == 0].iloc[0] + + for column in ( + "model_coverage", + "nested_pool_coverage", + "sobol_scramble_coverage", + "bound_policy_coverage", + "beta_coverage", + "local_penalty_coverage", + ): + assert baseline_region[column] == 0.5 + assert baseline_region["family_weighted_persistence"] == 0.5 + assert baseline_region["study_family_weighted_persistence"] == 0.5 + assert baseline_region["registry_family_weighted_persistence"] == 1 / 7 + assert baseline_region["persistence_basis"] == ( + "six_equal_weight_step2c_study_families" + ) + assert baseline_region["distinct_family_count"] == 1 + + +def test_control_omission_correspondence_is_regional_and_configured() -> None: + records = pd.DataFrame( + [ + ("full_a", "model", 0, 0, 0.00, 0.00, 3.0, np.nan), + ("omit_control", "model", 0, 1, 0.00, 0.05, 2.0, 1001), + ("full_b", "model", 8, 8, 0.80, 0.80, 1.0, 1002), + ], + columns=[ + "run_id", + "run_family", + "grid_0", + "grid_1", + "norm_0", + "norm_1", + "acquisition_score", + "omitted_sample_id", + ], + ) + registry = dict( + records[["run_id", "run_family"]].itertuples(index=False, name=None) + ) + + result = cluster_candidate_regions( + records, + distance_threshold=0.15, + core_run_registry=registry, + control_sample_id=1001, + ) + paired = result.regions.loc[result.regions["member_count"] == 2].iloc[0] + unpaired = result.regions.loc[result.regions["member_count"] == 1].iloc[0] + + assert paired["control_omission_correspondence_available"] + assert paired["control_included_represented_run_count"] == 1 + assert paired["control_included_total_run_count"] == 2 + assert paired["control_omitted_represented_run_count"] == 1 + assert paired["control_omitted_total_run_count"] == 1 + assert paired["control_included_vs_control_omitted_correspondence"] == 0.5 + assert unpaired["control_included_vs_control_omitted_correspondence"] == 0.0 diff --git a/tests/test_robustness_plots.py b/tests/test_robustness_plots.py new file mode 100644 index 0000000..9de9520 --- /dev/null +++ b/tests/test_robustness_plots.py @@ -0,0 +1,539 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path + +import matplotlib +import numpy as np +import pandas as pd +import pytest +from PIL import Image + +matplotlib.use("Agg", force=True) +from matplotlib import pyplot as plt # noqa: E402 + +from mobo_kit.robustness_plots import ( # noqa: E402 + DEBUG_WATERMARK, + plot_acquisition_quality_vs_persistence, + plot_ard_lengthscale_comparison, + plot_boundary_enrichment, + plot_bounded_utility_comparison, + plot_candidate_predictions_vs_observed_ranges, + plot_control_omission_candidate_region_comparison, + plot_local_penalty_tradeoff, + plot_loocv_diagnostics, + plot_model_policy_region_correspondence, + plot_nested_search_convergence, + plot_observation_influence_ranking, + plot_robust_region_overview, + plot_run_region_persistence_heatmap, + plot_shortlist_medoid_parallel_coordinates, + plot_shortlist_region_influence_sensitivity, +) + + +def _loocv_frame() -> pd.DataFrame: + return pd.DataFrame( + { + "objective": ["Uniformity", "Uniformity", "Thickness", "Thickness"], + "observed": [0.2, 0.8, 0.4, 0.9], + "predicted_mean": [0.25, 0.72, 0.48, 0.82], + "predicted_std": [0.1, 0.12, 0.15, 0.1], + } + ) + + +def _convergence_frame() -> pd.DataFrame: + return pd.DataFrame( + { + "pool_size": [16_384, 32_768, 65_536, 131_072], + "mean_matched_distance": [0.16, 0.11, 0.07, 0.0], + "maximum_matched_distance": [0.29, 0.21, 0.12, 0.0], + "regional_matches_0_10": [1, 2, 4, 5], + "regional_matches_0_15": [2, 4, 5, 5], + "regional_matches_0_20": [3, 5, 5, 5], + } + ) + + +def _hyperparameter_frame() -> pd.DataFrame: + rows = [] + for variant_index, variant in enumerate(("default_current", "conservative")): + for objective_index, objective in enumerate(("Uniformity", "Thickness")): + for fold in range(2): + rows.append( + { + "variant_name": variant, + "objective_name": objective, + "fit_key": f"{variant}-{objective}-{fold}", + "ard_lengthscale_speed": 0.08 + + 0.03 * variant_index + + 0.01 * objective_index + + 0.005 * fold, + "ard_lengthscale_time": 0.2 + + 0.04 * variant_index + + 0.01 * objective_index + + 0.005 * fold, + "ard_lengthscale_speed_near_floor": False, + "ard_lengthscale_speed_very_small_normalized_domain": False, + "ard_lengthscale_speed_extremely_large_flat": False, + } + ) + return pd.DataFrame(rows) + + +def _prediction_range_frame() -> pd.DataFrame: + rows = [] + ranges = {"Uniformity": (0.1, 0.9), "Thickness": (-1.5, 1.2)} + for objective_index, (objective, observed_range) in enumerate(ranges.items()): + for candidate_index, candidate in enumerate(("C1", "C2")): + for model_index, model in enumerate(("default_current", "conservative")): + rows.append( + { + "candidate_id": candidate, + "model_variant": model, + "objective_name": objective, + "predicted_mean": 0.25 + + 0.12 * candidate_index + + 0.04 * model_index + - 0.3 * objective_index, + "predicted_std": 0.08 + 0.01 * model_index, + "observed_minimum": observed_range[0], + "observed_maximum": observed_range[1], + } + ) + return pd.DataFrame(rows) + + +def _shortlist_frame() -> pd.DataFrame: + return pd.DataFrame( + { + "region_id": ["REGION-001", "REGION-002", "REGION-010"], + "medoid_norm_0": [0.1, 0.45, 0.82], + "medoid_norm_1": [0.25, 0.7, 0.55], + "medoid_norm_2": [0.35, 0.2, 0.9], + "family_weighted_persistence": [0.9, 0.65, 0.4], + } + ) + + +def _membership_frame() -> pd.DataFrame: + return pd.DataFrame( + { + "run_id": ["pool-1", "pool-1", "pool-2", "model-default", "model-alt"], + "run_family": ["nested", "nested", "nested", "model", "model"], + "region_id": [ + "REGION-001", + "REGION-002", + "REGION-001", + "REGION-002", + "REGION-010", + ], + "model_variant": [ + "default_current", + "default_current", + "default_current", + "default_current", + "conservative", + ], + "bound_policy": ["clip_ucb", "clip_ucb", "none", "clip_ucb", "clip_ucb"], + } + ) + + +def _region_quality_frame() -> pd.DataFrame: + return pd.DataFrame( + { + "region_id": ["REGION-001", "REGION-002", "REGION-010"], + "family_weighted_persistence": [0.9, 0.65, 0.4], + "median_acquisition_score": [4.1, 3.7, 2.8], + "distinct_family_count": [5, 4, 2], + } + ) + + +def _region_sensitivity_frame() -> pd.DataFrame: + rows = [] + for region_index, region in enumerate(("REGION-001", "REGION-002", "REGION-010")): + for sample in (1, 2, 10): + rows.append( + { + "region_id": region, + "omitted_sample_id": sample, + "prediction_change": 0.01 * (region_index + 1) * sample, + } + ) + return pd.DataFrame(rows) + + +def _assert_watermarked_png(path: Path) -> None: + assert path.is_file() + assert path.stat().st_size > 1000 + assert DEBUG_WATERMARK.encode() in path.read_bytes() + with Image.open(path) as image: + assert image.format == "PNG" + assert image.info["Description"] == DEBUG_WATERMARK + assert "Title" not in image.info + assert all( + "SECRET_RECIPE_VALUE" not in str(value) for value in image.info.values() + ) + + +def test_all_required_plot_topics_write_watermarked_pngs_and_close_figures(tmp_path): + paths = [ + plot_loocv_diagnostics(_loocv_frame(), tmp_path / "loocv.png"), + plot_nested_search_convergence(_convergence_frame(), tmp_path / "nested.png"), + plot_bounded_utility_comparison( + pd.DataFrame( + { + "policy": ["none", "none", "clip_ucb", "clip_ucb"], + "raw_value": [1.2, 0.8, 1.2, 0.8], + "bounded_value": [1.2, 0.8, 1.0, 0.8], + } + ), + tmp_path / "bounded.png", + ), + plot_local_penalty_tradeoff( + pd.DataFrame( + { + "variant": ["none", "radius_0.15", "radius_0.25"], + "sum_base_hvi": [4.2, 4.0, 3.8], + "minimum_within_batch_distance": [0.11, 0.16, 0.21], + } + ), + tmp_path / "penalty.png", + ), + plot_observation_influence_ranking( + pd.DataFrame( + {"sample_id": [1, 2, 3, 4], "influence_score": [0.9, 0.2, 0.5, 0.1]} + ), + tmp_path / "influence.png", + ), + plot_boundary_enrichment( + pd.DataFrame( + { + "input_name": ["speed", "time", "volume"], + "pool_lower_boundary_frequency": [0.1, 0.08, 0.12], + "top_lower_boundary_frequency": [0.2, 0.15, 0.25], + "selected_lower_boundary_frequency": [0.4, 0.2, 0.6], + "pool_upper_boundary_frequency": [0.1, 0.08, 0.12], + "top_upper_boundary_frequency": [0.2, 0.15, 0.25], + "selected_upper_boundary_frequency": [0.3, 0.2, 0.5], + } + ), + tmp_path / "boundary.png", + ), + plot_robust_region_overview( + np.array( + [ + [0.1, 0.2, 0.3], + [0.12, 0.18, 0.34], + [0.8, 0.7, 0.6], + [0.82, 0.73, 0.58], + ] + ), + ["A", "A", "B", "B"], + tmp_path / "regions.png", + persistence=[3.0, 3.0, 5.0, 5.0], + input_names=["speed", "time", "volume"], + ), + ] + assert len(set(paths)) == 7 + for path in paths: + _assert_watermarked_png(path) + assert plt.get_fignums() == [] + + +def test_extended_model_region_plot_topics_are_watermarked_and_headless(tmp_path): + membership = _membership_frame() + paths = [ + plot_ard_lengthscale_comparison( + _hyperparameter_frame(), tmp_path / "ard_lengthscales.png" + ), + plot_control_omission_candidate_region_comparison( + np.array([[0.1, 0.2, 0.3], [0.75, 0.7, 0.65]]), + np.array([[0.12, 0.22, 0.29], [0.68, 0.72, 0.61]]), + tmp_path / "control_omission_regions.png", + full_region_labels=["REGION-001", "REGION-002"], + omit_region_labels=["REGION-001", "REGION-002"], + ), + plot_candidate_predictions_vs_observed_ranges( + _prediction_range_frame(), tmp_path / "predicted_ranges.png" + ), + plot_shortlist_medoid_parallel_coordinates( + _shortlist_frame(), + tmp_path / "shortlist_parallel.png", + input_names=["speed", "time", "volume"], + ), + plot_run_region_persistence_heatmap( + membership, tmp_path / "run_region_heatmap.png" + ), + plot_model_policy_region_correspondence( + membership, tmp_path / "model_policy_regions.png" + ), + plot_acquisition_quality_vs_persistence( + _region_quality_frame(), tmp_path / "quality_persistence.png" + ), + plot_shortlist_region_influence_sensitivity( + _region_sensitivity_frame(), tmp_path / "region_sensitivity.png" + ), + ] + assert len(set(paths)) == 8 + for path in paths: + _assert_watermarked_png(path) + assert plt.get_fignums() == [] + + +def test_nested_plot_is_deterministic_and_row_order_invariant(tmp_path): + frame = _convergence_frame() + first = plot_nested_search_convergence(frame, tmp_path / "first.png") + second = plot_nested_search_convergence( + frame.iloc[::-1].reset_index(drop=True), tmp_path / "second.png" + ) + assert ( + hashlib.sha256(first.read_bytes()).digest() + == hashlib.sha256(second.read_bytes()).digest() + ) + + +def test_extended_heatmap_is_deterministic_and_row_order_invariant(tmp_path): + frame = _membership_frame() + first = plot_run_region_persistence_heatmap(frame, tmp_path / "heatmap-first.png") + second = plot_run_region_persistence_heatmap( + frame.sample(frac=1.0, random_state=73).reset_index(drop=True), + tmp_path / "heatmap-second.png", + ) + assert ( + hashlib.sha256(first.read_bytes()).digest() + == hashlib.sha256(second.read_bytes()).digest() + ) + + +def test_control_omission_region_comparison_is_row_order_invariant(tmp_path): + full = np.array([[0.1, 0.2, 0.3], [0.75, 0.7, 0.65], [0.4, 0.5, 0.6]]) + omitted = np.array([[0.12, 0.22, 0.29], [0.68, 0.72, 0.61], [0.39, 0.53, 0.57]]) + full_labels = np.array(["REGION-001", "REGION-002", "REGION-003"]) + omitted_labels = np.array(["REGION-001", "REGION-002", "REGION-003"]) + first = plot_control_omission_candidate_region_comparison( + full, + omitted, + tmp_path / "control-omission-first.png", + full_region_labels=full_labels, + omit_region_labels=omitted_labels, + ) + full_order = np.array([2, 0, 1]) + omitted_order = np.array([1, 2, 0]) + second = plot_control_omission_candidate_region_comparison( + full[full_order], + omitted[omitted_order], + tmp_path / "control-omission-second.png", + full_region_labels=full_labels[full_order], + omit_region_labels=omitted_labels[omitted_order], + ) + assert ( + hashlib.sha256(first.read_bytes()).digest() + == hashlib.sha256(second.read_bytes()).digest() + ) + + +def test_loocv_accepts_explicit_column_names(tmp_path): + renamed = _loocv_frame().rename( + columns={ + "objective": "target", + "observed": "truth", + "predicted_mean": "estimate", + "predicted_std": "uncertainty", + } + ) + path = plot_loocv_diagnostics( + renamed, + tmp_path / "custom.png", + objective_column="target", + observed_column="truth", + predicted_column="estimate", + std_column="uncertainty", + ) + _assert_watermarked_png(path) + + +@pytest.mark.parametrize( + "call, message", + [ + ( + lambda path: plot_loocv_diagnostics(pd.DataFrame(), path), + "must not be empty", + ), + ( + lambda path: plot_nested_search_convergence( + pd.DataFrame({"pool_size": [10]}), path + ), + "missing required columns", + ), + ( + lambda path: plot_bounded_utility_comparison( + pd.DataFrame( + { + "policy": ["none"], + "raw_value": [np.nan], + "bounded_value": [1.0], + } + ), + path, + ), + "complete and finite", + ), + ( + lambda path: plot_local_penalty_tradeoff( + pd.DataFrame( + { + "variant": ["none"], + "sum_base_hvi": [1.0], + "minimum_within_batch_distance": [-0.1], + } + ), + path, + ), + "non-negative", + ), + ( + lambda path: plot_observation_influence_ranking( + pd.DataFrame({"sample_id": [1, 1], "influence_score": [0.2, 0.3]}), + path, + ), + "unique", + ), + ( + lambda path: plot_boundary_enrichment( + pd.DataFrame( + { + "input_name": ["x"], + "pool_lower_boundary_frequency": [0.1], + "top_lower_boundary_frequency": [0.2], + "selected_lower_boundary_frequency": [1.1], + "pool_upper_boundary_frequency": [0.1], + "top_upper_boundary_frequency": [0.2], + "selected_upper_boundary_frequency": [0.3], + } + ), + path, + ), + r"remain in \[0, 1\]", + ), + ( + lambda path: plot_robust_region_overview( + np.array([[0.1, 0.2], [0.8, 0.9]]), ["A"], path + ), + "one label per", + ), + ], +) +def test_plot_helpers_reject_empty_missing_or_invalid_data(tmp_path, call, message): + with pytest.raises((TypeError, ValueError), match=message): + call(tmp_path / "invalid.png") + assert plt.get_fignums() == [] + + +@pytest.mark.parametrize( + "call, message", + [ + ( + lambda path: plot_ard_lengthscale_comparison( + _hyperparameter_frame().assign(ard_lengthscale_speed=0.0), path + ), + "strictly positive", + ), + ( + lambda path: plot_control_omission_candidate_region_comparison( + np.array([[0.1, 0.2]]), np.array([[0.1, 0.2, 0.3]]), path + ), + "share dimensions", + ), + ( + lambda path: plot_candidate_predictions_vs_observed_ranges( + _prediction_range_frame().assign( + observed_minimum=2.0, observed_maximum=1.0 + ), + path, + ), + "must not exceed", + ), + ( + lambda path: plot_shortlist_medoid_parallel_coordinates( + _shortlist_frame().drop(columns="medoid_norm_1"), path + ), + "contiguous", + ), + ( + lambda path: plot_run_region_persistence_heatmap( + pd.concat( + [ + _membership_frame(), + _membership_frame().iloc[[0]].assign(run_family="conflicting"), + ], + ignore_index=True, + ), + path, + ), + "exactly one run family", + ), + ( + lambda path: plot_model_policy_region_correspondence( + _membership_frame().assign(weight=-1.0), + path, + value_column="weight", + ), + "non-negative", + ), + ( + lambda path: plot_acquisition_quality_vs_persistence( + _region_quality_frame().assign(family_weighted_persistence=1.2), + path, + ), + r"remain in \[0, 1\]", + ), + ( + lambda path: plot_shortlist_region_influence_sensitivity( + _region_sensitivity_frame().iloc[:-1], path + ), + "cover every", + ), + ], +) +def test_extended_plot_helpers_fail_closed_on_invalid_inputs(tmp_path, call, message): + with pytest.raises((TypeError, ValueError), match=message): + call(tmp_path / "invalid-extended.png") + assert plt.get_fignums() == [] + + +def test_loocv_uncertainty_and_region_inputs_fail_closed(tmp_path): + invalid_std = _loocv_frame() + invalid_std.loc[0, "predicted_std"] = 0.0 + with pytest.raises(ValueError, match="strictly positive"): + plot_loocv_diagnostics(invalid_std, tmp_path / "std.png") + + with pytest.raises(ValueError, match="at least two rows"): + plot_robust_region_overview(np.array([[0.2, 0.3]]), ["A"], tmp_path / "one.png") + with pytest.raises(ValueError, match="strictly positive"): + plot_robust_region_overview( + np.array([[0.2, 0.3], [0.7, 0.8]]), + ["A", "B"], + tmp_path / "persist.png", + persistence=[1.0, 0.0], + ) + with pytest.raises(ValueError, match="one nonblank name"): + plot_robust_region_overview( + np.array([[0.2, 0.3], [0.7, 0.8]]), + ["A", "B"], + tmp_path / "names.png", + input_names=["only_one"], + ) + assert plt.get_fignums() == [] + + +def test_output_must_be_png_and_no_figure_is_left_open(tmp_path): + with pytest.raises(ValueError, match=r"\.png suffix"): + plot_observation_influence_ranking( + pd.DataFrame({"sample_id": [1], "influence_score": [0.5]}), + tmp_path / "not_png.pdf", + ) + assert plt.get_fignums() == [] diff --git a/tests/test_sobol_pool.py b/tests/test_sobol_pool.py new file mode 100644 index 0000000..c43f022 --- /dev/null +++ b/tests/test_sobol_pool.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +import itertools + +import numpy as np +import pytest + +from mobo_kit.candidate_pool import ( + CandidatePoolSamplingError, + physical_rows_to_grid_indices, +) +from mobo_kit.design import InputSpec, build_design +from mobo_kit.sobol_pool import ( + build_nested_sobol_discrete_pool, + hash_grid_index_prefix, + map_unit_points_to_grid_indices, +) + + +def _design(): + return build_design( + [ + InputSpec("a", 0.0, 8.0, 1.0), + InputSpec("b", 10.0, 22.0, 2.0), + InputSpec("c", -2.0, 2.0, 1.0), + ] + ) + + +def test_unit_points_map_by_floor_to_exact_grid_indices(): + design = build_design( + [InputSpec("x", 0.0, 3.0, 1.0), InputSpec("y", 10.0, 20.0, 10.0)] + ) + points = np.array( + [ + [0.0, 0.0], + [0.249999, 0.499999], + [0.25, 0.5], + [np.nextafter(1.0, 0.0), np.nextafter(1.0, 0.0)], + ] + ) + np.testing.assert_array_equal( + map_unit_points_to_grid_indices(points, design), + np.array([[0, 0], [0, 0], [1, 1], [3, 1]]), + ) + + +@pytest.mark.parametrize( + "points, message", + [ + (np.array([[1.0, 0.0]]), "half-open"), + (np.array([[-1e-12, 0.0]]), "half-open"), + (np.array([[np.nan, 0.0]]), "finite"), + (np.array([0.2, 0.3]), "shape"), + ], +) +def test_unit_point_mapping_fails_closed(points, message): + design = build_design( + [InputSpec("x", 0.0, 3.0, 1.0), InputSpec("y", 0.0, 1.0, 1.0)] + ) + with pytest.raises(ValueError, match=message): + map_unit_points_to_grid_indices(points, design) + + +def test_same_scramble_is_deterministic_nested_and_has_locked_prefix_hashes(): + sizes = (8, 16, 32, 64) + first = build_nested_sobol_discrete_pool(_design(), sizes, scramble_seed=73) + repeat = build_nested_sobol_discrete_pool( + _design(), tuple(reversed(sizes)), scramble_seed=73 + ) + expected_hashes = { + 8: "E36D0663991DFBF5A53C278C3F78DD66AB7D5245B8B7306D7422960F506F0361", + 16: "9E9AC7931E14CB3E4BE992D4B17D0670BEF0E6DB6AF467E059CE39A89D8A55F6", + 32: "79DBFBEEC3B6C7D1AA2E2D685E6DF19D5CA546BAD84F575A376FC83305B6A65B", + 64: "22AFBA550FA4A2F39104A9B565932225E374C0B675ABB4685FB19A38D6A748B7", + } + + assert first.accepted_sizes == sizes + assert first.accepted_count == 64 + assert dict(first.prefix_hashes) == expected_hashes + assert dict(repeat.prefix_hashes) == expected_hashes + assert first.scipy_version + for smaller, larger in zip(sizes, sizes[1:]): + np.testing.assert_array_equal( + first.pools[smaller].grid_indices, + first.pools[larger].grid_indices[:smaller], + ) + for size in sizes: + pool = first.pools[size] + assert pool.size == size + assert np.unique(pool.grid_indices, axis=0).shape[0] == size + np.testing.assert_array_equal( + physical_rows_to_grid_indices(pool.X_phys, _design()), pool.grid_indices + ) + assert np.all((pool.X_norm >= 0.0) & (pool.X_norm <= 1.0)) + assert first.prefix_hashes[size] == hash_grid_index_prefix(pool.grid_indices) + np.testing.assert_array_equal( + pool.grid_indices, repeat.pools[size].grid_indices + ) + + +def test_different_scramble_changes_the_accepted_order_and_hash(): + primary = build_nested_sobol_discrete_pool(_design(), [64], scramble_seed=73) + secondary = build_nested_sobol_discrete_pool(_design(), [64], scramble_seed=137) + assert not np.array_equal( + primary.largest_pool.grid_indices, secondary.largest_pool.grid_indices + ) + assert primary.prefix_hashes[64] != secondary.prefix_hashes[64] + + +def test_exclusions_constraints_and_off_grid_observed_partition_are_stable(): + design = build_design( + [ + InputSpec("a", 0.0, 4.0, 1.0), + InputSpec("b", 0.0, 2.0, 1.0), + InputSpec("c", 0.0, 1.0, 1.0), + ] + ) + observed = np.array([[0.0, 0.0, 0.0], [0.5, 1.0, 1.0]]) + pending = np.array([[2.0, 1.0, 1.0]]) + avoid = np.array([[4.0, 2.0, 1.0]]) + + def require_even_a(X_phys, supplied_design): + assert supplied_design is design + return (X_phys[:, 0] % 2.0) == 0.0 + + result = build_nested_sobol_discrete_pool( + design, + [5, 15], + scramble_seed=11, + observed_phys=observed, + pending_phys=pending, + avoid_phys=avoid, + row_constraints=[require_even_a], + max_raw_draws=4096, + ) + excluded = { + tuple(row) + for row in physical_rows_to_grid_indices( + np.vstack([observed[:1], pending, avoid]), design + ) + } + final = result.largest_pool + assert result.ignored_off_grid_observed == 1 + assert final.size == 15 + assert np.all(final.X_phys[:, 0] % 2.0 == 0.0) + assert not ({tuple(row) for row in final.grid_indices} & excluded) + assert final.rejected_avoid > 0 + assert final.rejected_constraint > 0 + np.testing.assert_array_equal(result.pools[5].grid_indices, final.grid_indices[:5]) + + +def test_pending_and_explicit_avoid_rows_must_be_exactly_on_grid(): + off_grid = np.array([[0.25, 10.0, 0.0]]) + with pytest.raises(ValueError, match="off-grid"): + build_nested_sobol_discrete_pool( + _design(), [4], scramble_seed=1, pending_phys=off_grid + ) + with pytest.raises(ValueError, match="off-grid"): + build_nested_sobol_discrete_pool( + _design(), [4], scramble_seed=1, avoid_phys=off_grid + ) + + with pytest.raises(ValueError, match="within the design bounds"): + build_nested_sobol_discrete_pool( + _design(), + [4], + scramble_seed=1, + observed_phys=np.array([[9.0, 10.0, 0.0]]), + ) + + +def test_sampler_never_materializes_the_cartesian_product(monkeypatch): + def forbidden(*args, **kwargs): + del args, kwargs + raise AssertionError("full Cartesian materialization was attempted") + + monkeypatch.setattr(np, "meshgrid", forbidden) + monkeypatch.setattr(np, "indices", forbidden) + monkeypatch.setattr(itertools, "product", forbidden) + result = build_nested_sobol_discrete_pool(_design(), [16, 32], scramble_seed=9) + assert result.largest_pool.size == 32 + + +def test_impossible_capacity_and_draw_limit_fail_with_structured_errors(): + tiny = build_design([InputSpec("x", 0.0, 1.0, 1.0)]) + with pytest.raises(CandidatePoolSamplingError) as capacity: + build_nested_sobol_discrete_pool( + tiny, + [2], + scramble_seed=1, + observed_phys=np.array([[0.0]]), + ) + assert capacity.value.draws == 0 + assert "exceeds" in capacity.value.reason + + larger = build_design([InputSpec("x", 0.0, 7.0, 1.0)]) + + def reject_all(X_phys, supplied_design): + del supplied_design + return np.zeros(X_phys.shape[0], dtype=bool) + + with pytest.raises(CandidatePoolSamplingError) as limited: + build_nested_sobol_discrete_pool( + larger, + [1], + scramble_seed=2, + row_constraints=[reject_all], + max_raw_draws=8, + ) + assert limited.value.draws == 8 + assert limited.value.accepted == 0 + assert limited.value.rejected_constraint > 0 + + +@pytest.mark.parametrize( + "sizes, seed, max_draws, message", + [ + ([], 1, None, "must not be empty"), + ([0], 1, None, "positive integers"), + ([2, 2], 1, None, "duplicates"), + ([2], True, None, "scramble_seed"), + ([2], 1, 0, "max_raw_draws"), + ], +) +def test_sampler_configuration_validation(sizes, seed, max_draws, message): + kwargs = {} if max_draws is None else {"max_raw_draws": max_draws} + with pytest.raises(ValueError, match=message): + build_nested_sobol_discrete_pool(_design(), sizes, scramble_seed=seed, **kwargs) + + +def test_prefix_hash_requires_an_integer_matrix(): + with pytest.raises(TypeError, match="integer dtype"): + hash_grid_index_prefix(np.array([[0.0, 1.0]])) + with pytest.raises(ValueError, match="two-dimensional"): + hash_grid_index_prefix(np.array([0, 1], dtype=np.int64)) diff --git a/tests/test_step2a_synthetic.py b/tests/test_step2a_synthetic.py new file mode 100644 index 0000000..f3e0456 --- /dev/null +++ b/tests/test_step2a_synthetic.py @@ -0,0 +1,81 @@ +from importlib.util import module_from_spec, spec_from_file_location +import json +from pathlib import Path +import sys + +import numpy as np + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +EXAMPLE_PATH = REPOSITORY_ROOT / "examples" / "d2d_step2a_synthetic.py" + + +def _load_example(): + spec = spec_from_file_location("d2d_step2a_synthetic", EXAMPLE_PATH) + assert spec is not None and spec.loader is not None + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_synthetic_cpu_end_to_end_writes_only_to_tmp_path(tmp_path: Path): + module = _load_example() + repository_output = REPOSITORY_ROOT / "local_outputs" + before_repository_outputs = ( + set(repository_output.glob("**/*")) if repository_output.exists() else set() + ) + summary = module.run_synthetic_step2a( + tmp_path, + ucb_pool_size=80, + qlognehvi_pool_size=64, + posterior_samples=16, + qlognehvi_samples=8, + ) + assert summary.observed_count == 15 + assert summary.ucb_selected_pool_indices.shape == (5,) + assert summary.qlognehvi_selected_pool_indices.shape == (3,) + assert np.unique(summary.ucb_selected_pool_indices).size == 5 + assert np.unique(summary.qlognehvi_selected_pool_indices).size == 3 + assert summary.ucb_minimum_distance >= 0.2 - 1e-12 + assert summary.qlognehvi_minimum_distance >= 0.2 - 1e-12 + assert len(summary.plot_paths) == 8 + assert summary.ucb_metadata["method"] == "ucb_hvi" + assert summary.qlognehvi_metadata["method"] == "qlognehvi" + assert all( + path.is_file() and path.parent == tmp_path for path in summary.plot_paths + ) + report_path = tmp_path / "synthetic_summary.json" + assert report_path.is_file() + report = json.loads(report_path.read_text(encoding="utf-8")) + assert report["production_candidate_generation"] == "NOT_RUN" + assert report["seeds"] == { + "global": module.TEST_ONLY_SEED, + "observed_pool": module.TEST_ONLY_SEED, + "ucb_pool": module.TEST_ONLY_SEED + 1, + "ucb_posterior": module.TEST_ONLY_SEED + 2, + "qlognehvi_pool": module.TEST_ONLY_SEED + 3, + "qlognehvi_mc": module.TEST_ONLY_SEED + 4, + } + ucb_report = report["ucb_hvi"] + assert ucb_report["metadata"]["beta"] == 1.0 + assert ucb_report["metadata"]["kappa"] == 1.0 + assert ucb_report["metadata"]["pool_draws"] >= ucb_report["pool_size"] + assert len(ucb_report["selection_steps"]) == 5 + assert len(ucb_report["selected_utility_diagnostics"]) == 5 + assert all( + len(row[key]) == 3 + for row in ucb_report["selected_utility_diagnostics"] + for key in ("utility_mean", "utility_std", "utility_ucb") + ) + qlog_report = report["qlognehvi"] + assert qlog_report["metadata"]["mc_samples"] == 8 + assert qlog_report["metadata"]["pool_draws"] >= qlog_report["pool_size"] + assert qlog_report["pending_counts_by_selection_step"] == [5, 6, 7] + assert len(qlog_report["selection_steps"]) == 3 + assert len(report["plots"]) == 8 + assert report["runtime"]["device"] == "cpu" + after_repository_outputs = ( + set(repository_output.glob("**/*")) if repository_output.exists() else set() + ) + assert after_repository_outputs == before_repository_outputs diff --git a/tests/test_step2c_artifacts.py b/tests/test_step2c_artifacts.py new file mode 100644 index 0000000..9f8e71e --- /dev/null +++ b/tests/test_step2c_artifacts.py @@ -0,0 +1,917 @@ +from __future__ import annotations + +import csv +import hashlib +import json +import os +from pathlib import Path + +from PIL import Image, PngImagePlugin +import pytest +import yaml + +from mobo_kit.step2c_artifacts import ( + CONSENSUS_BATCH_FILE, + DEBUG_WATERMARK, + NO_STABLE_BATCH_REASON_FILE, + REQUIRED_PLOT_DIRECTORIES, + REQUIRED_PLOT_FILES, + REQUIRED_CSV_COLUMNS, + REQUIRED_TOP_LEVEL_FILES, + Step2CArtifactContractError, + validate_step2c_artifact_bundle, +) + + +STAMP_COLUMNS = [ + "debug_only", + "approved_for_experiment", + "approved_for_production", + "candidate_status", +] + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest().upper() + + +def _canonical_mapping_sha256(value: dict) -> str: + payload = json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("utf-8") + return hashlib.sha256(payload).hexdigest().upper() + + +def _stamped_payload(**values): + return { + **values, + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": DEBUG_WATERMARK, + } + + +def _write_json(path: Path, **values) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(_stamped_payload(**values), indent=2, sort_keys=True), + encoding="utf-8", + ) + + +def _write_csv( + path: Path, *, row_count: int = 1, overrides: dict | None = None +) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + semantic_columns = REQUIRED_CSV_COLUMNS.get(path.name, ("value",)) + fieldnames = [*semantic_columns, *STAMP_COLUMNS] + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + for index in range(row_count): + row = {column: index for column in semantic_columns} + for column, value in (overrides or {}).items(): + if column in row: + row[column] = value(index) if callable(value) else value + row.update( + { + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": DEBUG_WATERMARK, + } + ) + writer.writerow(row) + + +def _write_consensus_csv(path: Path, *, row_count: int = 5) -> None: + semantic_columns = [ + "consensus_candidate_id", + "selection_order", + "distinct_nonbaseline_family_count", + "all_grid_valid", + "all_hard_distance_valid", + *[ + value + for dimension, name in enumerate( + ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", + ) + ) + for value in (name, f"medoid_grid_{dimension}", f"medoid_norm_{dimension}") + ], + ] + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=[*semantic_columns, *STAMP_COLUMNS]) + writer.writeheader() + for index in range(row_count): + row = { + "consensus_candidate_id": f"DEBUG-{index + 1}", + "selection_order": index + 1, + "distinct_nonbaseline_family_count": 3, + "all_grid_valid": True, + "all_hard_distance_valid": True, + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": DEBUG_WATERMARK, + } + for dimension, name in enumerate( + ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", + ) + ): + row[name] = index + row[f"medoid_grid_{dimension}"] = index + row[f"medoid_norm_{dimension}"] = index / 4.0 + writer.writerow(row) + + +def _write_png(path: Path, *, description: str | None = DEBUG_WATERMARK) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + metadata = PngImagePlugin.PngInfo() + if description is not None: + metadata.add_text("Description", description) + Image.new("RGB", (12, 8), color="white").save(path, pnginfo=metadata) + + +def _rewrite_manifest(output_dir: Path, update) -> None: + path = output_dir / "run_manifest.json" + payload = json.loads(path.read_text(encoding="utf-8")) + update(payload) + path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8") + + +def _rewrite_csv_rows(path: Path, update) -> None: + with path.open("r", encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle) + rows = list(reader) + fieldnames = list(reader.fieldnames or ()) + update(rows) + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def _build_bundle( + tmp_path: Path, + *, + consensus: bool = False, + inside_configured_root: bool = True, +) -> tuple[Path, Path, Path]: + repository = tmp_path / "repo" + repository.mkdir() + configured_relative = Path("local_outputs/d2d_step2c_robustness") + parent = ( + repository / configured_relative + if inside_configured_root + else repository / "some_other_output_root" + ) + output_dir = parent / "run-001" + output_dir.mkdir(parents=True) + + workbook = repository / "local_inputs" / "private_campaign_input.xlsx" + workbook.parent.mkdir() + workbook.write_bytes(b"immutable workbook fixture") + fixed_mtime = 1_700_000_000_123_456_700 + os.utime(workbook, ns=(fixed_mtime, fixed_mtime)) + workbook_hash = _sha256(workbook) + workbook_mtime = workbook.stat().st_mtime_ns + + config_directory = repository / "configs" + config_directory.mkdir() + input_names = ( + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", + ) + base_config_path = config_directory / "synthetic_step2b.yaml" + base_config_path.write_text( + yaml.safe_dump( + { + "inputs": [ + {"name": name, "start": 0, "stop": 4, "step": 1, "decimals": 0} + for name in input_names + ] + }, + sort_keys=False, + ), + encoding="utf-8", + ) + config = { + "run_mode": "debug", + "approved_for_experiment": False, + "approved_for_production": False, + "debug_watermark": DEBUG_WATERMARK, + "base_step2b_config": "configs/synthetic_step2b.yaml", + "candidate_search": {"nested_unique_sizes": [8, 16, 32, 64]}, + "observation_influence": {"omitted_sample_ids": [1, 2, 3]}, + "robust_regions": { + "clustering": "agglomerative_complete_link", + "primary_distance_threshold": 0.15, + "consensus_batch_size": 5, + "consensus_required_criteria": { + "largest_two_nested_regional_matches_within_0_15": 4, + "largest_two_nested_mean_matched_distance_max": 0.10, + "minimum_region_family_coverage": 3, + "require_grid_valid": True, + "require_hard_distance_valid": True, + }, + }, + "execution_modes": { + "fast": { + "nested_unique_sizes": [2, 4, 8, 16], + "omitted_sample_ids": [1], + }, + "full": { + "nested_unique_sizes": [8, 16, 32, 64], + "omitted_sample_ids": [1, 2, 3], + }, + }, + "outputs": {"root": configured_relative.as_posix()}, + } + source_config_path = config_directory / "d2d_step2c_debug.yaml" + source_config_path.write_text( + yaml.safe_dump(config, sort_keys=True), encoding="utf-8" + ) + (output_dir / "resolved_debug_config.yaml").write_text( + yaml.safe_dump(config, sort_keys=True), encoding="utf-8" + ) + + for name in REQUIRED_TOP_LEVEL_FILES: + path = output_dir / name + if name in {"resolved_debug_config.yaml", "run_manifest.json"}: + continue + if name == "DEBUG_ONLY_NOT_APPROVED_FOR_EXPERIMENT.txt": + path.write_text(f"{DEBUG_WATERMARK}\n", encoding="utf-8") + continue + if path.suffix == ".json": + _write_json(path, artifact=name) + elif path.suffix == ".csv": + _write_csv(path, row_count=0 if name == "model_fit_warnings.csv" else 1) + + evidence_count = 5 if consensus else 4 + _write_csv( + output_dir / "nested_pool_convergence_summary.csv", + overrides={ + "comparison_type": "prefix_vs_largest", + "reference_pool_size": 64, + "comparison_pool_size": 32, + "mean_matched_distance": 0.0, + "regional_matches_within_0.15": 5, + }, + ) + common_region_overrides = { + "region_id": lambda index: f"REGION-{index + 1:03d}", + "member_count": 1, + "distinct_nonbaseline_family_count": 3, + "all_grid_valid": True, + "all_hard_distance_valid": True, + "family_weighted_persistence": 1.0, + **{f"medoid_grid_{dimension}": lambda index: index for dimension in range(10)}, + **{ + f"medoid_norm_{dimension}": lambda index: index / 4.0 + for dimension in range(10) + }, + } + _write_csv( + output_dir / "robust_regions.csv", + row_count=evidence_count, + overrides=common_region_overrides, + ) + _write_csv( + output_dir / "r1_robust_shortlist_debug.csv", + row_count=evidence_count, + overrides={ + **common_region_overrides, + "shortlist_id": lambda index: f"R1-RS{index + 1:02d}", + "lower_boundary_dimensions": "", + "upper_boundary_dimensions": "", + **{name: lambda index: index for name in input_names}, + }, + ) + + consensus_checks = { + "full_mode_eligible_for_consensus": True, + "largest_two_nested_regional_matches": True, + "largest_two_nested_mean_matched_distance": True, + "five_regions_cover_three_core_families": consensus, + "exact_five_candidates": consensus, + "chosen_regions_cover_three_core_families": consensus, + "finite_and_bounded": consensus, + "unique_and_on_grid": consensus, + "chosen_pairwise_hard_distance_valid": consensus, + "debug_only": consensus, + "experimental_approval_false": consensus, + "production_approval_false": consensus, + } + consensus_observed = { + "largest_two_regional_matches_within_0_15": 5, + "largest_two_mean_matched_distance": 0.0, + "regions_with_minimum_family_coverage": evidence_count, + "consensus_candidate_count": evidence_count, + "chosen_regions_family_qualified": consensus, + "chosen_candidates_finite_and_bounded": consensus, + "chosen_candidates_unique_and_on_grid": consensus, + "chosen_pairwise_minimum_distance": ( + (10 * (1 / 4) ** 2) ** 0.5 if consensus else 0.0 + ), + "required_pairwise_minimum_distance": 0.15, + "chosen_hard_distance_valid": consensus, + "full_mode_eligible_for_consensus": True, + } + _write_json( + output_dir / "run_manifest.json", + schema_version="d2d-step2c-robustness-run-v1", + method_version="synthetic-test-v1", + git_commit="1" * 40, + git_dirty=True, + git_status_at_start=["synthetic fixture"], + step2b_checkpoint="2" * 40, + config_path=str(source_config_path), + config_sha256=_sha256(source_config_path), + resolved_config_hash=_canonical_mapping_sha256(config), + workbook_path=workbook.relative_to(repository).as_posix(), + workbook_sha256_before=workbook_hash, + workbook_sha256_after=workbook_hash, + workbook_mtime_ns_before=workbook_mtime, + workbook_mtime_ns_after=workbook_mtime, + source_workbook_modified=False, + mode="full", + consensus_passed=consensus, + objective_order=[ + "uniformity_score", + "optoelectronic_score", + "thickness_score", + ], + objective_source_columns=[ + "Uniformity score", + "Optoelectronic score", + "Thickness score", + ], + reference_point=[-0.01, -10.0, -0.01], + objective_bounds=[[0.0, 1.0], [None, None], [0.0, 1.0]], + moment_method="analytic_identity", + analytic_mc_comparison={"analytic_mc_debug_check_passed": True}, + sobol_seeds=[73, 137, 911], + nested_pool_sizes=[8, 16, 32, 64], + pool_prefix_hashes={ + "73": {str(size): "A" * 64 for size in (8, 16, 32, 64)}, + "137": {"64": "B" * 64}, + "911": {"64": "C" * 64}, + }, + local_refinement={"anchors_per_selection_step": 2, "max_sweeps": 2}, + beta_values=[1.0, 4.0, 9.0], + bound_policies=["none", "clip_ucb"], + local_penalty_variants=[ + {"label": label} + for label in ( + "no_soft_no_hard", + "no_soft_hard_0_15", + "radius_0_15", + "radius_0_25", + "radius_0_35", + ) + ], + model_variants=[ + {"name": "default_current"}, + {"name": "conservative"}, + ], + influence_common_pool_hash="D" * 64, + influence_common_pool_size=32, + influence_omitted_sample_ids=[1, 2, 3], + robust_region_clustering="agglomerative_complete_link", + robust_region_threshold=0.15, + robust_region_sensitivity_counts={ + "threshold_0.10": 6, + "threshold_0.20": 4, + }, + robust_region_count=evidence_count, + shortlist_count=evidence_count, + stability_criteria={ + "consensus_batch_size": 5, + "regional_match_threshold": 0.15, + "largest_two_nested_regional_match_minimum": 4, + "largest_two_nested_mean_matched_distance_maximum": 0.10, + "minimum_nonbaseline_core_family_coverage": 3, + "required_pairwise_minimum_distance": 0.15, + "require_finite_and_bounded": True, + "require_unique_and_on_grid": True, + "require_debug_only_and_approval_false": True, + }, + consensus_checks=consensus_checks, + consensus_observed=consensus_observed, + runtime_versions={ + name: "test" + for name in ( + "python", + "numpy", + "pandas", + "scipy", + "scikit_learn", + "matplotlib", + "torch", + "botorch", + "gpytorch", + ) + }, + hardware={"logical_cpu_count": 1}, + phase_runtime_seconds={"synthetic": 0.1}, + runtime_seconds_total=0.2, + known_uniformity_score_mismatch=True, + uniformity_warning_count=1, + control_assumption="measured_in_current_campaign", + off_grid_control_exception=[{"sample_id": 1, "value": 12.0}], + real_r2_proposal_generated=False, + workbook_writeback_performed=False, + output_directory=str(output_dir), + public_summary_archive_requested=False, + private_evidence_archive_requested=False, + ) + if consensus: + _write_consensus_csv(output_dir / CONSENSUS_BATCH_FILE, row_count=5) + else: + failed_checks = [ + name for name, passed in consensus_checks.items() if not passed + ] + _write_json( + output_dir / NO_STABLE_BATCH_REASON_FILE, + message="No stable batch in synthetic fixture.", + checks=consensus_checks, + failed_checks=failed_checks, + observed=consensus_observed, + consensus_passed=False, + ) + for relative in REQUIRED_PLOT_FILES: + _write_png(output_dir / relative) + return repository, output_dir, workbook + + +def _snapshot(paths: list[Path]) -> dict[Path, tuple[str, int, int]]: + return { + path: (_sha256(path), path.stat().st_mtime_ns, path.stat().st_size) + for path in paths + } + + +def test_valid_no_stable_bundle_returns_hash_map_and_is_read_only(tmp_path): + repository, output_dir, workbook = _build_bundle(tmp_path) + files = sorted(path for path in output_dir.rglob("*") if path.is_file()) + before = _snapshot([*files, workbook]) + + result = validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + assert result.output_dir == output_dir.resolve() + assert ( + result.ignored_output_root + == (repository / "local_outputs/d2d_step2c_robustness").resolve() + ) + assert result.conditional_artifact == NO_STABLE_BATCH_REASON_FILE + assert result.workbook_path == workbook.resolve() + assert result.workbook_sha256 == _sha256(workbook) + assert result.workbook_mtime_ns == workbook.stat().st_mtime_ns + assert "model_fit_warnings.csv" in result.csv_files_checked + assert set(result.png_files_checked) == set(REQUIRED_PLOT_FILES) + assert set(result.artifact_sha256) == { + path.relative_to(output_dir).as_posix() for path in files + } + assert result.artifact_sha256["run_manifest.json"] == _sha256( + output_dir / "run_manifest.json" + ) + assert _snapshot([*files, workbook]) == before + + +def test_consensus_allows_only_explicit_debug_preview_replicate(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, consensus=True) + preview = output_dir / "r1_replicate_debug_preview.csv" + _write_csv(preview) + + result = validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + assert result.conditional_artifact == CONSENSUS_BATCH_FILE + assert preview.name in result.csv_files_checked + + +def test_consensus_requires_full_mode_and_exactly_five_rows(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, consensus=True) + _rewrite_manifest(output_dir, lambda payload: payload.__setitem__("mode", "fast")) + with pytest.raises(Step2CArtifactContractError, match="forbidden outside a full"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + _rewrite_manifest(output_dir, lambda payload: payload.__setitem__("mode", "full")) + _write_consensus_csv(output_dir / CONSENSUS_BATCH_FILE, row_count=4) + with pytest.raises(Step2CArtifactContractError, match="exactly five"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_all_exact_required_top_level_names_are_enforced(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + (output_dir / "sobol_scramble_comparison.csv").unlink() + + with pytest.raises( + Step2CArtifactContractError, + match="Missing required top-level.*sobol_scramble_comparison.csv", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +@pytest.mark.parametrize("conditional_state", ["both", "neither"]) +def test_exactly_one_conditional_artifact_is_required(tmp_path, conditional_state): + repository, output_dir, _ = _build_bundle(tmp_path) + if conditional_state == "both": + _write_csv(output_dir / CONSENSUS_BATCH_FILE) + else: + (output_dir / NO_STABLE_BATCH_REASON_FILE).unlink() + + with pytest.raises(Step2CArtifactContractError, match="Exactly one conditional"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_required_plot_topic_directories_and_files_are_enforced(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + missing = output_dir / "plots/search_convergence/boundary_enrichment.png" + missing.unlink() + + with pytest.raises( + Step2CArtifactContractError, match="Missing required plot-topic artifact" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + assert set(REQUIRED_PLOT_DIRECTORIES) == { + "plots/model_validation", + "plots/search_convergence", + "plots/bounded_utility", + "plots/local_penalty", + "plots/influence", + "plots/robust_regions", + } + + +def test_zero_row_csv_requires_stamp_headers_but_not_stamp_rows(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + empty_path = output_dir / "model_fit_warnings.csv" + empty_path.write_text("warning,debug_only\n", encoding="utf-8") + + with pytest.raises( + Step2CArtifactContractError, match="missing debug stamp columns" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +@pytest.mark.parametrize( + "relative, field, bad_value, message", + [ + ("score_validation.csv", "debug_only", False, "debug_only=true"), + ( + "training_row_manifest.csv", + "approved_for_experiment", + True, + "approved_for_experiment=false", + ), + ( + "robust_regions.csv", + "candidate_status", + "NOT A DEBUG WATERMARK", + "exact debug watermark", + ), + ], +) +def test_every_csv_row_must_carry_exact_debug_approval_stamps( + tmp_path, relative, field, bad_value, message +): + repository, output_dir, _ = _build_bundle(tmp_path) + path = output_dir / relative + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=["value", *STAMP_COLUMNS]) + writer.writeheader() + row = { + "value": 1, + "debug_only": True, + "approved_for_experiment": False, + "approved_for_production": False, + "candidate_status": DEBUG_WATERMARK, + } + row[field] = bad_value + writer.writerow(row) + + with pytest.raises(Step2CArtifactContractError, match=message): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_additional_nested_csv_and_every_json_are_also_stamped(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + additional = output_dir / "plots" / "review" / "extra.csv" + additional.parent.mkdir() + additional.write_text("value\n1\n", encoding="utf-8") + with pytest.raises( + Step2CArtifactContractError, match="extra.csv.*missing debug stamp columns" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + additional.unlink() + audit = output_dir / "workbook_audit.json" + payload = json.loads(audit.read_text(encoding="utf-8")) + payload["approved_for_production"] = "false" + audit.write_text(json.dumps(payload), encoding="utf-8") + with pytest.raises( + Step2CArtifactContractError, match="approved_for_production=false" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +@pytest.mark.parametrize("description", [None, "DEBUG ONLY"]) +def test_every_png_requires_exact_description_watermark(tmp_path, description): + repository, output_dir, _ = _build_bundle(tmp_path) + path = output_dir / REQUIRED_PLOT_FILES[0] + _write_png(path, description=description) + + with pytest.raises( + Step2CArtifactContractError, match="PNG Description.*exact debug watermark" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_replicate_worklist_is_forbidden_without_consensus(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + _write_csv(output_dir / "r1_replicate_debug_preview.csv") + + with pytest.raises( + Step2CArtifactContractError, match="forbidden without.*consensus" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_consensus_replicate_file_must_be_named_debug_preview(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, consensus=True) + _write_csv(output_dir / "r1_replicate_worklist.csv") + + with pytest.raises( + Step2CArtifactContractError, match="explicitly named as a debug preview" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_output_must_be_below_resolved_config_ignored_root(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, inside_configured_root=False) + + with pytest.raises( + Step2CArtifactContractError, + match="strict descendant of configured ignored root", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_configured_output_root_cannot_escape_repository(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + config_path = output_dir / "resolved_debug_config.yaml" + config = yaml.safe_load(config_path.read_text(encoding="utf-8")) + config["outputs"]["root"] = "../outside-repository" + config_path.write_text(yaml.safe_dump(config), encoding="utf-8") + + with pytest.raises( + Step2CArtifactContractError, match="strict descendant of repository_root" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +@pytest.mark.parametrize( + "update, message", + [ + ( + lambda payload: payload.__setitem__("workbook_sha256_after", "0" * 64), + "SHA-256 changed", + ), + ( + lambda payload: payload.__setitem__( + "workbook_mtime_ns_after", payload["workbook_mtime_ns_before"] + 1 + ), + "mtime changed", + ), + ( + lambda payload: payload.__setitem__("source_workbook_modified", True), + "source_workbook_modified.*false", + ), + ], +) +def test_manifest_requires_unchanged_workbook_before_after_proof( + tmp_path, update, message +): + repository, output_dir, _ = _build_bundle(tmp_path) + _rewrite_manifest(output_dir, update) + + with pytest.raises(Step2CArtifactContractError, match=message): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_live_workbook_hash_must_match_manifest_proof(tmp_path): + repository, output_dir, workbook = _build_bundle(tmp_path) + workbook.write_bytes(b"changed after completed run") + + with pytest.raises( + Step2CArtifactContractError, match="live source workbook SHA-256" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_live_workbook_mtime_must_match_manifest_proof(tmp_path): + repository, output_dir, workbook = _build_bundle(tmp_path) + recorded = workbook.stat().st_mtime_ns + os.utime(workbook, ns=(recorded + 100, recorded + 100)) + + with pytest.raises(Step2CArtifactContractError, match="live source workbook mtime"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_manifest_requires_complete_step2c_provenance(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + _rewrite_manifest(output_dir, lambda payload: payload.pop("sobol_seeds")) + + with pytest.raises( + Step2CArtifactContractError, match="missing required provenance.*sobol_seeds" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_resolved_config_hash_is_recomputed_from_bundled_yaml(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + path = output_dir / "resolved_debug_config.yaml" + config = yaml.safe_load(path.read_text(encoding="utf-8")) + config["robust_regions"]["consensus_batch_size"] = 4 + path.write_text(yaml.safe_dump(config, sort_keys=True), encoding="utf-8") + + with pytest.raises( + Step2CArtifactContractError, match="canonical resolved_debug_config.*SHA-256" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_nested_convergence_claim_is_cross_checked_against_csv(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + path = output_dir / "nested_pool_convergence_summary.csv" + _rewrite_csv_rows( + path, + lambda rows: rows[0].__setitem__("regional_matches_within_0.15", "3"), + ) + + with pytest.raises( + Step2CArtifactContractError, + match="consensus check largest_two_nested_regional_matches disagrees", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_manifest_cannot_weaken_resolved_consensus_criteria(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + _rewrite_manifest( + output_dir, + lambda payload: payload["stability_criteria"].__setitem__( + "largest_two_nested_regional_match_minimum", 1 + ), + ) + + with pytest.raises( + Step2CArtifactContractError, + match="stability criterion.*does not match the resolved debug config", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_qualifying_region_count_is_cross_checked_against_csv(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + path = output_dir / "robust_regions.csv" + _rewrite_csv_rows(path, lambda rows: rows.pop()) + + with pytest.raises( + Step2CArtifactContractError, + match="robust_region_count disagrees", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_required_csvs_require_semantic_columns_and_nonempty_rows(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + _write_csv(output_dir / "model_validation_summary.csv", row_count=0) + + with pytest.raises( + Step2CArtifactContractError, + match="model_validation_summary.csv must contain at least one", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_consensus_requires_recipe_and_independently_validates_uniqueness(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, consensus=True) + _write_csv(output_dir / CONSENSUS_BATCH_FILE, row_count=5) + with pytest.raises( + Step2CArtifactContractError, match="missing safety/recipe columns" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + _write_consensus_csv(output_dir / CONSENSUS_BATCH_FILE, row_count=5) + path = output_dir / CONSENSUS_BATCH_FILE + with path.open("r", encoding="utf-8", newline="") as handle: + rows = list(csv.DictReader(handle)) + fieldnames = list(rows[0]) + for key in rows[0]: + if key in STAMP_COLUMNS or key in { + "consensus_candidate_id", + "selection_order", + }: + continue + rows[1][key] = rows[0][key] + rows[1]["consensus_candidate_id"] = "DEBUG-2" + rows[1]["selection_order"] = "2" + with path.open("w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + with pytest.raises(Step2CArtifactContractError, match="must be unique exact grid"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_consensus_recipes_must_match_eligible_shortlist_head(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, consensus=True) + path = output_dir / CONSENSUS_BATCH_FILE + + def change_to_different_valid_recipe(rows): + rows[0]["speed_1"] = "1" + rows[0]["medoid_grid_0"] = "1" + rows[0]["medoid_norm_0"] = "0.25" + + _rewrite_csv_rows(path, change_to_different_valid_recipe) + + with pytest.raises( + Step2CArtifactContractError, + match="do not match the eligible shortlist head", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_no_stable_reason_must_name_failed_manifest_checks(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + reason = output_dir / NO_STABLE_BATCH_REASON_FILE + payload = json.loads(reason.read_text(encoding="utf-8")) + original_failed = list(payload["failed_checks"]) + payload["failed_checks"] = original_failed[:-1] + reason.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises(Step2CArtifactContractError, match="must name every failed"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + payload["failed_checks"] = original_failed + payload["observed"] = {"fabricated": 999} + reason.write_text(json.dumps(payload), encoding="utf-8") + with pytest.raises(Step2CArtifactContractError, match="observed values must match"): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_shortlist_regions_must_match_robust_region_evidence(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path, consensus=True) + path = output_dir / "r1_robust_shortlist_debug.csv" + _rewrite_csv_rows( + path, lambda rows: rows[0].__setitem__("region_id", "NOT-IN-ROBUST-REGIONS") + ) + + with pytest.raises( + Step2CArtifactContractError, + match="shortlist region_id must reference robust_regions.csv", + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) + + +def test_unstamped_recipe_capable_formats_are_forbidden(tmp_path): + repository, output_dir, _ = _build_bundle(tmp_path) + (output_dir / "candidate_preview.xlsx").write_bytes(b"not permitted") + + with pytest.raises( + Step2CArtifactContractError, match="Unsupported artifact format" + ): + validate_step2c_artifact_bundle(output_dir, repository_root=repository) diff --git a/tests/test_ucb_hvi.py b/tests/test_ucb_hvi.py new file mode 100644 index 0000000..4b59b59 --- /dev/null +++ b/tests/test_ucb_hvi.py @@ -0,0 +1,447 @@ +import numpy as np +import pytest +import torch +from botorch.models import ModelListGP, SingleTaskGP +from botorch.models.transforms.outcome import Standardize + +from mobo_kit.ucb_hvi import ( + UCBHVIScoreResult, + apply_ucb_bound_policy, + hypervolume_improvement_scores, + posterior_identity_moments, + posterior_utility_moments, + propose_ucb_hvi_batch, + score_ucb_hvi_from_moments, + score_ucb_hvi_pool, +) +from mobo_kit.batch_selection import LocalPenalizationConfig, UndersizedBatchError +from mobo_kit.candidate_pool import CandidatePool +from mobo_kit.objectives import ObjectiveSpec, ObjectiveTransform +import mobo_kit.ucb_hvi as module + + +class IdentityTransform: + def transform(self, Y: torch.Tensor) -> torch.Tensor: + return Y + + +def _identity_contract(count=1): + return ObjectiveTransform( + [ + ObjectiveSpec(f"objective_{index}", "maximize", "identity") + for index in range(count) + ], + version="TEST-IDENTITY-v1", + ) + + +class _ExactPosteriorModel: + def posterior(self, X, *, observation_noise): + mean = torch.stack((X[..., 0] + 0.25, 1.5 - X[..., 0]), dim=-1) + variance = torch.full_like(mean, 0.09 if not observation_noise else 0.16) + return type("Posterior", (), {"mean": mean, "variance": variance})() + + +def _observed_2d(): + return np.array([[0.4, 0.8], [0.8, 0.4], [0.2, 0.2]]) + + +def test_beta_zero_and_uncertainty_definition(): + means = np.array([[0.7, 0.7], [0.6, 0.6]]) + std = np.array([[0.2, 0.1], [0.0, 0.0]]) + zero = score_ucb_hvi_from_moments( + means, std, _observed_2d(), np.array([0.0, 0.0]), beta=0.0 + ) + assert np.allclose(zero.utility_ucb, means) + positive = score_ucb_hvi_from_moments( + means, std, _observed_2d(), np.array([0.0, 0.0]), beta=4.0 + ) + assert positive.kappa == pytest.approx(2.0) + assert np.allclose(positive.utility_ucb, means + 2.0 * std) + assert positive.base_score[0] > zero.base_score[0] + with pytest.raises(ValueError, match="beta"): + score_ucb_hvi_from_moments( + means, std, _observed_2d(), np.array([0.0, 0.0]), beta=-0.1 + ) + with pytest.raises(ValueError, match="non-boolean"): + score_ucb_hvi_from_moments( + means, std, _observed_2d(), np.array([0.0, 0.0]), beta=True + ) + + +def test_hvi_matches_hand_calculation_and_dominated_is_true_zero(): + scores, baseline, pareto, _ = hypervolume_improvement_scores( + np.array([[0.7, 0.7], [0.3, 0.3]]), + _observed_2d(), + np.array([0.0, 0.0]), + ) + assert baseline == pytest.approx(0.48) + # Added area: (0.7-0.4)*(0.7-0.4) = 0.09. + assert scores[0] == pytest.approx(0.09) + assert scores[1] == 0.0 + assert pareto.shape == (2, 2) + + +def test_three_objective_hvi_and_chunk_invariance(): + observed = np.array([[0.5, 0.5, 0.5]]) + candidates = np.array([[0.6, 0.6, 0.6], [0.4, 0.4, 0.4]]) + chunked = hypervolume_improvement_scores( + candidates, observed, np.zeros(3), chunk_size=1 + ) + whole = hypervolume_improvement_scores( + candidates, observed, np.zeros(3), chunk_size=20 + ) + assert np.allclose(chunked[0], whole[0]) + assert chunked[0][0] == pytest.approx(0.6**3 - 0.5**3) + assert chunked[0][1] == 0.0 + + +def test_dominated_observed_rows_do_not_change_scores(): + candidates = np.array([[0.7, 0.7]]) + with_dominated = hypervolume_improvement_scores( + candidates, _observed_2d(), np.zeros(2) + )[0] + without_dominated = hypervolume_improvement_scores( + candidates, _observed_2d()[:2], np.zeros(2) + )[0] + assert np.allclose(with_dominated, without_dominated) + + +@pytest.mark.parametrize( + "reference, message", + [(None, "required"), (np.zeros(3), "shape"), (np.array([0.0, np.nan]), "finite")], +) +def test_reference_point_validation(reference, message): + with pytest.raises(ValueError, match=message): + hypervolume_improvement_scores( + np.array([[0.7, 0.7]]), _observed_2d(), reference + ) + + +def test_posterior_utility_moments_are_seeded_and_chunk_invariant(): + train_X = torch.tensor([[0.0], [0.5], [1.0]], dtype=torch.double) + train_Y = torch.tensor([[0.0], [1.0], [0.0]], dtype=torch.double) + model = SingleTaskGP(train_X, train_Y, outcome_transform=Standardize(m=1)) + model.eval() + pool = torch.linspace(0.1, 0.9, 6, dtype=torch.double).unsqueeze(-1) + one = posterior_utility_moments( + model, + pool, + IdentityTransform(), + mc_samples=16, + seed=13, + chunk_size=1, + ) + all_at_once = posterior_utility_moments( + model, + pool, + IdentityTransform(), + mc_samples=16, + seed=13, + chunk_size=100, + ) + repeat = posterior_utility_moments( + model, + pool, + IdentityTransform(), + mc_samples=16, + seed=13, + chunk_size=2, + ) + assert np.allclose(one.utility_mean, all_at_once.utility_mean) + assert np.allclose(one.utility_std, all_at_once.utility_std) + assert np.allclose(one.utility_mean, repeat.utility_mean) + assert np.allclose(one.utility_std, repeat.utility_std) + assert np.all(one.utility_std > 0) + + +def test_analytic_identity_moments_are_exact_chunk_dtype_and_device_invariant(): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + pool = torch.linspace(0.1, 0.9, 7, dtype=torch.float32, device=device).unsqueeze(-1) + model = _ExactPosteriorModel() + contract = _identity_contract(2) + + one = posterior_identity_moments( + model, pool, contract, chunk_size=1, observation_noise=False + ) + whole = posterior_identity_moments( + model, pool, contract, chunk_size=100, observation_noise=False + ) + expected = model.posterior(pool, observation_noise=False) + + assert one.moment_method == "analytic_identity" + assert one.objective_contract_version == "TEST-IDENTITY-v1" + assert one.utility_mean.dtype == pool.dtype + assert one.utility_mean.device == pool.device + assert one.utility_std.dtype == pool.dtype + assert one.utility_std.device == pool.device + assert torch.equal(one.utility_mean, expected.mean) + assert torch.equal(one.utility_std, expected.variance.sqrt()) + assert torch.equal(one.utility_mean, whole.utility_mean) + assert torch.equal(one.utility_std, whole.utility_std) + + +def test_analytic_identity_moments_reject_nonidentity_or_implicit_contracts(): + pool = torch.tensor([[0.2], [0.8]], dtype=torch.double) + nonidentity = ObjectiveTransform( + [ + ObjectiveSpec( + "scaled", + "maximize", + "affine", + lower_anchor=0.0, + upper_anchor=1.0, + ) + ], + version="TEST-NONLINEAR-v1", + ) + with pytest.raises(ValueError, match="every objective.*identity"): + posterior_identity_moments( + _ExactPosteriorModel(), pool, nonidentity, chunk_size=2 + ) + with pytest.raises(ValueError, match="explicit versioned objective contract"): + posterior_identity_moments( + _ExactPosteriorModel(), pool, IdentityTransform(), chunk_size=2 + ) + + +def test_analytic_moments_agree_with_high_sample_mc_and_stable_hvi_selection(): + train_X = torch.tensor([[0.0], [0.5], [1.0]], dtype=torch.double) + first_Y = torch.tensor([[0.1], [0.9], [0.4]], dtype=torch.double) + second_Y = torch.tensor([[0.8], [0.2], [0.7]], dtype=torch.double) + model = ModelListGP( + SingleTaskGP(train_X, first_Y, outcome_transform=Standardize(m=1)), + SingleTaskGP(train_X, second_Y, outcome_transform=Standardize(m=1)), + ) + model.eval() + pool = torch.linspace(0.05, 0.95, 9, dtype=torch.double).unsqueeze(-1) + contract = _identity_contract(2) + + analytic_moments = posterior_identity_moments(model, pool, contract, chunk_size=4) + mc_moments = posterior_utility_moments( + model, + pool, + contract, + mc_samples=4096, + seed=17, + chunk_size=3, + ) + np.testing.assert_allclose( + mc_moments.utility_mean, + analytic_moments.utility_mean.cpu().numpy(), + atol=0.02, + rtol=0.02, + ) + np.testing.assert_allclose( + mc_moments.utility_std, + analytic_moments.utility_std.cpu().numpy(), + atol=0.02, + rtol=0.05, + ) + + observed = torch.cat((first_Y, second_Y), dim=1) + reference = np.array([-0.1, -0.1]) + analytic_scores = score_ucb_hvi_pool( + model, + pool, + observed, + contract, + reference, + beta=4.0, + moment_method="analytic_identity", + posterior_chunk_size=4, + ) + mc_scores = score_ucb_hvi_pool( + model, + pool, + observed, + contract, + reference, + beta=4.0, + moment_method="monte_carlo", + mc_samples=4096, + seed=17, + posterior_chunk_size=3, + ) + assert analytic_scores.moment_method == "analytic_identity" + assert analytic_scores.mc_samples is None + assert analytic_scores.seed is None + assert int(np.argmax(analytic_scores.base_score)) == int( + np.argmax(mc_scores.base_score) + ) + + +def test_ucb_bound_policy_retains_raw_effective_and_clip_amounts(): + raw = np.array([[1.2, -3.0, -0.2], [0.8, -2.0, 1.1]]) + original = raw.copy() + bounds = [(0.0, 1.0), (None, None), (0.0, 1.0)] + + unchanged = apply_ucb_bound_policy(raw, bounds, "none") + clipped = apply_ucb_bound_policy(raw, bounds, "clip_ucb") + + np.testing.assert_array_equal(raw, original) + np.testing.assert_array_equal(unchanged.utility_ucb_raw, raw) + np.testing.assert_array_equal(unchanged.utility_ucb_effective, raw) + np.testing.assert_array_equal(unchanged.utility_ucb_clip_amount, 0.0) + np.testing.assert_allclose( + clipped.utility_ucb_effective, + np.array([[1.0, -3.0, 0.0], [0.8, -2.0, 1.0]]), + ) + np.testing.assert_allclose( + clipped.utility_ucb_clip_amount, + np.array([[0.2, 0.0, 0.2], [0.0, 0.0, 0.1]]), + ) + + +def test_score_uses_effective_bounded_ucb_for_hvi_and_keeps_compatibility_alias(): + means = np.array([[1.2, 0.8, 1.2]]) + observed = np.array([[0.5, 0.5, 0.5]]) + bounds = [(0.0, 1.0), (None, None), (0.0, 1.0)] + result = score_ucb_hvi_from_moments( + means, + np.zeros_like(means), + observed, + np.zeros(3), + beta=0.0, + bound_policy="clip_ucb", + utility_bounds=bounds, + moment_method="analytic_identity", + ) + expected_scores = hypervolume_improvement_scores( + np.array([[1.0, 0.8, 1.0]]), observed, np.zeros(3) + )[0] + np.testing.assert_allclose(result.base_score, expected_scores) + np.testing.assert_allclose(result.utility_ucb_raw, means) + np.testing.assert_allclose(result.utility_ucb_effective, [[1.0, 0.8, 1.0]]) + np.testing.assert_array_equal(result.utility_ucb, result.utility_ucb_effective) + assert result.bound_policy == "clip_ucb" + + +@pytest.mark.parametrize( + "bounds, policy, message", + [ + (None, "clip_ucb", "requires explicit"), + ([(0.0, 1.0)], "clip_ucb", "one.*per objective"), + ([(1.0, 0.0), (None, None)], "clip_ucb", "must not exceed"), + ([(None, None), (None, None)], "clip_ucb", "at least one"), + (None, "invalid", "none.*clip_ucb"), + ], +) +def test_ucb_bound_policy_rejects_invalid_contract(bounds, policy, message): + with pytest.raises(ValueError, match=message): + apply_ucb_bound_policy(np.ones((2, 2)), bounds, policy) + + +def test_zero_scores_remain_zero_while_log_scores_are_stabilized(): + result = score_ucb_hvi_from_moments( + np.array([[0.1, 0.1]]), + np.zeros((1, 2)), + _observed_2d(), + np.zeros(2), + beta=0.0, + log_epsilon=1e-9, + ) + assert result.base_score[0] == 0.0 + assert result.base_log_score[0] == pytest.approx(np.log(1e-9)) + + +def test_invalid_standard_deviation_and_shapes_fail(): + with pytest.raises(ValueError, match="negative"): + score_ucb_hvi_from_moments( + np.ones((1, 2)), + np.array([[-1.0, 0.0]]), + _observed_2d(), + np.zeros(2), + beta=1.0, + ) + with pytest.raises(ValueError, match="same shape"): + score_ucb_hvi_from_moments( + np.ones((1, 2)), + np.ones((2, 2)), + _observed_2d(), + np.zeros(2), + beta=1.0, + ) + + +def _proposal_pool(): + X = np.array([[0.1], [0.3], [0.5], [0.7], [0.9]]) + return CandidatePool( + grid_indices=np.arange(5)[:, None], + X_phys=X.copy(), + X_norm=X.copy(), + seed=9, + draws=5, + rejected_duplicate=0, + rejected_avoid=0, + rejected_constraint=0, + ) + + +def _static_ucb_result(scores): + scores = np.asarray(scores, dtype=float) + return UCBHVIScoreResult( + base_score=scores, + base_log_score=np.log(np.maximum(scores, 1e-12)), + utility_mean=np.column_stack([scores, scores]), + utility_std=np.zeros((scores.size, 2)), + utility_ucb=np.column_stack([scores, scores]), + baseline_hypervolume=0.1, + pareto_utility=np.array([[0.5, 0.5]]), + reference_point_utility=np.zeros(2), + beta=1.0, + kappa=1.0, + mc_samples=8, + seed=4, + observation_noise=False, + objective_contract_version="TEST_ONLY-v1", + ) + + +def test_ucb_proposal_selects_only_positive_hvi_and_exact_q(monkeypatch): + monkeypatch.setattr( + module, + "score_ucb_hvi_pool", + lambda *args, **kwargs: _static_ucb_result([0.0, 0.8, 0.7, 0.0, 0.6]), + ) + proposal = propose_ucb_hvi_batch( + _proposal_pool(), + torch.nn.Linear(1, 1).double(), + np.ones((2, 2)), + object(), + np.zeros(2), + q=2, + beta=1.0, + local_penalization_config=LocalPenalizationConfig( + radius=0.15, min_batch_distance=0.1 + ), + positive_score_tolerance=1e-6, + ) + assert proposal.selection.selected_pool_indices.size == 2 + assert np.all( + proposal.scoring.base_score[proposal.selection.selected_pool_indices] > 0 + ) + assert proposal.metadata["pool_seed"] == 9 + assert proposal.metadata["objective_contract_version"] == "TEST_ONLY-v1" + assert proposal.metadata["beta"] == 1.0 + + +def test_ucb_proposal_refuses_to_fill_with_zero_hvi(monkeypatch): + monkeypatch.setattr( + module, + "score_ucb_hvi_pool", + lambda *args, **kwargs: _static_ucb_result([0.0, 0.8, 0.0, 0.0, 0.0]), + ) + with pytest.raises(UndersizedBatchError): + propose_ucb_hvi_batch( + _proposal_pool(), + torch.nn.Linear(1, 1).double(), + np.ones((2, 2)), + object(), + np.zeros(2), + q=2, + beta=1.0, + local_penalization_config=LocalPenalizationConfig( + radius=0.15, min_batch_distance=0 + ), + ) diff --git a/tests/test_workbook_schema.py b/tests/test_workbook_schema.py new file mode 100644 index 0000000..38111ee --- /dev/null +++ b/tests/test_workbook_schema.py @@ -0,0 +1,681 @@ +from __future__ import annotations + +import hashlib +import os +from pathlib import Path + +import pytest +from openpyxl import Workbook, load_workbook + +from mobo_kit.d2d_campaign import load_d2d_debug_config, load_d2d_workbook_frame +from mobo_kit.workbook_schema import ( + WorkbookInputExceptionRule, + audit_campaign_workbook, +) + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +PRIVATE_WORKBOOK = os.environ.get("MOBO_KIT_D2D_PRIVATE_WORKBOOK") +PRIVATE_CONFIG = os.environ.get("MOBO_KIT_D2D_PRIVATE_CONFIG") +SYNTHETIC_SAMPLE_IDS = tuple(range(1001, 1016)) +V3_HEADERS = ( + "Sample number", + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol (uL)", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", + "Coverage", + "Uniformity", + "1 - Uniformity", + "Phase purity", + "PL - Implied Voc (Max)", + "Photoconductance (Max)", + "Log10 (Photoconductance (Max) x PL - Implied Voc (Max))", + "T1", + "T2", + "T3", + "T4", + "T anom", + "Thickness (avg)", + "Normalized thickness (sigma = 250)", + "Uniformity score", + "Optoelectronic score", + "Thickness score", + "Stability score?", + "Total combination - addition", + "Total combination - multiplied", + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", +) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as workbook_file: + for chunk in iter(lambda: workbook_file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _create_sanitized_summary_workbook(path: Path) -> None: + workbook = Workbook() + worksheet = workbook.active + worksheet.title = "Sheet1" + + headers: list[str | None] = [ + "Sample number", + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", + "Anneal Temp", + "Raw measurement A", + "Raw measurement B", + "Raw measurement C", + "Derived score A", + "Uniformity score", + "Derived score B", + "Derived score C", + "Uniformity score", + "Thickness score", + "Optoelectronic score", + "Total combination - multiplication", + "Total combination - addition", + None, + "BO objective 1", + "BO objective 2", + "BO objective 3", + "Notes", + ] + assert len(headers) == 29 + + for column, header in enumerate(headers, start=1): + worksheet.cell(row=1, column=column, value=header) + + for row, sample_id in enumerate(SYNTHETIC_SAMPLE_IDS, start=2): + worksheet.cell(row=row, column=1, value=sample_id) + + # Ensure worksheet.max_row/max_column are misleading. The auditor must + # ignore this style-only cell and report the range of nonblank values. + worksheet["AZ100"].number_format = "0.00" + workbook.save(path) + workbook.close() + + +def _create_sanitized_v2_workbook( + path: Path, *, note_cells: tuple[str, str] = ("P18", "R18") +) -> None: + workbook = Workbook() + worksheet = workbook.active + worksheet.title = "Sheet1" + headers: list[str | None] = [ + "Sample number", + "speed_1", + "time_1", + "speed_2", + "time_2", + "precur_conc", + "precur_vol (uL)", + "anneal_temp", + "anneal_time", + "anti_vol", + "anti_time", + "Coverage", + "Uniformity", + "1 - Uniformity", + "Phase purity", + "PL - Implied Voc (Max)", + "PL - Implied Voc (Max) Normalized", + "Photoconductance (Max)", + "Photoconductance (Max) Normalized", + "Uniformity score", + "Optoelectronic score", + "Thickness (avg)", + "Normalized thickness (sigma = 250)", + "Total combination - addition", + None, + "Uniformity score absolute difference", + "Optoelectronic score absolute difference", + "Thickness absolute difference", + "Total score absolute difference", + ] + assert len(headers) == 29 + for column, header in enumerate(headers, start=1): + worksheet.cell(row=1, column=column, value=header) + + grids = [ + list(range(1000, 6001, 500)), + list(range(5, 51, 5)), + list(range(0, 5001, 500)), + list(range(10, 61, 5)), + [round(1.0 + index * 0.05, 2) for index in range(21)], + list(range(40, 201, 10)), + list(range(100, 186, 5)), + list(range(10, 61, 5)), + list(range(100, 201, 5)), + list(range(9, 26, 2)), + ] + for sample_index in range(15): + row = sample_index + 2 + worksheet.cell(row=row, column=1, value=SYNTHETIC_SAMPLE_IDS[sample_index]) + for dimension, grid in enumerate(grids): + value = grid[(sample_index * (dimension + 1) + dimension) % len(grid)] + worksheet.cell(row=row, column=dimension + 2, value=value) + note = "*Likely normalize to a max theoretical, to be discussed" + for cell in note_cells: + worksheet[cell] = note + worksheet["AS47"].number_format = "0.00" + workbook.save(path) + workbook.close() + + +def _create_sanitized_v3_workbook(path: Path) -> None: + workbook = Workbook() + worksheet = workbook.active + worksheet.title = "Sheet1" + assert len(V3_HEADERS) == 35 + for column, header in enumerate(V3_HEADERS, start=1): + worksheet.cell(row=1, column=column, value=header) + + grids = [ + list(range(1000, 6001, 500)), + list(range(5, 51, 5)), + list(range(0, 5001, 500)), + list(range(10, 61, 5)), + [round(1.0 + index * 0.05, 2) for index in range(21)], + list(range(40, 201, 10)), + list(range(100, 186, 5)), + list(range(10, 61, 5)), + list(range(100, 201, 5)), + list(range(9, 26, 2)), + ] + for sample_index in range(15): + row = sample_index + 2 + worksheet.cell(row=row, column=1, value=SYNTHETIC_SAMPLE_IDS[sample_index]) + for dimension, grid in enumerate(grids): + value = grid[(sample_index * (dimension + 1) + dimension) % len(grid)] + worksheet.cell(row=row, column=dimension + 2, value=value) + worksheet.cell(row=row, column=12, value=0.9) + worksheet.cell(row=row, column=13, value=0.2) + worksheet.cell(row=row, column=14, value=0.8) + worksheet.cell(row=row, column=15, value=0.95) + worksheet.cell(row=row, column=16, value=1.1 + sample_index * 0.01) + worksheet.cell(row=row, column=17, value=100 + sample_index) + worksheet.cell(row=row, column=18, value=f"=LOG10(P{row}*Q{row})") + for column, thickness in zip(range(19, 23), (620, 640, 660, 680)): + worksheet.cell(row=row, column=column, value=thickness + sample_index) + worksheet.cell(row=row, column=23, value=999) + worksheet.cell(row=row, column=24, value=f"=AVERAGE(S{row}:V{row})") + worksheet.cell( + row=row, + column=25, + value=f"=EXP(-((AVERAGE(S{row}:V{row})-650)/250)^2)", + ) + worksheet.cell(row=row, column=26, value=0.65 + sample_index * 0.01) + worksheet.cell(row=row, column=27, value=2.0 + sample_index * 0.01) + worksheet.cell(row=row, column=28, value=0.9 - sample_index * 0.01) + + for row in range(17, 21): + worksheet.cell(row=row, column=1, value=f"Synthetic note row {row}") + worksheet["AI20"] = "Synthetic ignored-summary note" + # Whitespace-only values, including NBSP, must not extend the content range + # or be reported as notes. + worksheet["AJ21"] = " \u00a0\t " + worksheet["S17"] = "\u00a0" + worksheet["AZ100"].number_format = "0.00" + workbook.save(path) + workbook.close() + + +def test_audit_campaign_workbook_preserves_raw_schema_and_file(tmp_path: Path) -> None: + workbook_path = tmp_path / "sanitized_summary_table.xlsx" + _create_sanitized_summary_workbook(workbook_path) + + before_hash = _sha256(workbook_path) + before_mtime_ns = workbook_path.stat().st_mtime_ns + + audit = audit_campaign_workbook(workbook_path) + + assert audit.workbook_path == workbook_path + assert audit.sheet_names == ["Sheet1"] + assert audit.active_sheet == "Sheet1" + assert audit.used_range == "A1:AC16" + assert audit.column_count == 29 + assert len(audit.raw_headers) == 29 + assert audit.sample_row_count == 15 + assert audit.profile == "d2d_summary_step1_historical" + + assert audit.raw_headers[16] == "Uniformity score" # Q + assert audit.raw_headers[19] == "Uniformity score" # T + assert audit.duplicate_headers == {"Uniformity score": [17, 20]} + assert audit.raw_headers[24] is None # Y + assert audit.blank_header_columns == [25] + + duplicate_warning = next( + warning for warning in audit.warnings if "Duplicate header" in warning + ) + assert "Uniformity score" in duplicate_warning + assert "[17, 20]" in duplicate_warning + + anneal_warning = next( + warning + for warning in audit.warnings + if "anneal_temp" in warning and "Anneal Temp" in warning + ) + assert "normalize" in anneal_warning + + blank_warning = next( + warning for warning in audit.warnings if "Blank header" in warning + ) + assert "column 25 (Y)" in blank_warning + assert "Total combination - addition" in blank_warning + + assert _sha256(workbook_path) == before_hash + assert workbook_path.stat().st_mtime_ns == before_mtime_ns + + +def test_updated_v2_profile_aliases_grid_rows_notes_and_file_integrity( + tmp_path: Path, +) -> None: + workbook_path = tmp_path / "sanitized_summary_v2.xlsx" + _create_sanitized_v2_workbook(workbook_path) + before_hash = _sha256(workbook_path) + before_mtime_ns = workbook_path.stat().st_mtime_ns + + audit = audit_campaign_workbook(workbook_path) + + assert audit.profile == "d2d_summary_v2" + assert audit.sheet_names == ["Sheet1"] + assert audit.active_sheet == "Sheet1" + assert audit.used_range == "A1:AC18" + assert audit.column_count == 29 + assert audit.sample_row_count == 15 + assert audit.sample_rows == list(range(2, 17)) + assert audit.raw_headers[6] == "precur_vol (uL)" + assert audit.canonical_input_mapping["precur_vol (uL)"] == "precur_vol" + assert audit.canonical_input_positions == { + "speed_1": 2, + "time_1": 3, + "speed_2": 4, + "time_2": 5, + "precur_conc": 6, + "precur_vol": 7, + "anneal_temp": 8, + "anneal_time": 9, + "anti_vol": 10, + "anti_time": 11, + } + assert "Anneal Temp" not in audit.raw_headers + assert audit.header_positions["Uniformity score"] == [20] + assert audit.header_positions["Optoelectronic score"] == [21] + assert audit.header_positions["Thickness (avg)"] == [22] + assert audit.header_positions["Normalized thickness (sigma = 250)"] == [23] + assert audit.blank_header_columns == [25] + assert [note.cell for note in audit.notes] == ["P18", "R18"] + assert audit.input_rows_valid is True + assert audit.input_validation_errors == [] + assert audit.objective_mapping_approved is False + assert audit.formula_cell_count == 0 + assert not any("P18/R18" in warning for warning in audit.warnings) + assert _sha256(workbook_path) == before_hash + assert workbook_path.stat().st_mtime_ns == before_mtime_ns + + +def test_v3_profile_exact_contract_and_file_integrity( + tmp_path: Path, +) -> None: + workbook_path = tmp_path / "sanitized_summary_v3.xlsx" + _create_sanitized_v3_workbook(workbook_path) + before_hash = _sha256(workbook_path) + before_mtime_ns = workbook_path.stat().st_mtime_ns + + audit = audit_campaign_workbook(workbook_path) + + assert audit.profile == "d2d_summary_v3_scores" + assert audit.sheet_names == ["Sheet1"] + assert audit.active_sheet == "Sheet1" + assert audit.used_range == "A1:AI20" + assert audit.column_count == 35 + assert tuple(audit.raw_headers) == V3_HEADERS + assert audit.sample_row_count == 15 + assert audit.sample_rows == list(range(2, 17)) + assert {note.row for note in audit.notes} == {17, 18, 19, 20} + assert all(note.cell != "S17" for note in audit.notes) + + assert audit.canonical_input_mapping["precur_vol (uL)"] == "precur_vol" + assert audit.canonical_input_positions == { + "speed_1": 2, + "time_1": 3, + "speed_2": 4, + "time_2": 5, + "precur_conc": 6, + "precur_vol": 7, + "anneal_temp": 8, + "anneal_time": 9, + "anti_vol": 10, + "anti_time": 11, + } + assert audit.header_positions["Uniformity score"] == [26] + assert audit.header_positions["Optoelectronic score"] == [27] + assert audit.header_positions["Thickness score"] == [28] + assert audit.canonical_objective_mapping == { + "Uniformity score": "uniformity_score", + "Optoelectronic score": "optoelectronic_score", + "Thickness score": "thickness_score", + } + assert audit.canonical_objective_positions == { + "uniformity_score": 26, + "optoelectronic_score": 27, + "thickness_score": 28, + } + assert audit.ignored_model_positions == { + "Stability score?": 29, + "Total combination - addition": 30, + "Total combination - multiplied": 31, + "Uniformity score absolute difference": 32, + "Optoelectronic score absolute difference": 33, + "Thickness absolute difference": 34, + "Total score absolute difference": 35, + } + assert audit.objective_mapping_resolved_for_debug is True + assert audit.objective_mapping_approved is False + assert audit.blank_header_columns == [] + assert audit.formula_cell_count == 45 + + assert audit.input_rows_valid is True + assert audit.input_validation_errors == [] + assert audit.input_exceptions == [] + assert any( + "resolved for debug only" in warning + and "not approved for production" in warning + for warning in audit.warnings + ) + + assert _sha256(workbook_path) == before_hash + assert workbook_path.stat().st_mtime_ns == before_mtime_ns + + +def test_v3_profile_accepts_explicit_algorithmic_synthetic_exception( + tmp_path: Path, +) -> None: + workbook_path = tmp_path / "synthetic_exception_v3.xlsx" + _create_sanitized_v3_workbook(workbook_path) + grid = list(range(1000, 6001, 500)) + sentinel = (grid[0] + grid[1]) / 2.0 + workbook = load_workbook(workbook_path) + workbook["Sheet1"]["B2"] = sentinel + workbook.save(workbook_path) + workbook.close() + + rule = WorkbookInputExceptionRule( + sample_id=SYNTHETIC_SAMPLE_IDS[0], + input_name="speed_1", + observed_value=sentinel, + reason="algorithmic synthetic off-grid sentinel", + ) + audit = audit_campaign_workbook(workbook_path, allowed_input_exceptions=(rule,)) + + assert audit.input_rows_valid is True + assert len(audit.input_exceptions) == 1 + assert audit.input_exceptions[0].sample_id == SYNTHETIC_SAMPLE_IDS[0] + assert audit.input_exceptions[0].observed_value == sentinel + assert any("Observed-only input exception" in warning for warning in audit.warnings) + + +def test_v3_profile_rejects_nonexception_grid_and_bounds_errors( + tmp_path: Path, +) -> None: + workbook_path = tmp_path / "invalid_summary_v3.xlsx" + _create_sanitized_v3_workbook(workbook_path) + workbook = load_workbook(workbook_path) + worksheet = workbook["Sheet1"] + speed_grid = list(range(1000, 6001, 500)) + worksheet["B3"] = (speed_grid[0] + speed_grid[1]) / 2.0 + worksheet["B4"] = 999 + worksheet["C5"] = "NaN" + workbook.save(workbook_path) + workbook.close() + + audit = audit_campaign_workbook(workbook_path) + + assert audit.profile == "d2d_summary_v3_scores" + assert audit.input_rows_valid is False + assert any( + "Excel row 3" in error and "off-grid" in error + for error in audit.input_validation_errors + ) + assert any( + "Excel row 4" in error and "out-of-bounds" in error + for error in audit.input_validation_errors + ) + assert any( + "Row 5" in error and "non-finite" in error + for error in audit.input_validation_errors + ) + assert audit.input_exceptions == [] + + +def test_v3_profile_requires_exact_content_range(tmp_path: Path) -> None: + workbook_path = tmp_path / "short_summary_v3.xlsx" + _create_sanitized_v3_workbook(workbook_path) + workbook = load_workbook(workbook_path) + worksheet = workbook["Sheet1"] + worksheet["A20"] = None + worksheet["AI20"] = None + workbook.save(workbook_path) + workbook.close() + + audit = audit_campaign_workbook(workbook_path) + + assert audit.profile == "d2d_summary_v3_scores" + assert audit.used_range == "A1:AI19" + assert audit.input_rows_valid is False + assert any( + "requires content range A1:AI20" in error + for error in audit.input_validation_errors + ) + + +def test_v3_profile_rejects_missing_shifted_duplicate_and_ambiguous_headers( + tmp_path: Path, +) -> None: + def audit_with_headers(name: str, updates: dict[str, str | None]): + workbook_path = tmp_path / f"{name}.xlsx" + _create_sanitized_v3_workbook(workbook_path) + workbook = load_workbook(workbook_path) + worksheet = workbook["Sheet1"] + for cell, value in updates.items(): + worksheet[cell] = value + workbook.save(workbook_path) + workbook.close() + return audit_campaign_workbook(workbook_path) + + missing = audit_with_headers("missing", {"Z1": None}) + assert missing.profile == "unknown" + assert missing.blank_header_columns == [26] + + shifted = audit_with_headers( + "shifted", + {"AA1": "Thickness score", "AB1": "Optoelectronic score"}, + ) + assert shifted.profile == "unknown" + assert shifted.header_positions["Optoelectronic score"] == [28] + assert shifted.header_positions["Thickness score"] == [27] + + duplicate = audit_with_headers("duplicate", {"AA1": "Uniformity score"}) + assert duplicate.profile == "unknown" + assert duplicate.duplicate_headers == {"Uniformity score": [26, 27]} + assert any("Duplicate header" in warning for warning in duplicate.warnings) + + ambiguous = audit_with_headers("ambiguous", {"AC1": "Thickness-score"}) + assert ambiguous.profile == "unknown" + assert any( + "Ambiguous related headers" in warning + and "Thickness score" in warning + and "Thickness-score" in warning + for warning in ambiguous.warnings + ) + + +def test_v2_profile_reports_off_grid_and_duplicate_recipe_errors(tmp_path: Path): + workbook_path = tmp_path / "invalid_summary_v2.xlsx" + _create_sanitized_v2_workbook(workbook_path) + workbook = load_workbook(workbook_path) + worksheet = workbook["Sheet1"] + worksheet["B2"] = 1001 + for column in range(2, 12): + worksheet.cell( + row=3, column=column, value=worksheet.cell(row=4, column=column).value + ) + workbook.save(workbook_path) + workbook.close() + audit = audit_campaign_workbook(workbook_path) + assert audit.profile == "d2d_summary_v2" + assert audit.input_rows_valid is False + assert any("off-grid" in error for error in audit.input_validation_errors) + assert any( + "duplicate input grid tuples" in error + for error in audit.input_validation_errors + ) + + +def test_v2_note_location_anomaly_is_reported_without_changing_profile( + tmp_path: Path, +) -> None: + workbook_path = tmp_path / "sanitized_summary_v2_note_anomaly.xlsx" + _create_sanitized_v2_workbook(workbook_path, note_cells=("Q18", "S18")) + + audit = audit_campaign_workbook(workbook_path) + + assert audit.profile == "d2d_summary_v2" + assert [note.cell for note in audit.notes] == ["Q18", "S18"] + assert any("P18/R18" in warning for warning in audit.warnings) + + +def test_v2_profile_requires_exact_headers_sheet_rows_and_sample_ids( + tmp_path: Path, +) -> None: + wrong_header_path = tmp_path / "wrong_header.xlsx" + _create_sanitized_v2_workbook(wrong_header_path) + workbook = load_workbook(wrong_header_path) + workbook["Sheet1"]["L1"] = "Coverage renamed" + workbook.save(wrong_header_path) + workbook.close() + assert audit_campaign_workbook(wrong_header_path).profile == "unknown" + + wrong_sheet_path = tmp_path / "wrong_sheet.xlsx" + _create_sanitized_v2_workbook(wrong_sheet_path) + workbook = load_workbook(wrong_sheet_path) + workbook["Sheet1"].title = "Not Sheet1" + workbook.save(wrong_sheet_path) + workbook.close() + assert audit_campaign_workbook(wrong_sheet_path).profile == "unknown" + + incomplete_path = tmp_path / "incomplete_rows.xlsx" + _create_sanitized_v2_workbook(incomplete_path) + workbook = load_workbook(incomplete_path) + worksheet = workbook["Sheet1"] + worksheet["A16"] = None + worksheet["Z17"] = "not blank" + workbook.save(incomplete_path) + workbook.close() + incomplete = audit_campaign_workbook(incomplete_path) + assert incomplete.profile == "d2d_summary_v2" + assert incomplete.input_rows_valid is False + assert any("rows exactly" in error for error in incomplete.input_validation_errors) + assert any( + "unique numeric sample identifiers" in error + for error in incomplete.input_validation_errors + ) + assert any("row 17" in error for error in incomplete.input_validation_errors) + + +def test_sample_count_uses_only_nonblank_sample_identifiers(tmp_path: Path) -> None: + workbook_path = tmp_path / "sample_ids.xlsx" + workbook = Workbook() + worksheet = workbook.active + worksheet["A1"] = "Sample ID" + worksheet["B1"] = "Measurement" + worksheet["A2"] = "S-001" + worksheet["B3"] = "row without a sample identifier" + worksheet["A4"] = "S-002" + workbook.save(workbook_path) + workbook.close() + + audit = audit_campaign_workbook(workbook_path) + + assert audit.used_range == "A1:B4" + assert audit.sample_row_count == 2 + + +@pytest.mark.local_input +@pytest.mark.skipif( + not PRIVATE_WORKBOOK or not PRIVATE_CONFIG, + reason=( + "set MOBO_KIT_D2D_PRIVATE_WORKBOOK and MOBO_KIT_D2D_PRIVATE_CONFIG " + "to opt into the ignored private integration test" + ), +) +def test_private_workbook_schema_if_explicitly_enabled() -> None: + private_workbook = Path(str(PRIVATE_WORKBOOK)).expanduser().resolve() + private_config = load_d2d_debug_config( + Path(str(PRIVATE_CONFIG)).expanduser().resolve() + ) + before_hash = _sha256(private_workbook) + before_mtime_ns = private_workbook.stat().st_mtime_ns + _, audit = load_d2d_workbook_frame( + private_workbook, + expected_profile=private_config.workbook_profile, + expected_sample_ids=private_config.expected_sample_ids, + allowed_input_exceptions=private_config.off_grid_exceptions, + ) + + assert audit.sheet_names == ["Sheet1"] + assert audit.active_sheet == "Sheet1" + assert audit.profile == "d2d_summary_v3_scores" + assert audit.used_range == "A1:AI20" + assert audit.column_count == 35 + assert audit.sample_row_count == 15 + assert audit.sample_rows == list(range(2, 17)) + assert tuple(audit.raw_headers) == V3_HEADERS + assert audit.header_positions.get("Uniformity score") == [26] + assert audit.header_positions.get("Optoelectronic score") == [27] + assert audit.header_positions.get("Thickness score") == [28] + assert audit.canonical_input_mapping.get("precur_vol (uL)") == "precur_vol" + assert audit.canonical_objective_positions == { + "uniformity_score": 26, + "optoelectronic_score": 27, + "thickness_score": 28, + } + assert audit.ignored_model_positions == { + "Stability score?": 29, + "Total combination - addition": 30, + "Total combination - multiplied": 31, + "Uniformity score absolute difference": 32, + "Optoelectronic score absolute difference": 33, + "Thickness absolute difference": 34, + "Total score absolute difference": 35, + } + assert audit.blank_header_columns == [] + assert {note.row for note in audit.notes} == {17, 18, 19, 20} + assert audit.input_rows_valid is True + assert audit.input_validation_errors == [] + assert len(audit.input_exceptions) == len(private_config.off_grid_exceptions) + assert audit.objective_mapping_resolved_for_debug is True + assert audit.objective_mapping_approved is False + assert _sha256(private_workbook) == before_hash + assert private_workbook.stat().st_mtime_ns == before_mtime_ns diff --git a/unsorted/Code/20211101_PerovScaleup_Round3_Third_BatchBO_TwoConstraints_20201019_TH.ipynb b/unsorted/Code/20211101_PerovScaleup_Round3_Third_BatchBO_TwoConstraints_20201019_TH.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305141905.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305141905.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305142405.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305142405.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305143906.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305143906.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305145906.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305145906.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305150406.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305150406.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305151907.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305151907.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305152407.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305152407.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305153907.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305153907.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305154908.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305154908.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305155408.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305155408.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305160908.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305160908.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305161408.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305161408.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305162909.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305162909.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305163409.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305163409.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305164909.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305164909.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305165910.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305165910.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305170410.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305170410.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305173911.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305173911.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305174411.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305174411.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305175911.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305175911.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305180912.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305180912.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305183412.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305183412.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305184913.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305184913.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305185413.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305185413.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305191413.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305191413.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305193414.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305193414.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305194914.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305194914.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305195414.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305195414.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305200415.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305200415.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305203916.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305203916.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305204416.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305204416.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305205916.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305205916.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305211917.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305211917.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305212417.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305212417.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305214417.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305214417.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305221418.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305221418.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305222918.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305222918.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305224919.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305224919.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305225419.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305225419.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305230919.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305230919.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305232920.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305232920.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305234921.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305234921.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305235421.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210305235421.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306000921.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306000921.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306003422.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306003422.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306004922.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306004922.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306005422.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306005422.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306011423.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306011423.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306012923.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306012923.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306013424.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306013424.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306014924.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306014924.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306015424.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306015424.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306020924.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306020924.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306021425.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306021425.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306022925.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306022925.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306023425.bmp b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/BMP/20210306023425.bmp deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Color_operations.py b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Color_operations.py deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Crop_my_video.py b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Crop_my_video.py deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor.py b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor.py deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor_Xrite_CC.py b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor_Xrite_CC.py deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor_savefigs.py b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/RGB_extractor_savefigs.py deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Test_crop_box.py b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/Test_crop_box.py deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/input_extractor_2.ipynb b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/input_extractor_2.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/start and end values.txt b/unsorted/Code/DegradationChamber/DegradationAnalysis/Example/start and end values.txt deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/Emukit BO Example/Emukit_test_builtin_unknown_constraint_function.ipynb b/unsorted/Code/Emukit BO Example/Emukit_test_builtin_unknown_constraint_function.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/Emukit BO Example/Emukit_test_modified_unknown_constraint_function.ipynb b/unsorted/Code/Emukit BO Example/Emukit_test_modified_unknown_constraint_function.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/ML Perov Data/Data Summary for New Plasma Cleaned.csv b/unsorted/Code/ML Perov Data/Data Summary for New Plasma Cleaned.csv deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/ML Perov Data/ML Perov Data Stanford.xlsx b/unsorted/Code/ML Perov Data/ML Perov Data Stanford.xlsx deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/ML Perov Data/new_plamsa_previous_selected_20200927.xlsx b/unsorted/Code/ML Perov Data/new_plamsa_previous_selected_20200927.xlsx deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Check_All_Experimental_Data.ipynb b/unsorted/Code/PerovScaleup_Check_All_Experimental_Data.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb b/unsorted/Code/PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round1_First_BatchBO_TwoConstraints_20200927.ipynb b/unsorted/Code/PerovScaleup_Round1_First_BatchBO_TwoConstraints_20200927.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round2_Check_Experimental_Data_20200930.ipynb b/unsorted/Code/PerovScaleup_Round2_Check_Experimental_Data_20200930.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round2_Second_BatchBO_TwoConstraints_20200930.ipynb b/unsorted/Code/PerovScaleup_Round2_Second_BatchBO_TwoConstraints_20200930.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round3_Check_Experimental_Data_20201019.ipynb b/unsorted/Code/PerovScaleup_Round3_Check_Experimental_Data_20201019.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round3_Third_BatchBO_TwoConstraints_20201019.ipynb b/unsorted/Code/PerovScaleup_Round3_Third_BatchBO_TwoConstraints_20201019.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round4_Fourth_BatchBO_TwoConstraints_20201026.ipynb b/unsorted/Code/PerovScaleup_Round4_Fourth_BatchBO_TwoConstraints_20201026.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round5_Fifth_BatchBO_TwoConstraints_20201108.ipynb b/unsorted/Code/PerovScaleup_Round5_Fifth_BatchBO_TwoConstraints_20201108.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/PerovScaleup_Round6_Sixth_BatchBO_Entropy_Search_TwoConstraints_20201220.ipynb b/unsorted/Code/PerovScaleup_Round6_Sixth_BatchBO_Entropy_Search_TwoConstraints_20201220.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/Process Optimization for Perovskite Solar Cells v1.ipynb b/unsorted/Code/Process Optimization for Perovskite Solar Cells v1.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/Process Optimization for Perovskite Solar Cells v2.ipynb b/unsorted/Code/Process Optimization for Perovskite Solar Cells v2.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/ReadMe.txt b/unsorted/Code/ReadMe.txt deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Code/Salinan PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb b/unsorted/Code/Salinan PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb deleted file mode 100644 index 9b421da..0000000 --- a/unsorted/Code/Salinan PerovScaleup_Round1_Check_Experimental_Data_20200922.ipynb +++ /dev/null @@ -1,1577 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Check the pervoskite experimental data produced on Sep 22, 2020\n", - "\n", - "- Experiments are prepared by Nick Rolston and Thomas Colburn (Stanfrod University) \n", - "- Jupyter Notebook is prepared by Zhe Liu (Massachusetts Insititute of Technology)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "import emukit\n", - "import GPy\n", - "import sklearn\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Index(['ML Condition', 'Temp [degC]', 'speed [mm/s]', 'sprayFL [uL/min]',\n", - " 'plamsaH [cm]', 'gasFL [L/min]', 'plasmaDC [%]', ' Success or Fail',\n", - " 'Unnamed: 8'],\n", - " dtype='object')\n" - ] - }, - { - "data": { - "text/html": [ - "

\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ML ConditionTemp [degC]speed [mm/s]sprayFL [uL/min]plamsaH [cm]gasFL [L/min]plasmaDC [%]Success or Fail
0015520050001.235501
1116022530000.830750
2213520025001.025751
3315017520001.0201000
4417027545001.216500
5517525035001.225750
6614015040001.020751
7715522540000.830250
8813017535000.825751
9913512525001.220251
101013025045001.230501
111114512535001.025501
121214515045001.0161001
131316027535000.8201000
141416522540001.025251
151512527530000.820501
161617017550001.016750
171715010025001.035501
181814030020001.030500
191916512530001.030750
\n", - "
" - ], - "text/plain": [ - " ML Condition Temp [degC] speed [mm/s] sprayFL [uL/min] plamsaH [cm] \\\n", - "0 0 155 200 5000 1.2 \n", - "1 1 160 225 3000 0.8 \n", - "2 2 135 200 2500 1.0 \n", - "3 3 150 175 2000 1.0 \n", - "4 4 170 275 4500 1.2 \n", - "5 5 175 250 3500 1.2 \n", - "6 6 140 150 4000 1.0 \n", - "7 7 155 225 4000 0.8 \n", - "8 8 130 175 3500 0.8 \n", - "9 9 135 125 2500 1.2 \n", - "10 10 130 250 4500 1.2 \n", - "11 11 145 125 3500 1.0 \n", - "12 12 145 150 4500 1.0 \n", - "13 13 160 275 3500 0.8 \n", - "14 14 165 225 4000 1.0 \n", - "15 15 125 275 3000 0.8 \n", - "16 16 170 175 5000 1.0 \n", - "17 17 150 100 2500 1.0 \n", - "18 18 140 300 2000 1.0 \n", - "19 19 165 125 3000 1.0 \n", - "\n", - " gasFL [L/min] plasmaDC [%] Success or Fail \n", - "0 35 50 1 \n", - "1 30 75 0 \n", - "2 25 75 1 \n", - "3 20 100 0 \n", - "4 16 50 0 \n", - "5 25 75 0 \n", - "6 20 75 1 \n", - "7 30 25 0 \n", - "8 25 75 1 \n", - "9 20 25 1 \n", - "10 30 50 1 \n", - "11 25 50 1 \n", - "12 16 100 1 \n", - "13 20 100 0 \n", - "14 25 25 1 \n", - "15 20 50 1 \n", - "16 16 75 0 \n", - "17 35 50 1 \n", - "18 30 50 0 \n", - "19 30 75 0 " - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df_film = pd.read_excel('./ML Perov Data/ML perov data stanford.xlsx', sheet_name='2020_09_22_film')\n", - "print(df_film.columns)\n", - "df_film.iloc[:,:-1]" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Index(['ML Condition', 'Sample', 'Temp [degC]', 'speed [mm/s]',\n", - " 'sprayFL [uL/min]', 'plamsaH [cm]', 'gasFL [L/min]', 'plasmaDC [%]',\n", - " 'Isc', 'Jsc', 'Voc', 'FF', 'PCE'],\n", - " dtype='object')\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
ML ConditionSampleTemp [degC]speed [mm/s]sprayFL [uL/min]plamsaH [cm]gasFL [L/min]plasmaDC [%]Isc [mA]Jsc [mA/cm2]Voc [V]FF [-]Efficiency [%]
06B9R_14-FR0.dat14015040001.020753.99619.02801.0460000.5911.743293
16B9L_4-FR0.dat14015040001.020753.55316.91901.0440000.5810.244822
26B10L_7-FR0.dat14015040001.020753.82018.19001.0400000.6712.675124
36B10R_AFTERUP2-FR0.dat14015040001.020754.18119.90901.0230000.6914.053536
415B11L_YELLOWISH2-FR0.dat12527530000.820504.77322.72850.9813030.6113.605205
515B11R_YELLOWISH2-FR0.dat12527530000.820504.84423.06600.9213320.5812.326194
615B12L-FR0.dat12527530000.820504.45821.22800.8325190.5910.427182
715B12RPALE_2-FR0.dat12527530000.820504.50421.44760.8188770.6210.889036
810B13R_5-FR0.dat13025045001.230504.64422.11420.9457750.5912.339932
910B14L_YELLOWISH4-FR0.dat13025045001.230504.26720.31900.9666750.6212.177988
1010B14R_YELLOWISH10-FR0.dat13025045001.230504.55721.70000.8510560.6411.819466
118B15L-FR0.dat13017535000.825754.65122.14700.9025300.5510.993890
128B15R_AFTERUP-FR0.dat13017535000.825754.59521.88001.0440000.7416.904349
138B16R_AFTERUP-FR0.dat13017535000.825754.89723.31900.9959980.6414.864464
148B16L_5-FR0.dat13017535000.825754.60121.90901.0110000.6213.733328
152B17LPALE_4-FR0.dat13520025001.025754.72822.51420.9451260.6012.767302
162B18RPALE_4-FR0.dat13520025001.025754.86723.17610.9269200.6012.889485
172B18LPALE_2-FR0.dat13520025001.025754.50821.46600.8955740.6211.919493
189B20RGAPPY_AFTER2UP2-FR0.dat13512525001.220254.63022.04761.0250000.7116.045155
199B20LGAPPY_6-FR0.dat13512525001.220254.57521.78501.0100000.6714.742393
209B21LGAPPY_AFTERUP-FR0.dat13512525001.220254.65922.18570.9708170.6413.784492
219B21RGAPPY_AFTERUP3-FR0.dat13512525001.220254.72322.49000.9912910.6915.383279
2211B22L_4-FR0.dat14512535001.025504.98823.75231.0090000.5713.660707
2311B22R_AGAIN_5MINSOAKAFTERUP3-FR0.dat14512535001.025505.01023.85711.0470000.6816.985331
2411B23L_AFTERUP-FR0.dat14512535001.025504.43821.13331.0450000.7616.784093
2511B23R_AFTER2UP2-FR0.dat14512535001.025504.68222.29521.0310000.7717.699521
2612B24L_3-FR0.dat14515045001.0161004.16119.81420.9930360.499.641387
2712B24R_8-FR0.dat14515045001.0161004.78222.77140.9710780.5411.940930
2812B25L-FR0.dat14515045001.0161003.61717.22380.9675340.518.498957
2912B25R_7-FR0.dat14515045001.0161004.44021.14280.9967750.5912.434056
3017B28R-FR0.dat15010025001.035501.6597.90001.0050000.342.699430
3117B28L_2-FR0.dat15010025001.035501.3296.32850.9382140.342.018768
320B29L-FR0.dat15520050001.235502.35011.19040.9690770.363.903996
330B29R_3-FR0.dat15520050001.235503.41516.26190.9325140.395.914137
347B30L-FR0.dat15522540000.830253.83918.28000.8988060.538.708446
357B30R-FR0.dat15522540000.830253.81718.17611.0070000.549.883849
3614B36LPALE_3-FR0.dat16522540001.025254.59921.90000.9744280.6413.657583
3714B36RPALE_8-FR0.dat16522540001.025254.74922.61420.9688570.6213.584206
\n", - "
" - ], - "text/plain": [ - " ML Condition Sample Temp [degC] \\\n", - "0 6 B9R_14-FR0.dat 140 \n", - "1 6 B9L_4-FR0.dat 140 \n", - "2 6 B10L_7-FR0.dat 140 \n", - "3 6 B10R_AFTERUP2-FR0.dat 140 \n", - "4 15 B11L_YELLOWISH2-FR0.dat 125 \n", - "5 15 B11R_YELLOWISH2-FR0.dat 125 \n", - "6 15 B12L-FR0.dat 125 \n", - "7 15 B12RPALE_2-FR0.dat 125 \n", - "8 10 B13R_5-FR0.dat 130 \n", - "9 10 B14L_YELLOWISH4-FR0.dat 130 \n", - "10 10 B14R_YELLOWISH10-FR0.dat 130 \n", - "11 8 B15L-FR0.dat 130 \n", - "12 8 B15R_AFTERUP-FR0.dat 130 \n", - "13 8 B16R_AFTERUP-FR0.dat 130 \n", - "14 8 B16L_5-FR0.dat 130 \n", - "15 2 B17LPALE_4-FR0.dat 135 \n", - "16 2 B18RPALE_4-FR0.dat 135 \n", - "17 2 B18LPALE_2-FR0.dat 135 \n", - "18 9 B20RGAPPY_AFTER2UP2-FR0.dat 135 \n", - "19 9 B20LGAPPY_6-FR0.dat 135 \n", - "20 9 B21LGAPPY_AFTERUP-FR0.dat 135 \n", - "21 9 B21RGAPPY_AFTERUP3-FR0.dat 135 \n", - "22 11 B22L_4-FR0.dat 145 \n", - "23 11 B22R_AGAIN_5MINSOAKAFTERUP3-FR0.dat 145 \n", - "24 11 B23L_AFTERUP-FR0.dat 145 \n", - "25 11 B23R_AFTER2UP2-FR0.dat 145 \n", - "26 12 B24L_3-FR0.dat 145 \n", - "27 12 B24R_8-FR0.dat 145 \n", - "28 12 B25L-FR0.dat 145 \n", - "29 12 B25R_7-FR0.dat 145 \n", - "30 17 B28R-FR0.dat 150 \n", - "31 17 B28L_2-FR0.dat 150 \n", - "32 0 B29L-FR0.dat 155 \n", - "33 0 B29R_3-FR0.dat 155 \n", - "34 7 B30L-FR0.dat 155 \n", - "35 7 B30R-FR0.dat 155 \n", - "36 14 B36LPALE_3-FR0.dat 165 \n", - "37 14 B36RPALE_8-FR0.dat 165 \n", - "\n", - " speed [mm/s] sprayFL [uL/min] plamsaH [cm] gasFL [L/min] plasmaDC [%] \\\n", - "0 150 4000 1.0 20 75 \n", - "1 150 4000 1.0 20 75 \n", - "2 150 4000 1.0 20 75 \n", - "3 150 4000 1.0 20 75 \n", - "4 275 3000 0.8 20 50 \n", - "5 275 3000 0.8 20 50 \n", - "6 275 3000 0.8 20 50 \n", - "7 275 3000 0.8 20 50 \n", - "8 250 4500 1.2 30 50 \n", - "9 250 4500 1.2 30 50 \n", - "10 250 4500 1.2 30 50 \n", - "11 175 3500 0.8 25 75 \n", - "12 175 3500 0.8 25 75 \n", - "13 175 3500 0.8 25 75 \n", - "14 175 3500 0.8 25 75 \n", - "15 200 2500 1.0 25 75 \n", - "16 200 2500 1.0 25 75 \n", - "17 200 2500 1.0 25 75 \n", - "18 125 2500 1.2 20 25 \n", - "19 125 2500 1.2 20 25 \n", - "20 125 2500 1.2 20 25 \n", - "21 125 2500 1.2 20 25 \n", - "22 125 3500 1.0 25 50 \n", - "23 125 3500 1.0 25 50 \n", - "24 125 3500 1.0 25 50 \n", - "25 125 3500 1.0 25 50 \n", - "26 150 4500 1.0 16 100 \n", - "27 150 4500 1.0 16 100 \n", - "28 150 4500 1.0 16 100 \n", - "29 150 4500 1.0 16 100 \n", - "30 100 2500 1.0 35 50 \n", - "31 100 2500 1.0 35 50 \n", - "32 200 5000 1.2 35 50 \n", - "33 200 5000 1.2 35 50 \n", - "34 225 4000 0.8 30 25 \n", - "35 225 4000 0.8 30 25 \n", - "36 225 4000 1.0 25 25 \n", - "37 225 4000 1.0 25 25 \n", - "\n", - " Isc [mA] Jsc [mA/cm2] Voc [V] FF [-] Efficiency [%] \n", - "0 3.996 19.0280 1.046000 0.59 11.743293 \n", - "1 3.553 16.9190 1.044000 0.58 10.244822 \n", - "2 3.820 18.1900 1.040000 0.67 12.675124 \n", - "3 4.181 19.9090 1.023000 0.69 14.053536 \n", - "4 4.773 22.7285 0.981303 0.61 13.605205 \n", - "5 4.844 23.0660 0.921332 0.58 12.326194 \n", - "6 4.458 21.2280 0.832519 0.59 10.427182 \n", - "7 4.504 21.4476 0.818877 0.62 10.889036 \n", - "8 4.644 22.1142 0.945775 0.59 12.339932 \n", - "9 4.267 20.3190 0.966675 0.62 12.177988 \n", - "10 4.557 21.7000 0.851056 0.64 11.819466 \n", - "11 4.651 22.1470 0.902530 0.55 10.993890 \n", - "12 4.595 21.8800 1.044000 0.74 16.904349 \n", - "13 4.897 23.3190 0.995998 0.64 14.864464 \n", - "14 4.601 21.9090 1.011000 0.62 13.733328 \n", - "15 4.728 22.5142 0.945126 0.60 12.767302 \n", - "16 4.867 23.1761 0.926920 0.60 12.889485 \n", - "17 4.508 21.4660 0.895574 0.62 11.919493 \n", - "18 4.630 22.0476 1.025000 0.71 16.045155 \n", - "19 4.575 21.7850 1.010000 0.67 14.742393 \n", - "20 4.659 22.1857 0.970817 0.64 13.784492 \n", - "21 4.723 22.4900 0.991291 0.69 15.383279 \n", - "22 4.988 23.7523 1.009000 0.57 13.660707 \n", - "23 5.010 23.8571 1.047000 0.68 16.985331 \n", - "24 4.438 21.1333 1.045000 0.76 16.784093 \n", - "25 4.682 22.2952 1.031000 0.77 17.699521 \n", - "26 4.161 19.8142 0.993036 0.49 9.641387 \n", - "27 4.782 22.7714 0.971078 0.54 11.940930 \n", - "28 3.617 17.2238 0.967534 0.51 8.498957 \n", - "29 4.440 21.1428 0.996775 0.59 12.434056 \n", - "30 1.659 7.9000 1.005000 0.34 2.699430 \n", - "31 1.329 6.3285 0.938214 0.34 2.018768 \n", - "32 2.350 11.1904 0.969077 0.36 3.903996 \n", - "33 3.415 16.2619 0.932514 0.39 5.914137 \n", - "34 3.839 18.2800 0.898806 0.53 8.708446 \n", - "35 3.817 18.1761 1.007000 0.54 9.883849 \n", - "36 4.599 21.9000 0.974428 0.64 13.657583 \n", - "37 4.749 22.6142 0.968857 0.62 13.584206 " - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df_device = pd.read_excel('./ML Perov Data/ML perov data stanford.xlsx', sheet_name='2020_09_22_device')\n", - "print(df_device.columns)\n", - "df_device.columns = ['ML Condition', 'Sample', 'Temp [degC]', 'speed [mm/s]',\n", - " 'sprayFL [uL/min]', 'plamsaH [cm]', 'gasFL [L/min]', 'plasmaDC [%]',\n", - " 'Isc [mA]', 'Jsc [mA/cm2]', 'Voc [V]', 'FF [-]', 'Efficiency [%]']\n", - "df_device\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "df = df_device.iloc[:,2:]\n", - "df_cols = df.columns\n", - "\n", - "corr = df.corr(method='pearson')#'spearman'\n", - "corr.columns = df_cols\n", - "df_len = len(df_cols)\n", - "fs = 18\n", - "\n", - "\n", - "fig, ax=plt.subplots(figsize=(df_len*0.8,df_len*0.8))\n", - "sns.set(font_scale=1.5)\n", - "sns.set_style(\"ticks\",{'xtick.direction': 'in', # set the style of the plot using seaborn\n", - " 'ytick.direction':'in',\n", - " 'xtick.top': False,'ytick.right': False})\n", - "mask = np.triu(np.ones_like(corr, dtype=np.bool),k=1)\n", - "cmap = plt.get_cmap('coolwarm')\n", - "sns.heatmap(corr, mask = mask, cbar_kws={\"shrink\": .2}, annot=True, fmt='.2f', \n", - " cmap=cmap, cbar=False, ax=ax, square=True)\n", - "ax.set_xlim(0, df_len)\n", - "ax.set_ylim(df_len, 0)\n", - "ax.set_title(\"Pearson Coefficients for Linear Correlation\", fontsize = 20)\n", - "plt.xticks(rotation=75, fontsize = fs)\n", - "plt.yticks(rotation=0, fontsize = fs) \n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Visualize input data distribution as histogram" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABB4AAAD1CAYAAAD3ajZhAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAABasElEQVR4nO3deVxUVf8H8A+OILK4IEK54gaaIJArpikgloZhjykmuBSP5i+XzEpJS/NpUxPNXXF73FMslx4rU1wyQSs1TU0qM3MdRUBZnWG4vz9sJgZmmBmYy52Z+3m/Xr5ecu6dO9/vOeeemTlz51wnQRAEEBERERERERGJoIbUARARERERERGR4+LEAxERERERERGJhhMPRERERERERCQaTjwQERERERERkWg48UBEREREREREoqkpdQDmKCoqwrlz59CwYUMoFAqpwyFySBqNBnfu3EFgYCBcXV2lDsckjgtE4uO4QERlcVwgorLMGRfsYuLh3LlziIuLkzoMIlnYvHkzOnXqJHUYJnFcIKo+HBeIqCyOC0RUVkXjgl1MPDRs2BDAw0QeeeQRiaNxDPfu3UPdunWlDoNgO21x69YtxMXF6c43W2fJuGArdSwWR88PYI5SceRxwZbZYl+wJ6y/yjOn7hx5XGDfsR1sC9thrXHBLiYetJdFPfLII2jSpInE0TgGb29vu7g8Tg5srS3s5TJES8YFW6tja3P0/ADmKDVHHBdsmS33BXvA+qs8S+rOEccF9h3bwbawHdYaFyRdXFKj0WDVqlWIiopCaGgoBg8ejPT0dClDkg1nZ2epQ6C/sS30iTEuOHodO3p+AHMkeWFfqBrWX+XJve7knr8tYVvYDmu1haQTD2vWrMGCBQswaNAgLF26FM2aNcPo0aNx4cIFKcOSBaVSKXUI9De2hT4xxgVHr2NHzw9gjiQv7AtVw/qrPLnXndzztyVsC9thrbaQdOJh586diI6OxtixY9G9e3fMnTsX3t7e2LFjh5RhEZGEOC4QERERETkWSSceVCoVPDw8dH8rFAp4enri3r17EkZFRFLiuEBERERE5FgknXiIi4vD7t27kZ6ejtzcXKxfvx6//fYb+vfvL2VYRCQhjgtERERERI5F0rtavPDCCzh+/DhGjRqlK5s0aRIiIyOlC4qIJMVxgYiIiIjIsUg28SAIAhISEnDp0iXMnDkTrVq1QlpaGpYuXYo6deogLi6u3GPKfvAYOXIkEhIS4Ovri9zcXACAp6cnlEolNBpNucd7enrqtnt7e0OtViMrK8tgfL6+vlCr1SgoKICXlxeysrJQVFRUbj9XV1fddjc3Nzg7OxtdgMPLywvOzs7IzMzUxayNuzSFQiF6TgCsnlN9L2+4OBu/hYpKrcGDogLRcrLXdsrNzcWNGzckz+n27dsGH1udxBoXAIjW32q7e6J2LeOr/T5QFaOmwkm0/lannhcaNWpk9PlVag1KNPZ/Dj148AAAHHpcKC4u1hsLbCEnWxgXxKJSa4y+ZlW0jYiIrK/suFv2vQ3HZfvnJAiCIMUT//jjj4iLi8Mnn3yCfv366crnzZuHTZs24dixY3B3dwcAXLt2DZGRkUhNTbXr+3LbkqKiIlHujTvg9d1Gt32RFGP153MEYrWFpWzhPBNrXBC7jqXu91I/f3WwlfNETLaYoy2MC5awNF5j547U540t9gV7wvqrPHPqzpHHBfYdacnh/Yw9sta4INkaD7du3QIAhISE6JV37NgRhYWFuH79ugRRyQfvjWs72Bb/EGtcYB3bPzm0oRxyJPOwL1QN66/y5F53cs+fyBBrnReSTTz4+fkBAE6dOqVXfubMGdSsWROPPPKIBFHJR2ZmptQh0N/YFv8Qa1xgHds/ObShHHIk87AvVA3rr/LkXndyz5/IEGudF5Kt8RAYGIjevXtj1qxZyMnJQatWrfD9999j9erVGDFiBOrUqSNVaLLg6+srdQj0N7bFP8QaF1jH9k8ObSiHHMk87AtVw/qrPLnXndzzJzLEWueFpHe1WLhwIT755BOsWLEC9+7dQ/PmzTF9+nQMHTpUyrBkITc3F56enlKHQWBblCXGuMA6tn9yaEM55EjmYV+oGtZf5cm97uSeP5Eh1jovJJ14cHV1RWJiIhITE6UMQ5Y4sNoOtoU+McYF1rH9k0MbyiFHMg/7QtWw/ipP7nUn9/yJDLHWeSHZGg9ERERERERE5Pg48UBEREREREREouHEAxERERERERGJhhMPRERERERERCQaTjzIlEKhkDoE+hvbQnysY/snhzaUQ45kHvaFqmH9VZ7c607u+RMZYq3zghMPMsX7FNsOtoX4WMf2Tw5tKIccyTzsC1XD+qs8uded3PMnMsRa5wUnHmQqNzdX6hDob2wL8bGO7Z8c2lAOOZJ52BeqhvVXeXKvO7nnT2SItc4LTjwQERGRTdNoNFi1ahWioqIQGhqKwYMHIz09XeqwiIiIyEyceJApT09PqUOgv7EtxMc6tn9yaEM55FhZa9aswYIFCzBo0CAsXboUzZo1w+jRo3HhwgWpQxMF+0LVsP4qT+51J/f8iQyx1nnBiQeZUiqVUodAf2NbiI91bP/k0IZyyLGydu7ciejoaIwdOxbdu3fH3Llz4e3tjR07dkgdmijYF6qG9Vd5cq87uedPZIi1zgtOPMiURqOROgT6G9tCfKxj+yeHNpRDjpWlUqng4eGh+1uhUMDT0xP37t2TMCrxsC9UDeuv8uRed3LPn8gQa50XnHggIiIimxYXF4fdu3cjPT0dubm5WL9+PX777Tf0799f6tCIiIjIDDWlDoCIiIioIi+88AKOHz+OUaNG6comTZqEyMhI6YIiIiIis3HigYiIiGyWIAhISEjApUuXMHPmTLRq1QppaWlYunQp6tSpg7i4uHKPKTshMXLkSCQkJMDX11d3WzBzFstSKpXw9vaGWq1GVlaWwX18fX2hVqtRUFAALy8vZGVloaioqNx+rq6uuu1ubm5wdnY2+rvZ4uJiaDQaZGZm6mI2dDszhUJRLielUmnwslhPT0/d9qrmVL9BQ9Su5WzwsQBQ+ECN7Lt39Mq8vLzg7OxcLTnVq9/AaGwAoFJrkJd7r8rtVJ05GSJV37t9+7bBxxIRVYQTD0RERGSzTp48iZMnT+KTTz5Bv379AABdu3aFRqPBxx9/jIEDB8Ld3V3vMampqWjSpInB41myOrevry+Ahx8cGzVqZHQ/hUIBV1dXAA8/jFak9HZjx7xx44buw6o25oriLr1N+xhjrJXTgNd3G33sF0kxqG3k2NWVk6n4rNFOZZ/fFtupNGv1vZKSkgqfh4jIEK7xIFO8XZDtYFuIj3Vs/+TQhnLIsTJu3boFAAgJCdEr79ixIwoLC3H9+nUJohIX+wJJRe59T+75ExnC22lKQKWueEVPU9ulPn5pHFhtB9tCfKxj+yeHNpRDjpXh5+cHADh16pRe+ZkzZ1CzZk088sgjEkQlLvYFkorc+57c8ycyxFrnBX9qYQEXZ4XJS/ds+filKZVKk5f5UfVgW4iPdWz/5NCGcsixMgIDA9G7d2/MmjULOTk5aNWqFb7//nusXr0aI0aMQJ06daQO0erYF0gqcu97cs+fyBBrnReceJApb29vqUOgv7EtxMc6tn9yaEM55FhZCxcuxCeffIIVK1bg3r17aN68OaZPn46hQ4dKHZoo2BdIKnLve3LPn8gQa50XnHiQKbVaDYVCIXUYBLZFdWAd2z85tKEccqwsV1dXJCYmIjExUepQqgX7AklF7n1P7vkTGWKt84JrPMiUsVszUfVjW4iPdWz/5NCGcsiRzMO+QFKRe9+Te/5EhljrvODEAxERERERERGJhhMPRERERERERCQaTjwQERERERERkWg48UBEREREREREouHEAxERERER2aTU1FSEhobqlZ07dw4BAQHl/s2ZM0eiKInIFN5OU6Z8fX2lDoH+xrYQH+vY/smhDeWQI5mHfYGkYmt979SpU3jzzTfLlV+8eBFubm5Yt26dXrmPj0+Vns/W8ieyBdY6LzjxIFO8T7HtYFuIj3Vs/+TQhnLIkczDvkBSsZW+p1KpsH79eixcuBBubm5Qq9V62zMyMtCmTRuEhIRY9XltJX8iW2Kt84I/tZCpgoICqUOgv7EtxMc6tn9yaEM55EjmYV8gqdhK3/v222+RnJyMKVOmID4+vtz2jIwMBAQEWP15bSV/IltirfOCEw8y5eXlJXUI9De2hfhYx/ZPDm0ohxzJPOwLJBVb6XtBQUFITU3FiBEj4OTkVG77r7/+ips3byImJgaBgYGIiorCzp07q/y8tpI/kS2x1nnBiQeZysrKkjoE+hvbQnysY/snhzaUQ45kHvYFkoqt9D1fX1/UqVPH4DalUons7GxcuXIFY8eORXJyMjp37ozExETs2rWrSs9rK/kT2RJrnRdc40GmioqKpA6B/sa2EB/r2P7JoQ3lkCOZh32BpGIPfa9u3bpYs2YN/P39dYtJdu/eHbdv38aSJUswcOBAg4+LjIzU+3vkyJFISEiAr68vcnNzATzMX6lUQqPRlHu8p6cnPD09oVQq4e3tDbVabfQDma+vL9RqNQoKCuDl5YWsrCyDdevq6qrb7ubmBmdnZyiVSoPH9PLygrOzMzIzM3Uxa+MuTaFQ6OWkjdkecjLFHnNyhHYqLi5G3bp1K8zp9u3bBh9bGiceiIiIiIjILri6uqJHjx7lynv27ImjR48iPz8f7u7u5banpqaiSZMmBo/p6ekJ4OEHW1MfgLXbFQoFGjVqZHQ/hUIBV1dXAKYvVS+9vaJjln5+7YdRY0pvs5ecTLHHnByhnW7cuKGbKNHGXDankpKSCp8HsIGfWqSnp2Pw4MHo0KEDwsPDsWjRIoMzPUQkHxwXiIjIEanUFb+WmdpOwOXLl7FlyxaoVCq98gcPHsDV1RVubm4SRUZEFZH0ioeTJ09i9OjRiI6OxuTJk3H+/HksXLgQNWrUwPjx46UMjYgkwnGBiIgclYuzAgNe3210+xdJMdUYjX1SKpWYNWsWGjZsiKioKACAIAj45ptv0KlTJ4OLURKR9CSdeEhKSsITTzyB2bNnAwDCwsKQk5ODEydO8AMGkUxxXCAiIiJjOnfujI4dO2LmzJm4d+8eGjZsiO3btyMjIwNbt26VOjwiMkKyiYesrCycOnUKS5cu1St/4403JIpIXrS/+yHpsS3+Ida4wDq2f3JoQznkSOZhXyCp2EPfUygUWLZsGebPn49FixYhJycHjz32GNatW4fAwMAqHdse8ieqbtY6LySbeMjIyIAgCHBzc8PYsWNx7NgxeHh4YNiwYRg3bhxq1JB8+QmHxvsU2w62xT/EGhdYx/ZPDm0ohxzJPOwLJBVb7HsTJkzAhAkT9Mrq1auH//znP1Z/LlvMn0hq1jovJJt4yM7OBgBMmTIF0dHRGDVqFH744QcsX74ctWrVwpgxY8o9xpzb4Ih5KxJzKl2j0VT69iqmVkcFHq4qao2ctHgbHNM5iX0bnMLCQtSuXVvynMy5DY7YxBoX1Go11Gq1XY4LWsb6mznnXVFRkd2fQ4IgoHHjxg49Lty5c8foqtByHhfkyNyxhcja5N735J4/kSHWOi8km3hQq9UAgB49emDq1KkAgG7duiE7OxvLly9HQkICFAqF3mPMuQ0OIM2tSEo/xlq3VzHE2LEtzamoqMjkZTO8DU713AbHUFtIkZM5t8ERm1jjgjn93V7GBUv6m5arq6vdn0PaD9mOPC7Uq1evwn4q13FBjrgqP0lF7n1P7vkTGWKt80Ky3zNo76/bs2dPvfLu3bujoKAA169flyIs2XB2dpY6BPob2+IfYo0LrGP7J4c2lEOOZB72BZKK3Pue3PMnMsRa54VkEw/NmjUD8M83nFrFxcUAwFvhiMzYpbdU/dgW/xBrXGAd2z85tKEcciTzsC+QVOTe9+SeP5Eh1jovJJt4aN26NXx9ffH111/rlR85cgQ+Pj5o3LixRJERkVQ4LhAREREROR7JJh5q1KiByZMn4+DBg5g5cybS09ORlJSEnTt38q4WRDLFcYGIiIiIyPFItrgkAAwcOBA1a9bEypUr8fnnn+PRRx/FrFmzEBsbK2VYRCQhjgtERERERI5F0okHAIiOjkZ0dLTUYRCRDeG4QERERETkOHjdMhERERERERGJhhMPMmXqHvBUfdgW4mMd2z85tKEcciTzsC+QVOTe9+SeP5Eh1jovOPEgU7xPse1gW4iPdWz/5NCGcsixKtLT0zF48GB06NAB4eHhWLRoETQajdRhiYJ9gaQi974n9/yJDLHWecGJB5nKzMyUOgT6G9tCfKxj+yeHNpRDjpV18uRJjB49Gq1atcLKlSsRFxeHVatWYfny5VKHJgr2BZKK3Pue3PMnMsRa54Xki0uSNHx9faUOgf7GthAf69j+yaEN5ZBjZSUlJeGJJ57A7NmzAQBhYWHIycnBiRMnMH78eImjsz72BZKK3Pue3PMnMsRa5wWveJCp3NxcqUOgv7EtxMc6tn9yaEM55FgZWVlZOHXqFIYMGaJX/sYbb2Djxo0SRSUu9gWSitz7ntzzJzLEWucFJx5kigOr7WBbiI91bP/k0IZyyLEyMjIyIAgC3NzcMHbsWAQFBSEsLAyLFy9GSUmJ1OGJgn2BpCL3vif3/IkM4cQDERERObzs7GwAwJQpU9CiRQusWrUKw4YNw/Lly7F69WqJoyMiIiJzcI0HIiIisllqtRoA0KNHD0ydOhUA0K1bN2RnZ2P58uVISEiAQqHQe0xkZKTe3yNHjkRCQgJ8fX1139x4enqafG6lUglvb2+o1WpkZWUZ3MfX1xdqtRoFBQXw8vJCVlYWioqKyu3n6uqq2+7m5gZnZ2colUqDxywuLoZGo0FmZqYuZkPfOCkUinI5KZVKg3f78PT01G2vak6NGjUy+LjSbty4ofe3l5cXnJ2dqyUnc+KzRjtVJidz+h1g233v9u3bZuVARFRapSYeCgsLUbt2bQAPv4n48ssvUaNGDfTr1w/16tWzZnxEREQkY+7u7gCAnj176pV3794dmzdvxvXr19GsWTO9bampqWjSpInB45n7wQ/4Z0EthUJR4YdZhUIBV1dXAKbvd156u7Fj3rhxQ/dhVRtzRXGX3mZqETAxcjLE2LGlzKk0a7RT2ee3JCdTbLnvOepPnIhIXBZNPNy/fx+vvfYa7t+/j5SUFOTl5WHQoEG4efMmBEHAsmXLsGXLFjRt2lSseImIiEhGtJMK2isftIqLiwEATk5O1R4TERERWcaiNR4++eQTnDhxQvetw44dO3Djxg28+eab2LBhA2rUqIFPPvlEjDjJyspelkrSYVuIj3Vs/+TQhnLIsTJat24NX19ffP3113rlR44cgY+PDxo3bixRZOJhXyCpyL3vyT1/IkOsdV5YNPFw8OBBxMfHY+LEiQCAAwcOoEGDBnjppZfQpUsXxMXFIS0tzSqBkbh4n2LbwbYQH+vY/smhDeWQY2XUqFEDkydPxsGDBzFz5kykp6cjKSkJO3fuxLhx41CjhuOtk82+QFKRe9+Te/5EhljrvLDo1fru3bto06YNgIe31fjpp5/wxBNP6LbXr18fhYWFVgmMxMXbBdkOtoX4WMf2Tw5tKIccK2vgwIFISkrCqVOnMGbMGOzbtw+zZs3C0KFDpQ5NFOwLJBW59z25509kiLXOC4vWePD19cXVq1cBPLzaQaPRoHfv3rrtp06dwqOPPmqVwIiIiIi0oqOjER0dLXUYREREVAkWTTyEh4dj/fr1yMvLw969e1G3bl1ERERAqVRi1apV2L17N1555RWxYiUrsmRlZRIX20J8rGP7pVJr4OKsMNqG2u2OgP2UtNgXSCpy73tyz5/IEGudFxZNPLz55psoLCzEjh074Ovri3fffReurq749ddfsXnzZgwYMACjR4+2SmAkLqVSyd+x2Qi2hfhYx/bLxVmBAa/vNrr9i6SYaoxGXOynpMW+QFKRe9+Te/5EhljrvLBo4sHFxQXvv/8+3n//fb3ytm3b6laXJvug0WikDoH+xrYQH+uY7AH7KWmxL5BU5N735J4/kSHWOi8sWlxyxIgRSE9PL1fu4uICHx8fHDx4EM8884xVAiMiIiIiIiIi+1fhFQ+FhYXIzs7W/f39998jKioKzZs3L7dvSUkJvv32W1y7ds36URIRERERERGRXTI58TBw4EDdLTScnJzw4Ycf4sMPPzS4vyAIerfXJCIiIiIiIiJ5q3DiwcvLCx9//DF+/vlnCIKApUuXIioqCgEBAeX2rVGjBry8vPhTCyIiIiIiIiLSMbm4ZK9evdCrVy8AwI0bNzB06FAEBweLHhiJi7cLsh1sC/GxjskesJ+SFvsCSUXufU/u+RMZIsntND/66COrPClJjwOr7WBbiI91TPaA/ZS02BdIKnLve3LPn8gQSSYeAODbb7/FF198gczMTIO31nBycsL69eutEhyJh/cpth1sC/GxjskesJ+SFvsCSUXufU/u+RMZYq3zwqKJh82bN+P9998HADRo0AAuLi5VDoCk4e3tLXUI9De2hfhYx2QP2E9Ji32BpCL3vif3/IkMsdZ5YdHEw4YNG9C2bVusWrWKJ6adU6vVUCgUUodBYFtUB9Yx2QP2U9JiXyCpyL3vyT1/IkOsdV7UsGTnmzdvIjY2lpMODiArK0vqEOhvbAvxsY7JHrCfkhb7AknFFvteamoqQkND9coEQcDy5cvRu3dvBAcH48UXX8SlS5eq/Fy2mD+R1Kx1Xlg08dCsWTNkZmZa5YmJiIiIiIiMOXXqFN58881y5UuXLsXy5cvx0ksvYf78+cjNzcWoUaOQm5srQZREZA6LJh7GjBmDjRs34rfffhMrHiIiIiIikjGVSoVVq1ZhxIgRqFlT/5fheXl5WLNmDcaPH48RI0YgMjISa9asQX5+Pnbs2CFRxERkikVrPJw8eRLu7u6IiYlBixYt4OXlBScnJ719eFcLIiIiIiKqrG+//RbJycmYMmUKcnJysG7dOt22M2fOoKCgAJGRkbqyunXrokuXLjh69ChefPFFKUImIhMsmng4evQoAOCRRx5BYWEhrl+/LkpQREREREQkT0FBQUhNTUWdOnWwePFivW1//vknAKBp06Z65U2aNMHBgwerK0QispBFEw9incwqlQoxMTEIDg7G7NmzRXkOIrIvHBeIiIjkydfX1+i2vLw8uLi4wMXFRa/c3d0deXl5YodGRJVk0cSDWJYsWYI//vgDwcHBUociGxUN6FS92BaGWXNcYB2TPWA/JS32BZKKPfQ9QRDK/dRby1g5AL2fZgDAyJEjkZCQAF9fX92ilN4Njef/QFWMWi41oVQq4e3tDbVabXS1f19fX6jVahQUFMDLywtZWVkoKioqt5+rq6tuu5ubG5ydnaFUKg0e08vLC87OzsjMzNTFbGgxTYVCoZeTp6cnlEolNBpNuX09PT11220hJ1PsMSdHaKe6detCo9FUmNPt27cNPrY0iyYeRowYYdZ+GzZsMPuYFy5cwMaNG1G/fn1LQqEq4n2KbQfbojxrjwusY7IH7Kekxb5AUrGHvufp6QmVSgW1Wg1nZ2ddeX5+Pjw9PY0+LjU1FU2aNDF6TK0Br+82uM8XSTEA/pmcUSgUaNSokdHnUygUcHV1BfDww2hFSm+v6Jiln1/7YdSY0ttMfai3lZxMscecHKGdioqKdBMl2pjL5lRSUlLh8wAWTjxcu3atXFlJSQmys7Px4MEDNG7cGG3atDH7eMXFxZg2bRoSEhKwf/9+S0KhKiooKNB1SJIW20KfGOMC65jsAfspabEvkFTsoe81b94cgiDg2rVraNGiha687N9EZB3WGhesssaDRqNBamoq3n77bSQkJJh9vFWrVkGtVmPMmDGceKhmpma/qPqwLfSJMS6wjskesJ+SFvsCScUe+l5oaChq1aqFAwcOYPTo0QCAe/fu4fvvv8f48eMljo7I8VhrXLDKGg8KhQJ9+/bFmTNnMG/ePGzbts3kYy5duoQVK1bgv//9b7nFYUh8WVlZdvHiIgdsi3+INS6wjskesJ+SFvsCScUe+p67uzvi4+OxcOFC1KhRA35+flixYgU8PDwwePBgqcMjcjjWGhesurikn58fNm3aZHK/kpISTJ8+Hc8//zxCQ0PNPr6pRWFcatVGLRfjKVV1URhzKlyj0VR6sRFTvxUCgBs3buj9bcliI94NfeHirNDFUlbhAzWy796R5aIw9Rs0RO1azuUep6Wtm9KstShMbm6uXsxSLXRjzqIwYhJrXAAe/jZNrP4m9rigZay/mXPeFRUVVfocqsy5Yc2FlSozLpp7Dnk1aAhXM3OrjsWi8vLyDMZpSU6lxwV3jzomXxNrKpxselyQK2P9gEhs9tL3Jk+ejBo1amDt2rUoKChAaGgoZs+eXeFv6Ymocqw1Llht4kGlUmHPnj1o0KCByX03btyImzdvIjk5GcXFxbpyQRBQXFyMmjUNh2XOojDGFoQBxF0UpvRjrLXYiCHGjm1uTqbqp3apx8ptURhL6qa0quZ048aNcrFIsdCNOYvCiEnMcSE3N1eS/lb6MVIsQqTl6upapZwqe26ImVNphnIzt50szU3MxaJq1qxp9bHOktdEWxwXiIgmTJiACRMm6JXVrFkTb7zxBt544w2JoiIiS1nlrhYqlQqXL1/G/fv3yw0Mhhw4cAC3bt1C586d9covXryIXbt2VfhBgogcE8cFIiIiIiLHVOW7WgAPv4Vp2bIloqOjMWzYMJPHmTVrFvLz8/XK3njjDbRo0QLjxo2Dj4+PJWERkQPguEBERERE5JisclcLS7Vs2bJcmaurK+rVq4egoCCrPAcR2ReOC0RkikqlQkxMDIKDgzF79mypwyEiIrJ7KrVGtw6gIfUbNLTK81RqjQeNRoNz587h+vXrcHFxwaOPPor27dtbJSAiubH1+2U7AtYx2QP2U9OWLFmCP/74A8HBwVKHIir2BZIK+x6R/Lg4K8xaE6qqLJ54OHToEGbNmgWlUglBEAAATk5O8PHxwcyZMxEREVGpQHbvNp4skSOz9dtWScla4wLrmOwB+2nFLly4gI0bN6J+/fpShyI69gWSCvseEYmlhiU7//jjj5gwYQIEQcBrr72GpUuXYsmSJXjttdfg5OSEiRMn4tSpU2LFSuSQjN3mkKyHdUz2gP3UuOLiYkybNk13q1xHx75AUmHfIyKxWHTFw+LFi9G4cWPs2LGj3C23hg0bhkGDBmH58uVYtWqVVYMkcmRubm5Sh+DwWMdkD9hPjVu1ahXUajXGjBmD/fv3Sx2O6NgXSCrse0QkFouueDh79iwGDx5s8N7lHh4eeP7553HmzBmrBUckB87OzlKH4PBYx2QP2E8Nu3TpElasWIH3338fLi4uUodTLdgXSCrse0QklkotLmmMk5MT1Gq1NQ9J5PCUSiUaNWokdRgOjXVM9oD9tLySkhJMnz4dzz//PEJDQ81+XGRkpN7fI0eO1P1MIzc3FwAMfolSllKphLe3N9RqtdFL0H19faFWq1FQUAAvLy9kZWWhqKio3H6urq667W5ubnB2doZSqTR4zPpe3qitML7CeOEDNbLv3oFCoSiXk1KphEajKfcYT09P3faq5mROP71x44be315eXnB2dkZmZqYuZm3cpVkjJ3Pis0Y7VSYnc/odIF3fKy4uRuPGjSvM6fbt22blQERUmkUTD8HBwdixYweGDRtW7lKsvLw8pKSk8LZ3REREZBUbN27EzZs3kZycjOLiYl25IAgoLi5GzZqG38akpqaiSZMmBreZ+8EPgG49CYVCUeGHWYVCobsbgKnF+Upvr+iYplYYr13qsaVzMrUGhhg5GWLs2Nrn104aGCNGTqVZq51KP78lOZkiVd+7ceOGbqJEG3PZuEtKSkwnQERUhkUTD+PHj8eIESMQHR2N+Ph4+Pn5AQD++OMPbNmyBUqlErNmzRIjTiIiIpKZAwcO4NatW+jcubNe+cWLF7Fr164KJxiIiIjIdlg08dCpUycsXrwY7733HubOnQsnJycAD795aNiwIebPn49u3bqJEigRERHJy6xZs5Cfn69X9sYbb6BFixYYN24cfHx8JIqMiIiILGHxGg/t2rVDv3790K9fP1y7dg0AcO3aNWRlZaFTp05WD5CIiIjkqWXLluXKXF1dUa9ePf60k4iIyI5YdFeLX3/9Fc899xw2bNgAFxcX9O/fH/3798f9+/exZcsWDBw4EFevXhUrViIiIiIiIiKyMxZNPCQlJcHd3R179+5F27ZtdeVvvPEG9u7dC2dnZ8ybN8/qQRI5ssos2kWWYR2TPWA/Nc/u3bsxe/ZsqcMgckgch4hILBZNPPz0008YNWqUblHJ0po2bYr4+Hj88MMP1oqNSBZ4z2zxsY7JHrCfEpHUOA4RkVgsmngoKSkxeH9gLUEQKtxOROVlZmZKHYLDYx2TPWA/JSKpcRwiIrFYNPEQEhKCbdu24f79++W25efnIyUlBcHBwVYLjkgOTN2fnKqOdUz2gP2UiKTGcYiIxGLRXS3Gjx+P+Ph4REdHY8CAAWjevDmcnJzw119/Ye/evbhz5w4++ugjsWIlcki5ubnw9PSUOgyHxjome8B+SkRS4zhERGKxaOIhODgY69atw5w5c7BmzRq9bW3btsVHH32E0NBQqwZI5Oj4Ii8+1jHZA/ZTIpIaxyEiEotFEw8A0KlTJ6SkpCArKwvXr19HSUkJHn30Ufj4+IgRHxERkUNSqTVwcVbo/m7UqFGF24mIiIjslcUTD1peXl685Q4REVEluTgrMOD13Ua3f5EUU43REBEREYnHosUliYiIiIiIiIgswYkHIiIiIiIiIhINJx6IJKZQ8DfcYmMdExERmcbXSyISCyceiCTGe2aLj3VMRERkGl8viUgsnHggklhubq7UITg81jEREZFpfL0kIrFw4oGIiIiIiIiIRMOJByKJeXp6Sh2Cw2MdExERmcbXSyISCyceiCSmVCqlDsHhsY6JiIhM4+slEYmFEw9EEtNoNFKH4PBYx0RERKbx9ZKIxMKJByIiIiIiIiISDSceiIiIiIiIiEg0nHggIiIiIiIiItFw4oGIiIiIiIiIRFNT6gCI5I63rhIf65iIiMg0e3q9zM7ORrdu3cqVP/XUU1i0aJEEERFRRTjxQCQxe3qRt1esYyIiItPs6fXy4sWLAIC1a9fC3d1dV16vXj2JIiKiikg68aDRaLBhwwZs374dN2/eRKNGjTBs2DDExcXByclJytCIqo1SqYSvr6/UYdgMMcYF1jEREZFp9vR6mZGRAW9vbzzxxBNSh0JEZpB04mHZsmVITk7GK6+8gpCQEPz444/48MMPUVhYiNGjR0sZGlG18fb2ljoEmyLGuMA6JiIiMs2eXi8zMjIQEBAgdRhEZCbJJh40Gg3WrVuHhIQE/N///R8AICwsDFlZWVi7di0nHkg21Go1FAqF1GHYBLHGBdYxERGRafb0epmRkYFatWph6NChOH/+POrXr48RI0YgISGBV04T2SDJ7mqRl5eHgQMHom/fvnrlLVq0QFZWFgoKCiSKjKh6ZWVlSR2CzRBrXGAdExERmWYvr5cajQaXLl3C5cuXERsbi9WrV+OZZ55BUlISli5dKnV4RGSAZFc81K1bFzNmzChXfujQITzyyCNwc3OTICoikhLHBSIiIjLHihUr0KhRIzRv3hwA0LVrVxQUFGD16tUYPXo0atWqpbd/ZGSk3t8jR45EQkICfH19kZubC8C8xTWVSiW8vb2hVquNTtT4+vpCrVajoKAAXl5eyMrKQlFRUbn9XF1dddvd3Nzg7OwMpVJp8JheXl5wdnZGZmamLmZt3KUpFIpyOSmVSmg0mnL7enp66rbbQk6m2GNO9tBOXl5eBo9VmkajqTCn27dvmzyGTd3VIiUlBWlpaXj77belDoWIbATHBSLiYtREVJpCoUBYWFi58p49e+LTTz/FlStX4O/vr7ctNTUVTZo0MXg8S+7mof2ArFAo0KhRowpjdHV1BQCTH+xKb6/omKWfX/th1JjS20x9qLeVnEyxx5zsqZ0qop0o0cZcNqeSkhKTx7CZiYc9e/Zg5syZeOqppxAfH29wH1MzleYOGpWdLTJ3NqiyM2CmOjAA3LhxQ+9vS2bAzD2+rc5U1m/QELVrORs9tkqtQV7uvUrNVFam7q01U5mbm6t37MrMVLp71EEtF+On8wNVMWoqnKo8U1ndrDEuAED9Bg2NPscDVTFqudS02XFBy1h/M+e8KyoqqvTMeGXODWvO9lfm+c09hyw5thjfYFjy/Lb8DUZ14GLURFSaUqnE4cOHERUVpTfWPXjwAABQv359qUIjIiNsYuJh3bp1mDNnDiIiIjBv3jyj317Y8kxl6cdYawbMEGPHNjcnS45vizOVA17fbXT/L5JirDpTWZax/aua040bN8oduzIzlabqpvTzV3amsjpZe1wwVj9l68bWxwVL+puWq6ur1XMqzdS4JEZOpp6/qjkZO3Z15WTo+W31GwyxcTFqIipLpVJhxowZKCwsxKhRo3Tl+/btg5+fHxo2NP6FAxFJQ/KJh/nz52PlypUYOHAgPvjgA9SsKXlIRCQxjgtEpGXOorNc/4VIXpo2bYro6GgsXLgQTk5OaNWqFb7++mt88803XFySyEZJ+m5+/fr1WLlyJUaMGIFp06bxd5okS1X5NtQRcVwgotK46CxR9bGn9yQffPABli1bhvXr1+POnTto1aoVFi9eXO4nmERkGySbeLh9+zbmzZsHf39/PPPMMzhz5oze9sDAQH7LSbJgT/fMFhvHBSIyBxedJRKHPb0ncXV1xeTJkzF58mSpQyEiM0j2Dv67776DSqXCr7/+itjY2HLb09PTq/S7XCJ7UVBQoPvdttxxXCAiU6y16Kwt3zbP3IVHpbodm9SLzprKyZz4pLptntgLoVd1MWo4Kexm0Vkisi+STTz861//wr/+9S+pnp7IZvCD9D84LhBRRRxhMeqqLHJcWunHSn07NlPxGXp+KW4xV5rUt80zRcy+52iLURORfaghdQBEcmfsmwwiIvrH/PnzMXv2bMTExGDRokVwcXGROiQiIiIyE38sTSQxQ5dFEhHRP7joLBERkX3jxAMRERHZLC46S0REZP/4Sk1EREQ2i4vOEhER2T9OPBAREZHN4qKzRERE9o+LSxIRERERERGRaDjxQCQx7W2wiIiIiIiIHBEnHogkxt8mExERERGRI+PEA5HEsrKypA6BiIiIiIhINJx4IJKYm5ub1CEQERERERGJhhMPRBJzdnaWOgQiIiIiIiLRcOKBSGJKpVLqEIiIiIiIiETDiQciIiIiIiIiEg0nHoiIiIiIiIhINJx4ICIiIiIiIiLRcOKBiIiIiIiIiETDiQciIiIiIiIiEg0nHogk5uXlJXUIREREREREouHEA5HEnJ2dpQ6BiIiIiIhINJx4IJJYZmam1CEQERERERGJhhMPRBLz9fWVOgQiIiIiIiLRcOKBSGK5ublSh0BERERERCQaTjwQSYwTD0RERERE5Mg48UBEREREREREouHEAxERERERERGJhhMPRERERERERCQaTjwQERER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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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ueeew+zZs8UO5a688MILuHbtGr777juxQyEF69ixI+rVq4fPP/9c7FAUq6L25YkTJ2Lbtm04c+aMV1+XSImksL9IIQYipeD+VDL+juA1JmTBZrPhiSeecDut4cyZMzh37hxatmwpUmREREREREREd4encsiAv78/unXrhoSEBPj4+CA8PBw3btzAl19+iRo1atz1+dJScOXKFSQmJqJp06Zo1qyZ2OF45Pjx47h27ZrLnU+ISN6ysrKQmJgIg8GA9u3b39VrnTp1CmfPnq20i4IRKUliYiJ0Oh2efvrpStsm91miiiPGPi1l/B3xL05MyMSMGTPQqFEj7NixA9u2bYNer0dUVBRGjx6NWrVqiR3eXTt+/DiOHz+O4cOHy25iYsOGDdi9ezeAf696TETylpqaivHjx6NVq1Z3PTGxa9curFq1ykuREanL+PHjERISUqk/YrjPElUcMfZpKePviH/xGhNEREREREREJBpeY4KIiIiIiIiIRMOJCSIiIiIiIiISDScmiIiIiIiIiEg0nJggIiIiIiIiItFwYoKIiIiIiIiIRMOJCSIiIiIiIiISzf8H8xuRuO+QqIAAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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hGt/NOQe7tevCONC6cI5X6js+GH0fMTovluTx41o31+RmjAuAIguMV6v12vEC2/atEmTJk3SP//5TyUlJfm2r1u3To899pg+++wz3wRjz549GjlypBFhApazevVqdevWzZDXZlwAaibGBQBnMnJcABBdDLvSofhe1KdOnQqYRJw6dUo2my3gG420tDStXr1aycnJstlsEY8VsAKPx6Njx44pLS3NsBgYF4CahXEBwJlqwrgAILoYVnQo/m3mwYMHA36nefDgQbVq1Srg2Li4OCqpQASc+Ztpo16fcQGoORgXAJzJ6HEBQHQxbCHJVq1aqWnTptq+fbtvm9vt1gcffKAePXoYFRYAAzEuADgT4wIAANHNsKJDTEyMxo4dq7/97W969tln9eGHH+quu+7SiRMnNGbMGKPCilpr167V1VdfrU6dOmnEiBH6/PPPyz3+73//uzIyMtSxY0dde+21+tvf/hahSKPXjh071LVr1wqP279/v2655RZ17dpVvXv31pIlS2TQ0ilRJxrGhWA/a/7mzZuntm3bhjE68wm2vV0ulyZPnqxLLrlE3bp105133qkDBw5EKFqEQzSMC9Vx4sQJtW3btsR/9957r9GhhVVp51Sv16uFCxeqd+/e6ty5s2699VZ99913BkUYXqXlv3fv3lL7wjPPPGNQlAAQGob9vEKSRo4cqfz8fK1cuVLLly9X+/bt9dJLL6lFixZGhhV13njjDU2ZMkUTJkxQx44dtWrVKmVmZmrjxo2ltmXxolz9+vXTgw8+qB9//FEzZszQyZMndccddxiQQc332Wef6YEHHqjwuF9++UW33nqrzjvvPD333HP63//9Xz333HOy2WzKzMyMQKTRryaPC8F+1vzt379fixYtilCk5hBse7vdbt16663Kz8/XE088IZvNpmeffVZjx47Vpk2bFBsba0AWCIWaPC5U17fffitJevnllwPWp2jYsKFBEYVfWefU+fPna8mSJZo0aZKaNWumhQsXasyYMdqyZYtvbQ8zKCv/b7/9VvHx8Vq2bFnA9iZNmkQqNAAIDy+iWlFRkffKK6/0Pv74475tBQUF3vT0dO8TTzxR6mMyMjK8I0aM8BYVFfm2vfrqq95OnTp5XS5X2GOOJvn5+d4lS5Z4O3To4L344ou9Xbp0Kff4559/3nvJJZd4c3NzfdueffZZ7yWXXOIt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XoMHD5YkFRUVqUmTJnrnnXc0efLkMuOz2WwaMGCAmjRpovbt22vXrl0UHYAwu+yyy3TppZeG/Ngz+Y8JZWnVqpVatWqlAwcOUHSApfXt21fNmzev8Lh9+/ZJku677z5fUWHfvn1q3769JkyY4Duuos9eaarymJokKSkpIIctW7boj3/8o2699VYNGzZM3bt314cffqiBAwdKkl577TXFxcX5/u2vW7du6tatm9avX6/Dhw9HLAcAiAb8vMIiEhISdOWVV2rPnj365ZdfSuz/5ZdftHv3bl177bWKjY2NeHy1atVSRkaGDh06pL1795bY73a79e6776p79+5q0qRJxOMDACAaud1uSVK9evUCtvn/G79zOp2+Qk79+vXVsGFD/fzzz5Kk/Px8LVmyRBMmTJDNZjMyTACIOhQdLGTgwIHyeDylfju4detWeTyegJ9W7NmzR2PGjFHXrl3VtWtXjR49Wv/6179KPPaLL77Q2LFj1a1bN1166aUaN26c75uVYOOTpHfeeafEvp07d+rXX3+N2E8/AACIdunp6Zo3b54kqU+fPr51DA4fPqxPPvnEt47DoUOHSl3TYePGjRo6dKi6dOmiP/zhD3r88cflcrl8+0tb0+H999/XjTfeqM6dO+viiy/WPffcox9++CHgmLZt22rJkiVatmyZ+vbtq7S0NA0cOFBvv/12iRw+/PBDjRo1Sl27dlXPnj01ceJE31ou9913n9LS0vTbb78FPCY7O1sdO3bUM888E1R7NWrUSNnZ2ZJ+vwIzJyfH9xOWV199VfXr11dGRkZQzwkAoOhgKVdccYUSExO1devWEvu2bNmiZs2aqVu3bpKkHTt26Oabb9aRI0c0fvx4jR8/XkeOHNGYMWO0Y8cO3+P27NmjkSNH6rvvvtPtt9+u8ePH67///a9Gjx4d9AJvF1xwgc4999xSiw6bN29W3bp1ddVVVwWZNYDqys7OlsvlKvFf8TeoVT0WQNX99ttvpX7WXC6XPB6PJOmRRx7xnTcffvhhTZo0STNmzFCjRo3Upk0bzZgxo8zz6tKlSzV58mTVqVNH9913n6677jq99dZbuuOOO0osolhsw4YNGj9+vOrWrasHHnhAY8aM0eeff67hw4eXKDy8+uqrWr58uYYPH67JkycrNzdXEydO1P79+33HbN68WXfccYdOnjype+65R6NHj9bOnTs1ZswY/fbbb8rIyJDb7db27dsDnnvbtm0qKCgo9WcQ5bn44ou1YcMG/fe//9Xy5cvldrt1ySWXKC8vTy+++KImTJigWrWYOgNAsFjTwUJiY2N1zTXXaMOGDTpx4oSveu90OvXpp59q3LhxiomJUWFhof76178qJSVFr7/+uurXry9JuvHGG5WRkaGpU6fqD3/4g+x2u5555hk1bNhQr7/+uu/5rrjiCvXv319r1qwpc32GsgwcOFDPPvusvvnmG7Vv317S7ws37dixQ3369OFyUMAA/r/79rdy5coS6zcEcyyAqrvuuuvK3Pfmm2+qffv26tu3r7755hu9++67AWtAPP/88wHrGZz5JcHJkyc1d+5c9erVS4sXL/b9nKB58+Z67LHH9M9//lNXXHFFwGNycnI0bdo09e/fX3PmzPFtHz58uAYMGKBZs2Zp/vz5vu2//vqrtm3bpuTkZElS586dNXz4cG3evFnnn3++ioqKNH36dJ1//vlau3at4uLiJEkdO3bUrbfeqk2bNmn48OFq2LCh3n77bV1//fW+596yZYvatGmjCy64IKg2/dOf/qTbbrtNAwYMUK1atfTggw+qZcuWevnll9WgQQP1798/qOcDAPyOooPFDBo0SGvXrtX27ds1bNgwSdLbb78tr9fr++nC119/rZ9//lmTJk3yFRwkqUGDBho1apRmz56tvXv3qmXLlvryyy912223Bayg3bp1a73++usBq2dX1sCBA/Xcc89p69atvqLDhx9+qJycHH5aARjkwQcfVLt27UpsL21bMMcCqLqZM2cqKSmp1H3VvaPEzp07lZ+fr5EjRwasXzBo0CBdcMEFpd6h5p///KdycnLUt2/fgJ9g2Gw234KMhYWFvrs+XHTRRb6CgyTfOb/41t579+7VsWPHdOedd/oKDtLvi9WuW7dObdq0kd1u932ZcvLkSSUmJsrlcul//ud/NH78+KDzbtGihTZv3qz9+/crJSVFycnJys3N1YsvvqjHHntMtWrV0ptvvum7Q8X111/P1Q8AUAkUHSymW7duSk1N1datW31Fh82bN/t+2iD93zcerVu3LvH44olGVlaWbyJy9tlnlzgu2G8XijVr1kwXXnih3nnnHd99szdv3qzGjRurZ8+eVXpOANXToUOHSl+lEMyx5SkoKNDJkycDtsXFxSkhIaHazw2YwYUXXlipu1dURfHdF848v9epU0cdOnQo9TEHDhyQJN+5uzQul8u3GPSZt8YtXsS6qKio3BgkBdxae9CgQXrttde0fft2DR06VFu3blVhYWHQP63wjyMtLc3371deeUWNGzdWv3799J///EcPPfSQ/vznP6tly5a6//77lZKSouHDh1fptQDAKig6WExMTIwGDBigZcuW6ddff1V2dra+/PLLgIWgvF5vmY8v3me3230Tg5iYmJDGOHDgQP3lL3/Rvn371KJFC33wwQe64YYbStwTG4B5ff755xo9enTAtuuuu05PP/20QREB1lGV83vxY5544okyiyGJiYm+/6/o6oDKxnDRRRcpNTVVb7/9toYOHaq3335baWlppRYrgnXq1Cm99NJLmjp1qmJiYrR161a1bNnSd+vxa6+9Vps3b6boAAAV4K84Cxo0aJCWLl2q999/Xy6XSzabTQMGDPDtb9asmSTp+++/L/HY4oWgzjrrLKWkpEj6v283/M2cOVOJiYkaN25c0PH169dP06ZN07vvvqtzzz1Xp0+f5qcVgMW0a9dOy5YtC9jG7XKByCj+eeSBAwcCrnosKCjQAw88oIEDB6pv374BjymeOzgcDl122WUB+3bv3q2ioqKgbsntH8OZHn74YV144YUaNmyYYmJi1L9/f61YsUJZWVn69NNPg15PqiyrVq1SkyZNdM0110iSjh8/HnCFRsOGDUu9qxcAIBA/QrOg888/X23bttUHH3yg999/X927dw+YzHfo0EHJycl69dVXlZOT49uek5OjNWvWKDk5WWlpaUpJSVG7du20efPmgOMOHjyolStX6vjx41WKr2HDhurVq5cvvlatWgVcSgnA/BITE3XZZZcF/Ff8EzAA4XXZZZfJbrdr7dq1AVc/bt26tdQ7YBU/pk6dOnrxxRcD7lbjdDp11113adasWUFdOZGWliaHw6ENGzaooKDAt/3TTz/Vhg0blJub69s2cOBAud1uzZw5U16vV/369Qsm3VLl5ORo2bJluueee3xxJycn68iRI742OXTokO8LGABA2bjSwaIGDRqkBQsWKC8vT0899VTAPrvdrscee0wTJ07U0KFDdcMNN0iS1q9fr6NHj+qFF17wXRb58MMP6/bbb9fQoUM1bNgw1apVS6+88ooaNGigsWPHViu+iRMn6scff9SYMWOq/DwAAJjR9u3bAxZxPlPxnSmqonHjxpowYYKee+453Xbbberbt69+/vlnvfLKK7r00kuVnp5e4jEOh0P33Xefpk+frhEjRmjQoEEqLCzUmjVrlJ+frwcffDCoGGJjY/XQQw/pwQcf1B//+EcNGjRIp06d0sqVK3XOOef41qWSfr8y6rzzztOWLVt06aWXhqQQsHz5cp111lkBtxTt27ev5s2bp0cffVQtWrTQtm3bNGXKlGq/FgCYHUUHi8rIyNDs2bNVp06dUu/Rfe211yoxMVELFizQ/PnzVbt2bXXu3FnTpk1Tt27dfMd1795dK1as0AsvvKD58+erTp06uvjii/XAAw8ErEodrPT0dNWrV0/Z2dnVmjgBAGBG06dPL3d/dc+d48ePV3JyslauXKmnn35aycnJGj58uO65554y12MYM2aMUlJStGzZMj377LOKi4tThw4dNHPmTF100UVBxzB48GAlJCRo0aJFmj17tho0aKArr7xS999/v+Lj4wOOHThwoObMmaOMjIwq5esvOztbK1as0PTp0wOuzmjXrp2mTZum+fPn6/3331dmZqaGDh1a7dcDALOL8Za3aiAQJjfffLMOHz6s9957r1rP89BDD+mNN97Qvn37QhQZACMEOybMnTtX8+bN044dO8K2gj+A6LFkyRLNnTtXH3/8ccCClWVJT09Xs2bNtGrVqpDGEar5DQCYCWs6AAAAIGoVFBRow4YN6tu3b6UKDgCAyOLnFTBMbm6uNm7cKIfDoV69egX12G+++Ub79+/XwYMHwxQdgEirzJjw448/6osvvuDqJgByOp2aPn26/vvf/+qnn37SzJkzg3r88ePHtXHjRjVr1izgp6NVsWfPHh0+fLjKi2gDgJlRdIBhTpw4ocmTJ6tz585BFx02b96spUuXhikyAEaozJiwa9cu/eUvf4lsYABqpMTERO3Zs0eFhYWaMmWKOnbsGNTjv//+e02ePFn9+/evdtFh9erV2rJli6T/u30oAOB3rOkAAAAAAADCgjUdAAAAAABAWFB0AAAAAAAAYUHRAQAAAAAAhAVFBwAAAAAAEBYUHQAAAAAAQFhQdAAAAAAAAGHx/wDRbPl0kyM/ogAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df = df_device.iloc[:,2:]\n", - "df_cols = df.columns\n", - "n_col = 4 # num of columns per row in the figure\n", - "\n", - "for n in np.arange(0, 12, n_col):\n", - " fig,axes = plt.subplots(1, n_col, figsize=(18, 3.5), sharey = False)\n", - " fs = 20\n", - " for i in np.arange(n_col):\n", - " if n< len(df_cols):\n", - " axes[i].hist(df.iloc[:,n], bins=25)####\n", - " axes[i].set_xlabel(df_cols[n], fontsize = 18)\n", - " else:\n", - " axes[i].axis(\"off\")\n", - " n = n+1 \n", - " axes[0].set_ylabel('counts', fontsize = 18)\n", - " for i in range(len(axes)):\n", - " axes[i].tick_params(direction='in', length=5, width=1, labelsize = fs*.8, grid_alpha = 0.5)\n", - " axes[i].grid(True, linestyle='-.')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "image/png": "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p07F1KlTLR0GEZnIsGHDMGzYsHLX4XkEkfWz2DMriIiIiIiIiIjEcLKCiIiIiIiIiCSFkxVEREREREREJCmcrCAiIiIiIiIiSeFkBRERERERERFJCicriIiIiIiIiEhSOFlBRERERERERJLCyQoiIiIiIiIikhROVhARERERERGRpHCygoiIiIiIiIgkhZMVRERERERERCQpnKwgIiIiIiIiIknhZAURERERERERSQonK4iIiIiIiIhIUjhZQURERERERESSwskKIiIiIiIiIpIUTlYQERERERERkaRwsoKIiIiIiIiIJIWTFUREREREREQkKZysICIiIiIiIiJJ4WQFEREREREREUkKJyuIiIiIiIiISFI4WUFEREREREREksLJCiIiIiIiIiKSFE5WEBEREREREZGkcLKCiIiIiIiIiCSFkxVEREREREREJCmcrCAiIiIiIiIiSeFkBRERERERERFJCicriIiIiIiIiEhSOFlBRERERERERJLCyQoiIiIiIiIikhSLT1bs2bMHvXv3RocOHRAeHo5z586Vu/6VK1cQFRWFoKAg9OjRAwkJCRAEwUzREpFUGTuWEFHNx3GBqGbjeQRRzWbRyYp9+/Zh0aJFGDx4MNauXQtXV1dER0cjPT1ddP179+5h3LhxkMlkWLVqFUaMGIFVq1Zh69atZo6ciKTE2LGEiGo+jgtENRvPI4hqPotNVgiCgLVr12LEiBGYMmUKunfvjvj4eNSvXx9JSUmi2+zcuROFhYWIj49H9+7dMWnSJEycOBEJCQlQqVRmLgERSUFlxhIiqtk4LhDVbDyPILINFpusuH79Om7duoXQ0FBtmp2dHXr06IETJ06IbpOamorg4GA4Ojpq08LCwvDgwQP88ssv1R4zEUlPZcYSIqrZOC4Q1Ww8jyCyDRabrLh27RoAoEWLFjrpzZs3x40bN6BWq0W3EVu/ZH5EZFsqM5YQUc3GcYGoZuN5BJFtqG2pD87JyQEAODs766Q7OzujqKgI+fn5cHFx0dtGbP2S+WloBqm///7boHhUeQq9tJs3bxq0rbXlJ5YX86taflJp28rmV9kYNP3Lkgf+xowlphgXAOnsO8Z8jjk/yxrKZK46snRbVOWzOC6IM2ebMgbGILUYpDIuWMN5BGDe/YCKsS3MrzrHBYtNVmievCuTyUSXl5Vellq1dC8SyczMBACMHj26EtEV63kkptLbMj/byk/KsVU2P2O2yczM1Pu1wlyMGUtMMS4A0t53pPBZ1lwm1pHp8uK4YH6MgTFYIgZLjAvWcB4BSGM/oGJsC/My1bhgsckKV1dXAEBubi48PDy06bm5uZDL5XoznwDg4uKC3NxcnTTN36VnT/38/LBz5040bNgQcrnc1OETEYpnQjMzM+Hn52exGIwZSzguEFU/jgtEVJqpxwWeRxBZP0PGBYtNVmhmT9LT03VmUtLT0+Hl5SW6jZeXl94lJZrXE7Vq1Uon3cHBAR07djRhxEQkxlK/nJb+fEPGEo4LRObBcYGISjPluMDzCKKaoaJxwWIP2PTy8sJjjz2Gw4cPa9NUKhWOHTuG4OBg0W26du2K1NRU5OXladMOHz6MevXqoW3bttUeMxFJT2XGEiKq2TguENVsPI8gsg0Wm6yQyWSYMGECPv74Y6xcuRLHjx/HpEmTcP/+fYwdOxYAcOPGDfz888/abUaNGgWVSoWJEyfi6NGjiI+PR0JCAiZOnAh7e3vLFERiUlJSEBQUVOZyhUKB4OBgrF27ViddqVRiyZIleOaZZxAUFIRp06YhIyOjusOVlLLq7uDBgxg0aBD8/f3Ru3dvbN++XWc5686yDBlLShNra0EQEB8fjx49eiAgIADjxo3D1atXddZhW5uGWq1GYmIi+vXrh8DAQPTv3x87duzQ3oPMtjAvpVKJlStXIiQkBIGBgYiMjMSlS5e0y62xPcobF1xcXKp93/vnn38wd+5cdOnSBZ06dcL8+fP1HuBnSeZqc6nXQ0lKpRL9+vXD3LlztWm2Ug/379+Hj4+P3r9p06YBkGY9mOs8Ys+ePejduzc6dOiA8PBwnDt3rty4zp49i5deeglBQUHo2bMn1q1bB5VKVakyUtkqOt/RuHLlCqKiohAUFIQePXogISFBO96TaRjaFmfPnsWYMWPQsWNHPPvss5g9ezaysrIq/gDBwrZs2SJ0795d6NChgxAeHi6cPXtWu2zOnDmCt7e3zvoXLlwQwsPDBT8/P6FHjx7Cxo0bzR2yZP30009CUFCQEBgYWOY6b7zxhuDt7S2sWbNGJ33u3LlC586dhU8++UT48ssvhV69egmDBw8WCgsLqztsSSir7g4ePCj4+PgIy5YtE1JTU4UVK1YI3t7ewqeffqpdx9brTirKG0tKKqut165dK/j7+wtJSUnC4cOHheHDhwvPPvus8PDhQ+06bGvTWLNmjeDn5yfExcUJqampwpo1a4R27doJCQkJgiCwLczt7bffFoKCgoSdO3cKJ0+eFCZOnCg8+eSTws2bNwVBsO72KD0uvPXWW2bZ98aMGSOEhIQIX3zxhfDpp58KXbt2FSZOnGj28pfFXG0u9Xooafny5YK3t7cwZ84cbZqt1ENqaqrg7e0tnDx5Ujh37pz2319//SUIgrTroTrPIz799FOhbdu2wtq1a4Vjx44J0dHRQlBQkHDjxg3R9a9fvy4EBgYK48ePF06cOCFs27ZN6NChgxATE1OlMpIuQ853BEEQsrKyhKefflqIiooSjh07Jqxfv15o166dsHnzZjNFWvMZ2hZ//PGH4O/vL7zyyivCsWPHhAMHDgg9e/YUBg8eLCiVynK3tfhkBVXdo0ePhISEBMHX11fo1KlTmTtMSkqK0LlzZ8Hf319nsuL69etC27ZthYMHD2rT/vrrL8HHx0c4dOhQtcdvSeXVXVFRkdC9e3fhnXfe0dnmjTfeEGbOnCkIgm3XnbUpr62zs7OFwMBAnYOWBw8eCEFBQcLWrVsFQWBbm0phYaEQFBQkrFy5Uif97bffFrp27cq2MLOHDx8Kvr6+2roVBEHIz88XOnToIKxfv75GtYe59r3Tp08L3t7ews8//6xdR3MyePHixWosoWHM1eZSr4eSLl26JAQGBgpdunTRTlbYUj0kJiYKTz/9tOgyW6qHkoqKioSQkBBh4cKF2jSlUimEhoYK7777rug2GzduFPz9/YXc3Fxt2vLly4WgoCChqKio2mOu6Qw939FYvXq10LlzZyEvL0+btnLlSqFz584VniBT+Yxti7ffflsIDQ3Vqffz588L3t7ewrFjx8rd1mK3gZDpfPvtt0hISMDs2bMREREhuk52djbefvttzJ07V+9St++++w4A0KNHD22al5cXnnjiCZw4caLa4paC8uru4sWLuHPnDkaMGKGTvnz5csTGxgKw7bqzNuW19fnz55GXl4eePXtq09zc3NC5c2dtO7KtTSMnJwdDhw5F7969ddJbtmwJhUKB7777jm1hRo6OjtizZw+GDRumTatduzZkMhmUSmWN6hvm2vdOnz6NBg0aICAgQLtOly5d4OLiIon6MFebS70eNAoLCzFv3jxER0fD09NTm25L9ZCWlgYfHx/RZbZUDyVdv34dt27dQmhoqDbNzs4OPXr0KDNepVKJ2rVrw8HBQZtWr1495OXlQalUVnvMNZ0h5zslpaamIjg4GI6Ojtq0sLAwPHjwAL/88kt1hlrjGdsWbdq0wfjx42FnZ6dN0zzUtvRDb0vjZEUN4O/vj5SUFERGRpb5Xully5ahTZs2eP755/WW/fXXX/Dw8ICTk5NOerNmzXDt2rXqCFkyyqu7tLQ0AMX310dERMDPzw/du3fHRx99pF3HluvO2pTX1pq2at68uU56yXZkW5uGm5sbFi5ciPbt2+ukHz16FI0bN9be48y2MI/atWujffv2cHNzQ1FREdLT0zFv3jzIZDIMHjy4RvUNc+17f/31Fx5//HGd5bVq1ULTpk0lUR/manOp14PGpk2btM8xKMmW6iEtLQ35+fkYOXIk/P398dxzz2Hz5s0QBMGm6qEkTUyl31TQvHlz3LhxA2q1Wm+bwYMHQy6XY/ny5Xjw4AEuXLiApKQk9OrVC3Xq1DFH2DWaIec7JV27dk20/TTLqPKMbYvRo0dj9OjROmlHjhwBoP8mntIs9upSMp2SvwSIOX36NA4ePIgDBw6ILs/NzRV9H7WzszP+/vtvk8QoVeXVnUKhgFwux2uvvYZRo0Zh8uTJSElJwTvvvIN69eqhf//+Nl131qa8ts7JyYG9vb3eVUfOzs7ah3+xratPcnIyUlNTsWDBAraFBcXFxWkfvjxt2jS0atUK33zzTY1uj+rY98pbR0oPVQSqt82toR6uXr2KDRs24MMPP9Qrr63sD2q1GlevXoWjoyPmzJmDJk2a4NixY1i+fDkKCgpgZ2dnE/VQmiam0jE7OzujqKgI+fn5cHFx0Vn2+OOPY/bs2Vi4cCE2b94MAPD19cXSpUvNE3QNV9H5Tmk5OTmi7adZRpVnbFuUdufOHbz//vvw8/ND165dy12XkxU1XH5+Pv7zn/9g6tSperPiGoIglDkrZshsWU1VWFgItVqNESNG4NVXXwUABAcHIz09HevWrUP//v1ZdzWEIe3Itq4eBw4cwKJFi9CnTx9ERERg48aNbAsLCQsLQ+fOnXHmzBnExcVBpVLBwcGhxrZHde17giCgVi3xC1fLSreU6mxzqddDUVER5s+fjxdeeEH0Sfam+l6Qej0AwIYNG9CkSRPtr9BdunRBXl4eNm/ejFdffdVm6qEk4d83RhgztiUnJ2PBggUIDw9Hv379cPfuXaxZswYTJ04UnRAjy5HiPmcr7ty5g7Fjx6KoqAgrV66s8DiBLVXDrVy5Eq6uroiIiEBhYSEKCwsBFH9Ja/7v4uKC3NxcvW1zc3Ph6upq1nilRHM543PPPaeT/vTTT+PatWtQKpWsuxrC1dUVSqVS7/ViJduRbW16iYmJmD17Nnr06IHY2FjIZDK2hQW1bdsWnTt3xtSpUzFmzBhs2bIFjo6ONbI9qnPfK2+d0r/EWlp1trnU62H79u24c+cOpk+frnN8JAgCCgsLbWZ/kMvlCA4O1rtcvlu3bsjPz7eZ/aE0TdylY87NzYVcLhe9SiQhIQHdu3fHf//7XwQHB2PIkCFISEjATz/9VObVzVR9xPY5zd9S3OdswZUrVzBy5Ejk5ORg69atereGieFkRQ13+PBh/Prrr/D394evry98fX2RnZ2NuLg4+Pr6Aih+CFJWVhYKCgp0tr158yZatmxpibAlQfPFXfqhSIWFhdpfCFh3NUOLFi0gCILeQ35KtiPb2rRWrFiBmJgYDBkyBGvWrNH+4sS2MK/MzEx88sknepfEtmvXDkqlEm5ubjWuPap73/Py8kJ6errO8qKiIty6dUsS9WGuNpd6PRw+fBh///03OnXqpD0+unz5Mj777DP4+vqidu3aNlEPGRkZ2L17NxQKhU76o0ePAMBm9ofSNMeApWNOT0+Hl5eX6DZ37tzReYAoALRu3Rr16tXD1atXqyVOKpuXl5fefqtpz4qek0Cmd/78eYwePRpyuRw7d+5E27ZtDdqOkxU1XHx8PPbu3avzz8nJCSNGjMDevXsBFN/aoFartQ86AYofPPP7778jODjYUqFbXKdOnVCnTh189dVXOunHjh2Dv78/ateuzbqrIYKCglCnTh0cPnxYm/bPP//g+++/17Yj29p0kpKSsHHjRkRGRiImJga1a//vjkS2hXk9fPgQ8+bNw6FDh3TST506hQYNGiAsLKxGtYc59r3g4GBkZmbiwoUL2nXOnDmDnJwcSdSHudpc6vXwzjvv6B0feXl5ISQkBHv37sWAAQNsoh6USiUWLlyo98v/oUOH4OXlpX04ZE2vh9K8vLzw2GOP6ZRbpVLh2LFjZcbbsmVLnDt3Tift+vXrePDgAZo1a1at8ZK+rl27IjU1FXl5edq0w4cPo169egafKJNppKenY8KECfDw8MCuXbvKnPATw2dW1HBir6KSy+Vo1KgR/P39ARQ/EKhv3774z3/+g5ycHNStWxcrVqyAj48PwsLCzB2yZLi4uOCVV17BunXr4OLigs6dO+OLL77ADz/8gI0bNwJg3dUUzs7OiIiIwOrVq7VXzGzYsAEuLi548cUXAbCtTeXu3buIjY2Ft7c3BgwYgPPnz+ss9/PzY1uYUevWrdGnTx8sW7YMKpUKzZs3x9dff439+/djyZIlcHFxqTHtYa59r2vXrggICMCUKVMwe/ZsFBYWYtmyZejRowf8/PzMXu7SzNXmUq8HsV9WHRwcUK9ePe3xkS3UQ/PmzTFw4ECsXr0aMpkMrVu3xldffYWvv/4a69evN9n3o9TroTSZTIYJEybg3XffhZubG5588kns2LED9+/fx9ixYwEAN27cgEKhQGBgIABg0qRJmDFjBubPn4+BAwciMzMT69atQ9OmTTFkyBDLFcZGlG6PUaNGYceOHZg4cSKio6Nx+fJlJCQkYObMmXx+SDUr3RZLlixBTk4OFi5ciDt37uDOnTvadZs0aYJGjRqVnZlANcqaNWuEwMDActd56qmnhDVr1uik5ebmCgsWLBA6deokPPXUU8LUqVOFv//+uzpDlZyy6i4pKUno1auX4OfnJwwcOFD4+uuvdZaz7qyPWFurVCrhgw8+EJ5++mkhMDBQGDdunPDHH3/orMO2rrpPPvlE8Pb2LvPfvXv32BZmlpeXJ7z//vtCSEiI4OvrKwwZMkT48ssvtctrSnuYc9/LysoSpk+fLgQGBgqdO3cW3nrrLSE7O9ucxS2Xudpc6vVQ2uDBg4U5c+Zo/7aVesjPzxeWL18uhISECH5+fsKQIUN0jnVspR7EbNmyRejevbvQoUMHITw8XDh79qx22Zw5cwRvb2+d9Q8dOiQMHTpU8PX1Fbp37y689dZbQlZWlrnDrvHEjuPE2uPChQtCeHi44OfnJ/To0UPYuHGjOcO0CRW1hVKpFNq3b1/md+/mzZvLzV8mCP8+7paIiIiIiIiISAL4zAoiIiIiIiIikhROVhARERERERGRpHCygoiIiIiIiIgkhZMVRERERERERCQpnKwgIiIiIiIiIknhZAURERERERERSQonK2qAuXPnwsfHp8J/c+fOtXSo5QoNDdXG+vrrr1e47pgxY6o9JqVSiT179mDkyJHo0qUL/P39MWjQICQkJECpVGrXi42N1alrIjLcmDFjEBoaWu46Jce5Z555xkyRlW/37t3o2bOnQetyjCBb9emnn1Z4fHL48GEAwNq1a+Hj44MzZ84Y9Rk3b97UyW/Xrl1GbR8aGlrhGFTSihUrEBkZadRniKnMsdmIESO05TTHcRDZNnMdb1eXisYUzdhRUT809zEIjxn+p7alA6CqCw8PR3BwsPbvn376Cbt370Z4eDieeuopbfrjjz9uifCM0qpVK7z66qto2rSppUNBZmYmJk+ejAsXLqB3797o27cv5HI5Tp06heXLl+PkyZPYvHkz7O3tMWDAADzxxBPYs2cPfvzxR0uHTlRjvf/++3BwcLB0GACAU6dO4emnnzZoXY4RZOtKH5OU5OfnZ5LP6NWrF3r16oWAgACT5FeWU6dOoVevXlXO5/333zf62Gzq1KlQKBRYunRplT+fiIxjrmMQHjP8DycraoCgoCAEBQVp/1ar1di9ezcCAwMxZMgQC0ZmPA8PD0nELAgC3njjDVy+fBlbt27VOSEZM2YMNm3ahNjYWCxfvhxvvfUW2rVrh3bt2uH06dM2P6gQVScpjA8AUFRUhDNnzmDRokUGrc8xgmydOY5JfHx8qv0z/vnnH/z6668G9/3yVCbWbt26AQBWr15d5c8nIuOY6xiExwz/w9tAiER88803+P777zFx4kTRX04nTJiAVq1a4bPPPkNBQYEFIiQiS/r111/xzz//6FzVRkQ133fffQcXFxeTXQ1CRERl42SFjTl37hzGjRunvRpj/PjxuHDhgs46oaGh+O9//4vk5GT06dMHHTp0wPDhw3HhwgVkZmZi+vTpCAoKQrdu3bBixQoUFRVpt/Xx8UFcXBw2btyIZ599VvsZv/32W6Vj/uKLLzBkyBB06NABAwcOREpKSqXLBgDHjx/Hiy++iMDAQPTs2RM7d+7E/Pnzde5XPXjwIIDiy1bLsmnTJhw7dkwyl6QTAcW/+s2dOxc9evSAn58fwsLCsHz5cjx69Ei7zpgxYzB27FgcOXIE/fv3R4cOHTB06FAcOnRIL78//vgDkydPRseOHREQEICRI0fixIkTlV4vNTUVI0eORGBgIMLCwpCcnFzpss6dOxcDBw7ETz/9hPDwcHTo0AE9e/bEvn37oFKpsHz5cjz99NPo1KkTZsyYgfv37+vUwSuvvILDhw9j8ODB8Pf3x4ABA3D8+HHk5ORg4cKF6NSpE4KDg7Fw4UK9ScnU1FS0a9cO9evXN7jeiUzBlH18zJgxiI6OxsqVKxEUFITg4GCkpaUBAL788ktERETgqaeegp+fH0JDQ/H+++9rn9f08ccfw8fHB8ePH9eLccSIERg+fHg11kLFNPeir1271qB0Q506dQpdunRBrVrFh9A+Pj7YtGkTEhIS0KNHDwQEBGDMmDG4fv06/vrrL0RHRyMwMBChoaHYtm2bTl6l75X38fFBQkICEhMTERYWBj8/PwwaNAhffvllpWIlKktoaCjmz5+P5ORk9OzZE4GBgRg5ciS+++67crcTBAG7du3CCy+8gKCgIPj7+6Nv375ISEiAIAg6+Vf2XOL27duYOnUqnn32Wfj7+6N///7YtGmTzjqGxlHdMjIyMG/ePO05z/Dhw7XP4AGqdpxCxXgbiA05deoUXnnlFbRt2xbTp0+HUqnEp59+itGjRyMxMREdO3bUrnv48GF8/fXXiIqKgiAIiI+Px9SpU+Hq6oonnngCc+fOxddff42NGzeiZcuWeP7557XbJicnIycnB1FRUbCzs0NSUhJGjx6NvXv3olWrVkbF/Omnn+Ktt95CUFAQZs2ahevXr2PGjBmQyWQ6z7UwtGxHjx7F5MmT4e3tjddffx0ZGRmIiYmBk5MTnJ2dtfldunQJTZs2RcOGDcuMrVmzZkaVhcgcZsyYgV9//RWRkZFo1KgRzp07h4SEBDx48ADvvvuudr0//vgD06ZNw/DhwzFy5Eh89tlnmDZtGmJjYzFo0CAAQFpaGkaNGgUPDw+88sorsLOzw//93/9h4sSJWL58Ofr372/UeqmpqZgwYQK8vLwwY8YMKBQKLF68GDKZTHvSb6zMzEy8+uqrePHFFzF48GBs27YN8+bNw+eff47s7GxMnjwZV69exc6dO+Ho6Khzn/elS5dw7tw5REZGwtXVFRs3bsSMGTPQrl07ODo64o033sCPP/6I3bt3o1GjRpgyZYp229TUVJ2rKgytd6KqMmUfB4CzZ88iPT0ds2bNws2bN9GmTRskJydjwYIFCA0NxZtvvgmVSoVvvvkGW7ZsAQDMnj0bffv2xXvvvYcvv/wS3bt31+aXnp6O8+fP46233tKJOy8vDwqFQq88Li4usLe3N3U1VZvTp09j7NixOmnbt2+Ho6Mjxo8fj3v37mHz5s2YOnUqHjx4gB49eqBXr15ITk7G4sWL0bZtW3Tu3LnM/Hft2oWioiKMHj0aDg4OSEpKwuuvv47WrVvD29u7mktHtiQ1NRUHDhzAmDFj0LBhQ+zatQsvv/wytm7dWuY+umrVKmzYsAHPP/88RowYgdzcXHz22WdYvnw5nJ2dMXr0aO26lTmXUKlUePnll1FQUICxY8eibt26OH78OGJjY6FWq/Hqq68aHQcAZGdni44/Dx8+rHT9PXjwACNGjMCDBw8wevRoNG/eHP/3f/+HKVOmYN26dQgLCwNQteMUAiBQjfPJJ58I3t7ewieffKJNU6vVQs+ePYWRI0cKhYWF2vTc3FyhV69ewpAhQ7RpISEhgo+Pj3D58mVt2rJlywRvb29hxowZOtv6+voKb7zxhjbN29tbaNu2rXDx4kVt2h9//CG0b99eeP3118uNOyQkRIiIiND+XVhYKAQHBwvDhw8XlEqlXvk06xpTtrCwMKF3795Cfn6+Nu2bb74RvL29hZCQEG1aQECAMGLEiHLjFTNnzhzB29vb6O2ITCErK0vw9vYWNm/erJM+d+5cISoqSvt3RESE4O3tLSQmJmrT8vPzhV69egnPPvusoFarteuFhYUJubm52vVUKpUwatQo4emnnxYePXpk1HrPP/+80L17dyE7O1u73unTp/X6nxixvqVJ2759uzbt2LFj2vw0nysIgjBy5Ejh2Wef1auDI0eOaNN27NgheHt76/T9oqIi4bnnnhPCw8O1aQUFBYK/v79w4sQJQRAMr/eyykFkqOro497e3sLPP/+sk1/fvn2F8PBwoaioSJumUqmE5557Thg4cKA27ZVXXhE6duyo09c2bNggtG3bVvj7778FQfjfd3ZZ/0oeq6xZs0bw9vYWvvvuO6PqJT09XfD29hbWrFlTblpZ6SEhIRWOQYIgCDdv3hS8vb2FP//8U5vm7e0tBAQECJmZmdq0adOmCd7e3sIHH3ygTbt27Zrg7e0trFixQmfbOXPm6PwdGBgo3L17V5v2888/622nibnkMRORMUJCQgRvb2/hm2++0abdu3dP6Nixo853YMn9TKlUCk8++aTe8Xx2drbg5+cnvPLKKzrbVeZc4vz584K3t7fw5ZdfatcpKioSxo8fL8yePdvoODRjSkX/SvZDMWLf3e+//77g7e0t/Pjjj9q0goICISwsTBg+fLjOdpU5Tinrc20Nr6ywEb/++ivS09Px0ksv4Z9//tFZFhISgg8//BAZGRnw9PQEUPzmkJKvymnZsiUA6Dz92snJCQ0aNEBmZqZOfs888wx8fX21f7du3RrdunXDsWPHUFRUpL10siKXLl3CvXv3MGXKFNjZ2WnThwwZgpiYGKPLdv/+fdy4cQNz587VuXUjLCwMrVq10rmEtlatWlCr1QbFSSQVrq6ucHJywkcffYRmzZqhW7ducHJyEp2ld3V1xahRo7R/Ozg44KWXXkJMTAwuXryI5s2b4/vvv8eYMWNQUFCgcxtEr169sHTpUvzyyy9o1aqVQet5eXnh0qVLePnll+Hi4qJdp2vXrvDx8UFOTk6ly11yXPLy8gJQ/BC6kr/WNmvWDOfOndPZrk6dOtqH1QH/G+dKvo5UcxVXRkaGNu2nn36CIAjaK7aMqXeiqjBlH+/QoYM23d/fX2fbAwcOID8/HzKZTJt279491K1bF3l5edq0QYMG4ejRozh16hRCQkIAFN9G2alTJ+3xhEZ0dDSeffZZvTjbtGlTiZqwjNTUVDz22GPasUIjKCgIHh4e2r8141DJsUlzNebdu3fL/YynnnpK56rOdu3aAYDesRZRVbVq1Ur76z8AuLu7Y8iQIdixYwfu3buHBg0a6KxvZ2eH1NRUqFQqnfT79+/DxcVFZ2wAKncu0ahRI8hkMmzcuBHOzs7o0qUL7O3ttVd1VSYOAJgzZw7atm2rl56VlYVZs2aJV1AFjh07Bl9fX523HNWpUwcJCQmoU6eOzrqVPU4h3gZiM27cuAGg+JU777//vug6t2/f1h5clB6g5HI5gOKBrHS6UOreMLEDDy8vLxw9ehQPHjzQy6Mst27dAqD/ylW5XI4WLVpo/za0bJoDhJLbarRq1UrnuRoNGzYUvVyMSMrs7e3x3//+F//5z38wbdo02Nvbo3PnzujduzeGDh2q8+X5+OOP6116rekbmr4HFF/evH37dtHPu3PnjnYi0dD1xF7T16pVK9Hnyxiq5HilGavExrDSY1W9evVQu3Zto7c9deoUgoKCtJOextQ7UVWYso9rJivq1aun9yOCnZ0dfvjhB/zf//0f/vzzT9y4cQP37t0DAJ1bMENDQ+Hk5ISvvvoKISEhuHr1KtLS0vDee+/pxd6mTRuDX/UrVadOnRJ9qG7pMUMzrpQ83tGML6XHodJKHyNp2rDk/fpEpiB2vN6iRQsIgoBbt27p7ddA8dhw7NgxpKSk4K+//sL169e1PxSW3rcrcy7RuHFjzJo1CytWrMDLL78MJycnBAcHo3///ujXr582D2PiAABfX1906dJFL/3mzZvilWOAW7du6TzvTqP0ZCZQ+eMU4mSFzdB8yU2fPh2BgYGi65R8nkTJA/iSSv7KUpaSV0FoaK5SMPSqipKfJfa2jZJf2oaW7fbt2wAgem9s6ZOJoKAg7Nu3D5mZmWU+t2Lnzp1ITU3FG2+8gdatW1dcICIzGDRoELp164bDhw/j+PHjSE1NxcmTJ/HRRx8hOTlZu/+L9VNNX5LL5do+O3r0aJ1fXkpq06aNdmKjovU0VyZU1J8rQ2y8MmSsquw4d/r0afTu3VsnzdB6J6oqU/VxjZL/13j33XexY8cOtG/fXvvK0aCgILz77ru4c+eOdj1HR0eEhYUhJSUFSqUSX3zxBezs7PT6h5RUdrwRBAHfffcd5s+fr7esKsdMpRlznERUFeUdr4uNC4IgYNKkSTh69CieeuopBAUFITw8HJ06dUJUVJTe+pXtF9HR0Rg4cCC++eYbHD9+HKdOnUJKSgo+++wzbN682eg4qotarTa4j1f2OIU4WWEzNL+EODk56f2yceHCBfzzzz8me6uF5kqHkq5fv4569eqhXr16BufTvHlz7bYlaWZ8n3jiCQCGl02T37Vr1/QuRb127ZrO37169cK+ffuQnJyMSZMm6cVWVFSEPXv24OrVq3jnnXcMLhNRdcrNzcVvv/2GJ554Ai+88AJeeOEFKJVKfPDBB9i2bRtOnjyp/RXg5s2bEARB58tS0w9atGihfeClXC7X61d//PEHbt68CUdHR23/M2Q9mUym1581sViL+/fv47fffsPbb7+tTTOm3omqwpR9vCy3bt3Cjh07MGTIEL2rFbOysvTWHzhwIA4cOIAffvgBKSkp6NatG9zc3ExQ2qrRnGxp3l6iUdnbKX777Tc8ePCAryumGqOs43W5XC76EPkff/wRR48exaRJkzB9+nRtemFhIR48eKA9zq6KBw8e4PLly3jyyScRERGBiIgI5OXlYe7cuTh06BDS0tLw8OHDao/DEE2aNBGtw3379uGnn37CwoULzRJHTcfpWxvh5+eHhg0bYvv27cjNzdWm5+TkYMaMGXjrrbdEZ1Er48iRIzqXkV+5cgUnT540+peW9u3bo2nTpti1axfy8/O16QcPHtR5tY+hZfPz88Njjz2GvXv36hy8/Pzzz/j11191Pjs0NBSBgYHYtGkTzpw5oxfbmjVrcPnyZYwYMULnPlUiS/r999+1b97RsLe3R/v27QHo/lKSlZWl8zq8vLw87Nq1C15eXvDx8UGjRo3g5+eHffv26TyvQaVSYd68eZg2bRoKCwsNXs/d3R2dOnXCgQMHdE54zp07h0uXLlVLfVSH06dPw8XFBX5+fto0Y+qdqCpM2cfLormUuvQl4sePH8e1a9dQWFiok/7MM8/A3d0dycnJ+O233zBw4MDKF9CENLd5lX51emVfA3rq1Ck88cQT/M6nGuOXX37Bzz//rP07KysLBw4cQNeuXUUnHB88eABAf2zYs2cP8vPz9caGyjh16hSioqJw5MgRbZqTk5P2TThyudwscRjiueeewy+//IKLFy9q01QqFbZs2YKLFy/yikoT4ZUVNsLOzg4LFizA66+/jmHDhuGFF15AnTp1kJycjNu3byM2NrbMy7WMJZPJ8NJLL2HMmDFQqVRISkqCu7s7pk6danQ+//nPfzB58mSEh4dj+PDhyMjIwM6dO3Wu0DCmbHPnzsWMGTMwcuRIDBkyBAqFAtu2bdMbUGQyGVasWIGxY8di3Lhx6NOnDzp27Ij8/HwcO3YMP/zwAzp27Ig333yzyvVFZCoBAQHo2LEjVq5ciTt37sDHxwd37tzBjh070KpVK51fBO3s7PDWW2/h0qVLaNSoET755BNkZGRgw4YN2nUWLFiAqKgoDB8+HC+99BLq1auHgwcP4vz585g5c6b26gtD15szZw5Gjx6NESNGYPTo0cjPz8eHH35Y6deWWsLp06fRpUsXnUu1jal3oqowdR8X06ZNGzRp0gQbNmzAo0eP0LhxY1y4cAH79u1DnTp1dH4UAIovb+7Xrx927twJJycnk1xFlJiYiIMHD+qlBwcHo1+/fgbl4ejoiJ49e+LQoUOYP38+AgMD8d133+HcuXOil7/fv3+/zF9CJ02ahNOnT1v9MzeISrK3t8eECRMQFRUFBwcHfPTRRygqKsLs2bNF1w8KCoKLiwuWLl2KW7duwc3NDWfOnMEXX3whOjZURkhICFq2bIn58+fj0qVLePzxx/Hnn39i586dCA4ORps2bVCvXr1qj8MQr776Kg4dOoSoqChERESgUaNGOHjwIK5evYqtW7eaJQZbwMkKG9K3b1+4ubkhPj4ecXFxqFWrFp544gnEx8drn+JtCv369UPz5s2xefNmFBUV4ZlnnsGsWbPQqFEjo/MKCQnBxo0bsXbtWqxYsQKenp5YvHgxdu7cqbOeoWXr27cvVq5cifj4eHzwwQfw9PTEW2+9hc8++0zvgZpNmzZFcnIyPvroIxw6dAgnTpyAUqlEq1atMHfuXERERIge8BBZikwmw/r167Fu3TocPXoUu3fvhpubG3r37o3p06frTMo1atQI8+bNw7Jly5CZmQlfX18kJiaiU6dO2nWCgoKwa9curF27FomJiSgsLETLli0RExOD559/3uj1/Pz8sH37dixfvhzr1q1D3bp1MWXKFFy8eBFnz541TyVVUWpqKsaPH6+TZky9E1WFqfu4GHt7eyQkJCAmJgbbtm2DIAh4/PHHMW/ePBQWFmLx4sW4ePGiztVFgwYNws6dOxEaGgpHR8cql/Po0aOi6XXq1DF4sgIA3nnnHTg5OeGbb77BF198geDgYGzfvl306o+8vDzs3r1bNJ/hw4fjp59+Muv98ETVLTAwEAMGDEBcXByys7PRsWNHzJw5U/StGQDg4eGBhIQExMbGIj4+Hvb29mjZsiVWrFiBCxcuYNu2bcjKyqrS1UdOTk7YunUr1qxZg88//xxZWVlo2LAhRo0ahSlTppgtDkM0aNAAu3fvxvLly/Hxxx9DqVSibdu22Lp1K3+kMCGZwMeOkgn5+Pjg+eef13m1qKFCQ0PRtGnTMt8oUFVqtRr//POP6NtIBg0ahLp16+pNghhr7ty52LdvH9LS0qqUD1F1GjNmDG7duqVzmaXU1ZS+VVPKQdJm7j5+/vx5jBgxAgkJCejevbtZPrOkmzdvomfPnpgyZYrRV3Faq+o+ZqKajfuPcSz13c1jBj6zgmyIWq3Gc889p3eZZ1paGn7//Xfta9yIiIjIcB9//DEaNWqk9/BqIiKiquBtICQpWVlZ2L9/P5o2bYqOHTuaNG97e3v07dsXe/fuhUwmg5+fH+7evYtdu3ahfv36GDduXKXz/u2333DlyhWkp6ebMGIiKm3//v1wcHBAnz59LB2KUThGUE20YMECpKen47vvvsPcuXMt/jDZtLQ07N+/HwEBAfDy8rJoLNXlxIkTUCgUyMvLs3QoRDbHXMcgPGb4H05WkKT8+eefmD17Nvr372/yyQoAeO+999CyZUscOHAA+/btg6urK4KDgzFjxoxKPVND4+DBg9i0aZMJIyUiMbNnz4aHh4fVTVZwjKCa6N69e7hw4QLCw8MRGRlp6XDwzTff4JtvvsHbb79dYycr1q5di/Pnz1s6DCKbZK5jEB4z/A+fWUFEREREREREksJnVhARERERERGRpHCygoiIiIiIiIgkhZMVRERERERERCQpnKwgIiIiIiIiIknhZAURERERERERSQonK4iIiIiIiIhIUv4f2frwYSdFnj0AAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABB0AAAD1CAYAAAAcXY1iAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAABSL0lEQVR4nO3deVxU1f8/8BeMg6wiEzqKmiAfdxQwNyxSQbFEw9xTBI2wcq9Mzfq4VCqWay4opqaJppimqaWBaBqm+TGzLNHMBdEGcUBBwBmG+/vD38yXcYZlgOEOw+v5ePh4OOcu8z5z7zncec+959gIgiCAiIiIiIiIiKiK2YodABERERERERFZJyYdiIiIiIiIiMgsmHQgIiIiIiIiIrNg0oGIiIiIiIiIzIJJByIiIiIiIiIyizpiB1AeBQUF+OOPP9CgQQNIJBKxwyGyShqNBnfv3oWPjw/s7e3FDqdM7BeIzI/9AhE9qab1C0QkvhqRdPjjjz8wevRoscMgqhXi4+PRuXNnscMoE/sFourDfoGInlRT+gUiEl+NSDo0aNAAwOPOrVGjRiJHU3n379+Hq6ur2GFQKWrjMfr3338xevRoXXuzdKb0C7XxeBrDz+Exfg6PledzYL9A1YHHwnJYY79AROKrEUkH7S2SjRo1QtOmTUWOpvLc3d15O5qFq83HqKbckmxKv1Cbj2dx/Bwe4+fwmCmfA/sFMiceC8thjf0CEYmPA0mKQCqVih0ClYHHyLrweD7Gz+Exfg6P1fbPobbX35LwWFgOHgsiMgcmHUSgUCjEDoHKwGNkXXg8H+Pn8Bg/h8dq++dQ2+tvSXgsLAePBRGZA5MORERERERERGQWTDoQERERERERkVkw6UBEREREREREZsGkAxERERERERGZBZMO1Uil1gAAPDw8SlxW05QWd02uk7FjpF1GVFO5PWV8TvXadl7zcyCi2qy0vq6k/pGIqDLqiB1AbWInlWDgO/uMLvt2aVg1R1M1WCeqCWQymdghWASHulKj53ZtO6/5OTxW29tFba+/JeGxqF68ziGi6sY7HYjI6nHecSJDtb1d1Pb6WxIeCyIi68akAxFZvczMTLFDILI4tb1d1Pb6WxIeCyIi68akAxFZPblcLnYIRBantreL2l5/S8JjQURk3Zh0ICKrl5OTI3YIRBantreL2l5/S8JjQURk3Zh0ICKrxwtaIkO1vV3U9vpbEh4LIiLrxqQDEREREREREZkFkw5EREREREREZBZMOhARERERERGRWTDpQERERERERERmwaQDEVk9iUQidghEFsfc7SIpKQn+/v56ZYIgIDY2Fr169YKvry/GjRuHq1evlrmvs2fPYtiwYfD19UVISAh2795d6fjYL1gOHgsiIuvGpAMRWT3OAU9kyJzt4ty5c3j33XcNytesWYPY2Fi8+uqrWLZsGXJycjB27NhSZy+4evUqXnvtNTRt2hSrVq1Cr1698P777+P777+vVIzsFywHjwURkXVj0oGIrB6nYyMyZI52oVKpsGHDBkRERKBOnTp6y3Jzc7Fx40ZMmjQJERERCA4OxsaNG/Hw4cNS71yIi4tDkyZNsGzZMjz//POYPXs2XnrpJaxZs6ZSsbJfsBw8FkRE1o1JByIiIqoSP/74I+Li4jBjxgyEh4frLfvtt9+Ql5eH4OBgXZmrqyu6du2KEydOlLjPlJQU9OrVCzY2NrqyPn364PLly1AoFFVfCSIiIqpSTDoQkdVzcXEROwQii2OOdtGhQwckJSUhIiJCL0kAANevXwcANGvWTK+8adOmumVPysvLQ0ZGBpo3b65Xrt1HSduVB/sFy8FjQURk3Zh0ICKrx19DiQyZo13I5XLUq1fP6LLc3FzY2dnBzs5Or9zJyQm5ubklbqNd58ltii+vCPYLloPHgojIutUpexUioppNo9GIHQKRxanudiEIgsHdD1ollQuCUOpyW1vjv50Uf4QDACIjIxEVFQW5XK4bP0Cj0UChUBj9HFxcXODi4gKFQgF3d3eo1WoolUqj7yWXy6FWq5GXlweZTAalUomCggKD9ezt7XXLHR0dIZVKS/yyLZPJIJVKkZmZqYvZ2LgHEolEr07amGtanXJycnQxW0udirO0Otnb2xvdn5ZGoym1ThkZGaVuT0T0JCYdiIiIyOxcXFygUqmgVqshlUp15Q8fPizx9npnZ2fdOsVpX5e0XVJSEpo2bVpiHMDjL7plzZqgXS6RSODh4VHiehKJRPdFTiaTlbrP4stL22fx99d+ES1J8WU1sU63b9822K6m18kYS6pTabRJEm3MT9apqKioXPshItLi4xVERERkds2bN4cgCLh165Ze+a1bt+Dl5WV0GycnJzRo0ABpaWl65drXJW1HREREloNJByKyOKdOncKw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YExSUhKaNm1qdFlpyYYn19H+Ai+RSODh4VHi+hKJRPdFrqwZdoovL22fxd9f+0W0rHiLb1PWPi2pTkql0uA9anqdjLGkOpVGmyTRxvxknYqKisq1HyIiLT5eQURERNVm2bJliImJQVhYGD777DPY2dmVuO7Bgwfxyy+/6JUJggCVSgU3Nzdzh0rVhFMhExFZN97pQERERNViy5YtWL9+PSIiIjB79uwS73DQ2rFjB3Jzc7Fnzx7Y2j7+neT48eMoKChA586dqyNkqgbG7nQgIiLrwaQDERERmV1GRgaWLFmCVq1aITQ0FL/99pvech8fH9y+fRtKpRJ+fn4AgNdffx3R0dF49913MXjwYFy/fh0rV65Ev3790KlTJxFqQeZgbBwCIiKyHkw6EBERkdmdPHkSKpUKly9fxogRIwyWnzp1CmvXrsXevXt14zUEBgYiNjYWa9aswcSJE+Hs7IwhQ4Zg6tSp1R0+ERERVRCTDkRERGR2gwcPxuDBg0tdJyYmBjExMXplvXv3Ru/evc0ZGhEREZkRB5IkIiIiIiIiIrNg0oGIiIiIiIiIzIJJByIiIiISjb29vdghEBGRGTHpQERERESi4XSZRETWjUkHIiIiIhKNUqkUOwQiIjIjJh2IiIiISDSOjo5ih0BERGbEpAMRERERiUYqlYodAhERmRGTDkREREQkGoVCIXYIRERkRkw6EBEREREREZFZMOlARERERERERGbBpAMRERERERERmQWTDkRERERERERkFkw6EBEREREREZFZMOlARERERKKRyWRih0BERGbEpAMRERERiUYqlYodAhERmRGTDkREREQkmszMTLFDICIiM2LSgYiIiIhEI5fLxQ6BiIjMiEkHIiIiIhJNTk6O2CEQEZEZMelARERERKJh0oGIyLox6UBEREREREREZsGkAxERERERERGZBZMORERERERERGQWTDoQERERERERkVkw6UBEREREopFIJGKHQEREZsSkAxERERGJRi6Xix0CERGZEZMORERERCQaTplJRGTdmHQgIiIiIiIiIrNg0oGIiIiIROPi4iJ2CEREZEaiJx127dqFkJAQdOzYESNGjMCvv/4qdkhEJDL2C0TWy9T2ffnyZURGRsLf3x+9evVCXFwcBEGopmipOigUCrFDICIiMxI16bB3717MnTsXL730ElatWgUXFxdE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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df = df_film.iloc[:,1:-1]\n", - "df_cols = df.columns\n", - "n_col = 4 # num of columns per row in the figure\n", - "\n", - "for n in np.arange(0, 8, n_col):\n", - " fig,axes = plt.subplots(1, n_col, figsize=(18, 3.5), sharey = False)\n", - " fs = 20\n", - " for i in np.arange(n_col):\n", - " if n< len(df_cols):\n", - " axes[i].hist(df.iloc[:,n], bins=30)####\n", - " axes[i].set_xlabel(df_cols[n], fontsize = 18)\n", - " else:\n", - " axes[i].axis(\"off\")\n", - " n = n+1 \n", - " axes[0].set_ylabel('counts', fontsize = 18)\n", - " for i in range(len(axes)):\n", - " axes[i].tick_params(direction='in', length=5, width=1, labelsize = fs*.8, grid_alpha = 0.5)\n", - " axes[i].grid(True, linestyle='-.')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Visualize the partial/marginalized dependence as scatter plot" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The color is for variable speed [mm/s] with red as the highest and blue as the lowest\n" - ] - }, - { - "data": { - "image/png": "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EEknHRQZbPHixYwdO7bVz6uqqpgyZQrz589vsTwUCnHzzTdzyCGHMHbsWH7+859TVlYW73DTWmtl8dprr3HCCScwatQojjrqKB555JEWn0tZtN/e8lgpxT//+U+mTZvGgQceyE9+8hPWr1/f4jupksd7S9/q1aspLi7e4+/WW29t/o7d02eaJg8//DCzZs1izJgxHHvssTz66KM0PfyXDmX4Y2lMh3IUexcKhbjjjjuYPn06Y8aMYe7cuXz11VfNn8eqftfW1jJv3jwmTZrEhAkTuOGGG2hoaEhIGhMhFAoxa9Ys5s2b17xM8q5t1dXVe21Xfv7znwOZnX9PP/00Rx11FKNHj+bMM89kxYoVbX7/888/5+yzz2bs2LEcccQR3H333YTD4QRFmxl+7Jqlydq1azn//PMZO3Ys06ZN4/7770deFoit9pbF559/znnnncf48eOZOnUq1113HRUVFe3biBIZ6bPPPlNjx45VY8aMafU7V199tRo2bJi66667WiyfN2+emjhxonruuefUG2+8oY488kh14oknqkgkEu+w01JrZfHaa6+p4uJideutt6ply5ap22+/XQ0bNkw9//zzzd+Rsmif1vJ4/vz5atSoUWrBggVq0aJF6tRTT1VTp05VdXV1zd9JhTxuLX3PPPOMGjNmjFqxYkWLv23btjV/x+7pu+uuu9TIkSPVP/7xD7Vs2TJ11113qf3331/df//9Sqn0KMMfS2M6lKPYuz/84Q9q7Nix6rHHHlNLly5Vl1xyiTrooIPU1q1blVKxq9/nnXeemj59unr99dfV888/ryZPnqwuueSShKc3Xm677TY1bNgwdf311zcvk7xr27Jly9SwYcPU0qVLW7QrGzZsUEplbv49//zzavjw4Wr+/Pnq3XffVRdeeKEaO3as2rx5816/v2nTJjVmzBj105/+VC1ZskT95z//UaNHj1a33HJLgiNPX+25ZlFKqYqKCnXwwQer888/X7377rvqnnvuUfvvv7964IEHEhRp+mtvWXz33Xdq1KhR6tJLL1Xvvvuuevnll9URRxyhTjzxRBUKhX50O9JBkWGCwaC6//771YgRI9SECRNarWCLFy9WEydOVKNGjWrRQbFp0yY1fPhw9dprrzUv27BhgyouLlYLFy6Me/zppK2ysCxLHX744eqPf/xji99cffXV6pprrlFKSVm0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o0eP5owzzuCTTz7pcj4UFxdzww03sHz5cubMmcOBBx7I1KlTuf322zFNk++++44LL7yQsWPHcuihh/KnP/0Jv9/f/PuPP/6Y4uJiXnrpJZ5++mlmzZrFqFGjOOaYY3jppZcAWLx4MaeccgoHHnggRx99NI899tgecYTDYT766KMWFyubNm3iF7/4BdOnT2fkyJHMmDGDP/zhD5SXl3c53SJ9xWPfbvruqlWrmvfts846C6UUAO+88w4XXXQRkydPZsSIEUyePJkrrriixfvov/nNbyguLmbZsmV7xPzJJ59QXFzMHXfcEYcc6Zymsa2uuOKKPT7rylgPTb788ktqamqaX+mKRVuyt7iKi4uZN28en3/+Oeeddx5jx45lwoQJ/PKXv+zQeBki83T0+L83L774Iueddx4TJkxg5MiRTJ06da/nBDNmzOCCCy5gzZo1zcfcSZMmceONN+L3+ykrK+OXv/wl48aNY8qUKfzP//wPVVVVLdbx5ZdfcumllzJ16lRGjRrF0Ucfzd///ncaGhpafC8cDrNgwQLOOOMMxo0b1zwe3I033rjHOhPFNE0efvhhTjzxRMaMGcPhhx/Otdde2yKfmsa6WL58Offff3/z+dBJJ53EkiVLAHj22WeZNWsWBx54ICeccAJvvvlmUtJjJ/KKRxqpr69nzpw5rF27lilTpnDUUUexdetWnn76aZYsWcKTTz5Jr169Wvzml7/8JbW1tRx33HFs376dhQsXctFFFzFjxgzeffddjj76aILBIC+//DKXXXYZb775Jr17927+fSAQYO7cuTgcDs466yzWr1/Pyy+/zMcff8wzzzzT4rvtVVpayllnnUVFRQUzZsygqKiIJUuWcNFFF+31+9dffz0vvfQSQ4cO5ayzzsLv9/PGG29w1llncd999zFlypQW6X377bcpLi5mzpw5bN68mRtuuIEBAwa0WOfy5csJBAIceuihbT7G6Xa793oiJEQ8/OlPf+LJJ59k4sSJHHbYYdTX1/P6669z4YUX8vDDD+/xmPDFF19MeXk5p556KqWlpbz99tssX76cRx99lOHDhwPRg/7FF1/MRx99xOjRozn33HOprKzkjTfeYOnSpTzyyCMMGzaseZ3t3d8sy+Liiy9m+fLljB49miOPPJIvv/ySn/70p2RlZXU5L1auXMlLL73EtGnTOPvss3nrrbe47777qKys5K233mLkyJGcffbZvP/++zz66KMYhsFvfvObFut4+OGH2bRpE8cddxyTJ0/mhRde4LrrruPbb7/lkUce4ZhjjmH8+PG8/PLL3HTTTfTu3bt5vBmIvubV2NjIIYccAvzwWlh1dTVHH300vXr1Ys2aNTzxxBN8/PHHvPzyyzidzi6nXaSfeOzbTS6//HJGjRrFIYccgtfrRdM0Hn30Uf70pz8xcOBAjj/+eJxOJ19++SWLFy/mo48+4s0336RXr16cdNJJPPfcc7zyyiscfPDBLdb78ssvA9FBbzPFkiVL6N69+x6vlHS1Ldmbr776irlz5zJu3DjOPvtsVq1axRtvvMHq1at5/fXXcblc8UyqSHEdaSN2deutt/LQQw8xfPhwTj75ZDRN45NPPuHVV1/ls88+480338Tj8TR/f+vWrZx99tmMGTOGs846iyVLlvDUU09RU1PD6tWr6dGjB2eccQYrVqzglVdewe/3c8899wCwYcMGfvKTn6DrOscccwx5eXmsWLGCf/3rX3z55Zctxmm65pprWLhwIePGjeOMM84gFAqxdOlSnnrqKb766iuee+65+GXmXliWxaWXXsqSJUvYb7/9OO2006iurub111/no48+4tlnn21xDXTzzTdTWlrKcccdh8/n46WXXuLyyy/nrLPO4rnnnuPYY49l8uTJvPjii/zqV79i4MCBHHDAAQlNk51IB0Uauf3221m7di033nhjizv6ixcv5oorruB///d/ufPOO1v8pq6ujpdeeql5sM1rrrmGV199lbfeeovXX3+9eefq168f8+fPZ/HixcyZM6f597W1tQwcOJBHH320ucF66KGHuPXWW7njjju45ZZbOpyOO+64g/Lycm655RZOPvlkIDqTxqWXXrrHXcg33niDl156ieOPP55bb70VhyNapS+55BJOO+00rr/+ehYtWoTL5WLhwoW8/fbbzJw5k//7v/9rvlB47LHHuOmmm1qst7S0FIDBgwd3OH4h4qGhoYGnn36aCRMm8MgjjzQvP/300znttNN47LHH9riIqamp4aWXXqKwsBCAhQsX8vOf/5z//d//bV7HggUL+Oijj7jooou49tprm3973nnncfbZZ/Ob3/yGZ599FujY/vbCCy+wfPlyTj31VP785z+j69EH9v7617/y4IMPdjk/1q1bx69//evmd7DPOOMMZs2axbPPPstPf/pTrr/+egCuuOIKDj/8cF599dU9OijWrl3L008/zciRIwEYPnw4N954Iw899BD33Xcf06ZNA6LvgJ533nm8+uqrLS4qli5dSu/evRk6dCgAr7/+OiUlJdx8882ceuqpzd+76aabeOyxx/jggw+a1ylEk3jt200OOuigFiPVh0Ih7rjjDgYPHswLL7zQohP+D3/4A0888QTvvPMOZ555JhMmTKBfv368/fbb/PGPf2y+KA6FQixcuJBRo0ax7777ttjeokWLWh0P4fzzz28xuHeqWbp0KQcffPAer3x2tS3Zm7Vr13Lttdc235xRSnHRRRexdOlSPvroIw477LDYJ1CkjY60EU3Kysr497//zYQJE1iwYAGGYTR/dskll/Dee+/x6aefthgUesuWLcydO5cbbrgBiHaIHnbYYSxcuJBjjjmG//u//0PTNEzTZNasWSxatAi/309WVhZPP/009fX1LFiwgMmTJzev89JLL+Xdd99l3bp1DB06lC+++IKFCxdywgkn8Pe//735e5FIhJNPPpnVq1ezYcOGPQaaXbBgQavtTVdnLnn++edZsmQJxxxzDH/729+a28bp06dzzTXX8K9//Yvf/va3LfLplVdeoaioCICePXty33338fjjj/P88883dxqNHj2aefPm8dprr0kHhUh9kUiEF198kaFDh+7xusERRxzBQQcdxNtvv01DQwM5OTnNn51yyiktdt6DDjqIV199leOOO65Fz9/o0aMB9nrScfXVV7foTT3//PN59NFHWbhwITfddFOHevlDoRBvvfUWQ4cObe6cAPB6vfzP//wPZ5xxRovvN1043XDDDc0XSwADBgzgrLPO4t5772XZsmVMmzaNF154AYjeAd71LubZZ5/NI488woYNG5qX1dfXA5Cdnd3u2IWIJ8uyUEqxfft2ysvL6dmzJwCjRo1i0aJF9OnTZ4/fXH755c0nJwBHH30048aNY/ny5ZSVldH7/9u796AoqzcO4N8VWAG5rYYoKFgg0CxBiKLOBIqVNIsiC2pNmSVjMhqZaI41WVSYIgN4AcE7jkobqIgRV7mILIEiESoCyiVA8YoCKiK57O8P5n1/LLsrLBdBfD5/1bvvvnsW9zz7nmfPeY6REY4fPw49PT34+fnJPJeZppyQkMDeJKjS3xITE8HhcLBu3To2OQF0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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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2uTz+TBp0iTMnTtX7VCIATS1noeL2tqnh300nQtWpZundYSrV199FW+88Qbuuece9O/fH9u3b8dzzz0Ht9uNO+64AwsXLsQbb7yBhx9+GMnJyXjttddw66234uuvv0ZUVJTa4eve888/jy+++AIPP/wwunTpgv/85z+YPXs2VqxYgeTkZMpPmLnQ9nbgwAE4HA4sXry4yufatm2rUsQt48CBAwCAd955BxEREcHlsbGxAGCYft2rVy98/PHHVZZ5vV7cd9996N27N9q3b48ff/xRt31ix44deOSRR2osb0j+n3rqKXz33Xd47LHH4HA4MG/ePNx5551Yvnw53avdAi5Uu4g2vfLKKzh27Bj69eundihE55pTz8NBXe3Tw3EbnQtWw4lqAoEAz8jI4PPnz6+y/K9//SsfOnQodzqdvH///vz1118PvlZUVMQzMjL4O++808rRGk9JSQnv1atXlXXtdrt53759+cKFCyk/YeZC2xvnnD/77LN8xowZKkTXuhYvXsyHDx9e62tG79fPPvssHzJkCM/Pzw/+W299wuv18jfeeIP36tWLDxo0iPfv3z/4WkPyf/LkSZ6ens6/+uqr4HuOHz/O09LS+KpVq1qvIQbRkNpFtGfv3r28f//+fMiQIfyxxx5TOxyiU82t51pXX/s4D/99NJ0L1kS3daiotLQUU6dOxRVXXFFledeuXVFQUICffvoJLpcL48aNC74WExODwYMHY+PGja0druHY7XZ88sknuOaaa4LLTCYTGGPw+XzYtWsX5SeMXGh7c7lcOHjwINLS0lSKsPXU104j9+sjR47ggw8+wAMPPID4+HgA9a+rcLVhwwa88cYbePTRR3HTTTdVea0h+f/pp58AAKNHjw6+JyUlBT169NB9H1FDQ2oX0ZZAIIDHH38ct99+O5KSktQOh+hYc+u51tXXPiD899F0LlgTDU6oKCYmBk8++SR69uxZZfn333+Pdu3aBe/f7dSpU5XXO3bsiBMnTrRWmIZlMpnQs2dPxMTEQJZlnD59Go8//jgYY7j66quDOaD8hIcLbW8OhwOHDh1CdnY2pkyZgt69e+Pyyy/HZ599plLELefgwYNwu92YOXMm+vTpg1GjRuGtt94C59zQ/Xr+/PlISUnBddddF1ymxz7Rp08frFu3DrNnzwZjrMprDcn/8ePHkZCQAIfDUed7SOg0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3LhxOHXqVJU6QggJDwUFBdi4cSMGDx6MSZMmAQCWLl1a72fUOmeouHXj7rvvDg5MAEBCQgJmzpwJn89n6DpEgxMaderUKTz44IMYPnw4MjIycMcdd+Do0aO4/PLLa9z7eebMGTz11FMYP348+vTpg4yMDFxzzTX473//W+N7V65ciZkzZ2LQoEHIyMjAtddeiw8//BCc89ZqWovx+/346aefcOmllwaXpaWl4YknnsDWrVsxa9Ys9OvXDyNHjsS8efMgSRKOHDmC22+/HRkZGbj00kvxf//3f3C73cHP1zbnxM0334yxY8ciJycHDz30EIYMGYJ+/frhxhtvbLF7YokxVdxznZ+fj0ceeQQDBw7E4MGDcc899+Dw4cMN+o7PP/8cN998MwYNGoTevXtj5MiReOihh3D69Okq78vLy8Pjjz+Oyy+/HH369MHIkSPxyCOP4OTJk1XeN3bsWNx66604ePBgcNsZMmQInnzySbjdbuTm5uKBBx7AgAEDMGzYMDz88MMoKCio8h1+vx9LlizBddddhwEDBqB3794YM2YMnnzyyRrvvRCz2RysiV9//XVw+apVq+B0OnHVVVehU6dOdX7+4Ycfxl//+ld07969UX8XAKKiosAYqzJvRWlpKSIjI4P/XrhwIZKTkw1zrygxhl9//RV33XUXRo4ciT59+mDChAn45z//idLS0hrvXbt2LW6++WYMHDgQQ4YMwa233opt27Y1+W83d79e2eeffw6z2YwhQ4bgyiuvBAB8+umnTY6NkHBTVlaG5557DhMnTkSfPn0wbNgwzJkzB3v37q3x3szMTDzxxBMYNWoU+vXrh9/85jd455134Pf7m/S3586di549e6KwsBB//vOfMXToUGRkZOD222/HqVOn4PP58I9//AMjR47EJZdcgptvvhkHDhyo9bu+/PLL4EBjv379kJycHDwOqE1t5wwAsG3bNtx1110YMmQIBgwYgJkzZ9aYM665NWjEiBGYM2cO+vTpUyMui8UCAA2eD0uP6LYODTp58iRmzpyJoqIijB8/Hh07dsT333+PWbNmQZZltGvXLvjezMxMTJ8+HW63G5dffjnat2+P3NxcrFq1Cn/9618hSRJuuukmAMBXX32Fhx56CCkpKZg2bRoEQcC6devw9NNPo7CwEH/4wx/UanJI/PLLLygrK8OIESOqLN+1axe++OILjB49GjfccANWr16N119/Hfn5+Vi9ejV69+6NG264ARs2bMD7778PURTx+OOP1/u3ysrKMGvWLNjtdkydOhV5eXn4+uuvcfvtt+Ozzz5Djx49WrKpxGDuuOMOnDt3Dtdeey1ycnKwZs0abN26Fe+//36VX+2re/HFF/HOO+8gPT0d06ZNA2MM27Ztw8qVK/Hzzz/j22+/hc1mg9frxR133IFDhw7h8ssvx8SJE3Hq1Cl89dVX2LRpE7755hvExsYGvzczMxM33HAD+vfvj5kzZ2Ljxo34+OOPUVRUhD179iAhIQHXXXcddu7ciS+//BJutxsLFy4Mfv6hhx7CqlWrMGDAAFx33XXw+XzYtGkTPv74Y+zduxfLli1r1PqpuCRzx44dwWUVV0JUP/Co7bNNvaTTYrGgZ8+eWL58Oa688kqUlZVh9erVwctIT548ic8++wx///vfIYpik/4GIVpz/Phx3HbbbRAEARMnTkR0dDR27tyJN998E7/++iuWLFkSfO/rr7+OefPmoU2bNpgwYQKsVitWrlyJW2+9FW+88UaN/XVDhWK/fujQIezfvx9jxoxBdHQ0Bg0ahLZt22LNmjUoLi6udQ4bQvTmgQcewIYNGzBmzBiMHz8+eDy7adMmLF++HBdddBEAZXu5+eabUVxcjNGjR6Nr167YunUrXnzxRRw8eLDJk0pyzjF79mzIsoxp06bh0KFD2LRpE+666y506dIFhw4dwsSJE3Hu3Dl8++23uPPOO7Fq1aoqVxwAyi0doihiwoQJAJT5od58802sWLGiym2WFWo7Z/jiiy/wpz/9CTabDePGjUNcXBxWrVqFP/zhD3juuedw7bXXBt/bnBpUMf9VbSoGQpryg4lucKI5d911F09NTeXffPNNcJnX6+U33HADT01N5TfddFNw+V/+8heemprKN2/eXOU7du3axVNTU/n1118fXDZt2jTev39/7nQ6g8ucTicfMWIEHzp0KJdlmXPO+enTp3lqaiofM2YMf/nll2v976effqry9yre3xQ//fQTT01N5c8++2yV5S+//DJPTU3ly5Yta9D3zJs3j1966aU14kpNTeWLFy8OLjt69Ghw+QsvvBBc7nQ6+SWXXMKHDRtWb2w33XQTT01N5b///e+5z+cLLn/ttdd4amoq/8c//lFrO9asWdOgdhBS4bHHHgtuW/n5+cHl3377bY1aUPHeffv2cc45z8nJ4enp6fzGG2/kgUCgyvfecccdPDU1lW/cuJFzzvl3333HU1NT+UsvvVTlfW+99RZPTU3l77//fnDZmDFjamwTxcXFvF+/fjw1NZXfd999wVoSCAT45ZdfzlNTU7nL5eKcc75z506emprKH3rooSp/y+/388mTJ/PU1FR+7NixGn+vPi6Xi6empvLBgwcHl02fPp2npqbyvXv31vvZhqprO/7pp594//79gzVl5MiR/MSJE5xzzh988EE+efLk4PrgnHNJkur8G6mpqfzqq68OSbyEhELF8UDlWvPCCy/w1NRU/uOPP1Z575133slTU1P5oUOHOOecHzt2jPfs2ZNPnDiRnz17Nvi+EydO8P79+/PJkyfX+7er17QKzd2vV2/HihUrgsuee+45npqaypcsWVJrTBX7/+Li4npjJ0SLqh9XHzx4kKempvJHH320yvu++eabGtvSrFmzeFpaGl+1alVwmSzL/Le//S1PTU3le/bsqfPv1nWcX7GNz5gxg3u93uDy66+/nqempvKxY8dWOWeZO3cuT01N5f/73/+qfM+hQ4d4amoqv+2224LL9u/fz1NTU/nUqVNrjan6OUNRUREfMGAAHzZsWJVjkPz8fD5y5Eg+ePDg4DF/qGpQdcuXL+epqal88uTJNY4VjHR8QFdOaExBQQHWr1+PgQMHYuLEicHlFosFDz/8MG644YYq77/66qvRr18/DB8+vMryvn37wmazIT8/P7iMcw6Px4PDhw8jIyMDABAZGYlPP/0U0dHRNSaAOnPmTJ0TsMyZMwdDhgxpVltDbePGjbXOuGuxWDBr1qzgvy+66CLExcWhsLAQv/3tb4PLIyMj0a1bN+zatQsejwc2m63ev/fb3/62yj3kl112GebPn0+TZZGQ+/3vf1/laRITJkzAgAEDsHXrVuTm5iIpKanGZywWC/7+97+je/fuNX61HzRoENavXx+sDxWPvjx48CC8Xm/w/stZs2bhyiuvrHK1VoWKCeIAZVLHbt26Yc+ePbjtttuCtUQURfTq1QsnT55EVlYWunXrhnbt2uGFF16ocbWCyWTCgAEDcOjQIeTn56Nr164NXj8Vl0FWvqS84lF/ERERDf6ephgyZAhWrFiBDRs2wGq1Bn9tOXToEL7++mssWLAAjDEsWrQIb7/9NtxuN8aO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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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n9ckO+DBccB2XBwHcgIefN6mu5ptQyji4tm7iIBhGJgG1GgZwWbpuiCp0KnAjwvYtottO1imSUHOwQuIrutSU+vg3fs1wL7HHKvVv0VEREQO0OzIiZEjR7boANXV1RiG0eQ3laWlpYc18qKsrIxf//rXdOrUidNPPx2o/2Nv/vz5DB8+/Bvns5BDd7Bn/eWbGYZBj+IcyveECEVssnwWHfMDGIbRZG49HoMeXTxsKosRDEA0Vr+9e5eUr/KbtjLluqC+llyHm+/8HD+GYbCnun5CzI75Afy+gy9NahgGfbp7WbspQjBgErPrR1X1UP8WEREROUCr/kKKRqPMmDGDv/3tb+zevRuAjh07csUVV/C9730P04zPCqUjRoxg2LBh/OpXv2LPnj106tSJv//976xevZqZM2fG5RzSWDQaxbIO/ge3HJxpGnTukHXA9uZye/WEfB5/fg8btkUJBkwmn5NHpw66eWlOplwX1NeSKx75zsv2kZd9aI9bXX5OPk++sIfPN0cJ+A0uOzuP0s7ew4pDREREJBO16g7oN7/5DfPmzeOiiy6iV69ehMNhPvroI+69914qKiq4/fbb4xKcZVk89NBD3HPPPTzwwAPs3r2bo446iscee4yjjz46LueQxmprawkEAqkOIyM1l9v8HIv/nFyIbbuY5jevcNPeZcp1QX0tuVKV79ygyY2XdFD/FhEREfkGza7WcTBDhgzhzjvv5Nxzz220/dZbb+WNN95gyZIlcQuwpTRTuEhytKW+1pZiFWnL1NdERETkcDX7/MVFF13EokWLmnzNMAxqamoO2F5bWxu3RzokNSoqKlIdQsaKV27XbHaY9YbNrDdsPtuiifXaKvW15Er3fK/a5PDMGzZ/f9Pm823q1yIiItL+NPtYR48ePZgyZQrHHnssN998M8OHD2947cILL+TOO+/k7bffplevXsRiMT7++GM++OADbrzxxqQELokRCoVSHULGikdu1251mP+hg2UBLsz/sH6lj95dVBRsa9TXkiud8716s8OCjxwsE3Dh5fddzhtl0KOzHgERERGR9qPZO5p77rmHOXPmkJeXx+WXX84111zDsmXLAPjZz37GT37yEzZv3sxTTz3F008/TU1NDT//+c+56aabkha8SHuzYmP9c+s+D/i8X20Tkbbr0w0uptG4X6/cpNETIiIi0r4cdELMAQMG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aUvXBREREUlPhuu6bqqDiIf333+fyy67LNVhiLQbTz/9NMOHD091GAel64JIcrWF64KIiIikp4wpToRCIZYvX06nTp2wLCvV4YhkLNu22bFjB0cffTSBQCDV4RyUrgsiydGWrgsiIiKSnjKmOCEiIiIiIiIibZMmxBQRERERERGRlFJxoh35+9//zhlnnMHgwYO55JJLWLp06UH3f/HFFzn33HMZNGgQZ511Fs8880ySIm3bFi5cyNChQ79xvzVr1nDllVcydOhQxowZwyOPPIIGMmWOQ+1v+3vwwQfp379/AqPLPIea74qKCm677TZGjhzJ8OHDuf7669m4cWOSohURERGRr1Nxop2YM2cOv/rVrzj//POZNm0aubm5XHPNNWzatKnJ/V944QV+9KMf0bdvXx566CEuu+wyfv/73/PHP/4xyZG3LR9++CG33nrrN+63c+dOrr76agzD4L777uPb3/429913H48++mgSopREO9T+tr81a9YwY8aMJESZOQ4139FolKuvvpply5bxm9/8ht/97nds2rSJKVOmEIlEkhy9iIiIiIDmnGgXXNdl3LhxnHTSSfz6178G6v84P+usszj11FP5+c9/fsB7zjvvPLKzs5k5cyaGYQDwzDPP8Nvf/pbXX3+dDh06JPUzpLtIJMITTzzB/fffTzAYJBqNHvSb2wceeICnn36a119/naysLADuu+8+Zs6cyaJFi/B6vckKXeKsNf1tH9u2ueSSS/jyyy8pKytj9erVyQq7zWpNvp999lnuvPNOXnrpJUpKSgBYuXIlU6ZMYcaMGRx99NFJ/QwiIiIiopET7cKGDRvYsmULY8eObdjm9XoZM2YMb731VpPvWbduHSeeeGJDYQJg2LBhhEIh3nvvvYTH3Na8+eabPPLII9x2221cfvnl37j/O++8w/HHH99QmAA47bTT2L17N5988kkiQ5UEa01/2+fxxx+npqamRW1I6rUm3wsWLOCkk05qKEwADBw4kEWLFqkwISIiIpIiKk60A+vXrwegZ8+ejbZ3796djRs3Ytv2Ae/p2rUrW7dubbRt8+bNAGzZsiUxgbZhgwYNYuHChVxxxRWNCjrNWb9+fZO/j32vSdvVmv4G9TfZ06ZN4ze/+Q0+ny/RYWaM1uR79erVHHHEETz44IOceOKJHH300Vx77bUHXPNEREREJHlUnGgHqqurAcjOzm60PTs7G8dxqKurO+A9559/Ps8//zzPPvsslZWVLFu2jHvuuQfDMKitrU1K3G1JcXExeXl5Ld6/urq6yd/Hvtek7WpNf3Ndl5///OdMmDCB4cOHJyXOTNGafFdUVDB79mzeeust7rrrLn7/+9+zdu1arrvuOmKxWFLiFhEREZHGPKkOQBJv37QizX2j39T26667jh07dvCLX/yCn//85xQUFPCzn/2M2267rdGjCBJ/pqmaYVvWmv72zDPPsGHDBh5++OGExpaJWpPvWCxGNBrlT3/6U0NRsXv37lx00UXMnz+fs88+O3EBi4iIiEiTdBfUDuTm5gJQU1PTaHtNTQ2WZR3wjSOAz+fjjjvu4P3332fu3Lm89dZbDBs2DNd1yc/PT0rcmSwnJ6fJ38e+16TtOtT+tm3bNv7whz/ws5/9jEAgQCwWa7jhjsViOI6TnMDbqNZc34LBIIMHD2402mnQoEHk5eWxZs2axAYsIiIiIk3SyIl2YN+z2Js2bWr0XPamTZvo1atXk+959913MU2TUaNG0bdvX4CGlQMGDhyY2IDbgV69ejXM4bHPvmUPjzjiiFSEJHFyqP3t3Xffpaamhh/84AcHvPatb32LqVOnctNNNyUs3rauNde3Hj16EI1GD9gei8VaNGeMiIiIiMSfRk60A7169aJr164sWLCgYVs0GuX111/n+OOPb/I98+bN484772z42XVd/va3v1FSUkL//v0THnOmO+6443jnnXcazd+xYMECCgoKGDBgQAojk8N1qP3t1FNP5R//+Eejf1dffTUA//jHP/j2t7+dtNjbotZc30aPHs2HH35IWVlZw7YlS5ZQW1vL0KFDEx6ziIiIiBxIIyfaAcMwmDJlCr/5zW/Iz8/n2GOP5amnnmLXrl1cddVVAGzcuJGKigqGDBkCwMUXX8xzzz3HXXfdxdixY3nhhRdYtGgR99xzD5Zlpe7DtFFfz+/kyZN56qmnuPbaa7nmmmtYtWoVjzzyCD/60Y+0UkMbd6j9rUOHDnTo0KHRMT744AOg/lEDObjWXN+uuuoqnnvuOaZMmcIPfvAD6urq+P3vf8/QoUMZPXp06j6MiIiISDum4kQ7cdlllxEOh3nyySd5/PHHGThwIH/5y18alq986KGHmDNnTsOjG4MHD+b+++/n/vvv5+9//zu9evXi3nvvZfz48an8GG3W1/PbuXNnHnvsMe666y5+8IMfUFRUxA9/+EOuueaaFEcq8XCo/U0Oz6Hmu7CwkJkzZ/K73/2OW2+9Fa/Xy9ixY/npT3+qCWlFREREUsRw9828JiIiIiIiIiKSAvqKSERERERERERSSsUJEREREREREUkpFSdEREREREREJKVUnBARERERERGRlFJxQkRERERERERSSsUJEREREREREUkpFSeEa665hv79+7NgwYKD7mfbNieccAJDhw6lpqYmSdHBd7/7Xfr379/wb+nSpfTv35+JEyd+43v/+te/0r9/f6ZNm3bAcVauXJn44EUy2OzZsxv1qeb+7bu2bN68uUX733XXXQc97+23395o/8WLF7c4xm86toiIiIikhifVAUjqTZw4kUWLFvHSSy9x2mmnNbvf22+/zc6dO5k0aRLZ2dlJjLDelClT8Pv99OnTh169erFy5UrWrVtH7969m33PCy+8gGEYTJw4kdLSUkaOHMmCBQtYtWpVEiMXyWwjR45k5MiRzb7+9T5aWlrKpEmTmt3/mGOOadF5L7nkEjp16kRpaWmz+wwcOJCpU6eyZcsW5syZ06LjioiIiEjyqTghnH766eTk5PDqq69SV1dHVlZWk/u98MILAAe9qUika6+9lry8PKC+oHLfffcxd+5cpk6d2uT+Gzdu5OOPP2bkyJF0796d7t27A7BlyxYVJ0TiaOTIkdx0000t3r+0tPSQ9m/OpZdeysCBAw+6z8CBAxk4cCCLFy9WcUJEREQkjemxDiEQCHDmmWdSW1vL66+/3uQ+dXV1LFiwoGH0QapNmDABwzB46aWXmt3n+eefB1JXTBEREREREZGWUXFCgK9u4OfNm9fk66+++iq1tbVMnDgRwzAAiEQizJgxg7PPPpujjz6aUaNG8f3vf59PPvmkyWM899xzXHzxxQwdOpQTTzyR73//+60ewVBSUsLIkSNZu3Ytq1evbnKfF198kWAwyJlnntmqc4iIiIiIiEhyqDghAAwfPpxu3brxxhtvUF1dfcDr++Zu2FfECIfDXHXVVdx7771YlsWll17KCSecwKJFi7j00ksPmFzzl7/8JT/96U/ZuXMnEyZMYMyYMbzzzjtceumlrS5QHKygsnz5ctatW8cZZ5yRkvkxREREREREpOU054QAYBgGEyZMYPr06bz66qucf/75Da/t2rWLRYsWMXz48IZ5G/785z/zwQcfcMEFF/Cb3/wGj6e+KX366adMnjyZn/zkJxx33HHk5OTw7rvvMmvWLIYPH84f//hHcnJyALjwwguZPHky9913HzNmzDjkmM844wzuuOMOXnrpJW6++eZGr6V6fgyR9mTJkiVMmzatydcmTZpEt27dGm3bsmVLs/uPHDmSUaNGxT1GEREREUlvKk5Ig4kTJzJ9+nTmzZvXqDjx0ksvEY1GG93oz5kzh6ysLH72s581FCYAvvWtbzF58mQeffRR5s+fzwUXXMDcuXMB+NGPftRQmAA49thjueWWW8jPz29VvNnZ2Zx++un885//ZPny5Rx99NEAOI7D3LlzKS0t1U2OSBIsWbKEJUuWNPnayJEjmyxOPPjgg03uP3XqVPVbERERkXZIxQlp0KNHD4YNG8aiRYuorKxsWBnjxRdfJCsrq2HuhurqajZt2sSxxx7bqNiwz7Bhw3j00UcbHtdYtWoVlmUxaNCgA/a99tprDyvmSZMm8c9//pN58+Y1FCcWL17Mjh07uOGGGxrmxxCRxJk6deohrb4xcuRI/vrXvyYwIhERERFpa1SckEYmTpzIBx98wPz587nooovYsmULH374Ieeff35DIaKmpgaA3NzcJo/RuXNnAEKhEACVlZX4/X68Xm/c4x01ahRdu3bl5Zdf5tZbb8UwDJ5//vlG82OISOabPXs2W7ZsabRt4MCBnHbaaSmKSEREREQOhSbElEbGjx+P3+9vWKJz7ty5uK7b6EZ/3wSTZWVlTR6jsrISgIKCAgCCwSDhcJhYLHbAvnV1dYcVr2manH/++WzZsoWPPvqISCTCK6+8wrBhw+jRo8dhHVtE2o45c+bw4IMPNvr39Yl5RURERCR9aeSENJKbm8tpp53G/Pnzqays5OWXX6a0tJTjjjuuYZ+cnBy6devG+vXrqaiooLCwsNEx3nvvPQD69u0LQL9+/Vi5ciUrVqxg8ODBjfa94YYb+OSTT3jrrbfIyspqVcwTJ07kj3/8I/Pnz2fXrl1UVVVp1IRIO6PHRERERETaNo2ckANMmDCBaDTKM888w6effsr5559/wNwNkyZNIhQKcffddzcaEfHpp5/y1FNPkZeXx9ixYwEaJte89957Gx71AFi6dClLlixh6NChrS5MABxxxBEMHjyYV199lVdeeYWsrCzOOuusVh9PREREREREkksjJ+QAo0ePplOnTjz00EMAXHDBBQfsM2XKFBYtWsQLL7zA6tWrOe6449i5cycLFizAdV3uvffehjkqRo8ezYUXXshzzz3HhAkTOOmkk6ipqWHu3LlkZ2fzy1/+8rBjnjhxInfccQdlZWWcfvrpTU7UKSIiIiIiIulJIyfkAJZlcd5551FXV9fs3A1+v5/HH3+cH/zgB0SjUWbOnMm///1vTj31VGbNmnXAJHR33XUXv/rVrwgEAsyaNYtXXnmFk08+mZkzZ9K9e/fDjvmcc87B6/VSV1fXZDFFRERERERE0pdGTkiTfvzjH/PjH//4oPsEAgFuvPFGbrzxxm88nmEYTJ48mcmTJ8crxEYKCgpYvnx5Qo4tIk274IILDqkY2K1bN1avXp3AiERERESkrdLICRERERERERFJKY2ckDbjkUcewe/3c+WVV5KXl3fI7589ezZbtmxh5cqVCYhORJJt5syZdOrUiUmTJtGtW7cm91m5ciULFixgy5YtSY5ORERERA6FihPSZvzpT38C6lcKaU1xYs6cOSxZsiTeYYlIisyaNQuAkSNHHrQ48eCDDyYzLBERERFpBcN1XTfVQYiIiIiIiIhI+6U5J0REREREREQkpVScEBEREREREZGUUnFCRERERERERFJKxQkRERERERERSSkVJ0REREREREQkpVScEBEREREREZGU+v+ydxKta5R1lwAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df = df_device.iloc[:,2:]\n", - "\n", - "n_col = 4 # num of columns per row in the figure\n", - "y_indx = -1 #PCE (efficiency%)\n", - "color_indx = 1 #\n", - "column_names = df.columns.values\n", - "fs = 20\n", - "\n", - "cmap = plt.get_cmap('coolwarm', 10)\n", - "print(\"The color is for variable\", column_names[color_indx], \"with red as the highest and blue as the lowest\")\n", - "\n", - "for n in np.arange(0, 12, n_col):\n", - " fig,axes = plt.subplots(1, n_col, figsize=(18, 3.5), sharey = False)\n", - " for i in np.arange(n_col):\n", - " #print(n)\n", - " if n< len(column_names)-1:\n", - " im = axes[i].scatter(df.iloc[:,n],df.iloc[:,y_indx], \n", - " c=df.iloc[:,color_indx], s = 20, cmap=cmap, alpha =0.8, edgecolors = 'face')\n", - " axes[i].set_xlabel(column_names[n], fontsize = fs)\n", - " else:\n", - " axes[i].axis(\"off\")\n", - " #axes[i].set_title(sf_cols[n])\n", - " n = n+1 \n", - " axes[0].set_ylabel(column_names[y_indx], fontsize = fs)\n", - " for i in range(len(axes)):\n", - " axes[i].tick_params(direction='in', length=5, width=1, labelsize = fs*.8, grid_alpha = 0.5)\n", - " axes[i].grid(True, linestyle='-.')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The color is for variable speed [mm/s] with red as the highest and blue as the lowest\n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df = df_film.iloc[:,1:-1]\n", - "\n", - "n_col = 4 # num of columns per row in the figure\n", - "y_indx = -1 #PCE (efficiency%)\n", - "color_indx = 1 #\n", - "column_names = df.columns.values\n", - "fs = 20\n", - "\n", - "cmap = plt.get_cmap('coolwarm', 10)\n", - "print(\"The color is for variable\", column_names[color_indx], \"with red as the highest and blue as the lowest\")\n", - "\n", - "for n in np.arange(0, 8, n_col):\n", - " fig,axes = plt.subplots(1, n_col, figsize=(18, 3.5), sharey = False)\n", - " for i in np.arange(n_col):\n", - " #print(n)\n", - " if n< len(column_names)-1:\n", - " im = axes[i].scatter(df.iloc[:,n],df.iloc[:,y_indx], \n", - " c=df.iloc[:,color_indx], s = 20, cmap=cmap, alpha =0.8, edgecolors = 'face')\n", - " axes[i].set_xlabel(column_names[n], fontsize = fs)\n", - " else:\n", - " axes[i].axis(\"off\")\n", - " #axes[i].set_title(sf_cols[n])\n", - " n = n+1 \n", - " axes[0].set_ylabel(column_names[y_indx], fontsize = fs)\n", - " for i in range(len(axes)):\n", - " axes[i].tick_params(direction='in', length=5, width=1, labelsize = fs*.8, grid_alpha = 0.5)\n", - " axes[i].grid(True, linestyle='-.')\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Reduce the Data Complexity - Dimensionality Reduction using tSNE\n", - "- t-Student Stochastic Embedding (tSNE, tee-s-nee)\n", - "- More readings about `tSNE` vs `PCA` at this blog post: https://towardsdatascience.com/an-introduction-to-t-sne-with-python-example-5a3a293108d1\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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331Hly5daNy4MTt27GD8+PGUL1+el156KV9rsgUyuGFh8+bN48qVK0ybNs3kToIDBw7w9ttv8/nnn/Pll1+anPP48WN27txpnJh04sSJ/PDDD+zfv5/du3cbO0KZMmVYtGgRBw4coF+/fsbzHz16RPny5Vm/fr0xXFatWsWcOXOYP38+s2fPznEd8+fPJzw8nNmzZ9O9e3cgdcUSf39/s99m7tmzh507d/Lqq68yZ84c7O1Tf8xGjhxJz549ef/99wkKCkKn07Fv3z5++ukn2rVrx4IFC4wfGjZs2MCnn35q8rr//PMPABUrVsxx+4UoCGJiYti8eTMNGjTg22+/NW7v1asXPXv2ZMOGDWYfZqKioti5cyeenp4A7Nu3j3HjxvH5558bX2Pt2rUcP36c4cOH89577xnPHTBgAG+88QYffvghW7duBXLWP7dv386JEyfo0aMHn332GXZ2qTf7BQQEsHLlylz/efz9999MmTLF+Px479696dy5M1u3bmXo0KG8//77ALz99tu0bNmSH374wWxw48qVK2zevJkaNWoA8OKLLzJt2jRWrVrFsmXLaNWqFZD6TOqAAQP44YcfTD5cBAcH4+XlxQsvvADA7t27uXv3LjNnzqRHjx7G4z799FM2bNjA0aNHja8pRG5YKg/S1K1b12SW/6SkJObPn0/FihXZvn27yS8JPv74YzZu3MjBgwfp06cPDRo0oEyZMvz000988sknxg/USUlJ7Nu3j5o1a1K5cmWT9wsKCspw/odBgwaZTLZua4KDg3n55ZfNHoXNbf6k58qVK7z33nvGXx4pisLw4cMJDg7m+PHjtGjRIu8LFIVSTvIkTVhYGGvWrKFBgwasXbsWrVZr3Ddy5EgOHz7MyZMnTSboDg0NZeDAgUydOhVIHXht0aIF+/bto1OnTixYsACNRoNer6dz584EBQURHx9PkSJF2Lx5M9HR0axdu5bGjRsbX9Pf359Dhw7x999/88ILL/DHH3+wb98+XnvtNQIDA43HpaSk0L17d86fP8/169fNJvBdu3ZthtmU2xVitm3bxpEjR+jUqRNffPGFMUdbt27NxIkT+eabb/joo49M/px27dpF6dKlAShZsiTLli3j//7v/9i2bZtxwKlWrVp88MEH/PjjjzK4kQ4Z3LCglJQUduzYwQsvvGD2iETbtm2pW7cuP/30EzEx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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABDAAAADvCAYAAAAEsyT/AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/d3fzzAAAACXBIWXMAAAsTAAALEwEAmpwYAABjF0lEQVR4nO3deXhTZf7+8XeSbnShUJYiFNkURJFNKIug7Agq28gIsggiOiKOX9GBwX3UcRsUZRFwFBVFXFAUERAL4oioiAiKbMpatrIUShfapsn5/cGvkZDubZrk5H5dV6/LnnOSfD7S52765JznWAzDMBARERERERER8WNWXxcgIiIiIiIiIlIcTWCIiIiIiIiIiN/TBIaIiIiIiIiI+D1NYIiIiIiIiIiI39MEhoiIiIiIiIj4PU1giIiIiIiIiIjf0wSGnxo3bhzNmjUjKSmpyOMcDgedO3emTZs2ZGZmVlJ1MGrUKJo1a+b6+vnnn2nWrBmDBg0q9rFvv/02zZo1Y+bMmR7Ps337du8XLyIuH3/8sdsYLOwrP4sOHjxYouP//e9/F/m6//znP92O/+GHH0pcY3HPLWJ2JR23zZo1c3vcvn37uPXWW2nTpg1t27Zl7ty5ZGVlMXXqVDp06EDLli258847Xc//5ptvlrq2Hj160K5duwrq1PcKy6rjx49zxx130LJlS/r06cMHH3xQ4OMnTZrEkCFDPLb/8MMPbs87YcIEr/YhImIWIb4uQAo2aNAg1q1bx4oVK+jVq1ehx3377becPHmSwYMHExUVVYkVnjN+/HjCw8Np0qQJDRs2ZPv27ezdu5dGjRoV+pjPPvsMi8XCoEGDqFevHomJiSQlJbFjx45KrFxEzpeYmEhiYmKh+y8c0/Xq1WPw4MGFHt+qVasSve7NN99MrVq1qFevXqHHNG/enIkTJ3Lo0CGWLFlSoucVCQbFjdsLTZkyhc2bN9O7d28aN25Mu3btmDNnDh9//DEtWrSgc+fONGrUyDXmWrduXeqaRo8eTW5ubqkf5+8uzKpHHnmEH374gZtuuol9+/bxyCOPULt2bbp16+Z6zI4dO1i+fDnz5s3zeL569eoxceJEzpw5w4IFCyqrDRGRgKcJDD/Vu3dvoqOjWbNmDWfPnqVKlSoFHvfZZ58BFPmHhDfdcccdVK1aFTg36fLSSy/x+eefM3HixAKPP3DgAFu2bCExMZH69etTv359AA4dOqQJDBEfSkxM5J577inx8fXq1SvV8YUZPnw4zZs3L/KY5s2b07x5c3744QdNYIicp7Tj9rfffqNOnTrMmjXLtW3OnDkAvPjiizRo0MC1vbhxWZgxY8aU6XH+7vysOnnyJF999RX3338/d9xxB4Zh0L9/f9577z23CYyXX36ZNm3acO2113o8X0JCAvfccw8HDx7UBIaISCnoEhI/FRERQd++fcnKymLt2rUFHnP27FmSkpJcZzH42sCBA7FYLKxYsaLQY5YuXQr4bsJFREQkWNntdqpVq+a2Lf9sierVq/ugosB06NAhAC677DIALBYLl112GQcPHnQd88svv7BmzRr+7//+zxclioiYliYw/Fj+H/nLly8vcP+aNWvIyspi0KBBWCwW4Nwbkblz59K/f39atGhBhw4duOuuu/j1118LfI6PPvqIoUOH0qZNG66++mruuuuuMp8JUbduXRITE/njjz/YuXNngccsW7aMyMhI+vbtW6bXEBERkdKZOXOmaz2MHTt20KxZM3r06EGzZs3YsGEDAO3bt6dZs2YcPHiw0DUwduzYwX333cfVV19NmzZtGDx4MIsXL8YwDNcxBa2BYRgGixYtYvDgwbRs2ZL27dvzt7/9jW3btrkdl78uxMcff8zixYu58cYbufLKK7nmmmt47rnnOHv2rEdvSUlJjBo1inbt2tGhQwfGjBnDjz/+CJz7oKdt27Z069bNrcZ8U6dOpVmzZuzbt69U/z/zzzzNyspybcvIyCA6Otr1/fTp0+nUqRMdOnQo1XOLiEjRNIHhx9q1a0dCQgJff/01GRkZHvvz15LIn+jIyclhzJgxTJ8+HZvNxvDhw+ncuTPr1q1j+PDhHguCPvroozz44IOcPHmSgQMH0q1bN9avX8/w4cPLPIlR1KTL1q1b2bt3L3369PHJeh0iIiLBKDEx0XVpZ82aNZk4cSKjR49m4sSJrjUdxo8fz8SJE11/nF/ou+++4+abb+bLL7+kXbt2DBs2jOzsbB566CFmzpxZ5OtPmTKFxx9/HLvdzrBhw7juuuvYuHEjw4YN47vvvvM4/p133uHxxx/n0ksvZdSoUYSHhzN//nwefvhht+PmzZvH3Xffze7du+nbty/XX38927ZtY8yYMXz77bdUqVKFvn37cuTIETZu3Oj22JycHFatWkXr1q1p2LBhSf9XAucuoatZsyYLFy4kNTWVTZs28f3339O2bVvg3ETM+vXrdfaFiIgXaA0MP2axWBg4cCCzZ89mzZo1DBgwwLXv1KlTrFu3jnbt2rnWkXjttdf46aefGDJkCE8++SQhIef+eX/77TduueUWpk6dSseOHYmOjua7777j/fffp127dsybN8/1qcFf/vIXbrnlFl566SXmzp1b6pr79OnDE088wYoVK7jvvvvc9vl6vQ4RKdyGDRsK/SNk8ODBJCQkuG07dOhQoccnJibqU0eRSlDUuAXo2rUrrVu3pkOHDnTo0IFZs2ZRs2ZNt3UzNmzYwKFDh9zWtLqQw+HgoYcewjAM3n77bdq0aQPA//3f/zF06FDmzZvHiBEjqFGjhsdjV6xYwaeffsoNN9zAc88953pvcscdd3DTTTcxZcoUkpKSCAsLcz1mx44dLFy40PU6d911F3369GHlypU88cQTREVFsXfvXmbMmEHjxo1ZsGABtWrVAuDWW29l0KBBPPvss3z22WcMHDiQjz/+mGXLltG+fXvXa6xZs4aMjAy391YlFRoaytSpU5kyZQqdOnUCoHHjxtxxxx3AubMvunXr5rYIqmEYrrNlRUSk7DSB4ecGDRrE7NmzWb58udsv2RUrVmC3290mA5YsWUKVKlV46KGHXG8QAK644gpuueUW5s+fz6pVqxgyZAiff/45APfff7/bKY9t27Zl0qRJxMbGlqneqKgoevfuzaeffsrWrVtp0aIFAE6nk88//5x69erpDxsRP7RhwwbXqeQXSkxMLHAC4/yFAM83ceJEjXORSlDUuAWIiYkp051ELrR582YOHTrEX//6V9ekAkB4eDj//Oc/2bJlCzk5OQU+dvHixQAe703q16/PsGHDmDt3LuvXr3db/LJ9+/ZurxMTE0ObNm1YvXo1R48epUmTJqxcuZK8vDwmTJjgmrwAaNCgAVOmTCE7Oxu73U6HDh2oV68eX3zxBY888oirhqVLlxIaGkr//v3L9P/khhtuoFmzZqxfv57q1avTu3dvqlSpwtdff83mzZtZsmQJTqeTp59+2nWZzcCBA3n44YfdJmtERKR0NIHh5y6++GKuuuoq1q1bx5kzZ1yfjixbtsx1aiScu/YyOTmZtm3buk1I5LvqqquYP3++69KQHTt2YLPZuPLKKz2Ozf8EoawGDx7Mp59+yvLly10TGD/88APHjx9nwoQJ+gRCxA9NnDixVHczSExM5O233/ZiRSJSnNKO27LKf+9Q0GRI586d6dy5c6GP/e233wgPD2fhwoUe+/bu3QvA9u3b3SYwCrqkIyYmBji3EGlxNQ0bNszt+xtvvJG5c+eybt06unXrxunTp/nmm2+45ppryrV46aWXXsqll17q+t4wDF566SX69OlD8+bNeffdd3nvvfd48MEHiY6O5l//+hc1atTg3nvvLfNriogEO01gBIBBgwbx008/sWrVKm666SYOHTrEpk2bGDBggGuyIjMzE/jzF/yFateuDUB2djYAZ86cITw8nNDQ0Aqvt0OHDlx00UWsXLmSf/zjH1gsFpYuXeq2XoeIyIU+/vhj1+r++Zo3b06vXr18VJGIwLn3DECBH5AUJz09nby8vELP2AJIS0tz+76gMxTyP/zIX4yzNDUNGjSIuXPnsmzZMrp16+Y6i3XgwIEl7qMkvvjiC3bs2MG0adMA+PDDD+nevTu33HILcG4y591339UEhohIOWgCIwD069ePp556ihUrVnDTTTfx+eefYxiG22RA/qKYKSkpBT5H/i/6/NunRUZGkpOTQ15entspnXBu1e4qVaqUuV6r1cqAAQOYN28emzdv5oorruDLL7/kqquu4uKLLy7z84qIuS1ZssTjdPjBgwdrAkPExyIjI4E/Pyw5n91uxzCMQi+LiIyMJCoqqtBbwldETReeRZGdnU1YWBhW67m16hs1akTr1q1ZvXo1ubm5rFixgtjYWLp3715h9TidTmbMmMENN9xAkyZNANi3bx9XX32165iGDRty+vRp0tPTC/3ASUREiqa7kASAmJgYevXqxQ8//MCZM2dYuXIl9erVo2PHjq5joqOjSUhIYN++faSmpno8R/4txS655BIAmjZtisPh8LiFGcCECRNo165dgbcrK6lBgwYBsGrVKtatW0d6errOvhCRIr399tvs3LnT7evZZ5/1dVkiQa9p06YA/PLLLx77VqxYQatWrfjkk08KfGyzZs04evQox48f99i3du1apk+fXqY7nxVV01NPPUWrVq1ITk52bRs4cCBZWVkkJSXx008/cd1111XoWhRLly5l//79bpf0OBwOHA6H6/v8dUJ0Ka2ISNlpAiNADBw4ELvdznvvvcdvv/3GgAEDPH4BDh48mOzsbJ5++mny8vJc23/77TfeeecdqlatSo8ePQBcC4JOnz7ddVkJwM8//8yGDRto06ZNuc7CaNy4MS1btmTNmjV8+eWXVKlSheuuu67MzyciIiK+0b59ey666CI+/fRTtm/f7tqem5vLm2++ic1mc92N40KDBw/GMAyefPJJcnNzXduPHTvGY489xquvvlqmW6vfcMMNWK1W5s6dy6lTp1zbDxw4wIoVK6hfv77rLm0A119/PaGhoUybNo28vLwKvXzEbrczc+ZMhgwZ4namaaNGjdi8ebPr+y1btlCzZs0yXYojIiLn6BKSANGlSxdq1arFK6+8AsCQIUM8jhk/fjzr1q3js88+Y+fOnXTs2JGTJ0+SlJSEYRhMnz7d9UuzS5cu/OUvf+Gjjz5i4MCBdO3alczMTD7//HOioqJ49NFHy13zoEGDeOKJJ0hJSaF37976hS0iIlKBiruNKkD//v1dlzSUVUhICE8//TR33nknw4YNo3fv3tSoUYO1a9eyb98+pk6dSnx8fIGPHTJkCGvWrOGLL75g586ddO3alby8PFasWMHp06e5//773SYaSqpJkyZMnDiRGTNmMHDgQLp3745hGCxfvpycnByPs7diY2Pp0aMHX3zxBfXr1+eqq64q0/+LgixevJhjx44xYcIEt+033XQTTz31FBMmTCAqKorly5d73GJeRERKRxMYAcJms3HjjTcyf/78QteSCA8P58033+T111/ns88+Y9GiRVStWpXu3btz5513cvnll7sd/+9//5sWLVrw/vvv8/777xMeHs4111zDfffdV6Y3Exe6/vrreeaZZzh79myBEy4iIiJSdsXdRhXOLYRb3gkMOHe3kUWLFjFr1iy+/vprzp49yyWXXMJzzz3numy0IBaLhRkzZrBw4UI+/vhjPvzwQyIiIrjkkksYO3Zsuda4ufvuu2nUqBFvvfUWn376KRaLhTZt2vD3v/+dli1behx/3XXX8cUXX7jdlr68cnJymDNnDjfffDMXXXSR276RI0eSmprKu+++i8PhYPTo0dx+++0V9toiIsHIYuQv5yxSCqNGjWLDhg38+OOPrlu7lsc///lPlixZwieffELz5s0roEIR8WdlGfM//PADo0ePZvTo0Tz00ENerlBEzObFF19k3rx5rFq1igYNGpToMd5+f3Lw4EF69uxJz549XWfZiohI4bQGhoiIiIiY2tGjR1m8eDEdOnQo8eSFiIj4H11CIuXy6quvEh4ezq233lqmMzE+/vhjDh065LYomIgEj0WLFlGrVi0GDx5MQkJCgcds376dpKQkDh06VMnViUigW7p0KW+++SYHDhwgIyPD7S4hpVGSrCqNgwcPsmTJEtdt7kVEpGQ0gSHl8t///hc4t8p4WSYwlixZUuz1uyJiXu+//z4AiYmJRU5gzJo1qzLLEhGTqFOnDocOHSIiIoLJkyfTvn37Mj1PSbKqNA4dOqRcExEpg6BbAyM7O5utW7dSq1YtbDabr8uRAOVwODh+/DgtWrQgIiKi0ONOnz5NRkZGofujo6OpVq2aFyoUb1KOSEVQjgQ35YhUBOWIiASboDsDY+vWrYwYMcLXZYhJLFy4kHbt2hW47/Tp0/S5+mrS8vIKfXxsbCyrVq3Sm4YAoxyRiqQcCU7KEalIyhERCRZBN4FRq1Yt4FzQ16lTxyc1HDt2jNq1a/vktSuL2Xv87bff+Pvf/+76eSpIRkYGaXl5LGzdmjrh4R77j+bkMGLzZjIyMvSGIcAoRyqH2XtUjgQ35Yj3mb0/UI6ISPAJugmM/NM069SpUyHXMJaF1Wqlbt26PnntymL2Ho8dOwZQotN+64SHk1ClirdLkkqkHKkcZu9RORLclCPeZ/b+QDkiIsFHt1EVEREREREREb+nCQwfiImJ8XUJXmf2HqOionxdggQ5s48xMH+PyhHxNbOPMbP3B8oREQk+QXcJiT8Ihl+oJe3RcDrJPfErjuNbsUXGE1qvE9Yw//9lrDcM4mvKkT85nE6+yj5DKkdoGhbKFdaGhFr9/9ebckR8zew5Upr+7E4nm/OOYM3aQwNLJHExrbAqR0RE/I7OwPCBlJQUX5fgdSXtMX3Xu2Rungfbv8D50ztkf/sfDGfhq2T7ixMnTvi6BAlyypE//TP1CKvsGzjo3Mq63J/53PE/nE6nl6srP+WI+JrZc6Sk/TmdTqbbf2PP8eWcSf2BPSeSSE750MvVVQzliIgEG01g+EDNmjV9XYLXlaRHwzDIzNxFRHomjtBQ8kKtWNOO4Mw6XgkVlk9cXJyvS5Agpxw5J8PpYGteBheFnCHbCCPVEU668yRnsVdCheWjHBFfM3uOlLS/4+Rw1JHGRTlpnLWFcywkipys/QExEaocEZFg4//nxpmQ3W4v0WrRgawkPVosFkKsVbBHhBF6NgcsFowqVbGGV6+kKsvObi/FH0e//gxWi+d2p1FxBUnQUY6cUwUL4dhId4YTZckhwgKhRBBBaCVVWXbKEfE1s+dISfurSihYw8i0hRPhyCUUJ7bQWKxW//+cTzkiIsFGExg+kJqaavrbepW0x+pNRnLG+QHGqYOEhtcg7MpbsIRGVEKF5ZOWllbyg3t2gehIz+0ZWfDZmoorSoKKcuQcm9XKE9XrMSfdSa3Qg1wWHkLP0JbYAuAPD+WI+JrZc6Sk/VWxhnB72KWsrJlLozN/0NQSQUKNPpVQYfkpR0Qk2GgCQ7zuaO4Rvsj9mRyLQUfLxbSMvNK1L6RqfeKuut+H1YlIINjytcEbrzjJc8JNwyx0+8ufExQtw6swJ7wp0NR3BYqI31u2OZf/fG/HYoEpnULp1zLMte9ya3Uuj+oCUV18WKGIiBTH/z+ikoCXlLuZLIsTmwHfsJ90Ryk+LRCRoOd0Grw2y0muAyzAooUGGWk65VlESi7H7uSp7+w4gTwnPPGtnTyH/69xISIi7nQGhnhdDgahWAjBgoGB3WmH8y5J3XPsOB99kYLTAQN71+SyenV8V6yI+KVcB1QJBYsVztrBng3E/rk/NX0rJ459AVYrteNvpFrkJT6rVUT8j90BeYZBpO3cGhBn8gwcTgg57/1ISsYmkjO/w2YN4ZKY/sRE1PdRtSIiUhidgSFe19nWiFyLwRmrk8ucsVQL+XPFbIfDwWvvniR5byRHDlbh9YXp5Nr9/zaqIlJ5rFYLN1xvITMH0s9Cl0QL1eP/XIguz5HNiSMfQ146lpzTHDv4Hg6Hw4cVi4i/iY6wcnOTUNLyIC0PRlwaSnjon2+Ds/NOsydzLXayOevMYEfaJ74rVkRECqUzMMTrmldpTgNHfeyGnRhbLFbLn28YHIbBmdPhxFTLxWIxSE8NJzM3h7BQ/WiKyJ9uHGelcz+DPDvEN3BfRd/uyMTizMOwhmFYwOLMxcCB26leIhL0/tE3nGEnQ7FYICHO/TO8nLzTGBhYsWIB8gLgVswiIsFIfyX6QHx8vK9L8LoLe4y0RRd4XFhICC3bZfDLxmgwoOkVGVSNuLgySiyXkt5bXsRbgjFHatQt4PZ/QJitGkTUwpJ9HAOwRCYQYgsr8Fh/ohwRXzN7jhTUX/0aBZ98HB2aQLilCjnGWbAYVLcmeLu8CqEcEZFgowkMHzD7fdehdD2O63c5Wy4/jNPppHXDywPi/02p7rv+x68QXsBQy9GlMlJ2ypE/2Ww2Gtf/GyfSf8JisVEjum0lVFd+yhHxNbPnSGn6s9lCaF1jHMeyfsZmCadGlZZerq5iKEdEJNhoAsMHsrKyiIiI8HUZXlWaHm02G20bB9ZCWdnZ2SU/uHNHiC3gDJS0DNj+WcUVJUFFOeLOZgslvlpHL1dUsZQj4mtmz5HS9mezhXNRjHJERMSfaRFPH4iLiyv+oABn9h5jY2OLP0jEi8w+xsD8PSpHxNfMPsbM3h8oR0Qk+GgCwwdSU1N9XYLXmb3HtLQ0X5cgQc7sYwzM36NyRHzN7GPM7P2BckREgo8mMHygVKf7BSiz95iTk+PrEiTImX2Mgfl7VI6Ir5l9jJm9P1COiEjw0QSGiIiIiIiIiPg9TWCIiIiIiIiIiN/TBIaIiIiIiBRq+/btXHHFFRw9etRte+/evWnWrJnHVzCsPyIivhEQt1F1OBwsWLCADz74gCNHjlC3bl1uueUWRowYgcViwTAM5s6dy/vvv8+pU6do27YtDz/8ME2aNPF16SKQvANSwzy3Z+ZWfi1BTDkiAU054heUIxLQypgju3fv5s477yQvL8/9YZmZJCcnc//995OYmOi2r2rVquUuV0SkIAExgfHKK6/w6quvMmHCBFq3bs3GjRt5+umnOXv2LOPHj2f27Nm8+uqrPPDAA9SrV485c+YwZswYli9fTkxMjK/L92Dme67nM3uP4eHhJT+4dTuIK+DnMDUdVv1ecUVJkZQjgcfsPSpHAo9yJLCYvT/wbo7k5eXx/vvv88ILLxAaGuqxf+fOnRiGQc+ePTVJJyKVxu8nMBwOB2+88Qbjxo3jrrvuAqBTp06kpqYyf/58hg8fzuuvv87EiRMZPXo0AO3ataN79+4sXryYsWPH+rL8AgXDfcnN3qPuux5YlCOByew9KkcCi3Ik8Ji9P/Bujvz0009MmzaNcePGER8fz8MPP+y2f/v27URERNCwYUOv1SAiciG/XwMjIyODQYMG0adPH7ftjRo1IjU1le+//56srCx69uzp2hcbG0tiYiLffPNNZZdbIsFwXaDZe9R91wOLciQwmb1H5UhgUY4EHrP3B6XPkVx7LhmZGW5fWWczCzy2SZMmJCUlMXHiRGw2m8f+nTt3Ehsby6RJk2jXrh1t2rThvvvu4/jx42XqRUSkJPz+DIzY2FgeffRRj+1fffUVderUISUlBYD69eu77U9ISGDNmjWVUmNpRUZG+roErzN7j8FwWqqZKEcCk9l7VI4EFuVI4DF7f1D6HNm1axdbt25123Yqx1ngsTVr1izyuXbs2MGJEye49NJLGTVqFHv27GHGjBmMHj2aJUuWKONExCv8fgKjIB9++CHr16/n4YcfJiMjg7CwMMLC3BclioqKIiMjo9DnOP8TEoBbb73VdYpceno6ADExMaSkpOBwODweHxMT49pfs2ZN7HZ7oTP98fHx2O12srKyiIuLIyMjo8BjIyIiiIuLIzU1lcjISEJDQ11viC4UFxdHaGgoJ06ccNWcX/f5bDZbpfSUmppKdna2a7/T6cRqtZqqp3wREREFXgvqDdu3b+emm25i9erV1KlTp9DjMjMzmTZtGqtWrSIrK4t27drx0EMP6bTOIihH/H/MKUfE3ylH/HvM5WeImXo6X1lypGnTph7vDQ6fzuClbUtL9TwADz/8MIZh0KpVK+DcJVNNmjThlltuYenSpfz1r38t9XOKiBQn4CYwli5dymOPPUbfvn0ZOXIk8+bNw2KxFHhsYdsBVq9eTUJCQoH7zl9oKz4+vsh68vfbbDbq1q1b6HE2m801E52bm1vksedfs1nUcee/fv4vxsJ4u6cLrzM9fPiw22PN0NP59u3bV+TrVYTCVv0uyH333cevv/7K5MmTiYqKYtasWYwePZrPP//cLxeO8zXlSMGv729jTjki/kw5UvDr+9OYuzBDLtwfiD1dqLQ5EhYaRlio+yRbZI5RqufI17JlS49tV111FTExMezYsaNMzykiUhy/XwPjfG+88QaTJ0+mW7duTJs2DYvFQkxMDLm5udjtdrdjMzMz9YebBKS8vDwWLlzI0KFDycnJKfb4jRs38vXXX/Pcc88xePBg+vTpw5tvvkl6ejqLFi2qhIoDi3JERMpLOSLBLisri48++shjosLpdGK326levbqPKhMRswuYMzBefPFF5s2bx6BBg/j3v/9NSMi50hs0aIBhGBw8eJBGjRq5jr/wexGfOfE72Au4DjTN83RQKH7V7wt9++23REVFcfXVV7u2xcXF0b59e/73v/9xxx13lKt8M1GOSMAqZY6I9yhHJGBVYI6Eh4fz7LPP0qFDB2bNmuXavmbNGrKzs0lMTCxPpSIihQqIMzDeeust5s2bx+jRo3n22WddbxYA2rRpQ3h4OElJSa5taWlpbNiwgU6dOvmiXBF3l7Uit1lrMhpf7vaVdfGlBR5e3KrfF9qzZw8NGjTwOPbiiy9m7969FdKCGShHJKBd1gquTPT8uqyVrysLKsoRCWgVmCM2m40JEybw5Zdf8tRTT7F+/XrefPNNpkyZQs+ePenQoYMXGhARCYAzMI4dO8a0adNo2rQp119/PVu2bHHb36JFC0aOHMnLL7+M1WqlYcOGzJ07l+joaIYOHeqjqkXc7dq1i62/XbDq99mC17YobtXvC2VkZBAdHe2xvbiF44KJckREyks5IuJu7NixREdHs2DBAj788ENiY2MZNmwY99xzj69LExET8/sJjHXr1pGbm8uuXbu4+eabPfZ/9913TJo0CavVyvz588nKyqJNmzY8++yzuuZU/EbTpk1p2Kih27bDJ9N56X/lX6PCMApffCt/9fVgpxwRkfJSjkgwGzJkCEOGDPHYPnToUE3QiUil8vsJjMIC80IPPPAADzzwQCVUVH5FrSZtFmbvMTY2tlTHF3RrvciznrdZK4vo6GgOHjzosT0zM7PAMzOCkXIkMJm9x9LmiPiWciTwmL0/UI6ISPDRx7M+UNp7dgcis/foT/01atSI5ORkjzMx9u/fr4XjTMyffga9xew9mr0/8X9m/xk0e38QHD2KiJxPExg+cOLECV+X4HVm7zE1NdXXJbh06dKFM2fOsH79ete21NRUNm7cSOfOnX1YmXiT2ccYmL9Hf8oRCU5mH2Nm7w+UIyISfDSB4QPx8fG+LsHrzN5jaRfarEipqals3rzZtUBn+/btSUxMZNKkSXz44Yd8+eWXjBkzhpiYGIYPH+6zOsW7zD7GwPw9+jJHRMD8Y8zs/YFyRESCj9+vgWFG6enppl/Qy+w9ZmZmlvhYZ9o+HLbIArZnlem1165dy9SpU1mwYIHrNmWzZs3i2Wef5fnnn8fpdHLVVVfx0ksv6dpYEzP7GAPz9+jLHBEB848xs/cHyhERCT6awPCBYPiFavYeS/WG4eLLccZX89wefRpYXeRjC1o0rqBtsbGxPPPMMzzzzDMlrksCm9nHGJi/x8rKEZHCmH2Mmb0/UI6ISPDRJSQiIiIiIiIi4vc0gSEiIiIiIiIifk8TGCIiIiIiIiLi9zSBISIiIiIiIiJ+TxMYPmCz2XxdgteZvUerVUNHfMvsYwzM36NyRHzN7GPM7P2BckREgo9SzweC4b7kZu9R910XXzP7GAPz96gcEV8z+xgze3+gHBGR4KMJDB9IT0/3dQleZ/YeS3PbMhFvMPsYA/P3qBwRXzP7GDN7f6AcEZHgE+LrAkTMLic7meysUwVsz/BBNSISiJQjIlJeyhERMQNNYPhATEyMr0vwOrP3GBUVVeJjc2o3JbtOnOd2S2pFliRBxuxjDMzfo3JEfM3sY8zs/YFyRESCjy4h8YGUlBRfl+B1Zu/xxIkTvi5BgpzZxxiYv0fliPia2ceY2fsD5YiIBB9NYPiAw+HwdQleZ/YenU6nr0uQIGf2MQbm71E5Ir5m9jFm9v5AOSIiwafEExjHjh1j37592O32Io87efIkO3bsKHdhImI+yhERKQ9liIiISHArdgLj559/ZuDAgVx77bX069ePDh068OSTTxa6svOiRYsYPHhwhRcqIoFLOSIi5aEMEREREShmAmPHjh2MGTOGP/74g44dO3LNNddgsVhYuHAhgwcPZvfu3ZVVp4gEKOWIiJSHMkRERETyFXkXkpkzZ+JwOHjrrbdo164dAKdOneL5559nyZIljBo1ijfffJOmTZtWSrEiEniUIyJSHsoQERERyVfkGRgbN26kb9++rjcMANWrV+eZZ57h73//O6mpqdx2220kJyd7vVAzCYbbepm9x9LctizDfpQz9oMeXxn2o16s0H8oR7zD7GMMzN+jcqRklCHeY/YxZvb+QDkiIsGnyDMwMjMziY+PL3DfhAkTsNvtzJkzh9tuu41FixZRs2ZNrxRpNsHwC9XsPZbmDYNRrTFGXA3P7dknK7Ikv6Uc8Q6zjzEwf4/KkZJRhniP2ceY2fsD5YiIBJ8iz8CoW7cuP//8c6H77733Xv7yl7+QnJzMbbfdxunTpyu6PlMKhvuSm71H3Xe95JQj3mH2MQbm71E5UjLKEO8x+xgze3+gHBGR4FPkBEavXr3YvHkzzzzzDJmZmQUe8+STT9KtWzd27drFzTffzK5du7xSqJkEw6dDZu8xLi7O1yUEDOWId5h9jIH5e/R2jixbtozrr7+eli1b0q9fPz755JMij3c6ncyZM4eePXvSsmVLbrzxRj7//HOv1lgSyhDvMfsYM3t/oPcjIhJ8ipzAuPvuu2natClvvfUW7du359VXX/V8AquVGTNm0Lt3b/bv38+XX37ptWLNorj715uB2Xs0e38VSTniHcHwM2j2Hr3Z3/Lly3nggQfo0qULs2fPJjExkSlTprBy5cpCH/P000/zyiuvMHLkSObNm0erVq24//77+frrr71WZ0koQ7xHYyzwBUOPIiLnK3ICIyoqivfff59JkyZxxRVXFDqTHRYWxsyZM3nwwQeJjY31SqFmkpqa6usSvM7sPaalpfm6hIChHPEOs48xMH+P3syR6dOn069fP6ZOnUrXrl3517/+Rb9+/Xj55ZcLPP7AgQMsXLiQRx99lLFjx9KpUyeeeuop2rVrxzfffOO1OktCGeI9Zh9jZu8P9H5ERIJPkYt4AkRERHDHHXdwxx13FPtko0ePZtiwYezZs6dCihMRc1COiFSe5ORkDhw4wKRJk9y29+3blxUrVpCcnEz9+vXd9iUlJREREcGgQYPctr/zzjveLrdElCEiIiICxZyBURIbNmxg1qxZru/DwsK47LLLyvu0hVq9ejVt2rRx22YYBnPmzKFbt260atWKsWPHsnv3bq/VICIVSzkiUrzc3FwyMjPdvjKzsjyOy//DvVGjRm7bGzRoAMDevXs9HrNz504aNWrE+vXrGTBgAJdffjl9+vRh+fLlXuik4lV2hoByRERExBcqZAJj9uzZFVFLsTZt2sQ//vEPj+2zZ8923ULtxRdfJD09nTFjxpCenl4pdYkU5aTzOMecRz2+TjqP+7o0v6EcESnaSedx9p7Zza+HN7t97Ty2zePY/J/Z6Ohot+35t1vMyMjweExqaipHjhzhwQcfZOTIkbz22mtcccUV3HfffXz//fde6KhiVWaGgHJEApPej4iIGRR7CYk/yM3N5a233uLll18mMjLSbcGijIwMXn/9dSZOnMjo0aMBaNeuHd27d2fx4sWMHTvWV2WLABBSpQGhkZ7XbIdU0a3PKpNyRAJZSJUGVKtVlahq7gv2VQk/CSxz22YYRpHPZbV6fnZht9tJTU1l7ty5dO/eHYCOHTuyZ88eZs2aRceOHcvXgEkoRySQ6f2IiJhBuc/AqAz/+9//ePXVV5k8eTIjR45027dlyxaysrLo2bOna1tsbCyJiYk+X3hMRPyHckQCXWhYGFHRUW5fkVGRHsfFxMQAeNxyNP/Mi/z954uKisJms3H11Ve7tlmtVjp37szOnTsrso2AphwRERHxrYCYwLjyyitZvXo1o0ePxmKxuO3bt28fgMeCZAkJCa59/iY+Pt7XJXid2XsMhnvLm41yJPCYvUdv5Uj+2hcHDhxw275//363/edr0KABTqeTvLw8t+12u91jvAQz5UhgMXt/oPcjIhJ8yn0JSXR0NBdddFFF1FKoon4BZWRkEBYWRlhYmNv2qKioAq/zzXf+JyQAt956K+PGjSM+Pt51rWpMTAwpKSk4HA6Px8fExLj216xZ03X6bWH12+12srKyiIuL4/jx4zidTo/jIiIiiIuLIzU1lcjISEJDQ0lJSSnwOePi4ggNDeXEiROumgu6xtZms1VKT6mpqWRnZ7v25+XlERISYqqe8kVEROi+6xVMOaIcUY5UnAYNGpCQkMDKlSvp3bu3a/uqVato2LAhdevW9XhM165def3111mxYgV/+ctfgHP//7/55huuuuoqr9RZkSojQ0A5UhB/HnP5GWKmns6n9yMiEozKPYExZswYxowZUwGllI1hGIV+OlTUp0arV68mISGhwH3nn15b3Ox9/n6bzVbgm8J8NpuNiIgI4Nzq6HFxcYUee/6+op7z/NfP/8VYGG/3dGE/qampbtvM0NP5jhw5UuTrSekoR5QjF9YMypHyuPvuu5k6dSqxsbF069aN1atXs2LFCqZPnw6c+3974MABLrnkEqKjo+nUqRPXXnstTz31FFlZWTRs2JB3332XQ4cO8cILL3itzori6wwB5Yg/jrkLM+TC/YHY04X0fkREgk2Rl5AcPny4yE8NCrJlyxYWLFhQrqJKIyYmhtzcXI8Z6MzMzCJ/2fhSUb+IzMLsPcbGxvq6hIChHPEOs48xMH+P3syRIUOG8K9//Yt169Zx99138+OPP/Lcc8/Rv39/ANauXcvNN9/Mb7/95nrMjBkzGDZsGK+++ip33303p06dYv78+bRo0cJrdZZEIGQIKEf8kdn7A70fEZHgU+QERs+ePXnrrbc8tn/zzTc888wzBT6mqH3e0KBBAwzD4ODBg27bDx48WOB1vv6gsFMGzcTsPaalpfm6hIChHPEOs48xMH+P3s6RYcOGsWrVKn799VeWL1/OoEGDXPuGDBnCzp076dChg2tbREQEU6ZM4ZtvvuGXX37hvffeIzEx0as1lkQgZAgoR/yR2fsDvR8RkeBT5ASGYRgF3o7NF59sFKZNmzaEh4eTlJTk2paWlsaGDRvo1KmTDysrXEHXMZqN2XvMyckp8bFHjVQOGSc8vo4a5n9jBcoRbzH7GAPz96gcKZlAyBBQjvgjs/cHyhERCT7lXgPD16Kiohg5ciQvv/wyVquVhg0bMnfuXKKjoxk6dKivyxMhMqQuUaG1PLZnh0T4oBopiHJE/J1yxP8pR8TfKUdExAwCfgIDYNKkSVitVubPn09WVhZt2rTh2Wef9dtrTkXE/yhHRKS8lCNiVtu3b+emm25i9erV1KlTx7V93bp1TJ8+nT/++IMaNWowcuRIbrvtNh9WKiJmF3ATGPfccw/33HOP27aQkBAeeOABHnjgAR9VJSKBRDkiIuWlHJFgsXv3bu68807y8vLctm/atIm//e1v9OvXj3vvvZeffvqJ559/HsMwGDdunI+qFRGzC7gJDBERERER8a68vDzef/99XnjhBUJDQz32z5gxg8svv5z//Oc/AFxzzTXk5eUxd+5cRo0aRVhYWGWXLCJBoMhFPMU78u/rbWZm7zE8PNzXJUiQM/sYA/P3qBwRXzP7GDN7f+DdHPnpp5+YNm0at912m8dZRTk5OWzcuJE+ffq4be/bty9nzpxh06ZNXqtLRIKbJjB8IBjuS272HnXfdfE1s48xMH+PyhHxNbOPMbP3B6XPEXuunczMTLevrLNZBR7bpEkTkpKSmDhxIjabzW1fcnIydrvd4xbBDRo0AGDv3r2lqktEpKSKvYRk9erVHDp0yG3bjh07AJg6darH8fn7pHCpqamm/6Vq9h513/XSUY5UPLOPMTB/j8qRklOGeIfZx5jZ+4PS58iuXbvY+ttWt23ZpzILPLZmzZqFPk96ejoA0dHRbtujoqIAyMjIKFVdIiIlVewExvbt29m+fXuB+5YsWVLgdovFUr6qTC4yMtLXJXid2XsszWmpBzhDGp7XjqZxpiJL8mvKkYpn9jEG5u9ROVJyyhDvMPsYM3t/UPociWl+EW2axbttP3PoJD++9GWpXtcwjCL3W606yVtEvKPICYwFCxZUVh1BpaCFkMzG7D2Wpr9q1KI68R7bLUFyBZdyxDvMPsbA/D0qR0pGGeI9GmOBr9Q5YosH96tBCA0p/WKb+bcGzsx0P3sj/8wL3TpYRLylyAmMxMTEyqojqKSkpFC3bl1fl+FVZu/xxIkTvi4hYChHvMPsYwzM36NypGSUId5j9jFm9v7Adzly8cUXY7PZOHDggNv2/O8vXBtDRKSimP+jGxERERERqTDh4eG0a9eOVatWuV1O8sUXXxATE0OLFi18WJ2ImFmRZ2DMmjWrzE88ceLEMj9WRMxDOSIi5aEMEfFPd911F2PHjuW+++5j8ODB/Pzzz7z++uvcf//9VKlSxdfliYhJVdgExoWLZelNg4iAckREykcZIuKfOnXqxMyZM5kxYwZ333038fHxTJ48mdtuu83XpYmIiVXIBEZubi6vvfYa27ZtA6B9+/blr0xETEE5IiLloQwR8b0hQ4YwZMgQj+29e/emd+/ePqhIRIJVkRMYvXr1KvYJNm3axCOPPMLu3bupWrUq//jHPxg6dGiFFSgigU05IiLloQwRERGRfEVOYBQlIyODadOm8cEHH+B0Ornuuut4+OGHqVmzZkXWZ0pxcXG+LsHrzN5jbGxsiY/dbWQS7Uz32J5hZBZwdHBRjpSd2ccYmL9H5Uj5KUPKx+xjzOz9gXJERIJPmSYwkpKSePLJJ0lJSaFOnTo89thjdO/evaJrMy3dlzzwlaa/Ws44Yp21PLanOR0VWVLAUY6Uj9nHGJi/R+VI+ShDyk9jLPApR0Qk2JRqAuP48eM88cQTJCUlYbFYGDlyJPfddx9RUVHeqs+UTpw4QXx8vK/L8Cqz95iamurrEgKWcqRimH2Mgfl7VI6UjTKk4ph9jJm9P1COiEjwKfEExnvvvccLL7xAeno6l156KU899RStWrXyZm2mZfZfpmD+HnV6ctkoRyqO2ccYmL9H5UjpKUMqltnHmNn7A+WIiASfYicw9uzZwyOPPMKmTZsIDQ3l3nvvZfz48YSElHn5jKCXnp5OTEyMr8vwKrP3mJmp60VLQzlS8cw+xsD8PSpHSk4Z4h1mH2Nm7w+UIyISfIq9jeqrr76K3W6nefPmPP744zRu3Jjs7Oxinzg6OrrCijSbYPiFavYe9Yah5JQj3mH2MQbm71E5UjLKEO8x+xgze3+gHBGR4FPsBEa+7du3c/PNN5foSS0Wi+s+7CIS3JQjIlIeyhARERHJV+QERvv27SurDhExKeWIiJSHMkRERETyFTmB8fbbb1dWHSJiUsoRESkPZYiIiIjk0+pXIl623Z5Nldwsj+1n7cVfvy0iAsoRESk/5YiImEGJJzAOHTpE9erViYyMdG07duwYH3zwAfv27aN27doMGjSIpk2beqVQM7HZbL4uwevM3qPVai3xsfWcscQ4q3tsT3fmVGRJAUE5UnHMPsbA/D0qR0pPGVKxzD7GzN4fKEdEJPgUm3q7du1iyJAh9OrVix9//NG1ffv27QwYMIDZs2ezbNky5s+fz+DBg1m4cKFXCzaDYLgvudl79PZ915ctW8b1119Py5Yt6devH5988kmRx+/YsYNx48bRunVrOnTowOTJk0lJSfFqjaWhHKl4Zh9jYP4evZ0jZqIM8Q6zjzGz9wfKEREJPkVOYKSmpjJq1Ci2bdtGq1atiIuLA8DpdDJ58mROnz5Ny5Ytee+993jvvfe46qqrePrpp/nll18qpfhAlZ6e7usSvM7sPXrztmXLly/ngQceoEuXLsyePZvExESmTJnCypUrCzz+wIEDjBgxgoMHD/LEE0/w/PPPc/z4cYYPH86ZM2e8VmdJKUe8w+xjDMzfo25/WDLKEO8x+xgze3+gHBGR4FPkBMYbb7xBWloazz//PO+99x5XXnklAOvXr+f3338nPDycmTNn0rp1a1q3bs0rr7xC1apVWbBgQaUUL5Uj156JPe+sr8sIGtOnT6dfv35MnTqVrl278q9//Yt+/frx8ssvF3j8ggULyMvL44033mDAgAFce+21zJ07F8MweO211yq5ek/KEQHISIGcDF9XIYFIGSIAhgHpRyBXf6+LiAS1Iicwvv76a9q1a8eAAQPctn/11VcAdOnShdq1a7u2R0dHc+2117Jx40YvlGoeMTExvi6hxHYkJ7Hs6Ht8fvhdDh35ocSPC6QeyyIqKsorz5ucnMyBAwfo06eP2/a+ffuyZ88ekpOTPR6zd+9emjZtSt26dV3bwsPDufLKK/n666+9UmdpKEe8I1DGmGHAp+NgTguY1RR+ebfkjw2UHsvKWzliNsoQ7wmUMeZ0wId/hbktYVYz+H15yR4XKP2Vh3JERIJNkRMYhw4donnz5h7bf/jhBywWC1dffbXHvvj4eE6ePFlxFZqQP61NUJTsnDS2sZ8ou5Uwh4VNOVvB6SzRYwOlx7I6ceJEqY7PzbWTmZnp9pWV5bkS+J49ewBo1KiR2/YGDRoA5yYrLnTRRReRkpJCXl6e2/aDBw8WOOFR2ZQj3hEoYyzlF9i1FCKqgzUE1j7GuVmNkjw2QHosq9LmSLBShnhPoIyxfWth/1qIqAEYsPohSpQjgdJfeShHRCTYFDmB4XQ6CQlxv1HJyZMn+eOPPwDo2LGjx2PS09PdVgevTB988AF9+vShZcuW3Hzzzfz8888+qaM4DofD1yWUiNVixQI4Lee+rBln4J/94ZGhsGdbkY8NlB7LylnCiZx8u3btYtmyZW5fa9as9jgu/3rd6Ohot+35n7BkZHiegz9o0CCOHTvGQw89xOHDhzl58iTTp0/n999/5+xZ31/6oxzxjkAZY7ZQwAKGEwyHgfXsCZg7AuZPgOMHinxsoPRYVqXNkWAVaBkCypGKZgv9///hBKfDie3UAXjsRpg2DlKPFvq4QOmvPJQjIhJsipzAqFu3Lvv27XPbtnbtWte+xo0bezxmw4YN1KtXr8IKLKklS5bw2GOPMWDAAGbOnElMTAzjxo3zi0+gA1VYWAxtLU3JsToh9ywtt2zh7GU1cFjPwqL/+Lq8gLElK4eMBo2of931bl8XXdvD41ijmE+UCrpdWrt27Xj66adZs2YN3bt35+qrr2bPnj0MHz6ciIiICuujrJQjwa3W5dB6HOSkg9XIotuQeWSFGDhyTsHqeb4uL2BsycphQ6bn15Ys89/+MJAyBJQj3lC/C1x2E2SfhjDLGfp2fA5yciD1EHw219flBYxgzhERMY8iJzCuvfZavvnmG9dK3rm5uSxYsACLxcINN9zgcfySJUv4448/6Nq1q3eqLYRhGMycOZO//vWvTJw4kWuvvZY5c+ZQvXp13nrrrUqtxWwaJHRlYP0xdN+1h1rkYAmH3EurYtj1y66kGhkxXEZ1LrfWcPu61Op5L/b863UvXFU8/8yLwq7nHTJkCN9//z0rV65k3bp1zJw5k/T0dKpVq1axzZSBckR6PwP3JTsY/8RELmn4PdaMTHIdOZCb6+vSAkYjI4amzqoeX40M81/jHygZAsoRb7Fa4cY5MGlvFvcMH8rFMRvgVBqkp0Nutq/LCxjBnCMiYh4hRe28/fbb+fjjjxk1ahStWrUiOTmZI0eOULNmTcaOHes6buPGjSQlJfHOO+9QtWpVRo8e7fXCz7d//34OHTpEjx5/fqIdGhpKt27d+Oabbyq1FrOyObIwrGGQl4vFasHoOxyLr4syofy1Lw4cOECzZs1c2/fv3++2/3y7d+9m69atDBw40G3/tm3buPzyy71ccfGUIwJgdWZhy83BCA8HZx6WXDtG4l+UI1KsQMkQUI54my3jGDgNiDiXI+TYoXfl/zuLiIjvFHkGRlxcHIsWLaJly5Zs2LCBI0eOcMUVVzB//ny3T3b/7//+jzfffJOoqCheeeUVatSo4e263eSfWpq/0GG++vXrc+DAgaC4BtKbLFYbRvylWCKqYImKxohvgqVDf1+XZUoNGjQgISGBlStXum1ftWoVDRs2dLvTSL5du3YxefJkt9OTv//+e3bs2EGvXr28XnNxlCMCYAmLwlm3ERZLKBZLOEajlliadfB1WRIAAiVDQDnidTXqQfxFYA0DawS07QoNfT9RLyIilafIMzDg3Ce+b7/9NllZWezfv5+EhASP09hHjRpFTEwMAwYM4OzZs3z33Xd06tTJa0VfKP/0+gtvJRUVFYXT6eTs2bMeiyL27NnT7ftbb72VcePGER8f71pIMSYmhpSUlALfcMTExLj216xZE7vdTmpqaoH1xcfHY7fbycrKIi4uDsMwOHz4sMdxERERxMXFkZqaSmRkJKGhoYWuoB0XF0doaCgnTpxw1Zxf9/lsNluF9GTUG0JolZ/BmUdejTbEG2DPznb1lJqaSnb2n6dx5uTkcPjwYb/u6UIX/jtd2FO+iIgIr9627O6772bq1KnExsbSrVs3Vq9ezYoVK5g+fToAqampHDhwgEsuucR1u8CEhAQmTZrEPffcQ2pqKs888wytWrXyuO2gryhHlCMARttxhMb/gAULufUSqeN0FjnmlCOSLxAyBJQjlTHmLH99iMitazFCw8lq0YN4h6PQMZefIf7e04WUIyIihSt2AiNfZGQkQ4YMYeLEidx9991u++68807Xf8+bN4+FCxeyadOmiquyGPkLH1osBZ+MXND21atXk5CQUODx578pio+PL/K18/fbbLYCPx3PZ7PZXAsqFrewWFxcnOu/i3rO818//xdjYSqkp/oN3Y47v6fzay6I3/Z0gZL2VNAtUCvKkCFDyM3NZf78+Xz44YfUr1+f5557jv79z531snbtWqZOncqCBQvo0KEDkZGRvPbaa/z73//mvvvuo0qVKvTv35/77rsPm83mtTrLQjlSsKDKkYvdF1xUjkhp+HOGgHIkv2bvjrm60OQyAKr9/y3KERGR4FHkBMa3337L7t27Xd8bhsHmzZtZsGBBgcfb7XaWL19e6X80nb/wYc2aNV3bMzMzsdlsfjc7nZKSUuwvuECzJfcEpxy5dAivRRVrqCl7PJ+377s+bNgwhg0bVuC+IUOGMGTIELdtjRo14rXXXvNqTWWlHPEOM46x1MztOJ3ZVI+8EpstxJQ9ns/bOWIWgZIhoBzxNYfDQXrWNpzOPGKjW7rOoDAz5YiIBJsiJzCqVq3Ks88+i2EYGIaBxWJh3bp1xS5ENXLkyAotsjj515omJye7XXeanJxMw4YNK7WWkjj/TY0Z/PfM7yw8lQsGNIw4wbzal5uuxwsV9wmP/Ek54h1mG2N7jr1PzpkdAJwIX8eldSeYrscLKUdKJlAyBJQjvrb32Ducch4FICbzRy6NH+fjirxPOSIiwabICYwrr7ySOXPmkJqaimEYPPjgg/Tq1cvjek04d1pkSEgI8fHxtG/f3msFF6Rhw4ZcdNFFJCUl0aVLF+DcJzBr166lW7dulVpLSdjtdr87tb88lp7JpooVwiwG+3Js/Jx7gtZUM1WPF7Lb7SU+9sczdkIiPI/PO1Py5whkyhHvMFuO5JzZhWEJAayQk0qW/SChxJuqxwspR0omUDIElCO+5HBkc8p5FJtxrp90TpGRdYzYmIt8XJl3KUdEJNgUuwbGtdde6/rvH3/8sdA3Db5ksVgYP348Tz75JLGxsbRt25Z33nmHU6dOMWbMGF+X5yE1NbXY6y4DSfUQgwPZNhxWAwsQb6tC6nFz9XihtLS0Eh/bjEiqWDxPGz5LJMFy4qdypOKZLUcMWxiWvBwMixMDC6EhVUk9Zq4eL6QcKblAyBBQjvhWKDasODm3gKbVsHAmLYfYwpeuMAXliIgEmxIv4gnwzDPPeKuOchsxYgQ5OTksWLCAN998k+bNm/P6669Tv359X5dmeo/XaMBTJ/dzxgHDYmNoFBrLYTJ9XZb4KeWIFKRunZs4kvIpkEeN6l2JCKkOnPV1WeKH/DlDQDniKzabjcZV+3MgbRUGBgkx3cnJDPN1WSIiUsFKNYHh72677TZuu+02X5cRdBqHxjK/TktflyFSIZQjvlGtShOqNZzk6zJEKoRyxDeqxVxBtZgrXN8fzvS8RayIiAQ2q68LEBEREREREREpjiYwRERERERERMTvaQJDRERERERERPyeJjB8ID4+3tcleJ3Ze6xZs6avS5AgZ/YxBubvUTkivmb2MWb2/kA5IiLBx1SLeAYKM913vTBm77E0913fdCIPK3ke250nPLeJlJTZxxiYv0fliPhaMIwxM/cHyhERCT6awCjKzz/C50ugfmMYPhrCKuZ2XFlZWURERFTIc/krs/eYnZ1d4mM7hYYTGer5/yIrNJw1FVmU+KUzp37ndOp2Qmo0pE5sC6yWijnxzexjDMzfo3JESirj1G+cPfMHIdUaU61qCywWS4U8r9nHmNn7A+WIiAQfTWAU5vdt8OTdkH4G8vLg23Xw6psV8tRxcXEV8jz+zOw9xsbG+roECQBnj+8id9nzxGRn4rRZ+H3AGJrV6VMhz232MQbm71E5IiWRfXwLjg2vYTHyyAkNYW/H4TSO7Vwhz232MWb2/kA5IiLBR2tgFGbz95B2Gk5nnJvA+HoFbNtWIU+dmppaIc/jz8zeY1pamq9LkACQt/cnIs6cIsJ+lujTacSs/5gsZ0aFPLfZxxiYv0fliJSE8+DPOJw5GCEGVc5mkp68lrNGToU8t9nHmNn7A+WIiAQfTWAUpunl4MiDUCvYrJBjwO+/V8hTl+Z0v0Bl9h5zcirmzaOYW3i1BoTmZONwgsNmo0peHjm5pyvkuc0+xsD8PSpHpCRCYuoRkmfHmufAabEQZsmrsAkMs48xs/cHyhERCT66hKQwLTtA3Sth+09wPAus0dCpk6+rEpEAEnZJR7KatMByfC+5VaPIq12X6qG1fV2WiASQ0EbdOHn6R4z0FDKqxZITdzGxlmhflyVBIC8vj7Zt23pMkkRGRvLzzz/7qCoRCXaawCiMxQL/fRdeew1SUmDMGKitPzxEpBSsViJveJTsff/DMHKIbnANIbaKWQxYRIKDxRZK7db3cvzMJqIsBg1j2mKroMWARYqyd+9ecnJyeO6552jYsKFru9Wqnz8R8R1NYBQlKgruvdfXVYhIIAuPIqJZP19XISIBzBYSRZ24rr4uQ4LMjh07sFqt9O3blypVqvi6HBERQBMYADiddo4fXkLO2WTCwutSq95fsOlTUqkgW5MNQtKdHtvzThs+qEa8xeHI5vjhxeRmpxAW2ZD4uoOwWGy+LktMQjkSHJw56eRtfxMj6xjUaEFY06FYdLaFVJDS5sj27du5+OKLNXkhIn5FExjA6ZPfkpm+DUd2LrlZx0k9kUrz1ndh8dIpcma/JzmYv8fw8PASH9u5SijRUZ4TYhk5oXxSgTWJb506toaMzD1kWCyEpv/C9pMRdK95vddez+xjDMzfo3JELpT3x0fY03aTabMQevQbtlWNps1FypGyMnt/UPocCQsDe26u2/ZMcllVwPE7d+4kLCyMcePGsWnTJkJCQujXrx+TJ08mOlrrsIiIb2gCA3DkpeO052JxODGsVkJO7uVY6kHia17sldcLhvuSm71H3XddLuTIO0MOThy2MMLy8jjmOM4hI5N6liivvJ7ZxxiYv0fliFzIyDlFtsWJwxpKqMNCSu5+zhg5VLWU/I/U0jD7GDN7f1D6HNm1axe/bf3NbZuRc7rAY3fs2EFGRgZDhw7lb3/7G1u3bmXmzJns3buXBQsWYLFYylq2iEiZaQIDqFrtKk6nfI8lxAJOcB7LxTC8d1puamqq6X+pmr1H3XddLlQ1LpGUrF2E5+WSYw3l95i6XI1ypDzM3qNyRC5kTegOO/cRYbeTHRrB7lr16KQcKTOz9welz5GmTZvS6LwFOQEyTx9m1baZHsdOnz6d2NhYmjVrBkD79u2pUaMG//jHP1i/fj1XX311mesWESkrTWAA4VXqEllzKGk/LcJy6iw5l3QivkZ9r71eZGSk157bX5i9x2A4LVVKp0pUY+wNhvNDzm8ci4ihqa0+CV681aHZxxiYv0fliFwotHZrDlQZyx9nf+FI1TiahjQl1uK9nxOzjzGz9welz5Gw0DDCQt0vRzNyCj5TMDEx0WNbt27dgHNnZ2gCQ0R8QRMY/1+9hNbUqdOCPEcO4eHeOeU7X2hoqFef3x+YvUez9ydlc1XEpVwZ3phcnERbvPszEgw/g2bv0ez9Sdm0iWlN8+grMIAqypFyMXt/4L0eT548yZo1a+jYsSP16//5oV52djYA1atX98rriogUR0tbn8cWEuL1yQuAlJQUr7+Gr3mjxxynk+WrT/Du3FT27bJX+POXxokTJ3z6+uK/wiw2r09egHKkrHKduew+/RW/n1xGZs6xCn/+0vB2jixbtozrr7+eli1b0q9fPz755JMSP/bIkSNcddVVvPLKK94rUAoVYQn1+uQFmD9HvNWf3Z7BH6e+ZGval2Q6TnvlNUrKWzlisVh49NFHeeedd9y2L1++HJvNxlVXXeWV1xURKY7OwJCA8eb8Axx4og44LeycmcfEz2zUaqw5OBEpuc2p7xJ2JhnDAn+k/0bzuncRFlbV12VVuOXLl/PAAw9w66230qVLF5KSkpgyZQoRERFcd911RT7WMAwefPBBMjIyKqlakcBhGE42nHqPg1FWnMCerMNcFzWcMKvnXYICWVxcHCNGjODtt98mOjqadu3a8dNPPzF37lxGjBhBgwYNfF2iiAQpTWBIQDAMg+SPorFYDSzV7eSdDOWXpBx63uH/9ybfudsgPMVzEbacDO8tzCYinvKcuTjOHiPPasOw2gjJs3Mqdy/xYa18XVqxSpsj06dPp1+/fkydOhWArl27kpaWxssvv1zsBMa7777Lnj17yl+0iAk5ck5zuApgQKgBmVYHx4zjJFDP16UVq7Q5MmXKFOLj4/noo4949dVXiY+P5+9//zu33367t0sVESmUJjAkYMRcnM6ZX2Iw0kOxWCC+sc3XJZVI+2ohVI31HGpnQkLY7oN6RIKVzRJCbngVqmScxnA6MKxWwkJr+bqsEilNjiQnJ3PgwAEmTZrktr1v376sWLGC5ORkt2vaL3zstGnTePnllxk/fnxFlS9iGtaQSEKzIDscHBhYsRJlifF1WSVS2vcjoaGhjB8/XlkgIn5F599LQLBYLNz+SBVqdE8hLCGT9hPTuaKn+RfnEpGKY7FYaVa1P6er1uJMdDUianShWthFvi6rxOy5uWRmZLh9ZWVlehyXf/ZEo0aN3Lbnn/K9d+/eAp/f6XTyz3/+k379+nHNNddUcPUi5mANieBqazsicq1YjVCusF5Jdav5LkMTEfFXOgNDAkaNhDpMeqf440REClMnogl1Ipr4uowy2blrF79t/c1tmyPvlMdx6enpAERHu9/GNyrq3CLVha1t8dZbb3Hw4EHmzp1bEeWKmFataq0ZRGtflyEiEpQ0geEDcXFxvi7B68zeY2xsrK9LkCBn9jEG5u+xtDnSrGlTGjVs6LYtPf0wixe97LbNMIpeX8dq9Tz5cvfu3bz00kvMmDGDmJjAOB1eys/sY8zs/YHej4hI8NEEhg/ovuSBz+z9if8Lhp9Bs/dY2v5Cw8IIDXO/04HD4Xnr7/wJiMxM98tL8s+8uHCCwuFwMHXqVK677jquvvpq8vLyXPucTid5eXmEhOjtghlpjAW+YOhRROR8WgPDB7x1z25/YvYeU1NTfV2CBDmzjzEwf4/eypH8tS8OHDjgtn3//v1u+/MdOXKELVu28Mknn3DFFVe4vgBmzpzp+m8xH7OPMbP3B3o/IiLBJ6AmMDIyMujevTsrV6702Ldx40aGDh1Kq1at6NOnD4sXL/ZBhSUTHx/v6xK8zuw91qxZ09clSBkpRwKH2Xv0Vo40aNCAhIQEj5/xVatW0bBhQ+rWreu2vXbt2ixevNjjC2D48OF+PQ58RTkSGMzeH+j9iIgEn4A5JzQjI4MJEyZw+PBhj327d+/m9ttvp3v37txzzz2sW7eOhx56iOjo6GLvd+8L6enppr/GOOB73P07rF4JjS+BHn3hgmvGLzw1uyj7fjWoEuF5TfrZ7KKvU5eKpxwJLAHf45YtsGoVXHkl9O0LFovbbm/myN13383UqVOJjY2lW7durF69mhUrVjB9+nTg3Ke2Bw4c4JJLLiE6Oporr7yywOepXbt2ofuClXIkcJiiv59+hHXfQOu2cM21lZojIiL+KCAmMDZs2MBjjz3GyZMnC9z/6quvUq9ePV588UUsFgvXXHMNp06dYvbs2XrD4CMB3eMfv8PowZCeBoYTRo6Hfz7udkhp3jC0jLdQLdrzZKfTGRb+V95apcSUI4EnoHvcuBFj4ECc2ZlYLBYsT/wLy4S/ux3izRwZMmQIubm5zJ8/nw8//JD69evz3HPP0b9/fwDWrl3L1KlTWbBgAR06dChVa8FMORJYAr6/dV9jjBoBWZmAFf7zIpbRt7odovcjIhJsAuISkrvvvpumTZvy2muvFbh//fr1dOvWDct5s9K9evVi165dpKSkVFaZYhZrvoRTJyE989zXm/Pg2DFfVyXlpByRSrV0Kc6z6TijbBiOXHLfnYORm1WpJQwbNoxVq1bx66+/snz5cgYNGuTaN2TIEHbu3Fnk5MXOnTuZMGFCJVQaOJQjUpmMz5bAmTTIs0N2FsZTj0BOjq/LEhHxqYA4A2PhwoU0bdqUgwcPeuzLysri2LFjNGjQwG17/fr1Adi3b19QXAMpFajJpZCTCzYLWGyQ54Dtv0Ltnr6uTMpBOSKVybj8cgzDgSXTCU6DnEZxWDKPERbW0NelSTkoR6RSNb8M8vJcl40YVgfG0SNYGjT0bV0iIj7k0wkMu93usUr6+WrWrElsbCxNmzYt9Jj828JFRbnfSi7/+/z9F+rZ0/2P0VtvvZVx48YRHx9Peno6cO5WcykpKTgcDo/Hx8TEuPbXrFkTu91e6ErQ8fHx2O12srKyiIuL4+zZswVeOxsREUFcXBypqalERkYSGhpa6Cc2cXFxhIaGcuLECVfN+XWfz2azVUpPqampZGdnu/anp6dz+PDhwOypdVviLr8c5+ZfMUJCMeJiORFbDef//zeLiIgo8PHiG8oRdwE55kyYI5m9epJz5w3Erfqe3GZ1OD2yK7Z0B5azyhF/pBxxF4hjrqAcyc+QgO2p3wBiP/gvll/34KwSjqNJAicdTtD7EREJYj6dwEhJSXFdj1uQqVOnMmbMmCKfwzDOLTxkuWBRo3xWa8FXyaxevZqEhIQC951/vWRxn5bk77fZbB4ru5/PZrO5ftFUqVKlyGPj4uJc/13Ucee/fv4vxsJ4u6fzawY4fPiw22MDrqd3P8U6fxqknIC/jqVO66vcjtu3b1+RryeVRzlSsIAbc5gvR9L/PZ/dE9dgzc2ibp2uxEa6P0Y54j+UIwULtDF3Yc0XZsiF+/2/p7oY7y3D+dFcLGdzCBs4nrr1GrsdpxwRkWDj0wmMhIQEdu7cWa7niI6OBjwXMcr/3h8Xb7LZbL4uwesCqceU3LOctJ+lfpVYYqz/v+6qcfB/Txf6mMLeiErlU46YVyD1eMKZQYaRTR1rNSIs5361xtiiuCLhxkIfoxzxH8oRcwq0/tLzTpJrZBMbUpsQSygAlqoXYRv7r0IfoxwRkWATEGtgFCUqKopatWqRnJzstj3/+0aNGvmirCIFwzWwgdLjgcM7SPviv1Q7c4LNja/kyn73UC0kvNjH6b7r5qIc8U+B0uOezO0cP5FEeE4m+6Prk1jnr1Sxhhb7OOWIuShH/E8g9XfixLfk7V1BaOZZkms2pv7ldxCiHBER8WCKadtOnTrx1VdfuV1zmJSURNOmTalRo4YPKytYQddRmk2g9JizbjGN9m6jzolDdNywkuyVs0v0uNLctkwCg3LE/wRKj2dO/I+4I8lUPXWChCNbSNn3WYkepxwxH+WIfwmU/gzDibH3S2J37iN63yHqbv2OnPXvl+ixyhERCTYBfwYGwLhx47jpppu49957GTp0KOvXr2fp0qW8/PLLvi5N/FzsqdOEG3bysGFz5BH3x0YwDNeK3xXh0I+QVsBIy8irsJeQCqAckbKKTMvAZnXidFiw5jkJOfwLNB5Soa+hHAkMyhEpGwvhp9KxOvJwhtiwOAzCN6yFLiMr9FWUIyJiBqaYwLjsssuYM2cO06ZNY+LEidStW5dnnnmG6667ztelFcgfr4OtaIHSY1yr/rB/M6EhTggLIySkWokmLy5cZb4ozRtYiKvi+ZypZy0sOVqaasWblCP+J1B6rGdpxTHnEaxhVmxpuURao0v0OOWI+ShH/Eug9GexWIiIaIdhO4rVZsG25zQhtRqW6LHKEREJNgE1gVHUIltdu3ala9eulVxR2aSkpATUdZllESg9hnToDn/shi0roVocDJ5cosedOHHCy5WJtyhHAkeg9Bh1RQ/qLd1HxqnN2GJqEtN7dIkepxwJXMqRwBBI/YV1HoqxMxl2rsdSuxHcfH+JHqccEZFgY4o1MALN66+/7usSvC5gerRYYOR4+M9H8NB/oV7DEj3sgw8+8G5dIsUImDFWDgHTo9VK2KDbiRs7i9ibHscaW7tED1OOiK8FzBgro4DqLyQEy/jJWKZ9ApNnQmxcsQ8B5YiIBB9NYPjAW2+95esSvM7sPX700Ue+LkGCnNnHGJi/R+WI+JrZx5jZ+wPliIgEH01giIiIiIiIiIjf0wSGiIiIiIiIiPi9gFrEsyLk35v96FHfLrd88OBBn75+ZQiGHvN/noqSllPwz1ph28X/KUcqTzD0qBwJTsqRymH2/vIpR0QkWATdBMbx48cBGDFihE/r6Nmzp09fvzIEQ4/Hjx+nQYMGBe6Ljo4mNjaW2ZsL/1mLjY0lOrpkt1wU/6EcqTzB0KNyJDgpRyqH2fvLpxwRkWBhMQzD8HURlSk7O5utW7dSq1YtbDabr8uRAOVwODh+/DgtWrQgIiKi0ONOnz5NRkZGofujo6OpVq2aFyoUb1KOSEVQjgQ35YhUBOWIiASboJvAEBEREREREZHAo0U8RURERERERMTvaQJDRERERERERPyeJjAqyaZNmxg1ahTt2rWjS5cuTJ48mRMnTrgds3HjRoYOHUqrVq3o06cPixcv9lG1ZffBBx/Qp08fWrZsyc0338zPP//s65LKzOFw8MYbb9CvXz9at25N//79eeedd8i/6sowDObMmUO3bt1o1aoVY8eOZffu3T6uWsxMORJ4lCPib5QjgUc5IiLyJ01gVILdu3czZswYoqKieOGFF5gyZQqbNm1i3Lhx2O121zG33347CQkJzJw5k27duvHQQw+xcuVKH1dfckuWLOGxxx5jwIABzJw5k5iYGMaNG0dycrKvSyuTV155hRdffJEBAwYwZ84c+vXrx9NPP81rr70GwOzZs5kzZw633XYbL774Iunp6YwZM4b09HQfVy5mpBxRjoiUl3JEOSIiEvAM8brHH3/c6NGjh5Gbm+vatmXLFqNp06bG2rVrDcMwjMmTJxv9+/c3nE6n65gHHnjAuOGGGyq93rJwOp1G9+7djUcffdS1LTc31+jRo4fx5JNP+rCyssnLyzPatGljTJ8+3W37448/bnTs2NFIT083WrdubcybN8+17/Tp00abNm2M+fPnV3K1EgyUI8oRkfJSjihHREQCnc7AqASXXHIJt912G6Ghoa5tjRs3BuDgwYMArF+/nm7dumGxWFzH9OrVi127dpGSklK5BZfB/v37OXToED169HBtCw0NpVu3bnzzzTc+rKxsMjIyGDRoEH369HHb3qhRI1JTU/n+++/Jyspyu798bGwsiYmJAdmv+D/lSOCNK+WI+BvlSOCNK+WIiIi7EF8XEAxGjBjhsW3NmjXAuTcOWVlZHDt2jAYNGrgdU79+fQD27dtHfHy89wsth3379gEU2MOBAwdwOBwBdZ/72NhYHn30UY/tX331FXXq1HG9icv/N8qXkJDg+rcVqUjKEeWISHkpR5QjIiKBThMY5WS32zlw4ECh+2vWrElsbKzbtiNHjvD888/TokULOnbsyPHjxwGIiopyOy7/+4yMjAquuuLl11hQD06nk7NnzxIdHe2L0irMhx9+yPr163n44YfJyMggLCyMsLAwt2OioqIC4t9L/Ity5BzlyDnKESkL5cg5ypFzlCMiYlaawCinlJQU+vfvX+j+qVOnMmbMGNf3R44cYcyYMTidTqZPn47FYnGtIn3+6Zrns1r9/0qf4noobHugWLp0KY899hh9+/Zl5MiRzJs3z7S9SuVTjpyjHPlToPcqlU85co5y5E+B3quISEE0gVFOCQkJ7Ny5s0TH7tq1i/Hjx5OXl8f8+fO5+OKLAVyfBGRmZrodn/99TExMBVbsHfk1ZmZmUrNmTdf2zMxMbDabxychgeSNN97gueeeo0ePHkybNg2LxUJMTAy5ubnY7Xa3a4kzMzMD4t9L/Ity5BzlyDnKESkL5cg5ypFzlCMiYlb+P5VuElu2bGHEiBHYbDYWLlzIZZdd5toXFRVFrVq1PG7vlf99o0aNKrXWssi/1rSgHho2bOiDiirGiy++yLPPPsvAgQOZMWOG6xTNBg0aYBiGa9GzfAcPHgyIfy8JTMqRwKQcEX+iHAlMyhERkXM0gVEJkpOTGT9+PDVr1mTRokUF/gLt1KkTX331FQ6Hw7UtKSmJpk2bUqNGjUqstmwaNmzIRRddRFJSkmub3W5n7dq1dOrUyYeVld1bb73FvHnzGD16NM8++ywhIX+esNSmTRvCw8Pd+k1LS2PDhg0B26/4N+VIYI4r5Yj4E+VIYI4r5YiIyJ90CUklePrpp8nIyODRRx/lyJEjHDlyxLWvbt261K5dm3HjxnHTTTdx7733MnToUNavX8/SpUt5+eWXfVh5yVksFsaPH8+TTz5JbGwsbdu25Z133uHUqVNu19wGimPHjjFt2jSaNm3K9ddfz5YtW9z2t2jRgpEjR/Lyyy9jtVpp2LAhc+fOJTo6mqFDh/qoajEz5cgYX5dXasoR8TfKkTG+Lq/UlCMiIu4sRv5qR+IVdrud1q1bk5eXV+D+yZMnM27cOAC++eYbpk2bxp49e6hbty533nknQ4YMqcxyy23+/PksWLCAU6dO0bx5c6ZMmUKbNm18XVapffzxx0ydOrXQ/d999x1Vq1blpZdeYsmSJWRlZdGmTRseeughmjRpUomVSjBQjihHRMpLOaIcERExA01giIiIiIiIiIjf0xoYIiIiIiIiIuL3NIEhIiIiIiIiIn5PExgiIiIiIiIi4vc0gSEiIiIiIiIifk8TGCIiIiIiIiLi9zSBISIiIiIiIiJ+TxMYfu6XX35h3bp1bttSU1N5/vnn6d+/P61bt6Zt27bceOONTJs2jdTUVI/nGDVqFM2aNWPGjBmFvk5SUhLNmjVj5syZrm0//PADzZo1K9HXwYMHS91bVlYWPXr04N///nepHysiJaccEZHyUo6IiIg/CPF1AVK4tWvXctdddzFlyhS6dOkCwJ49e7jllls4c+YM11xzDddccw15eXls27aN//73v3z44Ye89dZbXHbZZR7P9+qrr3L99dfTpEmTUtVx2WWX0atXryKPqVq1aqmeMy8vjwceeIBDhw6V6nEiUjrKEREpL+WIiIj4C01g+LHU1FScTqfbtscee4zMzEzeffddWrdu7bbvk08+YcqUKUydOpUlS5Z4PJ/dbufhhx/m3XffxWKxlLiO5s2bc88995Sph4KcPn2aSZMm8e2331bYc4pIwZQjIlJeyhEREfEXuoQkgGRmZrJhwwZat27t8WYBYNCgQbRp04Zt27aRnJzssf/yyy9n06ZNLFq0qBKqLdiyZcvo378/3377LVdffbXP6hAJVsoRESkv5YiIiPiKJjD81D//+U+mTp0KwDPPPEOzZs3Yv38/AAcOHCA7O7vAxz344IPMmzeP6tWre+x7/PHHCQsL44UXXiAlJcV7xRfh/fffJyIigrlz53LnnXf6pAaRYKEcEZHyUo6IiIg/0QSGn+rVqxc9e/YEoEuXLkycOJGEhASuuOIKjh49yl//+lcWL17MiRMn3B7XsmVLunXrRnR0tMdzNmrUiLvuuouMjAyeeOKJSunjQhMmTGDFihV0797dJ68vEkyUIyJSXsoRERHxJ1oDw0/16tWLM2fOsHr1arp27cqYMWOAc59+jB07lp07d/LQQw8BcMkll9ChQwe6d+9O586dsdlshT7v+PHjWbFiBUlJSaxatYo+ffoUW8v27dvdVgO/UKtWrbjmmmtK1FenTp1KdJyIlJ9yRETKSzkiIiL+RBMYAaZZs2YsW7aMN954g88//5xDhw7xxx9/8Mcff7Bw4UKaNm3Kc889x+WXX17g40NDQ3nyyScZPnw4Tz75JJ06dSImJqbI19yxYwc7duwodP/o0aNL/IZBRHxPOSIi5aUcERERX9AERgCKi4vj/vvv5/7772f37t18//33rFu3jnXr1rFr1y7Gjh3L0qVLiY+PL/DxrVu35pZbbuGdd97hP//5T7Gnbw4ePJhnn33WG62IiI8oR0SkvJQjIiJS2TSBEeCaNGlCkyZNGDFiBCkpKdxzzz1s2bKF999/n7///e+FPu6+++4jKSmJDz74gAEDBlRYPW+++Sbp6elu2xITE+nQoUOFvYaIVCzliIiUl3JEREQqgxbxDCDz5s2jS5cu/PjjjwXuj4+Pd60Unr9CeGGio6N57LHHMAyDRx55hNzc3AqpccGCBcyaNcvta8OGDRXy3CJSfsoRESkv5YiIiPiKzsDwYxaLxe376Ohojh8/zpdffkn79u2LfEzt2rWLff4ePXpw3XXXsXLlSv773/+Wv2BgzZo1FfI8IlIxlCMiUl7KERER8Rc6A8OPhYScm1+y2+0ADBgwgNjYWBYuXMjHH3/scXxGRgYvvvgiVquVgQMHlug1Hn74YapWrcq2bdsqrnAR8RvKEREpL+WIiIj4C52B4cfyF71atGgRaWlpjBo1itmzZ3PXXXcxdepUXn/9dRITE6latSpHjx7l66+/Ji0tjalTp3LZZZeV6DVq1arF5MmTefjhhws9prjblgF07dqV1q1bl7g3EakcyhERKS/liIiI+AtNYPix9u3bM2LECD799FMWLlxI586d6dy5MytWrGDBggWsW7eOzz//nKysLOLi4ujcuTOjR48u9S/um266iaVLlxZ6bWhxty0DiImJ0RsGET+kHBGR8lKOiIiIv7AYhmH4uggRERERERERkaJoDQwRERERERER8XuawBARERERERERv6cJDBERERERERHxe5rAEBERERERERG/pwkMEREREREREfF7msAQEREREREREb+nCQwRERERERER8XuawBARERERERERv6cJDBERERERERHxe/8PGaEAArN6DD4AAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df = df_device.iloc[:,2:]\n", - "\n", - "from sklearn.manifold import TSNE\n", - "tsne_2d_model = TSNE(n_components=2, init='random', random_state = 5,\n", - " learning_rate=5, perplexity = 5, n_iter = 1000) #perplexity is the key hyperparameter to tune\n", - "X_tsne_2d = tsne_2d_model.fit_transform(df.iloc[:,:-1].values)\n", - "\n", - "n_col = 4 # num of columns per row in the figure\n", - "column_names = df.columns.values\n", - "for n in np.arange(0, 12, n_col):\n", - " fig,axes = plt.subplots(1, n_col, figsize=(18, 3), sharey = False)\n", - " cmap = plt.get_cmap('rainbow', 20)\n", - " for i in np.arange(n_col):\n", - " if n< len(column_names):\n", - " c = df.iloc[:,n]# Choose which column is shown as the superimposed color\n", - " im = axes[i].scatter(X_tsne_2d[:,0], X_tsne_2d[:,1], \n", - " marker = 'o', c=c, cmap=cmap, alpha =0.6, edgecolors = 'face', s = 10)\n", - " title = column_names[n]\n", - " axes[i].set_title(''+title, fontsize = fs)\n", - " fig.colorbar(im, ax=axes[i], drawedges=False)\n", - " axes[i].set_xlabel('tSNE-1', fontsize = fs)\n", - " axes[i].set_ylabel('tSNE-2', fontsize = fs)\n", - " else:\n", - " axes[i].axis(\"off\") \n", - " n = n+1 \n", - " \n", - " for i in range(len(axes)):\n", - " axes[i].tick_params(direction='in', length=5, width=1, labelsize = fs*.8, grid_alpha = 0.5)\n", - " axes[i].grid(True, linestyle='-.')\n", - " plt.subplots_adjust(wspace = 0.5)\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.1" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/unsorted/Constrained_MOBO.ipynb b/unsorted/Constrained_MOBO.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/Data Visualization/20221028 - MOBO Vectors.pptx b/unsorted/Data Visualization/20221028 - MOBO Vectors.pptx deleted file mode 100644 index 7d5740311838233b855982f0469896711d4abfe0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36823 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 X=`6*YTyi16ftQSgBD7dc|J(loIsJ#} diff --git a/unsorted/Data Visualization/round2_selectionX_constraints_R1.png b/unsorted/Data Visualization/round2_selectionX_constraints_R1.png deleted file mode 100644 index 4908569c6128ec9ae16a06603ee29de16049bf75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 296433 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↤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 TuOHI32mZz7=ls3L{_}qWzEVmt diff --git a/unsorted/Grid-snapped-LHS.ipynb b/unsorted/Grid-snapped-LHS.ipynb deleted file mode 100644 index e69de29..0000000 diff --git a/unsorted/PerovScaleup_Round0_Initial_Sampling_FOM_MIT_g2g_v2.ipynb b/unsorted/PerovScaleup_Round0_Initial_Sampling_FOM_MIT_g2g_v2.ipynb deleted file mode 100644 index 6ec39bb..0000000 --- a/unsorted/PerovScaleup_Round0_Initial_Sampling_FOM_MIT_g2g_v2.ipynb +++ /dev/null @@ -1,679 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Process Optimization - the `Main` code \n", - "\n", - "### Initial Sampling with Latin Hypercube Sampling\n", - "- UW - MIT collaboration, led by Ethan Schwartz, Nicky Evans, Karen Yang, and Rahul Patidar\n", - "- v1. Initial sampling in Apr 2025" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Load packages and functions" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "from emukit.core import ParameterSpace, ContinuousParameter, DiscreteParameter\n", - "from emukit.core.initial_designs.random_design import RandomDesign\n", - "from emukit.core.initial_designs.latin_design import LatinDesign" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "def x_normalizer(X, var_array):\n", - " \n", - " def max_min_scaler(x, x_max, x_min):\n", - " return (x-x_min)/(x_max-x_min)\n", - " x_norm = []\n", - " for x in (X):\n", - " x_norm.append([max_min_scaler(x[i], \n", - " max(var_array[i]), \n", - " min(var_array[i])) for i in range(len(x))])\n", - " \n", - " return x_norm\n", - "\n", - "def x_denormalizer(x_norm, var_array):\n", - " \n", - " def max_min_rescaler(x, x_max, x_min):\n", - " return x*(x_max-x_min)+x_min\n", - " x_original = []\n", - " for x in (x_norm):\n", - " x_original.append([max_min_rescaler(x[i], \n", - " max(var_array[i]), \n", - " min(var_array[i])) for i in range(len(x))])\n", - " \n", - " return x_original\n", - "\n", - "def get_closest_value(given_value, array_list):\n", - " absolute_difference_function = lambda list_value : abs(list_value - given_value)\n", - " closest_value = min(array_list, key=absolute_difference_function)\n", - " return closest_value\n", - " \n", - "def get_closest_array(suggested_x, var_list):\n", - " modified_array = []\n", - " for x in suggested_x:\n", - " modified_array.append([get_closest_value(x[i], var_list[i]) for i in range(len(x))])\n", - " return np.array(modified_array)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Parameter Optimiziton Space: \n", - "- Web Speed (Inorganic): 0.25 - 0.55 m/min (0.01 m/min - 31 steps)\n", - "- Web Speed (Organic): 0.25 - 0.55 m/min (0.01 m/min - 31 steps)\n", - "- Solution Pump Rate (Inorganic): 80 - 200 uL/min (1 uL/min - 121 steps)\n", - "- Solution Pump Rate (Organic): 100 - 280 uL/min (1 uL/min - 181 steps)\n", - "- Molar Concentration (Inorganic): 0.8 - 1.6 M (0.05 M - 37 steps)\n", - "- Molar Concentration (Organic): 0.4 - 1.2 M (0.05 M - 37 steps)\n", - "- Relative Humidity: 11% - 44% (1% - 34 steps)\n", - "- Ambient Temperature: 20 - 65 C (5 C - 10 steps)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def make_linspace(start, stop, step):\n", - " num_points = int(round((stop - start) / step)) + 1\n", - " return np.round(np.linspace(start, stop, num_points), 4) # round for cleaner floats\n", - "\n", - "speed_inorg_var = make_linspace(0.25, 0.55, 0.01) # m/min\n", - "speed_org_var = make_linspace(0.25, 0.55, 0.01) # m/min\n", - "inkFL_inorg_var = make_linspace(80, 200, 1) # uL/min\n", - "inkFL_org_var = make_linspace(100, 280, 1) # uL/min\n", - "conc_inorg_var = make_linspace(0.8, 1.6, 0.05) # M\n", - "conc_org_var = make_linspace(0.4, 1.2, 0.05) # M\n", - "humidity_var = make_linspace(11, 44, 1) # %RH\n", - "temp_var = make_linspace(20, 65, 5) # °C\n", - "\n", - "# Tracking unique values\n", - "speed_inorg_num = len(speed_inorg_var)\n", - "speed_org_num = len(speed_org_var)\n", - "inkFL_inorg_num = len(inkFL_inorg_var)\n", - "inkFL_org_num = len(inkFL_org_var)\n", - "conc_inorg_num = len(conc_inorg_var)\n", - "conc_org_num = len(conc_org_var)\n", - "humidity_num = len(humidity_var)\n", - "temp_num = len(temp_var)\n", - "\n", - "# Pack into var_array for downstream normalization\n", - "var_array = [speed_inorg_var, speed_org_var, \n", - " inkFL_inorg_var, inkFL_org_var,\n", - " conc_inorg_var, conc_org_var,\n", - " humidity_var, temp_var]\n", - "\n", - "x_labels = ['Speed (Inorg) [m/min]', 'Speed (Org) [m/min]', \n", - " 'inkFL (Inorg) [uL/min]', 'inkFL (Org) [uL/min]',\n", - " 'Conc. (Inorg) [M]', 'Conc. (Org) [M]',\n", - " 'RH [%]', 'Temp [C]']\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initial Sampling" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "parameter_space = ParameterSpace([ContinuousParameter('x1', 0, 1),\n", - " ContinuousParameter('x2', 0, 1),\n", - " ContinuousParameter('x3', 0, 1),\n", - " ContinuousParameter('x4', 0, 1),\n", - " ContinuousParameter('x5', 0, 1),\n", - " ContinuousParameter('x6', 0, 1),\n", - " ContinuousParameter('x7', 0, 1),\n", - " ContinuousParameter('x8', 0, 1),\n", - " ])\n", - "\n", - "# parameter_space = ParameterSpace([DiscreteParameter('x1', np.linspace(0,1, 51)),\n", - "# DiscreteParameter('x2', np.linspace(0,1, 51)),\n", - "# DiscreteParameter('x3', np.linspace(0,1, 51)),\n", - "# DiscreteParameter('x4', np.linspace(0,1, 51)),\n", - "# DiscreteParameter('x5', np.linspace(0,1, 51)),\n", - "# DiscreteParameter('x6', np.linspace(0,1, 51))\n", - "# ])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Filtering Functions" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "def check_clausius_clapeyron(humidity_vals, temp_vals):\n", - " \"\"\"\n", - " Return boolean mask for valid points satisfying Clausius-Clapeyron constraints.\n", - " Simplified: Ensures RH and Temp do not imply >100% saturation.\n", - " \"\"\"\n", - " # Saturation vapor pressure approximation (T in Celsius)\n", - " # Tetens formula: es(T) = 0.6108 * exp((17.27*T)/(T + 237.3)) [kPa]\n", - " # RH = (actual / saturation) * 100\n", - " # Check that vapor pressure does not exceed saturation vapor pressure\n", - " valid_mask = []\n", - " for RH, T in zip(humidity_vals, temp_vals):\n", - " es = 0.6108 * np.exp((17.27 * T) / (T + 237.3))\n", - " e = RH / 100.0 * es # approximate check\n", - " valid_mask.append(e <= es)\n", - " return np.array(valid_mask)\n", - "\n", - "def check_correlation(X_df, threshold=0.45):\n", - " corr = X_df.corr(method='pearson').values\n", - " # Zero out diagonal so we ignore self-correlation\n", - " np.fill_diagonal(corr, 0)\n", - " return (np.abs(corr) < threshold).all()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Create LHS Sampling" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Valid LHS found after 9205 attempts.\n" - ] - } - ], - "source": [ - "# Sampling loop\n", - "max_attempts = 20000\n", - "attempts = 0\n", - "valid = False\n", - "n_samples = 8\n", - "design = LatinDesign(parameter_space)\n", - "\n", - "while not valid and attempts < max_attempts:\n", - " attempts += 1\n", - " x_init = design.get_samples(n_samples)\n", - " x_init_original = get_closest_array(x_denormalizer(x_init, var_array), var_array)\n", - " df = pd.DataFrame(x_init_original, columns=x_labels)\n", - " df_cols = x_labels\n", - "\n", - " # 1. Filter by Clausius-Clapeyron\n", - " rh_vals = df['RH [%]'].values\n", - " temp_vals = df['Temp [C]'].values\n", - " mask_cc = check_clausius_clapeyron(rh_vals, temp_vals)\n", - "\n", - " if not mask_cc.all():\n", - " continue # skip invalid CC samples\n", - "\n", - " # 2. Filter by Pearson correlation\n", - " if not check_correlation(df):\n", - " continue # correlations too high\n", - "\n", - " valid = True # passed all tests\n", - "\n", - "if not valid:\n", - " print(\"No valid LHS found after max attempts.\")\n", - "else:\n", - " print(f\"Valid LHS found after {attempts} attempts.\")" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Speed (Inorg) [m/min]Speed (Org) [m/min]inkFL (Inorg) [uL/min]inkFL (Org) [uL/min]Conc. (Inorg) [M]Conc. (Org) [M]RH [%]Temp [C]
00.530.31147.0179.01.550.7530.025.0
10.340.49177.0134.01.251.1517.030.0
20.420.53132.0224.01.050.4538.045.0
30.490.42162.0269.01.151.0521.050.0
40.310.34192.0156.00.950.8542.060.0
50.460.2787.0201.01.450.9525.055.0
60.380.38117.0111.00.850.5513.040.0
70.270.46102.0246.01.350.6534.035.0
\n", - "
" - ], - "text/plain": [ - " Speed (Inorg) [m/min] Speed (Org) [m/min] inkFL (Inorg) [uL/min] \\\n", - "0 0.53 0.31 147.0 \n", - "1 0.34 0.49 177.0 \n", - "2 0.42 0.53 132.0 \n", - "3 0.49 0.42 162.0 \n", - "4 0.31 0.34 192.0 \n", - "5 0.46 0.27 87.0 \n", - "6 0.38 0.38 117.0 \n", - "7 0.27 0.46 102.0 \n", - "\n", - " inkFL (Org) [uL/min] Conc. (Inorg) [M] Conc. (Org) [M] RH [%] Temp [C] \n", - "0 179.0 1.55 0.75 30.0 25.0 \n", - "1 134.0 1.25 1.15 17.0 30.0 \n", - "2 224.0 1.05 0.45 38.0 45.0 \n", - "3 269.0 1.15 1.05 21.0 50.0 \n", - "4 156.0 0.95 0.85 42.0 60.0 \n", - "5 201.0 1.45 0.95 25.0 55.0 \n", - "6 111.0 0.85 0.55 13.0 40.0 \n", - "7 246.0 1.35 0.65 34.0 35.0 " - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "##### Visualize the distribution of the initial data" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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Igk48UZERERERERERCSB326uUFtbi4iICOv///zzz/j000/R0NCAgQMHol+/fv5aFRERERERERERUcDz+Yy3bdu24ZprrsFzzz1nfeyrr77CLbfcgkWLFuG9997D3XffjWeeecbXVREREREREREREemGTxNvBw4cwPjx43HkyBGcOHHC+vjzzz+P+vp6tG3bFgMHDoTRaMT777+PLVu2+BwwERERERERERGRHvg08bZ48WLU1dVhyJAheP755wEAP/74I06ePIno6Gh89NFHmDdvHl555RUIIbBixQq/BE2NFEWByWSCoihahxK0mGO5mN/Qxb6Xi/mVi/kl0i8et0StR1EUREVFaR0GkeZ8usZbdnY2TCYTnn/+ecTGxgKA9ay2fv36ITExEQAwZMgQpKamYs+ePb5FS00oigKz2ax1GEGNOZaL+Q1d7Hu5mF+5mF8i/eLEG1HrURQFcXFxWodBpDmfznjLy8tDhw4drJNuALB9+3YoioILL7ywybKpqak4deqUL6ujZoQQKC4uhhBC61CCFnMsF/Mbutj3cjG/cjG/RPrF45ao9QghUFZWpnUYRJrzaeItMjISdXV11v8vLy/HL7/8AgC44IILmixbVFTE00wlMBqNWocQ9JhjuZjf0MW+l4v5lYv5JSIics1g8Pl+jkS659NPTdu3b49Dhw6hpKQECQkJ2LhxI+rr65Geno7OnTtbl/v555/x559/onv37l6vKzs7G/PmzcO+fftQXV2NzMxM3HHHHbj66qs9amft2rX44IMP8Ntvv6Gurg4dO3bE8OHDcfPNN+vu1HOeuisfcyxXqOSX9aulUOl7rTC/coVKflm7KBhxnwt+rF2BQ1EUxMTEaB0GkeZ8mnjr168f9u7di/vuuw9DhgzBW2+9BUVRcNVVVwEAampqsHnzZjz33HNQFAVXXHGFV+tZu3YtJk2ahLCwMGRlZcFoNGLHjh146KGHcPjwYUyYMMGtdqZNm4YVK1YgMjISF154IWpra/H999/jiSeewLFjx/DYY495FZ9WLBYLTp06hTZt2vCbBEmYY7lCIb+sX/aFQt9rifmVKxTyy9pFwcpisQTtcUusXYHGYrGgtLSU10WlkOfTxNvo0aPx2Wef4bvvvsP3338PIQRSU1MxduxYAMCePXvw4IMPQgiBrl274o477vB4HYWFhZg2bRqio6Ox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234U8BNvN954IxYsWICFCxeif//+OP/88wEAR44cwezZs6EoCu68807r8nl5edYiqZ4qHB0djWHDhuG9997DE088gRdffBEREREQQuCll17CiRMncOWVV+L000/XZBttrV27FpMmTUJYWBiysrJgNBqxY8cOPPTQQzh8+DAmTJjgso0ffvgBd9xxB2pqatC1a1ecdtpp2LdvH+bPn49169bhgw8+QFpaWitsTeDxR36b++uvv/DUU09JiFZ//JHfuro6HDhwAPHx8Q4HR75ew6i1hFL9Yu2Si7VLPtavv4VS7VL99NNPeOaZZxw+n5eXh+HDh+PkyZPIyMjApZdeip9//hkLFizA1q1b8f777yM2NrbJa/7v//4Py5YtQ0JCAi6++GIUFBTgq6++wpYtW/DWW2/Z/cBK7nPVZydPnkRxcTHS0tJwwQUX2F1GvSC/in2mb6FYu1obj7uWLBYLHnjgAXz55ZeIiopCz549YTKZ8NNPP2HBggX48ssv8f7771vPtAzF9xNPcxSU+5HQgWXLlomMjAxxzjnniFGjRomxY8eKHj16iIyMDPG///2vybKPPfaYyMjIEI899liTx0tLS8U111wjMjIyxGWXXSbuv/9+cdVVV1n/Pzc3tzU3ya6CggLRs2dPcd5554mffvrJ+vihQ4fExRdfLDIzM8W+ffuctlFfXy/69+8vMjIyxPLly62P19XViccff1xkZGSI//znP9K2IZD5I7/NWSwWcfvtt4uMjAyRkZEhJk6c6O+wdcNf+f31119FRkaGuPfee2WG22pCoX6xdsnF2iUf61dLoVC7VF999ZXo06eP9XjYvn17i2Xuv/9+kZGRIWbOnGl9rKamRjz44IMiIyNDvPDCC02W//rrr0VGRob4xz/+IQoLC62Pf/755+Kcc84R/fv3F9XV1fI2Ksi502dffvmlyMjIEM8995xbbbLPgkMo1a7WxuPOvuXLl4uMjAwxZMgQcfz4cevjZWVlYty4cSIjI0M8+OCD1sdD8f3E0xwF434U0Nd4U918882YP38+evfujT179uD7779H165d8dprr2Hs2LFutREXF4cPPvjA+i3Hxo0bUVdXh1tvvRXLli3z+tbV/rR06VJUV1djxIgR6NGjh/Xxjh074uGHH4YQAosWLXLaxs6dO5Gbm4u+ffvipptusj4eFhaG//znPwCATZs2SYk/0Pkjv829++672LVrF/r27evvcHXHX/lVf6IVCKcE+0Mo1C/WLrlYu+Rj/WopFGpXbm4upkyZgnvvvRd1dXUOr/t0/PhxfPnll2jbti0eeOAB6+MRERF45plnEBMTg2XLljW5oPvbb78NAHj00UeRmJhoffzKK6/Eddddh9zcXHz22WeStix4udtngOfHI/ssOIRC7WptPO6cW7lyJQBg8uTJTc6EjI2NxfPPPw9FUbBhwwZUV1eH7PuJJzkCgnQ/0nLWj5oaOnSoyMjIEHv27Gnx3KlTp0RmZqa46KKLXLaTk5Mj/vjjjxaPHz9+XGRkZIi+ffv6JV698Vd+Vfv37xfdu3cX48aNEytXrgz5s0b8ld+nn35aZGRkiE2bNskIkyRg7ZKLtUs+1q/QpJ7tMmzYMLF//35x22232T2LY8mSJSIjI0M88cQTdtsZP358k34vKysTXbp0Eeedd56oq6trsbz6Tb7tt/vkHnf7TAhhPYvi0KFDLttlnxE5xuPOuXHjxomrrrpKFBUV2X3+ggsuEBkZGeLPP/8M2fcTT3KkLh9s+5EuzngLFerdczp16tTiObPZjOTkZBQWFqKwsNBpO2lpaWjfvn2Tx0pLS/H0008DAK6//no/Rawv/sovANTW1uKRRx5BTEwMnn32Wb/Hqkf+yq/6DUdBQQHuvPNOZGVloVevXhg5ciS2bt3q/8DJZ6xdcrF2ycf6FZrOPvtsvPjii1ixYgUyMzMdLnfgwAEAju9iqO436nKHDh2CxWLB2WefjbCwlpdTVm+uoS5P7nO3z4DG4zEqKgq//PILbrnlFvTp0wd9+/bFuHHj8OOPPzZZln1G5BiPO+fmzZuHzz77DG3atGnx3B9//IHi4mKEh4cjMTExZN9PPMkREJz7ESfeAkRJSQlqamoQExNj904mAKynNefn57vd7ooVK3DnnXdiwIAB2Lp1K4YNG4ZHH33ULzHrib/z+8orr+DAgQN46qmnnJ5uHSr8lV+LxYLffvsNADB16lQUFRWhb9++aNeuHXbt2oUxY8bgnXfe8f8GkNdYu+Ri7ZKP9St0jR07Fv/85z9bXKC5ObXfU1JS7D6vPl5QUODV8uQ+d/ussLAQeXl5qK6uxqOPPgqLxYKsrCyYzWZs2rQJt956K9atW2ddnn1G5BiPO+/Nnj0bAHDppZciMjKS7yd2NM9RsO5HAX9X01BRWVkJoPFOOo6ot8tVl3XHV199hW+++QYAYDQaUVpaipycHJxxxhk+RKs//szvzp07sXDhQlx//fW48sor/Rekjvkrv0ePHkVlZSUiIyMxe/ZsXH755dbn1q1bh0mTJuGll15Cnz590LNnTz9FT75g7ZKLtUs+1i9yRe33qKgou8+rj6vLVVRUAHC8TzVfnvxPPfu0TZs2ePPNN9GrVy8AsF6v8YUXXsCUKVNw/vnnIz09nX1G5Ac87ppasmQJPvnkE0RHR1uvV8z3k6bs5ShY9yOe8RYg1G8QFEVxuIwQosm/7njqqafw888/Y926dbj66quxYcMG3HrrrSgqKvItYJ3xV35LS0sxefJkpKWlYfr06f4NUsf8ld+OHTtix44dWLduXZMPrQBwzTXXYMSIEbBYLPjggw/8EDX5A2uXXKxd8rF+kStGoxGA432k+f7havnmryP/69+/P7Zu3YqPP/7Y+qENaOyTUaNGYdCgQaiursZHH30EgH1G5A887v62ePFiPPfcc1AUBc8995z15458P/mboxwF637EibcAof68xfYOJs3V1tYCAEwmk9vtpqWlISIiAh07dsTLL7+M/v37Iz8/H0uXLvUtYJ3xV36feuop5OTk4IUXXkB8fLx/g9Qxf+6/iYmJLa7zpbrssssAAL/88os3YZIErF1ysXbJx/pFrqj9XlNTY/d59XH123Z1eUf7lPq4s7MsyTeKoiA1NRVpaWl2n29+PLLPiHzH465xcuell17Cc889B4PBgBdeeAH/+Mc/rM/z/cR1joJ1P+JPTQNETEwMTCYTysrKUF1dbff007y8PACOf7/sjuuvvx5bt261nsIZKvyR359//hmffPIJzGYzVq1ahVWrVlmfO3HiBADghx9+wCOPPIKOHTti/PjxErYkMLXW/qu+tqqqyus2yL9Yu+Ri7ZKP9YtccXWNv+bXl1GXd3Q9GVfXoyH5mh+P7DMi+YL9uKuursakSZOwfv16REVF4eWXX8agQYOaLBPq7yfu5MgVve5HPOMtQCiKYr27yeHDh1s8X1xcjIKCAiQmJjq9IPbGjRsxZcoUfPnll3afj4iIAADU19f7IWr98Ed+1d+FFxcXY+3atU3+++GHHwA0fohdu3at9dpUocJf++/69evx8MMPY9myZXaf/+OPPwAA6enpfoia/IG1Sy7WLvlYv8gVZ/sHABw8eBAArHf769SpEwwGA44cOQKLxdJiefUuuo7uake+W7ZsGR566CFs2LDB7vPNj0f2GZHvQvm4Ky8vx6hRo7B+/XokJSVh8eLFdieUQvn9xN0cBet+xIm3ANK/f38AsLuTbdiwAUIIDBw40GkbJ06cwKpVq7B48WK7z2/ZsgUA0L17dx+j1R9f85uVlYXffvvN7n8vvPACAOC6667Db7/9hiVLlsjZiADmj/23rKwMn376Kd577z27v8NfvXo1AKBfv35+iJj8hbVLLtYu+Vi/yBl1/9i4cSMaGhqaPFdWVoZdu3bBZDKhd+/eABp/ztK3b1/rc82pXzC42qfIe7m5ufjss8+wYsWKFs8JIfDxxx8D+Pt4ZJ8R+S5Uj7u6ujqMHTsWP/zwA84880wsW7YM5557rt1lQ/X9xJMcBet+xIm3AHLjjTciOjoaCxcuxO7du62PHzlyBLNnz4aiKLjzzjutj+fl5eHw4cPWn8AAwD/+8Q/ExsYiOzsbCxcubNL+8uXLsWrVKphMJtxyyy3StyfQ+CO/5Jg/8jtkyBC0adMGBw4cwKuvvtrkW4vly5fjiy++QFJSEoYPH946G0VuYe2Si7VLPtYvcua0007DZZddhhMnTuCll16yTqzW1tbiiSeeQEVFBYYPH47Y2Fjra0aMGAEAeOaZZ5r8pGj9+vX45JNPkJqaimuvvbZ1NySEDB06FBEREdi0aROWL19ufdxisWDOnDn46aef0KlTJ1x11VXW59hnRL4J1eNu7ty5+P7775GSkoIlS5bg9NNPd7hsqL6feJKjYN2PFKH17R2oieXLl2P69OkwGo3IyspCREQEduzYgZqaGkycOBFjx461Ljt58mSsXr0aQ4cOxYwZM6yPf/nll/jPf/6Duro6dO7cGWeeeSYOHz6Mo0ePIjo6Gq+++qrmM75a8Ud+7Vm1ahWmTJmC6667DjNnzpS9GQHLH/ndunUr7rvvPtTU1KBDhw7IzMzEsWPH8Ntvv8FkMmHBggXWb4EocLB2ycXaJR/rF91+++3Izs7Gu+++i4svvrjJcydPnsTw4cORl5eHs88+G507d8bPP/+MP//8E926dcOSJUusN+pQPfLII1i7di1iY2Nx4YUX4tSpU9i9ezfCw8OxYMECZGVltebmBSVnfbZy5UpMmzYNFosFmZmZ6NChA/bv34/ff/8dycnJWLJkCc4+++wmr2GfEbnG4+5vJSUlGDhwIKqqqtClSxd07tzZ4bKTJ09GcnJyyL2feJOjYNyPeHOFAHPzzTcjPT0d8+fPx549e2A0GtG1a1fcddddGDJkiFttDB48GCtWrMC8efOQnZ2No0ePIikpCcOGDcPYsWNx1llnSd6KwOWP/JJj/shv//79sXLlSsybNw87d+7E119/jTZt2mDYsGG49957nX5DQtph7ZKLtUs+1i9y5rTTTsNHH32E1157DZs2bcLGjRvRrl073HPPPbj77rtbfEgCgBdffBHnnnsuli9fjq1btyI+Ph6XX3457r//fpxzzjkabEVoufHGG3HWWWdh/vz52L17N44cOYLU1FTcfvvtGD9+PJKSklq8hn1G5JtQO+5+/PFH60X+9+/fj/379ztc9v7770dycnLIvZ94k6Ng3I94xhsREREREREREZEEvMYbERERERERERGRBJx4IyIiIiIiIiIikoATb0RERERERERERBJw4o2IiIiIiIiIiEgCTrwRERERERERERFJwIk3IiIiIiIiIiIiCTjxRkREREREREREJAEn3oiIiIiIiIiIiCTgxBsREREREREREZEEnHjzs/379+PZZ5/FP/7xD/Tp0wc9evTAwIEDMWbMGLz33nuorq7WOkSpbr/9dmRmZmLWrFlevX7//v3o0aMH3njjjSaPX3755cjMzMRrr73mjzADzpQpU5CVlYW8vDyPX7tq1SpkZmY2+a+18vTTTz8hMzMTL7zwgrR1+LpPqezlydc2QwXrmpy6ZqugoABvvvkmRowYgUsuuQTdu3dHnz598K9//QuzZs3CH3/84W34Xqurq8NVV12FW2+9FQ0NDR6/fvLkyS2OuV27dkmItKUFCxYgMzMTX331lbR1qNv0zTff+NSOvTz52mYoUWv7gAED/NKeOt5YsWKF269p3n+u/istLW2xPhnv2xxTBcaYyrZNb3G8FVpY1xzjmMp/Yyq1zdtvv93rNjjecl9Yq60pBMyZMwdvvvkmLBYLYmNjccYZZyA8PBz5+fnYunUrtm7digULFuD1119Ht27dtA434NTV1WHSpElITk7G6NGjtQ6nVU2cOBFffPEFpkyZgrfffturNsLDw9GjRw8AQNu2bf0ZnkNbtmwBAL8NDGRKSkrC+eefDwA4cOAAysvLNY5IH1jXfONOXXvnnXfw2muvobKyEgDQrl07ZGZmIj8/H7/88gt+/vlnLFiwAPfddx/Gjx8PRVFaJfbw8HBMmTIFY8eOxYIFCzBu3Div2klMTESHDh0AAHFxcX6M0LHNmzcjPDwcF154YauszxcdOnSw1qbdu3drHA35okOHDkhMTHS5nNFolB4Lx1T6G1M5w/EWaUVvdY1jqtbF8Zb7OPHmJytXrsTrr78Ok8mEF154AYMHD25SgA4fPoypU6diz549GD16NNatW+dWEQsl77zzDg4cOIAXX3wRkZGRWofTqpKTk3H33Xdj9uzZ+OSTT3Dttdd63EZiYiI++OADCdE5tnnzZphMJvTt21faOl588UVUVVWhTZs2PrUzcOBADBw4EEDjt7rZ2dn+CC+osa75zlVdmz59OpYvXw4AuPHGGzFu3DiceeaZ1uf//PNPvPPOO3jvvffw6quvYt++fXj11VdhMLTOCesDBw7ExRdfjNdffx3XXHMNTj/9dI/buOSSSzBz5kwJ0dlXXl6OH374AX379kVMTIy09axbtw5A46DeF/fccw/uueceAPDpjJhQNXjwYJx77rkIDw/XOhSMGzcOw4YN0zoMABxT6XFM5QzHW6GFdc0+jqlaf0zlDMdbnuFPTf1k3rx5AIBHH30UV111VYtZ/44dO+LNN99EUlISTp06hcWLF2sRZsAqKirCvHnzcOaZZ+L666/XOhxN3H777YiLi8PMmTNRW1urdTguFRUV4ZdffsGFF16IiIgIaetp164dOnbsyAkdDbCu+cZVXVu6dCmWL18ORVHw3//+F88//3yTASLQuP9PmzYNs2bNQlhYGNavX48FCxa01iYAAO677z7U1NTg5ZdfbtX1emv79u2oq6uzfvCTpWPHjujYsSOio6Olroeci4uLQ8eOHXHGGWdoHUrA4JhKf2MqZzjeCj2say1xTBV4ON7yDCfe/KC0tBTHjx8HAJx77rkOl0tMTMSgQYMANF6rgf62YMECVFZW4qabbmq1bx0CTWxsLK677jr89ddf+Oijj7QOx6WtW7fCYrFIL7akDdY13zmra+Xl5Zg9ezYA4I477sANN9zgtK2rr74aI0eOBND481+1b1pDnz590LlzZ3z++ec4ePBgq63XW3r6SRaRDBxT6W9M5QzHW0QcUwUijrc8E5rvxn4WFvb3L3Y3btzodNn7778fn376aYtZbvVifwsXLsThw4cxYcIEZGVloVevXvjnP/+JRYsWoaamxmG73377LR544AH069cP3bt3x8UXX4x7770XO3bscBrPhg0bMHbsWFx00UXo3r07+vfvj4kTJ2Lv3r0OX1NaWoq5c+fiH//4B8477zz069cP06ZNQ35+vtN1OVJdXY2PPvoIiqJ4/M2smrcPPvgAJ06cwJQpUzBgwAB0794dAwYMwOOPP44TJ044fP2OHTtw//33W/N24YUXYsyYMVi/fr3d5dWLhaoXm+/bty969eqFYcOGobi42LrcN998g7Fjx6Jfv34499xzccMNN2Dp0qWwWCxOL7D7z3/+EwDw3nvveZQHR1577TXrRW3z8/Px5JNPYsCAAejRowcGDRqEWbNmWb8J3rVrF0aPHo2+ffuiZ8+eGDp0KNasWeOwbXvFVu2PFStW4NixY5g4cSIuueQS9OzZE9dccw3effddCCEAAOvXr8eIESNw/vnn47zzzsPw4cOxefPmFuuxd7HfEydOIDMzE5dccgmEEFixYgVuuukm9OrVC7169cK///1vrFy50rou8hzrmty69vHHH6O0tBRhYWFuX3/pvvvuQ0REBOrq6vDhhx9aH7c9HnJzczF27Fj07NkTF1xwASZOnGhdrr6+HitWrMDNN9+Mvn37onfv3rjjjjuwbds27Nq1y+kFdq+//noIIfxWm9Tj+ptvvsFPP/2E8ePHIysrC+edd16L2rNixQoMGzYM5513Hnr37o277roLe/bscdj2li1b0L59e5x99tnWx9Ta/fvvv2Pbtm0YNWoU+vTpg/PPPx/Dhw/Hpk2bADTmaMGCBbj22mvRs2dPZGVlYcKECTh8+HCL9di7MK96Qez//Oc/qKysxOzZs3HllVeiR48eyMrKwj333IPvvvvO5/zR3xxdhFzdx7Zs2YL9+/fjwQcfxMUXX4zu3bvjiiuuwPPPP4+ioiK311NUVIRrr70WmZmZuO6661BYWOjvTfELjqn+JmtMdcstt9h9Xq2j/v4JE8dboYd1rSmOqZxzdWMUtXb5chMFezje8gyv8eYHJpMJ559/Pnbv3o3XXnsNx48fx7/+9S+cf/75LX6alZKSgpSUFIdt/fbbb5gzZw4qKyvRuXNn1NfXY9++fdi3bx+++OIL/O9//2txIcWZM2di/vz5AICEhARkZGQgLy8PX331Fb766iuMGTMGkyZNavKa+vp6TJ48GWvXrgXQeCHUzMxMnDhxAp988gk+++wzTJ06FbfddluT1/35558YPXo0jhw5AqPRiM6dO6OmpgYrVqywXn/CU9u2bUNJSQm6du2KtLQ0j18PAL/++itmzpyJyspKnHHGGTjzzDNx6NAhfPTRR/j666+xatWqFhfHfeaZZ6wFz2w2o0uXLsjNzbVeMP7qq6/GSy+9ZPf6Ck899RR2796NTp06oaqqChERETCbzQCAN954A6+++iqAxuuMdOrUCceOHcPTTz+NnTt3Ot2OHj16wGw24/Dhw9i/fz+6dOniVT6a+/3333HDDTfg1KlT6NSpE4xGI/744w/MmzcPf/zxB/r27YunnnoK0dHR6NChA06cOIFff/0Vjz32GKqrqzF8+PAm7VksFmzbtg2dO3e2+3v73bt347nnnkN9fT06duwIRVFw+PBhzJgxA0VFRVAUBf/73/8QHx+PDh064OjRo/jhhx8wbtw4vPXWW25/cyKEwGOPPYaPP/4Y8fHxOOuss/DHH39gz5492LNnD44ePYpHHnnELzkMNaxrcuuaOnjIzMxEamqqW23Gxsbi4osvxqZNm7B582Y8+uijTZ6vra3F6NGjcezYMXTu3Bl//vknTjvtNABATU0NHnzwQesk6plnnomYmBh899132LlzJwYPHux03QMGDMDLL7+Mzz//HE8++aTfzqL5/PPPsXLlSkRERKBDhw74888/rbWnsrISu3fvxtq1a5GUlISzzjoLBw8exPbt2/Htt99ixYoVLWrkvn37kJeXhxEjRthd36JFi7B06VLEx8fj9NNPx++//44ffvgB99xzD1577TUsWbIEu3btQlpaGs466ywcOHAAX375Jb799lv8v//3/9x+jyotLcW///1vHDhwAKmpqejUqRMOHTqEjRs3YsuWLXjjjTdw6aWX+po+csOWLVvw4YcfQgiBDh06ICYmBsePH8eiRYuwadMmrFq1CrGxsU7bOHXqFEaNGoWDBw+ia9eueOedd3y+DpYsHFP9TdaYqjVxvEX2sK41xTFV6+N4y3P66+UANX36dJhMJgghsGbNGtx222244IILMHbsWLz11lv48ccfYbFYXLazatUqmM1mrF69GmvXrsVnn32GDz/8EMnJyfj+++/x0ksvNVn+ww8/xPz58xEfH4+XXnoJ2dnZWLVqFbZu3YpZs2bBZDJhwYIFLW4f/eqrr2Lt2rVIT0/HggUL8M0332DlypX45ptvMG3aNCiKgmeffRbbt29v8rpp06bhyJEjyMzMxBdffIGPP/4Yn3/+OVasWAFFUXDs2DGPc6eevdK7d2+PX6tavnw5OnXqhHXr1uGLL77Ap59+ig8//BAxMTEoKirCO++802R59cKaYWFheOKJJ7Bjxw589NFH2Lp1K2bPng2TyYTPPvsML774ot317d69G7NmzcKnn36Kr7/+2npL6+3bt1sv0jlt2jRs3boVK1euxPbt23Hbbbc5/NZXZTAY0KtXL2tb/vLZZ5/BbDbjs88+w9q1a7Fx40bcd999AIBPP/0UTz/9NEaNGoVdu3Zh9erV2LJlC/r37w8AeOutt1q0t2fPHhQXFzscsK1atQpdu3bFxo0b8fHHH2PLli3WC7MuWLAAb731Fh577DHs2rULq1atwtdff42uXbtCCGGdbHFHYWEhPvnkEzz++OPYuXMnVq1ahW3btlm/DXv33Xc9+uaPmmJdk1fX9u3bBwAefxA855xzAABHjx5tcd2i0tJS5OfnY82aNVi9ejW2bt2KsWPHAgBef/11bNy4EWazGYsXL8b69euxevVqfP311+jbty++/PJLp+vNzMxETEwMiouLnZ456Klly5bhiiuuwJYtW6y1p1+/fgAaP8ivX78e//3vf/HNN99g9erV+OKLL9C2bVvU1tZi0aJFLdpTz+Jw9JOspUuX4vbbb8fWrVuxevVqbNq0CZmZmRBC4IEHHsBvv/2GBQsWYMuWLfj444+xevVq63Y339+c2bZtG06dOoW3337buq6vvvoKmZmZaGhocPitNPnfkiVLcMkll2Djxo349NNP8eWXX+KNN96A0WjE77//7vJniCUlJbjrrrvw22+/oUePHli4cGHAfjgFOKayJWtM1Zo43iJ7WNea4piq9XG85TlOvPlJ165dsWLFiiYFoby8HJs3b8bLL7+Mm2++Gf369cOsWbNQVVXlsB2DwYA33njDWggAoFevXtbByooVK5CbmwugcSb+tddeAwA8//zzTU69VRQF11xzjfWMkNdeew319fUAGt88Fy5cCKDxm0R1ggVovBX07bffjlGjRkEIYf29PAD8+OOP2L59O4xGI+bOndvkTiw9e/b0+iKRu3bt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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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IgpmDU0w84RXM986dOiAnJwcfP75507X+9///ofs7Gx06NDBm5ejRiSKIgoLCznVnFSB+agN7CdSG+YkBQLmMSmBeUdEwUx1p53ec889EEURM2fOxPbt22XX2b59O2bNmgVBEHD33Xd783LUiERRRHl5OT9wSRWYj9rAfiK1YU5SIGAekxKYd0QUzPxR+7w67fSmm27CjTfeiF27dmHq1KmYP38+2rdvj+joaJSWluLXX39Fbm4uRFHEjTfeiJtvvtlXcRMREREREREREame1/dPXbx4MYYPHw69Xo+cnBzs3bsX27Ztw969e5GTkwO9Xo/7778fr732mlevc/jwYYwePRo9e/ZEly5dMGzYMHz22Wdetbl161akp6dj2rRpXrVDROQM6xcRaRFrFxFpEWsXEamRVzPfAMBgMGDu3LkYO3Ys9u3bhzNnzqC0tBRRUVFo3bo1+vTpg9TUVK9eY8uWLZg+fToMBgN69OgBvV6PgwcP4oknnkBGRgYmTZrkdpsXL17Es88+61VcREQNYf0iIi1i7SIiLWLtIiK18nrwTdKkSRPcc889do+dP3/e67vk5OXlYc6cOYiIiMDq1avRqVMnAEBGRgZGjhyJpUuX4sYbb0S7du1cblMURcyYMQPFxcVexUZE5AzrFxFpEWsXEWkRaxcRqZnXp53W1NRg0aJF6NevH0wmk92yl19+Gddeey0WLlyIqqoqj9pft24dKisrMXz4cGsBBYA2bdpgypQpEEURq1atcqvN999/H4cOHUL37t09iomIyBWsX0SkRaxdRKRFrF1EpGZeDb5VVVXh4Ycfxttvv43MzEz8/vvvdstzcnJQXl6Od955x6MpvgDw5ZdfAgAGDBhQb1n//v0hCAL27t3rcnsnT560DhYOGTLEo5iCgSAIiImJgSAISodCpNl8DLb6pdV+osDFnPRMsNUutWMekxK0mHesXUTkK/6ofV4Nvq1ZswaHDh1CfHw8XnnlFbRt27be8jfffBNJSUn46quv8PHHH7v9GqdPnwaAem0DgNFoRFJSEvLy8pCXl9dgW1VVVZg2bRqioqLwwgsvuB1LMNHiBy4FLq3mY7DVL632EwUu5qRngq12qR3zmJSgxbxj7SIiX/FH7fPqmm/btm2DTqfDihUr0KFDh/qNGwzo378/kpOTce+99+LTTz+td104Z4qKimAymRAVFYWoqCjZdVJSUpCTk4OcnBwkJiY6be+1117DqVOnsGTJEiQlJbkch+TYsWOIjIyUjaFJkyYQRRGAZx1lsVgcLpPaE0URgiC43b4oitbY5Oh0OrvYRVGE2WxGYWEhjEYjdLq/x2il17dYLHaP+yoW222V4nIldk/eE2k7nMUirWP7d0Pru0t6Lz3pJ0frexOLbR844k4/eZIntq8vCAJqampk89HTHJT+dbStzvrZVcFYvywWS71+qpsr7lLDfiHFYfuvo/aldVzdpz2Nw9f1y5u+8XX9cjcW23ped592JScdxeLpeyJtn/Racu+7J23X3T65+sXa5R5Xj72Url2iKKKwsBBxcXEwGNw7dFdL7ZLaV8Oxl1pql0QNx15ytctXx16u1C5pPW+wdsGuvcb83tjQcZcn3xtZu+TjYO2y5+pxlydt19TUQK/X272O9Lctd2qXV4Nvv//+O1q1aiU78Garc+fOaN68OU6dOuVW++Xl5QCAiIgIh+uEhYXZrevIN998g5UrV+L222/HTTfd5FYckpEjR8o+PmbMGEyfPh1FRUXQ6/WIiYlxu+28vDxUV1fLLktOToZer0dmZiZSU1Pd3lErKytRUFAguywsLAyJiYkoLS2F2WyG0WhEUVERysrKUFNTA5PJZPd6MTExiImJQXFxsUc30xBFEZmZmQ6Xp6amwmw2Izc3F02bNoXJZEJ+fr7sugaDASkpKaisrHSaI3LKy8sRFRWFgoKCetcqlBiNRkRGRiI3NxexsbEIDw9HVlaWw2KRnJyMkJAQt+IAgMLCQiQkJKCiogKFhYWy6zjqJ0d5HxcX5/DAw5msrCwkJSVZ880RV/spJCQEycnJbscBwG5/KiwsRFlZmV0+etq2VEil/clsNiMnJ8dunbKyMo9itqXF+hUSEuJR3kj1SxTFenVDql95eXke9Zcn9cvR+ynVL0/Y5o0jntQvd5WWlgKo3RZf1S9P96WqqioUFRUhJSXFZ/XLk1iqqqoQFhYmW7/kctKVfvL0PSktLUVMTIxd/arbT562nZeXB6PR6LR+sXa5x5VjLzXUrujoaERERKCkpATx8fFuxaGW2gWo59hLLbVLooZjL7na5atjL1dqF+B9/dJi7QqU740NHXd58r2Rtas+1q76XDnu8rTt/Px8xMfH+7R2eTX4JggCQkNDXVo3JiamXqANqfsrtRxXRsWLi4sxc+ZMNGnSBHPnznUrBlurV692+AsGUDvg4Slnv77YfmHw5NeR8PBwpKamOl0nOjra+ndcXBxiY2OdxuJoeUMEQXAaiyAIMBgMaNKkCYDa4tFQ7OHh4W7HIfWjs4NYaVuTkpKsf0txOVvfXdKHUURERIPb4m4/uatJkyZ2+eaIJ/3kLtv9KSEhAQkJCT5pV5o1IO1PBoOhXuzSAIc3WL9qSdvf0C/MjvijfnnCNm8caYz9wnZbfVm/PBEaGmqdDeDL+uVJHIA66pe0jbb7k7uDJY4kJibWOx6oW79Yu9yjpdolCIJHxztqqV2Aeo691FK7JGqtXb469nKldgHe1y/WrlqB8r2Rtas+1q76/HncZduXvqpdXg2+NW/eHKdPn0Z+fr7T4lxUVITTp0+jWbNmbrUv/YpZWVnpcB3pLqpyxU3y7LPPIjMzE++9955XSdehQwe7JK7Lmy92rkyD9LR9V6a12i6Xpm7m5uYiKSlJNjZPp6e6OsVWWsfd2N2JA3BtO7w9zdHV9j3pJ29yzlksdV+roXj8EUvd9pzloyft1n0vbfmin4OxflksFof91Jj1y9e5aPsaatovlK5fttunZD/J1XPpMVdy0h+xuNtPrnBUn339WsFYu5xRQ+2yWCzIycnx6NQ3tdQu2/ZZu+yp4djLUZ744tjLldpVdz1PsHb5pn1/7BOe9C1rl3wcrF2O25SLyx/te1O7vBp869+/P06dOoXZs2djyZIlsrPgampqMHfuXFRXV6Nv375utR8VFYXIyEiUlJSgsrJSdnQ3OzsbABxOI/z555+xdetWGI1GbNy4ERs3brQuO3/+PADghx9+wLRp09CmTRtMmDDBrRgDWU1NjdIhEFlpLR+DtX5prZ8o8DEn3ROstUvtmMekBC3lHWsXEamdV4Nvw4cPx4YNG7B3717ccsstuOOOO9CuXTtERkairKwMp06dwpYtW/Dnn38iNjYWo0ePdqt9QRCQlpaGo0ePIiMjo9615QoLC5Gbm4uEhASHvwZK5zYXFhZiy5YtsuucP38e58+fx9VXX80iSkQ+wfpFRFrE2kVEWsTaRURq59XgW1JSEpYsWYLJkyfj/Pnz+Pe//11vHVEUYTQasXTpUo8ucti7d28cPXoUu3btqldEd+3aBVEUnc6o69GjB06ePCm7bOPGjZg1axZuu+02vPrqq27HRkTkDOsXEWkRaxcRaRFrFxGpmdcnxHbt2hXbt2/HlClT0KVLFyQmJkKv1yM6OhodO3bEpEmTsH37dnTr1s2j9u+66y5ERERg5cqV+P77762PnzlzBosXL4YgCBg1apT18ezsbGRkZFinFRMRKYX1i4i0iLWLiLSItYuI1MyrmW+S2NhYjB07FmPHjvVFc3aaNm2K2bNnY+7cuXjggQfQo0cPhIaG4uDBgzCZTJg6dSrS09Ot67/22mvYtGkTBg8ejAULFvg8HiIiV7F+EZEWsXYRkRaxdhGRmvlk8M3fhg4ditTUVKxYsQJHjx6FXq9H+/btMXr0aAwcOFDp8IiIHGL9IiItYu0iIi1i7SIitdLE4BsA9OnTB3369GlwvQULFrj8y8WQIUMwZMgQb0MLSIIgICEhwS+3ayZyl9bzMVjql9b7iQIPc9I7wVK71I55TErQct6xdhGRt/xR+zQz+EaNSxAE2Vt0EymB+agN7CdSG+YkBQLmMSmBeUdEwcwfg29e33CBApPFYsHFixdhsViUDoWI+agR7CdSG+YkBQLmMSmBeUdEwcwftY+DbyRLEAQkJSVpcqo5BR7mozawn0htmJMUCJjHpATmHREFM858o0al1+uVDoHIivmoDewnUhvmJAUC5jEpgXlHROQ7HHwjWaIoIjMzE6IoKh0KEfNRI9hPpDbMSQoEzGNSAvOOiIKZP2ofB9+IiIiIiIiIiIj8hINvREREREREREREfsLBNyIiIiIiIiIiIj/h4BsREREREREREZGfcPCNiIiIiIiIiIjITzj4RkRERERERERE5CccfCNZgiAgNTUVgiAoHQoR81Ej2E+kNsxJCgTMY1IC846Igpk/ah8H38ghs9msdAhEVsxHbWA/kdowJykQMI9JCcw7IiLf4eAbyRJFEbm5uRBFUelQiJiPGsF+IrVhTlIgYB6TEph3RBTM/FH7DD5vkQKCTqdD06ZNlQ6DCADzUSvYT6Q2zEkKBMxjUgLzjoiCmU7n+3lqnPlGskRRRGVlJX/tIlVgPmoD+4nUhjlJgYB5TEpg3hFRMPNH7ePgG8kSRRH5+fn8wCVVYD5qA/uJ1IY5SYGAeUxKYN4RUTDj4BsREREREREREZGGcPCNiIiIiIiIiIjITzj4RkRERERERERE5CccfCMiIiIiIiIiIvITDr4RERERERERERH5CQffyCGDwaB0CERWzEdtYD+R2jAnKRAwj0kJzDsiIt9hRSVZOp0OKSkpSodBBID5qBXsJ1Ib5iQFAuYxKYF5R0TBTKfz/Tw1znwjWaIoory8HKIoKh0KEfNRI9hPpDbMSQoEzGNSAvOOiIKZP2ofB99IliiKqKio4AcuqQLzURvYT6Q2zEkKBMxjUgLzjoiCmT9qH087JVk6nQ6JiYlKh0EEgPmoFewnUhvmJAUC5jEpgXlHRMGMp51SoxFFESUlJfy1i1SB+agN7CdSG+YkBQLmMSmBeUdEwYynnVKj4QcuqQnzURvYT6Q2zEkKBMxjUgLzjoiCGQffiIiIiIiIiIiINISDb0RERERERERERH7CwTciIiIiIiIiIiI/4eAbERERERERERGRn3DwjYiIiIiIiIiIyE8MSgfgqsOHD2P58uU4fvw4KisrkZ6ejgcffBCDBg1yuY3ff/8db7/9Ng4ePIjc3FxERkaiU6dOeOihh9C7d28/Rq89giAgMjISgiAoHQqR5vMxWOqX1vuJAg9z0jvBUrvUjnlMStBy3rF2EZG3/FH7NDHzbcuWLRg5ciQOHz6M9u3bo3v37jh27BieeOIJLF261KU2vvvuOwwZMgQbN25EaGgo+vbti3/84x/Yv38/xowZg3fffdfPW6EtgiDAaDRq8gOXAo+W8zGY6peW+4kCE3PSc8FUu9SOeUxK0GresXYRkS/4o/apfuZbXl4e5syZg4iICKxevRqdOnUCAGRkZGDkyJFYunQpbrzxRrRr185hGzU1NXjqqadQXl6OJ554AuPHj7e+mQcOHMC4cePw6quvonfv3khLS2uU7VI7URRRVFSEuLg4zX3oUuDRaj4GW/3Saj9R4GJOeibYapfaMY9JCVrMO9YuIvIVURR9XvtUP/Nt3bp1qKysxPDhw60FFADatGmDKVOmQBRFrFq1ymkbhw4dwoULF9CxY0dMmDDB7k287rrrcO+998JiseCzzz7z23ZokV6vVzoEIist5mMw1i8t9hMFNuak+4Kxdqkd85iUoLW8Y+0iIjVT/cy3L7/8EgAwYMCAesv69+8PQRCwd+9ep22Ul5ejU6dO6NOnj+zyVq1aAQCysrK8ijWQCIKAmJgYpcMgAqDdfAy2+qXVfqLAxZz0TLDVLrVjHpMStJh3rF1E5CtBedrp6dOnAQBt27att8xoNCIpKQk5OTnIy8tDYmKibBsDBgyQLcKSn3/+GQCQmprqg4gDg8ViQUFBAeLj46HTqX6CJAU4reZjsNUvrfYTBS7mpGeCrXapHfOYlKDFvGPtIiJfsVgsPq99qh58KyoqgslkQlRUFKKiomTXSUlJQU5ODnJychwWUWdOnz6Nbdu2QRAEDBw40Om6x44dQ2RkpGwMTZo0gSiKADwbJbVYLA6XSe1J5x27274oitbY5Oh0OrvYRVGExWJBZWVlvbik1/c0GRuKxXZbpbhcid2T90TaDmexSOvY/t3Q+u6S3ktP+snR+t7EYtsHjrjTT54WLdttdZSPnuag9K+jbXXWz64Kxvol1091c8VdatgvpDhs/3XUvrSOq/u0p3H4un550ze+rl/uxmJbz+vu067kpKNYPH1PpO2TXkvuffek7brbJ1e/WLvc4+qxl9K1SxRFmEwmj4691FK7pPbVcOylltolUcOxl6Pa5YtjL1dql7SeN1i7YNdeY35vbOi4i7XL8fruxsHaZc/V4y5P2vZH7VL14Ft5eTkAICIiwuE6YWFhduu6Iz8/H5MmTUJNTQ3uuusupxffBICRI0fKPj5mzBhMnz4dRUVF0Ov1Hk3RzsvLQ3V1teyy5ORk6PV6ZGZmIjU11e0dtbKyEgUFBbLLwsLCkJiYiNLSUpjNZhiNRhQVFaGsrAylpaUA7D8UYmJiEBMTg+LiYhiNRrfiAGqTNTMz0+Hy1NRUmM1m5ObmomnTpjCZTMjPz5dd12AwICUlBZWVlU5zRE55eTmioqJQUFAAk8kku47RaERkZCRyc3MRGxuL8PBwZGVlOSwWycnJCAkJcSsOACgsLERCQgIqKipQWFgou46jfnKU93FxcQ4PPJzJyspCUlKSNd8ccbWfQkJCkJyc7HYcAOz2p8LCwnr56GnbNTU1dvuT2WxGTk6O3TplZWUexWxLi/UrJCTEo7yR6pcoivX6SapfeXl5HvWXJ/XL0fsp1S9P2OaNI57UL3dJ729MTIzP6pen+1JVVRWKioqQkpLis/rlSSxVVVUICwuTrV9yOelKP3n6npSWliImJsauftXtJ0/bzsvLg9FodFq/WLvc48qxlxpql7RtJSUliI+PdysOtdQuQD3HXmqpXRI1HHvJ1S5fHXu5UrsA7+uXFmtXoHxvbOi4y5Pvjaxd9bF21efKcZenbefn5yM+Pt6ntUvVg2/S6KSzouHKqLic7OxsPPzww/j999/RsWNHzJ07t8HnrF692uEvGEDtgIennP36YvuFwZNfR8LDwxucGh0dHW39Oy4uzvr/TZo0sRslll4/NjbW7Tik5zuLRRAEGAwGNGnSBEBt8Wgo9vDwcLfjkPrR2UGstK1JSUnWv6W4nK3vLunDKCIiosFtqdtPjvrB01iaNGlil2+OeNJP7rLdn4xGI0wmU7189ITBUFv2pP3JYDDUi1062PRGMNYv6Zcf236Stt+TX5gBz+qXr/cLwD5vHGmM/cJ2W31ZvzwRGhqKpKQkAL6tX57EAcjXL0c56a9+krbRdn9yd7DEkcTExHrHA3XrF2uXe1w59lJD7RJFEWVlZR59SVdL7QLUc+ylltolUcOxl1zt8tWxlyu1C/C+frF21VLie2NDx12e7DOsXfWxdtXnz+OuhIQE601nfFW7VD34Jv3SV1lZ6XCdqqoqAJAtbo789ttvGDduHC5cuIBOnTrh3XffdWnmVIcOHeySuC5vvti58qHmafuuTGu1XS4IAnQ6nfVfudg8/RB2dYqttI67sbsTB+Dadtiu449rXtgeLLjbT97knLNY6r5WQ/H4Ixbb9hrKR0/arfte2vJFPwdr/XLUT41Zv3ydi7avobb9oiH+rF+226dkP8nVc7nPNEc56Y9Y3O0nVzjbPl++VrDWLl+378t9QhpE9qR/1VK7bNtn7bKnhmMvR7XLF8dertSuuut5grXLN+37Y59g7fJdHKxdjtuUi8sbUg2s2743tUvVV8+MiopCZGQkSkpKHBbS7OxsAHB5GuGBAwcwbNgwXLhwAb169cKqVau8+uWBiEgO6xcRaRFrFxFpEWsXEamdqgffBEFAWloaACAjI6Pe8sLCQuTm5iIhIcE6BdOZLVu2YOzYsSgtLcU999yDt956y6NrhBARNYT1i4i0iLWLiLSItYuI1E7Vg28A0Lt3bwDArl276i3btWsXRFFE3759G2zniy++wIwZM1BTU4PHH38cL7zwgvVccqpPEAQYjUa/TN0lcpdW8zHY6pdW+4kCF3PSM8FWu9SOeUxK0GLesXYRka/4o/apfvDtrrvuQkREBFauXInvv//e+viZM2ewePFiCIKAUaNGWR/Pzs5GRkaGdVoxAOTm5mLWrFkwm8149NFHMXHixEbdBi0SBAGRkZGa+sClwKXVfAy2+qXVfqLAxZz0TLDVLrVjHpMStJh3rF1E5Cv+qH2qH8Jv2rQpZs+ejblz5+KBBx5Ajx49EBoaioMHD8JkMmHq1KlIT0+3rv/aa69h06ZNGDx4MBYsWAAAWLlyJQoLC2EwGPDnn39i2rRpsq911VVX4f7772+U7VI7i8WC3NxcJCUl+eUmA0Tu0Go+Blv90mo/UeBiTnom2GqX2jGPSQlazDvWLiLyFYvF4vPap/rBNwAYOnQoUlNTsWLFChw9ehR6vR7t27fH6NGjMXDgwAaff/jwYQBATU0Ntm7d6nRdFtFagiAgNjZWU792UeDScj4GU/3Scj9RYGJOei6YapfaMY9JCVrNO9YuIvKFoJz5JunTpw/69OnT4HoLFiyw/nIh2bBhg7/CCliCICA8PFzpMIgAaD8fg6V+ab2fKPAwJ70TLLVL7ZjHpAQt5x1rFxF5Kyiv+UbKsFgsuHjxIiwWi9KhEDEfNYL9RGrDnKRAwDwmJTDviCiY+aP2cfCNHBJFUekQiKyYj9rAfiK1YU5SIGAekxKYd0REvsPBNyIiIiIiIiIiIj/h4BsREREREREREZGfcPCNiIiIiIiIiIjITzj4RkRERERERERE5CccfCMiIiIiIiIiIvITDr6RLEEQkJycDEEQlA6FiPmoEewnUhvmJAUC5jEpgXlHRMHMH7WPg2/kkF6vVzoEIivmozawn0htmJMUCJjHpATmHRGR73DwjWSJoojMzEyIoqh0KETMR41gP5HaMCcpEDCPSQnMOyIKZv6ofRx8I1mCICA1NZVTzUkVmI/awH4itWFOUiBgHpMSmHdEFMx42ik1KrPZrHQIRFbMR21gP5HaMCcpEDCPSQnMOyIi3+HgG8kSRRE5OTmcak6qwHzUBvYTqQ1zkgIB85iUwLwjomDG006JiIiIiIiIiIg0hINvREREREREREREfsLBNyIiIiIiIiIiIj/h4BsREREREREREZGfcPCNiIiIiIiIiIjITzj4Rg4JgqB0CERWzEdtYD+R2jAnKRAwj0kJzDsiIt8xKB0AqZNOp0PTpk2VDoMIAPNRK9hPpDbMSQoEzGNSAvOOiIKZTuf7eWqc+UayRFFEZWUlRFFUOhQi5qNGsJ9IbZiTFAiYx6QE5h0RBTN/1D4OvpEsURRRXFzMD1xSBeajNrCfSG2YkxQImMekBOYdEQUzf9Q+nnZKsnQ6HVJSUpQOgwgA81Er2E+kNsxJCgTMY1IC846IghlPO6VGI4oiysvL+WsXqQLzURvYT6Q2zEkKBMxjUgLzjoiCGU87pUYjiiIKCwv5gUuqwHzUBvYTqQ1zkgIB85iUwLwjomDGwTciIiIiIiIiIiIN4eAbERERERERERGRn3DwjYiIiIiIiIiIyE84+EZEREREREREROQnHHwjIiIiIiIiIiLyEw6+kUNhYWFKh0BkxXzUBvYTqQ1zkgIB85iUwLwjIvIdg9IBkDrpdDokJiYqHQYRAOajVrCfSG2YkxQImMekBOYdEQUznc7389Q4841kiaKIkpISiKKodChEzEeNYD+R2jAnKRAwj0kJzDsiCmb+qH0cfCOHzGaz0iEQWTEftYH9RGrDnKRAwDwmJTDviIh8h6edkixBEGA0GpUOgwgA81Er2E+kNsxJCgTMY1IC846IgpkgCD5vUzMz3w4fPozRo0ejZ8+e6NKlC4YNG4bPPvvMrTZKS0uxaNEi3HzzzejcuTP69u2Lf/3rX8jLy/NT1NqVlZWFV155BVlZWUqHQqT5fAyW+qX1fqLAw5z0TrDULrWT8pjvGTUmLddP1i4i8pY/9nVNDL5t2bIFI0eOxOHDh9G+fXt0794dx44dwxNPPIGlS5e61EZZWRkefPBBLF++HDU1Nbj++usRERGBjz76CIMHD9bkB4s/ZWdn45133kF2drbSoRBpOh+DqX5puZ8oMDEnPRdMtUvtpDzOz89XOhQKIlqtn6xdROQL/vjMVf3gW15eHubMmYOIiAh8+OGHePfdd/H2229j8+bNSEpKwtKlS3HixIkG21m6dCl++eUX3HbbbdixYweWLFmC7du346GHHkJWVhZeeOGFRtgaIgomrF9EpEWsXUSkRaxdRKRmqh98W7duHSorKzF8+HB06tTJ+nibNm0wZcoUiKKIVatWOW2jtLQUH330ESIiIjBnzhwYDLWXutPpdHjqqafQvHlz7Ny5ExcuXPDrthBRcGH9IiItYu0iIi1i7SIiNVP94NuXX34JABgwYEC9Zf3794cgCNi7d6/TNr799luUl5ejW7du9S4cqtfrccMNNwBAg+0QEbmD9YuItIi1i4i0iLWLiNRM9YNvp0+fBgC0bdu23jKj0YikpCTk5eU5vSDeb7/9BgC47LLLZJdLbZ86dcrbcImIrFi/iEiLWLuISItYu4hIzQxKB+BMUVERTCYToqKiEBUVJbtOSkoKcnJykJOTg8TERNl1pAuFpqSkyC5PTk4GAOTm5souF0URAPDdd98hIiJC9vkpKSnW9aTb0raK1cFSFeJo86zrlJaWwmKxOFxHak8URQiCAEEQ3GpbFEVrbHJ0Op1d7KIowmKxoH379rBYLCguLraLRRAEWCwW6HQ6l+JwJxbbbZXiciV2T94TaTucxSKtY/t3Q+u7+55I76Un/eRofU/yBIB1WwH4rJ88yRPb15fed7l89DQHpfYdbWvdGDyh5frlSQ4DkO2nurni65rhyX4hte9p3jjiSf1yNZa6cfi6fnnTN76uX67GIlfP6+7TruSko1g8fU+k7ZNeS+5996TtutsnV79YuzyrXXLUVLssFgvS09NRU1OD0tJSj2qGUrWrbixKHnuppXbZxgIoe+zVUO3y5tjLndoFeF+/tFy7AG1/b2zouMud742sXc7jYO2yj8PV4y5P2i4vL0dJSYk1dl/ULlUPvpWXlwOAbOGShIWF2a3rrJ3w8HDZ5dLjjtooKysDAIwbN66BiN13BkDX+T5v1mdtDx06VDWx+IJa4gDUE4ta4gAajsWbfPRkO8vKyhATE+PR6wVz/fJF3VBLXjKO+tQSiztx+OqzzBexNEbbrF3Oqa2/XHXnnXeqIg53qSUWtcQBqCcWV+LwtH56uo2e1i/WLvW27Q61xAGoJxa1xAGoJxY17guu1C5VD75Jo9PSKKMcV0bF9Xq903YaaiMlJQWffvopysvLZdtISkqy/gpCRNoniiLKysoc/urpCtYvImpsrF1EpFXe1i/WLiJSgju1S9WDb9KU4crKSofrVFVVAQAiIyMdriMtc9SOyWQC4PiXEp1Oh44dOzYcMBEFDE9njUhYv4hICaxdRKRV3tQv1i4iUoqrtUvVN1yIiopCZGQkSkpKHBZA6bx8Z78gSKOQjs7Nz8nJabANIiJ3sH4RkRaxdhGRFrF2EZHaqXrwTRAEpKWlAQAyMjLqLS8sLERubi4SEhKQlJTksB2pDekOOHVJd7WR1iMi8hbrFxFpEWsXEWkRaxcRqZ2qB98AoHfv3gCAXbt21Vu2a9cuiKKIvn37Om2jW7duiIyMxOHDh613rJCYzWbs2bMHOp0Offr08V3gKrd582akp6fj66+/dut5paWlWLRoEW6++WZ07twZffv2xb/+9S+nt+wmaoin+ThkyBCkp6c7/E/u4KsxBUr9Onz4MEaPHo2ePXuiS5cuGDZsGD777DO32lB7X5H28HPMfwKldmmBxWLB+vXrce+99+Kqq65Cp06dcNNNN+GVV16xu8OkJDc3F8899xwGDBiAzp07o3///njllVesF3kn8kRVVRVuu+02pKen448//qi3XCt5x9pFrgiUfCfPZWVl4V//+hf69euHjh074rrrrsO0adPw559/1lvXl/mg+sG3u+66CxEREVi5ciW+//576+NnzpzB4sWLIQgCRo0aZX08OzsbGRkZ1mnFQO05+UOGDEFZWRmeeeYZ6/n+oijilVdewfnz5zFgwAC0aNGi8TZMQT/99BOef/55t59XVlaGBx98EMuXL0dNTQ2uv/56RERE4KOPPsLgwYORlZXlh2gp0Hmaj9XV1Th16hRiY2Nx2223yf7n7bWPvBUI9WvLli0YOXIkDh8+jPbt26N79+44duwYnnjiCSxdutSlNrTQV6Qt/Bzzr0CoXVpgsVjw+OOP45lnnsGJEyfQoUMHXHvttSguLsY777yDu+++2+7Ut+zsbAwdOhTr1q1DeHg4rr/+elgsFrzzzju47777UFpaquDWkJa99tprOHXqlOwyLeUdaxe5IlDynTxz4sQJ3H777fjoo48QERGB66+/HpGRkdiyZQuGDh2KCxcuWNf1eT6IGrB+/XoxLS1NvPzyy8WHHnpIHDt2rNipUycxLS1NfOutt+zWnTFjhpiWlibOmDHD7vHi4mLxlltuEdPS0sR+/fqJjz32mHjzzTdb/z8rK6sxN0kxu3fvFrt16yampaWJaWlp4oEDB1x+7oIFC8S0tDRx6tSpYnV1tSiKomg2m8V58+aJaWlp4qRJk/wVNgUob/Lx119/FdPS0sRHH33UjxF6T8v1Kzc3V+zcubN45ZVXij/99JP18dOnT4vXXnutmJ6eLh4/frzBdrTSV6QN/BxrHFquXVqxYcMGMS0tTRw4cKD4559/Wh8vKSkRx40bJ6alpYmTJ0+2Pv7YY4+JaWlp4quvvmp9zGQyiZMnTxbT0tLE+fPnN2b4FCAOHjwopqenW2vq2bNn7ZZrLe9Yu8iZQMt3co/JZBIHDRpUrx7U1NSIzz//vJiWliaOHz/e+riv80H1M98AYOjQoVixYgW6du2Ko0eP4siRI2jfvj3eeOMNjB071qU2YmJi8OGHH1p/7dizZw+qq6tx//33Y/369R7f1lorsrKyMGvWLDz66KOorq52eq0DOaWlpdbR4Tlz5sBgqL1Rrk6nw1NPPYXmzZtj586ddiPFRI54m48AcOzYMQBAhw4dfB2eT2m5fq1btw6VlZUYPnw4OnXqZH28TZs2mDJlCkRRxKpVqxpsRyt9RerGz7HGpeXapRWffvopAGDmzJl2s2iio6Mxb948CIKAXbt2obKyEn/++Sc+//xzNG3aFI8//rh13dDQUDz//POIiorC+vXrnd7pkaiu4uJizJw5Ey1btpS9gYAW8461ixwJxHwn9/zvf/9DRkYGBgwYYFcP9Ho9pk6dimbNmiEzMxNms9k/+eD18CFpgvTLzpAhQ8QTJ06IDzzwgFszBr744gsxLS1NfPjhh2WXv/DCC2JaWpq4bt06X4ZNAcrbfBRFUXzuuefEtLQ08csvv/RjpMFt8ODBYlpamnj06NF6ywoKCsT09HSxZ8+eDbbDviJf4OcYBZpx48aJN998s5ifny+7/OqrrxbT0tLEv/76S1yzZo2YlpYmPvPMM7LrTpgwgXWW3DZlyhTx8ssvF48ePSr269ev3kwg5h0FEuY7TZw4UUxLSxO//fbbBtf1Rz5oYuYbea9169Z46aWX8PHHHyM9Pd3t50t39rnssstkl7dt2xYAHJ4/T2TL23wE/p5NlZubi1GjRqFHjx7o0qULRo4cia+++sqX4QYt6U5f0v5ty2g0IikpCXl5eQ1eqJ59Rb7AzzEKNMuXL8dnn32G+Pj4esvOnTuHwsJChISEICEhwZqXju6wyPwld23duhVbt27F2LFjccUVV8iuw7yjQMF8J6D2O4lOp0Pnzp2RnZ2N9957D3PnzsVLL72Ew4cP263rj3wweBg3aYyr06wdkS5E6miatTR11/bCwESOeJuPFosFJ0+eBADMnj0b7dq1Q/fu3fH777/j0KFDOHToEGbMmIHRo0f7ItygVFRUBJPJhKioKERFRcmuk5KSgpycHOTk5CAxMVF2HfYV+Qo/xyiYLF68GABw/fXXIywsDDk5OQAge6qU7ePMX3LFxYsX8eyzz6J9+/aYOHGiw/WYdxQImO8E1N7l9q+//kJ8fDy+/PJLzJw50+6Ope+99x4GDx6MF154AQaDwS/5wME3ckl5eTkAIDw8XHa59Li0HpE//f777ygvL0dYWBgWL16MG264wbps+/btmD59Ol555RV069YNnTt3VjBS7ZL25YiICIfrhIWF2a0rh31FasHPMdKKNWvWYOvWrYiIiMCTTz4JgPlLviOKImbMmIHKykq8/PLLCAkJcbgu8460jvlOEunOpOXl5Zg6dSr69euHxx9/HKmpqfj222/xr3/9C5s2bUJKSgqmTJnil3zgaafkEr1eDwAQBEF2uSiKdv8S+VObNm1w8OBBbN++3W4wBwBuueUWDB8+HBaLBR9++KFCEWqfTlf78eBonwdc2+/ZV6QW/BwjLVi9ejVefPFFCIKAF198EW3atAHA/CXfef/993Ho0CE8+eSTDk/DlzDvSOuY7ySpqqoCAJhMJlx55ZVYsmQJ2rZti+joaPTr1w///ve/odPpsHLlShQXF/slHzj4Ri6JjIwEAId38zCZTACcz5Ih8qWEhAQ0b95cdlm/fv0AAL/88ktjhhRQpFNNnd3BR/oQk+qDI+wrUgN+jpGaiaKIV155BS+++CJ0Oh3mz5+Pf/7zn9blUv5KeVoX85dccfLkSSxatAjdu3fHQw891OD6zDvSMuY72bKdwTZ8+PB6yzt27IiOHTvCZDLhhx9+8Es+8LRTcol0jRxH5zQ3dE40UWOS8rCiokLhSLQrKioKkZGRKCkpQWVlpeyUa+kaWt7s9+wraiz8HCO1qqysxPTp07Fz506Eh4dj4cKFuPHGG+3WkfJXytO6mL/kitdeew1VVVUQBAFPPfWU3bKCggIAwEsvvYTIyEhMmDCBeUeaxnwnWzExMQgJCUF1dbXDSQHNmjXDTz/9hIKCAr/kAwffyCXSXT6kux/WJd1FztHdQIh8aefOndixYwd69OiBe++9t97yc+fOAQBSU1MbO7SAIQgC0tLScPToUWRkZKBDhw52ywsLC5Gbm4uEhAQkJSU5bId9RWrBzzFSo9LSUowZMwY//PADEhMTsWzZMtk78Ul5mZGRIduOlL+e3kGcgoN0baK6d/WztXv3bgDAPffcw7wjTWO+ky29Xo82bdrgxIkTyMrKQseOHeutI/1Am5iY6Jd84Gmn5JJu3bohMjIShw8fRklJid0ys9mMPXv2QKfToU+fPgpFSMGkpKQE27Ztw9q1a2XPs9+0aRMAoFevXo0dWkDp3bs3AGDXrl31lu3atQuiKKJv375O22BfkVrwc4zUprq6GmPHjsUPP/yAli1bYv369bIDb8Df9XjPnj0wm812y0pKSnDo0CFERkaia9eufo+btGvNmjU4efKk7H/NmjUDUPuj2cmTJ9GjRw/mHWka853qkr63bNu2rd6yvLw8HDt2DCEhIejcubNf8oGDb1RPdnY2MjIyrKeUAbXnMg8ZMgRlZWV45plnrNd6kq5Rcv78eQwYMAAtWrRQKmwKUHL5OHDgQMTHx+PUqVN4/fXXYbFYrMs2bNiA//3vf0hMTMSwYcOUCDlg3HXXXYiIiMDKlSvx/fffWx8/c+YMFi9eDEEQMGrUKOvj7CtSC36OkRYsXboUR44cQXJyMtasWeM095o1a4Z+/frh/PnzeOWVV6w/ZlRVVeGZZ55BWVkZhg0bhujo6MYKn4IA846CCfM98A0bNgyRkZHYtm0bPv74Y+vj5eXlePrpp1FeXo477rgDcXFxfskHQeTtOoLSiBEjcPjwYbz//vu49tpr7ZbNnDkTmzZtwuDBg7FgwQLr4yUlJRg2bBhOnz6NZs2aoWPHjvjtt99w5swZNGvWDB999JH13Ggid3iSj1999RUmTpwIk8mEVq1aIT09HWfPnsXJkycRGRmJd955h79M+cCGDRswd+5c6PV69OjRA6GhoTh48CBMJhOmTp2KsWPHWtdlX1Fj4ucYaVlRURH69u2LiooKtGvXzuld+GbOnImkpCRcuHABw4YNQ3Z2Nlq3bo3LLrsMP//8M/766y906NABa9assd4sh8hdN9xwAy5cuICdO3eiZcuW1seZdxSImO/Ba+fOnZgyZQqqq6uRlpaG5s2b4+eff0ZOTg7atWuHNWvWIDY2FoDv84Ez38hlMTEx+PDDD60zXfbs2YPq6mrcf//9WL9+Pb+wUKPq3bs3Pv30U9x6660oLS3FF198gYKCAgwZMgT//e9/OZjjI0OHDsWKFSvQtWtXHD16FEeOHEH79u3xxhtv2A28OcO+IrXg5xipxY8//mi90cyJEyewZcsWh/+VlZUBqJ2V8cknn+Cee+5BSUkJ9uzZg9DQUIwfPx6rV6/mF0LyC+YdBRPme+AbOHAgPv30U9xyyy3Iy8vD/v37ERUVhYkTJ+LDDz+0DrwBvs8HznwjIiIiIiIiIiLyE858IyIiIiIiIiIi8hMOvhEREREREREREfkJB9+IiIiIiIiIiIj8hINvREREREREREREfsLBNyIiIiIiIiIiIj/h4BsREREREREREZGfcPCNiIiIiIiIiIjITzj4RkRERERERERE5CccfCMiIiIiIiIiIvITg9IBkHdKS0uxefNmfPHFFzh58iQKCwsRGhqKFi1aoGfPnhg2bBguvfRSpcNUzFdffYVHHnkEL730Eu644w7r4+np6QCA+fPnY8iQIUqF5zcPPvggMjMzsWnTJkRGRrr13DfeeANLly61e0x6nzZu3IhZs2bZrTtw4MAG2xw3bhy+/PJLAMBtt92GV199FcDf/SBp0qQJ9u3b51a8FJhY25xzVNtsnTt3Dp988gm+/vprnD9/HsXFxYiOjkabNm3Qp08f3HPPPUhMTGzUuIuKijBw4ED06tULCxcudPv5I0aMwOHDh+0e2717N5o3b46ZM2di06ZNAAC9Xo/9+/cjISHBaXvV1dW47rrrUFRUBAB44YUXcM899+DQoUMYOXKk3bq2tYsoENT9DK7LYDAgKioKzZs3R+/evfHwww8jNja23no33HADLly4gEmTJuGxxx5z2qbtviXtuw05f/48+vfvb/fY1VdfjTVr1gAAzGYz3nzzTWzcuBG5ubm49NJL8cgjj+D222+Xba+wsBADBgxAq1at8PHHH7v0eldddRU+/PDDBmMlIvfV/X7hKts6EGgaqnu2qqqqsHXrVuzcuRO//vor8vPzYTAY0LRpU3Tv3h333nsvOnToYPccHucog4NvGrZnzx7MmjULBQUFAACj0Yi0tDQUFRXh9OnTOHnyJNatW4eJEydiwoQJCkfb+IqKijBr1ix06tTJ4QFYoHr66adx5513YsGCBXjuuec8aiM6OhppaWkA4PAL+o4dOxocfCsqKsKBAwdkl1111VUAgPz8fJw9e9ajOCnwsLY511BtM5vNWLhwIVavXo3q6moIgoAWLVrgkksuQWZmJo4cOYIjR45g+fLlmDNnDu6+++5Giz0uLg6TJ0/Gs88+i379+uHWW2/1qJ2mTZuiadOmAICwsLB6y81mM3bu3Ilhw4Y5befAgQPWgTdbMTEx1vr0xx9/IC8vz6M4ibSgVatWsgPVVVVVOHfuHI4dO4Zjx45h48aN2LBhg3XfU0KHDh0QFhZmPT4BgCVLlmD58uWIiIhA69atcebMGUyfPh0VFRW4995767WxbNkyFBcXY/r06bKvERYWZt3/L168iIsXL/pnY4gIQO33DGmfsyXtf6GhoejYsWO95bZ1IJDJ1T3J0aNHMWXKFFy4cAFA7fFLmzZtUFFRgXPnzuHMmTPYsGEDhg8fjlmzZsFgMFjX43FO4+Pgm0a99957eOmllwAAgwYNwsSJE3HZZZdZl2dnZ2PZsmX44IMPsHjxYphMJjzxxBMKRauMhQsXIicnB4sXL4YgCEqH06jS0tIwZMgQbNiwAXfeeafsB5orbTj6lddgMKCmpgZ79uyByWSS/fIr+fzzz1FdXS27TGrf01+8KPCwtjXMWW2rqanBhAkTsG/fPhgMBjz88MN48MEH0aRJE+s6p0+fxrJly7B161Y8/fTTOHXqFGbPnt1o8d97771YvXo15s2bhz59+sjOpGnIHXfcgSeffFJ2mVSfduzY0eDg2/bt22Ufb9++vbU+2c6oIwpE48aNc3gWgMViwZYtW/D0008jOzsbs2bNwsqVKxs3QBuLFi1Cy5Ytrf9fVFSE1atXIykpCZ988gmaNm2K48eP4+6778brr7+Oe+65Bzrd31fZOX/+PD744ANcf/31uPrqq2VfIzk52br/y50NQES+1bdvX/Tt27fe49L+Z7tPBqO6dU+yfft2TJ8+HTU1Nbj22mvx2GOP2X3nKy4uxvvvv4+3334ba9euRXFxMV555RUAPM5RCq/5pkFHjhyxTgl99NFHsXjxYrsvpwCQkpKCf/3rX3j00UcBAG+99RZ++eWXRo9VKb/99hs+/vhjXHPNNejWrZvS4Shi/PjxEAQB8+fP93nbiYmJaNu2LcrLyxs8TfSzzz4DUFvkiZxhbWtYQ7Vt4cKF2LdvH0JDQ/HOO+/gqaeesht4A4C2bdti4cKF1gHvVatWORyE8ge9Xo/x48cjLy8Pb731ls/bv/rqq6HT6XD48GHk5+c7XK+qqgq7d+9GQkICUlNTfR4HUSDQ6XS444478MgjjwAADh48iN9//13hqP52/PhxlJeXo3///tYZeZdffjm6d++OvLy8erEuWrQINTU1mDp1qhLhEhH5xNmzZ/H000+jpqYGQ4YMwbvvvltvskVsbKz1bAMA+O9//4vPP/9ciXDp/+Pgm8aIooi5c+fCbDbjiiuuwOTJk52uP2HCBDRt2hQWiwXvv/9+I0WpvKVLl8JisciebhAspGu0/PTTT9i7d6/P2x80aBCA2lNPHcnPz8c333yDNm3aNHhtGQpurG2ucVbbzp07h9WrVwMApk+fjp49ezpt66GHHsJNN90EAPi///s/lJaW+j5gB2655RYYjUasW7fO6QCZJxITE9GtWzeYzWanB5n79u1DaWkpbrrpJuj1ep/GQBRo+vXrZ/379OnTCkZiLycnBwAQHx9v97h0Gm1hYaH1sWPHjmHbtm248847g+Z0NSIKTC+88ALKy8txySWX4Nlnn7Wb4VvX3XffjSuuuAIA8O677zZWiCSDg28ac+TIEWRkZAAAxo4d2+D6oaGhmDdvHt5//308//zz9ZYXFRVh6dKluPPOO9GlSxdcccUVGDRoEF566SVkZ2fXW3/jxo1IT0/Hk08+ifLycixevBg33XQTOnXqhB49emD8+PH47rvvHMbz559/Yv78+bj55ptxxRVX4KqrrsKwYcOwYcMGWCwWN94Jx7KysrBr1y5ERUXhxhtvdOu5I0aMQHp6Ovbt24cTJ05g8uTJuPbaa9GxY0f0798f8+bNc/pF8X//+x/GjBmDa665Bh07dkSvXr3w2GOP4eDBg7Lrp6enIz09Hbm5uZg2bRq6dOmCrl27YuTIkaipqbGut2PHDowYMQLXXHMNrrzyStx7773YunUrzp8/j/T0dNxwww2y7d95550A4JeLkUqDb9Kpp3J27tyJmpoa/POf//T561NgYW1rWEO17YMPPkBNTQ2MRqPLPzxIp24WFRXhP//5j/XxQ4cOIT09HUOHDkVGRgbuu+8+dOrUCT179rSeFgwAFRUVeP/9963v89VXX43x48fjp59+sr6nM2fOrPe6oaGhuPnmm1FRUYFPPvnE3beiQa78OCDN9vP0unNEwcT2i50oigpGYk86bb3usVlubi6A2muGSl555RWEhoY2+OMOEWnLrl27MHbsWPTs2RMdO3ZE7969MXXqVBw7dkx2fen7V01NDbZt24Zhw4ahS5cu6N69O0aPHo0ffvgBAFBeXo5FixZhwIAB6NixI6699lo89dRTyMrKsmtP+j7WrVs3WCwWrFy5EoMGDULnzp3Rt29fPPnkk/j11199tr1//fUX9u/fD6D2JnuhoaENPmf27NlYvny5X844INdx8E1jvv76awC1p+1cc801Lj3n2muvxbXXXlvvrpcnT57ErbfeijfeeAMnT55Es2bNcOmll+LPP//E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CYLA+kTkgdatW1tn4589exZvvvmmwhHJ27t3Lw4dOoRhw4ahRYsW9ZZHRkZi9erVuOWWW1BaWoq8vDwMGDAAH374od+Pk4jIt8LCwrBs2TIsWrQIvXv3RnV1NY4fPw6z2YwePXrgpZdewuLFix3+COgPixYtwuTJkxEREYGTJ08iMTERI0aMwObNm12+9JA7brnlFuzcuROPPfYYrrrqKoiiiJMnTyIzMxOtW7fG/fffj02bNmH27NmN+j6QAyJRAJo5c6aYlpYmrl27VulQ/Grt2rViWlqaOGrUKNnld911l5iWliYePnzY5TaXLFkipqWlicOGDfNVmA369NNPxbS0NLF3796N9ppEWqSl2vbYY4+JaWlp4rJly+otKywsFDt16iR269ZNLC4udrnNBx54QExLSxNfe+01X4bq1IwZM8S0tDRx6tSpjfaaRPS3c+fOiWlpaWJaWpp49uzZRn1tJY6JiEhb/FGjGrPu8Tin8XDmGwWk8ePHQ6/Xu3RDArUymUzo1asX7r//fmRlZcmus3fvXgC1M33qOnHiBH7++WdcffXV6N69u19jJaLGoZbaduDAAfTv3x+TJ0+WXS7dYQyQr0+bNm2CyWTCgw8+6NWNKIiIiIiItICDbxSQWrZsicGDB+PEiRMO74CjdmFhYUhOTsaRI0ewYMECFBcXW5eZTCYsXboUe/fuRXh4OO666656z3/nnXcgCILDL8dEpD1qqW2XX345srKysGPHDrz77ruoqamxLsvLy8PUqVNRUFCA1q1bo2fPnnbPraqqwpo1axAfH4+RI0c2duhERERERI2ON1yggDVjxgzs378fL7/8MjZt2gSdTntjzU8//TQeeeQRbN++HV988QX+8Y9/QKfT4dy5cygrK0NYWBhefPFFXHrppXbP+/nnn7F161YMHz7cemcvd506dQr33XcfgNrZNn379vV6e+qS2pcuikpEDVNDbUtISMC0adMwf/58vPzyy3j77bfRrFkzVFRU4Ny5c6iurkZqaioWL16MkJAQu+euXbsW58+fx5IlSxAbG+vR6//nP/+xzqxbsmQJkpOTvd4mW7/++iuef/55ALDe5IOIlPfkk08iLCwM7du3x9y5c/3yGjk5OXj88ccBwHrjHCIipfij7vE4RxkcfKOAFRsbi3nz5uHhhx/Gpk2bZGeHqV23bt3w2WefYdWqVThw4AAuXryImpoapKSk4I477sDw4cPRtm3bes97+eWX0bJlS0ybNs3j1y4tLcX3338PoHYmiz9I7ROR69RS2x566CF07doVq1atws8//4wzZ84gJCQEbdu2xY033ojhw4cjPj7e7jmFhYVYvnw5br/9dtx0000ev/bFixetX4pNJpNX2yGnpKSE9YlIhY4dOwYAMBj89xXGZDJx/yci1fBH3eNxjjIEURRFpYMgIiIiIiIiIiIKRNo7D4+IiIiIiIiIiEgjOPhGRERERERERETkJxx8IyIiIiIiIiIi8hMOvhEREREREREREfkJB9+IiIiIiIiIiIj8hINvREREREREREREfsLBNyIiIiIiIiIiIj/h4BsREREREREREZGf/D/zJj/siIxAkgAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "df_cols = x_labels\n", - "sns.set_style(\"ticks\",{'xtick.direction': 'in', # set the style of the plot using seaborn\n", - " 'ytick.direction':'in',\n", - " 'xtick.top': False,'ytick.right': False})\n", - "\n", - "n_col = 4 # num of columns per row in the figure\n", - "for n in np.arange(0, 6, n_col):\n", - " fig,axes = plt.subplots(1, n_col, figsize=(15, 3.5), sharey = False)\n", - " fs = 20\n", - " for i in np.arange(n_col):\n", - " if n< len(df_cols):\n", - " axes[i].hist(df.iloc[:,n], bins = 20)\n", - " axes[i].set_xlabel(df_cols[n], fontsize = 18)\n", - " else:\n", - " axes[i].axis(\"off\")\n", - " n = n+1 \n", - " axes[0].set_ylabel('counts', fontsize = 18)\n", - " for i in range(len(axes)):\n", - " axes[i].tick_params(direction='in', length=5, width=1, labelsize = fs*.8, grid_alpha = 0.5)\n", - " axes[i].grid(True, linestyle='-.')\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "### df.to_excel(\"output.xlsx\") " - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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4eHo168fatasmeu2yGQyJCQkICEhAcnJybmu578qNTVVuH+pqalF3RwiIiIiKiC5SoRDBc/W1hbDhw/HgQMHcPr0afTt27eom5QvTE1NsXbtWsycORN+fn6YOVNzEpEhQ4Zg8uTJatvj4+Oxfft2lCtXDgsWLMhTW0JDQ9G4cWMAQPPmzbF///481fdfc/DgQaxevbqom0FEREREBYxB42ds9uzZcHV1xe+//45vv/0200yiX5IuXbrg/Pnz2Lt3L+7evYvAQPnSBGXLloWNjQ0GDRqEFi1aaCzr4OCAqKgoODg4qKzdSUREREREBaPIsqcS0X8Ps6fmHLOnEhERUVHjnEYiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEiUTlE3gIj+OzYNPFjUTfjy6FoXdQu+OHapr4q6CURERF8V9jQSERERERGRKAaNREREREREJIpBIxEREREREYli0EhERERERESiGDQSERERERGRKAaNREREREREJIpBIxEREREREYli0EhERERERESiGDQSERERERGRKAaNREREREREJIpBIxEREREREYli0EhERERERESiGDQSERERERGRKAaNREREREREJIpBIxEREREREYli0EhERERERESiGDQSERERERGRKAaNREREREREJIpBIxEREREREYli0EhERERERESiGDQSERERERGRKAaNREREREREJIpBIxEREREREYli0EhERERERESiGDQSERERERGRKJ2iPHl6ejouXLiAa9eu4enTp4iMjAQAmJiYwMrKCm3btkWfPn1QvHjxomxmkQgMDETnzp0BAPv27YOtrW2u6klISECfPn0gk8lw8eJF6OnpaTwuNTUVFy9ehIuLC54+fYqIiAhIpVIYGxvD2toaHTp0QJ8+fVCyZMlcX1N+2LRpE3bs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bNgwODo6YuDAgahSpQr09PRgaGgIS0tLDB48GMeOHcO0aZqzUiqWTlmzZg10/13ImoiIiIiIig6zpxJRoWH21Jxj9tScY/bUnGP2VCIiygx7GomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIiIiEgUg0YiIiIiIiISxaCRiIiIiIiIRDFoJCIiIiIiIlEMGomIiIjo/+zdd1hUxxoG8Hd36b0IAgKCBeyKotiNxN6NXaPRqFETE82NKUZTLDEaW4wx1hhL1Kix915iw4ZdsRd677C7sHv/OOwK7h5YkGby/p4nz/XuOTM7Z9j2nZn5hohIFINGIiIiIiIiEsWgkYiIiIiIiEQxaCQiIiIiIiJRDBqJiIiIiIhIFINGIiIiIiIiEsWgkYiIiIiIiEQxaCQiIiIiIiJRRmXdACL67+hUL7asm/DGUUFS1k144+ycdq6sm/DmMfYt6xa8cboqQ8q6CUREpYYjjURERERERCSKQSMRERERERGJYtBIREREREREohg0EhERERERkSgGjURERERERCSKQSMRERERERGJYtBIREREREREohg0EhERERERkSgGjURERERERCSKQSMRERERERGJYtBIREREREREohg0EhERERERkSgGjURERERERCSKQSMRERERERGJYtBIREREREREohg0EhERERERkSgGjURERERERCSKQSMRERERERGJYtBIREREREREohg0EhERERERkSgGjURERERERCSKQSMRERERERGJYtBIREREREREohg0EhERERERkSgGjURERERERCSqXASNoaGh8PX1ha+vL7Zu3Vps9W7fvl1b77NnzwpVVlOuMP+tWbNGWz4oKEj7+Pbt24vtml4VFRWFJk2aYODAgfmel56ejs2bN+Ojjz7C22+/DT8/P9StWxdt27bFuHHj8PfffyMzM7PE2mmoSZMmoU6dOrh9+3aR6xg6dKjO3+arr74qlvblfk0FBQW9Vl3btm2Dr68vZs6cWSxty0/u1+O5c+eKpc6ePXvq9POkSZOKpW4iIiIiKj+MyroB9Hq++uorJCcn4+uvvxY9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JSXMnTsXfX192tvbgfNVeKdNm8acOXMIDw8XI0AClyu0JcYmn3/+ORs3bkRPT4/rr7+eW2+9lYCAALS1tQed++233/L222/T3d3N4cOHsbGxkRYgJCSuAEk0SkhIqBxCFGz//v289957FBcXM3nyZK6//npmzJhBTU0NBQUFZGVlERsbKzYH19DQ4KOPPmLatGkjPIOrR21tLf/973/Ztm0bFhYWrFixggceeOCi55eWlnL06FF+/fVXUlNTgfNN2Tdu3DikYzbW6O3t5ciRI2JxoJKSEioqKqitrVWKYmhqahIREcF1113HlClTVL4R/VCkp6ezadMmmpqaKCsro76+XslGGhoajB8/nunTpzNlyhQ8PT1HcLQSo4H6+nr++c9/ilkkZmZmBAcH4+XlJVbe7erqYteuXezdu5fe3l6WLl3Kiy++OMIjl5AYfUiiUUJCQuUQImBr167l1KlTLFq0iKeffhpzc/NB57a1tXH06FH+97//kZuby/jx4/nggw+GPFcV2L59O6+88go6Ojo8+eSTLFmyBEDcyzhU5FAul5OSksInn3xCTEwMmpqaPPPMM1LFygtoaGigvLycwsJCCgoKKCoqoqSkhMrKShobG5XONTc3Jzg4mHfffVdlWmpcDg0NDVRUVAyyUUVFxSAbmZiYMGnSJCZPnsy0adOkhuwSSghRwu7ubr766it27txJcXExCoUCXV1dFAoFXV1dqKuri8+3hQsX8uyzz2Jubi5FGSUkrhBJNEpISKgkra2thIeHo6GhwaFDh7C1tRUdhKEE0s8//8yrr75Kc3Mz77//Ptdff71KOhV33HEHZ8+e5eGHH+aOO+7A0NDwsudZXV3NPffcQ05ODr6+vnz//fdjSvBcjKHSdHt7e6murqa0tJTi4mIKCgrEdi7V1dW0traio6NDUlKSSl5nA7nY/Pr6+qipqaGsrIyioiLy8/MpKiqivLyciooKMYUVYPny5WzYsEFKieb3662mpgYNDQ2xgNJYRMgqaWtrIy4ujqioKKKjo6mtrUUmk6GpqUljYyOenp7ccsstLFiwQFp8kJD4k0iFcCQkJFQKwaESSqoHBwdjZ2cntjqAoaNpN998M7/88gunTp0SV6tVzZFvbW3l7NmzaGhosHDhQrGS4OXMs6enB2trax5++GGefvppCgoKOHHiBLNnz77aw77mGXg9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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "corr = df.corr(method='pearson')\n", - "#np.fill_diagonal(corr.values, 0)\n", - "\n", - "corr.columns = x_labels\n", - "df_len = len(x_labels)\n", - "fs = 20\n", - "\n", - "fig, ax=plt.subplots(figsize=(df_len,df_len))\n", - "sns.set(font_scale=1.5)\n", - "sns.set_style(\"ticks\",{'xtick.direction': 'in', # set the style of the plot using seaborn\n", - " 'ytick.direction':'in',\n", - " 'xtick.top': False,'ytick.right': False})\n", - "mask = np.triu(np.ones_like(corr, dtype=bool),k=1)\n", - "cmap = plt.get_cmap('coolwarm')\n", - "sns.heatmap(corr, mask = mask, cbar_kws={\"shrink\": .2}, annot=True, fmt='.2f',\n", - " cmap=cmap, cbar=False, ax=ax, square=True)\n", - "ax.set_xlim(0, df_len)\n", - "ax.set_ylim(df_len, 0)\n", - "ax.set_title(\"Pearson Coefficients for Linear Correlation\", fontsize = 20)\n", - "plt.xticks(rotation=75, fontsize = fs)\n", - "plt.yticks(rotation=0, fontsize = fs)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjYAAAFtCAYAAAAUFyCcAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABmjElEQVR4nO3dd3wUdf7H8dfW9J4QQlEOFBRB6YiAeGBBReVQEERQT6oKYjmKiieIIKc/QUEhoHgnIhbgVEDxBMVTQARFQUFEUQSCmE56tszvj9yu2exms5tsssl+P8/Hw4dkZnbyec9MJp9M1WmapiGEEEIIEQL0wS5ACCGEECJQpLERQgghRMiQxkYIIYQQIUMaGyGEEEKEDGlshBBCCBEypLERQgghRMiQxkYIIYQQIUMaGyGEEEKEDGlshBBCCBEyjMEuQAhRsw0bNjBr1izn1waDgeTkZPr27cu0adNITU11mf748eO8/PLL7Nixg1OnTqHT6WjVqhWXX345t9xyi9v0APfccw8ffvgho0eP5tFHHw14hry8PObMmcPnn39OTk4OgwYN4oUXXvA47ZgxY/jiiy8A0Ol0REREkJyczPnnn88111zDlVdeiV5fu7/LNm7cSHZ2Nrfffntto3i0ZMkSli5d6vzaZDLRrFkzBg4cyNSpU4mNjfVrfgMHDqRXr148+eSTftdSXxmFaAqksRGiCVmwYAFt27altLSUvXv3kp6ezhdffMHGjRuJjIwE4OOPP+b+++8nISGB0aNH07FjRwB++OEH1q9fzyeffMLbb7/tMt/s7Gy2b98OVPxSnDFjBmFhYQGt/YUXXuDDDz9k/vz5nHXWWcTFxXmdvnXr1jz99NMAlJSUcOLECbZu3cq9995Ljx49WL58OTExMX7XsWnTJo4cOVJvv/RffPFFYmJiKCoq4pNPPuGVV17hwIEDvP766+h0Op/ns3TpUqKjo2tVQ31nFKIxk8ZGiCbk3HPPpXPnzgBcfPHF2Gw2XnjhBbZu3cr111/P8ePHuf/++2nTpg2vvPKKyy/+Pn36MHbsWD788EO3+b799ttYLBYuu+wytm/fzn/+8x+uu+66gNZ+5MgRzjrrLK6//nqfpg8PD6dLly4uw4YPH8769et56KGHmD17NosXLw5ojYFwwQUXkJiYCEDfvn3Jy8vjnXfe4auvvqJ79+4+z8fRkAoh/CPX2AjRhDl+8WdkZADwz3/+k+LiYv7+9797PJqh0+m48sor3YavX7+e5ORknnzyScLDw1m/fr3PNeTl5fHYY4/Rv39/OnXqxKBBg1i0aBHl5eUAnDhxgg4dOrBz505++uknOnToQIcOHdi9e3ctEsONN97IgAED2LJlCydPnnQOX7NmDaNHj6ZPnz506dKF6667jpUrV2KxWJzTjBkzhu3bt3Py5ElnHR06dHCOX7p0KcOHD6dXr15069aNv/zlL7z11lvU5V3BF110EfDHOqppeTkMHDiQmTNnOr/evXs3HTp0YNOmTSxatIh+/frRrVs3br/9do4ePepzxtdee43rr7+erl270rVrVwYPHswzzzxT63xCNDZyxEaIJuzYsWMAziMEn332GcnJyW5HOrz56quv+Omnn7jzzjtJSEjgyiuvZOPGjRw/fpzWrVt7/WxZWRljx47l+PHjTJkyhQ4dOrB3715WrFjBoUOHWLFiBc2aNeONN95gzpw5FBQUOE8vnXPOObULTcUv/U8++YS9e/fSsmVLAH799VeGDBlCq1atMJlMfP/99yxfvpyjR4+yYMECAP7+978ze/Zsjh8/7nI9jMPJkye5+eabadGiBQBff/018+bN4/Tp09xzzz21qvXXX38FKtaRL8urJs888wzdunXjiSeeoLCwkKeffprJkyfz3nvvYTAYvGbcvHkzc+bMYcyYMcyYMQO9Xs+xY8f48ccfa5VNiMZIGhshmhC73Y7VaqWsrIw9e/awbNkyoqKiGDhwIACnTp3i/PPP92ue69atAyqOhADcdNNNvPvuu2zYsIF7773X62f//e9/c/jwYRYvXszVV18NVJx+iYyM5Omnn2bHjh307duXLl26EB0djcVi8avpqo6j8fj999+dwypfXG232+nRowfx8fE89NBDzJw5k7i4OM455xxiY2Mxm80e63A0QI559OrVC03TeOWVV7j77rt9ukbGsY6Ki4vZvn07r7/+OmlpafTo0cPn5eXNOeec42wOAfR6PdOmTePAgQN06dLFa8avvvqK2NhYHnnkEeewPn361JhJiKZEGhshmpARI0a4fN2+fXsee+wxkpOTazW/oqIi3n//fbp27Uq7du0A6NWrF2eddRb//ve/mTJlite7jz7//HMiIyMZPHiwy/Bhw4bx9NNPs2vXrhp/UdeGp1NDBw8e5LnnnmPfvn3k5eW5jPvll1+cp4S82bVrF+np6Rw4cIDCwkKXcdnZ2T4t56p5u3XrxuOPP05YWFhAlpejiXVwnGbKyMiosWns3Lkzr776Kvfffz/XXHMN3bp1cx7tEyJUSGMjRBOycOFC2rVrh9FoJCkpiWbNmrmMT0tL48SJEz7P77333qO4uJirr76aM2fOOIdfffXVpKens2PHDvr371/t5/Py8khOTnY7kpGUlITRaHRrMALFcb2KI39GRgajR4/mT3/6Ew899BAtW7YkLCyM/fv3M3fuXEpLS2uc5/79+7nzzjvp1asXjz/+OM2bN8dkMrF161aWL1/u0zyg4jqn6OhoTCYTqampJCQkOMcFYnnFx8e7fG02mwF8qm/o0KHYbDbeeustpk6dit1up3PnzkybNq1eGlAhgkEaGyGakHbt2jnvivKkf//+rF69mq+//tqnUz6Oi4Tnz5/P/Pnz3cavW7fOa2MTHx/PN998g6ZpLr+ss7OzsVqtLr/UA+mjjz5Cp9PRs2dPALZu3UpxcTFLlixxXnMD8P333/s8z82bN2M0GklPT3e51X3r1q1+1dahQ4dqj4IEa3lVduONN3LjjTdSXFzMnj17WLJkCRMnTuSDDz5wWXZCNFVyV5QQIeS2224jMjLSeaFuVZqmOW/3/umnn9i3bx9XXXUVr7zyitt/ffr0Ydu2beTm5lb7/fr06UNxcbHbL3/Hc3Lq4/qN9evX89///pdrr73Wea2No0lwHL2Aiqxvvvmm2+fNZrPHoxs6nQ6DweBy6q20tJR33303YLU31PKqLmNlkZGRDBgwgEmTJmGxWOQCYhEy5IiNECGkdevWPPPMM9x3333ccMMN3Hrrrc6LiX/66SfWr1+PpmlcccUVzouGx40bx4UXXug2r6KiInbt2sW7777Lbbfd5vH7DR06lDVr1jBjxgxOnjxJ+/bt+fLLL0lPT2fAgAFccskltc5SWlrK119/7fz38ePH2bZtGx9//DG9evVizpw5zmkvueQSTCYT999/P+PGjaO8vJy1a9e6nF5zaN++Pf/5z3947bXX6NSpEzqdjs6dOzNgwABefvllHnjgAW6++Wby8vJ46aWXXJqluqrP5VVZdRkfeeQRwsPD6datGykpKWRmZrJixQpiYmK8HgkUoimRxkaIEPPnP/+ZjRs3smrVKl5//XVOnTqFXq+nVatW9O/fn1tvvRWLxcI777zD+eef77GpARgwYADNmzdn3bp11TY2YWFhvPLKKyxatIgXX3yR3NxcUlNT+etf/1rr26Mdjh8/zs033wxUHF1ISkqiY8eOPPvss26vVGjXrh1Llixh8eLFTJkyhfj4eIYMGcLtt9/O+PHjXeY7duxYjhw5wqJFiygoKEDTNA4fPkyfPn2YP38+K1euZNKkSaSmpjJixAgSExN5+OGH65TFoT6XV2XVZezRowcbNmzg/fffJz8/n4SEBLp3787ChQvlImIRMnRaXZ48JYQQQgjRiMg1NkIIIYQIGdLYCCGEECJkSGMjhBBCiJAhjY0QQgghQoY0NkIIIYQIGcrf7t2jRw/Ky8tJSUkJdilCCCGE8CAzMxOz2czevXtrnFb5xqasrAybzRbsMoQQQghRDavV6vHlt54o39g4XqK3bdu2IFcihBBCCE8GDRrk87RyjU0Is9vtFBcXY7fbg11Kg2tq2WfOnEmnTp3o2rWr8799+/bVal5NLXsgSXb1squaG9TO7o00NiHMZrORl5en5Km2pph91KhR7Nu3z/lf165dazWfppg9UCS7etlVzQ1qZ/dGGhshhBBChAxpbIRoJN555x169erFtddey6pVq+TwshBC1ILyFw8L0RiMGTOG6dOnExcXx4EDB5g2bRp6vZ7bb7892KUJIUSTIkdsQphOp8NsNqPT6YJdSoNratkvuOACEhMTMRgMdOnShfHjx/Pee+/Val5NLXsgSXb1squaG9TO7o0csQlhRqOR5OTkYJcRFE09u16vB02D09uh5BREpEFKf9AbavxsU89eF5Jdveyq5ga1s3sjR2yEaATee+89CgsL0TSNAwcOsHLZYq5stRe2/Rl23lLx/3fbwPENwS5VCCEaNWlsQpjFYiEjIwOLxRLsUhpcU8u+Zs0aLrvsMrp168aD0yYxqvOP/LXHL64TFZ+ET2+qsblpatkDSbKrl13V3KB2dm/kVJQQjcCaNWsq/mG3VRyZKc7xMJUG6ODLadDyBp9OSwkhhGrkiI0QjUnmp1B8wssEGhQfr5hOCCGEG2lshGhMSk4FdjohhFCMNDZCNCYRaYGdTgghFCONTQgzGo00a9YMo1G9S6mabPaU/hDZCqjuuRQ6iGxdMV01mmz2AJDs6mVXNTeond0baWxCmE6nw2g0KvnwpiabXW+A7s/+74uqtf/v6+6LvV443GSzB4BkVy+7qrlB7ezeSGMTwqxWK7m5uVit1mCX0uCadPbWw6D/Oohs6To8slXF8NbDvH68SWevI8muXnZVc0PjyL5t2zZuuOEGunTpQr9+/Vi7dm3QanGQ41chTNM0SkpKiI6ODnYpDa7JZ289rOKW7sxP/X7ycJPPXgeSXb3squaG4Gf/73//y5w5c3jqqafo0aMHhYWFZGVlBaWWyqSxEaKx0hsg9bJgVyGEEB49++yz3H333fTu3RuAuLg44uLiglyVnIoSQgghhJ+Ki4v57rvvKCwsZPDgwfTt25dp06aRmZkZ7NKksRFCCCGEf86cOYOmabzzzju89NJL/Oc//8FoNDJ9+vRglyaNTSjT6/XExMRUvClaMZJdsqtG1eyq5obgZo+MjARgzJgxtGzZkqioKKZOncquXbsoLi5u8Hoqk2tsQpjBYCAmJibYZQSFZJfsqlE1u6q5IbjZY2NjadGiheut5nYbANqxNyGxjc83PASaei2uQux2O6Wlpdjt9mCX0uAku2RXjarZVc0Nwc8+YsQIVq9ezenTpyn98Q2en3U5ff5USNS+O2Dbnyte6Ht8Q4PXJY1NCLPZbOTk5GCz2YJdSoOT7JJdNapmVzU3BD/7hAkT6NOnD9cPGcyAmx6mpLSEf/zltz8mKD4Jn97U4M2NnIoSQgghhN8MBgMzp/+Nmec9D8UnPEyhATr4clrFc7ka6LSUHLERQgghRO1kflpNU+OgQfHxiukaiDQ2QgghhKidklOBnS4ApLEJcSq/9VWyq0myq0fV3NAIskekBXa6AFB3a1CAyWSiWbNmwS4jKCS7ZFeNqtlVzQ2NJHtK/4oX9BafpOKamqp0FeNT+jdYSU36iE16ejodOnTgiSeecA7TNI0lS5bQr18/LrzwQsaMGcORI0eCWKUQQggRovQG6P7s/77QVRn5v6+7L27Q59k02cZm//79vPHGG3To0MFl+MqVK3n55Zd59NFHWbduHcnJydxxxx0UFhYGqdLgsVgs/Pbbb1gslmCX0uAku2RXjarZVc0NjSh762HQfx1EtnQdHtmqYnjrYQ1aTpNsbIqKivjb3/7GvHnzXN4kqmkar7zyCpMmTeLKK6+kffv2LFy4kNLSUjZt2hTEioNHxYdWOUh2NUl29aiaGxpR9tbD4PpfYNDHcMlrFf+//ucGb2qgiV5jM3fuXAYMGMAll1zCsmXLnMNPnDhBZmYm/fr1cw4zm8307NmTffv2MXLkSI/zO3XqFIMGDar2+23ZssVtmMlkAsBqtaJprucVDQYDer0em83mttHpdDqMRiOapmG1Wt3mazQa0el0Huer1+sxGAzY7XaPD2Ry1OTo3i0WCzabDYvFEtD5Biprfc3XcTGd3W53m7e3rI75VldTMJahY77+LEPHenfMK9DboaNeb8uwIbZvT/O12WzObT4Q8w3E9t1Q+wiLxeKcV12WYVPcR1Rd5/W1HTa2fUTldR6M/azHrIl9XfcRVb5vXdaNr5pcY7N582YOHjzIunXr3MY5XpeelJTkMjw5OZmMjAyv8/W0ATteLObpNewtWrQAIC8vj/Lycpdx8fHxREZGUlpaSn5+vsu4sLAwkpKS0DTN43xTU1MxGAycOXOG0tJSl3GxsbFER0dTVlZGbm6uyziTyURKSgoAWVlZaJqGzWZzvoysefPmmEwmCgsL3V5QFh0dTWxsLBaLhezsbJdxBoOB1NRUAI9PuExKSiIsLIyioiK3032RkZHEx8djtVrdsup0OtLS0pzLsOoPXUJCAhEREZSUlHDmzBmXceHh4SQmJmK32z0uw+bNmwNQWlpKdnY2BsMf53bj4uKIioqitLSUvLw8l8+ZzWaSk5MBz+u8WbNmGI1GCgoKKCkpcRkXExNDTEwM5eXl5OTkuIwzGo3OC/yys7PddiDJycmYzWYKCwspKipyGRcVFUVcXFyNyzA3N9e5k3Cs9/LycsLCwiguLqagoMDlszUtw8rrxtv27e8y9LZ9+7oMHdt3ZSkpKZhMJoqKipzbt2O9V16GWVlZLp/T6/XO7aXyMnRITEwkPDzc4zKMiIggISEBm83WKPYRjl/wgM/7iMocy7Cp7SNKS0vd1rkv+widTkd+fj5lZWUu45rSPqJyQ+fPPsLB2/bd2PYRNpvNZV/uTZNqbE6dOsUTTzzBqlWrCAsLq3Y6l5dyQY1dX1pamttRmZq6Sof4+HiPf41BxYZhNps91qbT6Zw7mcoczVRsbKzby80c48LCwjx+1sGx0Tg2+KSkJOdfGdHR0URFRXmcb+UdnyeJiYluwyr/8oiIiHAZ58hqNBq9zjc+Pr7a+UZERLita8d89Xq9x/k6xjt+MB1/sTg+4xhX9bOVtxtP83XUFBMTQ3R0tMs4x3zNZrPXrFWb7srzjY6Odr4xt+p8a1qGCQkJzn871rtj24uMjCQ8PNxl+pqWoUNN27e/y9CX7bumZejYvitzbN9RUVEUFxeTlJTkXO+1WYYOjqzelqHBYKjTMgzUPsJisTh/Yfq6j6isqe4jwsPDiYyMdFnnvu4j4uLiPB5Zccy3se8jKq/z+tq+G8s+wtemBppYY/Pdd9+RnZ3NsGF/nLOz2Wzs2bOHNWvWOJuTrKwsl1vgsrOzPf4gV1b5l19lOp2u2nHg/RkCBoOh2pVRl/nq9Xqvr6l3zNfxl5Tj0F6g5utJXbLWx3wNBoPzrydPmeqStbEvw8rrva7zrW3Whti+PTGbzc7sVecRrHXTUPsIxzbvOA0Qqtt3Vd7WebC2w4ZahpXXeTD2sxCcfURNmlRjc/HFF7Nx40aXYbNmzaJt27aMHz+e1q1bk5KSwo4dO+jYsSMA5eXl7NmzhwcffDAYJQeVXq93+2tQFZJdsqtG1eyq5ga1s3vTpBqb6Oho2rdv7zLMcY7WMXzs2LGkp6fTpk0bzj77bNLT0wkPD2fIkCHBKDmobDYbhYWFREdH+3UYLxRIdsku2dWgam5QO7s3Taqx8cX48eMpKytjzpw55Ofnc9FFF7Fq1Sq3850qsNvtFBUVERkZqdxGL9klu2RXg6q5Qe3s3jT5xmb16tUuX+t0OqZMmcKUKVOCVJEQQgghgqVJPqBPCCGEEMITaWyEEEIIETKksQlher2eqKgor7cWhirJLtlVo2p2VXOD2tm9afLX2IjqGQwGl3dpqUSyS3bVqJpd1dygdnZvpM0LYZqmYbFYav2+jaZMskt21aiaXdXcoHZ2b6SxCWGOd4d4eyVEqJLskl01qmZXNTeond0baWyEEEIIETKksRFCCCFEyJDGRgghhBAhQxqbEFf5FfGqkexqkuzqUTU3qJ29Oj43NuXl5WzcuJEVK1bw8ccfe5zm+PHjzJo1K2DFiboxmUykpaV5fTV8qJLskl01qmZXNTeond0bn55jU1BQwC233MKRI0eAig6xc+fOLFq0iJYtWzqny8nJ4e2332bBggX1U60QQgghhBc+HbFZsWIFv//+O88++ywff/wxTzzxBBkZGdx8883OZkc0PhaLhd9//x2LxRLsUhqcZJfsqlE1u6q5Qe3s3vjU2Gzbto3Jkydz1VVXkZaWxrBhw1i/fj2JiYmMHTuW77//vr7rFLWk8vMNJLuaJLt6VM0Namevjk+NTUZGBhdccIHLsNTUVFavXk1aWhq33XYbBw8erJcChRBCCCF85VNjExcXR25ursfh//znP2nZsiV33HEHBw4cCHiBQgghhBC+8qmxOffcc9m5c6fHcbGxsc7mZv78+QEtTgghhBDCHz41Nn379mXjxo3k5+d7HO9objp27BjQ4kTdGAwGEhMTMRgMwS6lwUl2ya4aVbOrmhvUzu6NTvPhtaB2u53S0lLCw8PR66vvhcrKysjKynK5BbyxGzRoEFBxgbQQQgghGh9/flf7dMRGr9cTGRnptakBCAsLa1JNTaiz2WwUFBRgs9mCXUqDk+ySXTWqZlc1N6id3Rt5pUIIs9vtFBQUYLfbg11Kg5Pskl01qmZXNTeond0baWyEEEIIETKksRFCCCFEyJDGRgghhBAhQxqbEKbT6QgPD1fytfaSXbKrRtXsquYGtbN743djc/7557N//36P47799lvOP//8OhclAsNoNJKYmIjR6NNL3EOKZJfsqlE1u6q5Qe3s3vjd2Hh77I3dbpfOsRHRNA2bzeZ1nYUqyS7ZVaNqdlVzg9rZvQnoqajvvvuOmJiYQM5S1IHVauX06dNKvv1Vskt21aiaXdXcoHZ2b3w6fvWvf/2LV155Bag4p3f33XdjNptdpikrKyM7O5urrroq8FUKIYQQQvjAp8YmKSmJc889F4CTJ0/SunVrYmNjXaYxm820b9+esWPHBr5KIYQQQggf+NTYDBkyhCFDhgAwZswYHnvsMdq1a1evhQkhhBBC+MvvS6lXr15dH3WIKrp27erydXl5OW3btmXjxo1BqkgIIYRo/Gp1j5imaRw4cICTJ09SVlbmNn7o0KF1rUt5+/btc/n6uuuu49prr/VrHkajkbS0tECW1WRIdsmuGlWzq5ob1M7ujd+Nzc8//8zkyZM5duyYx1vMdDqdNDYBtn//fn766Sf+8pe/+PU5lW+9l+xqkuzqUTU3qJ3dG78bm7lz51JeXs6iRYvo0KGD291RIvDWrVvHpZdeSmpqql+fs1qt5OXlER8fr9wDnCS7ZJfsalA1N6id3Ru/l8T+/ft5/PHHGTx4cH3UI6ooKSlh8+bNLFy40O/PappGeXm5kg9vkuySXTWqZlc1N6id3Ru/H9AXGRlJdHR0fdQiPHj//feJiIjgsssuC3YpQgghRKPnd2MzbNgwNm3aVB+1CA/eeusthg4dWnGY0W6D09vhl7UV/7fbgl2eEEII0aj4fSqqffv2bN68mUmTJjFw4EDi4+PdprnyyisDUZvyjh49yr59+5g/fz4c3wBf3gvFJ/6YILIVdH8WWg8LXpFCCCFEI+J3Y/PAAw8AcOLECbZv3+42XqfTcejQoToXJiouGu7Rowd/Mu6DT28CqpxHLT5ZMbz/Oo/NjcFgID4+HoPB0DAFNyKSXbKrRtXsquYGtbN743dj43hnlKh/06dPrzjd9G4b3Joa+N8wHXw5DVreAHrXjVuv1xMZGVn/hTZCkl2yq0bV7KrmBrWze+N3Y9OrV6/6qENUJ/NT19NPbjQoPl4xXeplLmPsdjulpaWEh4ej1wf0Re6NnmSX7JJdDarmBrWze1PrJVFQUMCnn37Ku+++S35+fiBrEpWVnKr1dDabjby8PGw29S4yluySXTWqZlc1N6id3ZtaPdHn+eefZ+XKlZSWlqLT6Vi3bh1xcXHcdttt9O3blwkTJgS6TnVF+Pi4bF+nE0IIIUKY30ds1qxZw/PPP89NN91Eenq6y4OB/vznP3u8oFjUQUr/irufqO7R2TqIbF0xnRBCCKE4v4/YrFmzhttvv53p06e7Hf46++yzOXbsWMCKE1RcENz92f/dFaXD9SLi/zU73Re7XTgshBBCqMjvIzbHjx+nf3/PRweioqI4c+ZMnYuqTnp6OjfeeCNdu3alT58+3HXXXRw9etRlGk3TWLJkCf369ePCCy9kzJgxHDlypN5qahCth1Xc0h3Z0nV4ZKtqb/WGilvvzWazki9Kk+ySXTWqZlc1N6id3Ru/j9jExMSQlZXlcdzJkydJSkqqc1HV+eKLLxg9ejSdO3fGZrOxaNEi7rzzTjZv3uy85W3lypW8/PLLPPnkk7Rp04Zly5Zxxx13sGXLlqb9KojWwypu6c78tOJC4Yi0itNPXo7UGI1GkpOTG7DIxkOyS3bVqJpd1dygdnZv/G5s+vTpw4svvsigQYMICwsDKrpGq9XK2rVr6devX8CLdHjppZdcvl6wYAF9+vThu+++o2fPnmiaxiuvvMKkSZOcTz9euHAhl1xyCZs2bWLkyJEe53vq1CkGDRpU7ffdsmWL2zCTyQRUvF216gvIDAYDer0em82G3W53GafT6TAajWiahtVqdZuv0Wh0Ls+q89Xr9RhSL8Nut1ecBrTZK/6rUpPFYgFw+bzJZPI+X4Phj/lWk9Ux30Blra/5Ot5y68ha+a8Zb1kd862uphrXTT0sQ8d8/VmGjtoMBgMGgyHg26GjXm/LsNbbdy2XYeX5OrI61nuwt++G2kdomuayvdRlGTalfYSndV5f22Fj20dUXudAg+9nG3of4Su/G5upU6dy0003ce2113L55Zej0+l49dVXOXToEBkZGSxevLhWhdRGQUEBAHFxcUDF05AzMzNdmiuz2UzPnj3Zt29ftY0N4HHhO54LkJmZ6TZ9ixYtAMjLy6O8vNxlXHx8PJGRkZSWlrrdCh8WFkZSUhKapnmcb2pqKgaDgTNnzlBaWuoyLjY2lujoaMrKysjNzXUZZzKZSElJASArK8u5YysuLiYyMpLmzZtjMpkoLCykuLjY5bPR0dHExsZisVjIzs52GWcwGEhNTQUgJyfHbTklJSURFhZGUVERhYWFLuMiIyOJj4/HarW6ZdXpdKSlpTmXYdUfuoSEBCIiIigpKXE7vRkeHk5iYiJ2u93jMmzevDk2m41jx44RFhbm8lTOuLg4oqKiKC0tJS8vz+VzZrPZ+dePp/k2a9YMo9FIQUEBJSUlLuNiYmKIiYmhvLycnJwcl3FGo5FmzZoBkJ2d7bYDSU5Oxmw2U1hYSFFRkcu4qKgo4uLialyGubm5zp2EY723aNGCmJgYiouLnT8rvi7DyuvG2/bt7zL0tn37ugwd23dlKSkpmEwm8vPzycrKIjIy0rneKy/Dqkeb9Xo9zZs3d1uGDomJiYSHh3tchhERESQkJGCz2RrFPsJms2Gz2WjZsiVWq9WnfYSnZdjU9hGFhYWcPn3aZZ37so/Q6XTk5+dTVlbmMq4p7SNsNhsWi4XWrVt7rKm6fYSDt+27se0jbDabz09Y1mm1aIl+/PFHFixYwO7du7FarRgMBnr37s3DDz9Mu3bt/J1drWiaxuTJkzlz5gyvvfYaAF999RWjRo3iv//9r/MHDWD27NlkZGS4HfEBnEdqqh6VaWx/jYH/Hb9jJ5SUlEREREST/GustsvQarVy+vRpEhMTnd+npqyN7a+xyvP1Zxk61nuzZs0ICwtrMn+NBeKITWlpKZmZmSQlJTmnD/b23VD7CIvFQk5OjvMXgypHbDytc1WO2FRe5479XiDmWzlrY9lHDB48GIBt27a5fcZtHjVO4cE555zDSy+9RHl5Obm5ucTFxREeHl6bWdXa3Llz+eGHH5xNTWVVL6TypXer/Muv6ryqGwd/nPbwxHEqINDz1ev1Xp8yWXm+BoPBeRoqkPOtqi5Z62u+er0ek8nkcZq6ZG0Ky9CxY6nrfGubtaG276ocWT2t92Ctm4bcRzjyhfr27Wm+ntZ5sLbDhlyGjnkFaz8bjH1ETer0DGaz2UxqamqDNzWPP/44H330Ef/617+ch5EBl8OslWVnZ8sFVkIIIYQCatUSnThxgvfff5+MjAy38+Q6nY758+cHpLiqNE3j8ccf58MPP2T16tXO84oOrVq1IiUlhR07dtCxY0cAysvL2bNnDw8++GC91CSEEEKIxsPvxmb79u3cc8892O12EhMTMZvNLuPr8376OXPmsGnTJl544QWioqKcFx7FxMQQHh6OTqdj7NixpKen06ZNG84++2zS09MJDw9nyJAh9VZXY2U0GklNTVXy5WiSXbKrRtXsquYGtbN743djs2jRIrp168aiRYvq9Zk1nqxduxaAMWPGuAxfsGABw4ZVPKRu/PjxlJWVMWfOHPLz87noootYtWpV036GTS3pdDqfryIPNZJdsqtG1eyq5ga1s3vjd2Nz7NgxlixZ0uBNDcDhw4drnEan0zFlyhSmTJnSABU1blarlTNnzhAbG1unC7GaIsku2SW7GlTNDWpn98bv41ctWrRwe8aBaJw0TaO0tLTWDzlqyiS7ZFeNqtlVzQ1qZ/fG78Zm4sSJrFq1yu3hQ0IIIYQQweb3sasDBw6QnZ3NFVdcQe/evUlISHCb5pFHHglIcUIIIYQQ/vC7sXn11Ved/968ebPbeJ1OJ42NEEIIIYLC78bm+++/r486RD3Q6/XExMQoeSugZJfsqlE1u6q5Qe3s3shl1CHMYDAQExMT7DKCQrJLdtWoml3V3KB2dm9q3djs2rWLXbt2kZeXR0JCAhdffDF9+vQJZG2ijux2O+Xl5ZjNZuU6esku2SW7GlTNDWpn98bvxqa8vJypU6fyySefoGma842iK1asYMCAASxZssTri61Ew7HZbOTk5JCSkqLcRi/ZJbtkV4OquUHt7N74vSSef/55PvvsMx544AF27tzJt99+y86dO3nwwQf57LPPeP755+ujTiGEEEKIGvnd2GzevJmJEycybtw4EhMTAUhMTOTOO+9k4sSJbNy4MeBFCiGEEEL4wu/G5rfffqNHjx4ex/Xo0YPTp0/XuSghhBBCiNrwu7FJTEys9p1Nhw8fdh7FEY2Dyu8PkexqkuzqUTU3qJ29On43NgMHDuS5557jP//5j8vwrVu3snTpUgYNGhSw4kTdmEwmmjVrpuTF3JJdsqtG1eyq5ga1s3vjd6t333338dVXX3HvvfcSERFBSkoKWVlZFBcX0759e+677776qFMIIYQQokZ+NzZxcXGsW7eODRs2sHv3bvLy8ujYsSN9+vRh6NChmM3m+qhT1ILFYiErK4vk5GTlOnrJLtkluxpUzQ1qZ/emVifnzGYzI0eOZOTIkYGuRwSYyq+zl+xqkuzqUTU3qJ29OrW+6ui3335jz549zicP9+jRg+bNmweyNiGEEEIIv/jd2NjtdubPn8/atWux2WzO4QaDgZEjR/Lwww/LExCFEEIIERR+NzZLlizh1VdfZcSIEQwZMoTk5GSysrLYuHEja9asITY2lnvvvbc+ahVCCCGE8Mrvxmb9+vWMHTuWhx56yDmsbdu29OrVi/DwcNavXy+NTSNhNBpJSUlR8jkHkl2yq0bV7KrmBrWze+P3OaP8/Hwuu+wyj+Muu+wy8vPz61qTCBCdTofJZEKn0wW7lAYn2SW7alTNrmpuUDu7N343Nueddx4///yzx3G//PIL5557bp2LEoFhs9nIz893uRZKFZJdsqtG1eyq5ga1s3vjd2Pzt7/9jRUrVrB9+3aX4R999BErVqxg5syZgapN1JHdbqeoqAi73R7sUhqcZJfsqlE1u6q5Qe3s3vh9Ym7OnDmUlZUxefJkoqKiSEpKIjs7m6KiIuLj45kzZ45zWp1Ox7vvvhvQgoUQQgghquN3YxMfH098fLzLsGbNmgWqHiGEEEKIWvO7sVm9enV91CGEEEIIUWfyJL0QptfriYqKUvKBiZJdsqtG1eyq5ga1s3tT65vfjxw5QkZGBmVlZW7jrrzyyjoVJQLDYDAQFxcX7DKCQrJLdtWoml3V3KB2dm/8bmx+/fVXpk6dyuHDhwH3F3DpdDoOHToUmOpEndjtdqxWK0ajUbmOXrJLdsmuBlVzg9rZvfG7sZk9ezZZWVnMmjWLdu3ayavSGzGbzUZWVhYpKSnKbfSSXbJLdjWomhvUzu6N343N/v37mTdvHtdee2191COEEEIIUWt+t3iJiYlER0fXRy1CCCGEEHXid2MzatQo3nrrrfqoRQghhBCiTvw+FTVu3DiefPJJhg0bRv/+/d0e1qfT6bj99tsDVJ6oK5XPu0p2NUl29aiaG9TOXh2dVvW2php88803TJgwodq3eDe1u6IGDRoEwLZt24JciRBCCCE88ed3td9HbObOnUtCQgLz58+Xu6KEEEII0aj43dj8+OOPPPPMM87uSTReFouF3NxcEhISlGtAJbtkl+xqUDU3qJ3dG79PzqWlpbk9lE80XlarNdglBI1kV5NkV4+quUHt7NXxu7GZMGECq1at8vgqBSGEEEKIYPL7VNTBgwc5ffo0l19+Ob1793a7KwrgkUceCURtQgghhBB+8buxefXVV53/3rRpk9t4nU4njY0QQgghgsLvxub777+vjzpEPTAYDCQmJmIwGIJdSoOT7JJdNapmVzU3qJ3dG78bG9F06PV6wsPDg11GUEh2ya4aVbOrmhvUzu5NrRubXbt2sWvXLvLy8khISODiiy+mT58+gaxN1JHNZqO4uJjIyEjlOvqaspeXlzN37lx27txJbm4uqampjBs3jptuuikI1QaWrHfJrlJ2VXOD2tm98buxKS8vZ+rUqXzyySdomobRaMRqtbJixQoGDBjAkiVL5H76RsJut1NQUEB4eLhyG31N2a1WKykpKfzzn/+kdevWfPPNN4wfP57mzZvTr1+/IFQcOLLeJbtK2VXNDWpn98bv272ff/55PvvsMx544AF27tzJt99+y86dO3nwwQf57LPPeP755+ujTr+tWbOGgQMH0rlzZ4YNG8bevXuDXZJoRCIjI7n33ns566yz0Ol0dOnShd69e/Pll18GuzQhhBB14Hdjs3nzZiZOnMi4ceNITEwEIDExkTvvvJOJEyeycePGgBfpr/fee48FCxYwefJk3n77bbp378748ePJyMgIdmmikSorK2P//v106NAh2KUIIYSoA78bm99++40ePXp4HNejRw9Onz5d56Lq6uWXX+bGG29k+PDhtGvXjocffpjmzZuzdu3aYJcmGiFN03j44Yc5++yzufLKK4NdjhBCiDrw+xqbxMREDh8+7PFC4cOHDzuP4gRLeXk53333HRMmTHAZ3rdvX/bt2+fxM6dOnfL67qstW7a4DXNcR2S1Wt1eMWEwGNDr9dhs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sIFZs2Z5HHf48GHnv0MxO1Q8oO+ll17i999/p3379syaNYuePXsGu6yAqvrsFocFCxY4T9NomsbSpUt54403yM/P56KLLuLRRx91awyaujFjxnDeeefx8MMPA6Gd++OPP+aZZ57hl19+oVWrVtxxxx2MGDHCOT6Us9eGNDZCCCGECBlyu7cQQgghQoY0NkIIIYQIGdLYCCGEECJkSGMjhBBCiJAhjY0QQgghQoY0NkIIIYQIGdLYCCGEECJkSGMjhBBCiJAhjY0Q9WzDhg106NDB+V/Hjh259NJLmTVrFqdPn/ZpHjNnzmTgwIH1VuPu3bvp0KEDu3fvrrfvAf7n+Oijj5g0aRKXXHIJnTp1olevXtx22228++67WCyWeqxUDRs3buSf//ynz9Pv3buXhx9+mGHDhtGpUyc6dOjAiRMn6q9AIWpB3hUlRANZsGABbdu2pbS0lL1795Kens4XX3zBxo0bne+0qs5dd93F2LFj6622Cy64gDfeeINzzjmn3r6HPzRN46GHHmLDhg0MGDCAmTNnkpaWRkFBAbt372bOnDnk5uZy2223BbvUJm3Tpk0cOXKE22+/3afpP//8c3bt2sX5559PVFQUX3zxRf0WKEQtSGMjRAM599xz6dy5M1DxtmKbzcYLL7zA1q1buf766z1+pqSkhIiICM4666x6rS06OpouXbrU6/fwx4svvsiGDRuYMmUK99xzj8u4gQMHMm7cOI4dOxak6tR11113OdfHSy+9JI2NaJTkVJQQQeJoJDIyMoCK0zRdu3bl8OHD/PWvf6Vr167Ov6Q9ncLp0KEDc+fO5e233+bqq6/moosu4vrrr+fjjz92+14//fQT999/v/OUzmWXXcb06dMpLy8HPJ+KctRz5MgRbrvtNrp06cLFF1/M3LlzKSkpcZn/mjVrGD16NH369KFLly5cd911rFy5slaniywWCy+++CJt27bl7rvv9jhNSkoKPXr0cH6dl5fHY489Rv/+/enUqRODBg1i0aJFznxVl9n69eu56qqruPDCCxk2bBhff/01mqbx4osvMnDgQLp27crYsWPdmqcxY8YwZMgQ9u7dy4gRI7jwwgvp378/ixcvxmazuUzrb02+rMdffvmFBx54gD59+tCpUyeuvvpq1qxZ4zKNY11u2rSJRYsW0a9fP7p168btt9/O0aNHXbJs376dkydP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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from sklearn.decomposition import PCA\n", - "\n", - "# make sure your system has required font\n", - "plt.rcParams['font.family'] = 'DejaVu Sans' # Also try Arial\n", - "\n", - "# perform PCA\n", - "pca = PCA(n_components=2) # Reduce to 2 components for visualization\n", - "pca_result = pca.fit_transform(df)\n", - "\n", - "# plot PCA scatterplot\n", - "plt.figure(figsize=(6, 4))\n", - "plt.scatter(pca_result[:, 0], pca_result[:, 1], color='orange')\n", - "for i, (x, y) in enumerate(pca_result):\n", - " plt.text(x, y, str(i), fontsize=9, ha='right', va='bottom') # Add point labels\n", - "plt.xlabel(\"Principal Component 1\", fontsize=12)\n", - "plt.ylabel(\"Principal Component 2\", fontsize=12)\n", - "plt.title(\"PCA of Data Points\", fontsize=12)\n", - "plt.tick_params(axis='both', which='major', labelsize=10)\n", - "plt.grid(True, linestyle='--', alpha=0.5)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAACSkAAAkqCAYAAADAIQkOAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9e7hWdZ0//j/vzZnNUUQERc1DoCGKiEkeSlCz1MQsSgNirKF0qGmsPpGNlpqHmUnTwEPTQc1SwQqxMgVEjb6KpxQC0aQkHEQwhS3HvWFz//7opxMDCLjvxYbN43FdXOVar/V6P+/y4q/ntVapXC6XAwAAAAAAAAAAUJCqxg4AAAAAAAAAAAA0bUpKAAAAAAAAAABAoZSUAAAAAAAAAACAQikpAQAAAAAAAAAAhVJSAgAAAAAAAAAACqWkBAAAAAAAAAAAFEpJCQAAAAAAAAAAKJSSUoGGDRuWYcOGNXYMAAAAAAAAAABoVM0bO0BTtmjRosaOAAAAAAAAAAAAja7BJaXBgwdXIsdGunTpkgkTJhSyGwAAAAAAAAAA2H4aXFJauHBhSqVSyuVyJfK8pba2tqL7AAAAAAAAAACAxlGRz721a9cuF154YSVWpVwuV2wXAAAAAAAAAADQ+CpSUmrVqlXOPPPMSqxKEiUlAAAAAAAAAABoQqoaOwAAAAAAAAAAANC0NfhNSuPGjUurVq0qkaXQnQAAAAAAAAAAQOMolcvlcmOHaKoGDx6cJHnggQcaOQkAAAAAAAAAADQen3sDAAAAAAAAAAAKpaQEAAAAAAAAAAAUqvn2OKS2tjY1NTVZt27d28716NFje8QBAAAAAAAAAAC2o8JKSqtXr84Pf/jD/PrXv86CBQu2OF8qlfLss88WFQcAAAAAAAAAAGgkhZSU3njjjXzqU5/KvHnzUi6Xt+qZrZ0DAAAAAAAAAAB2LoWUlG644Ya88MILad68eYYPH57Bgwdnjz32SLNmzYo4rslZv76cqqpSY8fYrvzmXcOu+JuTXfd3AwAAAAAAAMCbCikpTZ06NaVSKRdeeGHOOeecIo5o0qqqShk/5U95demqxo6yXbx7n845+eh9/eYmblf8zUnStXPbfOKkdzd2DAAAAAAAAABoVIWUlBYvXpyqqqqcddZZRazfJby6dFVe/tvKxo6xXXTt1CaJ39zU7Yq/GQAAAAAAAAD4u0JKSh07dkxdXV1atWpVxHoAAAAAAAAAAGAnUlXE0v79+2f58uVZvHhxEesBAAAAAAAAAICdSCElpX/+539Os2bNcv311xexHgAAAAAAAAAA2IkUUlLq06dPrrrqqtx999258MIL89JLLxVxDAAAAAAAAAAAsBNoXsTSwYMHJ0maNWuWiRMnZuLEienYsWOqq6s3+0ypVMrUqVOLiAMAAAAAAAAAADSiQkpKCxcu3OjasmXLsmzZss0+UyqViogCAAAAAAAAAAA0skJKSldeeWURawEAAAAAAAAAgJ1QISWlM888s4i1AAAAAAAAAADATqiqsQMAAAAAAAAAAABNm5ISAAAAAAAAAABQKCUlAAAAAAAAAACgUM0buuDggw9Okuy///75zW9+s8G1bVEqlfLss882NA4AAAAAAAAAALCDaXBJqVwub/Cf//e/AwAAAAAAAAAAu7YGl5R+8pOfJElat2690TUAAAAAAAAAAIAGl5SOOuqorboGAAAAAAAAAADsmqoaOwAAAAAAAAAAANC0KSkBAAAAAAAAAACFavDn3rakvr4+8+fPzxtvvJF169a97eyAAQOKjgMAAAAAAAAAAGxnhZWUXnnllVxzzTWZMmVK1qxZs8X5UqmUZ599tkFnzpgxIzfffHNmzpyZVatWpUePHjnllFMyatSotG3bdpt2jR07NuPGjXvbmW9961s5++yzGxIZAAAAAAAAAACavEJKSi+99FLOPvvsvPbaaymXy1v1zNbObc5tt92Wyy+/POVyOXvuuWe6d++eefPm5cYbb8zkyZNz++23p1OnTtu8t0uXLtl33303ea9r164NygwAAAAAAAAAALuCQkpK11xzTf72t79lt912y5e//OUce+yx2X333dOsWbMijsvs2bNzxRVXJEkuvfTSDB06NKVSKYsXL855552XOXPm5KKLLsrYsWO3effxxx+fq666qtKRAQAAAAAAAABgl1FVxNJHHnkkpVIp11xzTc4666x069atsIJSktxwww1Zv359zjjjjHziE59IqVRKknTr1i3XXHNNqqqqMnny5Dz33HOFZQAAAAAAAAAAADatkJJSXV1dWrdunaOPPrqI9RtYuXJlpk+fniQZOnToRvf322+/t3Lcd999hecBAAAAAAAAAAA2VMjn3vbee++89NJLRazeyNy5c1NXV5eWLVumb9++m5zp379/HnnkkcycOXOb9z/33HP58pe/nFdffTXV1dXp1atXTj311Bx00EENjQ4AAAAAAAAAALuEQkpKH/rQhzJ27Ng8+uijGThwYBFHvOXFF19MkvTo0SMtWrTY5Mw+++yzwey2mDt3bubOnfvWP0+bNi033XRTRowYka997Wtb/IzdokWLMnjw4M3ef+CBB7Y5EwAAAAAAAAAA7EwK+dzbueeem969e+eiiy4q/I1KNTU1SZKOHTtudubNe2/Obo3dd989n/3sZzNhwoQ8+uij+eMf/5h77rknn/zkJ1Mul3PrrbfmmmuuaVh4AAAAAAAAAADYBRTyJqXWrVvn5ptvzkUXXZSPfOQj+eAHP5hDDz001dXVb/vckCFDtvms2traJNnsW5SSpGXLlhvMbo2zzz57o2u9evXKJZdckr333jvf+c53csstt+Tss8/O3nvvvdk93bt397YkAAAAAAAAAAB2aYWUlJJk4cKF+dvf/pbVq1dn0qRJmTRp0tvOl0qld1RSatWqVZJk7dq1m52pq6vbYLahzj333PzkJz/JkiVL8uCDD2b48OEV2QsAAAAAAAAAAE1RISWl5557LsOHD8/q1auT/P0tR507d06zZs0qftbWfMptaz4Jty2aNWuWww47LFOmTMn8+fMrshMAAAAAAAAAAJqqQkpKY8eOzapVq9KzZ89cdtllOeqoo1JVVVXEUdlvv/2SJC+//HLWrl27yc++LViwYIPZSnjznHXr1lVsJwAAAAAAAAAANEWFNIeefvrplEqlfPe7383RRx9dWEEpSQ455JC0aNEidXV1mTVr1iZnnnrqqSTJ4YcfXrFzX3jhhSTJnnvuWbGdAAAAAAAAAADQFBXSHlq9enXatGmTPn36FLF+A9XV1Tn22GOTJBMmTNjo/vz58zNjxowkySmnnFKRMx966KG3SkrHHHNMRXYCAAAAAAAAAEBTVUhJad999826detSX19fxPqNnH/++SmVSpk0aVLGjx+fcrmcJFmyZEkuuOCCrF+/PieeeGJ69+69wXNnn312Bg0alFtuuWWD6y+88EIuvvjiPPfccxtcX79+fX7961/ny1/+cpLkAx/4QPr27VvcDwMAAAAAAAAAgCageRFLhwwZkquuuirTpk3LSSedVMQRG+jbt2/GjBmTq666KhdffHFuvPHGdO7cOfPmzUtdXV3e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s+dGMz179swVV1yRT3/607nxxhvzwAMPpG3bthk9enRGjhyZcrmciRMnFhEPAAAAAAAAAADYjgopKf30pz9NqVTK+eefv8XZ8847L8nf3770puHDhydJZs+eXUQ8AAAAAAAAAABgOyqkpPSnP/0p7du3T6dOnbY426lTp3To0CFz585961qPHj1SXV2dpUuXFhEPAAAAAAAAAADYjgopKdXV1WXlypVZvXr1FmdXrVqVFStWpK6ubqN7rVu3LiIeAAAAAAAAAACwHRVSUtp3331TX1+fO+64Y4uzd955Z+rr67Pvvvu+dW358uVZuXJldttttyLiAQAAAAAAAAAA21EhJaUhQ4akXC7n6quvzve///2sWbNmo5k1a9bkv//7v3P11VenVCrljDPOeOveM888kyQ56KCDiogHAAAAAAAAAABsR82LWDps2LBMnTo1Tz75ZK699trcdNNN6dOnT/bYY4+USqUsWbIks2fPzurVq1Mul3PkkUdm+PDhbz0/ceLEJMn73ve+IuIBAAAAAAAAAADbUSElpebNm+cHP/hBrrzyyvz85z/P6tWr88QTT6RUKiVJyuVykqSqqiof//jHM2bMmDRv/r9RLrvsslxyySVp27btNp07Y8aM3HzzzZk5c2ZWrVqVHj165JRTTsmoUaO2edemPPzwwxk1alSSZK+99sq0adMavBMAAAAAAAAAAJq6QkpKSdKmTZtceumlOe+883L//ffn2Wefzeuvv55yuZwuXbrkkEMOycknn5wePXps9Gx1dfU2n3fbbbfl8ssvT7lczp577pnu3btn3rx5ufHGGzN58uTcfvvt6dSp0zv+PStWrMg3v/nNd/w8AAAAAAAAAADsqgopKT3xxBNJkl69eqV79+4ZOXJkEce8Zfbs2bniiiuSJJdeemmGDh2aUqmUxYsX57zzzsucOXNy0UUXZezYse/4jO985ztZtGhRTjzxxEydOrVS0QEAAAAAAAAAoMmrKmLp8OHD8+lPf/qtz7oV7YYbbsj69etzxhln5BOf+MRbn5Xr1q1brrnmmlRVVWXy5Ml57rnn3tH+J598MnfeeWdOOumkDB48uJLRAQAAAAAAAACgySukpNS+ffu0b98+HTt2LGL9BlauXJnp06cnSYYOHbrR/f322y9HH310kuS+++7b5v21tbX593//97Rt2zYXXXRRw8ICAAAAAAAAAMAuqJCS0j777JOVK1emrq6uiPUbmDt3burq6tKyZcv07dt3kzP9+/dPksycOXOb919//fV58cUXc8EFF6Rbt24NygoAAAAAAAAAALui5kUsPfXUUzNnzpzce++9GTJkSBFHvOXFF19MkvTo0SMtWrTY5Mw+++yzwezWmjt3bn70ox+lb9++Oeecc95RvkWLFr3tJ+IeeOCBd7QXAAAAAAAAAAB2FoW8SWnEiBE5/PDDc9lll+Xhhx8u4oi31NTUJMnbflruzXtvzm6N+vr6fOMb30iSXHbZZamqKuR/KgAAAAAAAAAAaPIKeZPSTTfdlAEDBuRPf/pTPv/5z+fAAw/MEUcckS5durxt2Wf06NHbfFZtbW2SbPYtSknSsmXLDWa3xo9+9KPMmTMnn/3sZ9O7d+9tzvWm7t27e1sSAAAAAAAAAAC7tEJKSuPGjUupVEq5XE6SvPDCC5k3b94Wn3snJaVWrVolSdauXbvZmbq6ug1mt2T+/PkZN25c9t5773eUCQAAAAAAAAAA+F+FlJQGDBhQxNpN2ppPuW3NJ+H+0Te/+c3U1tbmW9/6Vtq0adPwkAAAAAAAAAAAsAsrpKR02223FbF2k/bbb78kycsvv5y1a9du8rNvCxYs2GB2S+bMmZNSqZQxY8ZsdG/NmjVJkkWLFuWYY45JkowdOzZHHHHEO0gPAAAAAAAAAABNXyElpe3pkEMOSYsWLVJXV5dZs2alf//+G8089dRTSZLDDz98q/eWy+X87W9/2+z99evXv3X/7T41BwAAAAAAAAAAu7qdvqRUXV2dY489Ng8++GAmTJiwUUlp/vz5mTFjRpLklFNO2aqdTz755Gbv/fKXv8zXv/717LXXXpk2bdo7Dw4AAAAAAAAAALuIwktKdXV1eeSRRzJ79uy89tprKZVK2W233XLooYdm4MCBadmyZYPPOP/88/PQQw9l0qRJOeKIIzJ06NCUSqUsWbIkF1xwQdavX58TTzwxvXv33uC5s88+O4sXL86IESMycuTIBucAAAAAAAAAAAA2VmhJafz48bnuuuuydOnSTd7v3LlzvvSlL2Xo0KENOqdv374ZM2ZMrrrqqlx88cW58cYb07lz58ybNy91dXV517velcsuu2yj5xYvXpyFCxdm+fLlDTofAAAAAAAAAADYvMJKSv/1X/+VH//4xymXy0mSbt26Zc8990ySvPLKK1m8eHFef/31fPOb38yCBQvyla98pUHnjRw5Mr169cqPf/zjzJo1K6+99lp69OiRU045JaNGjUp1dXWDfxMAAAAAAAAAALDtCikpPf744/nRj36UJDn55JPzr//6rznggAM2mPnLX/6S6667Lvfff39+9KMf5QMf+ECOPPLIBp07cODADBw4cKvnp02bts1nfPSjH81HP/rRbX4OAAAAAAAAAAB2VVVFLP3Zz36WJPnYxz6W733vexsVlJJk//33z3XXXZePfexjKZfL+elPf1pEFAAAAAAAAAAAoJEVUlJ6+umnU1VVlX/7t3/b4uyXvvSllEql/OEPfygiCgAAAAAAAAAA0MgKKSktXbo07du3T5cuXbY4u/vuu6dDhw5ZunRpEVEAAAAAAAAAAIBGVkhJqbq6OitXrkxtbe0WZ9esWZOVK1embdu2RUQBAAAAAAAAAAAaWSElpV69eqW+vj4///nPtzj7i1/8IuvWrUvv3r2LiAIAAAAAAAAAADSyQkpKp59+esrlcv7jP/4jd91112bn7rrrrlx11VUplUo544wziogCAAAAAAAAAAA0suZFLP3oRz+aSZMm5YknnsjFF1+cG264Ie9973vTrVu3JMkrr7ySxx57LIsXL065XM5RRx2VM888s4goAAAAAAAAAABAIyukpFRVVZUbb7wxF154YSZPnpxFixZl0qRJG8yUy+Ukycknn5wrrrgipVKpiCgAAAAAAAAAAEAjK6SklCTt2rXL9773vcyaNSv33ntvZs+enddeey1J0qVLl/Tp0ycf/vCH07dv36IiAAAAAAAAAAAAO4DCSkpv6tu3ryISAAAAAAAAAADswqoaOwAAAAAAAAAAANC0KSkBAAAAAAAAAACFKvRzbytWrMhDDz2U559/PjU1NVm7du1mZ0ulUq644ooi4wAAAAAAAAAAAI2gsJLSL3/5y1x++eVZtWrVW9fK5fJGc6VSKeVyWUkJAAAAAAAAAACaqEJKStOnT883vvGNlMvltGrVKocffnj22GOPNG9e6IubAAAAAAAAAACAHVAhraEf/vCHKZfLOfzww3PDDTdkt912K+IYAAAAAAAAAABgJ1BVxNI5c+akVCrlqquuUlACAAAAAAAAAIBdXCElpfr6+rRt2zb77bdfEesBAAAAAAAAAICdSCElpZ49e6auri719fVFrAcAAAAAAAAAAHYihZSUPvKRj2TdunX53e9+V8R6AAAAAAAAAABgJ1JISenTn/50Dj300FxyySWZP39+EUcAAAAAAAAAAAA7ieZFLP3Nb36TM844I9/73vdyxhln5IMf/GAOO+ywVFdXv+1zQ4YMKSIOAAAAAAAAAADQiAopKY0ZMyalUilJUi6X86tf/Sq/+tWv3vaZUqmkpAQAAAAAAAAAAE1QISWlHj16FLEWAAAAAAAAAADYCRVSUpo2bVoRawEAAAAAAAAAgJ1QVWMHAAAAAAAAAAAAmjYlJQAAAAAAAAAAoFBKSgAAAAAAAAAAQKGaV2LJ3XffXYk1GTJkSEX2AAAAAAAAAAAAO46KlJTGjBmTUqnUoB2lUklJCQAAAAAAAAAAmqCKlJSSpFwuV2oVAAAAAAAAAADQhFSkpPTAAw9UYg0AAAAAAAAAANAEVaSktNdee1ViDQAAAAAAAAAA0ARVNXYAAAAAAAAAAACgaVNSAgAAAAAAAAAACtXgktLixYsrkWMjS5YsKWQvAAAAAAAAAACwfTW4pHTSSSfl29/+dsXKSvfdd19OP/30TJgwoSL7AAAAAAAAAACAxtW8oQv22GOP/PSnP81dd92VQYMG5fTTT8/xxx+f5s23fvWCBQtyzz335Fe/+lUWLFiQUqmUvffeu6HRAAAAAAAAAACAHUCDS0q//e1vc9ttt+Wmm27Kb3/729x3333p0KFD+vbtm759+6Z3797p3LlzOnXqlJYtW6ampiY1NTV56aWXMmvWrMyaNSt/+ctfkiTlcjnHHHNMvva1r+Xd7353g38cAAAAAAAAAADQ+BpcUmrRokXOPffcnHXWWRk/fnzuuuuuvPTSS5k+fXp+//vfb/H5crmcFi1a5MQTT8ynPvWpHHnkkQ2NBAAAAAAAAAAA7EAaXFJ6U8eOHTNq1KiMGjUqjzzySKZPn54nnngic+fOTX19/Ubzu+++ewYMGJCjjjoqH/zgB7PbbrtVKgoAAAAAAAAAALADqVhJ6R+9733vy/ve974kydq1a/P666/n9ddfT21tbTp16pTddtstHTp0KOJoAAAAAAAAAABgB1NISekftWjRIt26dUu3bt2KPgoAAAAAAAAAANgBVTV2AAAAAAAAAAAAoGlTUgIAAAAAAAAAAAqlpAQAAAAAAAAAABRKSQkAAAAAAAAAACiUkhIAAAAAAAAAAFAoJSUAAAAAAAAAAKBQSkoAAAAAAAAAAEChlJQAAAAAAAAAAIBCKSkBAAAAAAAAAACFUlICAAAAAAAAAAAK1byhC+6+++4KxPi7IUOGVGwXAAAAAAAAAACwY2hwSWnMmDEplUoNDlIqlZSUAAAAAAAAAACgCWpwSalHjx6bvbd06dKsXr367wc1b55OnTolSZYtW5Z169YlSdq0aZPOnTs3NAYAAAAAAAAAALCDanBJadq0aZu8fscdd+Tyyy9P//79c/7552fAgAFp2bJlkqSuri5PPPFEbrzxxsycOTP//M//nLPPPruhUQAAAAAAAAAAgB1QVRFLH3300Vx22WUZNGhQbrvtthxzzDFvFZSSpGXLljnmmGNy22235YQTTshll12Wxx57rIgoAAAAAAAAAABAIyukpHTzzTenXC7n61//eqqqNn9EqVTKmDFjsn79+vz4xz8uIgoAAAAAAAAAANDICikpzZ49Ox06dEj37t23ONujR4906NAhf/zjH4uIAgAAAAAAAAAANLLmRSxduXJl6uvrU1dXt8Fn3jalrq4uq1atSrNmzYqIAgAAAAAAAAAANLJC3qS09957p76+PpMmTdri7KRJk7Ju3brsvffeRUQBAAAAAAAAAAAaWSElpVNPPTXlcjnf/va3M3HixM3O3X333fn2t7+dUqmUU089tYgoAAAAAAAAAABAIyvkc2+f+cxnMmXKlMydOzcXXnhhxo4dm6OOOirdunVLkixevDiPP/54Fi1alHK5nIMPPjif+cxniogCAAAAAAAAAAA0skJKSq1atcott9ySb3zjG5k6dWpefvnljT79Vi6XkySDBg3KFVdckVatWhURBQAAAAAAAAAAaGSFlJSSpGPHjhk3blxmzZqVe++9N7Nnz85rr72WJOnSpUv69OmTD3/4w+nbt29REQAAAAAAAAAAgB1AYSWlN/Xt21cRCQAAAAAAAAAAdmFVjR0AAAAAAAAAAABo2gp/k9L69esze/bsvPzyy1mzZk2GDBlS9JEAAAAAAAAAAMAOpNCS0m233ZYbb7wxS5cufevaP5aUampq8qlPfSrr1q3LnXfemU6dOhUZBwAAAAAAAAAAaASFfe7t0ksvzRVXXJHXX3891dXVKZVKG8107Ngx73nPe/LXv/41Dz30UFFRAAAAAAAAAACARlRISWn69Om5/fbb07Zt24wbNy5PPvlkdtttt03OnnbaaSmXy5k2bVoRUQAAAAAAAAAAgEZWSEnpzjvvTKlUyhe/+MWceOKJbzvbr1+/JMncuXOLiAIAAAAAAAAAADSyQkpKM2fOTJKcddZZW5xt165d2rVrl1dffbWIKAAAAAAAAAAAQCMrpKS0bNmytG/fPu3atdu6EFVVqa+vLyIKAAAAAAAAAADQyAopKbVr1y4rVqzI2rVrtzj7+uuv54033kjnzp2LiAIAAAAAAAAAADSyQkpKBx10UMrlcv74xz9ucfaee+5JkrznPe8pIgoAAAAAAAAAANDICikpnXTSSSmXyxk3blzWr1+/2bk//OEPue6661IqlfKhD32oiCgAAAAAAAAAAEAja17E0k9+8pP52c9+lkcffTSf/exnM3LkyLfKSq+//nqef/753HvvvZk4cWLWrVuXgw8+OKeddlqDz50xY0ZuvvnmzJw5M6tWrUqPHj1yyimnZNSoUWnbtu027Zo2bVqmT5+eOXPm5JVXXsnSpUvTvHnz7LXXXhk4cGBGjhyZvfbaq8GZAQAAAAAAAACgqSukpNSyZct8//vfz7nnnptHHnkkjz766Fv3jjnmmLf+e7lczj777JNx48alqqphL3W67bbbcvnll6dcLmfPPfdM9+7dM2/evNx4442ZPHlybr/99nTq1Gmr99188815/PHH06JFi3Tt2jXvfve7s3Tp0vz5z3/OCy+8kLvuuivjxo3Lscce26DcAAAAAAAAAADQ1BXyubck2W+//TJp0qT80z/9Uzp06JByubzBn+rq6nz605/OXXfd1eA3Es2ePTtXXHFFkuTSSy/NQw89lIkTJ2bq1Kl5z3vekz//+c+56KKLtmnnWWedlVtuuSV/+MMf8uCDD+YXv/hFpk2blvvvvz8DBgzI6tWr85WvfCWrVq1qUHYAAAAAAAAAAGjqCnmT0pvat2+fr33ta/na176WefPmZcmSJamvr0/Xrl1z0EEHpVmzZhU554Ybbsj69eszZMiQfOITn3jrerdu3XLNNdfkQx/6UCZPnpznnnsuvXv33qqdQ4YM2eT1ffbZJ9dee22OOeaYLF26NE888UTe//73V+JnAAAAAAAAAABAk1RoSekfHXjggTnwwAMrvnflypWZPn16kmTo0KEb3d9vv/1y9NFH55FHHsl999231SWlt7P77runU6dOWbZsWdasWdPgfQAAAAAAAAAA0JQV9rm37WXu3Lmpq6tLy5Yt07dv303O9O/fP0kyc+bMipz55z//OcuWLUtVVVUOOeSQiuwEAAAAAAAAAICmqtA3Ka1evTp33XVXfv/73+fll1/OmjVrMnXq1LfuL1++PA899FBKpVJOO+20d3TGiy++mCTp0aNHWrRoscmZffbZZ4PZd6JcLuf111/PU089le985ztJknPPPTc9e/Z82+cWLVqUwYMHb/b+Aw888I4zAQAAAAAAAADAzqCwktLcuXNz/vnn55VXXkm5XE6SlEqlDWbatWuXm266KX/5y1/Ss2fPHHbYYdt8Tk1NTZKkY8eOm515896bs9ti0qRJ+X//7/9tcG3//ffPd77znZx++unbvA8AAAAAAAAAAHY1hZSUli5dmlGjRuXVV1/Ne97znpx22mm5/vrrs3Llyg3mSqVSPvaxj+U//uM/MnXq1HdUUqqtrU2Szb5FKUlatmy5wey26NKlS4444oiUy+W88sorWbx4cebPn59f/epXGTBgQPbcc8+3fb579+7elgQAAAAAAAAAwC6tqoilt956a1599dUMHDgwd911V/7pn/4prVu33uTs+9///iTJY4899o7OatWqVZJk7dq1m52pq6vbYHZbHHvssbnjjjty55135qGHHsrkyZMzaNCgPPzwwxk6dGiWL1/+jnIDAAAAAAAAAMCuopCS0oMPPphSqZSvfvWrqap6+yP233//NG/ePH/961/f0Vlb8ym3rfkk3Nbq2bNnvve97+Wggw7K4sWL89Of/rTBOwEAAAAAAAAAoCkrpKS0YMGCtGjRIgcffPBWzbdr126jT8Ftrf322y9J8vLLL2/2bUoLFizYYLahmjVrluOOOy5JMnv27IrsBAAAAAAAAACApqqQklK5XE6zZs1SKpW2OLt+/fqsXLkybdq0eUdnHXLIIWnRokXq6uoya9asTc489dRTSZLDDz/8HZ2xKevWrUvy9/wAAAAAAAAAAMDmFVJS6tq1a9asWZPXXntti7PPPPNM1q5dm7322usdnVVdXZ1jjz02STJhwoSN7s+fPz8zZsxIkpxyyinv6Iz/q66uLg899FCSv5ekAAAAAAAAAACAzSukpHTkkUcmSSZOnPi2c+vXr891112XUqmUgQMHvuPzzj///JRKpUyaNCnjx49PuVxOkixZsiQXXHBB1q9fnxNPPDG9e/fe4Lmzzz47gwYNyi233LLB9T/+8Y+59tprM3/+/I3OevHFF3PeeedlwYIFadu2bYYOHfqOcwMAAAAAAAAAwK6geRFLR4wYkbvvvjs33XRTDj300Lz3ve/daGbRokW55JJL8thjj6V58+Y555xz3vF5ffv2zZgxY3LVVVfl4osvzo033pjOnTtn3rx5qaury7ve9a5cdtllGz23ePHiLFy4MMuXL9/g+qpVq3LjjTfmxhtvzG677Zbu3bunefPmefXVV/Pyyy8nSTp16pRrr7023bp1e8e5AQAAAAAAAABgV1BISenggw/OeeedlxtuuCH/9E//lD59+mTFihVJkksuuSR/+tOfMnPmzNTX1ydJLrjggvTs2bNBZ44cOTK9evXKj3/848yaNSuvvfZaevTokVNOOSWjRo1KdXX1Vu/q3bt3/v3f/z2PP/54/vSnP+Wvf/1r1qxZk3bt2qV///457rjj8olPfCK77bZbgzIDAAAAAAAAAMCuoJCSUpJ88YtfTIcOHXLddddl1qxZb12/88473/ocW+vWrfPlL385w4cPr8iZAwcO3KbPxk2bNm2T1zt27Jjhw4dXLBcAAAAAAAAAAOzKCispJX9/u9EZZ5yRX/3qV3nqqaeyZMmSrF+/PrvvvnuOOOKInHHGGdl9992LjAAAAAAAAAAAADSyQktKSdK5c+eMGDEiI0aMKPooAAAAAAAAAABgB1TV2AEAAAAAAAAAAICmrfA3KSXJihUr8uyzz+a1115LknTp0iWHHHJI2rVrtz2OBwAAAAAAAAAAGlGhJaW5c+fmuuuuy/Tp07N+/foN7lVVVeW4447LF7/4xRxyyCFFxgAAAAAAAAAAABpRYZ97+8UvfpGPf/zjefjhh1NfX59yubzBn/r6+jz00EMZOnRofv7znxcVAwAAAAAAAAAAaGSFvElp1qxZueiii7J+/frsu++++exnP5ujjz463bp1S5IsXrw4M2bMyM0335y//OUvufjii/Pud787ffv2LSIOAAAAAAAAAADQiAp5k9L3v//9rF+/PkcddVTuueeefPzjH0/Pnj3TsmXLtGzZMj179szHP/7x3H333Xnve9+b9evX5/vf/34RUQAAAAAAAAAAgEZWSEnpqaeeSqlUyre+9a20atVqs3MtW7bM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cumll2bo0KEplUpZvHhxzjvvvMyZMycXXXRRxo4du827jz/++Fx11VWVjgwAAAAAAAAAALuMBpeUDj744CTJ/vvvn9/85jcbXNsWpVIpzz777DvKcMMNN2T9+vUZMmRIPvGJT7x1vVu3brnmmmvyoQ99KJMnT85zzz2X3r17v6MzAAAAAAAAAACAd6aqoQvK5fJbfzZ1bVv+vBMrV67M9OnTkyRDhw7d6P5+++2Xo48+Okly3333vaMzAAAAAAAAAACAd67Bb1L6yU9+kiRp3br1Rte2h7lz56auri4tW7ZM3759NznTv3//PPLII5k5c+Y273/uuefy5S9/Oa+++mqqq6vTq1evnHrqqTnooIMaGh0AAAAAAAAAAHYJDS4pHXXUUVt1rSgvvvhikqRHjx5p0aLFJmf22WefDWa3xdy5czN37ty3/nnatGm56aabMmLEiHzta19Ls2bN3vb5RYsWZfDgwZu9/8ADD2xzJoB/5O8ZAAAAAAAAAHZ0DS4pNbaampokSceOHTc78+a9N2e3xu67757PfvazOfnkk9OzZ8+0a9cuL774Ym6//fbceeedufXWW9OiRYt89atfbdgPAAAAAAAAAACAJm6nLynV1tYmyWbfopQkLVu23GB2a5x99tkbXevVq1cuueSS7L333vnOd76TW265JWeffXb23nvvze7p3r27t5gAhfL3DAAAAAAAAAA7usJLSvX19Zk/f37eeOONrFu37m1nBwwYsM37W7VqlSRZu3btZmfq6uo2mG2oc889Nz/5yU+yZMmSPPjggxk+fHhF9gIAAAAAAAAAQFNUWEnplVdeyTXXXJMpU6ZkzZo1W5wvlUp59tlnt/mcrfmU29Z8Em5bNGvWLIcddlimTJmS+fPnV2QnAAAAAAAAAAA0VYWUlF566aWcffbZee2111Iul7fqma2d+7/222+/JMnLL7+ctWvXbvKzbwsWLNhgthLePGdLb4cCAAAAAAAAAIBdXSElpWuuuSZ/+9vfsttuu+XLX/5yjj322Oy+++5p1qxZxc865JBD0qJFi9TV1WXWrFnp37//RjNPPfVUkuTwww+v2LkvvPBCkmTPPfes2E4AAAAAAAAAAGiKqopY+sgjj6RUKuWaa67JWWedlW7duhVSUEqS6urqHHvssUmSCRMmbHR//vz5mTFjRpLklFNOqciZDz300FslpWOOOaYiO/lfy1fV5X+WLM/zf309/7NkeZavqmvsSAAAAAAAAAAANEAhJaW6urq0bt06Rx99dBHrN3L++eenVCpl0qRJGT9+/FufjluyZEkuuOCCrF+/PieeeGJ69+69wXNnn312Bg0alFtuuWWD6y+88EIuvvjiPPfccxtcX79+fX7961/ny1/+cpLkAx/4QPr27VvcD9sFvbpsdf7rp0/mvP+Ylq98b3rO+49p+a+fPplXl61u7GgAAAAAAAAAALxDhXzube+9985LL71UxOpN6tu3b8aMGZOrrroqF198cW688cZ07tw58+bNS11dXd71rnflsssu2+i5xYsXZ+HChVm+fPkG19etW5fx48dn/Pjx6dSpU3r06JFmzZplwYIFqampSZIceeSR+a//+q/t8vt2FctX1WXshKfz9POvbnD96edfzdgJT+erw45M+7YtGykdAAAAAAAAAADvVCFvUvrQhz6U2traPProo0Ws36SRI0fm5ptvzvHHH5/Vq1dn3rx56dGjRz7/+c/nF7/4RXbbbbet3rXXXnvlS1/6Uj7wgQ+kffv2+etf/5rnnnsuLVq0yPHHH5///M//zE9+8pN06NChwF+066lZUbtRQelNTz//ampW1G7nRAAAAAAAAAAAVEIhb1I699xzM2XKlFx00UW5+eab07NnzyKO2cjAgQMzcODArZ6fNm3aJq936NAh5513XqVisZVWrl7boPsAAAAAAAAAAOyYCikptW7dOjfffHMuuuiifOQjH8kHP/jBHHrooamurn7b54YMGVJEHHYS1W1aNOg+AAAAAAAAAAA7pkJKSkmycOHC/O1vf8vq1aszadKkTJo06W3nS6WSktIurmO7VunXq+smP/nWr1fXdGzXqhFSAQAAAAAAAADQUIWUlJ577rkMHz48q1evTpK0aNEinTt3TrNmzYo4jiaifduW+cLQfhk74ekNikr9enXNF4f2S/u2LRsxHQAAAAAAAAAA71QhJaWxY8dm1apV6dmzZy677LIcddRRqaqqKuIompiundrkq8OOTM2K2qxcvTbVbVqkY7tWCkoAAAAAAAAAADuxQkpKTz/9dEqlUr773e+mT58+RRxBE9a+bUulJAAAAAAAAACAJqSQ1xutXr06bdq0UVACAAAAAAAAAACKKSntu+++WbduXerr64tYDwAAAAAAAAAA7EQK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AAAAGkFhJaVSqfSOnu3YsWNGjhyZUaNGparKi54AAAAAAAAAAGBnV0hJacCAAUmS559/Pm+88UaSpHv37unWrVvK5XKWLFmSRYsWJfl7Kend73533njjjfzlL3/JsmXLct111+W5557Ltddeu03nzpgxIzfffHNmzpyZVatWpUePHjnllFMyatSotG3btsG/6+GHH86oUaOSJHvttVemTZvW4J0AAAAAAAAAANDUFfKqottuuy39+vXLG2+8kTPPPDNTpkzJgw8+mDvvvDPjx4/Pgw8+mClTpuSjH/1oampqMmDAgEyaNCmPPfZYRo8enSS5//77M2XKlG06c+TIkXnooYfSqlWrHHDAAVm4cGFuvPHGfOxjH8uyZcsa9JtWrFiRb37zmw3aAQAAAAAAAAAAu6JCSkr3339/fvCDH2TEiBG58sor07Nnz41mevbsmSuuuCKf/vSnc+ONN+aBBx5I27ZtM3r06IwcOTLlcjkTJ07cqvNmz56dK664Ikly6aWX5qGHHsrEiRMzderUvOc978mf//znXHTRRQ36Td/5zneyaNGinHjiiQ3aAwAAAAAAAAAAu5pCSko//elPUyqVcv75529x9rzzzkvy9zchvWn48OFJ/l4+2ho33HBD1q9fnzPOOCOf+MQnUiqVkiTdunXLNddck6qqqkyePDnPPffctv6UJMmTTz6ZO++8MyeddFIGDx78jnYAAAAAAAAAAMCuqpCS0p/+9Ke0b98+nTp12uJsp06d0qFDh8ydO/etaz169Eh1dXWWLl26xedXrlyZ6dOnJ0mGDh260f399tsvRx99dJLkvvvu28pf8L9qa2vz7//+72nbtm2D38YEAAAAAAAAAAC7okJKSnV1dVm5cmVWr169xdlVq1ZlxYoVqaur2+he69att/j83LlzU1dXl5YtW6Zv376bnOnfv3+SZObMmVvc939df/31efHFF3PBBRekW7du2/w8AAAAAAAAAADs6poXsXTffffN888/nzvuuCPnnnvu287eeeedqa+vz7777vvWteXLl2flypUbXNucF198Mcnf377UokWLTc7ss88+G8xurblz5+ZHP/pR+vbtm3POOWebnn3TokWL3vYTcQ888MA72gvwJn/PAAAAAAAAALCjK+RNSkOGDEm5XM7VV1+d73//+1mzZs1GM2vWrMl///d/5+qrr06pVMoZZ5zx1r1nnnkmSXLQQQdt8ayampokSceOHTc78+a9N2e3Rn19fb7xjW8kSS677LJUVRXyPxUAAAAAAAAAADR5hbxJadiwYZk6dWqefPLJXHvttbnpppvSp0+f7LHHHimVSlmyZElmz56d1atXp1wu58gjj8zw4cPfen7ixIlJkve9731bPKu2tjZJNvsWpSRp2bLlBrNb40c/+lHmzJmTz372s+ndu/dWP/d/de/e3VtMgEL5ewYAAAAAAACAHV0hJaXmzZvnBz/4Qa688sr8/Oc/z+rVq/PEE0+kVColScrlcpKkqqoqH//4xzNmzJg0b/6/US677LJccskladu27RbPatWqVZJk7dq1m52pq6vbYHZL5s+fn3HjxmXvvffO6NGjt+oZAAAAAAAAAABg0wopKSVJmzZtcumll+a8887L/fffn2effTavv/56yuVyunTpkkMOOSQnn3xyevTosdGz1dXVW33O1nzKbWs+CfePvvnNb6a2tjbf+ta30qZNm63OAgAAAAAAAAAAbKyQktITTzyRJOnVq1e6d++ekSNHFnFMkmS//fZLkrz88stZu3btJj/7tmDBgg1mt2TOnDkplUoZM2bMRvfWrFmTJFm0aFGOOeaYJMnYsWNzxBFHvIP0AAAAAAAAAADQ9BVSUho+fHiqqqry6KOPFrF+A4ccckhatGiRurq6zJo1K/37999o5qmnnkqSHH744Vu9t1wu529/+9tm769fv/6t+2/3qTkAAAAAAAAAANjVFVJSat++faqqqrb682oNUV1dnWOPPTYPPvhgJkyYsFFJaf78+ZkxY0aS5JRTTtmqnU8++eRm7/3yl7/M17/+9ey1116ZNm3aOw8OAAAAAAAAAAC7iKoilu6zzz5ZuXJl6urqili/kfPPPz+lUimTJk3K+PHjUy6XkyRLlizJBRdckPXr1+fEE09M7969N3ju7LPPzqBBg3LLLbdsl5wAAAAAAAAAALArKqSkdOqpp2bdunW59957i1i/kb59+2bMmDFJkosvvjgnnHBCzjzzzAwePDhz5szJu971rlx22WUbPbd48eIsXLgwy5cv3y45AQAAAAAAAABgV1RISWnEiBE5/PDDc9lll+Xhhx8u4oiNjBw5MjfffHOOP/74rF69OvPmzUuPHj3y+c9/Pr/4xS+y2267bZccAAAAAAAAAADAhpoXsfSmm27KgAED8qc//Smf//znc+CBB+aII45Ily5dUlW1+V7U6NGjG3TuwIEDM3DgwK2enzZt2jaf8dGPfjQf/ehHt/k5AAAAAAAAAADYVRVSUho3blxKpVLK5XKS5IUXXsi8efO2+FxDS0oAAAAAAAAAAMCOp5CS0oABA4pYCwAAAAAAAAAA7IQKKSnddtttRawFAAAAAAAAAAB2QlWNHQAAAAAAAAAAAGjaCnmTEjQ1y1fVpWZFbVauXpvqNi3SsV2rtG/bsrFjAQAAAAAAAADsFAorKb322mt5/PHH88ILL2TZsmVJkk6dOuXAAw/Me9/73nTp0qWoo6GiXl22OmMnPJ2nn3/1rWv9enXNF4b2S9dObRoxGQAAAAAAAADAzqHiJaUXX3wx1113XaZMmZL169dvcqaqqionnXRSvvjFL2b//fevdASomOWr6jYqKCXJ08+/mrETns5Xhx3pjUoAAAAAAAAAAFtQVcll9913X84666zcf//9qa+vT7lc3uSf+vr63H///TnrrLNy7733VjICVFTNitqNCkpvevr5V1OzonY7JwIAAAAAAAAA2PlU7E1KjzzySL7yla9k3bp1adWqVU499dScfPLJOfjgg9O5c+ckydKlS/Pss89m8uTJuffee7N69er8v//3/9KxY8ccc8wxlYoCFbNy9doG3QcAAAAAAAAAoEIlpdra2nz961/PunXr0rt371x33XXZd999N5rr1q1bunXrlhNOOCGf+9zn8q//+q95/vnnc+GFF2by5Mlp1apVJeJAxVS3adGg+wAAAAAAAAAAVOhzb7/85S+zePHi9OjRI7feeusmC0r/13777ZdbbrklPXr0yJIlSzJx4sRKRIGK6tiuVfr16rrJe/16dU3Hdop1AAAAAAAAAABbUpGS0kMPPZRSqZQvfvGL6dix41Y/17lz53zhC19IuVzOgw8+WIkoUFHt27bMF4b226io1K9X13xxaL+0b9uykZIBAAAAAAAAAOw8KvK5t+effz5JcuKJJ27zsyeddFK+/vWvv7UDdjRdO7XJV4cdmZoVtVm5em2q27RIx3atFJQAAAAAAAAAALZSRUpKr7/+etq3b5927dpt87Pt2rVLhw4dsnTp0kpEgUK0b9tSKQkAAAAAAAAA4B2qyOfeWrVqldra2nf8fG1tbVq2VAABAAAAAAAAAICmqCIlpa5du6auri5//vOft/nZP//5z6mtrU3Xrl0rEQUAAAAAAAAAANjBVKSkdOSRRyZJxo8fv83P3nnnnRvsAAAAAAAAAAAAmpaKlJROP/30lMvl3H777Xn44Ye3+rmHH344d9xxR0qlUk477bRKRAEAAAAAAAAAAHYwFSkpDRgwIMcff3zWrVuX0aNH54Ybbsjq1as3O79q1apcf/31GT16dOrr63PsscfmqKOOqkQUAAAAAAAAAABgB9O8Uov+8z//M0OHDs2CBQsyduzY/PjHP84xxxyTQw45JJ06dUqpVMrSpUszZ86c/H//3/+XVatWpVwuZ5999sl//ud/VioGAAAAAAAAAACwg6lYSalTp0654447csEFF+Sxxx7LihUrMnny5EyePHmj2XK5nCQ56qijcs0116Rz586VigEAAAAAAAAAAOxgKlZSSpIuXbrk1ltvzbRp03LHHXfkySef3Oizb23atMmRRx6Zs88+O4MGDark8QAAAAAAAAAAwA6ooiWlNw0aNCiDBg1KfX19Fi5cmGXLliX5+9uWevTokebNCzkWAAAAAAAAAADYARXaFmrWrFn22Wef7LPPPkUeAwAAAAAAAAAA7MCqGjsAAAAAAAAAAADQtDW4pLR48eJK5NjIkiVLCtkLAAAAAAAAAABsXw0uKZ100kn59re/XbGy0n333ZfTTz89EyZMqMg+AAAAAAAAAACgcTVv6II99tgjP/3pT3PXXXdl0KBBOf3003P88cenefOtX71gwYLcc889+dWvfpUFCxakVCpl7733bmg0AAAAAAAAAABgB9DgktJvf/vb3Hbbbbnpppvy29/+Nvfdd186dOiQvn37pm/fvundu3c6d+6cTp06pWXLlqmpqUlNTU1eeumlzJo1K7Nmzcpf/vKXJEm5XM4xxxyTr33ta3n3u9/d4B8HAAAAAAAAAAA0vgaXlFq0aJFzzz03Z511VsaPH5+77rorL730UqZPn57f//73W3y+XC6nRYsWOfHEE/OpT30qRx55ZEMjAQAAAAAAAAAAO5AGl5Te1LFjx4waNSqjRo3KI488kunTp+eJJ57I3LlzU19fv9H87rvvngEDBuSoo47KBz/4wey2226VigIAAAAAAAAAAOxAKlZS+kfve9/78r73vS9Jsnbt2rz++ut5/fXXU1tbm06dOmW33XZLhw4dijgaAAAA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6PCcEyZMyL777rtSQClJRo0ale9///tJknfffTfTp08vuW4AAAAAAAAAAOgLyhJSqqury5IlS/LOO++ste/TTz+dtra2jBgxoqS1Fi1alIcffjhJMnHixFXaP/ShD2WfffZJkkybNq2kNf7Wpptu2v7ouCVLlnTJnAAAAAAAAAAA0FuVJaQ0duzYJMmUKVM+sN/y5ctz5ZVXplAoZN999y1prZdeeimtra2prKzMLrvssto+e+yxR5LkmWeeKWmNv/WnP/0p8+fPT79+/bLjjjt2yZwAAAAAAAAAANBbDSjHpMcff3ymTp2aH/7wh9l5552z9957r9Jnzpw5+eY3v5nHH388AwYMyOc+97mS1vrLX/6SJNlyyy1TUVGx2j6jRo1aqW8pisVi5s2bl6eeeipXXHFFkuSkk07KVltt9YHj5syZk/Hjx6+x/YEHHii5JoDEdQYAAAAAAACADV9ZQkof+chHctppp+Waa67JiSeemDFjxmThwoVJkm9+85t5+eWX88wzz2TZsmVJkq9+9atrDfusSVNTU5KktrZ2jX1WtK3ouy7uuuuu/Ou//utKx7bZZptcccUV+fSnP73O8wEAAAAAAAAAQF9TlpBSkpx55pnZaKONcuWVV+bZZ59tP37rrbemWCwmSQYOHJizzjorxx13XMnrtLS0JMkad1FKksrKypX6rotNNtkku+++e4rFYt5888289dZbmTlzZn7+859nzz33zBZbbPGB44cPH24XE6CsXGcAAAAAAAAA2NCVLaSUJCeccEIOPfTQ/PznP89TTz2VuXPnZvny5dl0002z++6759BDD82mm27aqTWqqqqSJG1tbWvs09raulLfdbH//vtn//33b//5tddey+WXX577778/EydOzD333JMhQ4as87wAAAAAAAAAANBXlDWklCTDhg3L8ccfn+OPP74s83fkUW4deSRcR2211Va56qqrcuihh+aVV17JLbfcktNOO63T8wIAAAAAAAAAQG/Vr7sL6KwPfehDSZLZs2evcTelWbNmrdS3s/r375+PfvSjSZLnn3++S+YEAAAAAAAAAIDequw7KSXJwoUL8+KLL+add95JkmyyySbZcccdU1NT0+m5d9xxx1RUVKS1tTXPPvts9thjj1X6PPXUU0mS3XbbrdPrrbB06dIkyfLly7tsTgAAAAAAAAAA6I3KGlJ66aWXcuWVV+bhhx9eJczTr1+/fPSjH82ZZ56ZHXfcseQ1Bg8enP333z+/+c1vcvvtt68SUpo5c2Yee+yxJMlBBx1U8jrv19ramoceeihJOlU7AAAAAAAAAAD0BWV73Nsdd9yRI488Mv/93/+dZcuWpVgsrvRv2bJleeihhzJx4sT813/9V6fWOv3001MoFHLXXXfltttuS7FYTJLMnTs3X/3qV7N8+fJ84hOfyA477LDSuM9+9rMZN25cbrzxxpWOP/fcc/n+97+fmTNnrrLWX/7yl5x22mmZNWtWBg0alIkTJ3aqdgAAAAAAAAAA6O3KspPSs88+mwsvvDDLly/P1ltvnZNPPjn77LNPNt988yTJW2+9lcceeyw33HBD/vznP+cb3/hGtt9+++yyyy4lrbfLLrvk3HPPzeWXX55vfOMbufbaazNs2LD88Y9/TGtra/7u7/4u3/72t1cZ99Zbb+WNN97IggULVjre3Nyca6+9Ntdee2023njjDB8+PAMGDEhjY2Nmz56dJBk6dGi+//3vt58TAAAAAAAAAACwemUJKf3oRz/K8uXLs9dee+XHP/5xqqqqVmrfaqutstVWW+XQQw/NKaeckscffzw/+tGP8oMf/KDkNU844YSMHj06P/nJT/Lss8/mnXfeyZZbbpmDDjoop556agYPHtzhuXbYYYd8/etfzxNPPJGXX345r776apYsWZKamprsscce+ehHP5qjjjoqG2+8ccn1AgAAAAAAAABAX1GWkNJTTz2VQqGQiy++eJWA0vtVVlbmG9/4Rg4++OA89dRTnV533333zb777tvh/g8++OBqj9fW1ua4447Lcccd1+maAAAAAAAAAACgr+tXjkkXL16cmpqabLPNNmvtu+2222bIkCFZsmRJOUoBAAAAAAAAAAC6WVlCSsOHD09LS0uWL1++1r7Lli1LS0tLtthii3KUAgAAAAAAAAAAdLOyhJQOOOCAtLW15f77719r3/vvvz+tra058MADy1EKAAAAAAAAAADQzcoSUvrSl76UrbfeOhdeeGEef/zxNfabPn16vvGNb2SbbbbJqaeeWo5SAAAAAAAAAACAbjagHJPed999Ofroo/ODH/wgJ5xwQnbffffss88+2XzzzVMoFPLmm2/m8ccfz1NPPZUhQ4bkqKOOyn333bfauSZMmFCOEgEAAAAAAAAAgPWk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M27cuC5buFAodNlcAAAAAAAAAADAhqvDIaXu3kGpsbExv/vd7/Lcc8/l+eefz0svvZQlS5Zkp512yp133rnGcePGjcsbb7zxgXM/++yzqaqqWm3ba6+9lmuuuSaPPPJI5s2bl0022ST77bdfTjvttGy11VadOicAAAAAAAAAAOgLOhRSeuCBB8qyeP/+/Tvc95577slll11W8lrbb799ampqVtu2pl2dGhoactJJJ6W5uTm1tbXZfvvt89prr+WOO+7ItGnTcuONN2aXXXYpuSYAAAAAAAAAAOgLOhRSGjFiRLnrWKuampr8/d//fcaMGZMxY8Zk5syZ+e53v9vh8V//+tez9957d7j/4sWLM2nSpDQ3N+czn/lMLrroolRVVaWlpSUXX3xx7rzzzkyaNCm/+tWvMnDgwFJOCQAAAAAAAAAA+oQOP+6tux1xxBE54ogj2n/+oEe8dYXbbrstjY2N2XrrrXPxxRensrIySVJVVZVvfvObefLJJzNr1qz87Gc/y3HHHVfWWgAAAAAAAAAAoCfr190FbKimTZuWJDnssMPaA0orVFZW5vDDD0+S/PKXv1zvtQEAAAAAAAAAQE/SY3ZS6qxbb701P/nJT7JkyZJsuummGTt2bD796U+npqZmlb7Lli3L888/nyQZO3bsaudbcfy5557LsmXL0r9///IVDwAAAAAAAAAAPVifCSnde++9K/38i1/8IldeeWW+853vZL/99lup7Y033khbW1uSZNSoUaudb8Xx1tbWzJ49O1tttdVq+82ZMyfjx49fY10PPPBAh88BYHVcZ4D1wbUGAAAAAACAzuj1j3urr6/Pt7/97dx77715+umnM3369Pzwhz/MjjvumHfffTennXZaXnjhhZXGzJ8/v/310KFDVztvbW1t++umpqZylA4AAAAAAAAAAL1Cr99J6Tvf+c5KP1dXV+fjH/949t1333zuc5/LCy+8kCuuuCI33HBDe5/W1tb21xUVFaudt7Kysv31kiVL1rj+8OHD7SwAlJXrDLA+uNYAAAAAAADQGb1+J6U1GThwYL7yla8kSR577LG899577W3vDyCteOzb33p/kGngwIHlKRIAAAAAAAAAAHqBPhtSSpLdd989SbJ8+fLMmjWr/fj7H+X2/ke/vd/7H/H2/v4AKyxobs3rcxdkxqvz8vrcBVnQ3Lr2QX2Q9wkAAAAAAACg9+v1j3v7IO9/lNuyZcvaX48YMSIVFRVpa2vLrFmzsvnmm68ydkWoqbKyMltuuWX5iwV6lMb5izP59oY0zGhsP1Y/ui6TJtanbmh1N1a2YfE+AQAAAAAAAPQNfXonpZdffrn99fuDSAMGDMiYMWOSJE8++eRqx644vvPOO6d///5lrBLoaRY0t64SvEmShhmNmXx7g52C/i/vEwAAAAAAAEDf0emdlP7yl7/k6aefzty5czNv3ry0tLRk6NCh2XjjjbPttttm9913T3X1hrkbxnXXXZck2W677bLFFlus1HbggQemoaEhU6ZMycknn7zSrkutra258847kyQHHXTQ+isY6BGaFrasErxZoWFGY5oWtmTIoMr1XNWGx/sEAAAAAAAA0HeUFFJqaGjI7bffnocffjjvvPPOB/bt379/dtppp3z605/OoYcemiFDhpRUaCmuv/76VFZW5pBDDsmwYcPaj7/77rv53ve+l2nTpiVJJk2atMrYo446Ktdff31effXVXHTRRbnoootSVVWVlpaWXHzxxZk1a1Y222yzHHnkkevtfICeYdHitk619xXeJwAAAAAAAIC+Y51CSnfddVeuu+66/PGPf0yxWGw/PmjQoAwdOjRDhw5NVVVVmpqa0tTUlHfffTdLly7NM888k2effTbf+c53cvDBB+fLX/5yhg8fvk6FzpkzJxMmTGj/ubX1r48BmjFjRvbee+/24yeffHJOOeWUJMmbb76Z//iP/8gll1ySESNGZOONN86SJUvy5z//OUuXLk2/fv3y1a9+dbW7IQ0aNChXXnllTj755Nxxxx25//77M3LkyLz++utpamrKoEGDMnny5A12lyig+wyuruhUe1/hfQIAAAAAAADoOzoUUnr88cfz7//+73nppZdSLBZTW1ubAw88MGPHjs2uu+6arbfeerXjFi1alOeffz7PPPNMHnzwwTz99NP5r//6r/z85z/P8ccfny9+8YupqanpUKHLli3L/PnzVzm+dOnSlY4vWbKk/fXBBx+cYrGY5557LrNnz84f/vCH9O/fPyNHjsxee+2Vz33uc/nIRz6yxjX32GOP3HXXXbnmmmvyyCOP5OWXX86wYcNy+OGH5/TTT89WW23VodqBvqW2pir1o+tW+yiz+tF1qa2p6oaqNjzeJwAAAAAAAIC+o0Mhpc9//vNJkv333z9HH310Pvaxj6WiYu07XAwePDh777139t5775x66ql5/fXXM3Xq1Nxyyy257rrrMnDgwHz5y1/uUKEjR47MjBkzOtR3hd122y277bbbOo35W6NGjcrll1/eqTmAvmXIoMpMmlifybc3rBTAqR9dlzMn1mfIoMpurG7D4X0CAAAAAAAA6Ds6FFLaf//9M2nSpOy6666dWmzkyJE544wz8oUvfCG33HJLBg0a1Kn5ADZUdUOrc/axY9O0sCWLFrdlcHVFamuqBG/+hvcJAAAAAAAAoG/oUEjpuuuu69JFq6urc8opp3TpnAAbmiGDKoVtOsD7BAAAAAAAAND79evuAgAAAAAAAAAAgN5NSAkAAAAAAAAAACgrISUAAAAAAAAAAKCsBpQ68Pjjj1+n/lVVVdloo42y3Xbb5aMf/WjGjBlT6tIAAAAAAAAAAEAPUnJI6Yknnmh/XSgUkiTFYnGlPqs7XigUctVVV2XffffN5Zdfns0226zUEgAAAAAAAAAAgB6g5JDSGWeckba2ttx6661pamrKlltumT333DObb755isVi5s6dm+nTp2f27Nmpra3N0Ucfnebm5jz//PNpaGjIo48+mi984Qv5r//6r1RVVXXlOQEAAAAAAAAAABuQkkNKX/rSl3LiiSempaUll19+eSZMmLDaflOnTs3FF1+c5557Ltddd1369euXJ554Il/+8pfzxz/+MbfffnuOO+64UssAAAAAAAAAAAA2cP1KHXjjjTfmySefzAUXXLDGgFKSTJgwIRdccEEeffTR/Md//EeSZK+99spZZ52VYrGYX//616WWAAAAAAAAAAAA9AAlh5R+/vOfp3///jn00EPX2vfQQw9N//79M3Xq1PZjBx98cAqFQl555ZVSSwAAAAAAAAAAAHqAkkNKs2bNyuDBg1NZWbnWvpWVlRk8eHBeffXV9mNDhgzJRhttlIULF5ZaAgAAAAAAAAAA0AOUHFIaMGBA3nvvvcydO3etfefOnZv33nsvAwYMWOn44sWLM2TIkFJLAAAAAAAAAAAAeoCSQ0o77bRTkuSKK65Ya9/vfOc7KRaL2XHHHduPvf3222ltbc2mm25aagkAAAAAAAAAAEAPUHJI6dhjj02xWMzPf/7znHzyyXnyySezdOnS9valS5dm+vTpOeWUU3L33XenUCjkuOOOa29/+OGHkyQ777xz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AAAAAAAAAADwV/26u4ByWl1AaYWKiorss88+SZK//OUv66skAAAAAAAAAADoczq9k9Ljjz+ee+65JzNmzMj8+fOzdOnSNfYtFAq5//77O7tkl2ltbU2SVFdXr7HPwoUL841vfCOzZs1KRUVFRo0alXHjxmW//fZbX2UCAAAAAAAAAECPVnJIqVgs5vzzz8/UqVPbf16bQqFQ6nJdbsmSJXnggQeSJHvsscca+zU1NeW2225b6dgtt9ySfffdN9/97nez8cYbf+A6c+bMyfjx49fYvqIGgFK5zgDrg2sNAAAAAAAAnVFySOnmm2/OlClTkiQ77bRTxo0bl8022ywDBnR6c6b14vvf/37efvvtbLzxxjniiCNWaR84cGAmTJiQT3/609luu+2y8cYbp7GxMb/61a8yefLkPProozn99NPzf/7P/0n//v274QwAAAAAAAAAAKBnKDlRdOedd6ZQKOTII4/Mt771ra6sqezuueee3HDDDUmSb3/726mpqVmlz6677ppdd911pWMjRozISSedlPr6+hxzzDFpaGjIPffck3/6p39a41rDhw+3swBQVq4zwPrgWgMAAAAAAEBn9Ct14MyZM5MkZ511VlfVsl488sgjOeecc5Ik//Iv/5JPfOIT6zxHfX19DjzwwCTJfffd16X1AQAAAAAAAABAb1NySKmqqiobbbRRamtru7Kespo+fXq+/OUvp62tLaeeemq+9KUvlTxXfX19kv8X1gIAAAAAAAAAAFav5JDS9ttvn4ULF2bRokVdWU/ZNDQ05NRTT83ixYtz3HHHdXoHqIqKiiTJ0qVLu6I8AAAAAAAAAADotUoOKR1zzDFZtmxZ7rjjjq6spyyef/75nHLKKWlubs7EiRNzwQUXdHrOV155JUmyxRZbdHouAAAAAAAAAADozUoOKR100EE55phjcsUVV2Tq1KldWFLXmjFjRr7whS9kwYIFOfTQQ/PNb34zhUKhU3O+9dZbufvuu5Mk++23X1eUCQAAAAAAAAAAvdaAUgeed955SZLq6uqcd955ueqqqzJmzJgMHjx4jWMKhUIuvfTSUpdcZzNnzsxJJ52U+fPn55Of/GQuu+yy9OvXsVzWP//zP+fII4/MPvvskwED/t/b9Oyzz+bcc8/NggULstlmm+Xoo48uV/kAAAAAAAAAANArlBxSmjJlSgqFQorFYpJk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Tsns2bOz00475fLLL1+n8QMHDsxXvvKVJMljjz2W9957r73t/QGkFY99+1vvDzINHDhwndZekwXNrXl97oLMeHVeXp+7IAuaW9c+CAD6IPdMOsLfCQAAAAAAwIajQzspbWgWLVqUk08+OS+++GI+/OEP5/rrr09NTc06z7P77rsnSZYvX55Zs2ZlzJgxSVZ+lNv8+fOz+eabrzK2qamp/fX7+5eqcf7iTL69IQ0zGtuP1Y+uy6SJ9akbWt3p+QGgt3DPpCP8nQAAAAAAAGxYetxOSosXL84Xv/jFPP300/nQhz6UG264IcOGDStprvc/ym3ZsmXtr0eMGNHeNmvWrNWOXXG8srIyW265ZUnrr7CguXWVL9GSpGFGYybf3uB//QPA/+WeSUf4OwEAAAAAANjwdGonpWKxmDvuuCP33ntvZsyYkaamppXCPn+rUCjkxRdfLHm9lpaWnH766Zk+fXpGjBiRm266KXV1dSXP9/LLL7e/fv9uSQMGDMiYMWPS0NCQJ598MnvuuecqY5988skkyc4775z+/fuXXEOSNC1sWeVLtBUaZjSmaWFLhgyqXG07APQl7pl0hL8TAAAAAACADU/JIaVFixbl1FNPze9///sUi8WurGm12traMmnSpPzud7/LFltskZtuuilbbLFFp+a87rrrkiTbbbfdKnMdeOCBaWhoyJQpU3LyySevtOtSa2tr7rzzziTJQQcd1KkakmTR4rZOtQNAX+GeSUf4OwEAAAAAANjwlBxSuvrqq/PUU0+lf//+OeSQQ7L//vtn00037fSuQquzbNmyfO1rX8t///d/p66uLjfddFO22mqrtY67/vrrU1lZmUMOOWSlR8K9++67+d73vpdp06YlSSZNmrTK2KOOOirXX399Xn311Vx00UW56KKLUlVVlZaWllx88cWZNWtWNttssxx55JGdPr/B1RWdageAvsI9k47wdwIAAAAAALDhKTmkNG3atBQKhZx//vk55phjurKmVfzyl79sDxRVVlbmvPPOW2PfCy+8MDvuuGOS5M0338x//Md/5JJLLsmIESOy8cYbZ8mSJfnzn/+cpUuXpl+/fvnqV7+62t2QBg0alCuvvDInn3xy7rjjjtx///0ZOXJkXn/99TQ1NWXQoEGZPHlyqqurO31+tTVVqR9dt9rHktSPrkttTVWn1wCA3sA9k47wdwIA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tttuy/Tp01NdXb1O37a2tpx22ml5+OGHM3jw4Oyyyy5pbW3Ngw8+mAcffDBTpkzJueeeu8F5m5qa8qEPfSjLly9PfX193vSmN+WFF17IzTffnDvuuCNXX3119txzz3I+dAAAAAAAAAAA6PM6FVKaOXNmWSYfPHhwp9oVi8Vsv/32+eAHP5ijjjoqW2211dpzt9xyS84///zce++9+fa3v71e4Og///M/8/DDD2f77bfP97///ey8885J/vGYPvaxj+X73/9+9t577/WuEtXa2ppp06Zl+fLlee9735sLLrggNTU1aWtryxe+8IXMmDEj06ZNy5133pktttiim88E9C9Ll7enuaUty1o7Mqy2KvV1NRk+tHrTHSk7rw1QbtYZAAAAAAAANqRTIaWxY8eWu47XtOeee+aOO+5IVVXVeucmTZqUv/3tb/nmN7+Zm266KZ/4xCcyaNCgJMnf//73XH/99UmSL3/5y2sDSkkyceLEnHHGGbnsssty6aWXrhdSuuGGG7Jw4cLstNNO+cIXvrD2Ck01NTX54he/mFmzZmXOnDm56aabcsopp5TroUOfs3BJa6bf2JSm2QvXHmsc15BpkxvTMKK2gpXhtQHKzToDAAAAAADAxgyqdAGdUVdXt8GA0hpvf/vbkyRLlizJokWL1h6/55570tHRkZ122ikHHnjgev3e9773JUmeeOKJzJkzZ51zd9xxR5Lk2GOPXe8WctXV1TnuuOOSJLfffnsXHhH0T0uXt6/35XSSNM1emOk3NmXp8vYKVYbXBig36wwAAAAAAACvpU+ElDalra1t7c+vvvXaI488kiTZZ599Nthv9OjR2X777ddpmySrVq3K448/niTZd999N9h3zfHHHnssq1at6nLt0J80t7St9+X0Gk2zF6a5pW2D5yg/rw1QbtYZAAAAAAAAXkunbvfW2912221Jkl133TV1dXVrjz/33HNJkp122mmjfXfccce8+OKLefbZZ9cemzt3bjo6Otae31i/JGlvb89LL72UHXbYYYPt5s2bl4kTJ250/pkzZ270HPQ1y1o7unWerunMOuO1AbprU2vNZT+46TX7W2eATfH/TgAAAAAA/Vufv5LSE088keuvvz5JcuaZZ65zrrm5OUlSX1+/0f5rzr3yyitrjy1ZsmTtzyNGjHjNfq+eBwa6YbUbvy1jZ85TPl4boNysMwAAAAAAALyWPn0lpb///e+ZOnVqOjo6cthhh+Woo45a5/ya28BVVW38S7Hq6uokyYoVK9Yea29vX/vzxvqu6ffPff/Zdttt51/8MmDU19WkcVzDBm/30ziuIfV1NRWoqv/rzDrjtQG6a1NrzdLl7dYZoFv8vxMAAAAAQP/WZ6+ktHTp0kyZMiUvvfRS9thjj1x88cXrtamp+ceXYWtu3bYhawJJW2yxxdpjrw4gbazvq4NMr+4LA9nwodWZNrkxjeMa1jneOK4h50xuzPCh1RvpSbl5bYBys84AAAAAAADwWvrklZSWLVuWM844I08++WTe+MY35sorr0xdXd167bbccsskr307tjXn1rRN1r2V25IlSzJ69OiN9vvn9jDQNYyozadO3jfNLW1Z1tqRYbVVqa+r8eV0L+C1AcrNOgMAAAAAAMDG9LmQUmtraz784Q/nkUceyete97pcddVVGTly5Abbvu51r8sf//jHPP/88xsdb86cOWvbrjF27NhUVVWlo6Mjc+bM2WBIaU2/6urqjBkzphuPCPqf4UOrfSHdS3ltgHKzzgAAAAAAALAhfep2b21tbTn77LPz0EMPZezYsbnmmmvS0NCw0fZ77bVXkuSPf/zjBs/Pnz8/L7744jptk2TIkCEZP358kmTWrFkb7Lvm+Jvf/OYMHjx4cx8KAAAAAAAAAAAMGH0mpNTR0ZFp06bl/vvvz7bbbptrrrkm22677Wv2mThxYqqqqvLcc8/lgQceWO/89ddfnyTZfffds9NOO61z7vDDD0+S/OQnP0lHR8c659rb2zNjxowkyRFHHNHlxwQAAAAAAAAAAANBt2/39uyzz+aRRx7JggULsmjRorS1tWXEiBEZNWpU3vCGN2TvvfdObW1tt+ZYtWpVzj333Pz6179OQ0NDrrnmmuywww6b7Lf11lvnxBNPzHXXXZfPfvaz+f73v5+dd945SXLPPffkiiuuSJJ89KMfXa/viSeemCuvvDLPP/98LrjgglxwwQWpqalJW1tbvvCFL2TOnDnZZpttcsIJJ3TrsQEAAAAAAAAAQH/XpZBSU1NTbrzxxvz2t7/Nyy+//JptBw8enD322CPvfve7c8wxx2T48OGbPd/tt9+eO+64I0lSXV2dz3zmMxtte/7552f33Xdf+/unPvWpPPHEE2lqasrRRx+dN77xjVm+fHnmzJmTJPnQhz6UQw89dL1xhg4dmm9961s544wzcvPNN+fuu+/O9ttvnxdffDHNzc0ZOnRopk+f3u0AFgAAAAAAAAAA9HebFVL66U9/miuuuCLPPPNMisXi2uNDhw7NiBEjMmLEiNTU1KS5uTnNzc1ZvHhxVq5cmT/96U959NFH841vfCNHHXVUPvrRj2a77bbr9Lzt7e1rf547d27mzp270bZLly5d5/ctttgi1157ba655prceuutee6551JVVZX9998/J5988trbum3IPvvsk5/+9Ke57LLLct999+XPf/5zRo4cmeOOOy5nn312p67mBAAAAAAAAAAAA12nQkoPPvhgvvrVr+app55KsVhMfX19Dj/88Oy77755y1vekp122mmD/ZYtW5bHH388f/rTn3LPPffkkUceyf/+7//mZz/7WT7wgQ/kwx/+cOrq6jY5/3HHHZfjjjtu8x7Zq1RXV2fKlCmZMmXKZvfdcccdc/HFF3d5bgAAAAAAAAAAGOg6FVL64Ac/mCQ5+OCD8773vS/veMc7UlVVtcl+w4YNywEHHJADDjggZ555Zl588cXccsstue6663LFFVdkiy22yEc/+tHuPQIAAAAAAAAAAKBX61RI6eCDD860adPylre8pVuTbb/99pk6dWpOP/30XHfddRk6dGi3xgMAAAAAAAAAAHq/ToWUrrjiipJOWltb26VbrwEAAAAAAAAAAH3PoEoXAAAAAAAAAAAA9G9CSgAAAAAAAAAAQFkJKQEAAAAAAAAAAGU1pKsdP/CBD2xW+5qammy55ZbZZZdd8i//8i8ZP358V6cGAAAAAAAAAAD6kC6HlP7whz+s/blQKCRJisXiOm02dLxQKOTb3/52DjrooFx88cXZZpttuloCAAAAAAAAAADQB3Q5pDR16tR0dHTk+uuvT3Nzc8aMGZP99tsvo0ePTrFYzIIFC/LQQw/lpZdeSn19fd73vvdl+fLlefzxx9PU1JTf//73Of300/O///u/qampKeVjAgAAAAAAAAAAepEuh5Q+8pGP5LTTTktbW1suvvjiTJo0aYPtbrnllnzhC1/IY489liuuuCKDBg3KH/7wh3z0ox/NM888kxtvvDGnnHJKV8sAAAAAAAAAAAB6uUFd7Xj11Vdn1qxZ+exnP7vRgFKSTJo0KZ/97Gfz+9//Ptdee22SZP/9988nP/nJFIvF/PKXv+xqCQAAAAAAAAAAQB/Q5ZDSz372swwePDjHHHPMJtsec8wxGTx4cG655Za1x4466qgUCoU8/fTTXS0BAAAAAAAAAADoA7ocUpozZ06GDRuW6urqTbatrq7OsGHD8vzzz689Nnz48Gy55ZZpaWnpagkAAAAAAAAAAEAf0OWQ0pAhQ/LKK69kwYIFm2y7YMGCvPLKKxkyZMg6x1tbWzN8+PCulgAAAAAAAAAAAPQBXQ4p7bHHHkmSr3/965ts+41vfCPFYjG777772mN///vf097enq233rqrJQAAAAAAAAAAAH1Al0NKJ598corFYn72s5/ljDPOyKxZs7Jy5cq151euXJmHHnooU6ZMya233ppCoZBTTjll7fnf/va3SZI3v/nN3SgfAAAAAAAAAADo7YZsusmGHXrooTnttNNy1VVX5b777st9992XIUOGZMSIESkUClm8ePHa0FKxWMypp56aQw89dG3/xx57LLvuumsOO+yw7j8KAAAAAAAAAACg1+pySClJPv3pT2f8+PH59re/neeffz4dHR1ZuHDhOm122mmnTJs2LUcfffQ6xz//+c93Z2oAAAAAAAAAAKCP6FZIKUmOOuqoHHXUUXnqqafyxBNPZNGiRUmSUaNGZY899shuu+3W7SIB+qKly9vT3NKWZa0dGVZblfq6mgwfWl3psnodzxPAwGUPAAAAAACAgaPLIaVLL700SfLe97432223XXbbbTeBJID/n4VLWjP9xqY0zf7/X12ucVxDpk1uTMOI2gpW1rt4ngAGLnsAAAAAAAAMLIO62vE73/lO/vu//ztbb711KesB6POWLm9f70vXJGmavTDTb2zK0uXtFaqsd/E8AQxc9gAAAAAAABh4uhxSGjlyZIYNG5aqqqpS1gPQ5zW3tK33pesaTbMXprmlrYcr6p08TwADlz0AAAAAAAAGni6HlHbdddcsXbo0ixcvLmU9AH3estaObp0fKDxPAAOXPQAAAAAAAAaeLoeUTjzxxKxevTpXXXVVKesB6POG1b72FeY2dX6g8DwBDFz2AAAAAAAAGHi6HFI6/PDDc9ppp+X73/9+vva1r2XRokWlrAugz6qvq0njuIYNnmsc15D6upoerqh38jwBDFz2AAAAAAAAGHiGdLXjBz7wgSRJbW1trrrqqlxzzTXZcccds9VWW2XQoA1nnwqFQq655pquTgnQJwwfWp1pkxsz/camNM1euPZ447iGnDO5McOHVlewut7D8wQwcNkDAAAAAABg4OlySOkPf/jDOr+vWrUqzz77bJ599tmN9ikUCl2dDqBPaRhRm0+dvG+aW9qyrLUjw2qrUl9X40vXf+J5Ahi47AEAAAAAADCwdDmkNHXq1FLWAdDvDB9a7YvWTvA8AQxc9gAAAAAAABg4hJQAAAAAAAAAAICyGlTpAgAAAAAAAAAAgP5NSAkAAAAAAAAAACirLt/ubeXKlbn77rvz4IMP5umnn86SJUuSJCNGjMguu+ySAw88MIceemiGDOnyFAAAAAAAAAAAQD/QpQTRrbfemksuuSTz589fe6xYLCZJCoVCHn744dxwww3ZZptt8olPfCLHHHNMaaoFAAAAAAAAAAD6nM0OKX35y1/Oddddt04oaYcddsjIkSNTLBazZMmSvPDCCykWi5k/f37OO++8PProozn//PNLXjwAAAAAAAAAAND7bVZI6Qc/+EF++MMfJkne8IY3ZMqUKTn00ENTV1e3TruWlpbcddddueKKK/KXv/wlP/7xjzNmzJicfvrppascAAAAAAAAAADoEwZ1tuGCBQsyffr0FAqFHHvssfnJT36SSZMmrRdQSpK6urp12hSLxUyfPj0LFiwoafEAAAAAAAAAAEDv1+mQ0o033pjW1tY0NjbmoosuSnV19Sb7VFdX56KLLkpjY2Pa2tpy0003datYAAAAAAAAAACg7+l0SOm+++5LoVDIRz/60c2aYE2fYrGY3/3ud5tdIAAAAAAAAAAA0Ld1OqT0wgsvZNCgQTnggAM2e5IDDzwwgwYNypw5cza7LwAAAAAAAAAA0Ld1OqS0ZMmS1NXVZciQIZs9yZAhQzJ8+PC88sorm90XAAAAAAAAAADo2zodUho+fHiWLVuWYrG42ZOsXr06LS0tqaur2+y+AAAAAAAAAABA39bpkNLYsWOzatWq/PGPf9zsSf74xz9m1apVGTt27Gb3BQAAAAAAAAAA+rZOh5QOOuigFIvFfO9739vsSb73ve+lUCjkoIMO2uy+AAAAAAAAAABA39bpkNIJJ5yQIUOG5De/+U2++c1vdnqCb37zm/nNb36TwYMH54QTTuhSkQAAAAAAAAAAQN/V6ZDSjjvumFNOOWXt1ZROO+20zJo1a6PtH3rooZx66qlrr6J0yimnZMcddyxJ0QAAAAAAAAAAQN8xZHMaf+pTn8rzzz+fe+65Jw888EAeeOCBjBgxIrvvvntGjBiRJFmyZEmefPLJLFmyJElSLBZzyCGH5FOf+lSpawcAAAAAAAAAAPqAzQopDRo0KN/5znfyne98J1dccUVWrFiRxYsX5/7771+nXbFYTJLU1NTkjDPOyNSpU1MoFEpXNQAAAAAAAAAA0GdsVkgpSQqFQqZOnZr3v//9ufnmm/PAAw/kmWeeWXvlpBEjRmSXXXbJgQcemPe+970ZNWpUqWsGAAAAAAAAAAD6kM0OKa0xatSoTJkyJVOmTCllPQAAAAAAAAAAQD8zqNIFAAAAAAAAAAAA/ZuQEgAAAAAAAAAAUFadCindcccdJZ94/vz5aWpqKvm4AAAAAAAAAABA79KpkNLHPvaxHHPMMbnrrrtSLBa7NeG8efPypS99KYcddljuu+++bo0FAAAAAAAAAAD0fkM60+joo4/ObbfdlnPOOSdbbbVV3v3ud+eoo47KHnvskUKhsMn+ixYtysyZM/Ozn/0ss2bNyurVqzNmzJgccMAB3X4AAAAAAAAAAABA79apkNLXv/71fPCDH8xXv/rVzJo1K1dffXWuvvrq1NbWZo899si4ceMyatSo1NfXp7q6Oq+88kqam5vzwgsv5NFHH81LL72UJCkWi6mrq8uZZ56ZU089NdXV1WV9cAAAAAAAAAAAQOV1KqSUJG9+85tz3XXX5U9/+lP+53/+J3fccUeWL1+ehx56KLNmzdpgn1ffGm7XXXfNiSeemPe85z0ZNmxY9yunRyxd3p7mlrYsa+3IsNqq1NfVZPhQ4TKgb7O2AUDn2DMBAAAAACiVToeU1njLW96St7zlLfnc5z6Xhx56KA899FAeffTRLFiwIIsWLUpbW1tGjBiRUaNGZZdddsm+++6b/fffP294wxvKUT9ltHBJa6bf2JSm2QvXHmsc15BpkxvTMKK2gpUBdJ21DQA6x54JAAAAAEApbXZIaY26uroccsghOeSQQ0pZD73E0uXt630hkSRNsxdm+o1N+dTJ+/oX1ECfY20DgM6xZwIAAAAAUGqDKl0AvVNzS9t6X0is0TR7YZpb2nq4IoDus7YBQOfYMwEAAAAAKDUhJTZoWWtHt84D9EbWNgDoHHsmAAAAAAClJqTEBg2rrerWeYDeyNoGAJ1jzwQAAAAAoNSGVLqAzlq4cGHuv//+PPbYY3n88cfz1FNPZcWKFdljjz0yY8aMjfabMGFC5s6d+5pjP/roo6mpqdnguRdeeCGXXXZZ7rvvvixatChbbbVV3va2t+Wss87KDjvs0K3H1JvV19WkcVzDBm/x0DiuIfV1G36+AHozaxsAdI49EwAAAACAUuszIaXbbrstF110UZf7v+lNb0pdXd0GzxUKhQ0eb2pqyoc+9KEsX7489fX1edOb3pQXXnghN998c+64445cffXV2XPPPbtcU282fGh1pk1uzPQbm9b5YqJxXEPOmdyY4UOrK1gdQNdY2wCgc+yZAAAAAACUWp8JKdXV1eWtb31rxo8fn/Hjx+e5557LJZdc0un+n/vc53LAAQd0un1ra2umTZuW5cuX573vfW8uuOCC1NTUpK2tLV/4whcyY8aMTJs2LXfeeWe22GKLrjykXq9hRG0+dfK+aW5py7LWjgyrrUp9XY0vJIA+zdoGAJ1jzwQAAAAAoJT6TEjp+OOPz/HHH7/299e6xVsp3HDDDVm4cGF22mmnfOELX0h19T/+EF9TU5MvfvGLmTVrVubMmZObbropp5xySllrqaThQ6t9CQH0O9Y2AOgceyYAAAAAAKUyqNIF9FZ33HFHkuTYY49dG1Bao7q6Oscdd1yS5Pbbb+/x2gAAAAAAAAAAoC/pM1dS6q7rr78+P/jBD7JixYpsvfXW2XffffPud787dXV167VdtWpVHn/88STJvvvuu8Hx1hx/7LHHsmrVqgwePLh8xQMAAAAAAAAAQB/WIyGlZcuW5dOf/nQKhUKmT5/eE1Ou5xe/+MU6v//85z/Pt771rXzjG9/I2972tnXOzZ07Nx0dHUmSHXfccYPjrTne3t6el156KTvssMMG282bNy8TJ07caF0zZ87s9GMA2BDrDNATrDVAuVlnAAAAAAD6tx4JKbW3t+fuu+9OoVDoienW0djYmI985CPZZ599MmbMmHR0dOThhx/Ot7/97Tz55JM566yz8j//8z/ZY4891vZZsmTJ2p9HjBixwXHr6+vX/tzc3LzRkBIAAAAAAAAAAAx0/f52b9/4xjfW+b22tjaHHHJIDjrooLz//e/PE088ka9//eu56qqr1rZpb29f+3NVVdUGx62url7784oVKzY6/3bbbedf/AJlZZ0BeoK1Big36wwAAAAAQP82qNIFVMoWW2yRj33sY0mSBx54IK+88srac68OIK257ds/e3WQaYsttihPkQAAAAAAAAAA0A8M2JBSkuy9995JktWrV2fOnDlrj7/6Vm6vvvXbqzU3N2+wPQAAAAAAAAAAsK4BHVJ69a3cVq1atfbnsWPHrj336vDSq605Xl1dnTFjxpSxSgAAAAAAAAAA6NsGdEjpz3/+89qfR48evfbnIUOGZPz48UmSWbNmbbDvmuNvfvObM3jw4DJWCQAAAAAAAAAAfduADildccUVSZJddtkl22677TrnDj/88CTJT37yk3R0dKxzrr29PTNmzEiSHHHEET1QKQAAAAAAAAAA9F39OqR05ZVX5oc//GEWL168zvHFixfn85//fO64444kybRp09bre+KJJ6ahoSHPP/98LrjggrS1tSVJ2tracsEFF2TOnDnZZpttcsIJJ5T/gQAAAAAAAAAAQB82pLMNJ06c2OVJisVil/uuMW/evEyaNGnt7+3t7UmS2bNn54ADDlh7/IwzzsiUKVOSJH/7299y7bXX5stf/nLGjh2bUaNGZcWKFfnrX/+alStXZtCgQfnEJz6xwashDR06NN/61rdyxhln5Oabb87dd9+d7bffPi+++GKam5szdOjQTJ8+PbW1td1+bAAAAAAAAAAA0J91OqQ0d+7cctaxSatWrcqSJUvWO75y5cp1jq9YsWLtz0cddVSKxWIee+yxvPTSS/m///u/DB48ONtvv33233//vP/9789uu+220Tn32Wef/PSnP81ll12W++67L3/+858zcuTIHHfccTn77LOzww47lPIhAgAAAAAAAABAv9TpkNKxxx5bzjo2afvtt8/s2bM3q89ee+2Vvfbaq1vz7rjjjrn44ou7NQYAAAAAAAAAAAxknQ4pXXTRReWsAwAAAAAAAAAA6KcGVboAAAAAAAAAAACgfxNSAgAAAAAAAAAAyqrTIaVbbrklt99+ezlrAQAAAAAAAAAA+qEhnW143nnnpaGhIUceeeR656ZPn55ly5blvPPOK2lxAAAAAAAAAABA37dZt3srFosbPH7DDTfkmmuuKUlBAAAAAAAAAABA/7JZISUAAAAAAAAAAIDNJaQEAAAAAAAAAACUlZASAAAAAAAAAABQVkJKAAAAAAAAAABAWQ2pdAHwz5Yub09zS1uWtXZkWG1V6utqMnxodaXLAmAAszcB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X1IsFnP++edn0aJFG2338ssv5/Of/3wKhULe/va3l6MUAAAAAAAAAACgwoaUY9DTTz89N998c5555pkceeSROemkk3LQQQdl9OjRKRQKmTdvXn7/+9/nhhtuyJIlS7Llllvm9NNPL0cpAAAAAAAAAABAhZUlpLT11lvn0ksvzdSpU9Pc3Jzvfve7+e53v7teu2KxmC233DLf+c53stVWW3V5voULF+b+++/PY489lscffzxPPfVUVqxYkT322CMzZszY7PGKxWKamppyzz335OGHH85f//rXtLS0ZPjw4dl9990zadKkvPvd706hUOhyzQAAAAAAAAAAMFCUJaSUJPvvv39uvfXWXH755bnjjjvS3Ny8zvn6+vq8613vykc+8pGMHj26W3Pddtttueiii7o1xqs98MADOfXUU9f+vsMOO2Ts2LGZO3du7rvvvtx333257bbbMn369FRXV5dsXgAAAAAAAAAA6I/KFlJKkm233TZf/OIX88UvfjEvvPBCFi1alCQZNWpUdthhh5LNU1dXl7e+9a0ZP358xo8fn+eeey6XXHJJl8crFovZfvvt88EPfjBHHXXUOld5uuWWW3L++efn3nvvzbe//e2ce+65pXgIAAAAAAAAAADQb5U1pPRqO+ywQ0mDSa92/PHH5/jjj1/7e1du8fZqe+65Z+64445UVVWtd27SpEn529/+lm9+85u56aab8olPfCKDBg3q1nwAAAAAAAAAANCfSddsQF1d3QYDSmu8/e1vT5IsWbJk7dWhAAAAAAAAAACADSv7lZRWr16d5557Ls3NzVm5cuVrtt1vv/3KXU5JtLW1rf15iy22qGAlAAAAAAAAAADQ+5UtpLRgwYJccsklufPOO7NixYpNti8UCnnyySfLVU5J3XbbbUmSXXfdNXV1da/Zdt68eZk4ceJGz8+cObOktQEDj3UG6AnWGqDcrDNAuVlnAAAAACqrLCGl+fPnZ/LkyVmwYEGKxWKn+nS2XaU98cQTuf7665MkZ555ZoWrAQAAAAAAAACA3q8sIaVLL7008+fPz7Bhw/Lxj388EydOzDbbbJPBgweXY7oe8/e//z1Tp05NR0dHDjvssBx11FGb7LPddtv5l3hAWVlngJ5grQHKzToDlJt1BgAAAKCyyhJS+s1vfpNCoZAvf/nLOeKII8oxRY9bunRppkyZkpdeeil7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9a/5t3/7tyxbtizFYjFVVVUZNWpUBg8eXI7pAAAAAAAAAACAXqwsIaVLL700LS0t2WabbfLFL34xb3/728saUFq4cGHuv//+PPbYY3n88cfz1FNPZcWKFdljjz0yY8aMbo39wAMP5Kqrrsqf/vSnLF++PGPGjMkRRxyRM888M0OHDi3RIwAAAAAAAAAAgP6rLCGlBx98MIVCIV/96ldz0EEHlWOKddx222256KKLSj7uD3/4w3z5y19OsVjMtttum+222y7PPPNMLr/88vzyl7/Mj3/844wYMaLk8wIAAAAAAAAAQH9SlpDS0qVLU11dnQMOOKAcw6+nrq4ub33rWzN+/PiMHz8+zz33XC655JJujfn444/nK1/5SpLkS1/6UiZPnpxCoZD58+fnrLPOyhNPPJHzzz8/06dPL8VDAAAAAAAAAACAfqssIaWGhoYsWrQogwYNKsfw6zn++ONz/PHHr/29u7d4S5LLLrssq1evzqRJk3LiiSeuPT569OhccsklOfLII/PLX/4y//d//5ddd9212/MBAAAAAAAAAEB/VZYU0SGHHJIVK1bkySefLMfwZbds2bL89re/TZJMnjx5vfOve93rcuCBByZJ7rjjjh6tDQAAAAAAAAAA+pqyhJTOOuusjBw5Ml/5ylfS3t5ejinK6qmnnkp7e3uqq6uz5557brDNPvvskyT505/+1JOlAQAAAAAAAABAn1OW2721tbXloosuyr//+7/n2GOPzYc+9KHsueeeGTZs2Gv2GzNmTDnK2WzPPvtskn/UU1VVtcE2O+644zptN2bevHmZOHHiRs/PnDmzi1UC/IN1BugJ1hqg3KwzQLlZZwAAAAAqqywhpVf/weeVV17J5z73uU32KRQKveb2cM3NzUmS+vr6jbZZc25NWwAAAAAAAAAAYMPKElIqFos90qdc2trakmSjV1FKkurq6nXabsx2223nX+IBZWWdAXqCtQYoN+sMUG7WGQAAAIDKKktIqa//waempiZJ0tHRsdE27e3t67QFAAAAAAAAAAA2rCwhpbFjx5Zj2B7TmVu5deaWcAAAAAAAAAAAQDKo0gX0Rq973euSJC+99NJGr6Y0Z86cddoCAAAAAAAAAAAbVpYrKW3I3Llz8/LLL6dQKGTUqFG9+mpLu+++e6qqqtLe3p5HH300++yzz3ptHn744STJXnvt1cPVAQAAAAAAAABA31LWkNKCBQvyve99L7fddluWLFmyzrkRI0bk6KOPzpQpU7LNNtuUs4zNNmzYsBx88MH51a9+lRtvvHG9kNJzzz2XBx54IElyxBFHVKJEAAAAAAAAAADoM8p2u7eHH34473nPe/KjH/0oixcvTrFYXOe/xYsX57rrrssxxxyTP/7xj+Uq4zWddNJJmTBhQq6++ur1zp199tkpFAr56U9/mhtuuCHFYjHJP4JXn/jEJ7J69eoceuih2XXXXXu4agAAAAAAAAAA6FvKciWll19+OWeffXaam5tTV1eX973vfXnb296W0aNHJ0nmz5+f+++/PzfccEMWL16cs846K7/4xS+y1VZbdWm+efPmZdKkSWt/b29vT5LMnj07BxxwwNrjZ5xxRqZMmbL29/nz52fu3LlZunTpemPuueeeOe+883LxxRfn85//fC6//PKMHDkyzzzzTNrb2/P6178+F154YZfqBQAAAAAAAACAgaQsIaUf/OAHaW5uzs4775yrrrpqbThpjZ133jkHHXRQTj755Jx22ml59tlnc9VVV+Xcc8/t0nyrVq1a73ZySbJy5cp1jq9YsWKzxj311FMzbty4/OAHP8ijjz6al19+OWPGjMkRRxyRM888M8OGDetSvQAAAAAAAAAAMJCUJaT061//OoVCIRdeeOF6AaVXGz16dC688ML827/9W+69994uh5S23377zJ49e7P73XPPPZtsc9BBB+Wggw7qSlkAAAAAAAAAAECSQeUYdO7cuamtrc0+++yzybb77LNPamtrM3fu3HKUAgAAAAAAAAAAVFhZQkpJUiwWyzU0AAAAAAAAAADQh5QlpDR27NisWLEijzzyyCbbNjU1pbW1NWPHji1HKQAAAAAAAAAAQIWVJaT0L//yLykWizn//POzaNGijbZ7+eWX8/nPfz6FQiFvf/vby1EK3bR0eXteXLA0s59flBcXLM3S5e2VLgmg26xtANA59sy+w2sFAAAAAPR2Q8ox6Omnn56bb745zzzzTI488sicdNJJOeiggzJ69OgUCoXMmzcvv//973PDDTdkyZIl2XLLLXP66aeXoxS6YeGS1ky/sSlNsxeuPdY4riHTJjemYURtBSsD6DprGwB0jj2z7/BaAQAAAAB9QVmupLT11lvn0ksvzfDhw9Pc3Jzvfve7OfXUU3PkkUfmiCOO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zB133LG+KKmuri5Tp07N1KlTM2bMmDQ2NmabbbbJihUr0tramqeffjr3339/nn322Tz00EP5zGc+kwsvvDAf/ehH8773vS+1tbXlCA0AAAAAAAAAAKiwrS5SOuusszJ37tysXbs2Y8eOzeGHH54jjzwy//RP/5Rhw16+++eeey7z5s3L3Llzc/fdd+dLX/pS/vu//ztf/OIXs++++25teAAAAAAAAAAAQIVtdZHSDTfckN133z0nn3xyDj300AwdOnSL9t9uu+0yY8aMzJgxI88880y+8Y1vZM6cOVmwYIEiJQAAAAAAAAAAqAJbXaT05S9/OYcffnhqamq2OpiJEyfms5/9bE466aQ888wzW90fAAAAAAAAAABQeVtdpHTEEUeUI44N7Ljjjtlxxx3L3i8AAAAAAAAAAND3hlQ6AAAAAAAAAAAAoLopUgIAAAAAAAAAAAq11a9725hnn312i7avq6vLqFGjUltbW0Q4AAAAAAAAAABABRVSpDR9+vRe7bfzzjvnda97Xd773vdm8uTJZY4KAAAAAAAAAACohEJe91YqlXr1Z/Hixfn+97+ft73tbfnZz35WRGgAAAAAAAAAAEAfK+RJSvPmzcsf/vCHfOYzn8mQIUNy/PHHZ//998+4ceNSKpWybNmy3Hnnnbnmmmvy0ksv5fzzz8/OO++cP/zhD/nud7+bxx57LP/f//f/ZerUqZ6oBAAAAAAAAAAAA1whT1J66aWXcs4552TChAn56U9/mtNPPz0HHnhgdt111+y222458MADc/rpp+emm27KhAkTcs4552TbbbfNjBkzMmfOnOy9997p6urKd7/73SLCAwAAAAAAAAAA+lAhRUqXXXZZ2tracv7552fMmDGb3G7MmDE577zz0tramq9//etJktra2nz84x9PqVTKggULiggPAAAAAAAAAADoQ4UUKd1+++1paGjIP/7jP77stv/0T/+UhoaG/Pa3v13/2atf/eoMHz48S5YsKSI8AAAAAAAAAACgDxVSpPT8889n7dq1Pd6+VCrlueeeW//vQ4cOTUNDQ1566aUiwgMAAAAAAAAAAPpQIUVKY8eOzZo1a/L73//+Zbf9/e9/n/b29g1eC9fV1ZW//vWvm31VHAAAAAAAAAAAMDAUUqT02te+NqVSKZ/+9Kfz1FNPbXK7p59+Op/+9KdTU1OTgw8+eP3nixYtSqlUyoQJE4oIDwAAAAAAAAAA6EPDiuj01FNPzc9//vMsXrw4b33rW3P44Ydn//33zw477JCamposW7Ysd9xxR26++ea0t7dnxIgROfnkk9fvf9NNNyVJ9t9//yLCAwAAAAAAAAAA+lAhRUoTJkzIt771rZx22mn5y1/+khtuuCE33HBDt+1KpVK23377fPWrX83EiRPXf/6KV7win/rUp/LGN76xiPAAAAAAAAAAAIA+VEiRUpI0NzfnZz/7Wa666qr8/Oc/z2OPPZaXXnopSTJ06NDsvvvuOfTQQzNz5sxsu+22G+x7xBFHFBUWW2jl6s60tnVkVXtXRtQPT+PIuoxqqK10WABbRW4DgJ6xZg4crhVUJ3MbAAAAqCaFFSklyahRo3LyySfn5JNPTldXV1pbW1MqlTJ69OgMHz68yENTBstXtGfWdS1pWbh8/WfNU5py2ozmNI2ur2BkAL0ntwFAz1gzBw7XCqqTuQ0AAABUmyFFdDpt2rQccsghefLJJ9d/Nnz48Gy//fZpampSoDQArFzd2e1GWJK0LFyeWde1ZOXqzgpFBtB7chsA9Iw1c+BwraA6mdsAAABANSqkSGn58uV5/vnnM3ny5CK6pw+0tnV0uxG2TsvC5Wlt6+jjiAC2ntwGAD1jzRw4XCuoTuY2AAAAUI0KKVLaYYcdUiqViuiaPrKqvWur2gH6I7kNAHrGmjlwuFZQncxtAAAAoBoVUqT0mte8JmvWrMlDDz1URPf0gRH1m38l38u1A/RHchsA9Iw1c+BwraA6mdsAAABANSqkSOnEE09MfX19zjvvvLS3txdxCArWOLIuzVOaNtrWPKUpjSPr+jgigK0ntwFAz1gzBw7XCqqTuQ0AAABUo0KKlIYOHZrzzjsvf/zjH3PkkUfmO9/5Tu6777489dRTefbZZzf5h/5jVENtTpvR3O2GWPOUppw+ozmjGmorFBlA78ltANAz1syBw7WC6mRuAwAAANVoWBGdTp8+ff0/t7e354tf/OLL7lNTU+P1cP1M0+j6nDlz37S2dWRVe1dG1A9P48g6N8KAAU1uA4CesWYOHK4VVCdzGwAAAKg2hRQplUqlPtmH4o1qqHXzC6g6chsA9Iw1c+BwraA6mdsAAABANSmkSGnevHlFdAsAAAAAAAAAAAxAhRQpTZw4sYhuAQAAAAAAAACAAWhIpQMAAAAAAAAAAACqWyFPUlpn7dq1eeaZZ7JixYokyejRozNx4sQMGaI2CgAAAAAAAAAABotCipR+9atf5eqrr85dd92V1atXb9DW0NCQfffdN+9+97vz+te/vojDAwAAAAAAAAAA/UhZi5Sef/75fOITn8j//M//JElKpVK3bVatWpVf//rX+fWvf50DDzwwF110UbbbbruyHH/BggW54oorct9992X16tWZMGFCDjvssJx44olpaGjY4v5WrFiRK664IrfddlsWL16crq6ujBkzJnvvvXfe85735MADDyxL3AAAAAAAAAAAUM3KVqS0YsWKHH/88Vm8eHFKpVJGjBiR1772tXnlK1+ZMWPGpFQqZcWKFXn44Yfzu9/9LqtWrcqCBQvy7ne/O9dcc03GjBmzVce/6qqr8rnPfS6lUik77rhjxo8fn8ceeyyXXXZZbrnlllx99dUZPXp0j/tbtGhR3vve92bZsmUZMmRIJk6cmFGjRmXx4sW55ZZbcsstt+Rf//Vfc/LJJ29V3AAAAAAAAAAAUO3KVqT0yU9+Mk8++WSGDx+ek046KSeccMImn160evXqXHHFFfn617+exYsX55Of/GS++c1v9vrYDzzwQC644IIkyXnnnZcZM2akpqYmS5cuzUknnZQHH3ww55xzTmbNmtXjPj/zmc9k2bJl2WWXXTJ79uzsscceSZLOzs5cdtllufTSS3PJJZdk2rRp2XPPPXsdOwAAAAAAAAAAVLsh5ejkjjvuyK9//esMGzYsX/va13LyySdv9vVqDQ0NOeWUUzJ79uwMHTo0v/3tb7NgwYJeH//SSy/N2rVr87a3vS3HHXdcampqkiTjxo3Ll7/85QwZMiS33HJLHnnkkR7119bWljvuuCPJ34qv1hUoJUltbW3+9V//Na985StTKpXy61//utdxAwAAAAAAAADAYFCWIqWbbropSTJz5sy87nWv6/F+r3/96/Oe97wnpVJpfR9batWqVfnNb36TJJkxY0a39l122SUHHnhgkuTmm2/uUZ+dnZ0plUpJkp133nmj26z7vKura4tjBgAAAAAAAACAwaQsRUp33XVXampqctxxx23xvscff/z6Pnrj4YcfTmdnZ2pra7PXXnttdJt99tknSXLffff1qM+xY8dm/PjxSZJ77rmnW3tHR0ceeOCBJMk///M/9yZsAAAAAAAAAAAYNIaVo5Nly5altrY2u+666xbvu8suu6Suri7Lly/v1bGfeOKJJMmECRMyfPjwjW4zadKkDbbtiTPPPDMf//jHc+GFF2bIkCF54xvfmJEjR+bxxx/PV7/61Tz77LM59NBDc/DBB2+2nyVLlmT69OmbbJ83b16PYwLYGHkG6AtyDVA0eQYomjwDFE2eAQAA2LyyFCl1dXWltra21/vX1tams7OzV/u2trYmSRobGze5zbq2ddv2xFve8paMGDEis2fPzjnnnLNB25gxY3LuueeufwoUAAAAAAAAAACwaWUpUho7dmz+/Oc/Z+XKlRk1atQW7bty5cqsXLkyO+64Y6+O3dHRkSSbfIpSkvUFVOu27anFixentbU1NTU1GT9+fLbddtssXrw4L7zwQq699tpMnTo1zc3Nm+1j/Pjx/oYMUCh5BugLcg1QNHkGKJo8AxRNngEAANi8IeXoZMqUKUmSX/ziF1u87y233LJBH1uqrq4uyd+e5rQp657StG7bnvjsZz+bz33uc2loaMiNN96YW2+9NTfeeGPuvPPOfPzjH8/ChQvz/ve/Pw8++GCv4gYAAAAAAAAAgMGiLEVKb3jDG1IqlXLJJZdkxYoVPd7vhRdeyKxZs1JTU5M3vvGNvTp2T17l1pNXwv29Rx55JP/93/+dYcOGZdasWRsUUA0fPjwnnnhijjnmmHR0dOTiiy/uVdwAAAAAAAAAADBYlKVI6dhjj824ceOydOnSnHDCCXnyySdfdp9FixblhBNOyJ///OfssMMOOfbYY3t17F122SVJ8uyzz27yaUqLFy/eYNuXc/fdd6dUKmXy5MmZNGnSRrd53etelyS5//77tyxgAAAAAAAAAAAYZMpSpFRbW5sLLrggQ4cOzcKFC3PUUUfl7LPPzq9+9assW7YsnZ2d6ezszLJly3LbbbflU5/6VN72trdl4cKFGTZsWD73uc+ltra2V8eeOnVqhg8fns7Ozk0WDN19991Jkr333rtHfa5atSpJUlNT87LbrnuVHAAAAAAAAAAAsHHDytXRa1/72vznf/5nzj777LS3t2fOnDmZM2fOJrcvlUrZZptt8h//8R85+OCDe33cESNG5OCDD86tt96a6667Lvvss88G7YsWLcqCBQuSJIcddliP+tx1113X7/vUU09l55137rbNb37zmw22BQAAAAAAAAAANq4sT1Ja54gjjsgPf/jDvOlNb0pNTU1KpdJG/9TU1ORNb3pTfvCDH+TII4/c6uOefPLJqampyY033phrr702pVIpSbJs2bKcccYZWbt2bQ455JDsueeeG+x3/PHHZ9q0afnOd76zwecHH3xwtt9++7z44os5/fTT8+ijj65v6+rqyre+9a31BVhHH330VscPAAAAAAAAAADVrGxPUlpnt912y6xZs7J8+fLceeedefTRR7NixYokyejRo7P77rvngAMOSFNTU9mOuddee+Wss87KF77whZx77rm57LLLMmbMmDz22GPp7OzMrrvumvPPP7/bfkuXLs0zzzyTlStXbvB5fX19Lrroopx88sl56KGH8ta3vjUTJkzItttum8WLF69/Hdyb3/zmvOc97ynb9wAAAAAAAAAAgGpU9iKldZqamvKWt7ylqO67OeGEEzJlypRcfvnluf/++/Pcc89lwoQJOeyww3LiiSdmxIgRW9TfQQcdlJ/85Ce58sorc/vtt+fpp5/O0qVL09jYmFe/+tU55phj+vT7AQAAAAAAAADAQFVYkVIlHHTQQTnooIN6vP38+fM32z5x4sT8n//zf7Y2LAAAAAAAAAAAGNSGVDoAAAAAAAAAAACgum11kdJHP/rRPPDAA+WIJUmyZs2afPvb387VV19dtj4BAAAAAAAAAIDK2erXvf3qV7/Kr3/967zhDW/I8ccfn4MPPjhDhmx57dMzzzyTG2+8Md///vfz/PPP59RTT93a0AAqauXqzrS2dWRVe1dG1A9P48i6jGqorXRY/Y7zBDB4WQOgepnfAPQX1iQAAID+Y6uLlK644op84QtfyK233prbbrstY8eOzWGHHZZ99tkne+21V3baaaeN7tfe3p4HHngg9913X+bPn5+WlpYkyfDhw/OBD3wg73vf+7Y2NICKWb6iPbOua0nLwuXrP2ue0pTTZjSnaXR9BSPrX5wngMHLGgDVy/wGoL+wJgEAAPQvW12kdNBBB+WGG27InDlz8q1vfStPPPFErr766vWvaxsxYkTGjBmTxsbG1NbW5q9//WtWrFiRF154IWvXrk2SlEql1NXV5S1veUtOOeWUTJw4cWvDAqiYlas7u90AS5KWhcsz67qWnDlzX39jL84TwGBmDYDqZX4D0F9YkwAAAPqfrS5SSpKampq8/e1vz9vf/vbceeedufbaa/O73/0u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AAAADBCFPEmpo6Mjn//85/PJT34yxxxzTD74wQ9mr732yogRIza734QJE4oIBwAAAAAAAAAAqKBCipSmT5++/p//+te/5tOf/vTL7lNTU5OHHnqoiHAAAAAAAAAAAIAKKqRIqVQq9ck+AAAAAAAAAABA/1dIkdK8efOK6BYAAAAAAAAAABiACilSmjhxYhHdvqwFCxbkiiuuyH333ZfVq1dnwoQJOeyww3LiiSemoaGh1/3+6le/yvXXX5977703K1asyLbbbptJkyblgAMOyGmnnZZhwwo5jQAAAAAAAAAAUBWGVDqAcrnqqqtywgkn5LbbbktdXV3+4R/+Ic8880wuu+yyvOMd78iKFSu2uM8XX3wxZ555Zk488cT84he/yNChQ7PnnnumoaEhDzzwQL7+9a+no6Oj/F8GAAAAAAAAAACqSJ89AuiZZ57Jc889l5qamowdO7asT1t64IEHcsEFFyRJzjvvvMyYMSM1NTVZunRpTjrppDz44IM555xzMmvWrC3q99///d8zd+7c7Lnnnjn//POz1157rW9rb2/P7bffntra2rJ9DwAAAAAAAAAAqEaFFiktW7Ys3/jGN3LTTTd1e5LR6NGjc+SRR+YjH/lIdthhh606zqWXXpq1a9fm6KOPznHHHbf+83HjxuXLX/5yDj/88Nxyyy155JFHsueee/aozwULFuT666/PDjvskCuvvDKjR4/eoL2+vj7Tp0/fqrgBAAAAAAAAAGAwKOx1b3fffXeOOuqofP/7388LL7yQUqm0wZ8XXngh3/ve9/K2t70t99xzT6+Ps2rVqvzmN79JksyYMaNb+y677JIDDzwwSXLzzTf3uN/vfOc7SZIPfehD3QqUAAAAAAAAAACAnivkSUrPPfdcTj755LS2tmbkyJF517velde+9rUZN25ckmTp0qW5/fbbc+211+aFF17ISSedlJ/+9KfZbrvttvhYDz/8cDo7O1NbW7vB69j+3j777JPbb7899913X4/67OjoyG9/+9skyfTp03P//fdnzpw5efLJJ1NXV5d//Md/zDve8Y7suOOOWxwvAAAAAAAAAAAMNoUUKV1++eVpbW3NbrvtliuuuGJ9cdI6u+22Ww466KDMnDkzH/jAB/LEE0/kiiuuyCc+8YktPtYTTzyRJJkwYUKGDx++0W0mTZq0wbYv55FHHklXV1caGhry85//PF/60peydu3a9e233nprvvnNb+YLX/hCDj/88M32tWTJks2+Fm7evHk9iglgU+QZoC/INUDR5BmgaPIMUDR5BgAAYPMKed3br371q9TU1OT888/vVqD098aNG5fzzz8/pVIpt912W6+O1dramiRpbGzc5Dbr2tZt+3KWL1+eJOns7MyFF16Y5ubmzJkzJ3/4wx/y85//PIcddljWrFmTM888M4888kiv4gYAAAAAAAAAgMGikCcpPfPMM6mvr88+++zzstvus88+qa+vzzPPPNOrY3V0dCTJJp+ilCS1tbUbbPtyVq1alSR58cUXM2bMmHzjG9/IyJEjkyS77LJLvvKVr+TJJ5/Mww8/nMsuuyxf/epXN9nX+PHj/Q0ZoFDyDNAX5BqgaPIMUDR5BiiaPAMAALB5hTxJKUlKpVJRXW+grq4uSdLV1bXJbTo7OzfYtqd9Jslxxx23vkBpnSFDhuSEE05Ikvz2t7/d4FVwAAAAAAAAAADAhgopUpo4cWLWrFmTe++992W3bWlpSXt7eyZOnNirY/XkVW49eSXcxvpMkt12222j26z7vK2tLStWrOhRvwAAAAAAAAAAMBgVUqT0L//yLymVSjnnnHPy/PPPb3K75557Lueee25qamryute9rlfH2mWXXZIkzz777CafprR48eINtn05f1+YtKmnL/39556kBAAAAAAAAAAAmzasiE4/9KEP5Yc//GEee+yxHH744Tn++ONz0EEHZdy4campqcmSJUvyP//zP7n22muzYsWKbLvttvnQhz7Uq2NNnTo1w4cPT2dnZ+6///7ss88+3ba5++67kyR77713j/ocN25cJk6cmGeeeWZ9gdP/9tRTTyVJamtrM3r06F7FDgAAAAAAAAAAg0EhT1LafvvtM3v27IwaNSqtra35r//6r5xwwgk5/PDDc9hhh+UDH/hAvvGNb6wvUPra176W7bbbrlfHGjFiRA4++OAkyXXXXdetfdGiRVmwYEGS5LDDDutxv4cffniS5IYbbtjok5J+8IMfJEn233//DBtWSK0XAAAAAAAAAABUhUKKlJK/Fe/MnTs3xx13XLbddtuUSqUN/my77bY5/vjj8+Mf/zj77bffVh3r5JNPTk1NTW688cZce+21KZVKSZJly5bljDPOyNq1a3PIIYdkzz333GC/448/PtOmTct3vvOdbn1+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yZIccsghm93uwgsvzN13352ddtopP/nJTzJ37tz84he/yKWXXpra2tp885vfzPz58/soagAAAAAAAAAAGLgGVZHSXXfdlWuuuSZvetObMn369E1u95e//CXXXHNNkuRzn/tcdtttt/Vt06dPz4c//OEkyezZs4sNGAAAAAAAAAAAqsCgKVLq6OjIpz/96TQ0NOScc87Z7Lbz589PV1dXJk+enAMPPLBb+7ve9a4kyYMPPpjFixcXEi8AAAAAAAAAAFSLQVOk9LWvfS1PPPFEzjjjjIwbN26z2957771Jkn322Wej7ePGjctOO+20wbYAAAAAAAAAAMDGDat0AH3h4Ycfzre//e3stddeefe73/2y2y9atChJMnny5E1uM2nSpDz99NN54oknNtvXkiVLNvtquXnz5r1sPACbI88AfUGuAYomzwBFk2eAoskzQNHkGQBgoKv6Jym99NJLOfvss5Mk559/foYMefmv3NramiRpbGzc5Dbr2v7617+WIUoAAAAAAAAAAKheVf8kpW9/+9t58MEH8+EPfzh77rlnj/bp6OhIkgwfPnyT29TW1iZJ1qxZs9m+xo8fr3IdKJQ8A/QFuQYomjwDFE2eAYomzwBFk2cAgIGuqp+ktGjRosyePTs77bRTTj311B7vV1dXlyTp6ura5DadnZ1Jkm222WbrggQAAAAAAAAAgCpX1UVKn/nMZ9LR0ZF///d/T319fY/323bbbZP8v9e+bcy6tnXbAgAAAAAAAAAAG1fVr3t78MEHU1NTk7POOqtb27rXtC1ZsiSvfe1rkySzZs3Kq1/96uyyyy6555578uSTT26y78WLFydJdtlll/IHDgAAAAAAAAAAVaSqi5SSpFQq5S9/+csm29euXbu+fd3r3fbee+/MmTMn99xzz0b3Wbp0aZ5++un12wIAAAAAAAAAAJtW1UVKd9111ybb5syZk0996lOZOHFi5s+fv0Hb9OnTc/7552fRokVZsGBBDjzwwA3ar7nmmiTJ1KlTM3ny5PIHDgAAAAAAAAAAVWRIpQPoj7bffvscd9xxSZKzzz47f/rTn9a3zZ8/P9/61reSJKecckpF4gMAAAAAAAAAgIGkqp+ktDXOPPPMPPjgg2lpacmRRx6ZPfbYI6tXr87ixYuTJB/84AdzyCGHVDhKAAAAAAAAAADo/xQpbcI222yT7373u7ny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6A3jBgAAitVnT1JaZ+zYsbnwwgtzzDHH5DOf+UzOP//8zJ07N+edd15e8YpX5Etf+lJKpVKee+659dsPGVLeWqpSqZTnn38+d999dy666KIkyQc/+MHsvPPO67dZtGhRkmTy5Mmb7GfSpEl5+umn88QTT2xymyVLlmT69OmbbJ83b94WRg/0d61tHd1uZKzTsnB5Wts6MqqhdqPtvTFQ80xfnydg6wzUXEP/ZA1gY+SZ6mB+05/JMzC4VGJNkmcGPv8tQ38nzwAAA12fFymt85rXvCY33XRTZs+encsvvzzHHntsPvShD+WUU05JbW1ttt9++7If88Ybb8wnP/nJDT7bbbfdctFFF+Wtb33rBp+3trYmSRobGzfZ37q2v/71r2WOFBjIVrV3bVX7YOE8AQxe1gCoXuY3AP2FNYneMG4AAKBYFStSSpLa2tqcccYZOfLII3Puuefmv/7rv/Kzn/0s//7v/57XvOY1ZT/edtttl1e/+tUplUr585//nKVLl2bRokX58Y9/nP322y877rjj+m07OjqSJMOHD99s/EmyZs2aTW4zfvx4leswyIyo33Te6En7lhqoeaavzxOwdQZqrqF/sgawMfJMdTC/6c/kGRhcKrEmyTMDn/+Wob+TZwCAga6871HrpVe84hW55pprcu655+aFF17Ihz70oXzyk5/M888/X9bjHHzwwfnv//7vXHPNNbnttttyyy23ZNq0afnVr36VGTNmZOXKleu3raurS5J0dW36b0Z0dv7t/dPbbLNNWeMEBrbGkXVpntK00bbmKU1pHFnXxxH1T84TwOBlDYDqZX4D0F9Yk+gN4wYAAIrVp0VKbW1tmT17do4//vgceuihOf744zN79uz1xUHvfve787Of/SxvfvObM3fu3Bx++OGZM2dOYfHsvPPOueSSS7LHHntk6dKl+d73vre+bdttt03y/177tjHr2tZtC5Akoxpqc9qM5m43NJqnNOX0Gc3eW/9/OU8Ag5c1AKqX+Q1Af2FNojeMGwAAKFafve7t+eefz/HHH5/FixenVColSZ588snce++9+fGPf5yrr7462223Xbbffvt89atfzW233ZbzzjsvZ599dm644Yacd9552WWXXcoe19ChQ/Mv//IvefTRR/PAAw+s/3yXXXbJPffckyeffHKT+y5evHj9tgB/r2l0fc6cuW9a2zqy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fO655/KNb3wjzzzzzDrTV69enfvuuy9f+9rXkiQf/OAHs88++9TP32677XLSSSfV7++FF16onzdu3LjceOONSZKzzjpri44FAAAAAAAAAADaujb/ureVK1fmV7/6VX71q1+lV69e2WmnndKxY8fMnj07CxcuTJIMGjQoV111VYN1zz///EyfPj1Tp07Ncccdlz322CNLly7N7NmzkySnnnpqjjzyyBY9HgAAAAAAAAAAKDdtPkmpT58+OffcczNt2rQ8//zzmTVrVmpra9OzZ88cfvjhOe6443LcccelY8eODdbt0qVLbr311owaNSpjxozJzJkzU1lZmQMPPDBDhgzJ0UcfXYIjAgAAAAAAAACA8tJiSUp33XVXxo0blx122CGXXnrpRpcvFAq5/PLL88orr+Too4/O8ccf36T9brXVVjnjjDOatG6SVFVVZejQoRk6dGiTtwEAAAAAAAAAAO1ZiyQpLVq0KFdccUUWL16cn//855u0TkVFRY455pgMGTIkkydPzuDBg9OtW7cilxQAAAAAAAAAAGhuHVpiJ7/73e+yaNGifOhDH8qgQYM2eb39998/gwcPzoIFC/LAAw8UsYQAAAAAAAAAAECxtEiS0iOPPJKKior893//92av+/GPfzyFQiEPPfRQEUoGAAAAAAAAAAAUW4skKc2YMSNJcvDBB2/2ugcccECS5Omnn27WMgEAAAAAAAAAAC2jRZKU3njjjXTv3j3du3ff7HXXrDd//vwilAwAAAAAAAAAACi2FklSWr16dQqFwhZvAwAAAAAAAAAAKD8tkqTUq1evLF26NIsXL97sdRcvXpwlS5akV69ezV8wAAAAAAAAAACg6FokSenf/u3fkiQTJkzY7HXXrLPbbrs1a5kAAAAAAAAAAICW0SJJSgcffHAKhUJuvPHGzV73xhtvTEVFRQ455JAilAwAAAAAAAAAACi2FklS+vjHP56uXbvmySefzPe+971NXu973/tepk2bli5duuS///u/i1hCAAAAAAAAAACgWFokSWmbbbbJF77whRQKhdxyyy057bTTMnXq1EaXnzJlSk499dTccsstqaioyNChQ7Ptttu2RFEBAAAAAAAAAIBm1qmldvTFL34x//jHP3Lfffdl/PjxGT9+fLbeeuv8+7//e3r16pUkWbBgQZ555pnMnz8/SVIoFHLsscfmzDPP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ylx00UUZM2ZMkmSHHXZI7969s2DBgrzyyiupq6vLlClT0r179/p1itZmCpS9p556qjBgwIBCTU1NYfTo0YXVq1cXCoVC4ZVXXin813/9V6F///6Fs88+e7O2effddxfGjx9fWLFixTrTZ82aVfjMZz5T6N+/f+Gggw4qLFmypNmOo9wUo97f6Zvf/Gahf//+hTPPPLPQv3//woc+9KHmKHrZK0bdX3vttYX+/fsXLrjggmIUueyJM6UhzpSOONOyfv3rXxc+//nPF66++urCAw88ULj++usL/fv3L/zXf/3XBtf7zne+U+jfv3/hiCOOKDz//PP10//4xz8W9tprr0L//v0LY8eObbDe0qVLC+9///sL/fv3L1x00UWF5cuXFwqFQmH58uWFCy+8sNC/f//C4YcfXli2bFnzHmg70tRz+qEPfajQv3//woQJEzZrf84prZW+sPn+8pe/FI444ojCqFGjCq+99to68+6+++76+H7VVVc1WLe9jgtbUmftua2Vq6bGlQ1pifsOKEbbbUv3WO5BKVfGJYqpFPcGsCU29bN27ZPmpu2xKS655JJC//79Cx/96EcLTz755Drzli5dWvjjH/9YqK2tXWd6sdqMJKU24Iwzzij079+/8PWvf73BvBdffLEwYMCAQv/+/QszZsxolv3Nmzev0L9//0L//v0LDz/8cLNssxwVu94ff/zxQk1NTeGss84q/OY3v5E8sJZi1L0PbjZMnCkNcaZ0xJnSWtMeN/Sh5rx58wrvec97Cv379y889thjDeZfc801jW7j5ptvLvTv37/w4Q9/uEGi5IoVKwpHHnlkoX///oVbb711yw+GQqGwaee0UGj6l+XOKeVCX9i4RYsWNfhAZG3XXXddoX///oUDDzywsGrVqvrp7XlcaGqdFQrtu621FZsaVzakpe/3oFBonrbblu6x3IPSVhiXaE6luDeAptrUz9q1T5qbtsemeOyxxwr9+/cvHHrooYU33nhjk9YpZpvp0CzPhqJklixZkj//+c9JkhNPPLHB/F133TUHH3xwkuSBBx5oln1ut9129Y9SXb58ebNss9wUu95XrFiRSy+9NN26dctll122ZYVtY0rR5ts7caY0xJnSEWfKw7hx41JXV5d+/frVn4+1ffKTn0ySTJ8+vf7Ro2usOW//9V//1eBx4FVVVTnhhBOSJL/73e+KUXSKwDmFt7SFvlBdXZ3KyspG5x9++OFJkgULFmT+/Pn109vzuNDUOtsS5V5nvM21L5Sefghv0x9YWynuDaApNuezdu2T5qTtsaluueWWJG+97nlTX5lbzDbTabOWptWZMWNGamtrU1VVlX322We9y+y///4ZP358nnzyyWbZ5/PPP58FCxakQ4cO2XPPPZtlm+Wm2PX+4x//OC+++GIuu+yybL/99lta3Dal2HX/zDPP5Gtf+1rmzZuX7t27p6amJscee2z22GOPLS162RJnSkOcKR1xpjxMmzYtyVvnYn2233779O3bN3PmzMm0adOyyy67JElWrVqVv/3tb0mSQYMGrXfdNdOfeuqprFq1Kh07dmzm0rMxo0ePzs9+9rMsX7482223XQYNGpTjjz8+1dXVDZZ1TmnL9IWGVqxYUf//Ll261P/fuNC4xupsbdpa+1WK+z1obuV+j+UeFN5mXGJzNPe9ATTV5nzWrn3SnLQ9NsWKFSvy6KOPJkkGDx6cv/71r7nrrrsya9asdO7cOXvttVc+/vGPZ4cddlhnvWK2GUlKZe7FF19Mkuy0006NZpSvaRBrlm2KQqGQ+fPnZ/Lkybn66quTJKeeemp23nnnJm+znBWz3mfMmJGbbrop++yzTz796U9vWUHboGK3+RkzZmTGjBn1f48bNy7XX399PvvZz+aCCy5olx+uizOlIc6UjjhTHmbOnJkk6devX6PL7LLLLpkzZ8465+mll15KXV1d/fzG1kuS2travPzyy+02DpXS/fffv87f9913X370ox/lBz/4Qd7//vevM885pS3TFxr67W9/myQZMGDAOgk0xoXGNVZna9PW2q+Wut+DYir3eyz3oPA24xKbo7nvDaApNvezdu2T5qLtsameeeaZ1NXVpVu3bnnwwQfzgx/8IKtXr66f/9BDD+WGG27IlVdemWOOOaZ+ejHbjCSlMrdw4cIkSc+ePRtdZs28NctujnvvvTdf//rX15m222675eqrr87xxx+/2dtrK4pV76tWrcoll1ySJPnOd76TDh28kfGdilX32223XU4//fQcddRR2XnnnVNdXZ0XX3wxt99+e0aPHp1Ro0alsrIy559//pYdQBkSZ0pDnCkdcaY8bM55evPNN+unLViwoP7/jT3WdO1tLly40BerLWjgwIH54he/mP333z877bRT6urqMnny5Fx77bV5+umnc8YZZ+SXv/xl3vOe99Sv45zSFukL6zd9+vSMHj06STJs2LB15hkX1m9DdZZoaxT/fg+Kqa3cY7kHhbcZl9hUxbg3gM3VlM/atU+ag7bH5pg3b16St35IdtVVV2X//ffPJZdckj322CMvv/xyfvjDH+aBBx7I+eefn3e/+90ZMGBAkuK2GUlKZW7N4yw39F7eqqqqdZbdHNtuu23222+/FAqFvPLKK3n11Vczc+bM/N///V8OOOCABo/9ai+KVe833XRTpk+fntNPP70+ALCuYtX9pz71qQbTampq8u1vfzt9+/bN1VdfnVtuuSWf+tSn0rdv380sdXkTZ0pDnCkdcaY8bM55Wr58ef202tra+v83tu6a9d65LsX3gx/8YJ2/u3btmg996EM55JBD8ulPfzrTp0/P1VdfnZtvvrl+GeeUtkhfaOi1117L2Wefnbq6unz4wx/Oscceu85840JDG6uzRFuj+Pd7UExt5R7LPSi8zbjEpijWvQFsrqZ81q590hy0PTbHkiVLkiQrV67M1ltvnZ/+9Kf1TyDcdddd88Mf/jCzZs3KjBkzct111+VHP/pRkuK2GY9PKHOdO3dOkvpHra/Pmg8Q1yy7OQ499ND88pe/zOjRo/Pwww/n97//fY444og88sgjOfHEE7No0aKmFbzMFaPeZ86cmREjRqRv3745++yzt7yQbVSx2/z6nHrqqXnXu96VlStX5qGHHmqWbZYTcaY0xJnSEWfKw+acpy5dutRPW/tL08bWXfvL17XXpXS6dOmSc889N0kyYcKEdX6Z4ZzSnrTXvrBo0aIMHTo0L7/8ct7znvfkyiuvbLCMcWFdm1JnG9Je21p7VIprX2gJ5XSP5R4U3mZcYmOKeW8Am6Opn7Vrn2wpbY/NtfY100knnbTOK1KTpEOHDvn85z+fJHn00UfrXwVXzDYjSanMbcqjTTflUVybauedd861116bPfbYI6+++mp+8YtfbPE2y1Ex6v2b3/xmVqxYkW9961vp2rXrlheyjWrpNp8kHTt2zHvf+94kb79/sz0RZ0pDnCkdcaY8bLXVVkk27TytWTZZ95yt/bqa9a33zuUprf322y9Jsnr16syePbt+unNKe9Pe+sKSJUty+umn5+mnn84ee+yRm266qcGHKYlxYW2bWmcb097aWntVimtfaAnldI/lHhTeZlxiQ4p9bwCbo6mftWufbCltj8219jXTbrvttt5l1kxfvHhx/Wc9xWwzXvdW5nbdddckycsvv5y6urr1Pm5rzYeJa5bdUh07dsxhhx2W5557Ln/729+aZZvlphj1Pn369FRUVOTCCy9sMG/NI9Lmzp2b97///UmS4cOH139o3J6Uos0nbz/KbuXKlc22zXIhzpSGOFM64kx52HXXXTNlypTMmjWr0WXWd5769OmTysrK1NXVZfbs2dl+++0bXa+qqio77bRT8xacJlu7L65atar+/84p7U176gvLli3LF77whUybNi277rprbr755my99dbrXda48JbNqbONaU9trT0r1bUvtIRyucdyDwpvMy7RmJa4N4DN0dTP2rVPtpS2x+ZaOzGpsSdRrj19zZOUitlmPEmpzO25556prKxMbW1t/vrXv653mcmTJydJ9t1332bb75qb1zWNtL0pVr0XCoW89tprDf4tXrw4yVv1vWbahh6t1paVqs0/99xzSZIddtih2bZZLsSZ0hBnSkecKQ9r6n7KlCnrnf/qq69mzpw56yybJJ06dcpee+2VJHniiSfWu+6a6XvvvXc6duzYTCVmS/3973+v///aX4g7p7Q37aUvrFixImeeeWYef/zx9OnTJ6NGjUrv3r0bXd64sPl1tjHtpa21d6W69oWWUC73WO5B4W3GJdanpe4NYHM15bN27ZPmoO2xObbffvv06dMnSdZ5Uvba/vnPfyZ568dmvXr1SlLcNiNJqcx17949hx56aJLkjjvuaDB/5syZmTBhQpLkIx/5SLPss7a2Ng8//HCSt24a2qNi1PsTTzyRZ599dr3/rrjiiiRv/WJ1zbSDDjqomY6mvJSizT/88MP1H9ysyTxuT8SZ0hBnSkecKQ+DBw9OZWXlOudjbaNHj07yVgzp16/fOvOOPvroJMndd9/dIBmvtrY2d911V5LmO780jxtvvDFJsvvuuzf4IsU5pT1pD32hrq4u55xzTsaPH58ddtgho0aN2ugXqO19XGhKnW1Me2hrlObaF1pCOd1juQeFtxmXeKeWvjeATdXUz9q1T7aUtkdTHHPMMUmSe+65Z70Ph7jzzjuTJAceeGA6dXrrZWzFbDOSlNqAM888MxUVFbn33nvzq1/9KoVCIUnyr3/9K1/96lezevXqHHnkkRkwYMA6633qU5/KEUcckVtuuWWd6U899VSuueaa9b6P/MUXX8wZZ5yR2bNnp1u3bjnxxBOLdVitXnPXO5uuuev+ueeeyze+8Y0888wz60xfvXp17rvvvnzta19Lknzwgx/MPvvsU7wDa8XEmdIQZ0pHnGn9tttuu5x00klJkksuuSQvvPBC/bxx48bVf7F61llnNVj3pJNOSu/evTNr1qz6d3gnb/0y75vf/GZmz56dd73rXfnEJz7RAkfCGjfddFN+/vOf54033lhn+htvvJFvfOMbeeCBB5Ik55xzToN1nVPakvbeF1atWpXzzjsvjzzySHr37p1Ro0Zl55133uh67XlcaGqdtfe21t5s6B6hqde+0BLayz2We1DaG+MSm6IU9wZQbNonpaLttW+nnXZaevTokeeffz7/8z//k9ra2iRvPZVr1KhReeihh1JRUZFhw4bVr1PMNlNRWHOFR1m75ZZbcuWVV6ZQKGTHHXfM1ltvnX/84x+pra3Nu9/97tx+++3ZZptt1lnniCOOyEsvvZSzzz57nQ8dJ06cmM9+9rNJkm222SY77rhjOnXqlHnz5uXll19OkvTq1SvXXHNNDjnkkJY7yFaoOet9Q+66665cdNFF6dOnT8aNG1eMQyk7zVn3M2bMyMc+9rEkb7XtnXbaKR07dszs2bOzcOHCJMmgQYNy3XXXZauttmqxY2xtxJnSEGdKR5xpOXPnzq2vn+StJzAsXbo0nTp1SnV1df30008/PUOHDq3/e/ny5fn85z+fqVOnpmPHjtljjz2ydOnS+keWnnrqqbngggvWu8/Jkyfn9NNPz9KlS9OzZ8/07ds3c+bMycKFC9OtW7fcfPPNHmu7BZpyTi+//PLceuutqaioSJ8+fbLNNttk+fLleeGFF7Jy5cp06NAhX/3qV9dpA2tzTmmN9IXNt/YXp3369FnnNWPvdNlll63z1M32Oi40tc7ae1srV029btrYPUJTrn1hczR3222L91juQSlHxiWKqVT3BrClNvZZu/ZJsWh7bMj48eNzxhlnZPny5enZs2f69euXuXPnZt68eamoqMj555+f0047bZ11itVmOjXLEVFyn//851NTU5Of/exn+etf/5rXX389O+20Uz7ykY9k2LBh6d69+yZva8CAAbn00kszadKk/P3vf8+sWbOyfPnyVFdXZ//9989hhx2Wk046yU1Amrfe2TzNWfd9+vTJueeem2nTpuX555/PrFmzUltbm549e+bwww/Pcccdl+OOOy4dO3Ys4hG1fuJMaYgzpSPOtJxVq1ZlwYIFDaavXLlynenLly9fZ36XLl1y6623ZtSoURkzZkxmzpyZysrKHHjggRkyZEj9q2jWZ//998+9996bkSNH5i9/+Uv+/ve/Z+utt84JJ5yQM888c5N+mUfjmnJOjz322BQKhTz11FN5+eWX88wzz6Rjx47p27dvDjzwwHz605/Ov//7vze6T+eU1khf2HxrfsmVJC+99FJeeumlRpddtGjROn+313GhqXXW3ttauWrqddPGuO+g2Jq77bbFeyz3oJQj4xLFVKp7Ayg27ZNS0fbat/e97325995785Of/CTjx4/PjBkzUl1dnSOOOCKnnHJKDjzwwAbrFKvNeJISAAAAAAAAAABQVB1KXQAAAAAAAAAAAKBtk6QEAAAAAAAAAAAUlSQlAAAAAAAAAACgqCQpAQAAAAAAAAAARSVJCQAAAAAAAAAAKCpJSgAAAAAAAAAAQFFJUgIAAAAAAAAAAIpKkhIAAAAAAAAAAFBUkpQAAAAAAAAAAICikqQEAAAAAAAAAAAUlSQlSuKII45ITU1N7rrrriZvo7a2NkceeWT22muvzJ07txlLV/6WLFmSgw8+OAcccEDeeOONJm/nrrvuSk1NzTr//vM//7MZS1o+Jk6cWF8Hze2Pf/xjg3o+4ogjmn0/rJ941JAY0vzaSgy58MILU1NTkwsvvLDZt90cfbE1Ou+881JTU5P777+/1EVpdU477bTU1NTksccea/I25syZ06D919TU5M0332zGkpa34cOHp6amJieffHKzb/vyyy9vUPfFiA/rIx5tvrYWj8SQltFWYwjl6bHHHktNTU2GDh1a6qK0Or/97W9TU1OTr3/961u0nZNPPrlBv7zllluap5Ab8eabb643Js+ZM6dF9g+bo63FIzEEACiV9Y3fM2bMKMq+fA7RenQqdQHaskKhkAceeCD33Xdfnn766bz++uvp2LFjtt122/Tu3Tv77LNPBg0alEMOOSTV1dWlLm7Z+cUvfpF//vOf+fSnP50dd9xxnXlz5szJ4MGDkyRXXHFFTjjhhFIUsWS6d++eU089NT/4wQ/y4x//OJdeeukWba9Dhw7ZZpttkiRbb711cxRxi8yYMSMf+9jHssMOO+SRRx4pdXG2WFVVVbbbbrskyfLly7N48eJm34d4VFwbikfvNGnSpNx33315/PHHM2/evKxYsSLbbLNNampq8sEPfjAnnHBCunTp0kIlXz8xpLy0RAxpzdYe8zfmndcEF154Ye6+++706dMn48aNa/ayPfXUU7nvvvvSv3//HHPMMQ3mH3HEEXnppZfyX//1X7nyyiubff+t3TnnnJNHH3003//+9/Ob3/wmHTps2e8ntt5663Ts2DFJtnhb79Rc15a33HJLrrjiinz0ox/NVVdd1ZxFLInq6ur6+LNo0aKsWLGixCUqrXKOR2ubNWtW7rzzzowfPz4vvfRSFi9enJ49e2bnnXfOoYcemk984hPZfvvtm72Mm0sMEUPK2apVq/Lggw/m4YcfzpNPPpnXX389y5cvT48ePbLrrrtm0KBBOf7449O/f/9SF7XVWL16df310jnnnNNg/vDhwzNixIgkybPPPtuiZWsNjjnmmIwcOTJjxozJ5z73ubznPe/Zou1169Yt3bp1q///O60Zt9b41a9+lX333XeD2zzuuOPy3HPP1f/9zvPUoUOH+piwatWqLfrBDJtOPNp8G4tHa1uxYkXuvvvuPPzww3nmmWcyf/78VFZW5l3velcGDRqUY489NgcffHBLFHuDxBBoW97Zx5Lks5/9bC655JIWLcd//ud/5plnnllnWnv8vo72ZUt+pNze+0fPnj1TWVmZJOnUacMpLK+//np+85vf5LHHHsvzzz+fBQsW1H8PNWDAgHzgAx/IMccck6222mqd9drz5xCtjSSlInnzzTdz1llnZdKkSfXTOnXqlK5du2bu3Ln55z//mSlTptR/yNieg05TLFiwINddd12qqqryhS98odTFaZU+85nP5Gc/+1lGjx6dk08+Of369WvytnbccceifFnSVGPHjk2STf4SqDl07do17373u4uy7cMPPzx/+ctfkrz15JmLLrqoWbcvHhXXpsajN954IxdccME6STFVVVXp3LlzXnnllbzyyit55JFHcv311+eKK67I+9///pYofqPEkOZVzjFkbb1798673/3u9O7du2j72BLV1dUbTPJr6QTA733veykUCjnrrLNSUVHRovsuB/vuu28OPfTQPProoxkzZkw+9rGPbdH27rzzzvTt27d5ClckpYg/W2+9dd797ndvNIm2Kb785S/ny1/+cpL1fwhaTOLR5tmUeLRq1apcffXVufXWW7Ny5cokSceOHVNdXZ358+fntddey9SpU3PjjTfmnHPOyWmnndaSh9CAGNIy2moMKaVp06blggsuyMyZM+unVVZWpnv37lmwYEGmTJmSKVOm5Kc//WmOOuqo/OAHP0hVVVXpCtxK3H333XnmmWfywQ9+MPvss0+pi9PqdOjQIWeeeWa++tWv5nvf+15uvfXWLdreqaeeutHki7XdddddG0wwePLJJ9dJLlif6urq+vuazUn8penEo6bZ1Hj0l7/8JRdffHFeeeWV+mnV1dWpra3NCy+8kBdeeCF33HFHDj/88Hz/+98v6Y+6xBDYsLWToddWWVmZXr16paamJh/5yEfysY99rP4L9nfa3B8unHzyyZk0aVIOPPDA/PznP29SuTt37pwePXokyXp/mFxXV5ef/OQnuffeezN37tz06tUrH/zgB/OVr3wl22677QbL9sQTT+RXv/rVBuPg1ltvXZ8MMH/+/KxevbpJxwHlZE2bf6elS5dm6dKlG1ym1D9eL7Xhw4fnoIMO2uAyhUIhP/nJT3L99ddn2bJl9dO7deuWioqKvPTSS3nppZcyduzYXHXVVbnwwgvz8Y9/vH659vo5RGskSalIvv71r2fSpEnp2LFjPve5z+Wkk07KLrvskg4dOmTlypX5xz/+kT//+c+57777Sl3UsnTHHXfkzTffzNFHH50ddtih1MVplbp3757jjz8+t956a0aNGpVvfOMbpS5SsynFh/P77LNPHnjggRbbX3MSj4prU+LRa6+9lk9/+tOZNWtWOnbsmE9/+tP55Cc/md133z3JW4lkf/zjH3Pttddm7ty5GTZsWK666qr8x3/8R0seyjrEkOZVzjFkbV/72tfyta99rdTFaNQll1zSahItp02blscffzzbbbddPvzhD5e6OK3Wpz71qTz66KO58cYbtzjBoLVb80VPVVVVDj300Bbb75AhQzJkyJAW219LEY823abEo9WrV+ecc86pHyMPO+ywfOELX8h+++2Xjh07pra2NhMnTsyPf/zjTJ06Nd///vczd+7cLX7a4pYSQ4qvrcaQUhk3bly+/OUvp7a2Nr169cppp52Wo446KrvuumuSt5IFn3766fz+97/P7bffnt///vdZvny5pIAkN954Y5K3+j3rd/TRR2ebbbbJxIkT89RTT2Xvvfcu+j532mmnzJ07N7/97W9z8cUXN/rFym9+85skSZ8+ffLSSy8VvVxsnHjUdJsSj+6///6cf/75WblyZbbffvucc845Oeqoo9KzZ88kyfPPP59f/epXue222/KnP/0pJ510Un75y19uMCmg2MQQ2DRrJxcsWbIk8+bNy7x58/Loo49m9OjR+dnPflbf10vtP/7jPzb45O6vfOUr+cMf/pDkrS/4582bl1//+teZNGlSfvOb39QnOK3tN7/5TSZNmpSTTz55o4nja7/ucc3TxKGtW5Mw+05rJzs2tgwbVigUcv755+f//u//kiTvfe97c/rpp+fggw+uf2LS4sWLM2HChNx555156KGHMm7cuHWSlGg9mvcZ5iRJZs6cmYceeihJcu655+aCCy7IrrvuWv/I+E6dOmXAgAEZOnRo7r333pJ+CV2OCoVC7rjjjiTJRz/60RKXpnVbUz9jxoxZJ6O0nM2dOzdPP/10evTokQMPPLDUxWn1xKPi2pR4VCgU8rWvfS2zZs1KZWVlRowYkUsvvbQ+QSlJttpqq5xwwgm5++67M2DAgKxcuTKXXHJJnn/++RY5jsaIIdB0o0ePTpIce+yx9a8PoqHDDz88vXr1ynPPPZfJkyeXujhF9fDDD2flypU5+OCDvVqVFrUp8WjkyJH1CUrDhg3LjTfemAMOOKB++aqqqhx22GG5/fbb65OBfv7zn+eee+4pevk3RAyhnMycOTPnn39+amtrs/vuu+fee+/NsGHD6hMCkreeXrb33nvna1/7WsaOHespEP+/iRMn5oUXXsg222zTokl65aZTp071r/T81a9+1SL77Nu3bw444IAsXrw4Dz744HqXWb58ee6///5UVFS0+YTSciEeNd2mxKPnn38+l1xySVauXJn+/fvnnnvuySc+8Yl1khb+7d/+LRdffHFGjhyZysrKzJo1q+QJ+GIIbJq//OUv9f+mTZuWhx56KCeeeGKS5G9/+1u++93vlriEm+axxx7LH/7wh1RXV+e2227L1KlT88c//jF77LFHZs2atU6C0Rrz58/P97///eywww4599xzW7zMQPt2ww031Ccofe5zn8uvfvWrHHXUUeu80q26ujpHHnlkrr/++vziF7/woJNWTJJSEcyYMaP+/5tyA7e+XwgcccQRqampyV133ZXFixfnBz/4QY4++ujss88+Oeigg3LmmWfmySef3Oi2n3766Vx00UU58sgj8973vjcDBw7MRz/60fzwhz/M/PnzN7juihUrcuutt2bIkCE56KCDstdee+X9739/zjzzzPzpT3/a4LrLly/PyJEj8x//8R/ZZ599csghh2To0KF57LHHNlrmjRk/fnz++c9/Zquttsrhhx/epG2sXb+1tbW58cYb89GPfjT77rtv9t9//3z2s5/d6DGuWrUqd955Zz772c/W189hhx2WL33pS5k4cWKj65188smpqanJ8OHDU1dXl5/97Gc54YQTMmjQoNTU1Kyzbl1dXW655Zb853/+Z/bdd98ceOCBOfnkk+ufxrH2ttZn7733zq677ppFixbl/vvvb0JNbZrhw4enpqYmJ598cpK3nlLyuc99LgcddFD222+/fPKTn8wf//jHdda555578slPfjIHHHBABg4cmM985jOb1D7WbOfwww9f59Gpa9fFqlWrcsstt+RjH/tYBg4cmEMOOSRnnnnmOu8/XrZsWUaOHJnjjjsu++67bw466KCce+65mT179nr3O3HixNTU1Kz3fbJ33XVXampqcsQRRyR562bky1/+cg499NDstddeGTx4cK644oosXLhwo8fX3MSj0sejhx56KBMmTEiSfPGLX6xvJ+uz9dZb50c/+lE6d+6cpUuX5kc/+lGDZcSQt4ghpXHhhRempqYmF154YYN5a9fhmgS+T3ziE9lvv/0ycODAnHTSSbn33nubvO/rr78+NTU1+fd///f88pe/3JLDKLrFixfnd7/7XZLkuOOOa9I23lnXDzzwQE4++eQceOCBee9735v//M//zKhRozb6qOyJEyfmS1/6Ug477LDstddeOeigg/K5z30uv/nNb7Jq1ar1rvPOPvnggw/m1FNPzSGHHJIBAwY0iBl//OMf87nPfS6DBg2qj+033HBD6urqGmzrnaqqqnLUUUclSX3SZzG8s5+tz5w5c+r76Zw5c5q9DI09xW3NPidOnJg33ngjV1xxRY488sjss88++dCHPpT/9//+3zrj5EsvvZRvfvObOeKII7L33nvngx/8YK688sosXrx4vfvd0DlornZWCuLRptmUeDR//vzccMMNSZKDDjooX/3qVxvdXocOHfKd73wn//Zv/5Yk+d///d/U1taus4wYIoaUQwwphWuuuSaLFy9O586dM2LEiI1+WNqrV6+MHDlyvb9gnzdvXr73ve/l2GOPzcCBA7Pvvvvm2GOPzfe///289tpr693eO9voa6+9lu9+97v1beF973tfvvKVr2z0hxKrV6/O/fffnzPPPLP++uLggw/OCSeckKuvvjp///vfN71SNtGa/v2Rj3wknTpt/kPh33ktPmvWrFx00UX5wAc+kL322iuHH354Lr300rz66qsb3E5z1Pvs2bNz2WWX5Ygjjshee+3VIK689NJLufjii3P44YfXl+2iiy7KrFmzNinOrIn1v/3tb7NkyZLNraomWfPkwLvuumu98x988MEsWrQoBx54YKt/rWZ7IR413abEo2uuuSZLly5NVVVVfvSjH2WbbbZpdHsf+MAHcsYZZyR5K2Hg4YcfXme+GCKG0PrttNNO+c53vpNDDjkkSfK73/2uxfrPlhg/fnyS5MQTT8ygQYOSJDvvvHO+8pWvJFn/k16uuOKKLFiwIJdddpkfTkARLFq0KNddd10+8YlP5IADDshee+2VD3zgA/nqV7+aadOmrXedd47vL730Ui699NJ88IMfzN57750jjzwyP/zhD+tfNZckf//733PeeeflAx/4QPbee+8cddRRGTlyZOrq6ta7j7U/Z6utrc1Pf/rTHH/88dl3331zwAEH5JRTTskjjzxSjCqpN3/+/Fx33XVJkkMOOSQXXXRRKioqNrjOAQccUPIngNM4r3srsldeeaX+A9ymePPNN/Pxj388L774YiorK9O5c+csWLAgY8eOzUMPPZTvfOc7jT6m7Nprr83IkSNTKBSSJF27dk1dXV2effbZPPvss/nNb36Tn/70p9lzzz0brDtz5sx84QtfqH8neUVFRaqrq/Paa69l7NixGTt2bD71qU/lW9/6VoN1FyxYkFNOOSVPP/10krd+hbFy5cr86U9/yp///OctfmXQn//85yRvvTpnSx8xvHTp0gwZMiRPPvlkKisrU1lZmcWLF2fixImZNGlSvvvd7663fhctWpQzzzwzkyZNSvLWr4u6d++eefPm5cEHH6z/IP6CCy5odN8rVqzIySefnKlTp6ZTp07p3r17g7INGzYsjz/+eP0+qqqq8vjjj2fSpEkZNmzYJh3joEGDMnPmzPz5z3/Of//3f29q1TTZtddemx//+Mfp0KFDunfvniVLlmTq1Kk566yz8q1vfSuf/OQnc9FFF+Xuu+9Op06d6pMxnnjiiZx22mkZOXJkPvjBDza6/TUfzh955JHrnb9y5cqcfvrpGT9+fP05nT9/fsaOHZvHHnsst956a/r27ZtTTz01Tz/9dDp37pyKioosWLAgv/vd7zJp0qTceeed2WmnnZp0/P/3f/+Xiy66KHV1denRo0dWrVqVOXPm5JZbbslf/vKX/OpXv2pwrluKeFSaeHT77bcneev1aaeccspGt7nrrrvm2GOPzV133ZU//OEPmTdvXnr37t1gOTFEDGmtVq1albPOOitjx45Np06d0qVLlyxZsiTTpk3LtGnTMmvWrHzpS1/a5O2tXr063/3ud3Pbbbelc+fO+cEPftDqX582adKkLF++PN26dct73vOeLd7e//t//y+33XZbOnTokOrq6ixfvjzPPPNM/ud//idPP/10vve97613vSuuuKL+l28VFRXp0aNHFi1alAkTJmTChAkZM2ZMfvzjH2/wg6Urr7wyN998cyoqKrLVVlvVP4lvje9973v52c9+Vv/3Vlttleeffz5XX311Hnnkkey///4bPb5BgwbljjvuqI+pbdGKFSvy6KOPpqKiotEkh7lz5+brX/96XnnllXTr1i2rV6/Oyy+/nNtuuy0TJkzI6NGjM3PmzAwbNixvvPFGqqurs3r16sydOzc333xznnzyyfziF79o8pO7mtrOWjPxaNPi0Z133pnly5cnSc4+++yNfshTVVWVYcOG5YILLsirr76aP/7xj40+jVMMaR5iSPl77bXX6p8Qcfzxx+fd7373Jq/7zj45adKknHXWWXnzzTeTvHVvVVFRkX/84x/5xz/+kTvvvDMjR46s/7Jpff7xj3/k4osvzuuvv56uXbsmSV5//fXcf//9+dOf/pTbbrstAwYMaLDe/Pnz86Uvfan+HiNJevTokcWLF2f69OmZPn16XnjhhYwcOXKTj29jCoVCHn300STZ4DFtqgkTJuSMM87I0qVL07179xQKhbz66qv59a9/nUceeSR33nlntt9++wbrNUe9T506Nd/4xjeydOnSdO3adZ0fTayZf9ppp9V/sdmlS5csWrQod911V37/+99v0pMZ9t5773XukT7wgQ9sTvU0ydFHH53vfOc7mThxYubMmdMgiWBN4sEJJ5wgabEVEI+ablPi0b/+9a/6H0cdd9xx2W233Ta63c9//vO56aabsmTJktx2222NfqYhhoghtG6HHnpoHnvssdTV1WXWrFnr/Zy7NXnjjTeSJLvssss609c8Ve+dPyoeP358xowZkw9/+MONfq4KNN2TTz6ZM888sz7Ju2PHjunSpUteeeWV/Pa3v83999+fr3zlK/nCF77Q6DaefvrpXHLJJXnzzTdTXV2dVatW5Z///Geuv/76PPHEE/XfMZx77rlZtmxZevToUR+zfvSjH+W5557LD3/4w0a3X1dXl1NOOSVPPPFEOnXqlG7duuXNN9/M+PHjM378+Jx99tk555xzmr1ukreuB9YkWm3KZ1drvPNzKFoPZ6YI9t577/rOceWVV+bFF19s8rZGjBiR+fPn55prrsm0adMyefLk3H///TnwwAOzevXqfPOb38z06dMbrHfLLbfkxz/+cbp165avfe1refTRRzNt2rQ8+eST+c1vfpODDz448+bNyxlnnNEgq/vNN9/MqaeempkzZ+bggw/Obbfdlr/+9a954okn8sQTT+Siiy5Kt27d8stf/jKjRo1qsO9LL700Tz/9dKqqqvLtb387U6ZMyeOPP55x48blyCOPzP/8z/9s9KkpG7Lm5nNj77vdFNdee21eeeWV/PjHP87UqVMzderU/O53v8u+++6bQqGQyy+/PIsWLWqw3iWXXJJJkyalsrIyl156aSZPnpzHH398nS/xf/azn23wl9W33XZbnn322VxxxRWZPHlyJk2alAkTJtT/su/KK6/M448/ng4dOuS8886rTywYP358Tj755Pz0pz9d56kejXnve9+bJOvctBfLjBkzcv311+fcc8/NpEmT8sQTT+RPf/pT/eOPr7rqqgwfPjz3339/vv3tb+eJJ57IlClT8uCDD2avvfbKqlWr8u1vf7vRG84333wzTzzxRCorKxt9as3tt9+ep59+Oj/60Y8yderUTJkyJb/+9a+z8847Z+nSpbn88stz2WWXZeHChbnpppsybdq0TJ06Nbfccku22WabvP766/nf//3fJh3//Pnzc/HFF+djH/tYHn744frj+8Y3vpHKyso899xz9e+tbyniUWnj0cqVK+tfPXLooYducnLJmicCrF69utG+K4aIIa3V7bffnkmTJuXKK6/M5MmTM3ny5DzyyCP50Ic+lCS57rrr6pMON6a2tjbnnntubrvttmy11Va56aabWn1CQPJ2f9lzzz23+FVv48aNyx133JGL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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig= sns.pairplot(df, height= 3)\n", - "\n", - "for i, j in zip(*np.triu_indices_from(fig.axes, 1)):\n", - " fig.axes[i, j].set_visible(False)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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XMwOTgYAAAAAyYZB66lTpyo2NlYffvihQkNDVa9ePdO0xo0ba8mSJXYtEED+dPfylA9qAwBbdejQwdElAMjHDAaDxo8fr+eff17ff/+9Ll26pBIlSujRRx9VxYoVM72869ev6+TJk6bH8fHxiouLk6enp8qUKaPJkyfr7NmzmjhxoiSpa9euWrp0qaKiotSlSxcdOHBAq1ev1uTJk+32GgHkLB8fH/36669q0KDBffsdP348w7Oj76d3794aMWKEgoODVadOHa1YsUIJCQnq2rWrJKXLmXbt2mnmzJl68803NWjQIF28eFGTJk1Sx44dLV4aHAAAAED+lulB602bNmnw4MF67LHHTPd+vKtMmTJKSEiwW3EAAAA54Zdffrnv5TRXrVqlTp065WBFAPKLSpUqqVKlSlleTmxsrHr27Gl6fPdqEh06dFB0dLQSExPNfquVK1dOc+bMUVRUlJYuXSpfX1+NGjVKrVu3znItAByjYcOGWrZsmTp16pThFRNSUlK0fPlyNWrUKNPLb9u2rS5evKiZM2fq3LlzCggI0Jw5c1S2bFlJSpczRYsW1fz58zVhwgR17NhRJUqU0OOPP64hQ4bY9PoAAAAAOLdMD1pfuHBBVapUsTjNxcVFN27cyHJRAAAAOemVV17RihUrTDtd/2njxo16++23GbQGkGWXL1/WW2+9pcjISDVv3txinx07dmjNmjUaP368vLy8rF52o0aNdPTo0QynR0dHp2tr2LChPv/8c6ufA0Du1qtXL3Xq1EkDBgzQ+PHjVapUKbPpZ8+e1ejRo3XixAm9//77Nj1H9+7d1b17d4vTLOVM5cqVtWDBApueCwAAAED+kulB61KlSunYsWN69NFH0007evSo/P397VIYgGx2J1VK3CX9nSAV9pN8wiQXV0dXZeb333+Xq2taTXev7PD7779b7Mt9rgFkRWBgoPr27avly5erePHipvZt27bp9ddf17PPPuvA6gA4i88++0xHjhxRWFhYhn3CwsIUHR2tpUuXasCAATlYHYC8rlq1ahozZozGjRuniIgIBQcHmw7IO336tGJjY2U0GjV27FgFBgY6uFoAAAAAMJfpQetWrVrpo48+Uv369U0/cgwGg06fPq1PPvlEkZGRdi8SgJ2dWiPtHywlx/+vrYi/VG+aVC73rMNvvvlmurYRI0aYPTYajTIYDIqLi8upsgA4oWnTpqlbt27q37+/5s+fLzc3N+3Zs0dDhw7Vk08+qVGjRjm6RABO4Msvv1Tnzp1VoEDGP8MKFCigzp0768svv2TQGkCmdenSRVWrVtXs2bP1/fff6+DBg5KkwoULKywsTC+//LJq167t0BoBAAAAwJJMD1r3799f3377rTp37qyqVavKYDDozTff1MmTJ1WxYkX17ds3O+oEYC+n1ki7Okkymrcnn05rD1uVKwau796HMd/IA2e+A87Mw8NDc+fOVZcuXfTmm2+qa9euGjBggCIiIvTee+85ujzcRVYijztx4oRq1KjxwH5BQUGaOXNmDlSUh5EHQIbq1Kmjjz76SHfu3NHFixclSV5eXnJxcXFwZchxZCUAALAnvlsgm2V60NrDw0OffvqpFi1apJ07d+rhhx9W4cKF9fLLL6tXr14qVKhQdtQJwB7upKadYX3vgLX03zaDtH+IVPYph29sOnTo4NDnz1F55Mx3wNmVLl1as2fPVvfu3bVp0yaFhYXZfL9HZAOyEk4gNTX1vmdZ31WgQAHdvn07ByrKo8gDwCouLi4qWbKko8uAo5CVQP7DYBKQKcuWLdPy5ct1+vRpSVLVqlX16quvKjw8XFLaFT6nT5+uFStW6MqVK6pVq5bGjBmjqlWrOrJsx+G7BXJApgetJalQoULq27cvZ1UDeU3iLvONSjpGKflUWr9SzXKqqgf65Zdf7ntW0qpVq9SpU6ccrMiO8siZ74Az2rx5s8X2J554Qtu2bVPbtm21bds2U3urVq1yqjTci6yEk/Dx8dGvv/6qBg0a3Lff8ePH5e3tnUNV5THkAQA8GFkJ5D8MJgGZVrp0aQ0bNkwPP/ywJGnt2rXq37+/Pv/8c1WtWlVz587VggULFB0drQoVKmjWrFnq3bu3Nm3aJA8PDwdXn8P4boEcYtOgtSTdvHlThw4d0qVLl1SiRAkFBQWpYMGC9qwNgL39nWDffjnklVde0YoVK1S2bNl00zZu3Ki33347bw5a56Ez3wFnNGjQIBkMBhmNltZBacSIEaZpBoNBcXFxOVke7iIr4UQaNmyoZcuWqVOnTnJzc7PYJyUlRcuXL1ejRo1yuLo8gDwAgAcjK4H8h8EkwCYtWrQwezx06FAtX75cBw8eVJUqVbRo0SL169fPdBJDTEyMQkNDtWHDBnXt2tURJTsG3y2Qg2watF6wYIFmzpypa9euyWg0ymAwqGjRonr11Vf1wgsv2LtGAPZS2M++/XJIYGCg+vbtq+XLl6t48eKm9m3btun111/Xs88+68DqsiCPnvkOOItFixY5ugRYg6yEE+nVq5c6deqkAQMGaPz48SpVqpTZ9LNnz2r06NE6ceIEtyewhDwAgAcjK4H8hcEkwC5SU1O1adMmJScnq06dOoqPj1diYqKaNGli6uPu7q4GDRrowIEDGQ5aJyQkKCIiIsPn+ecV/fIMvlsgB2V60Hrx4sWKiYlR48aN1a5dO3l7eyspKUnr16/XpEmTVKBAAfXs2TM7agWQVT5haZcGSj4ty19mDWnTfcJyurL7mjZtmrp166b+/ftr/vz5cnNz0549ezR06FA9+eSTGjVqVKaWt2/fPs2bN0+xsbFKTEzUjBkz1LJlS9N0a+5XcuvWLcXExGjDhg26efOmHn30UY0dO1alS5e2vpA8euY74CwaNmzo6BJgDbISTqRatWoaM2aMxo0bp4iICAUHB5uuJHP69GnFxsbKaDRq7NixCgwMdHC1uRB5AAAPRlYC+QuDSUCWHD16VF27dtXNmzdVpEgRzZgxQ1WqVNGPP/4oSSpZsqRZf29vb505c8YRpToO3y2QgzI9aL1w4UI9+eSTmjhxoll7hw4dNGzYMC1atIhBayC3cnFNu5fNrk6SDDIfuDak/afeB7nuyEsPDw/NnTtXXbp00ZtvvqmuXbtqwIABioiI0HvvvZfp5SUnJyswMFCRkZEaOHBguunW3K/k3Xff1Y4dOzR16lSVKFFC0dHRevnll7VmzRq5ulr5/uXRM98BIEeRlXAyXbp0UdWqVTV79mx9//33OnjwoCSpcOHCCgsL08svv6zatWs7tMZcizwAgAcjK4H8hcEkIEsqVqyotWvX6sqVK9q8ebNef/11LVmyxDTdYDCY9c/oFnN3+fn55c2zqe+H7xbIQZketD537pzat29vcdpTTz2lzZs3Z7qIpUuXat68eUpMTFTVqlU1cuRI1a9f32LfH374Qe+//75OnDihv//+W2XKlFHXrl31/PPPm/X76quvNG3aNJ08eVIPP/ywhg4dqsceeyzTtQFOp1xk2r1s9g82PxKziH/agHUuvcdN6dKlNXv2bHXv3l2bNm1SWFiYzZfNDA8PV3h4uMVpRqPxgfcruXr1qlavXq2JEycqNDRUkjRp0iQ1a9ZM33zzjcLCrDxTPY+e+Q44qy+++EIbNmzQmTNndOPGDbNpBoNBW7dudVBl+RxZCSdUp04dffTRR7pz544uXrwoSfLy8pKLi4uDK8vlyAMAeDCyEshfGEwCssTd3V3ly5eXJNWoUUO//PKLFi1apJdeekmSlJSUJF9fX1P/8+fPy9vb2yG1OgzfLZCDMj1oXaFCBZ0/f97itMTERNMKbq0vv/xSUVFRevvtt1W3bl19+umneumll7Rx40aVKVMmXf8iRYroueeeU2BgoAoXLqz9+/fr7bffVuHChfXMM89Ikg4cOKChQ4dq8ODBatmypbZu3aohQ4Zo2bJlqlWrVmZfMuB8ykWm3csmcVfakZaF/dI2KrnoDOuMDoB54okntG3bNrVt29bsqLW7A8xZZc39SmJjY5WSkqLGjRub+pQqVUpVq1bVgQMHMhy0tnRPkyblS2hsRLyMklzMDtzLvWe+A85ozpw5mjJliqpUqaJq1arJ3d3d0SXhrjx6lRDAGi4uLukuN4f7IA8A4MHISiB/YTAJsCuj0ahbt27J399fPj4+2rNnj6pXry4p7XaR+/bt07BhwxxcZQ7juwVyUKYHrQcNGqT33ntP1atXV0BAgKn9yJEjmj59ut58881MLW/BggXq2LGjOnfuLEkaNWqUdu/ereXLl+u1115L17969eqmkJAkf39/bdmyRT/88INp0HrhwoUKDQ3Vyy+/LEmqXLmy9u7dq4ULF2rKlCmZfcmAc3JxzdX3shk0aJAMBkOGl1wZMWKEaZrBYFBcXJxdnjcxMVHS/e9XkpSUJDc3N3l6eqbrk5SUlKnn2/2nr8ZuC1b/R4/L1+Pm/ybk8jPfAWezcuVKde/eXaNHj3Z0KbAkj14lBEA2IA8A4MHISiD/YDAJsNmUKVPUtGlTlS5dWtevX9eXX36pvXv36uOPP5bBYFDPnj01e/ZsVahQQeXLl9fs2bNVqFAhtWvXztGl5zy+WyCHZHrQetWqVUpNTdXTTz+tKlWqyMfHR4mJifr111/l6+ur1atXa/Xq1ZLSBpJmzZqV4bJu3bqlQ4cOqW/fvmbtjRs31oEDB6yq5/Dhwzpw4ICGDBliajt48GC6y4WHhYVp4cKFGS7H0hmQ/+R09yEAcrlFixY59Pkze78Sa/rc954md1Jz9ZnvgLNLSkpSy5YtHV0G7icPXCUEQA4hDwDgwchKIP9gMAmwSVJSkkaMGKFz586pWLFiCgwM1Mcff2y6uuZLL72kmzdvaty4cbp8+bJq1aql+fPny8PDw8GVOwjfLZADMj1ofezYMbm6uqp06dK6du2arl27JintfrN3p99176DPvS5evKjU1FSLZzTePdsxI02bNtWFCxeUmpqqAQMGmM7UltLC5t5llixZ8oHLBJB7NGzY0CHP6+PjI+n+9yvx9vZWSkqKLl++bHa29fnz51WnTh3bnjiXn/kOOLugoCCdOnVKISEhji4F90NWAriLPACAByMrgfyDwSQg09577737TjcYDBo4cKAGDhyYQxXlAXy3QDbL9KD19u3b7V6EpTMaHzTgvXTpUiUnJ+unn37S5MmTVb58ebPLMmR2mfc9AxJAvmHN/UqCg4Pl5uamPXv2qG3btpKkc+fO6fjx4xo+fLjDagdguzfeeEPDhw9X9erVFRwcnOXl7du3T/PmzVNsbKwSExM1Y8YM05ncKSkp+uCDD/T111/r1KlT8vDwUGhoqF577TWVKlXKtIwePXpo7969Zstt27atpk6dmuX6AAAAAABOhsEkAEAel+lBa3vy8vKSq6trunvA/vOMxoyUK1dOkhQYGKikpCR9+OGHpkFrS/eVvXDhwgOXCSD3+uKLL7RhwwadOXNGN27cMJtmMBi0detWq5d1/fp1nTx50vQ4Pj5ecXFx8vT0VJkyZR54v5JixYqpY8eOiomJkZeXlzw9PRUTE6OAgACFhoba5wUDyFEjR47UpUuX1LlzZ3l7e6tEiRJm0w0Gg9atW2f18pKTkxUYGKjIyMh0R+TeuHFDhw8f1iuvvKJq1arpypUreu+99/TKK69ozZo1Zn27dOmiQYMGmR4XKlQo8y8OAAAAAAAAAHK5LA1aX7p0SR9//LGOHz+uUqVKqUePHqpatarV87u7uysoKEh79uzRY489Zmr/5ptv7nt/6XsZjUalpKSYHteuXVt79uwxu6/17t27bb9sLwCHmjNnjqZMmaIqVaqoWrVqcnd3z9LyYmNj1bNnT9PjqKgoSVKHDh0UHR1t1f1KRo4cqQIFCmjIkCG6ceOGQkJCFB0dLVdXLrsE5EUlSpRIN1CdFeHh4QoPD7c4rVixYlqwYIFZ21tvvaXOnTvrzJkzKlOmjKm9UKFCptsWAAAAAAAAAICzsmrQOiYmRv/5z3+0c+dOU1tycrI6deqk06dPy2g0SpI2btyozz77TJUqVbK6gN69e2vEiBEKDg5WnTp1tGLFCiUkJKhr166SpMmTJ+vs2bOaOHGipLTLgvv5+ZmeY//+/Zo/f76ee+450zJ79uyp5557TnPmzFFERIS2bdumb7/9VsuWLbO6LgC5x8qVK9W9e3eNHj3aLstr1KiRjh49muF0a+5XUrBgQY0ePdpuNQFwrMWLFzv0+a9duyaDwaDixYubta9fv17r1q2Tt7e3mjZtqv79+5sdQHOvhISE+x74x61QAAAAAAAAAORGVg1aHzhwwHTf1ruWLFmi+Ph4Pf/88xowYIB+/fVXDRw4UHPnzjWdtWiNtm3b6uLFi5o5c6bOnTungIAAzZkzR2XLlpUkJSYmKiEhwdT/zp07mjJliuLj4+Xq6qqHH35Yr732mmmQW5Lq1q2rKVOm6IMPPtC///1vlStXTlOnTlWtWrWsrgtA7pGUlGS6FywAOJubN2/q/fffV7t27cwGpNu3by9/f395e3vr+PHjmjx5so4cOZLuLG0AAAAAcDb79u3TvHnzFBsbq8TERM2YMcNs39Abb7yhzz//3GyeWrVqaeXKlTldKgAAsBOrBq1PnTpldildSdqxY4ceeughDR8+XK6urqpdu7Z69+6tJUuWZLqI7t27q3v37hanRUdHmz3u0aOHevTo8cBltmnTRm3atMl0LQByn6CgIJ06dUohISGOLgWAEzlz5ox8fHzk5uamM2fOPLD/Py/bbS8pKSkaOnSojEajxo4dazatS5cupr8DAgJUvnx5dezYUYcOHVJQUJDF5fn5+XE2NQAAAIA8Lzk5WYGBgYqMjMzwSnhhYWFmJ0+5ubnlVHkAACAbWDVofeXKFfn6+poe3759W7/88otatmxpdv/WRx55RImJifavEkC+9sYbb2j48OGqXr26goODHV0OACcRERGhFStWqGbNmmrRooUMBsN9+8fFxdn1+VNSUjRkyBDFx8dr4cKF973st5R2AI+bm5v+/PPPDAetAQAAAMAZhIeHKzw8/L593N3d5ePjk0MVAQCA7GbVoLW3t7fOnTtnenz48GHdvn073eCRi4uL3N3d7VshgHxv5MiRunTpkjp37ixvb2+VKFHCbLrBYNC6descUxyAPOu9995TuXLlTH8/aNDanu4OWP/5559atGiRvLy8HjjP8ePHlZKSwk4ZAAAAAJC0d+9ehYSEqHjx4mrQoIGGDh2qkiVLZtg/ISFBERERGU7nqlUAADiWVYPWQUFB+uyzz/T444+bBocMBkO6S/X+/vvv7EgFYHclSpRIN1ANAFnVoUMH09+RkZF2Xfb169d18uRJ0+P4+HjFxcXJ09NTvr6+GjRokA4fPqzZs2crNTXVdKUaT09Pubu76+TJk1q3bp3Cw8Pl5eWl3377TdHR0apevbrq1q1r11oBAAAAIK9p2rSp2rRpozJlyig+Pl7Tpk1Tr169tGbNGk6qAgAgj7Jq0Pqll15St27d1KZNG3l5eengwYOqX79+uktT7tixQzVq1MiWQgHkX4sXL3Z0CQCQKbGxserZs6fp8d37rHXo0EEDBgzQ9u3bJUlPPfWU2XyLFi1So0aN5Obmpu+++06LFy/W9evX5efnp/DwcA0YMMDs1iwAAAAAkB+1bdvW9HdAQICCg4PVokUL7dy5U61atbI4j5+fH2dTAwCQi1k1aF2rVi3NnDlT8+bNM12i97XXXjPrk5iYqL/++svuZyoBAADkNY0aNdLRo0cznH6/aVLazpQlS5bYuywAAAAAcEq+vr4qU6aM/vjjD0eXAgAAbGTVoLUkNWvWTM2aNctwuo+PD/eUBWA3Z86ckY+Pj9zc3HTmzJkH9i9TpkwOVAUAAAAAAIDc5uLFi0pISJCvr6+jSwEAADayetAaAHJSRESEVqxYoZo1a6pFixYyGAz37R8XF5dDlQEAAAAAACA7Xb9+XSdPnjQ9jo+PV1xcnDw9PeXp6anp06erVatW8vHx0enTpzV16lR5eXmpZcuWDqwaAABkBYPWAHKl9957T+XKlTP9/aBBawAAAAAAADiH2NhY9ezZ0/Q4KipKktShQweNHTtWx44d09q1a3X16lX5+PioUaNGmjp1qjw8PBxVMgAAyCIGrQHkSh06dDD9HRkZ6cBKAAAAAAAAkJMaNWqko0ePZjh93rx5OVgNAADICS6OLgAAAAAAAAAAAAAAkH9xpjVgizupUuIu6e8EqbCf5BMmubg6uioAzoScARyP9RBAdiJjADgbcg3IX1jnAQB23hYwaA1k1qk10v7BUnL8/9qK+Ev1pknluIw1ADsgZwDHYz0EkJ3IGADOhlwD8hfWeQBANmwLuDw4kBmn1ki7OpmvhJKUfDqt/dQax9QFwHmQM4DjsR4CyE5kDABnQ64B+QvrPAAgm7YFDFoD1rqTmnbUiIwWJv63bf+QtH4AYAtyBnA81kMA2YmMAeBsyDUgf2GdBwBk47aAQWvAWom70h81YsYoJZ9K6wcAtiBnAMdjPQSQncgYAM6GXAPyF9Z5AEA2bgsYtAas9XeCffsBwL3IGcDxWA8BZCcyBoCzIdeA/IV1HgCQjdsCBq0BaxX2s28/ALgXOQM4HushgOxExgBwNuQakL+wzgMAsnFbwKC1A0VERCgiIsLRZeQ6ufZ98QmTivhLMmTQwSAVKZfWLxvk2vcFuRqfG8ty7ftCziAPcrrPjZ3WQ6d7X+yE9wWZ5XSfGTImW/G+wBZ8biyz+n1x8G+YnMbnBbZwqs8N32WyFe8LbMHnxjLeF8vs8r5k4/c/Bq0Ba7m4SvWm/ffBvSvjfx/X+yCtHwDYgpwBHI/1EEB2ImMAOBtyDchfWOcBANm4LWDQGsiMcpFS2CqpSFnz9iL+ae3lIh1TFwDnQc4Ajsd6CCA7kTEAnA25BuQvrPMAgGzaFhSwQ2lA/lIuUir7lJS4K+1G8oX90i5zwBGEAOyFnAEcj/UQQHYiYwA4G3INyF9Y5wEA2bAtYNAasIWLq1SqmaOrAODMyBnA8VgPAWQnMgaAsyHXgPyFdR4AYOdtAZcHBwAAAAAAAAAAAAA4DIPWAAAAAAAAAAAAAACHMRiNRqOji3C0GjVqKDU1VX5+fjn6vAkJCZKU48+b2/G+WOao9yUhIUGurq765ZdfcvR5nQ05k7vwvlhGzuRt5EzuwvtiGTmTd5ExuQvvi2VkTN5GzuQuvC+WkTN5l6MyRmJ9soT3xDIyJm/ju0zuwvtiWW7PGe5pLalgwYK6detWjj8vK4tlvC+WOep9KVCggNzd3R3y3M6EnMldeF8sI2fyNnImd+F9sYycybvImNyF98UyMiZvI2dyF94Xy8iZvMtRGSOxPlnCe2IZGZO38V0md+F9sSy35wxnWgMAAAAAAAAAAAAAHIZ7WgMAAAAAAAAAAAAAHIZBawAAAAAAAAAAAACAwzBoDQAAAAAAAAAAAABwGAatAQAAAAAAAAAAAAAOw6B1Nlu6dKlatGihGjVqKDIyUj/88EOGfX/44Qd17dpVjRo1Us2aNdWmTRt98sknOVdsDsrM+/JP+/fvV/Xq1fXUU09lc4WOkZn35fvvv1dgYGC6f7/99lsOVozcgJyxjJyxjJyBLciZ9MgYy8gY2IKMsYycsYycgS3IGcvIGcvIGWQWGWMZGWMZGQNbkDOWkTOW5emcMSLbbNy40RgUFGRcuXKl8ddffzVOmDDBWLt2bePp06ct9j906JBx/fr1xmPHjhlPnTplXLt2rbFWrVrGTz/9NIcrz16ZfV/uunLlijEiIsL4wgsvGJ988skcqjbnZPZ9+e6774wBAQHG33//3Xju3DnTv9u3b+dw5XAkcsYycsYycga2IGfSI2MsI2NgCzLGMnLGMnIGtiBnLCNnLCNnkFlkjGVkjGVkDGxBzlhGzliW13PGYDQajY4ZLnd+nTt3VvXq1TVu3DhT2+OPP66WLVvqtddes2oZAwYMUOHChTVp0qTsKjPH2fq+DB06VOXLl5erq6u2bt2qL774IifKzTGZfV++//579ezZU/v27VPx4sVzslTkIuSMZeSMZeQMbEHOpEfGWEbGwBZkjGXkjGXkDGxBzlhGzlhGziCzyBjLyBjLyBjYgpyxjJyxLK/nDJcHzya3bt3SoUOH1KRJE7P2xo0b68CBA1Yt4/Dhwzpw4IAaNmyYHSU6hK3vy+rVq3Xy5EkNGDAgu0t0iKx8Xp5++mk1adJEvXr10nfffZedZSKXIWcsI2csI2dgC3ImPTLGMjIGtiBjLCNnLCNnYAtyxjJyxjJyBplFxlhGxlhGxsAW5Ixl5IxlzpAzBRz2zE7u4sWLSk1NVcmSJc3avb29lZiYeN95mzZtqgsXLig1NVUDBgxQ586ds7PUHGXL+/LHH39o8uTJWrp0qQoUcM6PrC3vi4+Pj9555x0FBQXp1q1b+uKLL/T8889r8eLFatCgQU6UDQcjZywjZywjZ2ALciY9MsYyMga2IGMsI2csI2dgC3LGMnLGMnIGmUXGWEbGWEbGwBbkjGXkjGXOkDPO+X8mFzEYDGaPjUZjurZ7LV26VMnJyfrpp580efJklS9fXu3atcvOMnOcte9LamqqXnvtNQ0cOFAVK1bMqfIcJjOfl0qVKqlSpUqmx3Xq1NFff/2lefPm8aUlnyFnLCNnLCNnYAtyJj0yxjIyBrYgYywjZywjZ2ALcsYycsYycgaZRcZYRsZYRsbAFuSMZeSMZXk5Zxi0ziZeXl5ydXVVUlKSWfv58+fl7e1933nLlSsnSQoMDFRSUpI+/PBDpwmTzL4v169fV2xsrOLi4vTOO+9Iku7cuSOj0ajq1atr3rx5CgkJyZHas1NWPi//VKtWLa1bt87e5SGXImcsI2csI2dgC3ImPTLGMjIGtiBjLCNnLCNnYAtyxjJyxjJyBplFxlhGxlhGxsAW5Ixl5IxlzpAz3NM6m7i7uysoKEh79uwxa//mm29Up04dq5djNBqVkpJi7/IcJrPvi4eHh9avX6+1a9ea/nXt2lUVK1bU2rVrVatWrZwqPVvZ6/MSFxcnHx8fe5eHXIqcsYycsYycgS3ImfTIGMvIGNiCjLGMnLGMnIEtyBnLyBnLyBlkFhljGRljGRkDW5AzlpEzljlDznCmdTbq3bu3RowYoeDgYNWpU0crVqxQQkKCunbtKkmaPHmyzp49q4kTJ0pKu1yDn5+f6VT8/fv3a/78+Xruuecc9hqyQ2beFxcXFwUEBJjNX7JkSRUsWDBde16X2c/LJ598In9/f1WpUkUpKSlat26dvvrqK3344YeOfBnIYeSMZeSMZeQMbEHOpEfGWEbGwBZkjGXkjGXkDGxBzlhGzlhGziCzyBjLyBjLyBjYgpyxjJyxLK/nDIPW2aht27a6ePGiZs6cqXPnzikgIEBz5sxR2bJlJUmJiYlKSEgw9b9z546mTJmi+Ph4ubq66uGHH9Zrr71m+jA5i8y+L/lFZt+XlJQUxcTE6OzZsypUqJCqVKmiOXPmKDw83FEvAQ5AzlhGzlhGzsAW5Ex6ZIxlZAxsQcZYRs5YRs7AFuSMZeSMZeQMMouMsYyMsYyMgS3IGcvIGcvyes4YjEaj0SHPDAAAAAAAAAAAAADI97inNQAAAAAAAAAAAADAYRi0BgAAAAAAAAAAAAA4DIPWAAAAAAAAAAAAAACHYdAaAAAAAAAAAAAAAOAwDFoDAAAAAAAAAAAAAByGQWsAAAAAAAAAAAAAgMMwaA0AAAAAAAAAAAAAcBgGrTPpp59+Uv/+/dWsWTMFBwcrNDRUzzzzjKKjox1d2gP16NFDPXr0sKrvlStX1KhRI23cuNHU9uGHHyowMFAXLlzIrhLtIiUlRS1bttQnn3xiVf81a9YoMDDQ9C87Xl9qaqpCQkKsrulB7v6/sEVcXJzZ6920aZNdaoL9kDPkjC3IGViLjCFjbEHGIDPIGXLGFuQMrEXGkDG2IGOQGeQMOWMLcgaZQc6QM7bIDznDoHUm7Ny5U127dtW1a9c0fPhwzZ8/X6NGjVLdunX15ZdfOro8u5o+fbp8fX3Vtm1bR5eSaW5uburfv79mzpypixcvWj3f9OnTtWLFChUvXtzuNe3bt08XLlzQY489Zpflde7cWStWrLBp3goVKmjFihUaM2aMXWqBfZEzeQM5c3/kTO5FxuQNZMz9kTG5GzmTN5Az90fO5F5kTN5AxtwfGZO7kTN5Azlzf+RM7kbO5A3kzP1lV84UsOvSnNzHH38sf39/zZs3TwUK/O+te+KJJzR8+HAHVmZfly5d0ooVK/Tmm2/KYDA4uhylpqYqNTVV7u7uVs/zxBNPKDo6WitWrFC/fv2smueRRx6Rv7+/rWXe11dffaXg4GCVLVvWLssrXbq0SpcubdO8hQsXVu3atXXz5k271AL7Imccg5xJj5xxTmSMY5Ax6ZExzouccQxyJj1yxjmRMY5BxqRHxjgvcsYxyJn0yBnnRc44BjmTXm7MGc60zoRLly7Jy8vLLEjucnExfytbtGihl19+WVu2bFH79u1Vo0YNRUREaNGiRenmvXbtmmJiYtSiRQsFBwcrLCxM7777rpKTk836GY1GLV26VE899ZRq1qypBg0aaNCgQTp16lS6fnPnzlXz5s1Vo0YNdejQQf/3f/9n9ev8/PPPlZqaatXRLz169FC7du30888/69lnn1WtWrUUERGhOXPm6M6dO2Z9z5w5o2HDhikkJETBwcF6/PHHNX/+fLN+8fHxCgwM1Ny5czVz5ky1aNFCNWrU0HfffSdJ2rp1q9q3b6/g4GBFRERo4cKFFi9h4O7urscff1wrV66U0Wi0+rVn9PoOHDigrl27qmbNmmrRooVWr14tKe2oqA4dOqhWrVpq3769vv7663TLMBqN2rJli1q3bm1qu/v52LFjh55++mnVrFlTjz/+uHbs2CEp7VISjz/+uGrXrq1OnTrpl19+MVumpdd8d5lff/21OnTooJo1a6pNmzZatWqVza8fOY+cSY+cIWdgP2RMemQMGQP7ImfSI2fIGdgPGZMeGUPGwL7ImfTIGXIG9kXOpEfOkDN3caZ1JtSuXVufffaZJkyYoPbt26t69epyc3PLsH9cXJzee+89DRgwQN7e3lq/fr3effddpaSkqE+fPpKkv//+W88995z++usv9evXT4GBgTp+/Lj+/e9/69ixY/rkk09MR6GMGTNGn3/+uXr06KFhw4bp8uXLmjFjhrp27aovvvhC3t7ektIuPzB9+nR16tRJrVu31l9//aXRo0frzp07qlix4gNf586dO/XII49YffmCxMREDR8+XL1799aAAQO0ZcsWTZ48Wb6+vnr66aclSRcuXFDXrl2VkpKiwYMHq2zZstq5c6diYmJ08uRJjR071myZixcvVoUKFfT666/Lw8ND5cuX19dff62BAweqfv36+uCDD3T79m3Nnz9fSUlJFutq2LChli9frmPHjtl8Xf67r+/NN9/Uiy++qNKlS2vJkiUaOXKkEhIS9NVXX6lfv37y8PDQzJkz1b9/f23dulWlSpUyzf/jjz8qMTFRrVq1MlvukSNHNGXKFNP8M2bM0MCBA9W3b199++23+te//iWDwaBJkyapX79+2rZtmwoVKnTfWo8cOaKYmBi99NJL8vb21meffaZRo0apfPnyatCggc3vAXIOOWMZOUPOwD7IGMvIGDIG9kPOWEbOkDOwDzLGMjKGjIH9kDOWkTPkDOyHnLGMnCFnJElGWO3ChQvGbt26GQMCAowBAQHGoKAg4zPPPGOcPXu28dq1a2Z9mzdvbgwMDDTGxcWZtffu3dtYt25dY3JystFoNBpnz55trFatmvHnn38267dp0yZjQECAcefOnUaj0Wg8cOCAMSAgwDh//nyzfgkJCcaaNWsaJ06caDQajcbLly8ba9SoYezfv79Zv/379xsDAgKMzz333ANfZ61atYxjxoxJ1/7vf//bGBAQYDx//ryp7bnnnjMGBAQYf/rpJ7O+bdu2Nb7wwgumx++//77Ffm+//bYxMDDQ+PvvvxuNRqPx1KlTxoCAAGPLli2Nt27dMuvbsWNHY3h4uPHmzZumtmvXrhkbNmxoDAgISFfvH3/8YQwICDAuW7bsvq939erVxoCAAOOpU6fSTbv7+n755RdT28WLF42PPPKIsWbNmsa//vrL1B4XF2cMCAgwLlq0yGwZ7777rrFdu3Zmbc2bN89w/saNG5s+H0aj0bhlyxZjQECAcdu2baa2u/8v7l1mjRo1jKdPnza13bhxw9iwYUPj6NGj07227777zhgQEGD8z3/+k+F7g5xHzpAzRiM5g+xDxpAxRiMZg+xFzpAzRiM5g+xDxpAxRiMZg+xFzpAzRiM5g+xFzpAzRiM5kxEuD54JXl5eWrZsmVatWqXXXntNLVq00B9//KHJkyerffv2unDhgln/qlWrqlq1amZt7dq107Vr13To0CFJ0o4dO1S1alU98sgjun37tulfkyZNZDAYtHfvXlM/g8GgJ5980qyft7e3qlWrZup34MAB3bx5U+3btzd73rp161p1nfsrV67o77//VsmSJa1+X3x8fFSzZk2ztsDAQJ05c8b0+LvvvlOVKlXS9YuMjJTRaDRdluGuFi1amB1dlJycrNjYWLVs2dLsngNFixZVixYtLNZ19zWcPXvW6tdiiY+Pj4KDg02PS5QooYceekiPPPKI2ZEulSpVkiSz1y0p3SUb7spo/kaNGqlw4cKm9sqVK1tcriWPPPKIypQpY3pcsGBBVahQwap5kTuQM5aRM2nIGWQVGWMZGZOGjIE9kDOWkTNpyBlkFRljGRmThoyBPZAzlpEzacgZ2AM5Yxk5kya/5wyXB7dBjRo1VKNGDUlSSkqK3n//fX3yySf6+OOPNWLECFO/u5dR+Ke7bZcuXZIknT9/Xn/++aeCgoIsPtfFixdN/YxGo0JDQy32K1eunNly7/fc93Pjxg1JaR9Ca5UoUSJdm7u7u9kN2C9dumQxzHx9fU3T/8nHx8fs8ZUrV2Q0Gi2GXEbBdzd0snoj+Ixen6en5wOf7+eff9aZM2fSXbJBUobz39t+N1SteR3W/L9A3kDOmCNnMn4+cga2IGPMkTEZPx8ZA1uRM+bImYyfj5yBLcgYc2RMxs9HxsBW5Iw5cibj5yNnYCtyxhw5k/Hz5aecYdA6i9zc3DRgwAB98sknOn78uNk0S9fAv9t293+6l5eXChYsqPfee8/i8r28vEz/NRgMWrp0qdkRIHfdbbu73Iye+0FHwdyd/96VO6tKlCihxMTEdO3nzp2T9L/Xedfd+yvcVbx4cRkMBp0/fz7dMjK618Dly5ctLjsnffXVV6pQoYICAgIcVgPyPnLGOuQMOQPbkDHWIWPIGNiOnLEOOUPOwDZkjHXIGDIGtiNnrEPOkDOwHTljHXLG+XOGy4Nnwt0P/r1+++03Sf87muOu48eP68iRI2ZtGzZsUNGiRU1HvDRr1kynTp1SiRIlTEfW/POfv7+/qZ/RaNTZs2ct9rt7A/jatWurYMGCWr9+vdnz/vjjjzp9+vQDX6O7u7vKlSunU6dOWfGOWC8kJES//vqr6XIVd61du1YGg0GNGjW67/xFihRRcHCwtm7dqlu3bpnar1+/rh07dlic5+5ruHvZA0fYvHmzxUs2ABkhZ2xHzgAPRsbYjowBrEPO2I6cAR6MjLEdGQNYh5yxHTkDWIecsR054/w40zoT+vTpo9KlS6t58+aqVKmSjEaj4uLiNH/+fBUpUkQ9e/Y06+/r66tXXnlFAwYMkI+Pj9atW6c9e/Zo2LBhpmvJ9+rVS5s3b9Zzzz2n559/XoGBgbpz544SEhK0e/duvfDCC6pVq5bq1aunZ555RiNHjlRsbKwaNGigwoULKzExUfv371dAQICeffZZeXp66oUXXtCsWbM0atQotWnTRn/99Zc+/PDDdJdCyEjDhg319ddf2/W9e/7557V27Vq9/PLLGjRokMqUKaOdO3dq2bJl6tatmypWrPjAZQwaNEgvv/yy+vTpo169eik1NVXz5s1T0aJFTUe7/NNPP/0kV1dXNWjQwK6vxVpxcXE6efKkxUs2ABkhZ2xHzgAPRsbYjowBrEPO2I6cAR6MjLEdGQNYh5yxHTkDWIecsR054/wYtM6EV155Rdu2bdPChQt17tw5paSkyMfHR6GhoXr55ZfTHWnxyCOPKDIyUh9++KH++OMP+fr66s0339Tzzz9v6lOkSBEtXbpUc+bM0YoVKxQfH69ChQrJz89PoaGhZpdZGD9+vGrVqqUVK1Zo+fLlunPnjnx9fVW3bl2zG88PHjxYRYoU0bJly/TFF1+oUqVKGjdunObPn2/V62zfvr1Wr16tn3/+Od0N7W310EMP6dNPP9XkyZM1efJkXb9+Xf7+/ho+fLh69+5t1TKaNm2qDz/8UNOmTdOQIUPk4+Ojbt266dy5c1q3bl26/lu3blXTpk1VvHhxu7yGzPrqq69UtmxZBQcHO+T5kTeRM7YjZ4AHI2NsR8YA1iFnbEfOAA9GxtiOjAGsQ87YjpwBrEPO2I6cyQeMyBbNmzc39u3b19Fl2Kxdu3bGMWPGOLqMB7p165axbdu2xt69e5u1//nnn8bAwEDj7t27H7iM1atXGwMCAox//vmnMSUlxW61Pf7448aoqCi7Lc+eUlJSjN98840xICDA+J///MfR5cBG5EzOIGdsQ87kfWRMziBjbEPGOAdyJmeQM7YhZ/I+MiZnkDG2IWOcAzmTM8gZ25AzzoGcyRnkjG2yI2c40xoWDR8+XAMGDNArr7yi0qVLO7ock5EjR6px48by8fFRUlKSli9frt9++02jRo0y6zdr1iyFhISocePGVi/7sccekyR9++23euihh7Jc65dffpnlZWSHuLg4Pf30044uAyBnyBkgW5ExZAyQ3cgZcgbITmQMGQNkN3KGnAGyGzlDzmQWg9awqGnTphoxYoTi4+NzVZhcv35dMTExunDhgtzc3FS9enXNmTNHoaGhpj63b99WuXLl1LdvX6uW2bx5c61atcr02FGXecgpFStWNHu9Dz/8sAOrQX5Gzjgvcga5ARnjvMgY5BbkjPMiZ5AbkDHOi4xBbkHOOC9yBrkFOeO8sitnDEaj0WiXJQEAAAAAAAAAAAAAkEkuji4AAAAAAAAAAAAAAJB/MWgNAAAAAAAAAAAAAHAYBq0BAAAAAAAAAAAAAA7DoDUAAAAAAAAAAAAAwGEYtAYAAAAAAAAAAAAAOAyD1gAAAAAAAAAAAAAAh2HQGgAAAAAAAAAAAADgMAxaAwAAAAAAAAAAAAAchkFrAAAAAAAAAAAAAIDDMGgNAAAAAAAAAAAAAHAYBq0BAAAAAAAAAAAAAA7DoDUAAAAAAAAAAAAAwGEYtAYAAAAAAAAAAAAAOAyD1gAAAAAAAAAAAAAAh2HQGgAAAAAAAAAAAADgMAxaAwAAAAAAAAAAAAAchkFrAAAAAAAAAAAAAIDDMGgNAAAAAAAAAAAAAHAYBq0BAAAAAAAAAAAAAA7DoDUAAAAAAAAAAAAAwGEYtAYAAAAAAAAAAAAAOAyD1gAAAAAAAAAAAAAAh2HQGgAAAAAAAAAAAADgMAxaAwAAAAAAAAAAAAAchkFrAAAAAAAAAAAAAIDDMGgNAAAAAAAAAAAAAHAYBq0BAAAAAAAAAAAAAA7DoDUAAAAAAAAAAAAAwGEYtAYAAAAAAAAAAAAAOAyD1gAAAAAAAAAAAAAAh2HQGgAAAAAAAAAAAADgMAxaAwAAAAAAAAAAAAAchkFrAAAAAAAAAAAAAIDDFHB0AQCQ02bPnq3Nmzfr999/V6FChVSnTh0NGzZMlSpVMvUJDAy0OO/w4cP14osvSpJ69OihvXv3mk1v27atpk6dmn3FAwAAAAAAAAAyxP5fIG9i0FpS/fr1devWLfn4+Di6FCDXSUxMlLu7u3744QdHl2I3e/fuVffu3VWjRg2lpqZq6tSp6tOnjzZu3KgiRYpIknbv3m02z9dff61Ro0apdevWZu1dunTRoEGDTI8LFSpk8TnJGSBjzpgzjkDOABkjZ7KOjAEyRsbYBzkDZIycyToyBsiYM2YM+3+B3MXanGHQWtLNmzeVmprq6DKAXOn27dsyGo2OLsOu5s2bZ/Y4KipKISEhOnTokBo0aCBJ6b5cbNu2TY0aNVK5cuXM2gsVKmTVFxFyBsiYM+aMI5AzQMbImawjY4CMkTH2Qc4AGSNnso6MATLmjBnD/l8gd7E2Zxi0luTr6yspLZQAmIuIiHB0Cdnu6tWrkiRPT0+L05OSkvR///d/io6OTjdt/fr1Wrdunby9vdW0aVP1799fHh4e6fr5+voqISHhvnWQQciv8kPO5AS+zwAZI2eyjowBMkbG2Ac5A2SMnMk6MgbIWH7IGPb/Ao5lbc4waA0gXzMajYqKilK9evUUEBBgsc/nn3+uokWLqlWrVmbt7du3l7+/v7y9vXX8+HFNnjxZR44c0YIFC3KidAAAAAAAAADAfbD/F8g7GLQGkK+NHz9ex44d07JlyzLss3r1arVv314FCxY0a+/SpYvp74CAAJUvX14dO3bUoUOHFBQUlG45fn5+HE0HAAAAAAAAADmE/b9A3uHi6AIAwFHeeecdbd++XQsXLlTp0qUt9vnhhx904sQJde7c+YHLCwoKkpubm/788097lwoAAAAAAAAAyAT2/wJ5C2da5xd3UqXEXdLfCVJhP8knTHJxdXRVgEMYjUa988472rJlixYvXqxy5cpl2HfVqlUKCgpStWrVHrjc48ePKyUlRT4+PvYsF7kFOQpkDusMAMDe2LYgr+MzDCA7kTGACft/kWuR1ffFoHV+cGqNtH+wlBz/v7Yi/lK9aVK5SMfVBTjIuHHjtGHDBs2cOVNFixZVYmKiJKlYsWIqVKiQqd+1a9e0adMmvf766+mWcfLkSa1bt07h4eHy8vLSb7/9pujoaFWvXl1169bNsdeCHEKOApnDOgMAsDe2Lcjr+AwDmXb27FlNmjRJu3bt0o0bN1ShQgW9++67Cg4OlpQ2KDV9+nStWLFCV65cUa1atTRmzBhVrVrVwZU7ABkDmGH/L3IlsvqBGLR2dqfWSLs6STKatyefTmsPW8XKgHxn+fLlkqQePXqYtUdFRSky8n/rw8aNG2U0GtWuXbt0y3Bzc9N3332nxYsX6/r16/Lz81N4eLgGDBggV1eOjHIq5CiQOawzAAB7Y9uCvI7PMJBply9fVrdu3dSoUSPNnTtXDz30kE6dOqXixYub+sydO1cLFixQdHS0KlSooFmzZql3797atGmTPDw8HFh9DiNjgHTY/4tch6y2CoPWzuxOatpRG/euBNJ/2wzS/iFS2ae4/ADylaNHj1rV75lnntEzzzxjcZqfn5+WLFliz7KQG5GjQOawzgAA7I1tC/I6PsOATebOnavSpUsrKirK1Obv72/622g0atGiRerXr59atWolSYqJiVFoaKg2bNigrl275njNDkHGABax/xe5ClltNRdHF4BslLjL/DID6Ril5FNp/QAA6ZGjQOawzgAA7I1tC/I6PsOATbZv367g4GANGjRIISEhevrpp7Vy5UrT9Pj4eCUmJqpJkyamNnd3dzVo0EAHDhywuMyEhARFRERk+C9PImMAIPcjq63GoLUz+zvBvv0AIL8hR4HMYZ0BANgb2xbkdXyGAZucOnVKy5cvV4UKFTRv3jx17dpVEyZM0Nq1ayXJdH/akiVLms3n7e2tpKSknC7XccgYAMj9yGqrcXlwZ1bYz779ACC/IUeBzGGdAQDYG9sW5HV8hgGbGI1GBQcH61//+pckqXr16vr111+1fPlyPf3006Z+BoMh3XwZ8fPz07Zt27KlXochYwAg9yOrrcaZ1s7MJ0wq4i/JkEEHg1SkXFo/AEB65CiQOawzAAB7Y9uCvI7PMGATHx8fVa5c2aytUqVKOnPmjGm6pHRnVZ8/f17e3t45U2RuQMYAQO5HVluNQWtn5uIq1Zv23wf3rgz/fVzvg3x/Y3cAyBA5CmQO6wwAwN7YtiCv4zMM2KRu3bo6ceKEWdsff/yhsmXLSpL8/f3l4+OjPXv2mKbfunVL+/btU506dXK0VociYwAg9yOrrcagtbMrFymFrZKKlDVvL+Kf1l4u0jF1AUBeQY4CmcM6AwCwN7YtyOv4DAOZ1qtXL/3000/66KOP9Oeff2r9+vVauXKlnn32WUlplwXv2bOnZs+erS1btujYsWN68803VahQIbVr187B1ecwMgYAcj+y2irc0zo/KBcplX1KStyVdiP3wn5plxngqA0AsA45CmQO6wwAwN7YtiCv4zMMZErNmjU1ffp0TZkyRTNmzJC/v79GjhypJ5980tTnpZde0s2bNzVu3DhdvnxZtWrV0vz58+Xh4eHAyh2EjAGA3I+sfiAGrfMLF1epVDNHVwEAeRc5CmQO6wwAwN7YtiCv4zMMZErz5s3VvHnzDKcbDAYNHDhQAwcOzMGqcjEyBgByP7L6vrg8OAAAAAAAAAAAAADAYRi0BgAAAAAAAAAAAAA4jE2XB9+8ebNNTxYSEqJixYrZNC8AAAAAAAByn9mzZ2vKlCnq2bOnRo0aJUkyGo2aPn26VqxYoStXrqhWrVoaM2aMqlat6uBqAQAAAORGNg1aDxo0SAaDQUaj0ep5DAaDVq1apaCgIFueEgAAAAAAALnMzz//rBUrVigwMNCsfe7cuVqwYIGio6NVoUIFzZo1S71799amTZvk4eHhoGoBAAAA5FY2DVpL0pgxY1S5cmWr+qampuqFF16w9akAAAAAAACQy1y/fl3Dhw/XhAkTNGvWLFO70WjUokWL1K9fP7Vq1UqSFBMTo9DQUG3YsEFdu3Z1VMkAAAAAcimbB62DgoJUs2ZNq/qmpqZm6qxsAAAAAAAA5G7jx49XeHi4QkNDzQat4+PjlZiYqCZNmpja3N3d1aBBAx04cCDDQeuEhARFRERk+Hzbtm2zX/EAAAAAchWbBq3Xrl2rihUrWt3f1dVVa9euVaVKlWx5OgAAAAAAAOQiGzdu1OHDh7Vq1ap00xITEyVJJUuWNGv39vbWmTNncqQ+AAAAAHmLTYPW1apVy5F5AAAAAAAAkLskJCTo3Xff1fz581WwYMEM+xkMBrPHD7oKn5+fH2dTAwAAAPmUzZcHBwAAAAAAQP5z6NAhnT9/XpGRkaa21NRU7du3T0uXLtWmTZskSUlJSfL19TX1OX/+vLy9vXO8XgAAAAC5n10GrX/44Qdt2LBBZ86c0Y0bN8ymGQwGLVy40B5PAyAfad++fabnMRgMmjp1qipXrpwNFQEAAAAAJOnRRx/V+vXrzdrefPNNVapUSS+99JLKlSsnHx8f7dmzR9WrV5ck3bp1S/v27dOwYcMcUTIAAMhl2P8L4F5ZHrRevXq1Ro0aJU9PT1WsWFFubm5m0x906ScAsOT48eOqXr26ihYtavU8P/zwQ7oDZwAAAAAA9uXh4aGAgACztiJFiqhEiRKm9p49e2r27NmqUKGCypcvr9mzZ6tQoUJq166dI0oGAAC5DPt/Adwry4PWH3/8sR5//HHFxMTI3d09ywXt27dP8+bNU2xsrBITEzVjxgy1bNnSNP369euaPHmytm7dqkuXLqls2bLq0aOHnn32WVOfW7duKSYmRhs2bNDNmzf16KOPauzYsSpdunSW6wOQc8aOHauaNWta1ff27dsKDg7O5ooAAAAAANZ46aWXdPPmTY0bN06XL19WrVq1NH/+fHl4eDi6NAAAkEuw/xfAP2V50PrMmTMaPXq0XQasJSk5OVmBgYGKjIzUwIED002PiorS999/r0mTJqls2bLas2ePxo0bJ19fX9Pg9rvvvqsdO3Zo6tSpKlGihKKjo/Xyyy9rzZo1cnV1tUudALJXRESEPD09re7v6uqqiIgIFS9e/IF9Z8+erc2bN+v3339XoUKFVKdOHQ0bNkyVKlUy9XnjjTf0+eefm81Xq1YtrVy50vSYA2QAAAAAIM3ixYvNHhsMBg0cONDivh0AAAD2/wK4V5YHrStXrqykpCR71CJJCg8PV3h4eIbTDx48qKefflqNGjWSJD3zzDNasWKFYmNj1bJlS129elWrV6/WxIkTFRoaKkmaNGmSmjVrpm+++UZhYWF2qxVA9pkxY0am+hsMBqvn2bt3r7p3764aNWooNTVVU6dOVZ8+fbRx40YVKVLE1C8sLExRUVGmx/fe/oADZAAAAAAAAAAg89j/C+BeWR60Hjp0qCZOnKhGjRqpVKlS9qjpvurWravt27erU6dO8vX11ffff68TJ05o5MiRkqTY2FilpKSocePGpnlKlSqlqlWr6sCBAxkOWickJCgiIiLD5922bZt9XwgAh5k3b57Z46ioKIWEhOjQoUNq0KCBqd3d3V0+Pj4Wl8EBMgAAAAAAIEvupEqJu6S/E6TCfpJPmOTCIAgAZBX7f4G8KcuD1kuXLtXVq1fVunVrVatWTSVKlDCbbjAYNGvWrKw+jclbb72l0aNHq2nTpipQoIAMBoMmTJig+vXrS5KSkpLk5uaW7rIS3t7edj0jHLA7fqg80Pnz53X69GndvHkz3bR/ftnIrKtXr0pSutzYu3evQkJCVLx4cTVo0EBDhw5VyZIlJdl2gAwHxwBwKLYzAJC/sR0AgNzl1Bpp/2ApOf5/bUX8pXrTpHKRjqsrP2NbCTgc+3+B/C3Lg9bHjh2Ti4uLHnroIZ07d07nzp0zm24wGLL6FGYWL16sgwcPatasWSpTpox++OEH0z2t7x7tYonRaLzvcv38/AgMOA4/VO7r3LlzGjFihL7//vt004xGowwGg+Li4mxattFoVFRUlOrVq6eAgABTe9OmTdWmTRuVKVNG8fHxmjZtmnr16qU1a9bI3d2dA2SQ/fixDHtiOwMA+RvbAQDIXU6tkXZ1knTP/srk02ntYavI55zGthJwKPb/2hH7FJGHZXnQevv27faowyo3btzQ1KlTNX36dDVr1kySVK1aNcXFxWnevHkKDQ2Vt7e3UlJSdPnyZbMwOX/+vOrUqZNjtQJW44fKA73zzjuKi4vTsGHDFBgYKHd3d7ste/z48Tp27JiWLVtm1t62bVvT3wEBAQoODlaLFi20c+dOtWrVKsPl3e8AGQ6OgdX4sQx7YjsDAPkb2wEAyF3upKb93rs3l6X/thmk/UOksk8xyJBT2FYCDsf+XzthnyLyuCwPWuek27dvKyUlJd3Z266urqagCA4Olpubm/bs2WMKnXPnzun48eMaPnx4jtcM3Bc/VKyyd+9ejRgxQh07drTrct955x1t375dS5YsUenSpe/b19fXV2XKlNEff/whSRwgg+zDj2XYE9sZAMjf2A4AQO6TuMt8MCEdo5R8Kq1fqWY5VVX+xbYSyBXY/2sH7FOEE3BxdAH3un79uuLi4kyXeoiPj1dcXJzOnDkjDw8PNWzYUJMmTdL333+vU6dOac2aNVq7dq1atmwpSSpWrJg6duyomJgYffvttzp8+LCGDx+ugICA+14+HHCIzPxQyccMBoP8/Pzstjyj0ajx48dr8+bNWrhwocqVK/fAeS5evKiEhAT5+vpKMj9A5q67B8jkyi8tyBse+GNZaT+W76TmYFHI09jOAED+xnYAAHKfvxPs2w9Zw7YSyBXY/5tF7FOEk7DpTOtHHnlEK1asUM2aNVWtWrX73rfaYDDo8OHDVi87NjZWPXv2ND2OioqSJHXo0EHR0dGaMmWKpkyZomHDhuny5csqU6aMhg4dqm7dupnmGTlypAoUKKAhQ4boxo0bCgkJUXR0tFxdORoOuQw/VKzSpk0b7dixw24HnowbN04bNmzQzJkzVbRoUSUmJkpKO+ilUKFCun79uqZPn65WrVrJx8dHp0+f1tSpU+Xl5WXxABkvLy95enoqJiaGA2SQNRxxD3tjOwMA+RvbAQDIfQpbOShjbT9kDdtKIFdg/28WsU8RTsKmQev+/furVKlSpr/vN2idWY0aNdLRo0cznO7j42MayM5IwYIFNXr0aI0ePdpudQHZgh8qVnn88cc1evRoGY1GNW/eXCVKlEjXJygoyOrlLV++XJLUo0cPs/aoqChFRkbK1dVVx44d09q1a3X16lX5+PioUaNGmjp1qjw8PEz9OUAGdsePZdgb2xkAyN/YDgBA7uMTlnZ/0eTTsnxGnCFtuk9YTleWP7GtBHIF9v9mEfsU4SRsGrQeMGCA6e+BAwfarRgg3+GHilV69eolSVqyZImWLl1qNs1oNMpgMJhuKWCN+x0YI0mFChXSvHnzHrgcDpCB3fFj2aH69etn03yjRo2y6jJTDsF2BgDyN7YDAJD7uLhK9ab9976jBpnn839PDKr3AfdPzilsK4Fcgf2/WcQ+RTgJmwatAdgJP1Ss8qCrKwBOgx/LDrVz5055e3vL3d3d6nkSEhJy9wF8bGcAIH9jOwAAuVO5SClsVdr9R/95Odci/mm5XC7SYaXlO2wrgVyB/b9ZxD5FOAm7DFrfunVL33zzjU6fPq1bt26ZTTMYDHr++eft8TSAc+KHygN16NDB0SUAOYMfyw43c+ZM1axZ06q+t2/fVnBwcDZXZAdsZwAgf2M7AAC5U7lIqexTafcX/Tsh7ew3nzB+7zkC20rA4dj/m0XsU4STyPKgdWxsrPr166fz58/LaEx/BAeD1oAV+KEC4C5+LDtMtWrVVLhwYav7u7i4qFq1aipUqFA2VmUnbGcAIH9jOwAAuZOLq1SqmaOrgMS2EkDexz5FOIEsD1qPGzdOHh4eGjdunCpXriw3Nzd71AXkP/xQMTN9+nR17txZpUqV0vTp0+/b12AwqH///jlUGZAD+LHsEGvXrs1UfxcXl0zP41BsZwAgf2M7AADA/bGtBHIU+3+zAfsUkcdledD6119/1fvvv6+IiAh71AMAktK+tDRt2pQvLci/+LEMAAAAAAAAJ8X+32zCPkXkYVketPbz87NHHQBg5siRIxb/BgAAyKsOHTpk03xVqlRRwYIF7VwNAAAAADgO+38B3CvLg9Yvvvii5s+fr7CwMLm7u9ujJgAAgByXmavGGAwGbd26NRurAeCMOnbsKIPBkOn5Vq1apaCgoGyoCAAAAAAAIHfI8qB1ZGSkTp8+rZYtW6phw4YqUaJEuj5vvfVWVp8GAAAgW50+fVrFihVT3bp1bRpUup99+/Zp3rx5io2NVWJiombMmKGWLVtaNe/+/fvVo0cPVa1aVV988YVd6wKQ8/r166eHH37Yqr6pqakaPXq0VX1tyZm9e/cqOjpax48fl6+vr1588UV169bNqucDAAAAAACwpywPWu/cuVOzZ8/W7du3tWHDhnTTDQYDg9YAsiQ1NVWLFy/W+vXrdebMGd28edNsusFg0P79+x1UHQBn0bBhQ+3bt0/Hjh3T008/rcjISJUrV84uy05OTlZgYKAiIyM1cOBAq+e7evWqXn/9dYWEhCgpKckutQBwrObNm6tmzZpW9U1NTbX6t1Rmc+bUqVPq27evOnfurEmTJunHH3/UuHHj9NBDD6l169ZWPScAAAAA2AP7fwFIdhi0njhxooKCgjRu3DhVrlxZbm5u9qgLAEzef/99LViwQI888ohCQ0O5FQGAbLFo0SLFx8dr1apVWrt2rWbPnq0GDRqoU6dOat26dZayJzw8XOHh4Zmeb8yYMWrXrp1cXV25HDngBKZPn66KFSta3d/V1VXTp09X+fLlH9g3sznz6aefys/PT6NGjZIkVa5cWb/88ovmz5/PoDUAAACAHMX+XwCSHQatT58+renTp6tatWr2qAcA0lm/fr369u2rf/3rX44uBYCT8/f315AhQzR48GDt2rVLq1ev1siRI/XOO+/oiSeeUNeuXRUYGJgjtaxevVonT57UpEmTNGvWLKvmSUhIuO+9ubdt22av8gDYwNrbAmR1HmscPHhQjRs3NmsLCwvT6tWrlZKSYvFgZDIGAAAAQHZg/y8AyQ6D1pUqVdK1a9fsUQsAWHTz5k2FhIQ4ugwA+YjBYFDTpk3VtGlTXbp0SQsWLNDHH3+sxMRETZ8+Pduf/48//tDkyZO1dOlSFSiQ5a9rAJBOUlKSvL29zdpKliyp27dv6+LFi/L19XVQZQAAAADyG/b/ApDsMGg9ePBgTZkyRfXr15ePj489agIAM40bN9bPP//MFxcAOcpoNOrrr7/W6tWrtX37dhUpUkT169fP9udNTU3Va6+9poEDB2bqMsKS5Ofnx5mOQC62du3aTPV/+umns6WOuwwGg9ljo9Fosf0uMgbAXcuWLdPy5ct1+vRpSVLVqlX16quvmm5T8MYbb+jzzz83m6dWrVpauXJljtcKAAByP/b/ApDsMGj96aef6sqVK2rVqpWqVasmT09Ps+kGg8HqS1oCgCVvvfWW+vbtq8KFCys8PDxdzkhSiRIlcr4wAE7p5MmTWr16tdauXatz586pYcOGioqKUqtWrVSwYMFsf/7r168rNjZWcXFxeueddyRJd+7ckdFoVPXq1TVv3jx+xAF51BtvvGEaEL47QJwRg8GQrYPW3t7eSkxMNGu7cOGCChQowPcqAA9UunRpDRs2TA8//LCktINy+vfvr88//1xVq1aVlHbLgaioKNM8lm47AAAAILH/F0CaLA9aHzt2TC4uLvLy8tLZs2d19uxZs+kZHaUPANby8PBQxYoVFRUVZbbT45/i4uJyuCoAzmbt2rVatWqV9u/fr9KlS6tjx46KjIyUv79/jtbh4eGh9evXm7UtW7ZM3333nf7973/neD0A7MvDw0OPP/64nnjiCRUtWtRhddSuXVs7duwwa9u9e7eCg4MZWALwQC1atDB7PHToUC1fvlwHDx40DVq7u7tzRT4AAGAV9v8CkOwwaL19+3Z71AEAGXr77be1adMmRUREqHLlyuxIBZAt3njjDXl4eKhz584KDQ2Vi4uLDh8+rMOHD1vs36pVK6uXff36dZ08edL0OD4+XnFxcfL09FSZMmU0efJknT17VhMnTpSLi4sCAgLM5i9ZsqQKFiyYrh1A3rJkyRKtXr1a69ev1/r169WmTRt17NjRLrceyEzOSFLXrl21dOlSRUVFqUuXLjpw4IBWr16tyZMnZ7kWAPlLamqqNm3apOTkZNWpU8fUvnfvXoWEhKh48eJq0KCBhg4dqpIlS953WQkJCYqIiMhwOrcoAADAObH/F4Bk46D1woUL1bp1a5UuXdre9QBAOlu3btW//vUv9enTx9GlAHBy165d08qVK/XZZ59JyvjyvQaDIVNH+MbGxqpnz56mx3ePGu7QoYOio6OVmJiohISELFQOIC+oX7++6tevr7feeksbN27U6tWr9dxzz6l8+fLq2LGjnn76afn6+tq07MzmTLly5TRnzhxFRUVp6dKl8vX11ahRo9S6deusvUgA+cbRo0fVtWtX3bx5U0WKFNGMGTNUpUoVSVLTpk3Vpk0blSlTRvHx8Zo2bZp69eqlNWvWyN3d3cGVAwCA3Ib9vwAkGwetZ8+erejoaAUHB6t169Zq1aqV6T5GAGBvBQoUUPXq1R1dBgAnt2jRomxbdqNGjXT06NEMp0dHR993/oEDB2rgwIH2LguAgxQtWlRdunRRly5d9Ouvv2rVqlX65JNP9O9//1t9+vTR0KFDM71MW3KmYcOG+vzzzzP9XAAgSRUrVtTatWt15coVbd68Wa+//rqWLFmiKlWqqG3btqZ+AQEBCg4OVosWLbRz5877Xq3Gz8+Ps6kBAMiH2P8LQLJx0Hr37t3au3evNm/erIULF2ry5MkKCAgwDWDfPbIWAOyhVatW2rVrl0JCQhxdCgAn1rBhQ0eXACAfqlKlijp27KiUlBQtX75cv/32m6NLAgCruLu7q3z58pKkGjVq6JdfftGiRYs0fvz4dH19fX1VpkwZ/fHHHzlcJZzanVQpcZf0d4JU2E/yCZNcXB1dFQDABuz/BSDZOGjt4uKiRx99VI8++qjGjBmj/fv366uvvtKqVav04YcfqkKFCmrVqpVat27N0TEAsuyJJ57Q6NGjdfv2bTVr1kyenp7p+gQFBTmgMgAAANtcu3ZNGzZs0OrVqxUbG6vy5ctryJAhevrppx1dGgDYxGg06tatWxanXbx4UQkJCTbfAgFI59Qaaf9gKTn+f21F/KV606RykY6rCwBgE/b/ApBsHLS+V7169VSvXj2NHDlSP//8s7766iv95z//0ezZs1W2bFm1bt1aI0aMsMdTAZZxdK1Te/755yWlXbp38eLFZtOMRmOm7y07e/Zsbd68Wb///rsKFSqkOnXqaNiwYapUqZIkKSUlRR988IG+/vprnTp1Sh4eHgoNDdVrr72mUqVKmZbTo0cP7d2712zZbdu21dSpU218pYCTyWPZvGDBArVv317e3t6Zmuepp57SQw89lI2VAdkkj62jzuK7777T6tWrtXnzZrm4uKhNmzZ6/fXXVb9+fUeXljV8noB8ZcqUKWratKlKly6t69ev68svv9TevXv18ccf6/r165o+fbpatWolHx8fnT59WlOnTpWXl5datmzp6NLhDE6tkXZ1kmQ0b08+ndYetoqBa8AZ8X3TqbH/Fxli3c9X7DJo/U81a9ZUzZo1NXz4cB05ckRfffWVNm/ezKA1sg9H1zq9qKgouy5v79696t69u2rUqKHU1FRNnTpVffr00caNG1WkSBHduHFDhw8f1iuvvKJq1arpypUreu+99/TKK69ozZo1Zsvq0qWLBg0aZHpcqFAhu9YK5Fl5MJsnTpyoevXqWT1onZqaqokTJ6phw4YMWiPvyYPrqDN47LHHFB8fr1q1aumtt97SE088oSJFiji6rKzj8wTkO0lJSRoxYoTOnTunYsWKKTAwUB9//LEaN26sGzdu6NixY1q7dq2uXr0qHx8fNWrUSFOnTpWHh4ejS0dedyc1bZtz74C19N82g7R/iFT2KXZo53Effvihpk+fbtbm7e2tPXv2SEobxJo+fbpWrFihK1euqFatWhozZoyqVq3qiHKR3fi+6fTY/wuLWPfzHbsPWv/TuXPntG7dOm3bti07nwb5GUfXOq07d+7IxcVFktShQwe7LnvevHlmj6OiohQSEqJDhw6pQYMGKlasmBYsWGDW56233lLnzp115swZlSlTxtReqFAh+fj42LU+IM/Lo9lsNBq1atUqff3111b3B/KkPLqOOoO7R/Bfv35dixYt0qJFizLsazAYtG7duhyszkZ8noB86b333stwWqFChdL95gLsJnGX+Y7rdIxS8qm0fqWa5VRVyCZVq1Y12z/j6vq/AxHmzp2rBQsWKDo6WhUqVNCsWbPUu3dvbdq0iQNknA3fN50W+39xX6z7+VK2Dlr//fffOnPmTHY+BfIzjq51aiEhIXrsscfUqlUrhYSEyM3NLdue6+rVq5Jk8V4pd127dk0Gg0HFixc3a1+/fr3WrVsnb29vNW3aVP3798/wx1FCQoIiIiIyfA4O8IFTyOPZvHLlSkeXAGSvPL6O5nUNGjRwdAn2xecJAJDT/k6wbz/kaq6urhYHioxGoxYtWqR+/fqpVatWkqSYmBiFhoZqw4YN6tq1a06XiuzC902nxv5fZIh1P9/K1kFrIFtxdK1T69+/v7Zs2aJ+/fqpaNGiatasmVq3bq2wsDAVLFjQbs9jNBoVFRWlevXqKSAgwGKfmzdv6v3331e7du3MvpC0b99e/v7+8vb21vHjxzV58mQdOXIk3VF6QL6Sh7P5yJEjji4ByH55eB11Bvfemy3P4/MEAMhphf3s2w+52p9//qkmTZrI3d1dtWrV0r/+9S+VK1dO8fHxSkxMVJMmTUx93d3d1aBBAx04cCDDQWsGk/Igvm86Nfb/IkOs+/lWrhu03rdvn+bNm6fY2FglJiZqxowZatmypVmf3377TZMmTdK+fft0584dVa1aVR988IHpkg23bt1STEyMNmzYoJs3b+rRRx/V2LFjVbp0aUe8JGQXjq51aj179lTPnj11/vx5bd68WVu2bNGQIUPk5uampk2bqnXr1mrWrFmW7wM5fvx4HTt2TMuWLbM4PSUlRUOHDpXRaNTYsWPNpnXp0sX0d0BAgMqXL6+OHTvq0KFDCgoKSrcsPz8/fgDB+ZHNQO7GOgp74vMEAMhpPmFp97JMPi3LZ18Z0qb7hOV0ZbCzmjVrKiYmRhUqVND58+c1a9Ysde3aVRs2bFBiYqIkqWTJkmbzeHt7c9VPZ8P3TafG/l9kiHU/38p1g9bJyckKDAxUZGSkBg4cmG76yZMn9eyzz6pjx44aNGiQihUrpt9++83syJt3331XO3bs0NSpU1WiRAlFR0fr5Zdf1po1a8zufYI8jqNr84WSJUuqW7du6tatmy5duqStW7dq8+bNev311+Xi4qLQ0FC1bt1aLVq0SHfplgd55513tH37di1ZssTiQS0pKSkaMmSI4uPjtXDhwgfeEykoKEhubm76888/LX5pAfIFshnI3VhHYU98ngAAOc3FVao37b/3uDTIfODakPafeh9wqVAnEB4ebva4du3aeuyxx7R27VrVqlVLkmQwGMz6GI2WDmT4HwaT8iC+b+YL7P9FOqz7+ZaLowu4V3h4uIYOHWq6H8m9pk6dqqZNm2rEiBGqXr26ypUrp2bNmpmOrLt69apWr16tN954Q6GhoapevbomTZqkY8eO6ZtvvsnJl4LsdvfoWhky6GCQipTj6FonUqJECXXq1Elz5szRt99+q/Hjx8vFxUVvv/22QkNDrV6O0WjU+PHjtXnzZi1cuFDlypVL1+fuF5Y///xTn3zyiby8vB643OPHjyslJcXi/ZaAfINsBnI31lGHqlu3rmJjY63uf+fOHdWtWzf33r6AzxMAwBHKRUphq6QiZc3bi/intZeLdExdyFZFihRRQECA/vjjD9N+l6SkJLM+58+fl7e3tyPKQ3bh+2a+w/5fSGLdz8dsOtO6Tp066Y5ks+TOnTu2LP6+y9u5c6defPFF9enTR4cPH5a/v79efvll0yXEY2NjlZKSosaNG5vmK1WqlKpWraoDBw4oLMzyh5h7muRBHF2br3l4eOipp55SgwYNtGPHDj300ENWzztu3Dht2LBBM2fOVNGiRU2XlSpWrJgKFSqk27dva9CgQTp8+LBmz56t1NRUUx9PT0+5u7vr5MmTWrduncLDw+Xl5aXffvtN0dHRql69uurWrZstrxnIE8hmIHdjHXWo5OTkTP1GMhqNSk5OVmpqajZWlQV8ngAAjlIuUir7VNq9LP9OSDvTyieMbY4Tu3Xrln777TfVq1dP/v7+8vHx0Z49e1S9enXT9H379mnYsGEOrhR2xffNfI39v/kY636+ZdOgdevWra0atLa38+fPKzk5WXPnztWQIUM0bNgw7dq1SwMGDNCiRYvUsGFDJSUlyc3NTZ6enmbzent7pzv6Dk7g7tG1+wdLyfH/ay/inxZaHF3r9H755RdNmDBBcXFxVs+zfPlySVKPHj3M2qOiohQZGam//vpL27dvlyQ99dRTZn0WLVqkRo0ayc3NTd99950WL16s69evy8/PT+Hh4RowYAC3IQDIZiB3Yx11qGeeeSZT/R3xuytT+DwBABzFxVUq1czRVSCbxMTEqHnz5vLz89OFCxc0a9YsXbt2TR06dJDBYFDPnj01e/ZsVahQQeXLl9fs2bNVqFAhtWvXztGlw974vpnvsf83n2Ldz5dsGrSOjo62dx1WuXtWQkREhJ5//nlJ0iOPPKIff/xRn376qRo2bJjhvNzTxIlxdC0y6ejRo/ed7u/v/8A+fn5+WrJkiT3LApwL2QzkbqyjDjFgwACb5vP19bVzJXbG5wkAANjZX3/9pX/961+6dOmSvLy8VLt2ba1cuVJly6ZdFv6ll17SzZs3NW7cOF2+fFm1atXS/PnzH3g/WuRRfN9EJrH/10mw7uc7Ng1aO4qXl5cKFCigypUrm7VXrlxZ+/fvl5R2RnVKSoouX75sdrb1+fPnVadOnRytFzmIo2sBIPchm4HcjXU0x9k6aJ0n8HkCACDfuXTpkk3zFS9eXC4uLvftM3Xq1PtONxgMGjhwoAYOHGhTDciD+L4J5E+s+/lKlget9+3b98A+DRo0yOrTSJLc3d1Vo0YNnThxwqz9jz/+MB1lFxwcLDc3N+3Zs0dt27aVJJ07d07Hjx/X8OHD7VIHAABwbt9//7127typAwcO6OzZs7px44a8vLxUpUoVNWrUSI8//nim7qUEAAAAAM7m0UcfzfStTAwGgz777DMFBQVlU1UAACCvyvKgdY8ePR745SQz9xq4fv26Tp48aXocHx+vuLg4eXp6qkyZMurTp4+GDh2qBg0aqFGjRtq1a5d27NihRYsWSZKKFSumjh07KiYmRl5eXvL09FRMTIwCAgIUGhpq24sEAAD5wueff645c+boxIkTKlKkiKpVq6bg4GC5u7vr8uXLOnr0qDZv3qzo6Gg9/vjjGjx4sOnAOQAAAADIbzp27KjSpUtb1Tc1NVUfffRRNlcEAADyqiwPWt8dLP6nixcvatu2bfrxxx81ZsyYTC0vNjZWPXv2ND2OioqSJHXo0EHR0dF67LHHNHbsWM2ZM0cTJkxQxYoV9e9//1v169c3zTNy5EgVKFBAQ4YM0Y0bNxQSEqLo6Gi5unKdeyCvWLBggVX9jh07ls2VAMgvIiMjFR8fryeffFIxMTEKDg62eMm6S5cuaevWrfr888/Vtm1bxcTEqE2bNg6oGAAAAAAcq0uXLqpZs6ZVfVNTUzVr1qxsrghAXsH+XwD3yvKgdcOGDS22t27dWmPGjNGuXbvUtGlTq5fXqFEjHT169L59OnXqpE6dOmU4vWDBgho9erRGjx5t9fMCyF1iYmKs7pvZS1EBgCXh4eHq06ePPDw87tuvRIkSpu8i+/bt0+XLl3OoQgAAAADIPd58881MXXnK1dVVb775pvz8/LKxKgB5Bft/Adwry4PW9/PYY4/pjTfe0KhRo7LzaQA4oW3btjm6BAD5zODBgzM9T4MGDbKhEgAAAADI/Xr16pUj8wBwTuz/BXCvbB20vnLlim7dupWdTwHASXGPWAAAAAAAAABwTuz/BXCvLA9anzlzJl3brVu3dPToUU2ePFm1atXK6lMAAADkqLVr12Y4zWAwqHjx4qpevbpKlSqVc0UBcDrXrl3TwYMHdfbsWd24cUNeXl6qUqWKAgICHF0aAADAA/3555/q0qWL3nnnHbVq1cpiny1btuitt97S0qVLVaVKlRyuEAAA5CVZHrRu0aKFxfsJGI1GVaxYUWPGjMnqUwDI53r27Hnf6QaDQQsXLsyhagDkB2+88Ybp+43RaDS1/7PNxcVFTz31lN555x0VKJCtF68B4ERu376tr776Sp9++ql+/PFH3blzJ13OlChRQu3bt9ezzz6rChUqOK5YAACA+1iyZImqVKmS4YC1lHb7yEWLFmnJkiUaO3ZszhUHIE9h/y8AyQ6D1u+99166QeuCBQuqbNmyqlGjhlxcXLL6FADyuX/uyL3r0qVLOnHihB566CF25gKwS/rCTgABAABJREFUu88++0xDhw5V48aN1a5dO5UsWVJJSUnasGGD9uzZo7Fjx+rQoUOaMWOGypYtqwEDBji6ZAB5wLZt2zRx4kSdPn1aoaGhGjp0qIKCgvTQQw+pYMGCunz5sk6dOqWDBw9q69atWrp0qTp27KghQ4booYcecnT5APKg9u3bZ3oeg8GgqVOnqnLlytlQEQBn8vXXX6tPnz4P7Pfkk0/q448/zoGKAORV7P8FINlh0DoyMtIedQBAhhYvXmyx/cSJE3r11VcZLAJgd4sWLdJjjz2m119/3dRWqVIlNWzYUNHR0Vq9erU++OADXb58WevXryeHAFjljTfeUK9evdStWzeVLFnSYp9atWqpXbt2euutt/Ttt99q1qxZWrZsGTkDwCbHjx9X9erVVbRoUavn+eGHH3Tjxo1srAqAs0hISFDFihUf2K9ChQr666+/cqAiAHkV+38BSHYYtAYAR6lYsaL69OmjSZMm6bPPPnN0OQCcyM6dOzVt2jSL05o2bapBgwZJkh599FEtWbIkJ0sDkIdt27ZNxYsXt7p/SEiIQkJCdOXKlWysCoCzGzt2rGrWrGlV39u3bys4ODibKwLgLFxdXZWSkvLAfikpKVyNE4BN2P8L5C82fVuYMGGCzpw5Y3V/o9GoCRMmcEQdALsrW7asjh8/7ugyADiZ1NRUnTx50uK0P//803TZKnd3d7m7u+dkaQDysMwMWNtjPgCIiIiQp6en1f1dXV0VERFB7gCwir+/vw4ePPjAfgcOHJC/v3/2FwTAKbH/F8g/bBq0Xrp0qZKSkqzuf+fOHS1dulTnz5+35ekAIEObN2+Wr6+vo8sA4GSaNGmiDz74QHv27DFr37Vrl6ZNm6bGjRtLkn7//XeVLVvWESUCAAA80IwZM1S+fHmr+xsMBs2YMUPlypXLxqoAOIvw8HAtWbJE586dy7DP2bNntXTpUjVv3jwHKwPgTNj/C+QfNl0e3Gg0aubMmfLy8rJ3PQCQzptvvpmu7datWzp27Jh+/fVXDR8+3AFVAXBmo0aNUo8ePfTiiy+qaNGiKlmypM6fP6/r16/r4Ycf1qhRo0x9e/Xq5cBKAeRVLVq0kMFgsDjNxcVFxYoVU40aNdSzZ09Vrlw5h6sDAOhOqpS4S/o7QSrsJ/mESS6ujq4KyFV69+6t1atXq2vXrho+fLiaN2+uQoUKSZJu3Lih7du36/3335fBYNDzzz/v2GIB5Grs/wUg2ThoXaZMGR07dixT8/j5+XH5TAA2+f7779O1FSxYUGXLllXfvn3Vvn17B1QFwJmVKlVKX3zxhdasWaMffvhBly5dUvXq1dWgQQN16NBBhQsXliR1797dwZUCyKsaNmyovXv36ty5c6pbt668vb2VmJioAwcOyNfXV35+ftqyZYu++OILLV68WDVq1HB0yQCczOeff67/+7//k9FoVNOmTdWxY0er5122bJmWL1+u06dPS5KqVq2qV199VeHh4ZLSTnaYPn26VqxYoStXrqhWrVoaM2aMqlatmi2vxe5OrZH2D5aS4//XVsRfqjdNKhfpuLqAXKZkyZKaPXu2Xn31Vf3rX/+Sq6ur6SSnixcvKjU1VSVLltSsWbP00EMPObhaALkZ+38BSDYOWm/fvt3edQBAhsgcADnpxo0bGjVqlLp166bu3bszMA0gWzRp0kQHDx7Uli1b5OfnZ2o/c+aMXnjhBbVs2VLR0dHq0aOHPvzwQ82ZM8eB1QJwNtOnT9dnn32mxx9/XMnJyZowYYJOnjypoUOHWjV/6dKlNWzYMD388MOSpLVr16p///76/PPPVbVqVc2dO1cLFixQdHS0KlSooFmzZql3797atGmTPDw8svOlZd2pNdKuTpKM5u3Jp9Paw1YxcA38Q82aNbVp0yatXLlSe/bsUUJCgiQpICBAYWFh6tSpU+5f7wE4HPt/AUg2Dlojm3EJKiD/Yb0Hco1ChQpp27Zt6tq1q6NLAfIetmdW++ijjzRw4ECzAWsp7apW/fv318yZM9WhQwc9//zzeu+99xxUZT7D5xdO6NatWxaverdmzRotWrTIdL/rOnXqaPLkyVYPWrdo0cLs8dChQ7V8+XIdPHhQVapU0aJFi9SvXz+1atVKkhQTE6PQ0FBt2LAhd3/HupOadob1vQPW0n/bDNL+IVLZp8gH4B88PDz0wgsv6IUXXnB0KQD+ie+3uQv/P4AHYtA6t+ESVIAk6dKlSypevLhcXFyydZ5cgfUeyHWqVaumY8eOqUGDBo4uBcg72J5lyp9//pnhWUfFixc3XXK3bNmy+vvvv3OytPyJzy+c1JNPPqnx48erYcOGZu23bt1SsWLFTI+LFi2q1NRUm54jNTVVmzZtUnJysurUqaP4+HglJiaqSZMmpj7u7u5q0KCBDhw4cN9B64SEBEVERGQ4fdu2bTbVaLXEXeY5kI5RSj6V1q9Us+ytBQCArMgD32/Z/5u7/n8AuUEeW7Od3N1LUN37A+nuJahOrXFMXYADhISEKDY21ur+qampCgkJUVxc3AP7zp49Wx07dlSdOnUUEhKiV199Vb///rtZH6PRqA8//FBNmjRRzZo11aNHDx0/ftysz61bt/TOO++oUaNGql27tvr166e//vrL6polsd4DudSwYcM0b9487d2719GlAHkD27NMK1OmjD7//HOL01avXm06A/vSpUvy9PTMydLyHz6/cGKdOnXSyy+/rJEjR+ry5cum9latWunFF1/U0qVLNXfuXE2YMEGPPfZYppZ99OhR1alTRzVq1NDbb7+tGTNmqEqVKkpMTJSUdq/bf/L29lZSUlLWX1R2+jvBvv0AJzdhwgSdOXPG6v5Go1ETJkzI/L4TAJmTR77fsv83d/3/AHIDzrTOLbgEFWDGaDQqMTHR6h8/qampMhotrT/p7d27V927d1eNGjWUmpqqqVOnqk+fPtq4caOKFCkiSVbdg+3dd9/Vjh07NHXq/7N35/Ex3fsfx9+DRKxJKgkhWpTEJfYltoitqpaWWKp10VYptZRbS61FaRKKapWGorVUo6jabquoW9UWVUpU0dvFFiRqiSUSMb8/8stcIxMmyUxmMnk9Hw8P5pwz53wy5rzPyfd7zvfMkZeXlyIjI/XSSy9p3bp1KljQiv2U/R5wWlOmTNH169fVt29flSxZUn5+fmbzDQaDNmzY4KDqACfD8Sxb+vXrp0mTJqlnz55q166dqTPniy++0M8//6ypU6dKkvbs2aPg4GAHV+vC+P7Cxb344otq166dJk+erCeeeEJjx45Vp06dNGbMGC1YsEDr1q2T0WhUly5dNHDgwCytu2LFilq/fr2uXr2qrVu3asyYMVqxYoVpvsFgMFvemt/X/P397X839f0U8X/wMllZDnBxK1eu1JNPPqmyZctatfydO3e0cuVKdenSRWXKlLFzdUA+lYfOb2n/da7/D8AZ0GntLBiCCshgyJAhWVr+3kaRzCxevNjsdUREhBo3bqwjR46oQYMGMhqND3wGW2JiotauXasZM2aoSZMmkqSZM2eqRYsW+u677xQaGvrgQtjvAafl5eUlLy8vR5cB5A0cz7KlR48epiv7IyMjTdN9fHw0ZcoUde/eXZI0cOBAi8+jhY3w/UU+EBAQoA8++ECbNm1SZGSk1q9frylTpmj48OEaPnx4ttfr7u5ueiZ2jRo1dPjwYS1btkz9+/eXJCUkJJhd+Hfx4kX5+Pjk6GexO9/QtKE6b5yR5cZlQ9p8Xyt+3wPyAaPRqPnz58vb29vRpQBIl8fOb2n/da7/D8DR6LR2FgxBBZiJiIjI1vsCAgKy/J7ExERJMg29ac0z2GJjY5WSkqKmTZualildurSqVKmiAwcOWDxpuff5bC0rndOEllYUyH4P5Lrly5c7ugQg7+A8NstSU1N18uRJPfHEE+rRo4d+//13Xb58WV5eXqpUqZJZQ4zTd/DkdXx/kY907NhRoaGhmjFjhjp16qSXX35ZL7zwgnV3CVnBaDQqOTlZAQEB8vX11e7du1WtWjVJaUNr7tu3TyNHjrTJtuymQMG0Z0vu6ibJIPOO6//P5npvcycU8P/Kli2r48ePZ+k9/v7+XJAH2FMeOr+l/fcuTvD/ATgDm3Za37lzR7du3VKRIkVsudr8gSGoADNdunTJle0YjUZFRESoXr16CgwMlKT7PoMtfbiahIQEubm5ZXjGZFae0/b3jcLWFcl+DwBwZpzHZpnRaFSHDh20YMEChYWF6dFHH3V0SfkX31/kA9euXdPBgwd169Yt1ahRQ9OnT1fnzp31+uuva+PGjZo2bZpq1qyZpXXOnj1bzZs3V5kyZXT9+nVt2bJFe/fu1QcffCCDwaA+ffooOjpaFSpU0COPPKLo6Gh5eHioY8eOdvopbah8uBS6Jm0oz7vvjCoakNZhXT7cYaUBzmbHjh2OLgHAvfLQ+S3tv3dxgv8PwBnkqNP61q1b2rx5s3bu3KmffvpJf//9t4xGo9zd3fXoo4+qUaNGevLJJ1W1alVb1eu6GIIKcIipU6fq+PHj+vjjjzPMy84z2O63TIbns91JlTZUYL8HnNTly5f14Ycf6ocfftClS5fk7e2tJk2aqG/fvhl+YQHyNc5js6xQoULy8fGx+nlssCO+v3BxP/30k15++WXdunVL7u7uunnzpiZOnKju3bvr888/1/vvv68+ffqoW7duGjFihIoVK2bVehMSEjR69GhduHBBJUqUUFBQkD744APTnUj9+/fXrVu3NGXKFF25ckW1atXSkiVLTM+HdHrlw9OeLRm/K+3OpyL+aTnAHdYAAGfH+W0GtP8CeUeB7LwpKSlJ8+bNU2hoqMaPH6///ve/aty4sfr06aMBAwaoc+fO8vb21qeffqouXbro2Wef1YEDB2xdu2tJH4JKkmnIKROGoALs4Y033tCOHTv00UcfqUyZMqbpvr6+kpThirm7n8Hm4+OjlJQUXblyJdNlHoj9HnBa58+fV3h4uN5//30lJiaqbNmySkxM1Pz589WlSxedP3/e0SUCzoPjWbZ06NBB69evd3QZ4PsLFzdt2jS1bt1a+/bt0549ezRmzBhNmzZNSUlJcnNz09ChQ7Vu3Tr9+uuvat++vdXrffPNN7Vjxw7Fxsbq+++/14cffmg2dKbBYNDQoUP17bff6vDhw1qxYoXpzqY8o0DBtGdLVngm7W9yAACQF3B+a4b2XyBvyVanddu2bbVx40YNGjRIu3bt0ubNmzVz5ky99tprGjFihKZMmaLFixdr7969WrJkicqVK6c+ffro008/tXX9riV9CKqi5cynFw1Im84QVIBNGI1GTZ06VVu3btVHH32k8uXLm82/+xls6dKfwVanTh1JUnBwsNzc3MyWuXDhgk6cOGFaxirs94BTmj17tpKSkrR69Wpt3rxZS5cu1ebNm7V69WrdunVLc+bMcXSJgHPheJZlVatW1YEDB9SnTx+tWLFCX375pbZu3Wr2B7mE7y9c2J9//qmOHTuqUKG0gfaefPJJ3bp1S3Fx/3tuYqVKlbRixQoNHTrUUWUCAABb4vyW9l8gj8rW8ODDhg1Tly5dVLDg/a/+MBgMaty4sRo3bqxhw4aZngWA+2AIKsDupkyZok2bNmn+/PkqVqyY6RkmJUqUkIeHh1XPYCtRooS6du2qqKgoeXt7y9PTU1FRUQoMDFSTJk2yVhD7PeB0du3apeHDh2d4vmPNmjU1bNgwzZ07N5N3AvkYx7MsGTNmjKS0kR327t2bYb7BYNDRo0dzu6z8i+8vXFTVqlX18ccfKzAwUB4eHvrggw9UokQJBQQEZFi2W7duDqgQAADYRT4/v6X9F8ibstVpnZ1fZMqXL5/hahZkIn0IKgB2sWrVKklS7969zaZHREQoPDztyjZrnsE2btw4FSpUSMOHD1dSUpIaN26syMjIB17QYxH7PeBUEhMTVa5cOYvzAgIClJiYmMsVAXkExzOrLVu2zNEl4F58f+GCpkyZoiFDhqhZs2aSJE9PT0VERMjNzc3BlQEAALvLx+e3tP8CeVO2Oq0BIC87duzYA5dJfwbb/YbIK1y4sCZOnKiJEyfasjwATiAgIEA7d+40ezZjum+++cbi3UkAkBUNGzZ0dAkA8oEqVapoy5Yt+vPPP5WcnKxKlSqpcOHCji4LAADArmj/BfKmHHdaz5s3L9N5BQoUUMmSJRUcHKzatWvndFMA8rETJ07op59+0vnz55WUlCRvb29VrlxZDRo0MLv6DQBsITw8XLNmzZLRaFTnzp3l6+ur+Ph4bdiwQStWrNCrr77q6BIB5HGpqak6cuSIzp49K4PBIH9/f1WvXj17V+wDwH0ULFhQjz76qKPLAJBPHDhwQHv27NHly5fl5eWlhg0bqm7duo4uC0AeQPsvAJt0WhsMBhmNxgzz0qcbDAY1aNBACxYsULFixe67vn379mnx4sWKjY1VfHy83nvvPbVp08bispMmTVJMTIzGjh2r5557zjQ9OTlZUVFR2rRpk27duqVGjRpp8uTJKlOmTI5+VgC568qVK4qJiVFMTIzOnj1rMWcKFSqk5s2bq3fv3mrcuLEDqgTgil588UWdOnVKK1as0MqVK03TjUajevTooX79+mVpfVk5v5GkrVu3atWqVTp69KiSk5NVpUoVDRkyRKGhodn+mQA4j0WLFmnx4sW6cuWK2XRPT0/1798/yxmTbuXKlVq8eLHi4+NVpUoVjRs3TvXr1890+Q0bNuiDDz7QX3/9pRIlSig0NFSjR4+Wt7d3trYPwPkcOXJEjz76qDw8PLL0nsqVK3NHNoAsSUpK0ogRI7Rz506z9huDwaCwsDC9/fbbWcoiAPkD7b8A7pbjTuuvvvpKL774orp27aqOHTvKx8dH8fHx2rx5s9auXauZM2fqr7/+0uTJkzV37lyNGzfuvuu7ceOGgoKCFB4eft9hGbZt26aff/5Zfn5+GeZNnz5dX3/9tebMmSMvLy9FRkbqpZde0rp167hzAcgjli1bpvfee0+S1L59ezVs2FDVq1fXQw89pMKFC+vKlSs6deqUDh48qO3bt+uFF15QkyZNNGnSJD3yyCMOrh5AXmcwGDR16lQ999xzZncJNGrUSBUrVszy+qw9v0m3b98+NWnSRCNGjFDJkiW1bt06DRo0SKtXr1a1atWy8yMBcBKvvvqqNm/erEqVKqlbt24KCAiQ0WjUmTNntH37dr311lv65ZdfNGvWrCytd8uWLYqIiNDrr7+uunXr6pNPPlH//v21efNmlS1bNsPyP/74o8aMGaOxY8eqZcuWOn/+vCZPnqwJEyaYzsEA5H3dunVTTEyMatasadXyqamp6tatm9asWaPq1avbuToArmTmzJnatWuXhg8fro4dO5pGq9q4caPmzZunmTNnMrwuADO0/wK4V447radPn66nnnpKAwYMME0rV66cBgwYoNu3b+udd97RBx98oJMnT2rt2rUP7LQOCwtTWFjYfZc5f/68pk6dqsWLF+ull14ym5eYmKi1a9dqxowZatKkiaS0k6YWLVrou+++4w4lII9Yvny5xo4dqw4dOsjNzS3DfB8fH/n4+KhOnTp6/vnndfLkSS1YsED//ve/NXDgQAdUDMAVVapUSZUqVcrxeqw5v7nb+PHjzV7/61//0vbt27Vjxw46rYE8bOPGjdq8ebNeffVV9e/fP8P8kSNHauHChZozZ45atWqlDh06WL3upUuXqmvXrurevbuktBz59ttvtWrVKouPNPj5559Vrlw59enTR5JUvnx5Pf300/rggw+y+dMBcEZGo1G//PKLbt26ZdXyqampFu9wAoAH2bJliwYNGmTWVluuXDkNHDhQt2/f1ooVK+i0BmCG9l8A98pxp/WePXvUt29fi/Pq1KmjRYsWmf69YMGCnG5Od+7c0ahRo9SvXz9VqVIlw/zY2FilpKSoadOmpmmlS5dWlSpVdODAgUw7rePi4tS6detMt7t9+/Yc1w7Aev/+979VqJD1EfXwww8rIiJCqampdqwKQH5z8eJFnTlzxmJDb4MGDXKtjjt37uj69evy8vK673KczwDObfXq1erUqZPFDut0AwYM0PHjx/XJJ59Y3WmdnJysI0eOmF1ILElNmzbVgQMHLL6nTp06mjNnjv7zn/+oefPmunjxor788sv7XmBDxgB505QpU6xeNv0RbwCQVUlJSZk+u7pu3bpavHhxLlcEwNnR/gvgXjnutHZ3d9cvv/xi8VkCsbGxcnd3l5TW2Fq0aNGcbk6LFi1SoUKFTHcE3CshIUFubm7y9PQ0m+7j46OEhIQcbx9A7sjKCcvdeAQAAFu4cOGCRo8erT179kiS6Y4jg8Fgasw9evRortWzZMkS3bx5U0888USubROA7R07dkzPP//8A5fr0KGDxowZY/V6L126pNTUVJUqVcpsevqjmyypW7eu3nrrLQ0fPlzJycm6ffu2WrVqxR1QgItZtmxZtt6XncehAMjfatWqpcOHD1tsIz58+LBq1KjhgKoAODPafwHcK8ed1q1bt9a7776rEiVKqF27dipZsqSuXr2qLVu2aP78+Wrfvr0k6fjx43r44YdztK3Y2FgtW7ZM69aty/KVvw8a3srf3587AwAAgCTpjTfe0NGjRzVy5EgFBQWZLsJzhE2bNmnevHmaP39+hg6pe3E+Azi3pKQklShR4oHLFS9eXElJSVle/72/I93vjsnffvtN06ZN0+DBg9WsWTPFx8drxowZev311/Xmm29afA8ZA+Q9DRs2dHQJAPKJCRMmaMCAASpWrJg6duwoT09PXblyRRs3blRMTIyio6MdXSIAAHByOe60Hjt2rP78809NmjRJr7/+ugoWLGh6BlLdunX12muvSUobonvIkCE52taPP/6oixcvqmXLlqZpqampioqK0rJly7Rjxw75+PgoJSVFV65cMbvb+uLFi6pTp06Otg/AMapWrZppg6vBYFCJEiUUHBys/v37q1GjRrlcHQBXtHfvXo0ePVpdu3Z1aB1btmzR+PHjNXfuXDVp0sShtQDIudKlS+v48eMPfLzAr7/+qjJlyli9Xm9vbxUsWDDDyFIXL16Uj4+PxfdER0erbt26evHFFyWlnW8VKVJEvXr10vDhw+Xn52f19gEAALp3767bt29r2rRpmjZtmqmNWEq7m/Lpp582LWswGLR//35HlQrACdH+C0CyQad1iRIltHLlSn3zzTfat2+fLl++LC8vLzVo0EDNmzc3BY21z2O7n6eeeipDg22/fv301FNPKTw8XJIUHBwsNzc37d6923SX94ULF3TixAmNGjUqxzUAyH2DBw/W+vXrdf36dbVs2dI01OXOnTtVrFgxtWrVSt9//71eeOEFLVq0yOyZ9gCQHQaDQf7+/g6tYdOmTRo3bpxmz56tFi1aOLQWALYRGhqqDz74QE888YQeeughi8v8/fffWrx4sdq0aWP1et3d3VW9enXt3r1bjz32mGn6d999l+kzqJOSkjIMq5f++kGjVAEAANzr8ccfz/LImACQjvZfAFIOO62TkpL03HPPadiwYQoLC1NYWFiOC7p+/bpOnjxpen369GkdPXpUnp6eKlu2rLy9vc2Wd3Nzk4+PjypVqiQprRO9a9euioqKkre3tzw9PRUVFaXAwEDuUALy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YblFixapZcuWqlGjhrp06aL//Oc/Vv+cn332mVJTUzNcZff+++/L09NTY8aMyfCeokWLauLEibp586bZ8AXpwx8cO3ZML7zwgurUqaPnnntOUtoQNOPGjVPDhg1Vp04dDRgwQKdOnbI4VEqnTp107do1i8PVWCt9KI2NGzdq5syZatasmerUqaOBAwcqISFB165d08SJExUSEqKQkBCNHTtW169fz7Cer776SkWLFlWTJk0k/W/oje+//14TJkxQSEiI6tatq9GjR+vGjRuKj4/XK6+8ovr166tZs2aKiopSSkqK2Trv/ZnT1/nDDz/o9ddfN9U0ZMgQnT9/PtufAZwfOUPOSOQM7IeMIWMkMgb2Rc6QMxI5A/siZ8gZiZyB/ZAxZIxExsC2LA0P3rt3b3Xs2FGHDh3Ss88+q1q1aql169ZauHCh2dDblly7dk39+vVTkyZNdOjQIXuXL0lKTk7WmjVr1LFjxwfeZZ2+H27atElz5sxRs2bNVLduXT333HP6/fffMyy/Zs0aPfnkk6pRo4YaNmyowYMH67///a/ZMs6UM5kN957+c+/ZsyfL6zQajfrqq6/0+OOPm6alH2O+/vprde7cWTVr1tQTTzyhr7/+2lTHE088odq1a6tbt246fPiw2TotDQ+evs5vvvlGXbp0Uc2aNdWuXTutWbMmyzXnlG3GsMjnateurU8//VTTpk1Tp06dVK1aNbm5uWW6/NGjR/Xmm29qyJAh8vHx0caNGzV9+nSlpKSoX79+kqSbN2/qn//8p86dO6eBAwcqKChIJ06c0DvvvKPjx4/rww8/lMFgkCRNmjRJn332mXr37q2RI0fqypUreu+999SzZ099/vnn8vHxkZQ2tMu8efPUrVs3Pf744zp37pwmTpyoO3fuqGLFig/8OXfu3Kl//OMfKlmypGnahQsXdOLECbVv315FihSx+L46deqoVKlS2r17t9n0lJQUDRo0SD179lT//v2VmpqqO3fuaODAgYqNjdWQIUNUvXp1HThwQC+++KLFdfv6+qpSpUr6z3/+o27duj3wZ7ifOXPmKCQkRBERETpz5oyioqL0r3/9S4UKFVJQUJBmz56tX375RXPmzFGxYsU0YcIEs/dv3bpVLVq0kLu7u9n0CRMmqG3btmbvT01N1R9//KHHHntMTz/9tL777jstWrRIfn5+ev755x9Y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/36Ly6emplr8vIsUKSI/Pz+dPHnS4tDgro5OaxsYNGiQtm/fro8++kgXLlxQSkqKfH191aRJE7300kumh52n+8c//qHw8HC9++67+vPPP+Xn56exY8eaXYlWtGhRrVy5UgsXLlRMTIxOnz4tDw8P+fv7q0mTJmbDuUydOlW1atVSTEyMVq1apTt37sjPz09169ZVzZo1Tcu98sorKlq0qD7++GN9/vnnqlSpkqZMmaIlS5ZY9XN26tRJa9eu1aFDh8zWK0ljxoxRo0aNtHLlSr3++uu6fv26HnroITVq1Ehz5861+gqVAgUK6P3331dkZKQWLVqklJQU1a1bVzNnzlSPHj1UsmRJs+X37NmjxMREderUyar129qNGzf07bffavLkyQ7ZPvIPcoacIWdgT2QMGUPGwN7IGXKGnIG9kTPkDDkDeyJjyBgyBs6sQIECevPNN1WsWDGNGjVKN2/eVPfu3bO0jiNHjuiVV17JML1Lly6ZDo3v7++v+vXra/v27Xr66aezVbslL730kh566CEtX75cW7ZskYeHhxo2bKh//etfqlChwgPf74icKViwoN5//3298cYbev311+Xu7q4OHTpo0qRJGjBgQIblb926ZfHzLleunJ588kmVK1dOwcHB2a4nzzIiV7Vs2dI4YMAAR5eRbR07djROmjQp17e7YcMGY2BgoHH//v1m00eOHGl8+umnrVpHy5YtjaNHjzampKQYU1NTbVLX5s2bjdWqVTNeunTJJuuzpdTUVGNKSoqxV69exg4dOji6HOQiciZ7yJmsI2fyJzIme8iYrCNj8i9yJnvImawjZ/IvciZ7yJmsI2fyJzIme8iYrCNj4Ay++OIL4z/+8Q/juXPnHF3KAzlrztzriSeeMEZERNhl3TmVkpJi/O6774yBgYHGf//73zZfP3daI0tGjRqlIUOGaNCgQSpTpoxdtrFp0yadP39egYGBKlCggA4ePKjFixerQYMGqlu3rmm5kydP6t///rc+/PBDq9e9fv16rV+/Xr169dKkSZNyXGv79u3Vvn37HK/HHoYMGaLt27dLkqpUqeLgagDrkTPmyBnAtsgYc2QMYHvkjDlyBrA9csYcOQPYFhljjowB7q9t27aqUaOGoqOjbbLP2Upeypl73e/Z24509OhRde7c2a7bMBiNRqNdtwAzrVq1UpUqVRQdHe3oUrJt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Hcu2vOvXr+uZZ57Rvffeq7lz5youLk43b97U4MGDlZiYqCeffFKzZ89WZGRknleZBAcHq1q1avrb3/5WoO/BKP/85z+VkpKS41cZ//nPfzR9+nQNGjRIs2bNUsmSJRUXF6d33nlHH374oZ5//nm99dZbunTpkgYPHqy0tLQ7rus///mPpk6dqv79+2vu3LmqVauWxo4dq7179xr18WAScgw55hZyDIxCniHP3EKegRHIMeSYW8gxcDUr5J8vv/xSkhQbG5vne2NjY3Xjxg3t2bMn2/T3339fX331lUaNGqUFCxaoWrVq+vzzzxUXF6cyZcro7bff1gsvvKBNmzbpo48+ynXZTZo00YkTJ3T06NFCfkLn2rp1q+67774cv7YaN26cSpYsqenTp+uZZ55RYmKiXn75ZT399NOqXbu23nnnHXXt2lWLFy/WsmXLHFrXuHHjVLRoUU2bNk0jR47Unj179MILLxjxseCByDPkGfIMjESOIcd4Q47h9uBOdv78eS1YsCCr8zdv3lx79uzRxo0b1bVr1xztDx8+rKeeekp333235syZozJlyuRo07t37xzTvv3220Lf237//v2SpLp16zr8nho1auitt97Keu3j46Phw4frm2++UYMGDXTx4kXNnTtXrVu31rRp07LaNW3aVO3bt9esWbOyTb9+/bqGDBmiHj16ZE3729/+pn379mnChAl69NFHJUktWrSQn59ftvf+Xp06dfT3v//d4c/hCp988okiIiIUHh6ebfr58+e1evVqVahQQZJUoUIFPfzww1q9erW2bduW7ZZ9Q4YM0Zdffqm2bdvedl3nzp3TypUrFRoaKkmKjo7WV199pY0bNyo6Otq5HwymIseQY24hx8Ao5BnyzC3kGRiBHEOOuYUcA1ezQv45efKkJKly5cp5vvfWLSxvtb3lrrvu0qJFi1S0aNGsacOHD1eFChW0aNGirCJpTExMnn3qVjz//Oc/c/x6ySznzp3Tnj17NGPGjBzz2rRpo1GjRknKzJMHDhxQYmKixowZk3XniubNm+uLL77Qxo0bNWDAgDuuLyYmRuPGjct6feHCBb355ptKSUlRcHCwcz4UPBZ5hjxDnoGRyDHkGG/IMQxaO1lwcHCOq1Vq1aqlw4cP52j7xRdfaMyYMWrTpo3i4+PzvP/+1KlTVb169WzTnPEw9tOnT8tmszn80HlJOTr7rY598uRJNWjQQPv371daWpq6deuWrV1ISIjuvfferFs2/N7vbwcnKesKmg4dOmSb3qlTpzyLMOXKldPZs2d148YNpz2ovrC2bduWrbh0y5/+9KesAowkVatWTVJmoer3BZhbf/M/Jufc/OlPf8oqwEiZCTw8PNyh98KzkGPIMbeQY2AU8gx55hbyDIxAjiHH3EKOgatZPf/ccut2lzabLdv0tm3bZivypqam6tChQ/p//+//Zft8xYsXV9u2bbVu3bocyy5XrpwkudUtJHfs2KGiRYsqJiYmx7z77rsv2+vq1atr+/btat26dY7pX3zxhUPru12ed4dCL8xFniHPkGdgJHIMOcYbcox7nK1aSG5Xq/j5+enatWs5pm/fvl3Xrl3To48+etsHxlevXl3169d3ZpiSpGvXrqlIkSLy9fV1+D1//Hy34r51S7bz589LUq4bd/ny5bNu/3BLsWLFVKJEiWzTzp8/ryJFiuRYV1BQUJ5x3XXXXbLb7VmfyWwHDx7UyZMnc9zqTpJKly6d7fWt7/CP028l4Ny2nT/Kz3YHz0aOOS+JHEOOgZHIM+clkWfIMzAKOea8JHIMOQZmsEL+uXXxxfHjx3MUmG85ceKEpMyLYX7vj3nn4sWLstvtWcXb38ttmvRbf3SnvvPJJ5+oVatW2S5quSWvvJHb9PT0dIfWd6c8D+9GniHPkGdgJHIMOcYbcgzPtDbRmDFj1KpVKw0aNMjhqyCcKTAwUNevX1dqaqrTlnlrg09JSckx7/Tp0zmurPnj1TK3lnHjxo2sgs4tuS3zlvPnz8vPz0/FixfPf9DKLOJIytGxz507V6DlffLJJwoPD1dERESB3g84AzmGHAMYjTxDngGMRI4hxwBmcdf807x5c0mZv8jJy44dO1SkSBE1adIk2/Q/5pNSpUrJZrPp7NmzOZZx5syZXJd94cKFrPgKys/PL9eiakHyyaVLl/T3v/891wtgAHdHniHPAEYix5BjPBWD1ia66667NGvWLN1333165plntH37dpeuv2rVqpKkn376yWnLbNiwofz9/bVhw4Zs03/++Wd99dVXuvfee++4jFvJaPPmzdmm//H17yUlJalGjRoFiDhTpUqVJElHjhzJNv3TTz8t0PK2bt2a4zZ+gKuRY3JHjgGchzyTO/IM4BzkmNyRYwDjuWv+uf/++1WjRg3Nnz9fP/zwQ473bd68WV988YV69ux5x9s7BgQEqF69etq+fXu2wuuVK1f02Wef5fqe48ePS1Kev4xyRKVKlXLkkh9++CHXz3Mnn376qWw2W45bZwKegDxDngGMRI4hx3gq8+8L5uWKFi2q6dOna+zYsRo2bJimTp2qzp0753s5KSkp2rJlS47plSpVyvP2Dk2bNpUk/etf/1Lt2rXzvc7clCpVSs8++6ymT5+uF198UZ06ddL58+c1Z84c3XXXXRo6dOgdlxETE6NGjRpp6tSpunz5surVq6f9+/fr448/lpTzipqbN2/q4MGD6tmzZ4Hjrl+/vqpWrao33nhDGRkZKlWqlLZv3659+/bl2j4jIyPX77tYsWIqX768fvrpJ66OgVsgx+REjgGcizyTE3kGcB5yTE7kGMA13DH/+Pr66p133tETTzyhPn36aMCAAWrQoIHS09P12WefafXq1WrSpIlGjx7tUGzPPfecnn76aQ0cOFD9+/dXRkaGFi1apOLFi2f9Eun3/vWvf8nX11fR0dEOLT83Dz/8sF544QVNmDBBDzzwgE6cOKGFCxfm+Yuno0eP5vr91a9fX5988olatGiR4zEKgKcgz5BnACORY8gxnohBazfg4+Oj119/XcWLF9cLL7ygq1evqlevXvlaxrfffqthw4blmN6tWzfFx8fn+p6QkBDdc8892rFjhx555JECxZ6bp59+WmXLltX777+vzZs3y9/fX02aNNHzzz+v8PDwO77fx8dH7777ruLj47VgwQJdv35djRo10ptvvqnevXurVKlS2dp//fXXunTpkrp06VLgmH19ffXuu+/q1Vdf1SuvvCI/Pz916tRJ48eP11NPPZWj/bVr13L9vitVqqSHHnpIlSpVUr169QocD+BM5JjsyDGA85FnsiPPAM5FjsmOHAO4jjvmn+rVq2v9+vVavHixPv74Y82dO1e+vr6qUaOGXnrpJfXu3TvreYd30qpVK82aNUszZ87U8OHDFRwcrEcffVSnT5/OcUcIKfP5mK1atcqRZ/KjS5cuOn36tD744AOtW7dONWvW1IQJEzRnzpxc269fv17r16/PMX38+PH64osvNGHChALHArgD8kx25BnAucgx2ZFjPIAdXm3Lli32P/3pT/aff/7Z7FDuaMOGDfaIiAj7vn37sk0fOXKk/ZFHHnFoGW3atLG/+OKL9uvXr9szMjKMCNPeoUMH+5QpUwxZdmFdv37d/uWXX9ojIiLs//d//2d2OPAC5BjnI8cA2ZFnnI88A/yGHON85BjAMWbln/T0dHvHjh3tAwYMyDb9xx9/tNeqVcv+xRdf3HEZa9eutUdERNh//PFH+/Xr1w2Jc9OmTfY6derYz507Z8jyCyMjI8N+/fp1e9++fe2dOnUyOxwgT+SZ2yPPAIVDjrk9ckzu+KW1l2vfvr3q16+vhIQEjR8/3uxwsiQmJurUqVOKiIiQj4+PDhw4oEWLFik6OlqNGjXKavfTTz/p//7v//Tee+85vOxbV7b07dvXkM98u2fJmenw4cPq2rWr2WHAy5BjyDGA0cgz5BnASOQYcgxgFlfln5deekktWrRQcHCwzpw5o5UrV+q///2vxo4dm63dvHnz1KxZM7Vo0cLhZd9///2SpL///e8qW7asU+Pu2LGjOnbs6NRlOsvQoUO1Y8cOSVLNmjVNjgbIG3nm9sgzQOGQY26PHJM7m91ut7t0jXA7R48e1aeffqqnnnpKPj4+ZocjSfrss880a9Ys/fTTT7p69aqCg4PVrl07DR8+PNv9/b/66iv9+OOPDt+u78iRI0pPT5cklStXTqGhoYbE747S0tL03XffZb2+++67Vbp0aRMjgrcgx3gHcgzMRJ7xDuQZmIUc4x3IMXBHrsg/w4YN0/79+/XLL7+oaNGiqlOnjp5++mm1atUqq82NGzc0f/58dejQQVWrVr3jMs+dO6ekpKSs13/6059UpIj3/G7mp59+ynqOpr+/PwNKcGvkGc9EnoGnIMd4JjNzDIPWAAAAAAAAAAAAAADTuMdl4gAAAAAAAAAAAAAAr8SgNQAAAAAAAAAAAADANAxaAwAAAAAAAAAAAABMw6A1AAAAAAAAAAAAAMA0DFoDAAAAAAAAAAAAAEzDoDUAAAAAAAAAAAAAwDQMWgMAAAAAAAAAAAAATMOgNQAAAAAAAAAAAADANAxaAwAAAAAAAAAAAABMw6A1AAAAAAAAAAAAAMA0DFoDAAAAAAAAAAAAAEzDoDUAAAAAAAAAAAAAwDQMWgMAAAAAAAAAAAAATMOgNQAAAAAAAAAAAADANAxaAwAAAAAAAAAAAABMw6A1AAAAAAAAAAAAAMA0DFoDAAAAAAAAAAAAAEzDoDUAAAAAAAAAAAAAwDQMWgMAAAAAAAAAAAAATMOgNQAAAAAAAAAAAADANAxaAwAAAAAAAAAAAABMw6A1AAAAAAAAAAAAAMA0DFoDAAAAAAAAAAAAAEzDoDUAAAAAAAAAAAAAwDQMWgMAAAAAAAAAAAAATMOgNQAAAAAAAAAAAADANAxaAwAAAAAAAAAAAABMw6A1AAAAAAAAAAAAAMA0DFoDAAAAAAAAAAAAAEzDoDUAAAAAAAAAAAAAwDQMWgMAAAAAAAAAAAAATMOgNQAAAAAAAAAAAADANAxaAwAAAAAAAAAAAABMw6A1AAAAAAAAAAAAAMA0RcwOwB3cc889Sk9PV3BwsNmhAG4nJSVFfn5++sc//mF2KB6NPAPkjTzjHOQZIG/kmcIjxwB5I8c4B3kGyBt5pvDIMUDeyDHOQZ4B8uZonmHQWtK1a9eUkZFhdhiAW7px44bsdrvZYXg88gyQN/KMc5BngLyRZwqPHAPkjRzjHOQZIG/kmcIjxwB5I8c4B3kGyJujeYZBa0nly5eXJO3YscPkSAD3Exsba3YIlkCeAfJGnnEO8gyQN/JM4ZFjgLyRY5yDPAPkjTxTeOQYIG/kGOcgzwB5czTP8ExrAAAAAAAAAAAAAIBpGLQGAAAAAAAAAAAAAJiG24N7o5sZUsou6WqyVCxECo6RfHzNjgoA3Bd5E3Ac/QWAkcgxAOCeyM8AjESOAQD35OT87LGD1qdOndKbb76pXbt2KS0tTeHh4XrttddUr149SZLdbtfs2bO1atUqXbx4UVFRURo/frxq1qxpcuQmO75O2jdMSk36bVpAmNR4plS5u3lxAYC7Im8CjqO/AG7FcudM5BgAcE/kZxjEcscyKBhyDAxCjgEKyYD87JG3B79w4YIeffRRFS1aVAsWLNCmTZs0evRolSpVKqvNggULtGTJEo0fP15r1qxRUFCQBgwYoMuXL5sYucmOr5N29cy+AUlS6onM6cfXmRMXALgr8ibgOPoL4FYsd85EjgEA90R+hkEsdyyDgiHHwCDkGKCQDMrPHjlovWDBAlWsWFFTpkxRZGSkwsLC1KxZM919992SMq+AWbp0qQYPHqz27dsrIiJCU6dOVVpamhITE02O3iQ3MzKveJA9l5m/Tts3PLMdAIC8CeQH/QVwO5Y6ZyLHAIB7Ij/DQJY6lkHBkGNgIHIMUAgG5mePvD34p59+qpYtW+q5557T3r17VaFCBf35z39W7969JUlJSUlKSUlRy5Yts97j5+en6Oho7d+/X3369MmxzOTkZMXGxua5zh07djj/g7hSyq6cVzxkY5dSj2e2q3Cfq6ICAPdF3gQcR38B3I6lzpnIMQDgnsjPMJCljmVQMOQYGMiIHCORZ+AlDMzPHvlL6+PHj2vlypUKDw/XokWL1KdPH02ePFnr16+XJKWkpEiSypUrl+19QUFBOnPmjKvDdQ9Xk53bDgCsjrwJOI7+ArgdS50zkWMAwD2Rn2EgSx3LoGDIMTAQOQYoBAPzs0f+0tput6tevXp6/vnnJUl16tTR999/r5UrV6pr165Z7Ww2W4735SUkJMTaV7kUC3FuOwCwOvIm4Dj6C+B2LHXORI4BAPdEfoaBLHUsg4Ihx8BARuQYiTwDL2FgfvbIX1oHBwerevXq2aZVq1ZNJ0+ezJovKccVL2fPnlVQUJBrgnQ3wTFSQJgkWx4NbFJA5cx2AADyJpAf9BfA7VjqnIkcAwDuifwMA1nqWAYFQ46BgcgxQCEYmJ89ctC6UaNG+uGHH7JNO3bsmCpVqiRJCgsLU3BwsHbv3p01Pz09XXv37lXDhg1dGqvb8PGVGs/89cUfN6RfXzd+O7MdAIC8CeQH/QVwO5Y6ZyLHAIB7Ij/DQJY6lkHBkGNgIHIMUAgG5mePHLTu37+//vWvf+ndd9/Vjz/+qI0bN2r16tX685//LCnzlg39+vVTQkKCtm3bpqNHj2rMmDHy9/dX586dTY7eRJW7SzFrpIBK2acHhGVOr9zdnLgAwF2RNwHH0V8At2K5cyZyDAC4J/IzDGK5YxkUDDkGBiHHAIVkUH72yGdaR0ZGavbs2Zo+fbrmzJmjsLAwvfTSS3rooYey2gwaNEjXrl3TxIkTdeHCBUVFRWnx4sUqUaKEiZG7gcrdpUoPSym7Mh+CXiwk8yf6XJEGALkjbwKOo78AbsOS50zkGABwT+RnGMCSxzIoGHIMDECOAZzAgPzskYPWktSmTRu1adMmz/k2m01xcXGKi4tzYVQewsdXqnCf2VEAgOcgbwKOo78AbsOS50zkGABwT+RnGMCSxzIoGHIMDECOAZzAyfnZI28PDgAAAAAAAAAAAACwBgatAQAAAAAAAAAAAACmYdAaAAAAAAAAAAAAAGAaBq0BAABMlpCQoFq1aum1117Lmma32zVr1iy1bNlSkZGReuyxx/Tdd9+ZGCUAAEDuOJYBAAAAUFgMWgMAAJjo4MGDWrVqlWrVqpVt+oIFC7RkyRKNHz9ea9asUVBQkAYMGKDLly+bFCkAAEBOHMsAAAAAcAYGrQEAAExy5coVvfDCC5o8ebJKly6dNd1ut2vp0qUaPHiw2rdvr4iICE2dOlVpaWlKTEw0MWIAAIDfcCwDAAAAwFmKmB0AAACAt5o0aZJat26t5s2ba968eVnTk5KSlJKSopYtW2ZN8/PzU3R0tPbv368+ffrkurzk5GTFxsbmub4dO3Y4L3gAAOD1OJYBAAAA4CwMWgMAAJhg06ZN+ve//601a9bkmJeSkiJJKleuXLbpQUFBOnnypEviAwAAuB2OZQAAAAA4E4PWAAAALpacnKzXXntNixcv1l133ZVnO5vNlu213W6/7XJDQkL4BRIAADAcxzIAAAAAnI1BawAAABf79ttvdfbsWXXv3j1rWkZGhvbu3avly5dry5YtkqQzZ86ofPnyWW3Onj2roKAgl8cLAADwexzLAAAAAHA2Bq0BAABc7N5779XGjRuzTRszZoyqVaumQYMGqXLlygoODtbu3btVp04dSVJ6err27t2rkSNHmhEyAABAFo5lAAAAADgbg9YAAAAuVqJECUVERGSbFhAQoDJlymRN79evnxISEhQeHq4qVaooISFB/v7+6ty5sxkhAwAAZOFYBgAAAICzMWgNAADghgYNGqRr165p4sSJunDhgqKiorR48WKVKFHC7NAAAADuiGMZAAAAAPnBoDUAAIAbeP/997O9ttlsiouLU1xcnEkRAQAAOI5jGQAAAACF4WN2AAAAAAAAAAAAAAAA78UvrQEAAOB5bmZIKbukq8lSsRApOEby8TU7KgAAYBT2/QAAAIClMWgNz8eJK+C96P+Adzq+Tto3TEpN+m1aQJjUeKZUubt5cQEAXINjQO/DvmTyDb0AAHBLSURBVB8A4AocYwDugb7otRi0hmfjxBXwXvR/wDsdXyft6inJnn166onM6TFryAEAYGUcA3of9v0AAFfgGANwD/RFr8YzreG5bp24/j55Sb+duB5fZ05cAIxH/we8082MzBOXPxatpd+m7Rue2Q4AYD0cA3of9v0AAFfgGANwD/RFr8egNTwTJ66A96L/A94rZVfOE5ds7FLq8cx2AABr4RjQO7HvBwAYjWMMwD3QFyEGreGpOHEFvBf9H/BeV5Od2w4A4Dk4BvRO7PsBAEbjGANwD/RFiEFreCpOXAHvRf8HvFexEOe2AwB4Do4BvRP7fgCA0TjGANwDfRFi0BqeihNXwHvR/wHvFRwjBYRJsuXRwCYFVM5sBwCwFo4BvRP7fgCA0TjGANwDfRFi0BqeihNXwHvR/wHv5eMrNZ7564s/5oBfXzd+O7MdAMBaOAb0Tuz7AQBG4xgDcA/0RYhBa3gqTlwB70X/B7xb5e5SzBopoFL26QFhmdMrdzcnLgCAsTgG9F7s+wEARuIYA3AP9EWIQWt4Mk5cAe9F/we8W+Xu0kPHpNjPpOYrMv/70A/0fQCwOo4BvRf7fgCAkTjGANwDfdHrFTE7AKBQKneXKj0speySriZnPs8gOIarbQBvQP8HvJuPr1ThPrOjAAC4GseA3ot9PwDASBxjAO6BvujVGLSG5+PEFfBe9H8AAADvwzEgAAAwAscYgHugL3otBq0BeIRvv/22QO+rUaOG7rrrLidHAwAAAAAAAAAAAGdh0BqAR+jRo4dsNlu+37dmzRrVrVvXgIgAAAAAAAAAAADgDAxaA/AYgwcP1t133+1Q24yMDL388ssGRwQAAAAAAAAAAIDCYtAagMdo06aNIiMjHWqbkZGhcePGObzsvXv3atGiRTp06JBSUlI0Z84ctWvX7rbv2bNnj+Lj4/Xdd9+pfPnyevLJJ/Xoo486vE4AAAAAAAAAAABIPmYHAACOmD17tqpWrepwe19fX82ePVtVqlRxqH1qaqpq1aql8ePHO9T++PHjeuqpp9S4cWOtX79egwcP1muvvaZPPvnE4RgBAAAAAAAAAADAL60BeIg7/eq5sO9p3bq1Wrdu7XD7Dz74QCEhIRo7dqwkqXr16vrmm2+0ePFiPfDAA/mOFQAAAAAAAAAAwFsxaA0ABXDgwAG1aNEi27SYmBitXbtW169fV9GiRXO8Jzk5WbGxsXkuc8eOHU6PEwAAAAAAAAAAwN0xaA3AI6xfvz5f7bt27WpIHLecOXNGQUFB2aaVK1dON27c0Llz51S+fHlD1w8AAAAAAAAAAGAVDFoD8AijR4+WzWaTJNnt9tu2tdlshg9a31rP792K64/TbwkJCeHX1AAkSStWrNDKlSt14sQJSVLNmjX17LPPZj2mYPTo0froo4+yvScqKkqrV692eawAAAB/xLEMAAAAAGdj0BqAxyhRooQ6dOigTp06qXjx4qbGEhQUpJSUlGzTfvnlFxUpUkRlypQxJygAHqNixYoaOXKk7r77bkmZd5MYMmSIPvroI9WsWVNS5iMHpkyZkvWe3B47AAAAYAaOZQAAAAA4G4PWADzCsmXLtHbtWm3cuFEbN27Ugw8+qB49euiee+4xJZ4GDRros88+yzbtiy++UL169SjGALijtm3bZns9YsQIrVy5UgcOHMgq9Pr5+Sk4ONiM8AAAAG6LYxkAAAAAzsagNQCPcM899+iee+7RuHHjtGnTJq1du1b/7//9P1WpUkU9evRQ165dC/Uc6StXruinn37Kep2UlKTDhw+rdOnSCg0N1bRp03Tq1Cm98cYbkqQ+ffpo+fLlmjJlinr37q39+/dr7dq1mjZtWqE/KwDvkpGRoS1btig1NVUNGzbMmr5nzx41a9ZMpUqVUnR0tEaMGKFy5crddlnJycmKjY3Ncz6PKAAAAM7GsQwAAAAAZ2DQGoBHKV68uHr37q3evXvr+++/15o1a/Tee+/pnXfe0cCBAzVixIgCLffQoUPq169f1utbt7Hr1q2b4uPjlZKSouTk5Kz5lStX1vz58zVlyhQtX75c5cuX19ixY/XAAw8U7gMC8BpHjhxRnz59dO3aNQUEBGjOnDmqUaOGJKlVq1Z68MEHFRoaqqSkJM2cOVP9+/fXunXr5OfnZ3LkAAAAHMsAAAAAcC4GrQF4rBo1aqhHjx66fv26Vq5cqf/+978FXlbTpk115MiRPOfHx8fnmNakSRN99NFHBV4nAO9WtWpVrV+/XhcvXtTWrVs1atQoLVu2TDVq1FDHjh2z2kVERKhevXpq27atdu7cqfbt2+e5zJCQEH6BBAAAXIJjGQAAAADOxKA1AI9z+fJlJSYmau3atTp06JCqVKmi4cOHq2vXrmaHBgAO8/PzU5UqVSRJ9evX1zfffKOlS5dq0qRJOdqWL19eoaGhOnbsmIujhOXczJBSdklXk6ViIVJwjOTja3ZUAAAPxLFMIbFPBgAAALJh0BqAx/jqq6+0du1abd26VT4+PnrwwQc1atQo3XPPPWaHBgCFZrfblZ6enuu8c+fOKTk5WeXLl3dxVLCU4+ukfcOk1KTfpgWESY1nSpW7mxcXAMASOJbJB/bJAAAAQA4MWgPeysOu6r7//vuVlJSkqKgojRs3Tp06dVJAQIDZYQGeycP6vxVNnz5drVq1UsWKFXXlyhVt3rxZe/bs0cKFC3XlyhXNnj1b7du3V3BwsE6cOKEZM2YoMDBQ7dq1Mzt0eKrj66RdPSXZs09PPZE5PWYNRXLADOyT4aE4likE9skAAI4B4cnYfmEgjxy0njVrlmbPnp1tWlBQkHbv3i0p8+re2bNna9WqVbp48aKioqI0fvx41axZ04xwAffjgVd1Hz9+XCVKlNCVK1e0dOlSLV26NM+2NptNGzZscGF0gAfxwP5vRWfOnNGLL76o06dPq2TJkqpVq5YWLlyoFi1aKC0tTUePHtX69et16dIlBQcHq2nTppoxY4ZKlChhdujwRDczMvv9H4vj0q/TbNK+4VKlhznRtBDOmTwA+2R4MI5lCoh9MuAwjmVgWRwDug3yTAGw/cJgHjloLUk1a9bUkiVLsl77+v52ML9gwQItWbJE8fHxCg8P17x58zRgwABt2bKFEyTAQ6/qjo6ONjsEwPN5aP+3otdffz3Pef7+/lq0aJELo4HlpezKfkKZg11KPZ7ZrsJ9rooKLsA5kxtjnwwPx7FMAbFPBvKFYxlYDseAboc8kw9sv3ABjx209vX1VXBwcI7pdrtdS5cu1eDBg9W+fXtJ0tSpU9W8eXMlJiaqT58+rg4VcB8efFX3+++/b3YIgGfz4P4PoJCuJju3HTwG50xuin0y4L3YJwP5wrEMLIVjQLdEnnEQ2y9cxGMHrX/88Ue1bNlSfn5+ioqK0vPPP6/KlSsrKSlJKSkpatmyZVZbPz8/RUdHa//+/Xkmk+TkZMXGxua5vh07djj9MwAux1XdgPei/wPeq1iIc9vBY3DO5KbYJwPei30ykC8cy8BSOAZ0S+QZB7H9wkU8ctA6MjJSU6dOVXh4uM6ePat58+apT58+SkxMVEpKiiSpXLly2d4TFBSkkydPmhEu4D64qhvwXvR/wHsFx2Q+Yyr1hHK/KtqWOT84xtWRwUCcM7kx9smA92KfDDiMYxlYDseAboc8kw9sv3ARjxy0bt26dbbXDRo00P3336/169crKipKkmSz2bK1sdtzOxn4TUhIiOde5QI4yoOv6m7UqJGWLl2qevXqOdT+5s2buueee7RixQrVrl3b4OgAD+DB/R9AIfn4So1n/vrsKZuyF8l/PWZu/Da38LIYzpncGPtkwHuxTwYcxrEMLIdjQLdDnskHtl+4iI/ZAThDQECAIiIidOzYsaznD5w5cyZbm7NnzyooKMiM8AD3ceuqbtnyaGCTAiq75VXdqampunnzpsPt7Xa7UlNTlZGRYWBUgAfx4P4PwAkqd5di1kgBlbJPDwjLnF65uzlxwWU4Z3Ij7JMB78Y+GSgQjmXg8TgGdHvkmdtg+4WLWGLQOj09Xf/9738VHByssLAwBQcHa/fu3dnm7927Vw0bNjQxSsAN3LqqW1LOHYz7X9X9yCOP6E9/+pND/+rVq5fjSjjAq3l4/wfgBJW7Sw8dk2I/k5qvyPzvQz9QHPcSnDO5EfbJANgnA/nGsQw8HseAbo88cxtsv3ARj7w9+NSpU9WmTRuFhITol19+0bx583T58mV169ZNNptN/fr1U0JCgsLDw1WlShUlJCTI399fnTt3Njt0wHy3rureN0xKTfptekBY5o7FTU+Shw4dWqD3lS9f3smRAB7MQ/s/ACfy8ZUq3Gd2FHABzpncHPtkAOyTgdviWAaWxDGgWyHP5BPbL1zAIwetf/75Zz3//PM6f/68AgMD1aBBA61evVqVKmXeWmnQoEG6du2aJk6cqAsXLigqKkqLFy9WiRIlTI4ccBOVu0uVHpZSdklXkzOfNREc49ZXQhV00BrAH3hg/wcA5B/nTB6AfTIAAHniWAaWxTGg2yDPFADbLwxm2KD1+fPnC/S+UqVKycfn9nctnzFjxm3n22w2xcXFKS4urkAxAF6Bq7oB70X/BwC3wDkT2CcDADwZxzJAAXEM6DDyjBti+4WBDBu0vvfee/P9PFmbzaYPP/xQdevWNSgqAAAAAHAPnDMBAABPxrEMAKORZwDvYujtwXv06KGKFSs61DYjI0PvvvuukeEAAAAAgFvhnAkAAHgyjmUAGI08A3gPQwete/furcjISIfaZmRkaN68eUaGAwAAAABuhXMmAADgyTiWAWA08gzgPW5/U/9CGDNmTNYD6x3h6+urMWPGKCQkxKiQAAAAAMBtcM4EAAA8GccyAIxGngG8i2G/tO7fv79L3gMAAAAAnohzJgAA4Mk4lgFgNPIM4F0MvT04ABjl8uXLOnDggE6dOqW0tDQFBgaqRo0aioiIMDs0AAAAAAAAAAAA5INhtwf/8ccf1bRpU23dujXPNtu2bVPTpk31/fffGxUGAAu5ceOGNm3apMcee0xNmzbVoEGDNHbsWL366qt6/vnn9fDDD6tZs2Z6/fXXdezYMbPDBQAAuC3OmQAAgCfjWAaA0cgzgHcxbNB62bJlqlGjhtq3b59nm/vvv18RERFatmyZUWEAsIgdO3aoU6dOGjVqlIoVK6YRI0Zo8eLF+vjjj7VlyxatWrVKb775pjp16qTt27erU6dOGj9+vH755RezQwcAAMgV50wAAMCTcSwDwGjkGcC7GHZ78M8//1wDBw68Y7uHHnpICxcuNCoMABYxevRo9e/fX48++qjKlSuXa5uoqCh17txZ48aN09///nfNmzdPK1as0NChQ10cLQCr6NKlS77fY7PZNGPGDFWvXt2AiABYCedMAIzGsQwAI3EsA8Bo5BnAuxg2aJ2cnKyqVavesV14eLh+/vlno8IAYBE7duxQqVKlHG7frFkzNWvWTBcvXjQwKgBW991336lOnToqXry4w+/5xz/+obS0NAOjAmAVnDMBMBrHMgCMxLEMAKORZwDvYtigta+vr65fv37HdtevX5ePj2F3KQdgEfkZsHbG+wDglgkTJigyMtKhtjdu3FC9evUMjgiAVXDOBMAVOJYBYBSOZQAYjTwDeBfDenFYWJgOHDhwx3b79+9XWFiYUWEAAAAUWGxsrEqXLu1we19fX8XGxnLBDACHcM4EwGgcywAwEscyAIxGngG8i2G/tG7durWWLVumnj17qnz58rm2OXXqlJYvX66ePXsaFQYAC2rbtq1sNluu83x8fFSyZEnVr19f/fr14zlsAAplzpw5+Wpvs9ny/R4ABXQzQ0rZJV1NloqFSMExko+v2VHlC+dMAIzGsQyssL+E++JYBoDRyDOAdzHsl9YDBgyQ3W5Xnz599H//93/ZnoeUlpamzZs369FHH5XNZtPjjz9uVBgALKhJkyay2+06deqUKlWqpKioKIWGhurUqVPKyMhQSEiItm3bph49euibb74xO1wAyGHFihXq0qWLGjVqpEaNGumRRx7R3/72t6z5drtds2bNUsuWLRUZGanHHntM3333nYkRA27m+DppQ7i0o4305Z8z/7shPHO6B+GcCYCn4ljGQ1hkfwn3xbEMAKORZwDvYtgvrcuVK6eEhAQ9++yzev755+Xr66vAwEBJ0rlz55SRkaFy5cpp3rx5Klu2rFFhALCgli1b6sCBA9q2bZtCQkKypp88eVJPPPGE2rVrp/j4eD322GOaNWuW5s+fb2K0AKzqo48+0t/+9jfZ7Xa1atVKPXr0cPi9FStW1MiRI3X33XdLktavX68hQ4boo48+Us2aNbVgwQItWbJE8fHxCg8P17x58zRgwABt2bJFJUqUMOojAZ7h+DppV09J9uzTU09kTo9ZI1Xubkpo+cU5EwAzcSxjcRbaX8J9cSwDwGjkGcC7GDZoLUmRkZHasmWLVq9erd27dys5OVmSFBERoZiYGPXs2ZOTFXgWbqvlFt59913FxcVlG7CWpNDQUA0ZMkRz585Vt27d9Pjjj+v11183KUo3xTYMOMXs2bP14YcfqkOHDkpNTdXkyZP1008/acSIEQ69v23bttlejxgxQitXrtSBAwdUo0YNLV26VIMHD1b79u0lSVOnTlXz5s2VmJioPn36OP3zAB7jZoa0b5hyFOClX6fZpH3DpUoPe8z+jXMmN8UxEyyOYxmLs+D+Eu7LUscy7P8Bt2SpPAO4kgfu1wwdtJakEiVK6IknntATTzxh9KoAYx1fl3nSl5r027SAMKnxTK5OdrEff/wxzwORUqVK6cSJE5KkSpUq6erVq64Mzb2xDQP5lp6eLj8/vxzT161bp6VLl6pKlSqSpIYNG2ratGkOF3p/LyMjQ1u2bFFqaqoaNmyopKQkpaSkqGXLlllt/Pz8FB0drf3799+20JucnKzY2Ng85+/YsSPf8QFuJWVX9v1YDnYp9Xhmuwr3uSqqQuOcyc1wzAQL4VjGS1l0fwn3ZYljGfb/gFuzRJ4BXOn/t3fn8THd+x/H3yNEhFRCglhqT1z70tiD4mopbcVSrR+uq5Si9FrboqI0aNHF0lhbRUtR11ZFdKGLuK3W0pZqqxWCSGNfQszvj9zM7chEMsmsJ6/n45FHO+ecOefzNXM+53Pme873eOlxzWnPtAYMJWNYrTtP+jKG1eJ5UC5VtmxZffjhhzbnrVu3znIH9vnz51W8eHFXhua5+A4DufLwww8rPj4+0/TU1FQFBARYXhctWlRpaWl2rfvIkSNq0KCB6tSpoxdffFHz5s1TtWrVlJSUJCl9CKy/Cg4O1rlz53LRCsBAriU6djngTtRMMBhqmXyK4yVgH47/AAAj8eLjmtPutJ46dar++c9/qmzZsjla3mw2a9q0aXryySdVpkwZZ4UF2I9htTzOgAEDNGnSJPXq1UsPPvig5cePbdu26fvvv9eUKVMkSXv37lXt2rXdHK0H4DsM5Fr37t311FNPqWPHjho3bpzlQpgOHTroySefVLdu3XT16lW9++67+vvf/27XuitXrqwNGzbo4sWL2r59u8aNG6cVK1ZY5ptMJqvlzWZb+7C10NBQ7kCCsRUJzX4Ze5ZzM86ZPAw1EwyIWiafMtjxEp7LELUMx3/AoxkizwCu5OXHNafdab1y5Uq7rqC9ffu2Vq5cqeTkZGeFBOSOPcNqwSV69uyp6OhoJSQkaPr06Ro9erSmT5+uhIQERUdHq0ePHpKkwYMHa+bMmW6O1gPwHQZy7cknn9SmTZt09uxZdezYUZs2bZIkjRs3Tq1atdL69ev10UcfqWvXrnruuefsWrevr68qVqyoOnXqaNSoUapRo4aWL1+ukJAQScpURyUnJys4ONgxDQO8VUhk+nBWMmWxgEnyr5C+nBfgnMnDUDPBgKhl8imDHS/huQxRy3D8BzyaIfIM4Epeflxz2p3WZrNZ8+fPV1BQkLM2AbgGw2p5lLS0NP3xxx/q2LGjevbsqV9//VXnz59XYGCgqlSpYnU1Pz+I/BffYSBPypcvr8WLF2vz5s2aPn26NmzYoOjoaI0cOVIjR4502HbMZrNSU1NVvnx5hYSE6IsvvlDNmjUlpQ/huW/fPo0ePdph2wO8UgGf9Ocv7e6u9B/i/3rl8H9rgEaveeTVwrZwzuRhqJlgUNQy+ZDBjpfwXIaoZTj+Ax7NEHkGcCUvP645rdO6bNmyOnr0qF3vCQ0Nla+vr5MiAnKJYbU8itls1kMPPaQFCxaodevWqlq1qrtD8nx8hwGH6Ny5syIjIzVz5kx16dJFTz/9tP75z3/Kx8f+H/tmz56tVq1aqUyZMrpy5Yq2bt2q+Ph4LV68WCaTSX379lVsbKwqVaqkihUrKjY2Vn5+furcubMTWgZ4mQpRUuTa9OGu/nr1sH/59B/gK0S5LTR7cc7kYaiZYHDUMvmMgY6X8FyGqGU4/gMezRB5BnAlLz+uOa3TeteuXc5aNeBaGcNqXT0p288BMKXPZ1gtlyhYsKCCg4Nz9Dw0/BffYSBPLl++rO+++043btxQnTp1NG3aND366KN68cUXtWnTJk2dOlV169a1a53nzp3T2LFjdfbsWQUEBCg8PFyLFy9WixYtJEkDBw7UjRs3FB0drQsXLqhevXpaunSpihUr5owmAt6nQlT685eSdqdfHVwkNP045mV3jHHO5GGomWBQ1DL5mEGOl/BchqhlOP4DHs0QeQZwJS8/rjmt0xowDIbV8jgPPfSQNmzYoDZt2rg7FO/AdxjItW+//VZPP/20bty4IV9fX127dk0TJ05Ujx499O9//1tvvfWW+vbtq+7du+vZZ59V0aJFc7Tel19++a7zTSaThg8fruHDhzuiGYAxFfCRSrdxdxQwEmomGBC1DDheAtng+A8AMBIvP64VcHcAgFfIGFbLv5z1dP/y6dMZVsulatSoof3796tv375asWKFPv74Y23fvt3qD3fgOwzkytSpU9WuXTvt27dPe/fu1bhx4zR16lRdv35dhQoV0vDhw7V+/Xr99NNP6tSpk7vDBQDkFTUTDIZaBgBygOM/AMBIvPi4xp3WQE4xrJbHGDdunCTpzJkzio+PzzTfZDLpxx9/dHVYno/vMGC348ePa8yYMSpYML1kevjhh/XSSy8pMTFRlStXliRVqVJFK1as0Nq1a90ZKgDAUaiZYCDUMgCQQxz/AQBG4qXHNTqtAXswrJZHWL58ubtD8F58hwG71KhRQ6tWrVJYWJj8/Py0ePFiBQQEqHz58pmW7d69uxsiBAA4BTUTDIJaBgDswPEfAGAkXnhco9MagNdp3Lixu0MAkE9ER0dr2LBhatmypSSpePHiiomJUaFChdwcGQAAQPaoZQAAAAB4CzqtAXidtLQ0HT58WKdOnZLJZFJoaKhq1aolHx/PHtoCgPepXr26tm7dquPHjys1NVVVqlRR4cKF3R0WAABAjlDLAAAAAPAWLu203r9/v/bu3avz588rMDBQjRs3VsOGDV0ZAgAvt2jRIi1ZskQXLlywml68eHENHDhQAwYMcFNkAIzKx8dHVatWdXcYAPIJzpkAOBq1DABXopYB4GzkGcC4XNJpff36dT377LP69NNPZTabLdNNJpNat26t1157TX5+fq4IBYAXGzVqlLZs2aIqVaqoe/fuKl++vMxms06ePKm4uDi9+uqr+uGHHzRr1qxcrX/lypVasmSJkpKSVL16dT3//PO67777slx+48aNWrx4sX7//XcFBAQoMjJSY8eOVVBQUG6bCMDDHD58WFWrVrWrTjl8+LCqVavGXUwA7MI5EwBnoJYB4CrUMgCcjTwDGF8BV2zklVde0e7duzVy5EjFxcXpwIEDiouL04gRI7Rnzx698sorrggDgBfbtGmTtmzZolGjRmnr1q0aPXq0evXqpccff1yjR4/WRx99pGeffVZbt27Vli1b7F7/1q1bFRMToyFDhmjDhg1q1KiRBg4cqFOnTtlc/j//+Y/GjRun7t27a/PmzXrttdd08OBBTZgwIa9NBeBBunfvrqNHj+Z4+bS0NHXv3l3Hjh1zYlQAjIhzJgDOQC0DwFWoZQA4G3kGMD6X3Gm9detWDRkyRE899ZRlWrly5TR48GDdunVLK1as0MSJE10RCgAvtWbNGnXp0kUDBw7McplBgwbp6NGjev/99/XQQw/Ztf5ly5apW7du6tGjhyTphRde0J49e/Tee+9p1KhRmZb//vvvVa5cOfXt21eSVKFCBT322GNavHixXdsF4NnMZrN++OEH3bhxI0fLp6WlWV3tCwA5xTkTAGeglgHgKtQyAJyNPAMYn8uGB8/qmQINGzbUkiVLXBEGAC925MgR9e/fP9vlHnroIY0bN86udaempurw4cMaNGiQ1fQWLVpo//79Nt/ToEEDzZkzR5999platWql5ORkffzxx2rdunWW20lMTFS7du2ynB8XF2dX3ABcIzo6OsfLms1mmUwmJ0YDwKg4ZwLgLNQyAFyBWgaAs5FnAONzSad1vXr1dPDgQTVr1izTvIMHD6pOnTquCAOAF7t+/boCAgKyXa5YsWK6fv26XetOSUlRWlqaSpYsaTU9ODhYSUlJNt/TsGFDvfrqqxo5cqRSU1N169YttW3blqv5AINZvnx5rt5XuXJlB0cCwOg4ZwLgDNQyAFyFWgaAs5FnAONzSaf1hAkTNGjQIBUtWlSdO3dW8eLFdeHCBW3atEmrV69WbGysK8IA4MVKly6to0ePKiIi4q7L/fTTTypTpkyutnHnHQV3u8vg2LFjmjp1qoYOHaqWLVsqKSlJM2fO1IsvvqiXX37Z5ntCQ0O5mxrwMo0bN3Z3CADyCc6ZADgDtQwAV6GWAeBs5BnA+FzSad2jRw/dunVLU6dO1dSpU+Xj46O0tLT0AAoW1GOPPWZZ1mQy6ZtvvnFFWAC8SGRkpBYvXqyOHTuqRIkSNpf5888/tWTJErVv396udQcFBcnHx0fnzp2zmp6cnKzg4GCb74mNjVXDhg315JNPSpJq1KihIkWKqHfv3ho5cqRKlSplVwwAACB/45wJAAB4M2oZAM5GngGMzyWd1g888ADPRAKQJ4MGDdLmzZvVs2dPjRo1Svfff7/8/PwkpQ8dvmvXLs2ePVvXrl3TwIED7Vq3r6+vatWqpS+++EJ///vfLdO//PLLLJ9Bff36dfn4+FhNy3htNpvt2j4AAADnTAAAwJtRywBwNvIMYHwu6bSePn26KzYDwMDKlCmjBQsWaPjw4frXv/4lHx8fBQUFSfrfM6lLlCihBQsWqHTp0navv3///ho7dqxq166tBg0aaPXq1UpMTFSvXr0kSbNmzdKZM2c0c+ZMSdL999+viRMnatWqVYqMjNTZs2f18ssvq27durnaPgAAyN84ZwIAAN6MWgaAs5FnAONzSac1ADhCo0aN9PHHH2vNmjX68ssvlZiYKEkKCwtTixYt1KNHDwUEBORq3Z06dVJKSormz5+vs2fPKiwsTAsXLlS5cuUkSUlJSZbtSVJUVJSuXLmilStXasaMGQoICFDTpk01ZsyYvDcUAAAAAAAAAAAgH3FZp3Vqaqq+/PJLnTx5UqmpqVbzTCaT/vGPf7gqFABeLCAgQAMGDNCAAQMcvu7evXurd+/eNufZupKvT58+6tOnj8PjAAAA+RPnTAAAwJtRywBwNvIMYGwu6bQ+dOiQBg8erOTkZJvPeiWZAAAAAMjPOGcCAADejFoGgLORZwDjc0mndXR0tIoVK6bo6GhVrVpVhQoVcsVmARjIwIED9eyzz6pmzZo5Wj41NVUrV66Ur69vlndPA0B25s6dm+NlTSaThg4d6sRoABgZ50wAnIFaBoCrUMsAcDbyDGB8Lum0PnbsmF599VW1a9fOFZtzjttpUtJu6VqiVCRUComUCvi4Oyog3wgODlb37t1Vt25dPfroo2rcuLGqVKlitczly5d14MABxcXFafPmzQoICNDMmTPdFDEAIxw7bf3QazKZsryilx96AeSW158zGSDnA0ZELQPAVby+lnEl6iYgV7w+z7DvA9lySad1aGio09YdGxur2bNnq2/fvnrhhRckSWazWXPnztXq1at18eJF1atXT5MmTVL16tVzt5ET66VvRkhXE/43zb+81Oh1qUKUA1oBIDsxMTHq06ePFi5cqKlTpyotLU1+fn4KCgpS4cKFdeHCBZ0/f15ms1lly5bV4MGD1bt3b/n6+ro7dCB/Msix86uvvrJ6nZaWppYtW+rtt99WeHi4m6ICYERefc5kkJwPGBG1DABXcWYtI7noN2BXoG4Cco1zJsD4CrhiI08++aSWLl2q1NRUh673wIEDWr16daYTrUWLFmnZsmWaNGmS1q5dq+DgYPXv31+XL1+2fyMn1ku7u1snE0m6ejJ9+on1eWgBAHvUrFlTr732mj777DPFxMTo0Ucf1d/+9jeVLVtWzZs317Bhw/Tuu+9q586d6t+/Px3WgLsY6NgZFBSU6U+SAgICspwHALnhtedMBsr5gBFRywBwFWfVMpKLfgN2BeomIE84ZwKMzyV3WkdFRenkyZNq3769GjdurMDAwEzLTJgwwa51XrlyRWPGjNHUqVO1YMECy3Sz2azly5dr8ODB6tChgyRpxowZat68uTZv3qxevXrlfCO309KvflHmYbPSp5mkb0ZK5R5hGAfAhUqWLKlHH31Ujz76qLtDAXAnjp0AJIY9ywWvPGci5wMAXI0aw2M5o5aRXPQbsCtQNwF5xjkTYHwu6bT+9NNPFRsbq1u3bmnz5s2Z5ptMJruTyZQpU9S6dWs1b97cKpkkJCQoKSlJLVu2tEzz9fVVRESE9u/fn2UySUxMzPQshHplUjT7oQSby6czS1dPpBfLpdvYFT8AAIaUtDvzlaNWOHZK6cNObd++Xb/++qv8/PzUoEEDjR49WlWqVLEsM378eH344YdW76tXr57WrFnj6nAB+zDsWa544zkT50tA/kUtA7egxvBozqhlJMfWM7Z+//2ruLg4u+PLMc6VgTzjnAkwPpd0Ws+cOVO1atVSdHS0qlatqkKFCuVpfVu2bNEPP/ygtWvXZpqXlJQkKf1OzL8KDg7WqVOn7NpOCf8bOVvwWqJd6wUAwLByekzM58fO+Ph49e7dW3Xq1FFaWprmzJmjAQMGaMuWLfL397csFxkZqZiYGMvrvNZQgNNlDHt251XkGcOeRa7lR+UseOM5E+dLQP5FLQOXo8bweI6uZSTX/QbsEpwrA3nGORNgfC7ptD558qTmzp2rGjVq5HldiYmJmjZtmpYuXarChQtnuZzJZLJ6bTbbGn7hf0JDQzNfTXfmUynu/uyDKhKa/TIAAOQHOT0mevmx8846w15Lliyxeh0TE6NmzZrp8OHDioiIsEz39fVVSEhInrYFuAzDnuWJV54zcb4EeC1qGXgVagyv4MhaRnJOPWPz919XySfnyoAzcc4EGJ9LOq2rVKli/8Pps3D48GElJycrKup/V0+mpaVp3759WrlypbZt2yZJOnfunEqVKmVZJjk5WcHBwfZtLCQyfZihqydluzA2pc8PicxFSwAAMCCDHTu7dOlic/qzzz4rX19fq2kmk0kbN27M1XYuXbokSSpevLjV9Pj4eDVr1kz33HOPIiIi9Oyzz2a6yvev3DrcHcCQh3niledMBsv5gBFRy8AQqDG8giNrGcnFvwG7AnUTkGecMwHG55JO6xEjRmj27Nm677778nyFbdOmTbVp0yarac8995yqVKmigQMHqkKFCgoJCdEXX3yhmjVrSpJSU1O1b98+jR492r6NFfBJfy7O7u6STLJOKv+9wqbRa1zFCQBABoMdOwMDAzNN++vdQ45gNpsVExOjRo0aKSwszDK9VatWevDBB1W2bFklJCTo9ddfV79+/bR+/fpMPzIDHoEhD/PEK8+ZDJbzASOiloEhUGN4BUfWMpKLfwN2BeomIM84ZwKMzyWd1u+//74uXryoDh06qEaNGpmuvDWZTFYPub+bYsWKWZ0ESZK/v78CAwMt0/v27avY2FhVqlRJFStWVGxsrPz8/NS5c2f7g68Qlf5cnG9GWF/V6V8+PZnwvBwAAKwZ6Nj57rvvOn0bU6ZM0dGjR7Vq1Sqr6Z06dbL8f1hYmGrXrq22bdvq008/VYcOHWyuy63D3QEMeZgnXnvOZKCcDxgRtQwMgRrDKziylpHc8BuwK1A3AXnCORNgfC7ptD569KgKFCigoKAgnTlzRmfOnLGan9dnKd1p4MCBunHjhqKjo3XhwgXVq1dPS5cuVbFixXK3wgpR6c/FSdqdftVmkdD04Rq4+gUAANvy4bHTbDbbXdO89NJL2rVrl1asWKEyZcrcddlSpUqpbNmyOn78eB6iBJyIYc/yxKvPmfJhzgeMiFoGHosawyu4upaRnPAbsCtQNwG5xjkTYHwu6bTetWuXU9d/55XDJpNJw4cP1/Dhwx23kQI+PBcH8AIdOnTQ7du3tXPnTneHAiAfHTs3btyo+fPnW555lB2z2ayXXnpJO3bs0LvvvqsKFSpk+56UlBQlJiZaPUsJ8CgMe5YnXn/OlI9yPmBE1DLwaNQYXsHZtYzkot+AXYG6CcgVzpkA43NJpzUAuIrZbNbt27fdHQYAA7l06ZJ27typc+fOqXLlymrbtq0KFCggSdq+fbveeOMNHTt2TGXLls3xOqOjo7V582bNnz9fRYsWVVJSkiQpICBAfn5+unLliubOnasOHTooJCREJ0+e1Jw5cxQUFKT27ds7pZ2AQzDsGQB4HGoZGAI1BgAAgOG5rNM6NTVV69evV3x8vFJSUvTiiy+qUqVK2rlzp8LDw3N0VS4AZGfHjh3uDgGAgfz+++/q3bu3kpOTLUNmRkREaP78+frXv/6l3bt365577tGYMWPUp0+fHK/3vffek6RM74mJiVFUVJR8fHx09OhRbdiwQZcuXVJISIiaNGmiOXPmePZQd4DEsGd5wDkTAEejloGhUGN4PGoZAM5GngGMzSWd1n/++af69eunn3/+WcHBwUpOTtaVK1ckSXFxcdqzZ48mT57silAAAABy7PXXX9fly5c1bNgw1a5dWwkJCVqwYIF69eqlY8eOqUePHhozZozuueceu9Z75MiRu8738/PTkiVL8hI64F4Me2Y3zpkAOAO1DAyHGsNjUcsAcDbyDGB8Lum0fuWVV3Tx4kWtW7dO4eHhql27tmVekyZNtGjRIleEAcAgfvvtNyUlJalx48aZ5sXHx6tUqVKqVKmS6wMDYDjx8fEaMmSInnrqKcu0e++9VwMHDlSvXr04GQLgMJwzAXAGahkArkItA8DZyDOA8RVwxUY+/fRTPfPMM6pVq5ZMJpPVvNKlS+v06dOuCAOAQUyfPl1xcXE2533yySeaPn26iyMCYFQpKSlq2LCh1bRGjRpJkjp16uSOkAAYFOdMAJyBWgaAq1DLAHA28gxgfC7ptL58+bLKli1rc96tW7eUlpbmijAAGMTBgwcVERFhc15ERIQOHTrk4ogAGFVaWpoKFy5sNS3jddGiRd0REgCD4pwJgDNQywBwFWoZAM5GngGMzyXDg5cvX17fffedmjVrlmnegQMHVLlyZVeEAcAgLl26JH9/f5vz/Pz8dOHCBRdHBMDIfv31V/n4+FheZ5wE/frrr5mWrVWrlsviAmAsnDMBcBZqGQCuQC0DwNnIM4DxOa3Tet++fapZs6aKFi2qLl26aNGiRapevbratGkjSTKZTDpw4ICWL1+uIUOGOCsMAAZUunRpHThwQM2bN88078CBAwoJCXFDVACM6rnnnrM5fezYsZb/N5vNMplM+vHHH10VFgAD4JwJgCtQywBwFmoZAM5GngHyF6d1Wvft21erV69W3bp1NXDgQH377bcaNmyYihcvLkkaMGCAzp8/r8jISPXt29dZYQAwoPbt22vhwoWqX7++mjZtapm+d+9eLVq0SN27d3djdACMJCYmxt0hADAwzpkAOBu1DABnopYB4GzkGSB/cVqntdlstvx/oUKFtGjRIm3dulWffvqpkpOTFRQUpDZt2uihhx5SgQIuebQ2AIMYOnSo9uzZo/79+6tSpUoqU6aMTp8+rePHj6tatWoaPny4u0MEYBBdu3Z1dwgADIxzJgDORi0DwJmoZQA4G3kGyF9c8kxrKX2YhoceekgPPfSQqzYJwKACAgK0evVqvf3229q9e7dOnTqloKAgDR8+XP369VPRokXdHSIAAIDdOGcCAADejFoGgLORZwBjc1mnNQA4UtGiRTV06FANHTrU3aEAAAAAAAAAAAAgD5zaad2vXz+ZTKZslzOZTPrmm2+cGQoAAAAAeBzOmQAAgDejlgHgbOQZIP9waqd148aNVaJECWduAkA+9e9//1ubN2/WqVOndP36dat5JpNJO3fudFNkAAAAOcc5EwAA8GbUMgCcjTwD5B9O7bQeOnSo6tat68xNAMiHFi5cqNmzZ6tatWqqUaOGfH193R0SAABArnDOBAAAvBm1DABnI88A+QfPtAbgddasWaPevXtr4sSJ7g4FAAAAAAAAAAAAeUSnNeBpbqdJSbula4lSkVApJFIq4OPuqDzKuXPn1L59e3eHATge+z8AAPBm1DIAAHgGjskA4NnI0zbRaQ14khPrpW9GSFcT/jfNv7zU6HWpQpT74vIwtWrV0okTJ9SsWTN3hwI4Dvs/AADwZtQyAAB4Bo7JAODZyNNZKuCsFf/00088ZwCwx4n10u7u1olKkq6eTJ9+Yr174vJA48eP19KlS3Xo0CF3hwI4Bvs/4Llup0lnPpWOv5f+39tp7o4IBsI5EwyDWgaA0VAD5gi1jAfimAyDIc/AcMjTd8Wd1oAnuJ2WfmWNzDZmmiWZpG9GSuUeYYgISc8//7zOnz+vHj16KDg4WIGBgVbzTSaTNm7c6J7gAHux/wOeiytfASB71DIAjIYaEN6KYzIAeDbydLacdqc1ADsk7c58ZY0Vs3T1RPpyUGBgoKpXr6777rtPlSpVUmBgoNVf8eLF3R0ikHPs/4Bn4spXAMgZahkARkINCG/GMRkAPBt5OlvcaQ14gmuJjl3O4N599113hwA4Dvs/4Hm48hUAco5aBoBRUAPC23FMBgDPRp7OFndaA56gSKhjlwPgPdj/Ac/Dla8AkHPUMgCMghoQ3o5jMgB4NvJ0trjTGvAEIZHpz0e6elK2r+g1pc8PiXR1ZB7r/Pnzevvtt/X1118rJSVFQUFBat68ufr168fw4PAu7P+A5+HKVwDIOWoZAEZBDQhvxzEZADwbeTpb3GkNeIICPlKj1//7wnTHzP++bvQaw0/915kzZxQVFaW33npLly5dUtmyZXXp0iXNnz9fXbt21ZkzZ9wdIpBz7P+A5+HKVwDIOWoZAEZBDQhvxzEZADwbeTpbdFoDnqJClBS5VvIvZz3dv3z69ApR7onLA82ePVvXr1/XmjVrtGXLFi1btkxbtmzRmjVrdOPGDc2ZM8fdIQL2Yf/Pd2JjY9WtWzc1aNBAzZo109NPP61ff/3Vahmz2aw333xTLVu2VN26ddWnTx/9/PPPboo4n8m48jXTCUQGk+RfIV9f+QoAVqhl8h1qGRgSNSCMgGMyAHg28vRdMTw44EkqREnlHkl/PtK1xPSrd0Mi8/WVNbbs3r1bI0eOVN26da2m161bV88884xef/31LN4JeDD2/3wlPj5evXv3Vp06dZSWlqY5c+ZowIAB2rJli/z9/SVJixYt0rJlyzR9+nRVqlRJCxYsUP/+/bVt2zYVK1bMzS0wuIwrX3d3V/qPln8dsokrXwHAJmqZfIVaBoZEDQij4JgMAJ6NPJ0lOq0BT1PARyrdxt1ReLRLly6pXLlyNueVL19ely5dcnFEgIOw/+cbS5YssXodExOjZs2a6fDhw4qIiJDZbNby5cs1ePBgdejQQZI0Y8YMNW/eXJs3b1avXr3cEXb+knHl6zcjpKsJ/5vuXz79x8p8fuUrANhELZNvUMvAsKgBYRQckwHAs5GnbaLTGoDXKV++vD799FO1aNEi07zPP/9c5cuXd0NUAJB7GRfbFC9eXJKUkJCgpKQktWzZ0rKMr6+vIiIitH///ix/6E1MTFS7du2y3E5cXJwDo84HuPIVAIAcoZaBoVADAgAAuAWd1gC8TlRUlGbNmiWz2axHH31UISEhSkpK0saNG7VixQqNGjXK3SECQI6ZzWbFxMSoUaNGCgsLkyQlJSVJkkqWLGm1bHBwsE6dOuXyGPM1rnwFAOCuqGVgSNSAAAAALkenNQCv8+STT+rEiRNasWKFVq5caZluNpvVs2dPDRgwIFfrXblypZYsWaKkpCRVr15dzz//vO67774sl09NTdW8efO0ceNGJSUlqUyZMho8eLC6d++eq+0DyJ+mTJmio0ePatWqVZnmmUwmq9dmsznTMn8VGhrKHUgAAMClqGUAAAAAOAKd1gC8jslk0pQpU/SPf/xDe/fu1fnz5xUYGKimTZuqcuXKuVrn1q1bFRMToxdffFENGzbU+++/r4EDB2rLli0qW7aszfeMGDFCycnJmjZtmu699179+eefunXrVl6aBiCfeemll7Rr1y6tWLFCZcqUsUwPCQmRJJ07d06lSpWyTE9OTlZwcLDL4wQAALCFWgYAAACAo9BpDcBrValSRVWqVHHIupYtW6Zu3bqpR48ekqQXXnhBe/bs0XvvvWdzuPHPP/9c+/bt086dOxUYGChJPEsbQI6ZzWa99NJL2rFjh959911VqFDBan758uUVEhKiL774QjVr1pSUPrrDvn37NHr0aHeEDAAAYEEtAwAAAMDR6LQG4BUuXLigCRMmKCoqSvfff7/NZT755BOtX79eU6ZMUVBQUI7XnZqaqsOHD2vQoEFW01u0aKH9+/fbfM+uXbtUu3ZtLV68WP/+97/l7++vtm3basSIEfLz87P5nsTERLVr1y7LOBgGD8g/oqOjtXnzZs2fP19Fixa1PPcxICBAfn5+MplM6tu3r2JjY1WpUiVVrFhRsbGx8vPzU+fOnd0cPQAAyO+oZQAAAAA4Gp3WALzCBx98oJ9++kmRkZFZLhMZGanp06dr5cqVGjZsWI7XnZKSorS0NJUsWdJqenBwsOXHlzudOHFC33zzjQoXLqx58+YpJSVF0dHROn/+vGJiYnK8bQD503vvvSdJ6tOnj9X0mJgYRUVFSZIGDhyoGzduKDo6WhcuXFC9evW0dOlSFStWzOXxAgAA/BW1DAAAAABHo9MagFfYunWrevTooYIFs05bBQsWVI8ePbR161a7Oq0zmEwmq9dmsznTtDvnvfrqqwoICJAkjR8/Xs8884xefPFFm3dbh4aGcjc1AEnSkSNHsl3GZDJp+PDhGj58uAsiAgAAyDlqGQAAAACOVsDdAQBATvz222+qU6dOtsvVqlVLx48ft2vdQUFB8vHx0blz56ymJycnKzg42OZ7QkJCVLp0aUuHtSRVrVpVZrNZp0+ftmv7AAAAAAAAAAAA+Rmd1gC8Qlpa2l3vss5QsGBB3bp1y651+/r6qlatWvriiy+spn/55Zdq0KCBzfc0bNhQZ8+e1ZUrVyzTfvvtNxUoUEBlypSxa/sAAAAAAAAAAAD5GZ3WALxCSEiIjh07lu1yP//8c5Z3R99N//79tXbtWq1du1a//PKLXn75ZSUmJqpXr16SpFmzZmns2LGW5Tt37qzAwEA999xzOnbsmPbt26dXXnlF3bp1szk0OAAAAAAAAAAAAGzjmdYAvELjxo21atUqde/eXYUKFbK5zM2bN/Xee++pSZMmdq+/U6dOSklJ0fz583X27FmFhYVp4cKFKleunCQpKSlJiYmJluWLFi2qpUuXaurUqerWrZsCAwPVsWNHjRw5MlftAwAAAAAAAAAAyK/otAbgFfr166fu3btr2LBhmjJlikqXLm01/8yZM5o4caJ+++03vfrqq7naRu/evdW7d2+b86ZPn55pWtWqVbVs2bJcbQsAAAAAAAAAAADp6LQG4BVq1KihSZMmKTo6Wu3atVPt2rUtd0GfPHlShw4dktls1uTJkxUeHu7maAEAAAAAAAAAAJBTdFoD8Bo9e/ZU9erVFRsbq7179+q7776TJBUpUkSRkZF66qmnVL9+fbfGCAAAAAAAAAAAAPvQae1pbqdJSbula4lSkVApJFIq4OPuqACP0aBBA7311lu6ffu2UlJSJElBQUEqUKCAmyPLI/Z9AAAAOAu1JgAAQP5DDZj/8JnDy3llp/WqVav03nvv6eTJk5Kk6tWr6+mnn1br1q0lSWazWXPnztXq1at18eJF1atXT5MmTVL16tXdGXb2TqyXvhkhXU343zT/8lKj16UKUe6LC/BABQoUUMmSJd0dhmOw7wMAOLGEgxn2nAn2o9YEAM9FDZglahkgj6gBs2W4PMNnDgPwylsTy5Qpo9GjR2vdunVat26dmjZtqqFDh+rnn3+WJC1atEjLli3TpEmTtHbtWgUHB6t///66fPmymyO/ixPrpd3drROKJF09mT79xHr3xAXAudj3AQAn1ksbK0lx90tfPpH+342VOAYgTwx5zgT7UWsCgOeiBrwrahkgD6gBc8RQeYbPHAbhlZ3Wbdu2VevWrVW5cmVVrlxZzz77rPz9/fXdd9/JbDZr+fLlGjx4sDp06KCwsDDNmDFD169f1+bNm90dum2309KvgJHZxsz/TvtmZPpyAIyDfR8AwIklnMRw50ywH7UmAHguasBsUcsAuUQNmGOGyTN85jAQrxwe/K/S0tK0bds2Xb16VQ0aNFBCQoKSkpLUsmVLyzK+vr6KiIjQ/v371atXL5vrSUxMVLt27bLcTlxcnMNjt0janblItWKWrp5IX650G+fFAcC12PcBIH/L9sTSlH5iWe4RholEnhjinAn2o9YEAM9EDWg3ahnADtSAueLVeYbPHAbitZ3WR44cUa9evXTjxg35+/tr3rx5qlatmr799ltJyvSs2+DgYJ06dcodoWbvWqJjlwPgHdj3ASB/48QSTmaocybYj1oTADwTNWCOUcsAuUANaBdD5Bk+cxiI13ZaV65cWRs2bNDFixe1fft2jRs3TitWrLDMN5lMVsubzbauXvyf0NBQ911NVyTUscsB8A7s+wCQv3FiCScz1DkT7EetCQCeiRowx6hlgFygBrSLIfIMnzkMxCufaS2lD8VQsWJF1alTR6NGjVKNGjW0fPlyhYSESJLOnTtntXxycrKCg4PdEWr2QiIl//KSTFksYJL8K6QvB8A42PcBIH/jxBJOZqhzJtiPWhMAPBM1YI5RywC5QA1oF0PkGT5zGIjXdlrfyWw2KzU1VeXLl1dISIi++OILy7zU1FTt27dPDRo0cGOEd1HAR2r0+n9f3JlY/vu60Ws8xwYwGvZ9AMjfOLGEi3n1ORPsR60JAJ6JGjDXqGWAHKAGzBOvzDN85jAQrxwefPbs2WrVqpXKlCmjK1euaOvWrYqPj9fixYtlMpnUt29fxcbGqlKlSqpYsaJiY2Pl5+enzp07uzv0rFWIkiLXSt+MsH6ujX/59IRSIcptoQFwIvZ9AMi/Mk4sd3dX+onkX4cZ48QSeWPIcybYj1oTADwPNWCOUMsAeUANmCOGyjN85jAIr+y0PnfunMaOHauzZ88qICBA4eHhWrx4sVq0aCFJGjhwoG7cuKHo6GhduHBB9erV09KlS1WsWDE3R56NClFSuUekpN3pz60pEpp+VWU+L1IBw2PfB4D8ixNLOIlhz5lgP2pNAPA81IDZopYB8ogaMFuGyzN85jAAr+y0fvnll+8632Qyafjw4Ro+fLiLInKgAj5S6TbujgKAq7HvA0D+xYklnMDQ50ywH7UmAHgeasC7opYBHIAa8K4MmWf4zOHlvLLTGgAAADAUTiwBAADyH2pAAAAAiwLuDgAAAAAAAAAAAAAAkH/RaQ0AAAAAAAAAAAAAcBs6rQEAAAAAAAAAAAAAbkOnNQAAgBvs27dPgwcPVsuWLRUeHq6dO3dazR8/frzCw8Ot/nr27OmmaAEAAKxRywAAAABwpILuDgAAACA/unr1qsLDwxUVFaXhw4fbXCYyMlIxMTGW14UKFXJVeAAAAHdFLQMAAADAkei0BgAAcIPWrVurdevWd13G19dXISEhLooIAAAg56hlAAAAADgSndYAAAAeKj4+Xs2aNdM999yjiIgIPfvssypZsmSWyycmJqpdu3ZZzo+Li3NGmAAAADZRywAAAADIKTqtAQAAPFCrVq304IMPqmzZskpISNDrr7+ufv36af369fL19XV3eAAAAHdFLQMAAADAHnRaAwAAeKBOnTpZ/j8sLEy1a9dW27Zt9emnn6pDhw423xMaGsodSAAAwCNQywAAAACwRwF3BwAAAIDslSpVSmXLltXx48fdHQoAAIDdqGUAAAAA3A2d1gAAAF4gJSVFiYmJKlWqlLtDAQAAsBu1DAAAAIC7YXhwAAAAN7hy5Yr++OMPy+uEhAT9+OOPKl68uIoXL665c+eqQ4cOCgkJ0cmTJzVnzhwFBQWpffv2bowaAAAgHbUMAAAAAEei0xoAAMANDh06pL59+1pex8TESJK6du2qyZMn6+jRo9qwYYMuXbqkkJAQNWnSRHPmzFGxYsXcFTIAAIAFtQwAAAAAR6LTGgAAwA2aNGmiI0eOZDl/yZIlLowGAADAPtQyAAAAAByJZ1oDAAAAAAAAAAAAANyGO63hXLfTpKTd0rVEqUioFBIpFfBxd1QAnI19H/A87JcAANiHY2f+xOcOADlHzgRgROQ2t6HTGs5zYr30zQjpasL/pvmXlxq9LlWIcl9cAJyLfR/wPOyXAADYh2Nn/sTnDgA5R84EYETkNrdieHA4x4n10u7u1ju2JF09mT79xHr3xAXAudj3Ac/DfgkAgH04duZPfO4AkHPkTABGRG5zOzqt4Xi309KvRJHZxsz/TvtmZPpyAIyDfR/wPOyXAADYh2Nn/sTnDgA5R84EYETkNo9ApzUcL2l35itRrJilqyfSlwNgHOz7gOdhvwQAwD4cO/MnPncAyDlyJgAjIrd5BDqt4XjXEh27HADvwL4PeB72SwAA7MOxM3/icweAnCNnAjAicptHoNMajlck1LHLAfAO7PuA52G/BADAPhw78yc+dwDIOXImACMit3kEOq0NrF27dmrXrp3rNxwSKfmXl2TKYgGT5F8hfTkHcFs7XSg/tBGeLUffQRfv+3lhtH3KaO2BA7Ffug3tATyL0b7DtMeJvOjYCQdywOfuUd9jB6A9gGfxqO8wOTMTo7UHyJCvvtucB0hy/2dOpzUcr4CP1Oj1/764cwf/7+tGr6UvB8A42PcBz8N+CQCAfTh25k987gCQc+RMAEZEbvMIdFrDOSpESZFrJf9y1tP9y6dPrxDlnrgAOBf7PuB52C8BALAPx878ic8dAHKOnAnAiMhtblfQ3QHAwCpESeUekZJ2pz+cvkho+tAJXIkCGBv7PuB52C8BALAPx878ic8dAHKOnAnAiP6b2/7VJ0Il/G9owtR55DYXotMazlXARyrdxt1RAHA19n3A87BfAgBgH46d+ROfOwDkHDkTgBEV8NH3p4MkSRPIcS7F8OAAAAAAAAAAAAAAALeh0xoAAAAAAAAAAAAA4DYms9lsdncQ7lanTh2lpaUpNDTU3aE4VGJioiQZrl13yg/tdGcbExMT5ePjo4MHD7p820bi7XnGaPsZ7fEs5BnHIM94FtrjWcgzeUeO8Sy0x7OQYxyDPONZaI9nIc/kHTnGs9Aez0KOcQxvzzO2ePt3Oy/ya9ud1e6c5hmeaS2pcOHCSk1NdXcYDpdfdqb80E53trFgwYLy9fV12/aNwtvzjNH2M9rjWcgzjkGe8Sy0x7OQZ/KOHONZaI9nIcc4BnnGs9Aez0KeyTtyjGehPZ6FHOMY3p5nbPH273Ze5Ne2O6vdOc0z3GkNAAAAAAAAAAAAAHAbnmkNAAAAAAAAAAAAAHAbOq0BAAAAAAAAAAAAAG5DpzUAAAAAAAAAAAAAwG3otAYAAAAAAAAAAAAAuA2d1l5o3759Gjx4sFq2bKnw8HDt3LnTav748eMVHh5u9dezZ0+rZVJTU/XSSy+pSZMmql+/vgYPHqzTp0+7shl3FRsbq27duqlBgwZq1qyZnn76af36669Wy5jNZr355ptq2bKl6tatqz59+ujnn3+2WsaT25mTNhrhs4R3yC6v2BIfH6+oqCjVqVNH7dq103vvveeCSLNnb1u2b9+u/v37q2nTpmrYsKEee+wx7d6920XRZi83n02Gb775RjVr1tQjjzzixAiB7Bkpx0jkmb8iz8BTkGfIM4ArrFy5Um3btlWdOnUUFRWl//znP3ddPjU1VXPmzNH999+v2rVrq3379lq7dq2Los2eve3ZuHGjHn74YdWrV08tW7bUc889p5SUFBdFmzWjHQOQf5FjPDPHSOQZGJuj+mK80apVq9SlSxc1bNjQch712WefWeYbtd13io2NVXh4uKZNm2aZ5q6202ntha5evarw8HBNmjQpy2UiIyO1Z88ey9/ChQut5k+bNk07duzQnDlztGrVKl29elVPPfWU0tLSnB1+jsTHx6t3795as2aNli1bprS0NA0YMEBXr161LLNo0SItW7ZMkyZN0tq1axUcHKz+/fvr8uXLlmU8uZ05aaPk/Z8lvENO8spfnThxQoMGDVKjRo20YcMGDR48WNOmTdPHH3/s5EizZ29b9u3bp+bNm2vhwoVav369mjRpoiFDhuiHH35wcqQ5Y297Mly6dEnjxo1Ts2bNnBQZkHNGyjESeSYDeQaehDxDngGcbevWrYqJidGQIUO0YcMGNWrUSAMHDtSpU6eyfM+IESP01Vdfadq0adq2bZtmz56tKlWquDDqrNnbnv/85z8aN26cunfvrs2bN+u1117TwYMHNWHCBBdHnpnRjgHIn8gxnptjJPIMjM1RfTHeqEyZMho9erTWrVundevWqWnTpho6dKilc9ao7f6rAwcOaPXq1QoPD7ea7ra2m+HVwsLCzDt27LCaNm7cOPOQIUOyfM/FixfNtWrVMm/ZssUy7fTp0+YaNWqYP//8c6fFmhfJycnmsLAwc3x8vNlsNptv375tbtGihTk2NtayzI0bN8yNGjUyv/fee2az2fvaeWcbzWZjfpbwfLbyyp1mzpxpfvDBB62mTZw40dyzZ09nhma3nLTFlk6dOpnffPNNJ0SUN/a0Z+TIkeY5c+aY33jjDfPDDz/s5MiAnDNSjjGbyTPkGXgi8kw68gzgWN27dzdPmjTJatqDDz5ofvXVV20u/9lnn5kbNWpkTklJcUF09rO3PYsXLza3a9fOatry5cvNrVq1clqMuWG0YwDyD3KMd+QYs5k8A+PLTV+MkURERJjXrFmTL9p9+fJlc4cOHcxffPGF+f/+7//MU6dONZvN7v3MudPaoOLj49WsWTM98MADmjBhgpKTky3zDh06pJs3b6pFixaWaaVLl1b16tW1f/9+d4SbrUuXLkmSihcvLklKSEhQUlKSWrZsaVnG19dXERERljZ4WzvvbGMGo32WMIbvvvvO6nsnpY8KkPGd9Ga3b9/WlStXFBgY6O5Qcm3dunX6448/NGzYMHeHAuSKkXOMRJ4BPAF5xvORZ+BJUlNTdfjwYavfICSpRYsWWZ5779q1S7Vr19bixYsVGRmpBx54QDNmzND169ddEfJd5aY9DRo00OnTp/XZZ5/JbDbr3Llz+vjjj9W6dWtXhOxQRj8GwPuQY4yVYyTyDLxbbvpijCAtLU1btmzR1atX1aBBg3zR7ilTpqh169Zq3ry51XR3tr2gU9cOt2jVqpUefPBBlS1bVgkJCXr99dfVr18/rV+/Xr6+vjp37pwKFSqUqXM0ODhY586dc1PUWTObzYqJiVGjRo0UFhYmSUpKSpIklSxZ0mrZ4OBgyzAz3tROW22UjPdZwjjOnTun4OBgq2klS5bUrVu3lJKSolKlSrkpsrxbunSprl27po4dO7o7lFw5fvy4Zs2apZUrV6pgQQ7z8E5GzjESeQbwBOQZz0aegadJSUlRWlqazd8gMn6fuNOJEyf0zTffqHDhwpo3b55SUlIUHR2t8+fPKyYmxhVhZyk37WnYsKFeffVVjRw5Uqmpqbp165batm2riRMnuiJkhzL6MQDehxxjrBwjkWfgvXLbF+PNjhw5ol69eunGjRvy9/fXvHnzVK1aNX377beSjNvuLVu26IcfftDatWszzXPnZ86d1gbUqVMntWnTRmFhYWrbtq0WLVqk48eP69NPP73r+8xms2sCtNOUKVN09OhRzZ49O9M8k8lk9TonbfDEdmbVRqN9ljCWrPa/O6d7k82bN2vu3LmaM2dOpoOyN0hLS9OoUaM0fPhwVa5c2d3hAHlixBwjkWcAT0Ke8UzkGXgyW3kjq5yRMe/VV19V3bp11bp1a40fP14ffvihR9wJKdnXnmPHjmnq1KkaOnSo1q1bp8WLFyshIUEvvviiK0J1OKMeA+DdyDHGyTESeQbeydF9Md6gcuXK2rBhg1avXq3HH39c48aN07FjxyzzjdjuxMRETZs2Ta+88ooKFy6c5XLuaDuXLOcDpUqVUtmyZXX8+HFJ6VdD3Lx5UxcuXLC6Qzc5OVkNGjRwU5S2vfTSS9q1a5dWrFihMmXKWKaHhIRISr9q7a9XpiUnJ1uuYvOWdmbVRlu8+bOEsdi6OvbPP/9UwYIFvXYYyq1bt+qFF17Q66+/nmlIFG9x5coVHTp0SD/++KNeeuklSenDg5rNZtWsWVNLlixRs2bN3BwlkD0j5hiJPAN4EvKM5yLPwBMFBQXJx8cn04hmf/0N4k4hISEqXbq0AgICLNOqVq0qs9ms06dPq1KlSs4M+a5y057Y2Fg1bNhQTz75pCSpRo0aKlKkiHr37q2RI0d61V2DRj0GwHuRY4yVYyTyDLxTXvpivJmvr68qVqwoSapTp44OHjyo5cuXa+DAgZKM2e7Dhw8rOTlZUVFRlmlpaWnat2+fVq5cqW3btklyT9u50zofSElJUWJiouXLVbt2bRUqVEhffPGFZZmzZ8/q559/9piOTrPZrClTpmj79u165513VKFCBav55cuXV0hIiFUbUlNTtW/fPksbPL2d2bXRFm/8LGFM9evX15dffmk1bc+ePZbvpLfZvHmzxo8fr1mzZqlNmzbuDifXihUrpk2bNmnDhg2Wv169elmuGKxXr567QwRyxGg5RiLPAJ6GPOO5yDPwRL6+vqpVq5bVubckffnll1meezds2FBnz57VlStXLNN+++03FShQINsL1p0tN+25fv26ChSw/hnRx8dHkvfdcWTEYwC8GznGWDlGIs/AuziiL8ZIzGazUlNTDd3upk2bZjrnql27trp06aINGzaoQoUKbms7d1p7oStXruiPP/6wvE5ISNCPP/6o4sWLq3jx4po7d646dOigkJAQnTx5UnPmzFFQUJDat28vSQoICFC3bt00Y8YMBQUFqXjx4poxY4bCwsI85mr86Ohobd68WfPnz1fRokUtV6YFBATIz89PJpNJffv2VWxsrCpVqqSKFSsqNjZWfn5+6ty5s2VZT25ndm28cuWKIT5LeIe75ZWyZctq1qxZOnPmjGbOnClJ6tWrl1auXKmYmBj17NlT+/fv17p16zRr1ix3NcHC3rZs3rxZ48aN0/PPP6969epZ9kU/Pz+rK5bdxZ72FChQwPK8mQwlS5ZU4cKFM00HXMlIOUYiz5Bn4InIM+QZwNn69++vsWPHqnbt2mrQoIFWr16txMRE9erVS5Iy7ZedO3fW/Pnz9dxzz+mZZ55RSkqKXnnlFXXr1k1+fn7ubIok+9tz//33a+LEiVq1apUiIyN19uxZvfzyy6pbt65Kly7tzqYY7hiA/Ikc47k5RiLPwNgc0RfjrWbPnq1WrVqpTJkyunLlirZu3ar4+HgtXrzY0O0uVqxYpnMrf39/BQYGWqa7q+10WnuhQ4cOqW/fvpbXMTExkqSuXbtq8uTJOnr0qDZs2KBLly4pJCRETZo00Zw5c1SsWDHLe55//nkVLFhQI0eO1PXr19WsWTNNnz7dcgWbu7333nuSpD59+lhNj4mJsQxZMHDgQN24cUPR0dG6cOGC6tWrp6VLl3pNO7Nro4+PjyE+S3iHu+WV6dOnKykpSYmJiZb5FSpU0MKFCxUTE6OVK1eqVKlSeuGFF/TAAw+4PPY72duW1atX69atW5oyZYqmTJlimZ6xvLvZ2x7AExkpx0jkGcATkWfIM4CzderUSSkpKZo/f77Onj2rsLAwLVy4UOXKlZOkTN/jokWLaunSpZo6daq6deumwMBAdezYUSNHjnRTC6zZ256oqChduXJFK1eu1IwZMxQQEKCmTZtqzJgx7mqChdGOAcifyDGem2Mk8gyMzVF9Md7o3LlzGjt2rM6ePauAgACFh4dr8eLFatGihSTjtjsn3NV2k9kbx9cAAAAAAAAAAAAAABgCz7QGAAAAAAAAAAAAALgNndYAAAAAAAAAAAAAALeh0xoAAAAAAAAAAAAA4DZ0WgMAAAAAAAAAAAAA3IZOawAAAAAAAAAAAACA29BpDQAAAAAAAAAAAABwGzqtAQAAAAAAAAAAAABuQ6e1g61fv17h4eFKSEiw+7179+5VeHi4tm3blqNt2PqbMWOGZbm2bdvqqaeesjuODHPnzlWnTp10+/Ztq+kpKSmaNWuWOnXqpHr16qlhw4bq2bOnVq5cqZs3b+Z6e1m5efOm2rdvr7fffjtHy9/57/Pnn3/avc2EhASFh4dryZIlOVp++fLlatKkiW7dumX3tmwJDw/Xm2++mav3Pv3005a2d+7c2SHxwDh++uknPffcc2rbtq3q1KmjBg0aqGvXrlq0aJHOnz/v7vDs9txzz2nAgAGW1/buu+702muvqWvXrplybFb+mtf+2r6MY0d4eLjWr19v8719+/ZVeHi42rZtazX9kUcesbw3L8cL4K/IM56DPAMjIsd4DnIM8huj558MiYmJmjJlitq3b686deooIiJCffr00caNG2U2mx0ex4ULF3Tfffdp586dOVr+zTfftOz3DRo0sJrXp08fhYeHq127djZj3bdvn818s3PnTqscdfDgwbw1Csgl8gx5BgDyu4LuDsBo2rRpo9WrV6tUqVJO31ZMTIyqVKliNc1R2z1z5oyWLFmimJgYFSjwv2sbfvnlFw0YMEBXr15V//791aBBA924cUOffPKJpk2bpm3btmnhwoUqUqSIQ+KQpEKFCmno0KGKiYnRI488oqCgoBy9b+7cuQoJCdE999zjsFiysn37drVt21YFCzpml1q9erXKlCmTq/eOGTNGgwYNUnR0tFMuIoD3WrNmjaKjo1W5cmUNGDBA1apV061bt3To0CG9//77+u677zRv3jx3h5ljP/zwgzZs2KA1a9a4O5RcGTBggFauXKkPP/xQ3bp1y9F7unfvrh49eqhs2bKZ5hUtWlRr165VVFSU1fQTJ04oPj5exYoVy/SemTNn6tq1axo2bFjuGgHcgTzjWcgzMBpyjGchxyA/yS/555tvvtHgwYPl7++vAQMGKDw8XJcuXdJHH32kMWPG6JNPPtGsWbOsfifKq+LFi+sf//iHZs6cqVatWsnX1zdH71u9erXNOIoWLaqEhAR9/fXXatasmdW8devWqVixYrp8+bLV9IiICK1evVqffvqpFixYkPvGAHlAniHPAPnB+vXr9dxzz1le+/j4qESJEoqIiNCIESNUqVIlq+X79OmjlJQUbd68OdO6/vzzTzVr1kzDhg3T8OHDs9zm3r171bdvX8vrtWvXqk6dOpKko0eP6sUXX9RPP/2kypUr64UXXlCjRo2s3r948WJ98MEH2rhxowoXLmw17+mnn1ZcXJwkqXr16jbjhH3otHawEiVKqESJEi7ZVvXq1S07l6MtX75cAQEB6tChg2VaWlqannnmGV2+fFkffPCBKleubJnXunVrNW7cWM8++6xiYmI0ZcqULNedlpamtLS0HBcIkvTQQw9p+vTpWr16tQYPHpyj9/ztb39T+fLlc7yN3Dp37py++eYbPfnkkw5bZ/369XP93ozPpVixYkpJSXFQRPB2+/fv1+TJk9W8eXPNnz/fav9r0aKF+vfvr927d7sxQvstXLhQdevWdVoetNe1a9fsumAnICBADz/8sBYuXKioqCiZTKZs31OmTJks80OnTp30wQcf6Pjx41YF3rp161S6dGmFhYXpl19+sXpPeHi4JNmVj4GskGecjzyD/Iwc43zkGMC2/JJ/Ll68qOHDhysgIEBr1qxRcHCwZV779u0VHh6uWbNm6W9/+5sGDRqU5bpv3rwpk8lk10X9vXr10oIFC/Txxx+rS5cuOXpPVrmkbNmylotg/tqZdPnyZW3btk1dunTJ1IlWvHhx1a9fX7/++muOYwYciTxDngHym4wbMm/cuKFvv/1Wb731lvbu3auPPvpIxYsXd8o2J02apFq1aqlq1aqSpFu3bmn48OGqWrWq5s6dqy1btujpp5/Wjh07LDdCJiQkaN68eZo/f36mDmuJGwidgeHBHczW8OB9+vRR586ddeDAAT3xxBOqV6+e2rVrp4ULF2Y7lNrly5c1YMAANW/eXAcOHHB2+JKk1NRUrV27Vp07d7a6mmzHjh06duyYBg0aZNVhnaFTp05q2bKl1q1bp6SkJEn/G95u0aJFmj9/vmV4m6+//lpS+tAoXbp0Ue3atdWuXTu98847liFY/srX11cdO3bUmjVr8jRMTNu2bTV+/PhM0/v06aM+ffrkap07duyQv7+/mjdvLul/34GvvvpKEyZMUJMmTdSwYUONHTtWV69eVVJSkkaMGKH77rtPLVu21IwZMzIltDuHB89Y59dff60XX3xRTZo0UZMmTTRs2DCdOXMmV3Ejf4mNjZXJZNJLL71k80c9X19ftWvXzvL69u3bWrRokR588EHVrl1bzZo109ixY3X69Gmr99mT3y5evKjp06erXbt2lnUOHDgw04+POXHu3Dnt3LlTDz/8cLbL2rP/2Nvuffv2qVevXqpXr56ef/55SdLp06f1zDPPqEGDBrrvvvs0atQoHThwwOaQl4888oiOHz9uyYl50bx5c4WGhmrdunVW7dmwYYMeffRRh16lDNhCniHPkGfgTOQYcgw5Bu6SX/LPBx98oOTkZI0aNcqqIynDwIEDVaVKFS1ZssTyG0bG8P4bNmzQ9OnTFRkZqTp16uj333+XlH7n6AMPPKDatWurU6dO2rRpk8aPH59pqP/g4GA1b95c77//vt3tsaVbt27asWOHLl68aJm2detWSem/XQGehjyTjjwD5B/Vq1dX/fr11aRJEw0ZMkQDBw5UcnJyjofxz41q1aqpfv368vf3lyT9/vvvOn78uCZPnqwWLVooOjpaqamp+u677yzvmTx5sh544IFMoypkqFy5surXr29z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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# make scatterplots for all feature pairs\n", - "features = df.columns\n", - "plt.figure(figsize=(20, 20)) # adjust size as needed\n", - "plot_index = 1\n", - "for i in range(len(features)):\n", - " for j in range(i + 1, len(features)):\n", - " plt.subplot(len(features) - 1, len(features) - 1, plot_index)\n", - " plt.scatter(df[features[i]], df[features[j]], color='orange', edgecolor='orange')\n", - " plt.xlabel(features[i], fontsize=12)\n", - " plt.ylabel(features[j], fontsize=12)\n", - " plt.tight_layout()\n", - " plt.tick_params(axis='both', which='major', labelsize=10)\n", - " plot_index += 1\n", - "plt.suptitle(\"Scatterplots for Feature Pairs\", y=1.02, fontsize=14)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\eschw\\AppData\\Local\\Temp\\ipykernel_121920\\4185179031.py:8: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.\n", - " plt.tight_layout()\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "correlation_matrix = df.corr()\n", - "plt.figure(figsize=(6, 5))\n", - "plt.matshow(correlation_matrix, cmap='coolwarm', fignum=1)\n", - "plt.colorbar()\n", - "plt.xticks(range(len(features)), features, rotation=45, fontsize=10)\n", - "plt.yticks(range(len(features)), features, fontsize=10)\n", - "plt.title(\"Pearson Correlation Matrix\", fontsize=14, pad=20)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:base] *", - "language": "python", - "name": "conda-base-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/unsorted/UW-MIT FOM MOBO-Copy4.ipynb b/unsorted/UW-MIT FOM MOBO-Copy4.ipynb deleted file mode 100644 index bb0ab0b..0000000 --- a/unsorted/UW-MIT FOM MOBO-Copy4.ipynb +++ /dev/null @@ -1,1560 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 72, - "id": "cda845d3-bb40-428a-b2a5-e4cce3c5a659", - "metadata": {}, - "outputs": [ - { - "ename": "ImportError", - "evalue": "cannot import name 'qLogNoisyExpectedHypervolumeImprovement' from 'botorch.acquisition.multi_objective.monte_carlo' (C:\\Users\\eschw\\anaconda3\\envs\\mobo-env\\lib\\site-packages\\botorch\\acquisition\\multi_objective\\monte_carlo.py)", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mImportError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[72], line 21\u001b[0m\n\u001b[0;32m 18\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mmatplotlib\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpyplot\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mplt\u001b[39;00m\n\u001b[0;32m 19\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msklearn\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmetrics\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m r2_score, mean_absolute_error, mean_squared_error\n\u001b[1;32m---> 21\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mbotorch\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01macquisition\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmulti_objective\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmonte_carlo\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m qExpectedHypervolumeImprovement, qNoisyExpectedHypervolumeImprovement, qLogNoisyExpectedHypervolumeImprovement\n\u001b[0;32m 22\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mbotorch\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mutils\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmulti_objective\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpareto\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m is_non_dominated\n\u001b[0;32m 23\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mbotorch\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mutils\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mmulti_objective\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mhypervolume\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Hypervolume\n", - "\u001b[1;31mImportError\u001b[0m: cannot import name 'qLogNoisyExpectedHypervolumeImprovement' from 'botorch.acquisition.multi_objective.monte_carlo' (C:\\Users\\eschw\\anaconda3\\envs\\mobo-env\\lib\\site-packages\\botorch\\acquisition\\multi_objective\\monte_carlo.py)" - ] - } - ], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import torch\n", - "from sklearn.preprocessing import StandardScaler\n", - "\n", - "from botorch.models import SingleTaskGP, ModelListGP\n", - "from gpytorch.kernels import MaternKernel, ScaleKernel, RBFKernel\n", - "from botorch.fit import fit_gpytorch_mll\n", - "from gpytorch.mlls import ExactMarginalLogLikelihood, LeaveOneOutPseudoLikelihood\n", - "from gpytorch.mlls.sum_marginal_log_likelihood import SumMarginalLogLikelihood\n", - "from gpytorch.priors import GammaPrior, LogNormalPrior\n", - "import gpytorch\n", - "\n", - "import matplotlib.pyplot as plt\n", - "from mpl_toolkits.mplot3d import Axes3D\n", - "\n", - "from scipy.spatial import Delaunay\n", - "import matplotlib.pyplot as plt\n", - "from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error\n", - "\n", - "from botorch.acquisition.multi_objective.monte_carlo import qExpectedHypervolumeImprovement, qNoisyExpectedHypervolumeImprovement, qLogNoisyExpectedHypervolumeImprovement\n", - "from botorch.utils.multi_objective.pareto import is_non_dominated\n", - "from botorch.utils.multi_objective.hypervolume import Hypervolume\n", - "from botorch.utils.multi_objective.box_decompositions import NondominatedPartitioning\n", - "from botorch.sampling.normal import SobolQMCNormalSampler\n", - "from botorch.optim.optimize import optimize_acqf, optimize_acqf_list\n", - "\n", - "from botorch.utils.transforms import unnormalize\n", - "import seaborn as sns\n", - "from scipy.spatial.distance import pdist, squareform" - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "35959029-3e5a-4c9e-a582-dadaeb1503e4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2.4.0\n", - "True\n", - "12.4\n", - "NVIDIA GeForce GTX 1080 with Max-Q Design\n" - ] - } - ], - "source": [ - "import torch\n", - "print(torch.__version__)\n", - "print(torch.cuda.is_available())\n", - "print(torch.version.cuda)\n", - "print(torch.cuda.get_device_name(0))\n" - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "3e1e062d-8959-4682-bf9b-8f05a47f83ba", - "metadata": {}, - "outputs": [], - "source": [ - "def x_normalizer(X, var_array):\n", - " \n", - " def max_min_scaler(x, x_max, x_min):\n", - " return (x-x_min)/(x_max-x_min)\n", - " x_norm = []\n", - " for x in (X):\n", - " x_norm.append([max_min_scaler(x[i], \n", - " max(var_array[i]), \n", - " min(var_array[i])) for i in range(len(x))])\n", - " \n", - " return x_norm\n", - "\n", - "def x_denormalizer(x_norm, var_array):\n", - " \n", - " def max_min_rescaler(x, x_max, x_min):\n", - " return x*(x_max-x_min)+x_min\n", - " x_original = []\n", - " for x in (x_norm):\n", - " x_original.append([max_min_rescaler(x[i], \n", - " max(var_array[i]), \n", - " min(var_array[i])) for i in range(len(x))])\n", - " \n", - " return x_original\n", - "\n", - "def get_closest_value(given_value, array_list):\n", - " absolute_difference_function = lambda list_value : abs(list_value - given_value)\n", - " closest_value = min(array_list, key=absolute_difference_function)\n", - " return closest_value\n", - " \n", - "def get_closest_array(suggested_x, var_list):\n", - " modified_array = []\n", - " for x in suggested_x:\n", - " modified_array.append([get_closest_value(x[i], var_list[i]) for i in range(len(x))])\n", - " return np.array(modified_array)" - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "2bfcba47-fe51-47be-9be4-798dc3aa4d4f", - "metadata": {}, - "outputs": [], - "source": [ - "def make_linspace(start, stop, step):\n", - " num_points = int(round((stop - start) / step)) + 1\n", - " return np.round(np.linspace(start, stop, num_points), 4) # round for cleaner floats\n", - "\n", - "speed_inorg_var = make_linspace(0.25, 1.00, 0.01) # m/min\n", - "speed_org_var = make_linspace(0.25, 1.00, 0.01) # m/min\n", - "inkFL_inorg_var = make_linspace(80, 240, 1) # uL/min\n", - "inkFL_org_var = make_linspace(100, 280, 1) # uL/min\n", - "conc_inorg_var = make_linspace(0.8, 1.4, 0.05) # M\n", - "conc_org_var = make_linspace(0.4, 1.2, 0.05) # M\n", - "humidity_var = make_linspace(2, 37, 1) # g/m^3\n", - "temp_var = make_linspace(20, 50, 1) # °C\n", - "\n", - "# Tracking unique values\n", - "speed_inorg_num = len(speed_inorg_var)\n", - "speed_org_num = len(speed_org_var)\n", - "inkFL_inorg_num = len(inkFL_inorg_var)\n", - "inkFL_org_num = len(inkFL_org_var)\n", - "conc_inorg_num = len(conc_inorg_var)\n", - "conc_org_num = len(conc_org_var)\n", - "humidity_num = len(humidity_var)\n", - "temp_num = len(temp_var)\n", - "\n", - "# Pack into var_array for downstream normalization\n", - "var_array = [speed_inorg_var, speed_org_var, \n", - " inkFL_inorg_var, inkFL_org_var,\n", - " conc_inorg_var, conc_org_var,\n", - " humidity_var, temp_var]\n", - "\n", - "x_labels = ['Speed (Inorg) [m/min]', 'Speed (Org) [m/min]', \n", - " 'inkFL (Inorg) [uL/min]', 'inkFL (Org) [uL/min]',\n", - " 'Conc. (Inorg) [M]', 'Conc. (Org) [M]',\n", - " 'AH [g/m^3]', 'Temp [C]']" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "3c85426d-2ead-4f86-b604-bb9bc2c6c47f", - "metadata": {}, - "outputs": [], - "source": [ - "# parameter_space = ParameterSpace([ContinuousParameter('x1', 0, 1),\n", - "# ContinuousParameter('x2', 0, 1),\n", - "# ContinuousParameter('x3', 0, 1),\n", - "# ContinuousParameter('x4', 0, 1),\n", - "# ContinuousParameter('x5', 0, 1),\n", - "# ContinuousParameter('x6', 0, 1),\n", - "# ContinuousParameter('x7', 0, 1),\n", - "# ContinuousParameter('x8', 0, 1),\n", - "# ])\n", - "\n", - "## why is the parameter space different in the round 1 of the Joule code?" - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "id": "a948d7fa-4a19-4ead-b20d-988188fc711e", - "metadata": {}, - "outputs": [], - "source": [ - "def check_clausius_clapeyron(ah_vals, temp_vals):\n", - " \"\"\"\n", - " Return boolean mask for valid points satisfying Clausius-Clapeyron constraints.\n", - " Ensures absolute humidity does not exceed saturation limit at given temperature.\n", - " \n", - " Parameters:\n", - " ah_vals: array-like, absolute humidity [g/m³]\n", - " temp_vals: array-like, temperature [°C]\n", - " \n", - " Returns:\n", - " valid_mask: boolean array, True if AH is within saturation limit\n", - " \"\"\"\n", - " valid_mask = []\n", - " for AH, T in zip(ah_vals, temp_vals):\n", - " # Saturation vapor pressure (kPa)\n", - " es = 0.6108 * np.exp((17.27 * T) / (T + 237.3))\n", - " # Max absolute humidity (g/m³)\n", - " AH_max = 216.7 * es / (T + 273.15)\n", - " valid_mask.append(AH <= AH_max)\n", - " return np.array(valid_mask)" - ] - }, - { - "cell_type": "code", - "execution_count": 154, - "id": "ce471e12-4e94-47ad-9588-f2040cf3e219", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "tensor([[0.9057, 0.0000, 0.0947],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.6506, 0.2377, 0.7895],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [1.0000, 1.0000, 0.0000]], device='cuda:0', dtype=torch.float64)\n", - "tensor([[0.0667, 0.0667, 0.0156, 0.1111, 1.0000, 0.0000, 0.0000, 0.1000],\n", - " [0.4400, 0.0667, 0.6875, 0.1889, 0.0833, 0.4375, 0.0286, 0.1567],\n", - " [0.9333, 0.4400, 0.5625, 0.8111, 0.1667, 0.5625, 0.4857, 0.8000],\n", - " [0.5600, 0.9333, 0.4375, 0.4389, 0.4167, 0.9375, 0.1143, 0.3933],\n", - " [0.6933, 0.1867, 0.3125, 0.9389, 0.5833, 0.3125, 0.1143, 0.2833],\n", - " [0.1867, 0.5600, 0.9375, 0.5611, 0.6667, 0.1875, 0.3714, 0.7200],\n", - " [0.0667, 0.8133, 0.0625, 0.6889, 0.8333, 0.6875, 0.4857, 0.7200],\n", - " [0.8133, 0.3067, 0.8125, 0.0611, 0.9167, 0.8125, 0.4000, 0.5433]],\n", - " device='cuda:0', dtype=torch.float64)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 154, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#Load your real LHS experimental results (replace with real path)\n", - "dataset_final = pd.read_csv(r'C:\\Users\\eschw\\Downloads\\R0 Summary for MOBO R1 conditions.csv')\n", - "#dataset_np = dataset_final.to_numpy()\n", - "#print(dataset_np)\n", - "\n", - "#Extract input features (first 8 columns assumed to be process parameters) ===\n", - "X = dataset_final.iloc[:, 0:8].values # shape: (8, 8)\n", - "\n", - "#Extract targets\n", - "y = dataset_final[['PCE', 'Stability', 'Repeatability']].values # shape: (8, 3)\n", - "#print(y)\n", - "\n", - "#Standardize input and output\n", - "X_scaled = x_normalizer(X, var_array)\n", - "# Y_scaled = StandardScaler().fit_transform(y) ## is this the right way to normalize this? do i need to normalize this?\n", - "Y_max = np.max(y, axis=0)\n", - "Y_min = np.min(y, axis=0)\n", - "Y_scaled = (y - Y_min) / (Y_max - Y_min)\n", - "Y_scaled[:, 2] = 1 - Y_scaled[:, 2]\n", - "#print(Y_scaled)\n", - "\n", - "#Convert to torch tensors\n", - "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", - "train_X = torch.tensor(X_scaled, dtype=torch.float64, device=device)\n", - "#print(train_X)\n", - "train_Y = torch.tensor(Y_scaled, dtype=torch.float64, device=device)\n", - "print(train_Y)\n", - "print(train_X)\n", - "\n", - "#Set seeds for reproducibility\n", - "np.random.seed(20)\n", - "torch.manual_seed(20)" - ] - }, - { - "cell_type": "code", - "execution_count": 129, - "id": "ae38a521-2b43-48f3-81c8-c82733926285", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Speed (Inorg) [m/min]Speed (Org) [m/min]Pump rate (Inorg) [uL/min]Pump rate (Org) [uL/min]Conc. (Inorg) [M]Conc. (Org) [M]AH (Org) [g/m^3]Temp (Org) [C]PCEStabilityRepeatability
00.300.3082.51201.400.40223.014.880.000.86
10.580.30190.01340.850.75324.70.000.000.00
20.950.58170.02460.900.851944.00.000.000.00
30.670.95150.01791.051.15631.80.000.000.00
40.770.39130.02691.150.65628.50.000.000.00
50.390.67230.02011.200.551541.610.691.350.20
60.300.8690.02241.300.951941.60.000.000.00
70.860.48210.01111.351.051636.316.435.680.95
\n", - "
" - ], - "text/plain": [ - " Speed (Inorg) [m/min] Speed (Org) [m/min] Pump rate (Inorg) [uL/min] \\\n", - "0 0.30 0.30 82.5 \n", - "1 0.58 0.30 190.0 \n", - "2 0.95 0.58 170.0 \n", - "3 0.67 0.95 150.0 \n", - "4 0.77 0.39 130.0 \n", - "5 0.39 0.67 230.0 \n", - "6 0.30 0.86 90.0 \n", - "7 0.86 0.48 210.0 \n", - "\n", - " Pump rate (Org) [uL/min] Conc. (Inorg) [M] Conc. (Org) [M] \\\n", - "0 120 1.40 0.40 \n", - "1 134 0.85 0.75 \n", - "2 246 0.90 0.85 \n", - "3 179 1.05 1.15 \n", - "4 269 1.15 0.65 \n", - "5 201 1.20 0.55 \n", - "6 224 1.30 0.95 \n", - "7 111 1.35 1.05 \n", - "\n", - " AH (Org) [g/m^3] Temp (Org) [C] PCE Stability Repeatability \n", - "0 2 23.0 14.88 0.00 0.86 \n", - "1 3 24.7 0.00 0.00 0.00 \n", - "2 19 44.0 0.00 0.00 0.00 \n", - "3 6 31.8 0.00 0.00 0.00 \n", - "4 6 28.5 0.00 0.00 0.00 \n", - "5 15 41.6 10.69 1.35 0.20 \n", - "6 19 41.6 0.00 0.00 0.00 \n", - "7 16 36.3 16.43 5.68 0.95 " - ] - }, - "execution_count": 129, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dataset_final" - ] - }, - { - "cell_type": "code", - "execution_count": 130, - "id": "921588f7-a878-4948-a9d1-49f1a4a8a2ee", - "metadata": {}, - "outputs": [], - "source": [ - "def fit_gp_models(X, Y, kernel_fn=None, noise_prior=None):\n", - " \"\"\"\n", - " Fits a list of GP models for each output dimension.\n", - " Returns a ModelListGP.\n", - " \n", - " Args:\n", - " X: torch tensor of shape (N, D)\n", - " Y: torch tensor of shape (N, M)\n", - " kernel_fn: function taking D and returning a GPyTorch kernel\n", - " noise_prior: artificial noise to kernel\n", - "\n", - " Returns:\n", - " model: ModelListGP with one GP per output\n", - " \"\"\"\n", - " models = []\n", - " for j in range(Y.shape[1]):\n", - " covar_module = ScaleKernel(kernel_fn(X.shape[1])) if kernel_fn else RBFKernel(ard_num_dims = X.shape[1])\n", - " likelihood = gpytorch.likelihoods.GaussianLikelihood(noise_prior=noise_prior) if noise_prior else gpytorch.likelihoods.GaussianLikelihood()\n", - " gp = SingleTaskGP(X, Y[:, j:j+1], covar_module=covar_module, likelihood=likelihood)\n", - " mll = ExactMarginalLogLikelihood(gp.likelihood, gp)\n", - " #mll = LeaveOneOutPseudoLikelihood(gp.likelihood, gp)\n", - " fit_gpytorch_mll(mll)\n", - " models.append(gp)\n", - " return ModelListGP(*models)\n", - "\n", - "# for i, gp in enumerate(gp_models):\n", - "# ls = gp.covar_module.base_kernel.lengthscale.detach().cpu().numpy().flatten()\n", - "# print(f\"Objective {i+1} lengthscales: {np.round(ls, 3)}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": 131, - "id": "d4b8f005-c5d5-438c-bb94-59ab3f8cb0fc", - "metadata": {}, - "outputs": [], - "source": [ - "matern_options = {\n", - " \"Matern_0.5\": lambda d: MaternKernel(nu=0.5, ard_num_dims=d),\n", - " \"Matern_1.5\": lambda d: MaternKernel(nu=1.5, ard_num_dims=d),\n", - " \"Matern_2.5\": lambda d: MaternKernel(nu=2.5, ard_num_dims=d),\n", - "}\n", - "\n", - "noise_options = {\n", - " \"None\": None,\n", - " \"Gamma(1.1, 0.05)\": GammaPrior(1.1, 0.05).to(device),\n", - " \"LogNormal(-4, 0.1)\": LogNormalPrior(-4.0, 0.5).to(device),\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": 132, - "id": "e10eaa40-f13b-4441-9deb-777b06bf6c5d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Kernel NoisePrior Objective R2 RMSE\n", - "Matern_0.5 None PCE -1.532 0.672\n", - "Matern_0.5 None Stability -0.246 0.367\n", - "Matern_0.5 None Repeatability -1.025 0.575\n", - "Matern_0.5 Gamma(1.1, 0.05) PCE -1.441 0.660\n", - "Matern_0.5 Gamma(1.1, 0.05) Stability -0.063 0.339\n", - "Matern_0.5 Gamma(1.1, 0.05) Repeatability -0.974 0.567\n", - "Matern_0.5 LogNormal(-4, 0.1) PCE -1.674 0.691\n", - "Matern_0.5 LogNormal(-4, 0.1) Stability -0.238 0.366\n", - "Matern_0.5 LogNormal(-4, 0.1) Repeatability -1.009 0.572\n", - "Matern_1.5 None PCE -1.473 0.664\n", - "Matern_1.5 None Stability -0.247 0.367\n", - "Matern_1.5 None Repeatability -1.375 0.622\n", - "Matern_1.5 Gamma(1.1, 0.05) PCE -1.472 0.664\n", - "Matern_1.5 Gamma(1.1, 0.05) Stability -0.347 0.382\n", - "Matern_1.5 Gamma(1.1, 0.05) Repeatability -1.379 0.623\n", - "Matern_1.5 LogNormal(-4, 0.1) PCE -1.600 0.681\n", - "Matern_1.5 LogNormal(-4, 0.1) Stability -1.807 0.551\n", - "Matern_1.5 LogNormal(-4, 0.1) Repeatability -0.876 0.553\n", - "Matern_2.5 None PCE -1.444 0.660\n", - "Matern_2.5 None Stability -0.671 0.425\n", - "Matern_2.5 None Repeatability -1.417 0.628\n", - "Matern_2.5 Gamma(1.1, 0.05) PCE -1.441 0.660\n", - "Matern_2.5 Gamma(1.1, 0.05) Stability -0.476 0.399\n", - "Matern_2.5 Gamma(1.1, 0.05) Repeatability -1.419 0.628\n", - "Matern_2.5 LogNormal(-4, 0.1) PCE -1.476 0.665\n", - "Matern_2.5 LogNormal(-4, 0.1) Stability -1.841 0.554\n", - "Matern_2.5 LogNormal(-4, 0.1) Repeatability -0.910 0.558\n" - ] - } - ], - "source": [ - "# Grid search with LOO-CV\n", - "results = []\n", - "best_rmse = [np.inf] * train_Y.shape[1]\n", - "best_models = [None] * train_Y.shape[1]\n", - "\n", - "for kernel_name, kernel_fn in matern_options.items():\n", - " for noise_name, noise_prior in noise_options.items():\n", - " preds_all = [[] for _ in range(train_Y.shape[1])]\n", - " actuals_all = [[] for _ in range(train_Y.shape[1])]\n", - "\n", - " for i in range(train_X.shape[0]):\n", - " X_cv = torch.cat([train_X[:i], train_X[i+1:]])\n", - " Y_cv = torch.cat([train_Y[:i], train_Y[i+1:]])\n", - " model_cv = fit_gp_models(X_cv, Y_cv, kernel_fn, noise_prior)\n", - "\n", - " for j, gp in enumerate(model_cv.models):\n", - " pred = gp.posterior(train_X[i:i+1]).mean.item()\n", - " preds_all[j].append(pred)\n", - " actuals_all[j].append(train_Y[i, j].item())\n", - "\n", - " for j in range(train_Y.shape[1]):\n", - " r2 = r2_score(actuals_all[j], preds_all[j])\n", - " rmse = mean_squared_error(actuals_all[j], preds_all[j], squared=False)\n", - " \n", - " # Save best model for this objective\n", - " if rmse < best_rmse[j]:\n", - " best_rmse[j] = rmse\n", - " model_j = fit_gp_models(train_X, train_Y, kernel_fn, noise_prior).models[j]\n", - " best_models[j] = model_j\n", - "\n", - "\n", - " results.append({\n", - " \"Kernel\": kernel_name,\n", - " \"NoisePrior\": noise_name,\n", - " \"Objective\": [\"PCE\", \"Stability\", \"Repeatability\"][j],\n", - " \"R2\": round(r2, 3),\n", - " \"RMSE\": round(rmse, 3)\n", - " })\n", - " \n", - "model = ModelListGP(*best_models)\n", - "\n", - "# Show results in a DataFrame\n", - "results_df = pd.DataFrame(results)\n", - "print(results_df.to_string(index=False))" - ] - }, - { - "cell_type": "code", - "execution_count": 133, - "id": "fd4993b4-1d74-4fd5-894c-1b9f34bc814c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[ 8.81750162e-01 1.68933651e-02 8.67137507e-02]\n", - " [ 9.09196457e-03 1.56690456e-02 9.96708348e-01]\n", - " [ 5.12375491e-04 1.05653783e-02 9.99984523e-01]\n", - " [ 1.95982376e-02 1.46428673e-02 9.85204061e-01]\n", - " [-4.48693139e-03 1.68445666e-02 1.00061802e+00]\n", - " [ 6.28982832e-01 2.64144661e-01 8.09152369e-01]\n", - " [ 2.78691662e-02 1.26756171e-02 9.81969048e-01]\n", - " [ 9.92981047e-01 8.86240355e-01 2.38606894e-02]]\n" - ] - }, - { - "data": { - "image/png": "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7KFTp07ky5ePSpUq0adPH+bPn09YWNhDXbtnz56UKVMmTpmDgwP9+vWja9eunD59mh07diR4bmRkJH5+fnHa7qZNm1pj/eSTT+KMOstIjz32GKVKleKff/5h//798fbH9EJTTzMRkZwnKCiIP/74g/bt2xMZGYmvry9NmjQBzN7IY8aMwd7ensWLF8eZgsze3p4RI0bw7LPPcv78eRYuXBjv2pGRkbz22mt07drVWpY/f35++uknnJ2d+euvv+KM8vrnn3+YN28e3t7eLF26NE6bnC9fPn744Qe8vLxYvHgxZ8+ete5zd3enV69eeHp6xvl8Dw8PvvzyS7y8vFiwYEGC06s9SKdOnfDx8YlTZrFYaNeuHW+//TZBQUHWHvoJ1f/LL7+kevXq1rKqVatap077+uuvCQ4OTnFMaaFcuXI0adKE69evJ7iOjL4bSHpREl1Ekm3ixIk0btyYxo0bU7VqVby8vPjjjz9wdHRkxowZ5M2b19qQurq6pvrz6tSpE2f41r0/NWvWTPX1RUREMtp7773Hn3/+yVNPPUWuXLkwDINjx47h5+dH165dqVChQpJTryTl6NGjjB49mo4dO9K8eXNrmx0zp/rff/+d4HkNGzakVq1a8cr79u2Ls7MzZ86cSXDYekawWCz06tULiD9se//+/fzzzz8UKVKEJ5980hbhiYhIBrt3DnB3d3datmzJ0aNHee655+IkhLdv387ly5epVatWos+Obdu2BbC2k/fr379/vLJChQrRqVMnwJxrPcbSpUsB6NKlC3nz5o13Xu7cuXn88ccxDMM6/em9/vzzT9555x2efvppmjZtam3Db926xd27d63rj6XUuXPn+PDDD+nSpQuPPvqo9brz588HEv9uULx4cdq1axev/JlnnqFkyZLcuXOHrVu3PlRMaaFv375A/O8G165dY/Xq1ZrmTdKFg60DEJGs48SJE9bGO1euXBQpUoSmTZvy7rvvWnuGx3xhuHPnTqo/b+HChWnSo11ERCQzadGiBS1atCAkJITdu3ezc+dOVq1axYYNGzh37hxPPfUUe/fupVKlSsm+5qRJk3j//feTnL88ICAgwfLKlSsnWO7q6oqXlxcnTpzg+PHjKYonLfXp04dx48bx888/8/HHH+PgYD7CxDw4d+/eHXt7e5vEJiIiGat8+fIUKlQIwzC4fPkyp0+fxtHR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RERyrpkzZwL6biDpT0l0ERERERHJ9L744gsuXbpEs2bNqFKliq3DERERERtbvHgxu3fvpkyZMprmTdKdg60DEBERERERScjly5fp2rUrN27c4ODBg9jZ2fHBBx/YOiwRERGxoebNmxMcHMy+ffsAmDBhQrqu4SICSqKLiIiIiEgmFRoaysaNG3F0dOSRRx5h7Nix+Pr62josERERsaGNGzdib29PmTJlePfdd7WgqGQILSwqIiIiIiIiIiIiIpIIjXUQEREREREREREREUmEpnNJpujoaC5evEjevHmxWCy2DkdERHIwwzAIDg6mWLFimvvvPmqvRUQks1B7nTi11yIiklkkt71WEj2ZLl68iJeXl63DEBERsfr3338pUaKErcPIVNRei4hIZqP2Oj611yIiktk8qL1WEj2Z8ubNC5g31M3NLU2uGRERwdq1a2nVqhWOjo5pcs3MSPXMXlTP7EX1zJqCgoLw8vKytk0SKy3b6+z27ya1dD9i6V7E0r2IS/cjlu6F2uukqL3OXHQPU0f3L/V0D1NH9y91ktteK4meTDFDzNzc3NI0iZ47d27c3Nyy9T9y1TN7UT2zF9Uza9Pw5/jSsr3Orv9uHpbuRyzdi1i6F3HpfsTSvYil9jo+tdeZi+5h6uj+pZ7uYero/qWNB7XXmphNRERERERERERERCQRSqKLiIiIiIiIiIiIiCRCSXQRERERERERERERkUQoiS4iIiIiIiIiIiIikggl0UVEREREREREREREEqEkuoiIiIiIiIiIiIhIIpREFxERERERERERERFJhJLoIiIiIiIiIiIiIiKJUBJdRERERERERERERCQRSqKLiIiIiIiIiIiIiCRCSXQRERERERGRHGrTpk20adOGYsWKYbFYWLZs2QPP2bhxI7Vr18bZ2ZkyZcrwzTffpH+gIiIiNuRg6wDut2nTJj7++GP27NnDpUuXWLp0Ke3bt7fut1gsCZ43efJkBg8enOA+Pz8/+vTpE688JCQEZ2fnNIk7JcLCwvjnn384ffo0O3fu5ObNmzg4ZLpfRZqJjIzk77//zjb1dHR0pFChQtSsWRN3d3dbhyMiIukkPDycU6dOsWTJEiIjI20djs1ltfbc0dGRggULUqNGDfLly2frcEREMq07d+5QvXp1+vTpw7PPPvvA4/39/Xnqqad46aWX+PHHH9m6dSuvv/46BQsWTNb5aS0iIoLTp0+zdOlSIiMjMQwjw2PI6mzZxjs6OpI/f35q1KhB/vz5M/SzRURSItM9AT2oAb906VKc7dWrV9OvX78HNtZubm4cO3YsTpktEugbNmxg0aJFRERE4OnpCcC///6LnV32HRQQHR0NZI96GoZBREQE69evZ968eTRs2JAePXpk+XqJiEhc27Zt46effuLSpUtUqFCBPHny5Pj/67Naex4eHs61a9eYN28edevWpXfv3lki+S8iktFat25N69atk338N998Q8mSJZk6dSoAlStXZvfu3XzyyScZnkTftWsXc+bM4eLFi2qvU8GWbXxMe71gwQJq1qxJv379cHR0zNAYRESSI9M9STyoAS9SpEic7eXLl9OiRQvKlCmT5HUtFku8czPa1q1b+eWXX2jatClPPPEE+fLl499//8XLywt7e3ubxpaeoqKisl0979y5w44dO1i0aBGGYdC7d29bhyQiImlk165dzJ07l3r16lGtWjVq1qyZbdqv1MiK7XlISAi7du1i/vz5REVF8corr9g6JBGRLG/79u20atUqTtkTTzzBzJkziYiIyLAE6P79+5k5cya1atWiV69e1KpVK8u0T5mNrdv40NBQ9uzZwy+//MI333zDgAEDEp2FQETEVjJdEj0lrly5wsqVK5kzZ84Dj719+zbe3t5ERUVRo0YNxo8fT82aNRM9PiwsjLCwMOt2UFAQYA4Vi4iISHGshmHw66+/UqtWLZ577jksFov1bW/Mn9lVdqyns7MzzZs3B2DevHm0bt2avHnzAjzUv4+sJKZ+qmf2oHqmr5CQEEJDQxPd7+zsjIuLS4qvm91/X7a2atUqfHx86NmzJ+fPn7d1OJIKLi4uNG3aFAcHB+bMmcOVK1coXLiwrcMSkcxq9myoXh1q1bJ1JJna5cuX4/1fWrhwYSIjI7l+/TpFixaNd05aP18DrFixgvLly9OrVy8uXbqUrZ43M5qtn9kdHR1p0KABjo6OzJgxg9OnT1OyZEmbxPIwcsozVXrSPUydnHL/AgMDuXXrVqL73d3dH2oax+TetyydRJ8zZw558+alY8eOSR5XqVIl/Pz88PHxISgoiM8//xxfX1/+/vtvypcvn+A5kyZNYuzYsfHK165dS+7cuVMc69WrVzl27BitWrWK90B+4cKFFF8vK8qO9SxRogTBwcF8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bN4M991nalgiIiIObdEiGD7cWIsEoHFj2LQJfHzMjUtERESy5danzuPj44mIiKBIkSK4uLgAd3jq/Pp16NPHuJkO8Oabxvpk9erlR+giIpIJSqLns1svrJGRkdy4cYM9e/Yk3ZHO1+lc9uyBZ55JLrdvD198ASVL5s/ri4iIOKO4OPjss+QEes+esHw5FC5sblwiIiKSbbc+dR4WFsbu3btp3rw53t7eQAZPnQcHG9O3HDlilN3c4OOPlUAXESlglETPZ7deWP/44w/++ecf6tevT5UqVYB8ns6lRQsYNAiWLDFGw82ebVywRUREJO+4uhoLhPn5GQn0adOMtUhERETEbt0+XUuhQoXw9vamZEaD1E6dgs6djTXKwJhadf16aNs2j6MVEZGsUhLdmVkssHChcdHu2dPsaERERJxHmTLwww/GQqIiIiLifHbvhkcfhdBQo1yxorFeitZGEREpkJREz2e3TucSHh5OfHw8hw4d4tdffwXyeDqXgACIioKHHkquK1RICXQREZG8dPw4vPACrFoFpUsn1yuBLiIi4pxWrYL+/ZPXJqtf35gPvXx5U8MSEZH0KYmez26dzuXnn38mODiYWrVqUfvm3eY8m87lgw9g2DDw9IQDB6BWrbx5HREREUn2zTfQqxdcuwY9esCOHeDubnZUIiIiYpbYWGPh0MQE+kMPGWuTFStmblwiIpIhTcDp6OLiYORIeP554+dr12DePLOjEhERcXyLFxsLhV27ZpSjo5N/FhEREefk5maMOi9XDgYPhk2blEAXEbEDGomez26dziX05txnx48f5/z580AuT+cSGgq9e4O/f3Ld//4HM2bkzvFFREQktfh4ePFFeOed5LoePWDFCihc2Ly4REREpGCoXBmOHAFfX2OtMhERKfCURM9nFSpUoGjRogD89NNP/Pvvv1SoUIH//Oc/AHh7e+fOC/32G3TrZvwNxt3u99+HZ57JneOLiIhIahER8PTTsGFDct3YscYNbKseABQREXE6Fy/Cq6/Cu++mvJmu+c9FROyKkuj57O+//04aiX7p0iUAgoKCuHHjBmCMRC9ZsmTOXsTf35h/9epVo1ymDKxbBy1a5Oy4IiIikr7gYOMG9uHDRtnFBRYsMKZUExEREadjPX0annwSzp0zkulr1xqfD0RExO4oiZ7Pbl1YdM+ePVy5coVKlSrRsmVL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WTM+++wzevfuTaFChShXrhzFixfnvffey4+QHZb+Neex20fw2my2DEf1ptU+rfqCaPjw4fz000989913GbarUaMGNWrUSCr7+fnx119/MWvWLFq1apXXYWZL586dk36uW7cufn5+3H333XzyySeMGTMmzX3s+b0EWLJkCZ07d6Z8+fLptrHH9zI9Wf23mt19CoLY2Fj69OlDQkICCxcuzLBt06ZNUyzK2bx5c+6//37ee+895s2bl9ehZkvv3r2Tfq5Tpw6NGjWicuXKbNmyhcceeyzd/ez1/ZS8o98JyUhmP/c4sho1anDs2DGuXr3K2rVr6d+/P4GBgU6XSP/rr78YNWoU27dvN3XNkIIiO5+bRe7Emb5X55Xsfq5ZuXIlkydPZuPGjZQtWzavwivwMtt/8fHxPPXUU0yZMoV77703v8KzC1n5HUxISMBisfDZZ5/h7e0NwOzZs3n88cdZsGABnp6eeR5vQZOV/jt58iQjR47k1VdfpVOnTly4cIFx48YxZMgQlixZkh/hOiQl0fNI6dKlcXFxSXVXKCQkJNXdo0TlypVLs72rqyulSpXKs1hzw4gRI9i0aRO7d+/mrrvuyvL+TZs2ZcWKFXkQWd4oUqQIdevWTXc0rj2/lwB//vknO3bsYN26dVne197ey3LlygHGiBVfX9+k+oz+rSbul5V/3wVFbGwsvXr14ty5c+zatSvDUehpsVqtPPDAAwV6JPrtfH19qVy5coYx2+v7KXkjO9dwcS45/dzjKAoVKsQ999wDQKNGjTh06BBz587l/fffNzmy/HXkyBFCQkJo2LBhUl18fDy7d+9m/vz5REdH4+LiYmKE5rrT52aRjDjb9+q8kJPPNatXr2bQoEF8+eWXTjtVVVb779q1axw+fJij/9/evUfHdO7/A39PMpncmktJEJcIjaDukqo0YiJxcFAhQS84cW9UV2mp0vpKcnq0iqMtB6cLibYILVGttO5JiIi6JC2KkBsJgoMmROT2+f3hN7OMmYkkItf3a61ZS5797Gd/nr0Tn72f2fvZSUl45513ADwcEBYRKJVK7NmzB76+vtUSe21Rmd9BJycntGjRQjuADgAdO3aEiCArKwvt2rV7pjHXJpXZf5999hm8vLzwwQcfAAC6du0Ka2treHt741//+pfO2AeVH6dzeUZUKhXc3d2xd+9enfK9e/filVde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fHy8wW0WFxfL119/LV5eXmJrayvm5ubi7OwsgwYNktWrV8vdu3e1dSuaw4uKimTOnDni7OwsSqVS79jExMSIl5eX2NjYiL29vQwePFhOnDih3VcxMTE6dTX5OC4uTry9vcXKykrs7e3l9ddfl0uXLun1rSK531BdEZHo6Gh56aWXxNLSUntOY4inp6cAkH379hlcTiQiohAx8DwLERERERFRPbZ48WJ8+OGH+Oabb/joNhER1TiFQgG1Wo3Y2NiaDqVBKSgoQIsWLWBvb4+LFy8afbEtEedEJyIiIiKiBqWgoAArV65Eo0aNMGrUqJoOh4iIiGpIeHg4bt26hbfeeosD6FQmzolOREREREQNQnx8POLi4rB7925cunQJixYtgqWlZU2HRURERNVs0aJFuHHjBr7++ms0adIEwcHBNR0S1XIcRCciIiIiogZh3759CAsLg4ODA9577z3MmjWrpkMiIiKiGjBv3jyoVCp069YNy5cvh62tbU2HRLUc50QnIiIiIiIiIiIiIjKCc6ITERERERERERERERnBQXQiIiIiIiIiIiIiIiM4iE5EREREREREREREZAQH0YmIiIiIiIiIiIiIjOAgOhERERERERERERGRERxEJyIiIiIiIiIiIiIygoPoRERERERERERERERGcBCdiIiIiIiIiIiIiMgIDqITERERERERERERERnx/wBEGVOTnsBIPAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--- Model 1 (PCE) ---\n", - "Kernel: MaternKernel\n", - "Matern ν: 2.5\n", - "Noise prior: GammaPrior\n", - "\n", - "--- Model 2 (Stability) ---\n", - "Kernel: MaternKernel\n", - "Matern ν: 0.5\n", - "Noise prior: GammaPrior\n", - "\n", - "--- Model 3 (Repeatability) ---\n", - "Kernel: MaternKernel\n", - "Matern ν: 1.5\n", - "Noise prior: LogNormalPrior\n", - "\n" - ] - } - ], - "source": [ - "# Evaluate posterior predictions on training data\n", - "\n", - "posterior = model.posterior(train_X)\n", - "pred_std = torch.sqrt(posterior.variance).detach().cpu().numpy()\n", - "pred_mean = posterior.mean.detach().cpu().numpy()\n", - "print(pred_mean)\n", - "#pred_mean = torch.cat([p.mean for p in posterior.distributions], dim=-1)\n", - "true_Y = train_Y.detach().cpu().numpy() # already scaled\n", - "\n", - "# Reverse scaling\n", - "pred_mean_unnorm = pred_mean * (Y_max - Y_min) + Y_min\n", - "pred_std_unnorm = pred_std * (Y_max - Y_min)\n", - "true_mean_unnorm = true_Y * (Y_max - Y_min) + Y_min\n", - "\n", - "# # Flip repeatability back\n", - "# pred_mean_unnorm[:, 2] *= -1\n", - "# true_mean_unnorm[:, 2] *= -1\n", - "\n", - "\n", - "labels = ['PCE', 'Stability', 'Repeatability']\n", - "num_obj = train_Y.shape[1]\n", - "\n", - "fig, axes = plt.subplots(1, num_obj, figsize=(5 * num_obj, 5))\n", - "\n", - "for i in range(num_obj):\n", - " ax = axes[i]\n", - "\n", - " # Compute metrics\n", - " r2 = r2_score(true_mean_unnorm[:, i], pred_mean_unnorm[:, i])\n", - " rmse = mean_squared_error(true_mean_unnorm[:, i], pred_mean_unnorm[:, i], squared=False)\n", - "\n", - " # Parity scatter with uncertainty bars\n", - " ax.errorbar(\n", - " true_mean_unnorm[:, i], pred_mean_unnorm[:, i],\n", - " yerr=pred_std_unnorm[:, i],\n", - " fmt='o', color='blue', ecolor='gray', elinewidth=1, capsize=3,\n", - " alpha=0.7\n", - " )\n", - "\n", - " # Parity line\n", - " min_val = min(true_mean_unnorm[:, i].min(), pred_mean_unnorm[:, i].min())\n", - " max_val = max(true_mean_unnorm[:, i].max(), pred_mean_unnorm[:, i].max())\n", - " ax.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2)\n", - "\n", - " # Labels and metrics\n", - " ax.set_xlabel('True ' + labels[i], fontsize=14)\n", - " ax.set_ylabel('Predicted ' + labels[i], fontsize=14)\n", - " ax.set_title(f'{labels[i]}', fontsize=16)\n", - " ax.text(0.05, 0.95, f'R² = {r2:.2f}\\nRMSE = {rmse:.2f}', transform=ax.transAxes,\n", - " fontsize=12, verticalalignment='top', bbox=dict(boxstyle=\"round\", facecolor='white', alpha=0.6))\n", - "\n", - " ax.grid(True)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()\n", - "\n", - "for i, gp in enumerate(model.models):\n", - " print(f\"--- Model {i+1} ({['PCE', 'Stability', 'Repeatability'][i]}) ---\")\n", - " # Kernel type\n", - " kernel = gp.covar_module.base_kernel\n", - " print(\"Kernel:\", type(kernel).__name__)\n", - "\n", - " # Check Matern ν if it's a Matern kernel\n", - " if hasattr(kernel, \"nu\"):\n", - " print(\"Matern ν:\", kernel.nu)\n", - "\n", - " # Noise prior info\n", - " noise_prior = gp.likelihood.noise_covar.raw_noise_constraint.initial_value\n", - " prior_type = type(gp.likelihood.noise_covar.noise_prior).__name__ if gp.likelihood.noise_covar.noise_prior else \"None\"\n", - " print(\"Noise prior:\", prior_type)\n", - " \n", - " # print(\"Lengthscales:\", gp.covar_module.base_kernel.lengthscale.detach().cpu().numpy().flatten())\n", - " # print(\"Outputscale:\", gp.covar_module.outputscale.item())\n", - " # print(\"Noise:\", gp.likelihood.noise.item())\n", - " print()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "42e0c6fe-1314-4ce0-ba54-0386fec624e1", - "metadata": {}, - "outputs": [], - "source": [ - "# --- Fit generic GP model with default settings ---\n", - "model_generic = fit_gp_models(train_X, train_Y)\n", - "\n", - "# --- Posterior prediction ---\n", - "posterior_generic = model_generic.posterior(train_X)\n", - "pred_std_gen = torch.sqrt(posterior_generic.variance).detach().cpu().numpy()\n", - "pred_mean_gen = posterior_generic.mean.detach().cpu().numpy()\n", - "true_Y = train_Y.detach().cpu().numpy()\n", - "\n", - "# --- Reverse scaling ---\n", - "pred_mean_unnorm_gen = pred_mean_gen * (Y_max - Y_min) + Y_min\n", - "pred_std_unnorm_gen = pred_std_gen * (Y_max - Y_min)\n", - "true_mean_unnorm = true_Y * (Y_max - Y_min) + Y_min\n", - "\n", - "# --- Flip repeatability back ---\n", - "# pred_mean_unnorm_gen[:, 2] *= -1\n", - "# true_mean_unnorm[:, 2] *= -1\n", - "\n", - "labels = ['PCE', 'Stability', 'Repeatability']\n", - "num_obj = train_Y.shape[1]\n", - "\n", - "# --- Plot parity plots ---\n", - "fig, axes = plt.subplots(1, num_obj, figsize=(5 * num_obj, 5))\n", - "for i in range(num_obj):\n", - " ax = axes[i]\n", - " r2 = r2_score(true_mean_unnorm[:, i], pred_mean_unnorm_gen[:, i])\n", - " rmse = mean_squared_error(true_mean_unnorm[:, i], pred_mean_unnorm_gen[:, i], squared=False)\n", - "\n", - " ax.errorbar(\n", - " true_mean_unnorm[:, i], pred_mean_unnorm_gen[:, i],\n", - " yerr=pred_std_unnorm_gen[:, i],\n", - " fmt='o', color='blue', ecolor='gray', elinewidth=1, capsize=3, alpha=0.7\n", - " )\n", - "\n", - " min_val = min(true_mean_unnorm[:, i].min(), pred_mean_unnorm_gen[:, i].min())\n", - " max_val = max(true_mean_unnorm[:, i].max(), pred_mean_unnorm_gen[:, i].max())\n", - " ax.plot([min_val, max_val], [min_val, max_val], 'r--', linewidth=2)\n", - "\n", - " ax.set_xlabel('True ' + labels[i], fontsize=14)\n", - " ax.set_ylabel('Predicted ' + labels[i], fontsize=14)\n", - " ax.set_title(f'{labels[i]}', fontsize=16)\n", - " ax.text(0.05, 0.95, f'R² = {r2:.2f}\\nRMSE = {rmse:.2f}',\n", - " transform=ax.transAxes, fontsize=12, verticalalignment='top',\n", - " bbox=dict(boxstyle=\"round\", facecolor='white', alpha=0.6))\n", - " ax.grid(True)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 135, - "id": "a4882379-5d50-42e1-a16d-311a714d6ad1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "tensor([[0.9057, 0.0000, 0.0947],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [0.6506, 0.2377, 0.7895],\n", - " [0.0000, 0.0000, 1.0000],\n", - " [1.0000, 1.0000, 0.0000]], device='cuda:0', dtype=torch.float64)\n", - "Ref point: tensor([-0.1000, -0.1000, -0.1000], device='cuda:0')\n", - "Hypervolume: 0.32563138008117676\n", - "Pareto size: 4\n" - ] - } - ], - "source": [ - "pareto_mask = is_non_dominated(train_Y)\n", - "pareto_Y = train_Y[pareto_mask]\n", - "\n", - "ref_point = torch.tensor([-0.1, -0.1, -0.1], device=device) #\n", - " \n", - "hv = Hypervolume(ref_point=ref_point)\n", - "volume = hv.compute(pareto_Y)\n", - "print(train_Y)\n", - "print(\"Ref point:\", ref_point)\n", - "print(\"Hypervolume:\", volume)\n", - "print(\"Pareto size:\", pareto_Y.shape[0])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7588eaa2-cef0-431f-8bc7-59b0cece3541", - "metadata": {}, - "outputs": [], - "source": [ - "pareto_Y[:, 2] *= -1\n", - "pareto_Y_np = pareto_Y.detach().cpu().numpy()\n", - "\n", - "fig = plt.figure(figsize=(8, 6))\n", - "ax = fig.add_subplot(111, projection='3d')\n", - "\n", - "# Plot Pareto-optimal points\n", - "ax.scatter(pareto_Y_np[:, 0], pareto_Y_np[:, 1], pareto_Y_np[:, 2],\n", - " c='green', s=50, label='Pareto Front', depthshade=True)\n", - "\n", - "# Optionally, plot the reference point\n", - "ax.scatter([ref_point[0].item()], [ref_point[1].item()], [ref_point[2].item()],\n", - " c='red', s=60, marker='*', label='Reference Point')\n", - "\n", - "# Label axes\n", - "ax.set_xlabel('PCE (normalized)', fontsize=12)\n", - "ax.set_ylabel('Stability (normalized)', fontsize=12)\n", - "ax.set_zlabel('Repeatability (normalized, flipped)', fontsize=12)\n", - "ax.set_title('Pareto Front (3D)', fontsize=14)\n", - "\n", - "ax.legend()\n", - "ax.view_init(elev=25, azim=-45)\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 192, - "id": "c4f9b9e3-f525-4d08-90c4-ad369a4151b8", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\eschw\\anaconda3\\envs\\mobo-env\\lib\\site-packages\\botorch\\acquisition\\multi_objective\\monte_carlo.py:321: NumericsWarning: qNoisyExpectedHypervolumeImprovement has known numerical issues that lead to suboptimal optimization performance. It is strongly recommended to simply replace\n", - "\n", - "\t qNoisyExpectedHypervolumeImprovement \t --> \t qLogNoisyExpectedHypervolumeImprovement \n", - "\n", - "instead, which fixes the issues and has the same API. See https://arxiv.org/abs/2310.20708 for details.\n", - " legacy_ei_numerics_warning(legacy_name=type(self).__name__)\n" - ] - } - ], - "source": [ - "acq_func = qNoisyExpectedHypervolumeImprovement(\n", - " model=model,\n", - " ref_point=ref_point,\n", - " X_baseline=train_X,\n", - " sampler=SobolQMCNormalSampler(sample_shape=torch.Size([64])),\n", - " #eta=0.05,\n", - " prune_baseline=True,\n", - " alpha=0.0,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 112, - "id": "36c0de0a-b6e1-44df-a3c4-224cec2423e8", - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'model_generic' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[1;32mIn[112], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m acq_func_gen \u001b[38;5;241m=\u001b[39m qNoisyExpectedHypervolumeImprovement(\n\u001b[1;32m----> 2\u001b[0m model\u001b[38;5;241m=\u001b[39m\u001b[43mmodel_generic\u001b[49m,\n\u001b[0;32m 3\u001b[0m ref_point\u001b[38;5;241m=\u001b[39mref_point,\n\u001b[0;32m 4\u001b[0m X_baseline\u001b[38;5;241m=\u001b[39mtrain_X,\n\u001b[0;32m 5\u001b[0m sampler\u001b[38;5;241m=\u001b[39mSobolQMCNormalSampler(sample_shape\u001b[38;5;241m=\u001b[39mtorch\u001b[38;5;241m.\u001b[39mSize([\u001b[38;5;241m128\u001b[39m])),\n\u001b[0;32m 6\u001b[0m \u001b[38;5;66;03m#eta=0.05,\u001b[39;00m\n\u001b[0;32m 7\u001b[0m prune_baseline\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mFalse\u001b[39;00m,\n\u001b[0;32m 8\u001b[0m alpha\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m0.1\u001b[39m,\n\u001b[0;32m 9\u001b[0m )\n", - "\u001b[1;31mNameError\u001b[0m: name 'model_generic' is not defined" - ] - } - ], - "source": [ - "acq_func_gen = qNoisyExpectedHypervolumeImprovement(\n", - " model=model_generic,\n", - " ref_point=ref_point,\n", - " X_baseline=train_X,\n", - " sampler=SobolQMCNormalSampler(sample_shape=torch.Size([128])),\n", - " #eta=0.05,\n", - " prune_baseline=False,\n", - " alpha=0.0,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 193, - "id": "65b89431-0024-47ad-88af-2ec7bcab4996", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Final candidate batch:\n", - " [[ 0.85 0.64 210. 231. 1.35 0.85 12. 26. ]\n", - " [ 0.97 0.57 216. 182. 1.4 0.8 28. 37. ]\n", - " [ 0.89 0.29 220. 275. 1.4 1.1 19. 49. ]\n", - " [ 0.83 0.33 240. 222. 1.4 1.15 30. 43. ]]\n" - ] - } - ], - "source": [ - "NUM_RESTARTS = 20\n", - "RAW_SAMPLES = 512\n", - "BATCH_SIZE = 4\n", - "MAX_ATTEMPTS = 1\n", - "\n", - "bounds = torch.tensor([[0.0] * 8, [1.0] * 8], device=device)\n", - "\n", - "valid_candidates = None\n", - "valid_candidates_gen=None\n", - "attempts = 0\n", - "\n", - "# while attempts < MAX_ATTEMPTS:\n", - "# attempts += 1\n", - "\n", - " # Optimize acquisition function in normalized space\n", - "candidates, _ = optimize_acqf(\n", - " acq_function=acq_func,\n", - " bounds=bounds,\n", - " q=BATCH_SIZE,\n", - " num_restarts=NUM_RESTARTS,\n", - " raw_samples=RAW_SAMPLES,\n", - " options={\"batch_limit\": 4, \"maxiter\": 200},\n", - " sequential=True,\n", - ")\n", - "\n", - " # Optimize acquisition function in normalized space for generic regressor\n", - " # candidates_gen, _ = optimize_acqf(\n", - " # acq_function=acq_func_gen,\n", - " # bounds=bounds,\n", - " # q=BATCH_SIZE,\n", - " # num_restarts=NUM_RESTARTS,\n", - " # raw_samples=RAW_SAMPLES,\n", - " # options={\"batch_limit\": 5, \"maxiter\": 200},\n", - " # )\n", - " \n", - " # Convert to original space and snap to valid grid\n", - "X_cont = x_denormalizer(unnormalize(candidates, bounds).cpu().numpy(), var_array)\n", - "X_snapped = get_closest_array(X_cont, var_array)\n", - "\n", - " # Convert generic to original space and snap to valid grid\n", - " # X_cont_gen = x_denormalizer(unnormalize(candidates_gen, bounds).cpu().numpy(), var_array)\n", - " # X_snapped_gen = get_closest_array(X_cont_gen, var_array)\n", - "\n", - " # Check Clausius-Clapeyron constraint\n", - " # RH_vals = X_snapped[:, 6] # RH [%]\n", - " # Temp_vals = X_snapped[:, 7] # Temp [C]\n", - "\n", - " # RH_vals_gen = X_snapped_gen[:, 6] # RH [%]\n", - " # Temp_vals_gen = X_snapped_gen[:, 7] # Temp [C]\n", - " \n", - " # cc_mask = check_clausius_clapeyron(RH_vals, Temp_vals)\n", - " # cc_mask_gen = check_clausius_clapeyron(RH_vals_gen, Temp_vals_gen)\n", - "\n", - " # if cc_mask.all(): #i know this isn't great logic, but we never break cc relation; just benchmarking right now\n", - "valid_candidates = X_snapped\n", - " # # valid_candidates_gen = X_snapped_gen\n", - " # print(f\"Found valid candidates after {attempts} attempts.\")\n", - " # break\n", - "\n", - "# if valid_candidates is not None: #and valid_candidates_gen is not None:\n", - "print(\"Final candidate batch:\\n\", valid_candidates)\n", - " # print(\"Final generic candidate batch:\\n\", valid_candidates_gen)\n", - "# else:\n", - "# print(\"No valid batch found after max attempts.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ce701516-4db9-4e9b-9df5-a1f19328d4d6", - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Final candidate batch:\\n\", valid_candidates)\n", - "#print(\"Final generic candidate batch:\\n\", valid_candidates_gen)" - ] - }, - { - "cell_type": "code", - "execution_count": 194, - "id": "70a83751-9300-459e-abee-b7b4d4e7fbb4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Random candidate acquisition values: [0.00288875]\n", - "Acquisition values of optimized suggested candidates: [0.01465591]\n" - ] - } - ], - "source": [ - "# After optimizing\n", - "acq_val = acq_func(torch.tensor(x_normalizer(valid_candidates, var_array), dtype=torch.float64, device=device))\n", - "#acq_val_gen = acq_func_gen(torch.tensor(x_normalizer(valid_candidates_gen, var_array), dtype=torch.float64, device=device))\n", - "\n", - "# Random candidates\n", - "rand_X = torch.rand(BATCH_SIZE, train_X.shape[-1], device=device)\n", - "rand_acq_val = acq_func(rand_X)\n", - "\n", - "print(\"Random candidate acquisition values:\", rand_acq_val.detach().cpu().numpy())\n", - "print(\"Acquisition values of optimized suggested candidates:\", acq_val.detach().cpu().numpy())\n", - "#print(\"Acquisition values of generic suggested candidates:\", acq_val_gen.detach().cpu().numpy())" - ] - }, - { - "cell_type": "code", - "execution_count": 195, - "id": "e5d89875-dc9a-492e-b0bc-68faf9f09295", - "metadata": {}, - "outputs": [], - "source": [ - "X_new_scaled = torch.tensor(x_normalizer(valid_candidates, var_array), dtype=torch.float64, device=device)\n", - "#X_new_scaled_gen = torch.tensor(x_normalizer(valid_candidates_gen, var_array), dtype=torch.float64, device=device)\n", - "\n", - "posterior_new = model.posterior(X_new_scaled)\n", - "mean_new = posterior_new.mean.detach().cpu().numpy()\n", - "std_new = posterior_new.variance.sqrt().detach().cpu().numpy()\n", - "mean_new_unnorm = mean_new * (Y_max - Y_min) + Y_min\n", - "std_new_unnorm = std_new * (Y_max - Y_min)\n", - "# mean_new_unnorm[:, 2] *= -1\n", - "#std_new_unnorm[:, 2] *= -1\n", - "\n", - "# posterior_new_gen = model_generic.posterior(X_new_scaled_gen)\n", - "# mean_new_gen = posterior_new_gen.mean.detach().cpu().numpy()\n", - "# std_new_gen = posterior_new_gen.variance.sqrt().detach().cpu().numpy()\n", - "# mean_new_unnorm_gen = mean_new_gen * (Y_max - Y_min) + Y_min\n", - "# std_new_unnorm_gen = std_new_gen * (Y_max - Y_min)\n", - "# mean_new_unnorm_gen[:, 2] *= -1\n", - "\n", - "columns = ['PCE', 'Stability', 'Repeatability']\n", - "results_df = pd.DataFrame(mean_new_unnorm, columns=[f'{col}_mean' for col in columns])\n", - "for i, col in enumerate(columns):\n", - " results_df[f'{col}_std'] = std_new_unnorm[:, i]\n", - "\n", - "# results_df_gen = pd.DataFrame(mean_new_unnorm_gen, columns=[f'{col}_mean' for col in columns])\n", - "# for i, col in enumerate(columns):\n", - "# results_df_gen[f'{col}_std'] = std_new_unnorm_gen[:, i]\n", - "\n", - "# Add parameter values too\n", - "param_df = pd.DataFrame(valid_candidates, columns=x_labels)\n", - "full_results = pd.concat([param_df, results_df], axis=1)\n", - "#print(full_results.values.tolist())\n", - "\n", - "# param_df_gen = pd.DataFrame(valid_candidates_gen, columns=x_labels)\n", - "# full_results_gen = pd.concat([param_df_gen, results_df_gen], axis=1)" - ] - }, - { - "cell_type": "code", - "execution_count": 196, - "id": "f2b89a41-e17d-4e06-aade-0ac2e34970e7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Speed (Inorg) [m/min]Speed (Org) [m/min]inkFL (Inorg) [uL/min]inkFL (Org) [uL/min]Conc. (Inorg) [M]Conc. (Org) [M]AH [g/m^3]Temp [C]PCE_meanStability_meanRepeatability_meanPCE_stdStability_stdRepeatability_std
00.850.64210.0231.01.350.8512.026.09.9680405.0304190.9568913.4550280.6215200.098370
10.970.57216.0182.01.400.8028.037.013.9541563.9406800.5822913.3298071.2907050.187314
20.890.29220.0275.01.401.1019.049.08.8746083.7705210.9570705.1770921.3447090.241858
30.830.33240.0222.01.401.1530.043.012.6467852.7696170.8904114.1611921.5790610.149726
\n", - "
" - ], - "text/plain": [ - " Speed (Inorg) [m/min] Speed (Org) [m/min] inkFL (Inorg) [uL/min] \\\n", - "0 0.85 0.64 210.0 \n", - "1 0.97 0.57 216.0 \n", - "2 0.89 0.29 220.0 \n", - "3 0.83 0.33 240.0 \n", - "\n", - " inkFL (Org) [uL/min] Conc. (Inorg) [M] Conc. (Org) [M] AH [g/m^3] \\\n", - "0 231.0 1.35 0.85 12.0 \n", - "1 182.0 1.40 0.80 28.0 \n", - "2 275.0 1.40 1.10 19.0 \n", - "3 222.0 1.40 1.15 30.0 \n", - "\n", - " Temp [C] PCE_mean Stability_mean Repeatability_mean PCE_std \\\n", - "0 26.0 9.968040 5.030419 0.956891 3.455028 \n", - "1 37.0 13.954156 3.940680 0.582291 3.329807 \n", - "2 49.0 8.874608 3.770521 0.957070 5.177092 \n", - "3 43.0 12.646785 2.769617 0.890411 4.161192 \n", - "\n", - " Stability_std Repeatability_std \n", - "0 0.621520 0.098370 \n", - "1 1.290705 0.187314 \n", - "2 1.344709 0.241858 \n", - "3 1.579061 0.149726 " - ] - }, - "execution_count": 196, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "full_results" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "id": "b74ce42a-432a-41b1-a910-78216e03269d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[[ 0.54852876 0.88516943 0.04352899]\n", - " [-0.02403182 0.12423929 -0.04972143]\n", - " [-0.11500688 0.12856106 0.06174395]\n", - " [ 0.76973734 0.4876084 -0.06272536]\n", - " [ 0.81066491 0.86717505 -0.42012853]]\n" - ] - } - ], - "source": [ - "# full_results_gen\n", - "print(mean_new)" - ] - }, - { - "cell_type": "code", - "execution_count": 145, - "id": "9b052b77-01da-4c18-acc5-98ed20864a23", - "metadata": {}, - "outputs": [], - "source": [ - "def compute_diversity_score(X: np.ndarray) -> float:\n", - " \"\"\"\n", - " Computes the average pairwise Euclidean distance between rows in X.\n", - " A higher value indicates greater diversity among candidate points.\n", - " \n", - " Args:\n", - " X (np.ndarray): 2D array of shape (n_points, n_features)\n", - "\n", - " Returns:\n", - " float: average pairwise distance\n", - " \"\"\"\n", - " if len(X) < 2:\n", - " return 0.0 # Not enough points to compute diversity\n", - " distances = pdist(X, metric='euclidean') # All pairwise distances\n", - " return distances.mean()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 197, - "id": "868334b5-5df3-4fb4-bfb1-74c52efccc07", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Diversity score of batch: 0.7861\n" - ] - } - ], - "source": [ - "diversity = compute_diversity_score(X_new_scaled.cpu().numpy())\n", - "#diversity_gen = compute_diversity_score(X_new_scaled_gen.cpu().numpy())\n", - "print(f\"Diversity score of batch: {diversity:.4f}\")\n", - "#print(f\"Diversity score of generic batch: {diversity_gen:.4f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 198, - "id": "8d7b4c18-631a-49ea-90e5-86c598b3c6b6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.7712543606758118\n", - "0.5372302532196045\n", - "0.5414943099021912\n", - "0.5301148891448975\n" - ] - }, - { - "data": { - "image/png": "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0GbNmtndrnnuT4cOHS76+ADgKZdffrlt6avdu3drxIgRqlOnjq644gqNHz9eq1evlsViKdOxx4wZo+bNm9uV+fv7a+LEibrlllv0yy+/6Msvv3TaNj8/X4mJiXbX8D59+thiffHFF+3uPnKngQMHqmnTpvrmm2+UmprqsL9o9iEzDIGqLScnR59//rmGDRum/Px89ezZU71795Z0djZyXFycqlWrpvfff99uKapq1arp8ccf10033aTff/9da9ascTh2fn6+7r77bt1yyy22snr16mnlypUKCgrSV199ZXe3zzfffKN3331XkZGRWrt2rd21uU6dOvrXv/6liIgIvf/++/r1119t+2rXrq2xY8eqbt26dv2HhobqtddeU0REhP797387XWbrQkaMGKF27drZlZlMJg0dOlTTp09XTk6ObYa+s/f/2muvqX379raytm3b2pbQWrRokU6cOFHqmMrDZZddpt69eys7O9vp80T4jAAA4OKRRAdKaO7cuerVq5d69eqltm3bKiIiQp9//rkCAgL05ptvqlatWrYvzjVq1Ljo/qKiouxu1zz3p2PHjhd9fADwpMcee0wbN27Uddddp8DAQBmGof379ysxMVG33HKLLr/88mKXXinOvn37NGvWLA0fPlz9+vWzXbuL1lT/+uuvnbbr3r27OnXq5FA+YcIEBQUF6dChQ06XK3AHk8mksWPHSnK8XT81NVXffPONGjZsqH/84x+eCA+AB527Bnjt2rU1aNAg7du3T6NGjbJLCO/YsUOZmZnq1KmTy++SQ4YMkSTb9fJ806ZNcyhr0KCBRowYIensWutF1q5dK0kaOXKkatWq5dAuODhY11xzjQzDsC2HeK6NGzfqgQce0PXXX68+ffrYruV///23cnNzbc8jKq309HQ9++yzGjlypAYMGGA77urVqyW5/oxo3Lixhg4d6lB+ww03qEmTJjp16pS2bdtWppjKw4QJEyQ5fkb8+eef+uSTT1juCwCAi+Tv6QAAX3HgwAHbl/XAwEA1bNhQffr00YMPPmibGV70H4RTp05ddH9r1qwplxntAOCt+vfvr/79++v06dPatWuX/u///k8bNmxQSkqK0tPTdd1112nPnj264oorSnzMhIQEPfHEE8WuX/7XX385LW/durXT8ho1aigiIkIHDhzQTz/9VKp4ytP48eM1e/ZsvfPOO3rhhRfk73/2a1xRwuSOO+5QtWrVPBIbAM9p2bKlGjRoIMMwlJmZqV9++UUBAQHq0qWL6tSpY6v37bffSjr7wMtevXo5PVbRcyqK7q48V0BAgC677DKn7Yqunz/99JNDf2vXrnW5znrRDPRz+ztz5oxGjRqldevWOW1TxNW1vDjLli3TlClTlJeXV+rjtmrVSn5+jnPQTCaTWrVqpfT0dP30008e+2PmzTffrPvuu08ff/yxsrOzVb9+fUnSO++8I6vVqhEjRjjM7gcAACXHTHSghJYuXSrDMGQYhiwWi3799Vf961//sltapXHjxpLO/ucEAFAy1atXV+/evTVjxgxt3LhRW7ZsUY0aNXT69Gm99NJLJT7Oli1b9Nhjj8lkMikhIUHff/+9Tp48qcLCQhmGoccff1ySZLVanbYveiidM0UPL/XUrfqSFBkZqQEDBigrK0uffPKJpLPLCxQ9CI/b9IGq6bHHHtPWrVu1bds2HTx4UFu3blWtWrU0Y8YMrVixwlbv77//lnR2ZvK2bduc/nz//feSpNOnTzv0U69ePadJZMn5NbKov59//tllf7///rtDf88++6zWrVunhg0bavny5Tp06JDy8vJs38N79uwpyfW13JWDBw/qzjvvVF5enh588EHt3btXOTk5ts+IN998s9jjevtnRI0aNTRy5EhZrVatWrXKVs5SLgAAlA+S6EA56tGjhyTp2LFj+uabbzwcDQD4pl69emnq1KmSpK+++qrE7VauXClJeuihh/Too4/qyiuvVI0aNWQymSRJv/32W7Ht//zzT5f7srKyJMnpkgTudP7t+p988omysrIUFRWlNm3aeDI0AF6iZ8+etoTw/fffr5ycHElSzZo1JUm33367LSHt6sfZclpHjx51eZePs2tkUX9vvvnmBfuLi4uztSu6licmJmr06NGKjIyU2Wy27b/QtdyVf//737Jarbrlllv04osvqkOHDqpVq1al/oz49ttvtXfvXpb7AgCgHJBEB8pRo0aNbLfHLly40MPRAIDvKnoA3ZkzZ2xlRYkOV4ruAir6g+b5XK1zW+THH390Wp6bm6v09HRJZx+KWhEu9N6KDB8+XKGhofroo4/0119/KTExURIzDAHYGzZsmLp166a//vpLL7/8siTpyiuvlCR99913ZTqm1WrVwYMHne4run6ee40sa3/FXcuPHj3qdKkZqeI/I/bv3+/0jwhFz/SQPP8Z0aNHD11xxRXavXu3vvvuO9tnBMt9AQBw8UiiA+XsiSeekHR21s2GDRuKrfvHH3/YZtsAQFWRnZ0twzCKrVO0fm7Lli1tZdWrV5fkfJmBc/cfOXLEYV9SUtIFEyTbt29XamqqQ/nbb7+tvLw8RUZGqlWrVsUeo6wu9N6KBAUF6dZbb9WZM2c0f/58/ec//+FhcQCcevTRRyVJr776qk6ePKnevXurfv36+vrrr8v84GZnk0T+/PNPrVmzRpIUHR1tK7/xxhslSStWrNDRo0dL3Edx1/KXXnpJBQUFxbYry2fEvn377B7C6szvv//utM7HH3+sX3/9VTVq1LAtNVPeSvoZIZ19foYkLVmyxPb/DP7QCgDAxSOJDpSza6+9VrGxsSosLNSNN96o+Ph4ZWdn29X5888/9dJLL6ldu3bauXOnhyIFAM9YsWKFOnTooDfffNMhsXL8+HE99dRTtnV8i5IB0v9mp2/evNnpcYvuBHr22WeVlpZmK9+5c6cmTJigoKCgYuPy9/fXuHHjbA+6k6StW7fqqaeekiTNmDGjxLMBS6vovW3fvl35+fnF1i26Xf/pp5/WmTNnNGTIEB4WB8DBkCFD1Lp1ax07dkyLFi1SUFCQZs+eLensQyjXrl3r8AfN7777To888oi2bdvmcDx/f38tXLjQljCXzj6E84477lBeXp6ioqLUv39/276oqCiNHDlSR48e1aBBg7R371674xUUFCglJUW33367LBaLrbzoWv7ggw/q5MmTks7O9l6+fLlefPFFl9fyouvoli1bnP6h9ty7Rc/9g+lPP/2km2++WYGBgU6Pe+77v/fee20PTJWkH374Qffcc48kacqUKRW2nEvRe9u5c6dyc3OLrTtmzBj5+/tr/vz5OnLkCMt9AQBQXgwAxYqMjDQkGUuXLi1Vu9mzZxsBAQGGJMPPz8+4/PLLja5duxqXXXaZ4efnZ0gygoODjZUrV9rapKWlGZIMSUZUVJTRs2dPlz9btmwp53cKAO4xb94827VOktGsWTOja9euRsuWLY3AwEBb+YwZM+zabdmyxbbv8ssvN/r06WP07dvX+OSTTwzDMIy///7baN68uSHJCAwMNNq1a2e0atXKkGRceeWVRmxsrCHJmDVrlt1xZ82aZUgypk2bZkRERBj+/v5Ghw4dbG0lGYMHDzYKCgrs2m3atMmQZPTt29fhPRa1O1/fvn0NScamTZvsyv/++2+jTp06hiQjPDzc6Nmzp9G3b18jISHB6RheddVVtj7+85//XGDEAVRGJfmOumTJEkOS0bBhQ+P06dOGYRjGo48+art+1K1b1+jSpYvRqVMno27durbyouuqYfzv+2lkZKRx9913215HRUUZ1atXNyQZ9erVM3744QeH/k+cOGEMGjTIdtwmTZoYV199tdGuXTtbW0m22AzDMHbt2mWYzWZDkhESEmJ07tzZaNSokSHJGD16tMvr6M8//2z7DImMjDR69+5t9O3b1zY+VqvV6NatmyHJqFatmtG6dWujbdu2hslkMsLDw405c+YYkoyxY8faHXfp0qWGJOOWW24xOnbsaJhMJqNt27ZGu3btDJPJZEgyunTpYpw8edKu3bnj5up3l5aWZlc+duxYp7/TgoICo2XLlrax7t69u9G3b1/j/vvvd/ylG4YxePBg29jOnz/faR0AAFA6zEQHKsiTTz6p/fv36+GHH1aHDh2UnZ2tPXv26NixY+rZs6eeeeYZ/fzzz7rtttuctt+1a5e2bdvm8qc0t8UCgDeZOnWqNm7cqIceekg9evRQQUGBUlNTdfjwYUVGRmrMmDH64osv9MILL9i16927t9555x117dpVhw8f1pYtW7R582ZlZmZKkkJCQrR161aNGTNGISEh2r9/v86cOaPY2Fjt2LHjgjME69evr6+++kpjxozRkSNHlJaWplatWum5557TBx98ID+/ivvaFBISoqSkJMXExMhisWjHjh3avHmz9u3b57R+0a35PCwOQHHuuOMONWrUSJmZmXr77bclSQkJCdq2bZtuu+021ahRQ19//bUOHTqkSy+9VBMmTNDHH3+sgQMHOj3eggUL9Morr6hWrVr67rvvVKNGDd1+++3avXu3Wrdu7VC/Zs2a+vTTT7Vy5Upde+21ys3N1Z49e5Sdna2rrrpKjzzyiL766iu72eWdO3fWli1bNGjQIBUWFmrfvn1q0KCBXn31VdsDM51p0aKFPvroI/Xt21fHjh3T1q1btXnzZtta6P7+/vrss8907733KiwsTD///LOOHz+uiRMnavfu3WrcuHGxY2k2m7V582bbw1r379+vJk2a6NFHH9WmTZtUo0aNC/06yszPz08ff/yxRowYoWrV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gAGsX3kezh3XfOncKbprBgAAAPBGPLAXAFAZ+FwS/ejRo/rtt9+K/UDt3r27kpOT9cADD9jKkpKS1KNHD5dtzGaz0+VhAgICfCLB6CtxutO548H4uMbYOPKlc8ebYwMAAAB4YC8AoDLweBL95MmT+vnnn23baWlpSk1NVd26dVW3bl3FxcXppptuUnh4uA4dOqTHHntM9evX14033mhrM2bMGDVu3FgJCQmSpPvvv199+vTRc889p6FDh2r9+vX6/PPPtXXrVre/PwAAAAAAAACA7/J4En3Xrl3q37+/bbtoSZWxY8dq0aJF+vbbb7V8+XIdP35c4eHh6t+/v1avXm13u1d6err8/Pxs2z169NC7776rJ554Qk8++aRatGih1atX6+qrr3bfGwMAAAAAAAAA+DyPJ9H79esnwzBc7v/ss88ueIyUlBSHshEjRmjEiBEXExoAAAAAAAAAoIrzu3AVAAAAAAAAeJuFCxeqWbNmCgoKUufOnfXFF18UW3/lypVq3769goODFR4ervHjx+vo0aNuihYAfBdJdAAAAAAAAB+zevVqTZ8+XY8//rj27t2r3r17KyYmRunp6U7rb926VWPGjNHEiRP1/fffa82aNdq5c6cmTZrk5sgBwPeQRAcAAACgLVu2aPDgwWrUqJFMJpPWrVt3wTabN29W586dFRQUpObNm+v111+v+EABAJKkl19+WRMnTtSkSZPUunVrzZs3TxEREVq0aJHT+l9++aWaNm2q++67T82aNVOvXr00efJk7dq1y82RA4DvIYkOAAAAQKdOnVL79u01f/78EtVPS0vTddddp969e2vv3r167LHHdN999+n999+v4EgBAGfOnNHu3bsVHR1tVx4dHa3t27c7bdOjRw/9/vvv2rBhgwzD0JEjR/Tee+/p+uuvd9mPxWJRTk6O3Q8AVEUef7AoAAAAAM+LiYlRTExMieu//vrratKkiebNmydJat26tXbt2qUXX3xRN910UwVFCQCQpOzsbBUUFCgsLMyuPCwsTJmZmU7b9OjRQytXrtSoUaOUl5en/Px8DRkyRK+99prLfhISEhQfH1+usQOALyKJDgAAAKDUduzY4TAD8tprr9WSJUtktVoVEBDg0MZischisdi2i2Y0Wq1WWa3Wig24jM6Ny5vj9JSi8WBcHHHuuOZrY+PN8ZlMJrttwzAcyor8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mjRx2mbkyJE6cuSIlixZossuu0xZWVnKz88vt5gAAAAAb1CqJLok3XDDDfr222/18ssvKykpSXv37pUkNWnSRDfffLMeeOABEugAUMnNnz/faflHH31kt52enq758+frlVdecUdYAFAlHThwQIsXL9aPP/6o06dP2+0zmUz673//W6LjvPzyy5o4caImTZokSZo3b54+++wzLVq0SAkJCQ71P/30U23evFm//PKL6tatK0n8PwAAAACVUqmT6KdPn9aOHTvUqFEjPfTQQxoyZIguueSSiogNAOClCgsLPR0CAEDSd999p27duqlx48b6+eefddVVVyk7O1uHDx9WRESEWrRoUaLjnDlzRrt379ajjz5qVx4dHa3t27c7bfPhhx8qKipKzz//vP71r3+pRo0aGjJkiJ5++mmXS8lYLBZZLBbbdk5OjiTJarXKarWWKFZJ8pd7P4eK+nN3v6UZE084N77S/g49hXOneO6M05fGxhfObQBAxSpVEv2PP/5Qnz59lJaWJsMwZDKZNGPGDH3yySfq1q1bRcUIAAAAwInHHntM1157rVavXq3AwEAtWbJEnTp10scff6wJEyZozpw5JTpOdna2CgoKFBYWZlceFhamzMxMp21++eUXbd26VUFBQVq7dq2ys7M1depU/fXXXy7XRU9ISFB8fLxDeVJSkoKDg0sUqyRdV6fEVctVdJ0st/a3YcMGt/ZXWnl5ebbXGzduVFBQkAejKRnOneJ5Ynx8YWxyc3MrIBIAgC8pVRL9iSee0OHDh/XEE0+oW7duOnDggJ555hndfffdtmVdAAAAALjHnj17tHDhQvn5+Un6351C119/vWbMmKGZM2dq8+bNJT6eyWSy2y6aOONMYWGhTCaTVq5cqdq1a0s6uyTMiBEjtGDBAqez0WfOnKnY2Fjbdk5OjiIiIhQdHa2QkJASx3nPyj0lrlse/FWo6DpZSjrWQPnyc1u/82/v5La+yuLUqVO21wMGDFBoaKjngikhzp3iuXN8fGlsiu6aAQBUXaVKoicnJ+uxxx7Tk08+KUmKiYlRixYtNGTIEB05csRh5goAoGpYsWKF5s2bpx9//NFuVlqRgoICD0QFAJXfsWPHVLduXfn5+SkgIEDHjh2z7YuKitLs2bNLdJz69eurWrVqDrPOs7KyXH7HDw8PV+PGjW0JdElq3bq1DMPQ77//rpYtWzq0MZvNMpvNDuUBAQEKCAgoUayS3JpwO79fd/ZdmjHxhHPjK+3v0FM4d4rnifHxhbHxhXMbAFCxSvVJlZmZqT59+tiV9evXT4Zh6MiRI+UaGAC4Q+7xbGUf2mf3czT9J9v+7PSfHPZnH9qn3OPZHozau3z44YcaP368OnbsqNOnT2v8+PG69dZbVaNGDbVs2VJPPfWUp0MEgEqrcePGys4++5l02WWXacuWLbZ933zzjWrWrFmi4wQGBqpz585KTk62K09OTlaPHj2ctunZs6f++OMPnTx50lb2008/yc/PT5deemlp3woAAADgtUo1E72goMDhtsyide/y8/PLLyoAcJN9KR8odf1bLvd/NHey0/IOQyep07C7Kiosn/Lss88qNjZWc+fO1ZIlSzR16lR16tRJmZmZ6t27tyIiIjwdIgAPmZi40y39WC2nba9jV6fKZK7hln4lacm4Lm7ry5levXpp+/btGjZsmG6//XbNmjVLGRkZCgwMVGJiou64444SHys2NlajR49WVFSUunfvrjfeeEPp6emaMmWKpLNLsRw+fFjLly+XJN122216+umnNX78eMXHxys7O1sPPfSQJkyY4PLBogAAAIAvKlUSXZL2798vf///NSu6RX/fvn0OdTt18u41/ADgin7D1aRDH4dyfxWqZ8hRbcup5/T20uDQ+u4Izyfs379f8fHxtjVzi/6o2rBhQz3xxBN64YUXNGHCBE+GCACV1uOPP64//vhDkvTII48oMzNTK1eulMlk0siRI/Xiiy+W+FijRo3S0aNHNXv2bGVkZKht27basGGDIiMjJUkZGRlKT0+31a9Zs6aSk5N17733KioqSvXq1dPIkSNL/DBTAAAAwFeUOok+btw4p+WjR4+2vS56ABFr4ALwdsGh9Z0mxP1VqBZ1MrX/WEOPrZ3pKwoKChQYGCg/Pz/VqFHDbj3dJk2a6JdffvFgdABQubVo0UItWrSQJFWrVk2vvvqqXn311TIfb+rUqZo6darTfYmJiQ5lV1xxhcMSMPA87gIBAAAoX6VKoi9durSi4gAA+KhmzZrZZkG2b99eq1at0pAhQyRJ7733nsLDwz0ZHgBUGadPn9Zff/2lsLAwuztHAQAAAFycUn27Hjt2bEXFAQDwUQMHDtTnn3+uW2+9Vffff79GjRqlnTt3KjAwUPv379ezzz7r6RABoFLbtGmTHnvsMe3ceXb28VdffaVOnTpp2rRpGjhwoIYPH+7hCAEAAADfxhoFAICL8swzz+if//ynJOnmm2/We++9p/bt2+vKK6/UkiVL9NBDD3k4QgCovDZu3Kjo6Gjl5eVpxowZKiwstO2rX7++0yVYAAAAAJQO93kCAC6K2WyW2Wy2bQ8fPpxZjwDgJk899ZSuu+46rV+/Xvn5+Xr++edt+9q3b89yjAAAAEA5IIkOALgozZs319q1a9W+fXuHfd99952GDBnCw0XhIPd4tnKPZ9uV5VstttfZ6T/JFFDdoZ2rhwEDVdXevXu1Zs0aSZLJZLLbd8kllygrK8sTYQEAAACVCkl0AMBFOXTokCwWi9N9eXl5+vXXX90cEXzBvpQPlLr+LZf7P5o72Wl5h6GT1GnYXRUVFuBz/P39ZbVane7LyspSrVq13BwRAAAAUPmQRAcAXLTzZz8W+eWXX0jgwKkr+g1Xkw59HMr9VaieIUe1Laee8p08uoVZ6IC9Ll266F//+peGDh3qsO+9995T9+7dPRAVAAAAULmQRAcAlNqyZcu0bNky2/bdd9+tkJAQuzqnT5/W119/rb59+7o7PPgAV8uy+KtQLepkav+xhk6T6ADsPfroo7r22mt14403asyYMTKZTPq///s/vf3223rvvfe0adMmT4cIAAAA+DyS6ACAUsvNzdWff/4p6ews9OPHjzss6WI2mzVq1CjFx8d7IkQAqBKuueYaLVu2TNOnT9f69eslSdOmTVNoaKgSExPVq1cvD0cIAAAA+D6S6ACAUrv77rt19913S5KaNWum999/3+mDRQEAFe+OO+7QTTfdpG3btikrK0v169dXz549VaNGDU+HBgAAAFQKJNEBABclLS3N9vqvv/7S888/r++++06NGzfW/fffryuvvNKD0QFA1VC9enVdc801ng4DAAAAqJRIogMASm3GjBn697//rfT0dFvZqVOnFBUVpV9//VWGYUiS3n33XX311Vdq1aqVp0IFgEovJydHCxYs0KZNm3T06FHVq1dP/fv31913363Q0FBPhwcAAAD4PJ7YBQAote3bt+uWW26xK5s/f74OHTqk6dOn6/jx49q+fbtq1qypZ5991kNRAkDll5aWpquuukqPP/64Dhw4oMDAQB04cECPP/642rdvr19++cXTIQIAAAA+jyQ6AKDU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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "labels = ['PCE', 'Stability', 'Repeatability']\n", - "num_candidates = mean_new_unnorm.shape[0]\n", - "x = np.arange(num_candidates)\n", - "\n", - "fig, axes = plt.subplots(1, 4, figsize=(15, 5))\n", - "hv_values = []\n", - "# Define reference point and compute Pareto front\n", - "for i in range(num_candidates):\n", - " pareto_mask_cand = is_non_dominated(torch.tensor(mean_new[i:i+1], device=device, dtype=torch.float64))\n", - " pareto_Y_cand = torch.tensor(mean_new[i:i+1], device=device, dtype=torch.float64)[pareto_mask_cand]\n", - " \n", - " # Compute hypervolume\n", - " hv = Hypervolume(ref_point=ref_point)\n", - " hv_value = hv.compute(pareto_Y_cand)\n", - " print(hv_value)\n", - "\n", - "for i in range(3):\n", - " axes[i].bar(x, mean_new_unnorm[:, i], yerr=std_new_unnorm[:, i], capsize=5, alpha=0.7)\n", - " axes[i].set_title(labels[i], fontsize=16)\n", - " axes[i].set_xlabel('Candidate Index', fontsize=12)\n", - " axes[i].set_ylabel(labels[i], fontsize=12)\n", - " axes[i].grid(True)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d8000fd5-5864-427d-abc7-6008ca5c67bf", - "metadata": {}, - "outputs": [], - "source": [ - "labels = ['PCE', 'Stability', 'Repeatability']\n", - "num_candidates_gen = mean_new_unnorm_gen.shape[0]\n", - "x = np.arange(num_candidates_gen)\n", - "\n", - "fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n", - "\n", - "for i in range(3):\n", - " axes[i].bar(x, mean_new_unnorm_gen[:, i], yerr=std_new_unnorm_gen[:, i], capsize=5, alpha=0.7)\n", - " axes[i].set_title(labels[i], fontsize=16)\n", - " axes[i].set_xlabel('Candidate Index', fontsize=12)\n", - " axes[i].set_ylabel(labels[i], fontsize=12)\n", - " axes[i].grid(True)\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3725dc75-85f2-4e53-97bc-db4cdaf3951b", - "metadata": {}, - "outputs": [], - "source": [ - "# Combine old and new predicted means (in normalized space)\n", - "pred_old = torch.tensor(pred_mean, dtype=torch.float64, device=device)\n", - "pred_new = torch.tensor(mean_new, dtype=torch.float64, device=device)\n", - "\n", - "combined = torch.cat([pred_old, pred_new], dim=0)\n", - "\n", - "# Define reference point and compute Pareto front\n", - "pareto_mask_combined = is_non_dominated(combined)\n", - "pareto_Y_combined = combined[pareto_mask_combined]\n", - "\n", - "# Compute hypervolume\n", - "hv = Hypervolume(ref_point=ref_point)\n", - "hv_value = hv.compute(pareto_Y_combined)\n", - "\n", - "print(f\"Updated Pareto front has {pareto_Y_combined.shape[0]} points\")\n", - "print(f\"Hypervolume after batch: {hv_value:.4f}\")\n", - "print(f\"Volume Improvement of {hv_value - volume}\")\n", - "print(\"Max predicted PCE:\", combined[:, 0].max().item())\n", - "print(\"Max predicted Stability:\", combined[:, 1].max().item())\n", - "print(\"Max predicted Repeatability (flipped):\", combined[:, 2].max().item())\n", - "\n", - "\n", - "\n", - "# Plot 3D Pareto front\n", - "pareto_Y_combined[:, 2] *= -1\n", - "pareto_Y_np = pareto_Y_combined.detach().cpu().numpy()\n", - "\n", - "fig = plt.figure(figsize=(8, 6))\n", - "ax = fig.add_subplot(111, projection='3d')\n", - "\n", - "# Plot Pareto-optimal points\n", - "ax.scatter(pareto_Y_np[:, 0], pareto_Y_np[:, 1], pareto_Y_np[:, 2],\n", - " c='green', s=50, label='Pareto Front', depthshade=True)\n", - "\n", - "# Plot the reference point\n", - "ax.scatter([ref_point[0].item()], [ref_point[1].item()], [ref_point[2].item()],\n", - " c='red', s=60, marker='*', label='Reference Point')\n", - "\n", - "# Labels\n", - "ax.set_xlabel('PCE (normalized)', fontsize=12)\n", - "ax.set_ylabel('Stability (normalized)', fontsize=12)\n", - "ax.set_zlabel('Repeatability (normalized, flipped)', fontsize=12)\n", - "ax.set_title('Updated Pareto Front (3D)', fontsize=14)\n", - "\n", - "ax.legend()\n", - "ax.view_init(elev=25, azim=-45)\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8a69a0ee-be14-427d-8d29-26e06111376b", - "metadata": {}, - "outputs": [], - "source": [ - "# Combine old and new predicted means (in normalized space)\n", - "pred_old = torch.tensor(pred_mean, dtype=torch.float64, device=device)\n", - "pred_new_gen = torch.tensor(mean_new_gen, dtype=torch.float64, device=device)\n", - "\n", - "combined_gen = torch.cat([pred_old, pred_new_gen], dim=0)\n", - "\n", - "# Define reference point and compute Pareto front\n", - "pareto_mask_combined_gen = is_non_dominated(combined_gen)\n", - "pareto_Y_combined_gen = combined_gen[pareto_mask_combined_gen]\n", - "\n", - "# Compute hypervolume\n", - "hv = Hypervolume(ref_point=ref_point)\n", - "hv_value_gen = hv.compute(pareto_Y_combined_gen)\n", - "\n", - "print(f\"Updated Pareto front has {pareto_Y_combined_gen.shape[0]} points\")\n", - "print(f\"Hypervolume after batch: {hv_value_gen:.4f}\")\n", - "print(f\"Volume Improvement of {hv_value_gen - volume}\")\n", - "print(\"Max predicted PCE:\", combined_gen[:, 0].max().item())\n", - "print(\"Max predicted Stability:\", combined_gen[:, 1].max().item())\n", - "print(\"Max predicted Repeatability (flipped):\", combined_gen[:, 2].max().item())\n", - "\n", - "\n", - "\n", - "# Plot 3D Pareto front\n", - "pareto_Y_combined_gen[:, 2] *= -1\n", - "pareto_Y_np_gen = pareto_Y_combined_gen.detach().cpu().numpy()\n", - "\n", - "fig = plt.figure(figsize=(8, 6))\n", - "ax = fig.add_subplot(111, projection='3d')\n", - "\n", - "# Plot Pareto-optimal points\n", - "ax.scatter(pareto_Y_np_gen[:, 0], pareto_Y_np_gen[:, 1], pareto_Y_np_gen[:, 2],\n", - " c='green', s=50, label='Pareto Front Generic', depthshade=True)\n", - "\n", - "# Plot the reference point\n", - "ax.scatter([ref_point[0].item()], [ref_point[1].item()], [ref_point[2].item()],\n", - " c='red', s=60, marker='*', label='Reference Point')\n", - "\n", - "# Labels\n", - "ax.set_xlabel('PCE (normalized)', fontsize=12)\n", - "ax.set_ylabel('Stability (normalized)', fontsize=12)\n", - "ax.set_zlabel('Repeatability (normalized, flipped)', fontsize=12)\n", - "ax.set_title('Updated Pareto Front (3D)', fontsize=14)\n", - "\n", - "ax.legend()\n", - "ax.view_init(elev=25, azim=-45)\n", - "plt.tight_layout()\n", - "plt.show()\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9a329791-1e58-4b4d-a6eb-fc8304517bdd", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:mobo-env] *", - "language": "python", - "name": "conda-env-mobo-env-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.16" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 600ef6062811a6b6a74e394e8cb8ae82e6ce8728 Mon Sep 17 00:00:00 2001 From: Colin-Qi Date: Wed, 29 Jul 2026 03:16:17 -0400 Subject: [PATCH 02/39] Fix the GP, add the campaign path, and wire the workbook interface The R0 models were not learning. Two separate degenerate modes were found and fixed, one objective was made learnable by modelling the right quantity, and the three-round campaign path now runs end to end. Model - Retire `default_current` to `legacy_matern_no_prior` and register `dim_scaled_prior` as the default. The old contract is kept byte-identical so archived Step 2B/2C runs stay reproducible; it must now be asked for by name. - BoTorch 0.15.1 already supplies a dimension-scaled LogNormal lengthscale prior; MOBO-Kit was discarding it by passing an explicit covar_module. Restored. Median ARD lengthscale 1121 -> 0.88, flat directions 6/10 -> 0/10. - The lengthscale prior alone opens a second degenerate mode: the outputscale collapses and the model calls the data pure noise (10/15 thickness folds, latent sd 1e-4 against fitted noise 0.93). Both priors are now required, and `_assert_signal_not_collapsed` checks it numerically on every fit, because a config name cannot prevent a degeneracy on refit. - Predictive interval coverage moves toward nominal on all three objectives. Objectives - Thickness trains on nanometres, not on its score. The score is a peaked Gaussian on 650 nm, so the map is 2-to-1 and destroys learnable signal. LOO R2 goes -0.503 (score) to +0.384 (nm with a structured mean). - Physics-informed mean functions, declared per objective in config. Thickness needs log(speed_1)+log(precur_conc); optoelectronic needs a single linear anneal_temp term. Opposite shapes; neither generalises. Optoelectronic's plain GP sat below the null at -0.342. - `ObjectiveSpec.model_link` records that a GP output is log-space. Link decode happens in exactly one place, so the sampling and quadrature paths cannot disagree; a test asserts they match to 4e-3. - Lognormal utility expectations use Gauss-Hermite quadrature. Moment-matching is ~500x less accurate and its bias changes sign across the range, which reorders candidates rather than shifting them. - Reference point declared in utility space. The old raw-scale point gave the optoelectronic axis 4.01x the uniformity axis. Campaign - `campaign.py`: run_r0_lhs / run_r1_ucb(5) / run_r2_qlognehvi(3), orchestrating the existing modules. 23 distinct conditions, three replicate films each. - The canonical config is runnable: resolved objectives, fixed scales, declared mean functions. - Debug/production approval tiers replaced by one validity check (count, uniqueness, on-grid, finite, spacing). Approval is a human decision recorded outside the code. - `assert_scaling_is_campaign_fixed` runs inside `build_objective_transform`, so no transform can exist without it. Data-derived scaling would make hypervolume incomparable between rounds. Workbook - `workbook_io.py` reads the source and writes candidates to a sibling file. openpyxl discards cached formula values on save, and Uniformity score is a formula column, so adding sheets to the source would blank a training column for every non-Excel reader. Verified directly. - Entry columns come from `model_source_columns(config)`, so thickness-in-nm is collected. Round detection fails closed on a partly scored sheet. - UTF-8 forced at every boundary; utf-8-sig for Excel-facing CSVs. Docs: GP_MODEL_DECISION.md records the numbers, both resolution floors, and the decision to keep sample 1. CAMPAIGN_STATUS.md orients collaborators and lists open issues. Not validated for fabrication. See CAMPAIGN_STATUS.md: an unexplained 0.089 discrepancy on optoelectronic, column AA is a stale pasted literal rather than a formula, and no proposed batch has been reviewed by a human. Tests: 517 passed, 2 skipped. Co-Authored-By: Claude Opus 5 --- configs/FA0.9CS0.1PbI3_260407_Config.yaml | 143 ++++- configs/d2d_step2c_debug.yaml | 4 +- docs/CAMPAIGN_STATUS.md | 135 ++++ docs/D2D_STEP2C_ROBUSTNESS_METHOD.md | 2 +- docs/GP_MODEL_DECISION.md | 294 +++++++++ scripts/gp_diagnostic.py | 392 ++++++++++++ scripts/pool_null_shards.py | 88 +++ scripts/thickness_null_chunk.py | 66 ++ scripts/thickness_objective_check.py | 181 ++++++ scripts/thickness_permutation_and_mean.py | 252 ++++++++ scripts/validate_structured_means.py | 149 +++++ src/mobo_kit/campaign.py | 648 ++++++++++++++++++++ src/mobo_kit/d2d_step2c_config.py | 8 +- src/mobo_kit/d2d_step2c_robustness.py | 36 +- src/mobo_kit/main.py | 4 +- src/mobo_kit/model_validation.py | 240 +++++++- src/mobo_kit/objectives.py | 187 +++++- src/mobo_kit/step2c_artifacts.py | 2 +- src/mobo_kit/structured_mean.py | 284 +++++++++ src/mobo_kit/workbook_io.py | 347 +++++++++++ tests/test_campaign.py | 319 ++++++++++ tests/test_d2d_baseline.py | 11 +- tests/test_d2d_step2c_config.py | 2 +- tests/test_d2d_step2c_robustness_helpers.py | 6 +- tests/test_model_validation.py | 195 +++++- tests/test_objectives.py | 317 ++++++++++ tests/test_robustness_plots.py | 12 +- tests/test_step2c_artifacts.py | 2 +- tests/test_structured_mean.py | 150 +++++ tests/test_workbook_io.py | 157 +++++ 30 files changed, 4527 insertions(+), 106 deletions(-) create mode 100644 docs/CAMPAIGN_STATUS.md create mode 100644 docs/GP_MODEL_DECISION.md create mode 100644 scripts/gp_diagnostic.py create mode 100644 scripts/pool_null_shards.py create mode 100644 scripts/thickness_null_chunk.py create mode 100644 scripts/thickness_objective_check.py create mode 100644 scripts/thickness_permutation_and_mean.py create mode 100644 scripts/validate_structured_means.py create mode 100644 src/mobo_kit/campaign.py create mode 100644 src/mobo_kit/structured_mean.py create mode 100644 src/mobo_kit/workbook_io.py create mode 100644 tests/test_campaign.py create mode 100644 tests/test_structured_mean.py create mode 100644 tests/test_workbook_io.py diff --git a/configs/FA0.9CS0.1PbI3_260407_Config.yaml b/configs/FA0.9CS0.1PbI3_260407_Config.yaml index 86d3018..8e7bccc 100644 --- a/configs/FA0.9CS0.1PbI3_260407_Config.yaml +++ b/configs/FA0.9CS0.1PbI3_260407_Config.yaml @@ -1,11 +1,19 @@ -# Canonical D2D input grid for Step 1 infrastructure tests. +# D2D campaign configuration - FA0.9Cs0.1PbI3 slot-die/spin campaign. # -# Objective formulas, control policy, process constraints, and campaign reference -# point are intentionally unresolved. This file must not be used to generate R1/R2 -# candidates until those scientific decisions are approved and recorded. +# Rounds: R0 LHS (15, complete) -> R1 UCB-HVI (5 conditions) -> R2 qLogNEHVI (3). +# Each proposed condition is run in triplicate; the three films share a +# replicate_group and are aggregated to one condition-level observation before +# the next round trains on them. +# +# Objective rationale and the measurements behind it are in +# docs/GP_MODEL_DECISION.md. The short version: thickness trains on raw +# nanometres and the 650 nm Gaussian is applied to the posterior, because the +# score itself is a 2-to-1 folded transform that destroys learnable signal. campaign: name: D2D_FA0.9Cs0.1PbI3 - status: baseline_only + status: active + schema_version: d2d-campaign-v2 + workbook_profile: d2d_summary_v3_scores inputs: - name: speed_1 @@ -59,10 +67,127 @@ inputs: stop: 25 step: 2 +# All three utilities are maximised after transformation. `model_source_column` +# is what the GP TRAINS on; `transform` maps a model output to utility. The two +# differ only for thickness. objectives: - # TBD - experimental-team decision. Empty by design in Step 1. - names: [] + contract_version: d2d-objectives-v2-nm-thickness + # Scales are FIXED for the whole campaign and must never be re-derived from + # observed data. If a scale tracks the data, utility space moves between + # rounds and hypervolume stops being comparable - the progress plot silently + # becomes meaningless. Widen a range deliberately and bump contract_version; + # never let it follow the measurements. + scaling_mode: fixed_affine + specs: + - name: uniformity + model_source_column: "Uniformity score" # workbook Z = L*N*O + transform: affine + goal: maximize + # fixed campaign scale, NOT recomputed per round; hypervolume would + # otherwise be incomparable between rounds + lower_anchor: 0.0 + upper_anchor: 1.0 + # No learnable signal from the 15 R0 points (permutation p = 0.82). + # R1 is exploration-dominated for this objective by design. + signal_status: exploration_only + + - name: optoelectronic + model_source_column: "Optoelectronic score" # workbook AA = LOG10(P*Q) + transform: affine + goal: maximize + # log10(photoconductance x implied Voc); observed R0 span -9.14 to -7.27 + lower_anchor: -10.0 + upper_anchor: -6.0 + signal_status: learnable + # A single linear term on anneal_temp and nothing else. Six forms were + # tested; every addition made it worse, and an Arrhenius 1/T form bought + # nothing over plain linear temperature (+0.226 vs +0.244). anneal_temp is + # also the least search-contaminated choice: it came from a marginal + # correlation already on record (rho = -0.651, p = 0.009), not from the + # search over mean shapes -- and it happens to win anyway. + # Plain GP -0.342 (worse than the -0.148 null) -> structured +0.355. + mean_function: + response: identity + features: + - column: anneal_temp + transform: identity + + - name: thickness + # trains on nanometres, NOT on the workbook thickness score + model_source_column: "Thickness (avg)" # workbook X, nm + transform: gaussian_target + goal: target + target: 650.0 + # the workbook writes EXP(-(((T-650)/250)^2)), which has no factor of 1/2; + # in the exp(-0.5*((T-c)/sigma)^2) convention used here that is 250/sqrt(2) + sigma: 176.7766952966369 + equivalent_workbook_formula: "EXP(-(((X-650)/250)^2))" + signal_status: learnable + # Spin-coating theory gives T ~ omega^-0.5; the measured exponent is -0.38. + # NOTE the opposite pattern to optoelectronic: neither term alone is worth + # much here (+0.159, +0.187), the PAIR carries the signal. Do not assume + # either shape generalises to a new objective. + # Plain GP +0.183 -> structured +0.384. + # response: log makes the model output lognormal, so the utility + # expectation must use Gauss-Hermite quadrature, not the Gaussian closed + # form -- moment-matching there is 506x less accurate and its bias changes + # sign across the range, which reorders candidates. + mean_function: + response: log + features: + - column: speed_1 + transform: log + - column: precur_conc + transform: log + +# Reference point in UTILITY space, after the transforms above. Every objective +# is on a comparable [0, 1]-ish scale there, so no axis silently dominates the +# hypervolume. On the previous raw-scale reference [-0.01, -10.0, -0.01] the +# optoelectronic axis was 4.01x the uniformity axis. +reference_point_utility: [-0.01, -0.01, -0.01] + +rounds: + r1: + method: ucb_hvi + batch_size: 5 + replicates_per_condition: 3 + beta: 4.0 + candidate_pool_size: 32768 + posterior_samples: 256 + # thickness utility is a nonlinear function of the model output, so utility + # moments must come from posterior samples, not from moments of the mean + moment_method: monte_carlo + r2: + method: qlognehvi + batch_size: 3 + replicates_per_condition: 3 + candidate_pool_size: 32768 + mc_samples: 128 + sequential_pending: true + +local_penalization: + distance_metric: normalized_euclidean + dimension_weights: null + radius: 0.25 + min_batch_distance: 0.15 + min_observed_distance: 0.0 + +model: + variant: dim_scaled_prior + # R0 has no replicates. Once R1 triplicates land, pool their within-condition + # variance (5 conditions x 2 dof = 10 dof) and pass it as train_Yvar. Pool + # thickness variance in NANOMETRES, matching what the GP trains on. Applying + # an R1-derived estimate to the R0 rows assumes the measurement process is + # unchanged between rounds; that assumption is recorded here deliberately + # rather than left implicit. + observation_noise: fit_from_marginal_likelihood + +reproducibility: + seed: 73 + record_git_commit: true + record_environment_versions: true + record_resolved_config_hash: true -# No D2D process constraint has been approved. In particular, the legacy -# humidity/temperature Clausius-Clapeyron constraint does not apply here. +# No process constraint applies to this campaign. The legacy humidity/temperature +# Clausius-Clapeyron constraint is for a different design. constraints: [] diff --git a/configs/d2d_step2c_debug.yaml b/configs/d2d_step2c_debug.yaml index cb0a28c..553aec7 100644 --- a/configs/d2d_step2c_debug.yaml +++ b/configs/d2d_step2c_debug.yaml @@ -72,12 +72,12 @@ local_penalty_study: models: variants: - - {name: default_current, type: existing_default} + - {name: dim_scaled_prior, type: existing_default} - name: conservative type: explicit_conservative min_noise: 0.01 min_lengthscale: 0.05 - primary_for_debug: default_current + primary_for_debug: dim_scaled_prior exact_leave_one_out: true report_training_posterior_only_as_diagnostic: true diff --git a/docs/CAMPAIGN_STATUS.md b/docs/CAMPAIGN_STATUS.md new file mode 100644 index 0000000..05dd6dc --- /dev/null +++ b/docs/CAMPAIGN_STATUS.md @@ -0,0 +1,135 @@ +# Campaign status and how to use it + +Snapshot for collaborators. The full loop runs: R0 LHS -> R1 UCB-HVI (5) -> +R2 qLogNEHVI (3), three replicate films per condition, 23 distinct conditions. + +## Running a round + +```python +from mobo_kit.campaign import load_campaign_config, run_r0_lhs, run_r1_ucb, run_r2_qlognehvi + +config = load_campaign_config("configs/FA0.9CS0.1PbI3_260407_Config.yaml") + +r0 = run_r0_lhs(config, n=15) # space-filling, no model +r1 = run_r1_ucb(config, X_phys, Y_model, n=5) # UCB-HVI + local penalisation +r2 = run_r2_qlognehvi(config, X_phys, Y_model, n=3) # qLogNEHVI +``` + +Each returns a `RoundResult` with: + +| field | contents | +|---|---| +| `conditions` | distinct proposed conditions, physical units, columns = input names | +| `replicates` | one row per film, with `candidate_id` / `replicate_group` / `replicate_index` | +| `diagnostics` | method, seed, pool size, objective contract version, validity report | + +`diagnostics["validity"]` carries `min_pairwise_distance` and +`boundary_coords_per_condition`, which are the numbers to plot per round. + +## What `Y_model` must contain + +**Not the three final scores.** Column order comes from +`model_source_columns(config)`, currently: + +``` +("Uniformity score", "Optoelectronic score", "Thickness (avg)") +``` + +Thickness is in **nanometres**, because the GP trains on the raw measurement and +the 650 nm Gaussian is applied to the posterior. See `GP_MODEL_DECISION.md` for +why. Anything that collects data for the next round must collect nm. + +## For the plotting work + +**Contour slice through the GP.** Fit with the same path a round uses, then +evaluate on a 2-D grid with the other eight inputs held fixed: + +```python +from mobo_kit.campaign import _fit_models, build_objective_transform, _normalise +from mobo_kit.design import build_design_from_config + +design = build_design_from_config(config) +model = _fit_models(config, X_phys, _normalise(design, X_phys), Y_model, seed=73) + +model.eval() +with torch.no_grad(): + post = model.posterior(torch.tensor(grid_norm)) # grid_norm in [0,1]^10 + mean, var = post.mean, post.variance +``` + +Two things to respect when turning that into a utility surface: + +* the GP output for thickness is **log(nm)**, not nm. `ObjectiveSpec.model_link` + records this. Use `transform.expected_transform(mean, var)` rather than + transforming the mean yourself; it dispatches per objective and integrates the + lognormal by quadrature where needed. +* inputs are normalised to `[0,1]` against the config grid bounds, not the + observed range. `_normalise(design, X_phys)` is the conversion. + +**Round-comparison plot.** Keep each `RoundResult` and plot `conditions` per +round on shared axes (R0 grey / R1 blue / R2 orange), plus per-round +`min_pairwise_distance` and boundary counts from `diagnostics`. Contour slices +should show **23 distinct conditions**, not 39 films -- replicates share inputs +and would otherwise overplot. + +Hypervolume is comparable across rounds only because objective scales are fixed +in config; `assert_scaling_is_campaign_fixed` enforces that. Do not re-derive +scales from observed data between rounds. + +## Model state + +Validated on the 15 R0 observations, exact leave-one-out, null R2 = -0.148: + +| objective | plain GP | with structured mean | +|---|---:|---:| +| thickness (nm) | +0.183 | **+0.384** | +| optoelectronic | -0.342 | **+0.267 to +0.355** (see open issues) | +| uniformity | no learnable signal (permutation p = 0.82) | n/a | + +Uniformity is exploration-only by measurement, not by choice. The interface must +not imply the model knows more than it does about it. + +## Open issues -- read before trusting a batch + +1. **An unexplained 0.089 discrepancy on optoelectronic.** Two implementations of + the same pipeline on the same 15 rows give LOO R2 +0.355 (two-stage) and + +0.267 (mean module). Ruled out: the mean feature (verified raw `anneal_temp`, + not logged) and sampling noise (no resampling involved). Also ruled out: the + residual-vs-target standardization scale, whose direction contradicts the + observed asymmetry. Untested suspects: MLL optimiser seeding, and the + training-target interaction with the fitted outputscale. Both numbers are far + better than plain (-0.342), so the direction is not in doubt -- but the gap + should be closed before optoelectronic candidates are acted on. + +2. **`Optoelectronic score` (column AA) is a pasted literal, not a formula.** + R2 holds `=LOG(P2*Q2)`; AA holds a frozen copy of its value. Editing P or Q + will not update AA. This is the same failure that produced the original + uniformity discrepancy. The fix is to compute all three objectives in Python + from the literal measurement columns (L/N/O, P/Q, X) and demote the formula + columns to a cross-check that warns on disagreement. Not yet done. Column AB + deserves the same audit. + +3. **openpyxl discards cached formula values on save.** Verified: Z2:Z4 read + `[0.657, 0.587, 0.561]` before a save that only added an empty sheet, and + `[None, None, None]` after. This is why `workbook_io` writes candidates to a + sibling file and never opens the source for writing. Do not "simplify" that + by adding sheets to `Summary Table.xlsx`. + +4. **No batch has been reviewed by a human.** Boundary counts and pairwise + distances look healthy ([3,4,2,2,2], min 0.921), but nobody has inspected the + five proposed conditions in physical units. Fifteen films is a real cost. + One specific thing to look for: whether anything lands near + `speed_1 = 1000`, a region with two contradictory observations in it. + +5. **Not started:** the tkinter launcher, replicate-variance pooling into + `train_Yvar` (Phase 4), and removal of the legacy Step 2C debug ceremony. + +## Reproducing the analysis + +```bash +python scripts/gp_diagnostic.py --variants legacy_matern_no_prior dim_scaled_prior +python scripts/validate_structured_means.py +python scripts/thickness_objective_check.py +``` + +These need the ignored private workbook at `local_inputs/Summary Table.xlsx`. diff --git a/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md b/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md index 5eb5ded..d3fc8b3 100644 --- a/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md +++ b/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md @@ -38,7 +38,7 @@ to `[0, 1]`; it never clips training targets or the Optoelectronic score. Two explicit model variants are evaluated: -- `default_current`, matching the Step 2B model; +- `dim_scaled_prior`, matching the Step 2B model; - `conservative`, with documented observation-noise and ARD-lengthscale floors. Every fit is strict: failed optimization raises rather than silently returning diff --git a/docs/GP_MODEL_DECISION.md b/docs/GP_MODEL_DECISION.md new file mode 100644 index 0000000..f1f655f --- /dev/null +++ b/docs/GP_MODEL_DECISION.md @@ -0,0 +1,294 @@ +# GP model decision record + +Measured 2026-07-28 on the corrected `Summary Table.xlsx` (15 R0 observations, +10 inputs). Reproduce with `python scripts/gp_diagnostic.py`. + +## What was wrong + +`ScaleKernel(MaternKernel(nu=2.5, ard_num_dims=10))` was built with no lengthscale +prior. GPyTorch's default constraint is `Positive()` — lower bound 0, upper bound +infinity — so nothing bounded the fit. At N=15 in 10 dimensions the marginal +likelihood interpolates every observation by making a few directions extremely +wiggly and switching the rest off: + +| Objective | Fitted ARD lengthscales | Directions ≥ 10 | Noise | +|---|---|---|---| +| Uniformity | 0.22 … 2229 | 6/10 | pinned at 1e-3 floor | +| Optoelectronic | 3.0 … 38105 | 9/10 | pinned at 1e-3 floor | +| Thickness | 0.13 … 2150 | 7/10 | pinned at 1e-3 floor | + +This is overfitting, not the prior-mean collapse originally hypothesised: the +posterior mean varied over 104–131% of each observed range across the design +space. Noise at its floor means the model believed the data were noiseless. + +## The fix + +BoTorch 0.15.1's `SingleTaskGP` already applies a dimension-scaled LogNormal +lengthscale prior — `LogNormal(loc = √2 + ln(d)/2, scale = √3)` — whenever +`covar_module` is not supplied. MOBO-Kit was discarding it by passing an explicit +module. `dim_scaled_prior` restores the prior while keeping the `ScaleKernel` +wrapper that the hyperparameter readout and plots depend on. + +| Variant | Median lengthscale | Flat directions | Uniformity LOO R² | +|---|---|---|---| +| `legacy_matern_no_prior` | 1121 | 6/10 | −1.557 | +| `conservative` | 2735 | 6/10 | −1.453 | +| **`dim_scaled_prior`** | **0.88** | **0/10** | **−0.537** | + +## Naming + +`default_current` was retired to `legacy_matern_no_prior` rather than redefined, +so archived Step 2B/2C artifacts stay interpretable. `dim_scaled_prior` is +`PRIMARY_VARIANT`; the retired contract must be requested by name and is kept +only for reproducing old runs. + +## Two resolution floors. Check both before comparing anything. + +**Null: −0.148.** Predicting the leave-one-out mean of the other N−1 gives +LOO R² = 1 − (N/(N−1))² and Spearman exactly −1, independent of the data. A model +below this learned nothing. + +**Resolution: ±0.236.** Parametric bootstrap at N=15, 4000 resamples from the +same underlying truth, gives an LOO R² standard deviation of **0.236** and a +central 95% range of **[−0.231, +0.666]**. The identical relationship produces +anything in that range purely by resampling. + +So **two LOO R² values less than about half a point apart are not a comparison at +this N.** This has been walked into twice in this project — once arguing −0.017 +against −0.145, once arguing +0.355 against +0.244 — both times because the +number moved in the pleasing direction. The floor is written down here so the +next person can check before reaching for a difference. + +Differences that survive the floor: the plain-vs-structured swings below +(0.20 and 0.70). Differences that do not: anything in the second decimal place. + +## The bar for "the model learned something" + +For N observations, predicting the leave-one-out mean of the other N−1 gives + + LOO R² = 1 − (N/(N−1))² (−0.1480 at N=15) + Spearman = −1 exactly + +both independent of the data, because that prediction is a strictly decreasing +function of the held-out value. So a **negative LOOCV Spearman is the signature +of a model that learned nothing**, and the bar to clear is −0.148, not 0. + +## What the data supports + +- **Uniformity** — no learnable signal. Nothing beat the null across ~240 model + configurations, 7 target transforms, or modelling Coverage / (1−Uniformity) / + Phase purity separately. Permutation p = 0.82. Treat as exploration-only. +- **Optoelectronic** — weak but real, via `anneal_temp` (single-input LOO + R² +0.244). `=LOG10(P*Q)` is monotone, so model the score directly. +- **Thickness** — see below. + +## Thickness: model nanometres, not the score + +`Normalized thickness = EXP(-(((T-650)/250)^2))` is a peaked Gaussian on a 650 nm +target. The map T → score is **2-to-1**: films at 400 nm and 900 nm receive +near-identical scores from opposite sides of the peak, and the observed films +straddle the target (4 below, 11 above, 360–1303 nm). A GP trained on the score +must represent a folded bimodal ridge in process space; a GP trained on +nanometres sees a smooth trend. + +Raw thickness is the most predictable quantity in the campaign: +`log T ~ log(speed_1) + log(precur_conc)` gives LOO R² **+0.449**, Spearman +**+0.714**, permutation p **0.0067**, with fitted speed exponent −0.38 against +spin-coating theory's −0.5. + +Predicting the score, exact leave-one-out (`dim_scaled_prior`): + +| Approach | LOO R² | Spearman | +|---|---|---| +| train on the score directly | −0.444 | −0.764 | +| train on nm → E[score], analytic | **−0.147** | **+0.279** | +| train on nm → score(mean) only | −0.492 | +0.161 | +| null | −0.148 | −1.000 | + +Rank correlation flips from actively misleading to usable, which is what drives +candidate selection. R² only reaching the null is the honest outcome: the raw-nm +posterior is wide (median 157 nm against a Gaussian width of 250/√2 ≈ 177 nm), so +expected scores are correctly pulled toward the middle. + +Note the third row. Transforming only the posterior *mean* is worse than the null +— it is biased by Jensen's inequality and blind to variance. Use +`ObjectiveTransform.expected_transform`, which has a closed form for +`Y ~ N(μ, v)`: + + E[exp(-½((Y-c)/s)²)] = √(s²/(s²+v)) · exp(-½(μ-c)²/(s²+v)) + +verified against Monte Carlo to <1e-3. It reduces to the plain transform at v = 0 +and penalises uncertainty at the target: at μ = 650 exactly, expected score is +0.994 / 0.870 / 0.508 for posterior σ of 20 / 100 / 300 nm. + +The workbook's `exp(-((T-650)/250)²)` has no ½, so in this parameterisation +`sigma = 250/√2 ≈ 176.78`. + +## Both priors are required, not just the lengthscale one + +With the lengthscale prior but a bare noise floor, the fit has a *second* +degenerate mode: the outputscale collapses to ~0 and the model declares the data +pure noise. Measured on the thickness score, 10 of 15 leave-one-out folds landed +there — fitted noise 0.93 against a latent predictive sd of 1e-4, giving +z-scores in the thousands. Adding BoTorch's `LogNormal(-4, 1)` noise prior +removes it entirely. + +The collapse is specific to the thickness *score*, the folded 2-to-1 objective — +uniformity, optoelectronic and raw nm are stable either way, and the noise prior +costs them nothing (latent sd 0.1226 vs 0.1236). It matters because acquisition +consumes the *latent* posterior: a predictive interval can look well calibrated +while the latent variance has collapsed, because the large fitted noise hides it. + +Calibration, exact leave-one-out, predictive (noise-inclusive) intervals against +nominal 0.68 / 0.95: + +| Variant | Uniformity | Optoelectronic | Thickness | Mean NLPD | +|---|---|---|---|---| +| `legacy_matern_no_prior` | 0.067 / 0.533 | 0.467 / 0.667 | 0.467 / 0.600 | 1.76 / 2.82 / 1.35 | +| `dim_scaled_prior` | 0.400 / 0.800 | 0.533 / 0.800 | 0.533 / 0.867 | 0.58 / 1.44 / 0.82 | + +Interval coverage must use the predictive sd, not the latent sd. Using the latent +sd understates every interval and makes a calibrated model look overconfident. + +## Structured means: two objectives, opposite shapes + +Physics fixes the features in advance, so this is not selection on the outcome. +Linear coefficients are refit inside every fold. + +| Objective | Mean function | LOO R² | Spearman | +|---|---|---:|---:| +| thickness nm | none | +0.183 | +0.586 | +| thickness nm | `log T ~ log(speed_1) + log(precur_conc)` | **+0.384** | +0.682 | +| optoelectronic | none | **−0.342** | −0.100 | +| optoelectronic | linear `anneal_temp` | **+0.355** | +0.618 | + +Null −0.148. Both swings (0.20 and 0.70) clear the ±0.236 resolution floor. + +**The optoelectronic result is the larger finding.** Its plain GP sat *below the +null* — actively worse than predicting the mean — so the seven irrelevant inputs +were not merely diluting the fit, they were doing damage. Removing the +temperature trend first fixes it. + +The two shapes are **opposite** and neither generalises. Thickness needs a pair +of log terms and neither alone is worth much (+0.159, +0.187). Optoelectronic +needs exactly one linear term: every addition tested made it worse, and an +Arrhenius `1/T` form bought nothing over plain linear temperature (+0.226 vs ++0.244). `anneal_temp` is also the least search-contaminated choice available — +it came from a marginal correlation already on record (ρ = −0.651, p = 0.009), +not from the six-form search that was run afterwards. + +**Not claimed:** that linear-mean-plus-GP beats linear-mean-alone. The observed +gap (+0.355 vs +0.244) is 0.47 sd of the ±0.236 resolution floor — indistinguishable +from sampling noise. It is an open hypothesis with a specific test attached: does +linear-mean-plus-GP beat linear-mean-alone under the permutation null? That +question rides along with the optoelectronic permutation run. + +## Structured mean for thickness + +Physics fixes the two predictors in advance, so this is not selection on the +outcome. Refitting the linear coefficients inside every fold: + +| Raw-nm model | LOO R² | Spearman | +|---|---|---| +| plain GP, 10 inputs | +0.183 | +0.586 | +| **GP + linear mean on log(speed_1), log(precur_conc)** | **+0.384** | **+0.682** | +| 2-input log-log reference | +0.449 | +0.714 | + +Carried through to the score: R² −0.145 → **−0.019**, Spearman +0.243 → **+0.461**. +The structured mean legitimately reaches what the legacy model reached by +accident of overconfidence. + +**Not yet wired into `campaign.py`.** R1 candidates generated before this lands +use the plain GP. + +The linear coefficients are refit **inside every fold**, on the 14 training rows +only (`scripts/thickness_permutation_and_mean.py`, in the fold loop). The held-out +value never touches them. The two predictors are fixed from physics before any +fitting, so this is not selection on the outcome. + +## Significance of the rank improvement + +Permutation test, 200 shuffles, permuting the nm measurements and redoing the +full leave-one-out fit plus transform. When the structured mean is used, its +linear coefficients are refit inside every null fold too, so the null is not +flattered. + +| Pipeline | Shuffles | Observed ρ | Null mean | Null sd | p | 95% CI | +|---|---:|---:|---:|---:|---:|---| +| plain GP | 200 | +0.243 | −0.241 | 0.349 | 0.12 | — | +| structured mean | 200 | +0.461 | −0.167 | 0.295 | 0.020 | [0.006, 0.050] | +| **structured mean** | **1800** | **+0.461** | −0.131 | 0.305 | **0.0350** | **[0.0270, 0.0446]** | + +The 1800-shuffle run is reported **standalone**, not pooled with the earlier 200. +Pooling would be defensible but carries an optional-stopping flavour, since the +larger run was commissioned because the first result was borderline. Reporting +the fresh run alone sidesteps the question at no cost. + +**The result holds and the interval clears.** 63 exceedances in 1800, upper bound +0.0446, below 0.05. Note the point estimate moved 0.020 → 0.035: the 200-shuffle +figure was optimistic, which is exactly why the re-run was worth doing. Its +interval did contain the final value. + +The plain GP does not clear p < 0.05. **The structured mean does.** That earns +"thickness is genuinely predictive" rather than "directionally right". On R² the +structured mean reaches p = 0.144 (95% CI [0.129, 0.162]), not significant — but +rank drives candidate selection, and rank is significant. + +The null mean is −0.17, not 0: the leave-one-out shrinkage artifact drags it +negative, which is why a positive observed value carries information. + +## Sample 1 stays in. Do not exclude it. (decided, closed) + +**This section exists because "excluding the control nearly doubles R²" is a true +sentence that will get rediscovered and acted on. It is the wrong action.** + +Dropping sample 1 does improve the thickness fit — raw-nm LOO R² goes +0.384 → ++0.632, Spearman +0.682 → +0.824, and the score prediction reaches +0.207 against +a −0.160 null. Sample 1 is also the off-grid literature control, so there is a +ready-made provenance story for excluding it. + +That story is wrong. Ranking every point by how much dropping it improves the fit: + +| Dropped sample | Leverage | LOO R² after drop | Δ | +|---|---:|---:|---:| +| **12** | **0.462** | **+0.879** | **+0.429** | +| 1 | 0.297 | +0.676 | +0.226 | +| 13 | 0.199 | +0.465 | +0.015 | +| … | | | | +| 7 | 0.316 | +0.261 | −0.188 | + +Sample 1 is not the drag. **Sample 12 is, by nearly double**, and it has the +highest leverage in the design. + +The mechanism is visible in the inputs. Samples 1 and 12 are the *only* two +points at `speed_1 = 1000`, the minimum, so between them they anchor the entire +low-speed end of the strongest predictor — and they contradict each other: +sample 1 has the higher concentration (1.4 vs 1.1) but the *thinner* film +(687 vs 1155 nm), inverting the expected relationship. + +So the gain from dropping sample 1 is a **high-leverage-endpoint artifact**, not +a signal about its provenance. Acting on it would commit you to also dropping +sample 12 — an ordinary LHS point with no provenance justification at all. The +declared provenance difference is real; it is simply not what the residual was +reporting. + +A tempting explanation for the inversion — sample 1 runs `speed_2 = 5000` against +sample 12's 500, so a fast second stage could be thinning the film — **does not +survive testing**: adding `log(speed_2 + 1)` to the structured mean drops LOO R² +from +0.449 to −0.827. Do not add it. + +The low-speed corner therefore remains genuinely unexplained, with two +contradictory observations in it. That makes it something **R1 should probe**, +not something to model around. When the real R1 batch is generated: if the +acquisition function proposes nothing near `speed_1 = 1000`, notice it. It may +mean the model has concluded the region is bad when what it actually has is two +points that disagree. + +## Open + +- Wire the structured mean into the campaign path before generating R1 + candidates for fabrication. +- Hypervolume reference: fixed. `configs/FA0.9CS0.1PbI3_260407_Config.yaml` now + declares `reference_point_utility` in utility space after the transforms, so + no axis dominates. The old raw-scale `[-0.01, -10.0, -0.01]` gave the + optoelectronic axis 4.01x the uniformity axis. diff --git a/scripts/gp_diagnostic.py b/scripts/gp_diagnostic.py new file mode 100644 index 0000000..5a98057 --- /dev/null +++ b/scripts/gp_diagnostic.py @@ -0,0 +1,392 @@ +"""Phase 1.1 baseline GP diagnostic. + +Answers one question: is the GP learning anything from the 15 R0 observations? + +For each objective and each model variant it reports + * the fitted ARD lengthscales, outputscale and noise; + * exact leave-one-out MAE / RMSE / R2 / Spearman / coverage; + * the spread of the posterior mean over a Sobol sample of the design space, + as a fraction of the observed range of that objective. + +A GP that has learned nothing shows large lengthscales, Spearman near zero and +near-zero posterior-mean spread: the posterior has collapsed to its prior mean. + +Reads the workbook read-only. Writes nothing except an optional CSV. + +Usage: + python scripts/gp_diagnostic.py --workbook "local_inputs/Summary Table.xlsx" + python scripts/gp_diagnostic.py --variants current dim_scaled_prior --csv out.csv +""" + +from __future__ import annotations + +import argparse +import warnings +from dataclasses import dataclass +from pathlib import Path + +import gpytorch +import numpy as np +import torch +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.models.transforms.outcome import Standardize +from botorch.models.utils.gpytorch_modules import ( + get_covar_module_with_dim_scaled_prior, + get_gaussian_likelihood_with_lognormal_prior, +) +from gpytorch.constraints import GreaterThan +from gpytorch.kernels import MaternKernel, ScaleKernel +from gpytorch.likelihoods import GaussianLikelihood +from gpytorch.mlls import ExactMarginalLogLikelihood +from openpyxl import load_workbook +from scipy.stats import qmc + +from mobo_kit.model_validation import compute_prediction_metrics + +# Canonical grid, docs/D2D_CAMPAIGN_SPEC.md. (name, start, stop) +DESIGN = ( + ("speed_1", 1000.0, 6000.0), + ("time_1", 5.0, 50.0), + ("speed_2", 0.0, 5000.0), + ("time_2", 10.0, 60.0), + ("precur_conc", 1.0, 2.0), + ("precur_vol", 40.0, 200.0), + ("anneal_temp", 100.0, 185.0), + ("anneal_time", 10.0, 60.0), + ("anti_vol", 100.0, 200.0), + ("anti_time", 9.0, 25.0), +) +INPUT_NAMES = tuple(d[0] for d in DESIGN) +OBJECTIVE_NAMES = ("Uniformity", "Optoelectronic", "Thickness") + +# 0-based workbook column offsets: inputs B:K, objectives Z/AA/AB. +INPUT_COLS = tuple(range(1, 11)) +OBJECTIVE_COLS = (25, 26, 27) + +DTYPE = torch.double + + +# --------------------------------------------------------------------------- # +# model variants +# --------------------------------------------------------------------------- # + + +def _build_current(X: torch.Tensor, y: torch.Tensor) -> SingleTaskGP: + """The retired contract, now model_validation.LEGACY_NO_PRIOR. + + ScaleKernel(Matern 2.5 ARD) with no lengthscale prior. Kept here so the + before/after comparison stays runnable from one script. + """ + covar_module = ScaleKernel(MaternKernel(nu=2.5, ard_num_dims=X.shape[1])) + likelihood = GaussianLikelihood(noise_constraint=GreaterThan(1e-3)) + return SingleTaskGP( + X, + y, + covar_module=covar_module, + likelihood=likelihood, + outcome_transform=Standardize(m=1), + ) + + +def _build_conservative(X: torch.Tensor, y: torch.Tensor) -> SingleTaskGP: + """The repo's 'conservative' variant: noise floor 1e-2, lengthscale floor 0.05.""" + covar_module = ScaleKernel( + MaternKernel( + nu=2.5, + ard_num_dims=X.shape[1], + lengthscale_constraint=GreaterThan(0.05), + ) + ) + likelihood = GaussianLikelihood(noise_constraint=GreaterThan(0.01)) + return SingleTaskGP( + X, + y, + covar_module=covar_module, + likelihood=likelihood, + outcome_transform=Standardize(m=1), + ) + + +def _build_dim_scaled_prior(X: torch.Tensor, y: torch.Tensor) -> SingleTaskGP: + """Keep Matern 2.5 ARD, add BoTorch's dimension-scaled LogNormal lengthscale prior. + + The prior is LogNormal(loc=sqrt(2) + log(d)/2, scale=sqrt(3)), which at d=10 + concentrates lengthscales around exp(loc) ~ 12.9 in raw units but with enough + mass at moderate values to stop the unbounded drift the prior-free fit shows. + """ + base = get_covar_module_with_dim_scaled_prior( + ard_num_dims=X.shape[1], use_rbf_kernel=False + ) + covar_module = ScaleKernel(base) + likelihood = get_gaussian_likelihood_with_lognormal_prior() + return SingleTaskGP( + X, + y, + covar_module=covar_module, + likelihood=likelihood, + outcome_transform=Standardize(m=1), + ) + + +def _build_lengthscale_prior_only(X: torch.Tensor, y: torch.Tensor) -> SingleTaskGP: + """Lengthscale prior but a bare noise floor: the degenerate configuration. + + Kept so the outputscale-collapse mode stays reproducible from this script. + """ + base = get_covar_module_with_dim_scaled_prior( + ard_num_dims=X.shape[1], use_rbf_kernel=False + ) + return SingleTaskGP( + X, + y, + covar_module=ScaleKernel(base), + likelihood=GaussianLikelihood(noise_constraint=GreaterThan(1e-3)), + outcome_transform=Standardize(m=1), + ) + + +def _build_botorch_default(X: torch.Tensor, y: torch.Tensor) -> SingleTaskGP: + """Pure BoTorch 0.15.1 default: RBF ARD + dim-scaled LogNormal lengthscale prior, + LogNormal(-4, 1) noise prior, no ScaleKernel. Nothing overridden.""" + return SingleTaskGP(X, y, outcome_transform=Standardize(m=1)) + + +BUILDERS = { + "current": _build_current, + "conservative": _build_conservative, + "dim_scaled_prior": _build_dim_scaled_prior, + "lengthscale_prior_only": _build_lengthscale_prior_only, + "botorch_default": _build_botorch_default, +} + + +def _fit(model: SingleTaskGP) -> SingleTaskGP: + mll = ExactMarginalLogLikelihood(model.likelihood, model) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fit_gpytorch_mll(mll) + return model + + +# --------------------------------------------------------------------------- # +# hyperparameter readout +# --------------------------------------------------------------------------- # + + +@dataclass +class Hypers: + lengthscales: np.ndarray + outputscale: float | None + noise: float + + +def _read_hypers(model: SingleTaskGP) -> Hypers: + covar = model.covar_module + if isinstance(covar, ScaleKernel): + ls = covar.base_kernel.lengthscale + outputscale = float(covar.outputscale.detach().reshape(-1)[0]) + else: # BoTorch default returns a bare kernel + ls = covar.lengthscale + outputscale = None + noise = float(model.likelihood.noise.detach().reshape(-1)[0]) + return Hypers( + lengthscales=ls.detach().cpu().numpy().reshape(-1).copy(), + outputscale=outputscale, + noise=noise, + ) + + +def _posterior( + model: SingleTaskGP, X: torch.Tensor, *, observation_noise: bool = False +) -> tuple[np.ndarray, np.ndarray]: + """Posterior mean and sd. + + Interval coverage and NLPD must use the *predictive* sd, which includes the + observation noise; the latent sd alone understates the interval and makes a + well-calibrated model look overconfident. Posterior-mean spread uses the + latent sd, since noise is constant across the design space. + """ + model.eval() + with torch.no_grad(), gpytorch.settings.fast_pred_var(): + post = model.posterior(X, observation_noise=observation_noise) + mean = post.mean.detach().cpu().numpy().reshape(-1) + std = post.variance.clamp_min(1e-12).sqrt().detach().cpu().numpy().reshape(-1) + return mean, std + + +# --------------------------------------------------------------------------- # +# diagnostics +# --------------------------------------------------------------------------- # + + +def loocv( + X: np.ndarray, y: np.ndarray, builder, seed: int +) -> tuple[np.ndarray, np.ndarray]: + """Exact leave-one-out: N fits on N-1 rows, each predicting the held-out row.""" + n = X.shape[0] + mean = np.empty(n) + std = np.empty(n) + for i in range(n): + keep = np.ones(n, dtype=bool) + keep[i] = False + torch.manual_seed(seed) + Xt = torch.tensor(X[keep], dtype=DTYPE) + yt = torch.tensor(y[keep], dtype=DTYPE).unsqueeze(-1) + model = _fit(builder(Xt, yt)) + m, s = _posterior( + model, torch.tensor(X[i : i + 1], dtype=DTYPE), observation_noise=True + ) + mean[i], std[i] = m[0], s[0] + return mean, std + + +def posterior_spread( + model: SingleTaskGP, d: int, n: int, seed: int +) -> tuple[float, float]: + """(max-min, std) of the posterior mean over a Sobol sample of the unit cube.""" + sob = qmc.Sobol(d=d, scramble=True, seed=seed) + P = sob.random(n) + mean, _ = _posterior(model, torch.tensor(P, dtype=DTYPE)) + return float(mean.max() - mean.min()), float(mean.std()) + + +# --------------------------------------------------------------------------- # + + +def load_data(path: Path) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + ws = load_workbook(path, data_only=True)["Sheet1"] + rows = [] + for r in ws.iter_rows(min_row=2, values_only=True): + if r[0] is None: + break + rows.append(r) + ids = np.array([int(r[0]) for r in rows]) + X_phys = np.array([[float(r[j]) for j in INPUT_COLS] for r in rows]) + Y = np.array([[float(r[j]) for j in OBJECTIVE_COLS] for r in rows]) + lo = np.array([d[1] for d in DESIGN]) + hi = np.array([d[2] for d in DESIGN]) + X = (X_phys - lo) / (hi - lo) + if X.min() < -1e-9 or X.max() > 1 + 1e-9: + raise ValueError("Inputs fall outside the declared design bounds.") + return ids, X, Y + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--workbook", default="local_inputs/Summary Table.xlsx") + ap.add_argument( + "--variants", nargs="+", default=["current"], choices=sorted(BUILDERS) + ) + ap.add_argument("--sobol-n", type=int, default=1024) + ap.add_argument("--seed", type=int, default=73) + ap.add_argument("--csv", default=None, help="optional path for a tidy metrics CSV") + args = ap.parse_args() + + ids, X, Y = load_data(Path(args.workbook)) + n, d = X.shape + print(f"workbook : {args.workbook}") + print(f"data : {n} observations, {d} inputs, {Y.shape[1]} objectives") + print(f"samples : {ids.tolist()}") + + records = [] + for variant in args.variants: + builder = BUILDERS[variant] + print(f"\n{'='*78}\nVARIANT: {variant}\n{'='*78}") + + for k, obj_name in enumerate(OBJECTIVE_NAMES): + y = Y[:, k] + obs_range = float(y.max() - y.min()) + + torch.manual_seed(args.seed) + full = _fit( + builder( + torch.tensor(X, dtype=DTYPE), + torch.tensor(y, dtype=DTYPE).unsqueeze(-1), + ) + ) + hp = _read_hypers(full) + span, sd = posterior_spread(full, d, args.sobol_n, args.seed) + lo_mean, lo_std = loocv(X, y, builder, args.seed) + met = compute_prediction_metrics( + y, + lo_mean, + lo_std, + variant_name=variant, + objective_index=k, + objective_name=obj_name, + ) + + print(f"\n--- {obj_name} (observed range {obs_range:.4f}) ---") + print(" ARD lengthscales (normalised input space):") + for nm, v in zip(INPUT_NAMES, hp.lengthscales): + flag = " <-- flat" if v >= 10.0 else "" + print(f" {nm:>13}: {v:>12.4f}{flag}") + n_flat = int(np.sum(hp.lengthscales >= 10.0)) + print( + f" {'median':>13}: {np.median(hp.lengthscales):>12.4f}" + f" ({n_flat}/{d} at or above 10)" + ) + os_txt = "n/a" if hp.outputscale is None else f"{hp.outputscale:.4f}" + print(f" outputscale : {os_txt} noise : {hp.noise:.6f}") + print( + f" LOOCV : MAE {met.mae:.4f} RMSE {met.rmse:.4f} " + f"R2 {met.r_squared:+.4f} Spearman {met.spearman_rank_correlation:+.4f}" + ) + print( + f" coverage : 68% {met.coverage_68_percent:.3f} " + f"95% {met.coverage_95_percent:.3f} NLPD {met.mean_gaussian_nlpd:.3f}" + ) + print( + f" posterior mean spread over {args.sobol_n} Sobol pts: " + f"range {span:.5f} ({100*span/obs_range:.2f}% of observed range), " + f"sd {sd:.5f}" + ) + + records.append( + { + "variant": variant, + "objective": obj_name, + "median_lengthscale": float(np.median(hp.lengthscales)), + "n_lengthscale_ge_10": n_flat, + "outputscale": hp.outputscale, + "noise": hp.noise, + "loocv_mae": met.mae, + "loocv_rmse": met.rmse, + "loocv_r2": met.r_squared, + "loocv_spearman": met.spearman_rank_correlation, + "coverage_68": met.coverage_68_percent, + "coverage_95": met.coverage_95_percent, + "nlpd": met.mean_gaussian_nlpd, + "post_mean_range": span, + "post_mean_range_frac_of_observed": span / obs_range, + **{ + f"ls_{nm}": float(v) + for nm, v in zip(INPUT_NAMES, hp.lengthscales) + }, + } + ) + + if len(args.variants) > 1: + print(f"\n{'='*78}\nSUMMARY\n{'='*78}") + print( + f"{'variant':>18} {'objective':>15} {'med LS':>9} {'flat':>5} " + f"{'R2':>8} {'Spearman':>9} {'spread%':>9}" + ) + for r in records: + print( + f"{r['variant']:>18} {r['objective']:>15} " + f"{r['median_lengthscale']:>9.3f} {r['n_lengthscale_ge_10']:>5d} " + f"{r['loocv_r2']:>+8.3f} {r['loocv_spearman']:>+9.3f} " + f"{100*r['post_mean_range_frac_of_observed']:>9.2f}" + ) + + if args.csv: + import pandas as pd + + pd.DataFrame(records).to_csv(args.csv, index=False) + print(f"\nwrote {args.csv}") + + +if __name__ == "__main__": + main() diff --git a/scripts/pool_null_shards.py b/scripts/pool_null_shards.py new file mode 100644 index 0000000..1fa2a04 --- /dev/null +++ b/scripts/pool_null_shards.py @@ -0,0 +1,88 @@ +"""Pool permutation-null shards into one p-value with a binomial interval. + + python scripts/pool_null_shards.py --observed-rho 0.4607 --observed-r2 -0.0191 + +The binomial interval matters: at 200 shuffles a p of 0.020 is about 4 +exceedances, whose 95% interval reaches 0.05. More shuffles shrink that, and the +interval is what says whether the p-value is safe to quote. +""" + +from __future__ import annotations + +import argparse +import glob +import json +from pathlib import Path + +import numpy as np +from scipy.stats import beta + + +def clopper_pearson(successes: int, trials: int, alpha: float = 0.05): + lower = ( + 0.0 + if successes == 0 + else beta.ppf(alpha / 2, successes, trials - successes + 1) + ) + upper = ( + 1.0 + if successes == trials + else beta.ppf(1 - alpha / 2, successes + 1, trials - successes) + ) + return float(lower), float(upper) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--shards", default="local_outputs/null_shards/shard_*.json") + ap.add_argument("--observed-rho", type=float, required=True) + ap.add_argument("--observed-r2", type=float, default=None) + args = ap.parse_args() + + paths = sorted(glob.glob(args.shards)) + if not paths: + raise SystemExit(f"no shards matched {args.shards!r}") + + rho: list[float] = [] + r2: list[float] = [] + for path in paths: + payload = json.loads(Path(path).read_text(encoding="utf-8")) + rho.extend(payload["rho"]) + r2.extend(payload["r2"]) + print( + f" {Path(path).name}: n={len(payload['rho'])} " + f"mean_rho={np.mean(payload['rho']):+.4f}" + ) + + rho_a = np.array(rho) + n = len(rho_a) + print(f"\npooled shuffles: {n} from {len(paths)} shard(s)") + + for label, null, observed in ( + ("Spearman", rho_a, args.observed_rho), + ("R2", np.array(r2), args.observed_r2), + ): + if observed is None: + continue + exceed = int((null >= observed).sum()) + p = exceed / len(null) + lo, hi = clopper_pearson(exceed, len(null)) + verdict = ( + "CLEARS p<0.05" + if hi < 0.05 + else ( + "significant but interval touches 0.05" + if p < 0.05 + else "not significant" + ) + ) + print(f"\n {label}") + print(f" observed {observed:+.4f}") + print(f" null mean/sd {null.mean():+.4f} / {null.std():.4f}") + print(f" 95th percentile {np.percentile(null, 95):+.4f}") + print(f" exceedances {exceed}/{len(null)}") + print(f" p {p:.4f} 95% CI [{lo:.4f}, {hi:.4f}] {verdict}") + + +if __name__ == "__main__": + main() diff --git a/scripts/thickness_null_chunk.py b/scripts/thickness_null_chunk.py new file mode 100644 index 0000000..dd4ead0 --- /dev/null +++ b/scripts/thickness_null_chunk.py @@ -0,0 +1,66 @@ +"""One shard of the thickness permutation null. + +Emits JSON so shards can be pooled into a single null distribution: + + python scripts/thickness_null_chunk.py --n 250 --seed 0 --out shard0.json + +Each shard permutes the nm measurements, redoes the full leave-one-out fit +(refitting the structured linear mean inside every fold, so the null is not +flattered), applies the utility transform, and records the resulting Spearman +and R2. +""" + +from __future__ import annotations + +import argparse +import json +import warnings +from pathlib import Path + +import numpy as np +from scipy.stats import spearmanr + +from thickness_permutation_and_mean import ( # noqa: E402 + expected_score, + load, + loo_nm, + r2, +) + +warnings.filterwarnings("ignore") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--workbook", default="local_inputs/Summary Table.xlsx") + ap.add_argument("--n", type=int, required=True) + ap.add_argument("--seed", type=int, required=True) + ap.add_argument("--out", required=True) + ap.add_argument("--structured", action="store_true", default=True) + args = ap.parse_args() + + X, Xphys, T_nm, score = load(Path(args.workbook)) + n = len(T_nm) + rng = np.random.default_rng(args.seed) + + rhos, r2s = [], [] + for _ in range(args.n): + perm = rng.permutation(n) + mu_p, var_p = loo_nm( + X, T_nm[perm], structured_mean=args.structured, Xphys=Xphys + ) + e_p = expected_score(mu_p, var_p) + rhos.append(float(spearmanr(score[perm], e_p).statistic)) + r2s.append(float(r2(score[perm], e_p))) + + Path(args.out).write_text( + json.dumps({"seed": args.seed, "n": args.n, "rho": rhos, "r2": r2s}), + encoding="utf-8", + ) + print( + f"shard seed={args.seed} n={args.n} mean_rho={np.mean(rhos):+.4f} -> {args.out}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/thickness_objective_check.py b/scripts/thickness_objective_check.py new file mode 100644 index 0000000..bf5c759 --- /dev/null +++ b/scripts/thickness_objective_check.py @@ -0,0 +1,181 @@ +"""Acceptance check for plan rev. 2 section 1.2. + +Compares two ways of getting a thickness *score* prediction out of a GP: + + A. train on the score directly (what the repo does today) + B. train on raw nanometres, then push the posterior through the 650 nm + Gaussian analytically via ObjectiveTransform.expected_transform + +Both are scored against the true score by exact leave-one-out, so the comparison +is like for like. Also reported is the naive variant of B that transforms only +the posterior mean, to show what ignoring the variance costs. + + python scripts/thickness_objective_check.py +""" + +from __future__ import annotations + +import argparse +import math +import warnings +from pathlib import Path + +import numpy as np +import torch +from openpyxl import load_workbook +from scipy.stats import spearmanr + +from mobo_kit.model_validation import ( + DIM_SCALED_PRIOR, + LEGACY_NO_PRIOR, + fit_model_variant, +) +from mobo_kit.objectives import ObjectiveSpec, ObjectiveTransform + +DESIGN = ( + ("speed_1", 1000.0, 6000.0), + ("time_1", 5.0, 50.0), + ("speed_2", 0.0, 5000.0), + ("time_2", 10.0, 60.0), + ("precur_conc", 1.0, 2.0), + ("precur_vol", 40.0, 200.0), + ("anneal_temp", 100.0, 185.0), + ("anneal_time", 10.0, 60.0), + ("anti_vol", 100.0, 200.0), + ("anti_time", 9.0, 25.0), +) +TARGET_NM = 650.0 +# workbook: =EXP(-(((X-650)/250)^2)), which is exp(-0.5*((X-650)/s)^2) with s = 250/sqrt(2) +SIGMA_NM = 250.0 / math.sqrt(2.0) + +THICKNESS_UTILITY = ObjectiveTransform( + [ + ObjectiveSpec( + "thickness", "target", "gaussian_target", target=TARGET_NM, sigma=SIGMA_NM + ) + ], + version="D2D-thickness-nm-v1", +) + + +def load(path: Path): + ws = load_workbook(path, data_only=True)["Sheet1"] + rows = [r for r in ws.iter_rows(min_row=2, values_only=True) if r[0] is not None] + lo = np.array([d[1] for d in DESIGN]) + hi = np.array([d[2] for d in DESIGN]) + X = (np.array([[r[j] for j in range(1, 11)] for r in rows], float) - lo) / (hi - lo) + T_nm = np.array([r[23] for r in rows], float) # X: Thickness (avg) + score = np.array([r[27] for r in rows], float) # AB: Thickness score + ids = tuple(int(r[0]) for r in rows) + return ids, X, T_nm, score + + +def loo_posterior(X, y, ids, variant, seed=73): + """Exact LOO posterior mean and variance for a single objective.""" + n = len(y) + mean = np.empty(n) + var = np.empty(n) + for i in range(n): + keep = [j for j in range(n) if j != i] + torch.manual_seed(seed) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + rec = fit_model_variant( + torch.tensor(X[keep], dtype=torch.double), + torch.tensor(y[keep], dtype=torch.double).unsqueeze(-1), + sample_ids=tuple(ids[j] for j in keep), + objective_names=("y",), + variant=variant, + seed=seed, + ) + rec.model.eval() + with torch.no_grad(): + post = rec.model.posterior( + torch.tensor(X[i : i + 1], dtype=torch.double) + ) + mean[i] = float(post.mean.reshape(-1)[0]) + var[i] = float(post.variance.reshape(-1)[0]) + return mean, var + + +def r2(y, p): + return 1.0 - np.sum((y - p) ** 2) / np.sum((y - y.mean()) ** 2) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--workbook", default="local_inputs/Summary Table.xlsx") + ap.add_argument( + "--variant", + default="dim_scaled_prior", + choices=["dim_scaled_prior", "legacy_matern_no_prior"], + ) + args = ap.parse_args() + + variant = ( + DIM_SCALED_PRIOR if args.variant == "dim_scaled_prior" else LEGACY_NO_PRIOR + ) + ids, X, T_nm, score = load(Path(args.workbook)) + n = len(ids) + null = 1.0 - (n / (n - 1)) ** 2 + + # sanity: the workbook score must be reproducible from raw nm + recomputed = ( + THICKNESS_UTILITY(torch.tensor(T_nm, dtype=torch.double).unsqueeze(-1)) + .numpy() + .reshape(-1) + ) + print( + f"score == exp(-((T-650)/250)^2) from raw nm ? " + f"max|diff| = {np.abs(recomputed - score).max():.2e}" + ) + print(f"model variant: {variant.name}") + print(f"N = {n}, LOO-mean null R2 = {null:+.4f}\n") + + # --- A: train on the score directly ----------------------------------- + a_mean, _ = loo_posterior(X, score, ids, variant) + + # --- B: train on nanometres, transform the posterior ------------------- + b_mu, b_var = loo_posterior(X, T_nm, ids, variant) + mu_t = torch.tensor(b_mu, dtype=torch.double).unsqueeze(-1) + var_t = torch.tensor(b_var, dtype=torch.double).unsqueeze(-1) + b_expected = THICKNESS_UTILITY.expected_transform(mu_t, var_t).numpy().reshape(-1) + b_meanonly = THICKNESS_UTILITY(mu_t).numpy().reshape(-1) + + print("=== predicting the THICKNESS SCORE, exact leave-one-out ===") + print(f"{'approach':>46} {'LOO R2':>9} {'Spearman':>10}") + for label, pred in ( + ("A train on score directly", a_mean), + ("B train on nm -> E[score] (analytic)", b_expected), + ("B' train on nm -> score(mean) only", b_meanonly), + ): + print( + f"{label:>46} {r2(score, pred):>+9.4f} " + f"{spearmanr(score, pred).statistic:>+10.4f}" + ) + print(f"{'null (LOO mean)':>46} {null:>+9.4f} {-1.0:>+10.4f}") + + print("\n=== the underlying raw-nm model ===") + print( + f" LOO R2 = {r2(T_nm, b_mu):+.4f} Spearman = " + f"{spearmanr(T_nm, b_mu).statistic:+.4f}" + ) + print( + f" posterior sd over the 15 folds: min {np.sqrt(b_var).min():.1f} nm, " + f"median {np.median(np.sqrt(b_var)):.1f} nm, max {np.sqrt(b_var).max():.1f} nm" + ) + + print("\n=== what the uncertainty penalty is doing ===") + print( + f"{'sample':>7} {'true nm':>9} {'pred nm':>9} {'sd nm':>8} " + f"{'true score':>11} {'E[score]':>10} {'score(mean)':>12}" + ) + for i, sid in enumerate(ids): + print( + f"{sid:>7} {T_nm[i]:>9.0f} {b_mu[i]:>9.0f} {math.sqrt(b_var[i]):>8.0f} " + f"{score[i]:>11.4f} {b_expected[i]:>10.4f} {b_meanonly[i]:>12.4f}" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/thickness_permutation_and_mean.py b/scripts/thickness_permutation_and_mean.py new file mode 100644 index 0000000..b2fb77b --- /dev/null +++ b/scripts/thickness_permutation_and_mean.py @@ -0,0 +1,252 @@ +"""Two evaluations that gate R1 generation. + +1. Permutation test on the route-B Spearman: permute the thickness measurements, + redo the full leave-one-out fit plus analytic transform, and build the null + distribution. Turns "rank improved" into a p-value. + +2. Structured mean function. Spin-coating physics says T ~ speed^-0.5, and + log T ~ log(speed_1) + log(precur_conc) reaches LOO R2 +0.449 while the plain + 10-input GP reaches +0.183. Test whether giving the GP a linear mean on those + two log inputs closes the gap. Selection of the two inputs is from physics, + fixed before fitting; the linear coefficients are refitted inside every fold. + + python scripts/thickness_permutation_and_mean.py --permutations 200 +""" + +from __future__ import annotations + +import argparse +import math +import warnings +from pathlib import Path + +import numpy as np +import torch +from botorch.fit import fit_gpytorch_mll +from botorch.models import SingleTaskGP +from botorch.models.transforms.outcome import Standardize +from botorch.models.utils.gpytorch_modules import ( + get_covar_module_with_dim_scaled_prior, + get_gaussian_likelihood_with_lognormal_prior, +) +from gpytorch.kernels import ScaleKernel +from gpytorch.means import LinearMean +from gpytorch.mlls import ExactMarginalLogLikelihood +from openpyxl import load_workbook +from scipy.stats import spearmanr +from sklearn.linear_model import LinearRegression + +from mobo_kit.objectives import ObjectiveSpec, ObjectiveTransform + +warnings.filterwarnings("ignore") +torch.set_default_dtype(torch.double) + +DESIGN = [ + ("speed_1", 1000, 6000), + ("time_1", 5, 50), + ("speed_2", 0, 5000), + ("time_2", 10, 60), + ("precur_conc", 1, 2), + ("precur_vol", 40, 200), + ("anneal_temp", 100, 185), + ("anneal_time", 10, 60), + ("anti_vol", 100, 200), + ("anti_time", 9, 25), +] +TARGET_NM, SIGMA_NM = 650.0, 250.0 / math.sqrt(2.0) +UTILITY = ObjectiveTransform( + [ + ObjectiveSpec( + "thickness", "target", "gaussian_target", target=TARGET_NM, sigma=SIGMA_NM + ) + ], + version="D2D-thickness-nm-v1", +) +SPEED_1, PRECUR_CONC = 0, 4 + + +def load(path: Path): + ws = load_workbook(path, data_only=True)["Sheet1"] + rows = [r for r in ws.iter_rows(min_row=2, values_only=True) if r[0] is not None] + lo = np.array([d[1] for d in DESIGN], float) + hi = np.array([d[2] for d in DESIGN], float) + Xp = np.array([[r[j] for j in range(1, 11)] for r in rows], float) + return ( + (Xp - lo) / (hi - lo), + Xp, + np.array([r[23] for r in rows], float), + np.array([r[27] for r in rows], float), + ) + + +def _gp(X, y, *, mean_features=None): + """SingleTaskGP under the dim_scaled_prior contract, optional linear mean.""" + base = get_covar_module_with_dim_scaled_prior( + ard_num_dims=X.shape[1], use_rbf_kernel=False + ) + model = SingleTaskGP( + X, + y, + covar_module=ScaleKernel(base), + likelihood=get_gaussian_likelihood_with_lognormal_prior(), + outcome_transform=Standardize(m=1), + ) + if mean_features is not None: + model.mean_module = LinearMean(input_size=mean_features, bias=True) + fit_gpytorch_mll(ExactMarginalLogLikelihood(model.likelihood, model)) + return model + + +def loo_nm(X, y, *, structured_mean=False, Xphys=None, seed=73): + """LOO posterior over raw nm. With structured_mean, a physics linear trend on + log(speed_1) and log(precur_conc) is removed first and added back after.""" + n = len(y) + mu = np.empty(n) + var = np.empty(n) + for i in range(n): + keep = [j for j in range(n) if j != i] + torch.manual_seed(seed) + if structured_mean: + F = np.c_[np.log(Xphys[:, SPEED_1]), np.log(Xphys[:, PRECUR_CONC])] + lin = LinearRegression().fit(F[keep], np.log(y[keep])) + resid = np.log(y[keep]) - lin.predict(F[keep]) + m = _gp(torch.tensor(X[keep]), torch.tensor(resid).unsqueeze(-1)) + m.eval() + with torch.no_grad(): + p = m.posterior(torch.tensor(X[i : i + 1])) + r_mu = float(p.mean.reshape(-1)[0]) + r_var = float(p.variance.reshape(-1)[0]) + log_mu = float(lin.predict(F[i : i + 1])[0]) + r_mu + # lognormal moments back to nm + mu[i] = math.exp(log_mu + r_var / 2.0) + var[i] = (math.exp(r_var) - 1.0) * math.exp(2 * log_mu + r_var) + else: + m = _gp(torch.tensor(X[keep]), torch.tensor(y[keep]).unsqueeze(-1)) + m.eval() + with torch.no_grad(): + p = m.posterior(torch.tensor(X[i : i + 1])) + mu[i] = float(p.mean.reshape(-1)[0]) + var[i] = float(p.variance.reshape(-1)[0]) + return mu, var + + +def r2(y, p): + return 1.0 - np.sum((y - p) ** 2) / np.sum((y - y.mean()) ** 2) + + +def expected_score(mu, var): + return ( + UTILITY.expected_transform( + torch.tensor(mu).unsqueeze(-1), torch.tensor(var).unsqueeze(-1) + ) + .numpy() + .reshape(-1) + ) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--workbook", default="local_inputs/Summary Table.xlsx") + ap.add_argument("--permutations", type=int, default=200) + ap.add_argument("--seed", type=int, default=0) + ap.add_argument( + "--structured", action="store_true", help="permute the structured-mean pipeline" + ) + args = ap.parse_args() + + X, Xphys, T_nm, score = load(Path(args.workbook)) + n = len(T_nm) + null_r2 = 1.0 - (n / (n - 1)) ** 2 + + print("=== 2. structured mean on log(speed_1), log(precur_conc) ===") + plain_mu, plain_var = loo_nm(X, T_nm) + struct_mu, struct_var = loo_nm(X, T_nm, structured_mean=True, Xphys=Xphys) + print(f"{'raw-nm model':>34} {'LOO R2':>9} {'Spearman':>10}") + for label, mu in ( + ("plain GP, 10 inputs", plain_mu), + ("GP + physics linear mean", struct_mu), + ): + print( + f"{label:>34} {r2(T_nm, mu):>+9.4f} " + f"{spearmanr(T_nm, mu).statistic:>+10.4f}" + ) + print(f"{'2-input log-log reference':>34} {'+0.4494':>9} {'+0.7143':>10}") + + print(f"\n{'resulting score prediction':>34} {'LOO R2':>9} {'Spearman':>10}") + plain_s = expected_score(plain_mu, plain_var) + struct_s = expected_score(struct_mu, struct_var) + for label, s in ( + ("plain GP -> E[score]", plain_s), + ("structured mean -> E[score]", struct_s), + ): + print( + f"{label:>34} {r2(score, s):>+9.4f} " + f"{spearmanr(score, s).statistic:>+10.4f}" + ) + print(f"{'null':>34} {null_r2:>+9.4f} {-1.0:>+10.4f}") + + print("\n=== 3. how much of the structured-mean result rests on sample 1? ===") + print(" sample 1 is the off-grid literature control and the one") + print(" extrapolation point, so LOO metrics are sensitive to it") + keep1 = np.arange(1, n) + mu_x, var_x = loo_nm( + X[keep1], T_nm[keep1], structured_mean=True, Xphys=Xphys[keep1] + ) + s_x = expected_score(mu_x, var_x) + n_x = len(keep1) + print(f"{'':>34} {'LOO R2':>9} {'Spearman':>10}") + print( + f"{'raw nm, all 15':>34} {r2(T_nm, struct_mu):>+9.4f} " + f"{spearmanr(T_nm, struct_mu).statistic:>+10.4f}" + ) + print( + f"{'raw nm, sample 1 excluded (N=14)':>34} {r2(T_nm[keep1], mu_x):>+9.4f} " + f"{spearmanr(T_nm[keep1], mu_x).statistic:>+10.4f}" + ) + print( + f"{'score, sample 1 excluded':>34} {r2(score[keep1], s_x):>+9.4f} " + f"{spearmanr(score[keep1], s_x).statistic:>+10.4f}" + ) + print(f"{'null at N=14':>34} {1.0 - (n_x/(n_x-1))**2:>+9.4f} {-1.0:>+10.4f}") + + print(f"\n=== 1. permutation test, {args.permutations} shuffles ===") + print(" permuting the nm measurements and redoing LOO + transform") + print( + f" structured mean: {args.structured} " + "(when true the linear mean is refit inside every null fold too)" + ) + rng = np.random.default_rng(args.seed) + observed = struct_s if args.structured else plain_s + obs_rho = spearmanr(score, observed).statistic + obs_r2 = r2(score, observed) + null_rho, null_r2s = [], [] + for k in range(args.permutations): + perm = rng.permutation(n) + T_p, s_p = T_nm[perm], score[perm] + mu_p, var_p = loo_nm(X, T_p, structured_mean=args.structured, Xphys=Xphys) + e_p = expected_score(mu_p, var_p) + null_rho.append(spearmanr(s_p, e_p).statistic) + null_r2s.append(r2(s_p, e_p)) + if (k + 1) % 25 == 0: + print( + f" {k+1}/{args.permutations} done, running null mean rho = " + f"{np.mean(null_rho):+.4f}" + ) + null_rho = np.array(null_rho) + null_r2s = np.array(null_r2s) + print(f"\n observed Spearman = {obs_rho:+.4f}") + print( + f" null Spearman : mean {null_rho.mean():+.4f}, sd {null_rho.std():.4f}, " + f"95th pct {np.percentile(null_rho, 95):+.4f}" + ) + print(f" p(null >= observed) = {np.mean(null_rho >= obs_rho):.4f}") + print(f"\n observed R2 = {obs_r2:+.4f}") + print(f" null R2 : mean {null_r2s.mean():+.4f}, sd {null_r2s.std():.4f}") + print(f" p(null >= observed) = {np.mean(null_r2s >= obs_r2):.4f}") + print("\n Note the null mean for Spearman is NOT zero: the leave-one-out") + print(" shrinkage artifact drags it negative, which is exactly why a") + print(" positive observed value is meaningful.") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_structured_means.py b/scripts/validate_structured_means.py new file mode 100644 index 0000000..22e3fb0 --- /dev/null +++ b/scripts/validate_structured_means.py @@ -0,0 +1,149 @@ +"""Confirm the declarative structured means reproduce the hand-rolled results. + +Targets, exact leave-one-out, null = -0.1480: + + thickness (nm) plain GP +0.183 -> structured +0.384 + optoelectronic plain GP ? -> structured +0.244 (anneal_temp alone) + + python scripts/validate_structured_means.py +""" + +from __future__ import annotations + +import argparse +import warnings +from pathlib import Path + +import numpy as np +import torch +from openpyxl import load_workbook +from scipy.stats import spearmanr + +from mobo_kit.campaign import load_campaign_config +from mobo_kit.model_validation import DIM_SCALED_PRIOR, fit_model_variant +from mobo_kit.structured_mean import ( + MeanFeature, + StructuredMeanSpec, + apply_structured_mean, + fit_structured_mean, +) + +warnings.filterwarnings("ignore") +torch.set_num_threads(1) + +THICKNESS = StructuredMeanSpec( + response="log", + features=(MeanFeature("speed_1", "log"), MeanFeature("precur_conc", "log")), +) +OPTO = StructuredMeanSpec( + response="identity", features=(MeanFeature("anneal_temp", "identity"),) +) + + +def _gp_residual_posterior(X_norm, resid, test_norm, seed=73): + rec = fit_model_variant( + torch.tensor(X_norm, dtype=torch.double), + torch.tensor(resid, dtype=torch.double).unsqueeze(-1), + sample_ids=tuple(range(len(X_norm))), + objective_names=("residual",), + variant=DIM_SCALED_PRIOR, + seed=seed, + ) + rec.model.eval() + with torch.no_grad(): + post = rec.model.posterior(torch.tensor(test_norm, dtype=torch.double)) + return ( + float(post.mean.reshape(-1)[0]), + float(post.variance.reshape(-1)[0]), + ) + + +def loo(X_phys, X_norm, y, names, spec): + """Exact LOO. Mean coefficients refit on the 14 training rows each fold.""" + n = len(y) + mean = np.empty(n) + var = np.empty(n) + for i in range(n): + keep = [j for j in range(n) if j != i] + if spec is None: + r_mu, r_var = _gp_residual_posterior( + X_norm[keep], np.asarray(y, float)[keep], X_norm[i : i + 1] + ) + mean[i], var[i] = r_mu, r_var + else: + coef, resid = fit_structured_mean( + X_phys[keep], np.asarray(y, float)[keep], spec, names + ) + r_mu, r_var = _gp_residual_posterior(X_norm[keep], resid, X_norm[i : i + 1]) + post = apply_structured_mean( + coef, + X_phys[i : i + 1], + spec, + names, + np.array([r_mu]), + np.array([r_var]), + ) + if post.link == "log": + # report on the original scale for comparability + mean[i] = float(np.exp(post.mean[0] + post.variance[0] / 2.0)) + var[i] = float( + (np.exp(post.variance[0]) - 1.0) + * np.exp(2 * post.mean[0] + post.variance[0]) + ) + else: + mean[i], var[i] = float(post.mean[0]), float(post.variance[0]) + return mean, var + + +def r2(y, p): + y = np.asarray(y, float) + return 1.0 - np.sum((y - p) ** 2) / np.sum((y - y.mean()) ** 2) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--workbook", default="local_inputs/Summary Table.xlsx") + ap.add_argument("--config", default="configs/FA0.9CS0.1PbI3_260407_Config.yaml") + args = ap.parse_args() + + config = load_campaign_config(args.config) + names = [item["name"] for item in config["inputs"]] + lo = np.array([item["start"] for item in config["inputs"]], float) + hi = np.array([item["stop"] for item in config["inputs"]], float) + + ws = load_workbook(Path(args.workbook), data_only=True)["Sheet1"] + rows = [r for r in ws.iter_rows(min_row=2, values_only=True) if r[0] is not None] + X_phys = np.array([[float(r[j]) for j in range(1, 11)] for r in rows]) + X_norm = (X_phys - lo) / (hi - lo) + thickness_nm = np.array([float(r[23]) for r in rows]) + optoelectronic = np.array([float(r[26]) for r in rows]) + + n = len(rows) + null = 1.0 - (n / (n - 1)) ** 2 + print(f"N = {n}, null LOO R2 = {null:+.4f}\n") + print( + f"{'objective':>16} {'mean function':>34} {'LOO R2':>9} {'Spearman':>10} {'target':>9}" + ) + + cases = [ + ("thickness nm", "none (plain GP)", thickness_nm, None, "+0.183"), + ( + "thickness nm", + "log T ~ log(speed_1)+log(conc)", + thickness_nm, + THICKNESS, + "+0.384", + ), + ("optoelectronic", "none (plain GP)", optoelectronic, None, "-"), + ("optoelectronic", "linear anneal_temp", optoelectronic, OPTO, "+0.244"), + ] + for label, mean_label, y, spec, target in cases: + mu, _ = loo(X_phys, X_norm, y, names, spec) + print( + f"{label:>16} {mean_label:>34} {r2(y, mu):>+9.4f} " + f"{spearmanr(y, mu).statistic:>+10.4f} {target:>9}" + ) + + +if __name__ == "__main__": + main() diff --git a/src/mobo_kit/campaign.py b/src/mobo_kit/campaign.py new file mode 100644 index 0000000..26cb145 --- /dev/null +++ b/src/mobo_kit/campaign.py @@ -0,0 +1,648 @@ +"""The campaign path: three rounds, one function each. + + run_r0_lhs(config, n=15) -> space-filling initial worklist + run_r1_ucb(config, ..., n=5) -> UCB-HVI batch with local penalisation + run_r2_qlognehvi(config, ..., n=3) -> qLogNEHVI batch + +Each returns a :class:`RoundResult` carrying the proposed conditions in physical +units plus a diagnostics dict. These functions orchestrate; the mathematics +lives in ``lhs``, ``candidate_pool``, ``ucb_hvi``, ``batch_selection``, +``qlognehvi_batch`` and ``discrete_refinement``, which are not reimplemented +here. + +Two things are worth knowing before reading further. + +**Thickness trains on nanometres.** The campaign's thickness utility is a +Gaussian on a 650 nm target, and that map is 2-to-1: films at 400 nm and 900 nm +score alike from opposite sides of the peak. Training on the score forces the +GP to represent a folded ridge; training on nanometres leaves a smooth trend. +So ``model_source_column`` (what the GP sees) and the utility transform are +declared separately per objective. See docs/GP_MODEL_DECISION.md. + +**Utility moments come from posterior samples.** Because the thickness utility +is nonlinear, the mean of the transform is not the transform of the mean. +``ucb_hvi.posterior_utility_moments`` already applies the transform to posterior +samples, so ``moment_method="monte_carlo"`` is correct and required here; the +``analytic_identity`` fast path is only valid when every transform is identity. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping, Sequence + +import numpy as np +import pandas as pd +import torch + +from .batch_selection import LocalPenalizationConfig +from .candidate_pool import CandidatePool, sample_discrete_candidate_pool +from .constraints import constraints_from_config +from .design import Design, build_design_from_config +from .lhs import lhs_dataframe_optimized +from botorch.models.model_list_gp_regression import ModelListGP + +from .model_validation import fit_model_variant, model_variant_spec +from .structured_mean import build_structured_mean, mean_spec_from_config +from .objectives import ObjectiveSpec, ObjectiveTransform +from .qlognehvi_batch import propose_qlognehvi_penalized_batch +from .ucb_hvi import propose_ucb_hvi_batch + +__all__ = [ + "CampaignConfigError", + "EXCEL_CSV_ENCODING", + "write_worklist_csv", + "FIXED_SCALING_MODES", + "assert_scaling_is_campaign_fixed", + "RoundResult", + "build_objective_transform", + "expand_replicates", + "load_campaign_config", + "run_r0_lhs", + "run_r1_ucb", + "run_r2_qlognehvi", + "validate_batch", +] + + +#: Encoding for CSVs a human will open in Excel. Excel does not detect UTF-8 +#: without a BOM and falls back to the system ANSI codepage, which mangles any +#: non-ASCII cell -- and does so on the reader's machine, not the writer's, so +#: it is invisible during development. ``utf-8-sig`` writes the BOM; pandas and +#: every other reader strip it transparently. +EXCEL_CSV_ENCODING = "utf-8-sig" + + +def write_worklist_csv(frame: pd.DataFrame, path: str | Path) -> Path: + """Write a worklist CSV that Excel will open correctly.""" + destination = Path(path) + frame.to_csv(destination, index=False, encoding=EXCEL_CSV_ENCODING) + return destination + + +class CampaignConfigError(ValueError): + """The configuration cannot support the requested round.""" + + +class BatchValidityError(RuntimeError): + """A proposed batch failed a validity check and must not be issued.""" + + +@dataclass +class RoundResult: + """One round's proposal.""" + + round_name: str + conditions: pd.DataFrame + """Distinct proposed conditions, physical units, columns == design.names.""" + replicates: pd.DataFrame + """One row per physical film, with candidate_id and replicate_group.""" + diagnostics: dict[str, Any] = field(default_factory=dict) + + @property + def n_conditions(self) -> int: + return len(self.conditions) + + +# --------------------------------------------------------------------------- # +# configuration +# --------------------------------------------------------------------------- # + + +def load_campaign_config(path: str | Path) -> dict[str, Any]: + """Read a campaign YAML. UTF-8 is explicit: the default codec is locale + dependent and silently fails on non-ASCII under some Windows locales.""" + import yaml + + with open(path, "r", encoding="utf-8") as handle: + config = yaml.safe_load(handle) + if not isinstance(config, Mapping): + raise CampaignConfigError(f"{path} did not parse to a mapping.") + return dict(config) + + +def _objective_specs(config: Mapping[str, Any]) -> tuple[ObjectiveSpec, ...]: + objectives = config.get("objectives") + if not isinstance(objectives, Mapping) or "specs" not in objectives: + raise CampaignConfigError( + "config['objectives'] must be a mapping containing 'specs'. A config " + "with an empty objective list cannot propose candidates." + ) + raw_specs = objectives["specs"] + if not isinstance(raw_specs, Sequence) or not raw_specs: + raise CampaignConfigError("config['objectives']['specs'] must be non-empty.") + + specs: list[ObjectiveSpec] = [] + for entry in raw_specs: + if not isinstance(entry, Mapping): + raise CampaignConfigError("Every objective spec must be a mapping.") + if not entry.get("model_source_column"): + raise CampaignConfigError( + f"Objective {entry.get('name')!r} must declare " + "model_source_column: the GP trains on that column, which is not " + "always the final score." + ) + mean_spec = mean_spec_from_config(entry) + specs.append( + ObjectiveSpec( + name=str(entry["name"]), + goal=str(entry["goal"]), + transform=str(entry["transform"]), + # a log-response mean function means the GP emits log(y), so the + # utility must exponentiate and its posterior is lognormal + model_link=( + "log" + if mean_spec is not None and mean_spec.response == "log" + else "identity" + ), + source_column=str(entry["model_source_column"]), + lower_anchor=entry.get("lower_anchor"), + upper_anchor=entry.get("upper_anchor"), + target=entry.get("target"), + sigma=entry.get("sigma"), + scale=entry.get("scale"), + ) + ) + return tuple(specs) + + +#: Scaling modes that are fixed for the whole campaign. Anything else means the +#: scale would be re-derived from whatever data happens to exist this round. +FIXED_SCALING_MODES = frozenset({"already_normalized", "fixed_affine"}) + + +def assert_scaling_is_campaign_fixed(config: Mapping[str, Any]) -> None: + """Refuse objective scales that are re-derived from observed data. + + This is the single most consequential check inherited from + ``production_gate.py``, and it is not about approval. If an objective's + scale moves with the data each round -- observed min/max, a percentile, a + round-local standardisation -- then the utility space itself moves, and + hypervolume computed in round N is not comparable with round N+1. The + progress plot silently stops meaning anything. + + The temptation is immediate and specific: once R1 measurements land, the + observed ranges will look like better anchors than the declared ones. They + are not. Widen a declared range deliberately and version it; never let it + track the data. + """ + for spec in _objective_specs(config): + if spec.transform == "affine": + if spec.lower_anchor is None or spec.upper_anchor is None: + raise CampaignConfigError( + f"Objective {spec.name!r} uses an affine transform but does " + "not declare fixed lower_anchor/upper_anchor. Round-by-round " + "min/max scaling makes hypervolume incomparable across rounds." + ) + if not (spec.lower_anchor < spec.upper_anchor): + raise CampaignConfigError( + f"Objective {spec.name!r} requires lower_anchor < upper_anchor." + ) + + declared = (config.get("objectives") or {}).get("scaling_mode", "fixed_affine") + if declared not in FIXED_SCALING_MODES: + raise CampaignConfigError( + f"objectives.scaling_mode must be one of {sorted(FIXED_SCALING_MODES)}; " + f"got {declared!r}. Observed or data-derived scaling is forbidden " + "because it makes hypervolume incomparable between rounds." + ) + + +def build_objective_transform(config: Mapping[str, Any]) -> ObjectiveTransform: + """Build the versioned raw-output-to-utility contract from a campaign config.""" + version = config.get("objectives", {}).get("contract_version") + if not version: + raise CampaignConfigError( + "config['objectives']['contract_version'] is required so that " + "hypervolume stays comparable across rounds." + ) + assert_scaling_is_campaign_fixed(config) + return ObjectiveTransform(_objective_specs(config), version=str(version)) + + +def model_source_columns(config: Mapping[str, Any]) -> tuple[str, ...]: + """The workbook columns the GP trains on, in objective order.""" + return tuple(spec.source_column for spec in _objective_specs(config)) + + +def _reference_point(config: Mapping[str, Any], n_objectives: int) -> np.ndarray: + raw = config.get("reference_point_utility") + if raw is None: + raise CampaignConfigError( + "reference_point_utility is required and must be declared in UTILITY " + "space, after the objective transforms." + ) + point = np.asarray(raw, dtype=float) + if point.shape != (n_objectives,): + raise CampaignConfigError( + f"reference_point_utility must have {n_objectives} entries; " + f"got {point.shape}." + ) + if not np.all(np.isfinite(point)): + raise CampaignConfigError("reference_point_utility must be finite.") + return point + + +def _penalization(config: Mapping[str, Any]) -> LocalPenalizationConfig: + raw = config.get("local_penalization") or {} + weights = raw.get("dimension_weights") + return LocalPenalizationConfig( + radius=raw.get("radius"), + min_batch_distance=float(raw.get("min_batch_distance", 0.0)), + min_observed_distance=float(raw.get("min_observed_distance", 0.0)), + dimension_weights=None if weights is None else np.asarray(weights, float), + ) + + +def _round_settings(config: Mapping[str, Any], key: str) -> dict[str, Any]: + rounds = config.get("rounds") or {} + settings = rounds.get(key) + if not isinstance(settings, Mapping): + raise CampaignConfigError(f"config['rounds']['{key}'] is required.") + return dict(settings) + + +# --------------------------------------------------------------------------- # +# validity +# --------------------------------------------------------------------------- # + + +def validate_batch( + conditions: pd.DataFrame, + design: Design, + *, + expected_count: int, + min_pairwise_distance: float = 0.0, +) -> dict[str, Any]: + """Refuse to issue a batch that is malformed. + + Five checks, all of which catch real bugs: the batch is the requested size, + its rows are distinct, every value sits exactly on the declared grid, every + value is finite and in bounds, and the rows are at least + ``min_pairwise_distance`` apart in normalised space. + + This replaces the previous debug/production approval tiers. Whether a batch + is approved for fabrication is a human decision recorded outside the code; + it is not something this function can compute, so it does not pretend to. + """ + report: dict[str, Any] = {} + values = conditions.to_numpy(dtype=float) + + report["expected_count"] = expected_count + report["actual_count"] = len(conditions) + if len(conditions) != expected_count: + raise BatchValidityError( + f"Expected exactly {expected_count} conditions; got {len(conditions)}." + ) + + if not np.all(np.isfinite(values)): + raise BatchValidityError("Proposed conditions contain non-finite values.") + report["finite"] = True + + duplicates = pd.DataFrame(values).duplicated().to_numpy() + if duplicates.any(): + raise BatchValidityError( + f"Proposed conditions must be unique; {int(duplicates.sum())} duplicate " + "row(s) found." + ) + report["unique"] = True + + off_grid: list[str] = [] + for j, name in enumerate(design.names): + grid = np.asarray(design.var_array[j], dtype=float) + for value in values[:, j]: + if not np.any(np.isclose(grid, value, rtol=0.0, atol=1e-9)): + off_grid.append(f"{name}={value!r}") + if off_grid: + raise BatchValidityError( + "Proposed values must lie exactly on the declared grid; off-grid: " + f"{sorted(set(off_grid))}" + ) + report["on_grid"] = True + + lowers = np.asarray(design.lowers, dtype=float) + uppers = np.asarray(design.uppers, dtype=float) + if np.any(values < lowers - 1e-9) or np.any(values > uppers + 1e-9): + raise BatchValidityError("Proposed conditions fall outside design bounds.") + report["in_bounds"] = True + + span = np.where(uppers > lowers, uppers - lowers, 1.0) + norm = (values - lowers) / span + if len(norm) > 1: + from scipy.spatial.distance import pdist + + distances = pdist(norm) + report["min_pairwise_distance"] = float(distances.min()) + if min_pairwise_distance > 0 and distances.min() < min_pairwise_distance: + raise BatchValidityError( + f"Minimum pairwise distance {distances.min():.4f} is below the " + f"configured floor {min_pairwise_distance:.4f}." + ) + else: + report["min_pairwise_distance"] = float("inf") + + boundary = (np.isclose(norm, 0.0, atol=1e-9)) | (np.isclose(norm, 1.0, atol=1e-9)) + report["boundary_coords_per_condition"] = boundary.sum(axis=1).tolist() + return report + + +def expand_replicates( + conditions: pd.DataFrame, *, replicates: int, round_name: str +) -> pd.DataFrame: + """One row per physical film, sharing a replicate_group per condition. + + The experimentalists run each proposed condition ``replicates`` times to + measure reproducibility. Those films are separate experimental rows, but + they are one design point: aggregate them to a condition-level mean before + the next round trains on them, and pool their within-condition variance + across conditions for an observation-noise estimate. + """ + if replicates < 1: + raise ValueError("replicates must be at least 1.") + rows = [] + for index, (_, condition) in enumerate(conditions.iterrows(), start=1): + candidate_id = f"{round_name}_C{index:02d}" + for replicate in range(1, replicates + 1): + row = dict(condition) + row["candidate_id"] = candidate_id + row["replicate_group"] = candidate_id + row["replicate_index"] = replicate + row["round"] = round_name + rows.append(row) + return pd.DataFrame(rows) + + +# --------------------------------------------------------------------------- # +# rounds +# --------------------------------------------------------------------------- # + + +def run_r0_lhs( + config: Mapping[str, Any], *, n: int = 15, seed: int | None = None +) -> RoundResult: + """Space-filling initial worklist. No model is involved.""" + design = build_design_from_config(dict(config)) + constraints = constraints_from_config(dict(config), design) + resolved_seed = ( + int(config.get("reproducibility", {}).get("seed", 0)) if seed is None else seed + ) + conditions = lhs_dataframe_optimized( + design, + n, + seed=resolved_seed, + snap_to_grids=True, + row_constraints=constraints or None, + ) + replicates_per = int( + _round_settings(config, "r1").get("replicates_per_condition", 1) + ) + report = validate_batch(conditions, design, expected_count=n) + return RoundResult( + round_name="R0", + conditions=conditions, + replicates=expand_replicates( + conditions, replicates=replicates_per, round_name="R0" + ), + diagnostics={"seed": resolved_seed, "validity": report, "method": "lhs"}, + ) + + +def _fit_models( + config: Mapping[str, Any], + X_phys: np.ndarray, + X_norm: np.ndarray, + Y_raw: np.ndarray, + seed: int, +) -> Any: + """One GP per objective, each with its declared structured mean. + + Objectives with a ``mean_function`` train on the response-space target with + the OLS trend frozen into the mean module, so ``posterior()`` already carries + it and no caller has to add it back. Objectives without one are unchanged. + """ + variant = model_variant_spec(str(config.get("model", {}).get("variant"))) + specs = _objective_specs(config) + entries = config["objectives"]["specs"] + design = build_design_from_config(dict(config)) + lowers = np.asarray(design.lowers, dtype=float) + uppers = np.asarray(design.uppers, dtype=float) + names = list(design.names) + + models = [] + for index, (spec, entry) in enumerate(zip(specs, entries)): + mean_spec = mean_spec_from_config(entry) + y = np.asarray(Y_raw, dtype=float)[:, index] + mean_module = None + if mean_spec is not None: + mean_module, y = build_structured_mean( + X_phys, y, mean_spec, names, lowers, uppers + ) + torch.manual_seed(seed) + record = fit_model_variant( + torch.tensor(X_norm, dtype=torch.double), + torch.tensor(y, dtype=torch.double).unsqueeze(-1), + sample_ids=tuple(range(len(X_norm))), + objective_names=(spec.name,), + variant=variant, + seed=seed, + mean_module=mean_module, + ) + models.append(record.model.models[0]) + return ModelListGP(*models) + + +def _normalise(design: Design, X_phys: np.ndarray) -> np.ndarray: + lowers = np.asarray(design.lowers, dtype=float) + uppers = np.asarray(design.uppers, dtype=float) + span = np.where(uppers > lowers, uppers - lowers, 1.0) + return (np.asarray(X_phys, dtype=float) - lowers) / span + + +def _on_grid_mask(design: Design, X_phys: np.ndarray) -> np.ndarray: + """Which observed rows sit exactly on the declared grid. + + The R0 control is a declared off-grid exception (anti_time = 12 against a + 9/11/13... grid). It stays in the GP and in the distance references, but it + cannot take part in grid-index bookkeeping, and it does not need to: an + off-grid point can never collide with a pool candidate by construction. + """ + values = np.asarray(X_phys, dtype=float) + mask = np.ones(len(values), dtype=bool) + for j in range(values.shape[1]): + grid = np.asarray(design.var_array[j], dtype=float) + for i, value in enumerate(values[:, j]): + if not np.any(np.isclose(grid, value, rtol=0.0, atol=1e-9)): + mask[i] = False + return mask + + +def run_r1_ucb( + config: Mapping[str, Any], + observed_X_phys: np.ndarray, + observed_Y_raw: np.ndarray, + *, + n: int | None = None, + seed: int | None = None, +) -> RoundResult: + """UCB-HVI batch with local penalisation. + + ``observed_Y_raw`` holds the MODEL SOURCE values in objective order, not the + final scores: see :func:`model_source_columns`. For this campaign that means + thickness arrives in nanometres. + """ + design = build_design_from_config(dict(config)) + settings = _round_settings(config, "r1") + q = int(settings["batch_size"]) if n is None else int(n) + resolved_seed = ( + int(config.get("reproducibility", {}).get("seed", 0)) if seed is None else seed + ) + transform = build_objective_transform(config) + reference = _reference_point(config, transform.objective_count) + penalization = _penalization(config) + + observed_norm = _normalise(design, observed_X_phys) + model = _fit_models( + config, + np.asarray(observed_X_phys, dtype=float), + observed_norm, + observed_Y_raw, + resolved_seed, + ) + + on_grid = _on_grid_mask(design, observed_X_phys) + pool = sample_discrete_candidate_pool( + design, + int(settings.get("candidate_pool_size", 32768)), + seed=resolved_seed, + observed_phys=np.asarray(observed_X_phys, dtype=float)[on_grid], + row_constraints=constraints_from_config(dict(config), design) or None, + ) + + proposal = propose_ucb_hvi_batch( + pool, + model, + observed_Y_raw, + transform, + reference, + q=q, + beta=float(settings.get("beta", 4.0)), + local_penalization_config=penalization, + mc_samples=int(settings.get("posterior_samples", 256)), + seed=resolved_seed, + moment_method=str(settings.get("moment_method", "monte_carlo")), + ) + + conditions = pd.DataFrame( + np.asarray(proposal.selection.X_phys, dtype=float), columns=list(design.names) + ) + report = validate_batch( + conditions, + design, + expected_count=q, + min_pairwise_distance=penalization.min_batch_distance, + ) + replicates_per = int(settings.get("replicates_per_condition", 1)) + return RoundResult( + round_name="R1", + conditions=conditions, + replicates=expand_replicates( + conditions, replicates=replicates_per, round_name="R1" + ), + diagnostics={ + "method": "ucb_hvi", + "seed": resolved_seed, + "beta": float(settings.get("beta", 4.0)), + "pool_size": pool.size, + "objective_contract": transform.version, + "moment_method": str(settings.get("moment_method", "monte_carlo")), + "off_grid_observations_excluded_from_pool_bookkeeping": int( + (~on_grid).sum() + ), + "validity": report, + }, + ) + + +def run_r2_qlognehvi( + config: Mapping[str, Any], + observed_X_phys: np.ndarray, + observed_Y_raw: np.ndarray, + *, + n: int | None = None, + seed: int | None = None, +) -> RoundResult: + """qLogNEHVI batch. ``observed_Y_raw`` follows the same contract as R1. + + qLogNEHVI is the numerically stable formulation of qNEHVI and is the correct + choice here; it is not a deviation from the brief. + """ + design = build_design_from_config(dict(config)) + settings = _round_settings(config, "r2") + q = int(settings["batch_size"]) if n is None else int(n) + resolved_seed = ( + int(config.get("reproducibility", {}).get("seed", 0)) if seed is None else seed + ) + transform = build_objective_transform(config) + reference = _reference_point(config, transform.objective_count) + penalization = _penalization(config) + + observed_norm = _normalise(design, observed_X_phys) + model = _fit_models( + config, + np.asarray(observed_X_phys, dtype=float), + observed_norm, + observed_Y_raw, + resolved_seed, + ) + + on_grid = _on_grid_mask(design, observed_X_phys) + pool = sample_discrete_candidate_pool( + design, + int(settings.get("candidate_pool_size", 32768)), + seed=resolved_seed, + observed_phys=np.asarray(observed_X_phys, dtype=float)[on_grid], + row_constraints=constraints_from_config(dict(config), design) or None, + ) + + from .objectives import ConfiguredMCMultiOutputObjective + + proposal = propose_qlognehvi_penalized_batch( + pool, + model, + torch.tensor(observed_norm, dtype=torch.double), + ConfiguredMCMultiOutputObjective(transform), + reference, + q=q, + local_penalization_config=penalization, + mc_samples=int(settings.get("mc_samples", 128)), + seed=resolved_seed, + ) + + conditions = pd.DataFrame( + np.asarray(proposal.selection.X_phys, dtype=float), columns=list(design.names) + ) + report = validate_batch( + conditions, + design, + expected_count=q, + min_pairwise_distance=penalization.min_batch_distance, + ) + replicates_per = int(settings.get("replicates_per_condition", 1)) + return RoundResult( + round_name="R2", + conditions=conditions, + replicates=expand_replicates( + conditions, replicates=replicates_per, round_name="R2" + ), + diagnostics={ + "method": "qlognehvi", + "seed": resolved_seed, + "pool_size": pool.size, + "objective_contract": transform.version, + "off_grid_observations_excluded_from_pool_bookkeeping": int( + (~on_grid).sum() + ), + "validity": report, + }, + ) diff --git a/src/mobo_kit/d2d_step2c_config.py b/src/mobo_kit/d2d_step2c_config.py index 6de5118..dad95b5 100644 --- a/src/mobo_kit/d2d_step2c_config.py +++ b/src/mobo_kit/d2d_step2c_config.py @@ -701,7 +701,7 @@ def load_step2c_config(path: str | Path) -> ResolvedStep2CConfig: models = _mapping(raw.get("models"), field="models") expected_models = [ - {"name": "default_current", "type": "existing_default"}, + {"name": "dim_scaled_prior", "type": "existing_default"}, { "name": "conservative", "type": "explicit_conservative", @@ -714,7 +714,7 @@ def load_step2c_config(path: str | Path) -> ResolvedStep2CConfig: "models.variants must preserve default and conservative settings." ) if ( - models.get("primary_for_debug") != "default_current" + models.get("primary_for_debug") != "dim_scaled_prior" or models.get("exact_leave_one_out") is not True or models.get("report_training_posterior_only_as_diagnostic") is not True ): @@ -878,8 +878,8 @@ def load_step2c_config(path: str | Path) -> ResolvedStep2CConfig: refinement_tolerance=tolerance, penalty_variants=penalties, primary_penalty_variant=primary_penalty, - model_variant_names=("default_current", "conservative"), - primary_model_variant="default_current", + model_variant_names=("dim_scaled_prior", "conservative"), + primary_model_variant="dim_scaled_prior", influence_pool_size=influence_pool_size, influence_pool_seed=influence_pool_seed, influence_sample_ids=influence_ids, diff --git a/src/mobo_kit/d2d_step2c_robustness.py b/src/mobo_kit/d2d_step2c_robustness.py index 928c84f..4491d92 100644 --- a/src/mobo_kit/d2d_step2c_robustness.py +++ b/src/mobo_kit/d2d_step2c_robustness.py @@ -58,7 +58,7 @@ ) from .model_validation import ( CONSERVATIVE, - DEFAULT_CURRENT, + DIM_SCALED_PRIOR, ModelFitCache, ModelValidationResult, extract_model_hyperparameters, @@ -1854,7 +1854,7 @@ def _run_observation_influence( config=config, mode_settings=mode_settings, training=training, - model_variant="default_current", + model_variant="dim_scaled_prior", beta=config.primary_beta, bound_policy=config.primary_bound_policy, penalty_label=config.primary_penalty_variant, @@ -1932,7 +1932,7 @@ def _run_observation_influence( config=config, mode_settings=mode_settings, training=training, - model_variant="default_current", + model_variant="dim_scaled_prior", beta=config.primary_beta, bound_policy=config.primary_bound_policy, penalty_label=config.primary_penalty_variant, @@ -2306,7 +2306,7 @@ def _boundary_plot_frame(enrichment: pd.DataFrame) -> pd.DataFrame: def _shortlist_prediction_plot_frame(shortlist: pd.DataFrame) -> pd.DataFrame: rows: list[dict[str, Any]] = [] for _, candidate in shortlist.iterrows(): - for variant_name in ("default_current", "conservative"): + for variant_name in ("dim_scaled_prior", "conservative"): for objective_name in D2D_OBJECTIVE_NAMES: prefix = f"{variant_name}_{objective_name}" rows.append( @@ -3043,7 +3043,7 @@ def _run_d2d_step2c_robustness_resolved( control_sample_ids=config.control_sample_ids, cache=fit_cache, ) - for variant in (DEFAULT_CURRENT, CONSERVATIVE) + for variant in (DIM_SCALED_PRIOR, CONSERVATIVE) ] validations_by_name = {result.variant.name: result for result in validation_results} models = { @@ -3077,7 +3077,7 @@ def _run_d2d_step2c_robustness_resolved( largest_pool = primary_sobol.pools[largest_size] primary_mean, primary_std, primary_analytic, primary_moment_runtime = ( _analytic_numpy_moments( - models["default_current"], + models["dim_scaled_prior"], largest_pool.X_norm, objective_transform=transform, chunk_size=config.score_chunk_size, @@ -3105,7 +3105,7 @@ def _run_d2d_step2c_robustness_resolved( (config.primary_beta, config.primary_bound_policy) ] analytic_mc_frame, analytic_mc_payload = _analytic_mc_comparison( - model=models["default_current"], + model=models["dim_scaled_prior"], pool=largest_pool, training_y=training.Y_objectives, observed_norm=training.X_norm_all, @@ -3121,7 +3121,7 @@ def _run_d2d_step2c_robustness_resolved( beta, bound_policy = key cached = CachedGridScorer( _grid_score_function( - models["default_current"], + models["dim_scaled_prior"], config.design, training.Y_objectives, config, @@ -3151,7 +3151,7 @@ def _run_d2d_step2c_robustness_resolved( run_family="nested_pool_raw", core_run=False, selection=raw_selection, - model_variant="default_current", + model_variant="dim_scaled_prior", pool=pool, pool_hash=primary_sobol.prefix_hashes[size], beta=config.primary_beta, @@ -3174,7 +3174,7 @@ def _run_d2d_step2c_robustness_resolved( config=config, mode_settings=mode_settings, training=training, - model_variant="default_current", + model_variant="dim_scaled_prior", beta=config.primary_beta, bound_policy=config.primary_bound_policy, penalty_label=config.primary_penalty_variant, @@ -3190,7 +3190,7 @@ def _run_d2d_step2c_robustness_resolved( for result in secondary_sobol: pool = result.largest_pool mean, std, _, moment_runtime = _analytic_numpy_moments( - models["default_current"], + models["dim_scaled_prior"], pool.X_norm, objective_transform=transform, chunk_size=config.score_chunk_size, @@ -3206,7 +3206,7 @@ def _run_d2d_step2c_robustness_resolved( secondary_moment_runtime[str(result.scramble_seed)] = moment_runtime + runtime scorer = CachedGridScorer( _grid_score_function( - models["default_current"], + models["dim_scaled_prior"], config.design, training.Y_objectives, config, @@ -3228,7 +3228,7 @@ def _run_d2d_step2c_robustness_resolved( config=config, mode_settings=mode_settings, training=training, - model_variant="default_current", + model_variant="dim_scaled_prior", beta=config.primary_beta, bound_policy=config.primary_bound_policy, penalty_label=config.primary_penalty_variant, @@ -3294,7 +3294,7 @@ def _run_d2d_step2c_robustness_resolved( config=config, mode_settings=mode_settings, training=training, - model_variant="default_current", + model_variant="dim_scaled_prior", beta=config.primary_beta, bound_policy="none", penalty_label=config.primary_penalty_variant, @@ -3315,7 +3315,7 @@ def _run_d2d_step2c_robustness_resolved( config=config, mode_settings=mode_settings, training=training, - model_variant="default_current", + model_variant="dim_scaled_prior", beta=beta, bound_policy=config.primary_bound_policy, penalty_label=config.primary_penalty_variant, @@ -3358,7 +3358,7 @@ def _run_d2d_step2c_robustness_resolved( run_family="local_penalty", core_run=penalty.label in penalty_core_labels, selection=selection, - model_variant="default_current", + model_variant="dim_scaled_prior", pool=converged_pool, pool_hash=converged_pool_hash, beta=config.primary_beta, @@ -3460,7 +3460,7 @@ def _run_d2d_step2c_robustness_resolved( common_pool_hash = primary_sobol.prefix_hashes[influence_size] influence, influence_batches, influence_candidates, influence_predictions = ( _run_observation_influence( - validation=validations_by_name["default_current"], + validation=validations_by_name["dim_scaled_prior"], common_pool=common_pool, common_pool_hash=common_pool_hash, full_pool_scores=baseline_scoring.base_score[:influence_size], @@ -3476,7 +3476,7 @@ def _run_d2d_step2c_robustness_resolved( if batch.run_id.startswith("influence_omit_") } for sample_id in mode_settings.omitted_sample_ids: - fit = validations_by_name["default_current"].loocv.fold_records[sample_id] + fit = validations_by_name["dim_scaled_prior"].loocv.fold_records[sample_id] batch = influence_batch_by_id[int(sample_id)] influence_runtime_rows.append( { diff --git a/src/mobo_kit/main.py b/src/mobo_kit/main.py index a5c9e2e..0c956ff 100644 --- a/src/mobo_kit/main.py +++ b/src/mobo_kit/main.py @@ -73,7 +73,7 @@ def generate_initial_experiments( set_seeds(seed) # Load configuration - with open(config_path, "r") as f: + with open(config_path, "r", encoding="utf-8") as f: config = yaml.safe_load(f) # Build design space through the validated config-to-design path. @@ -241,7 +241,7 @@ def run_mobo_experiment( if config_path is not None: if not os.path.isfile(config_path): raise FileNotFoundError(f"Configuration file not found: {config_path}") - with open(config_path, "r") as f: + with open(config_path, "r", encoding="utf-8") as f: config = yaml.safe_load(f) if propose_candidates: # This executes before campaign CSV parsing, model fitting, or any diff --git a/src/mobo_kit/model_validation.py b/src/mobo_kit/model_validation.py index 6040064..13a01db 100644 --- a/src/mobo_kit/model_validation.py +++ b/src/mobo_kit/model_validation.py @@ -25,6 +25,10 @@ from botorch.models import SingleTaskGP from botorch.models.model_list_gp_regression import ModelListGP from botorch.models.transforms.outcome import Standardize +from botorch.models.utils.gpytorch_modules import ( + get_covar_module_with_dim_scaled_prior, + get_gaussian_likelihood_with_lognormal_prior, +) from gpytorch.constraints import GreaterThan from gpytorch.kernels import MaternKernel, ScaleKernel from gpytorch.likelihoods import GaussianLikelihood @@ -33,9 +37,13 @@ import torch -DEFAULT_CURRENT_NAME = "default_current" +DIM_SCALED_PRIOR_NAME = "dim_scaled_prior" +LEGACY_NO_PRIOR_NAME = "legacy_matern_no_prior" CONSERVATIVE_NAME = "conservative" -DEFAULT_CURRENT_MIN_NOISE = 1.0e-3 +#: BoTorch's MIN_INFERRED_NOISE_LEVEL, the floor that ships with its LogNormal +#: noise prior. The prior, not the floor, is what stops the variance collapse. +DIM_SCALED_PRIOR_MIN_NOISE = 1.0e-4 +LEGACY_NO_PRIOR_MIN_NOISE = 1.0e-3 CONSERVATIVE_MIN_NOISE = 0.01 CONSERVATIVE_MIN_LENGTHSCALE = 0.05 GAUSSIAN_95_Z = 1.959963984540054 @@ -64,17 +72,41 @@ def _seed(value: Any) -> int: @dataclass(frozen=True) class ModelVariantSpec: - """One explicit GP model contract used by Step 2C.""" + """One explicit GP model contract. + + ``use_dim_scaled_prior`` selects BoTorch's dimension-scaled LogNormal + lengthscale prior. Without it, an unregularised ARD kernel fitted to 15 + observations in 10 dimensions drives lengthscales to bimodal extremes + (measured: 0.13 to 3.8e4) and pins the likelihood noise at its floor, which + is interpolation rather than learning. + + ``use_lognormal_noise_prior`` selects BoTorch's ``LogNormal(-4, 1)`` noise + prior. It is required alongside the lengthscale prior, not optional: with a + bare noise floor the fit has a second degenerate mode in which the + outputscale collapses to zero and the model declares the data pure noise. + That mode was observed on 10 of 15 leave-one-out folds of the thickness + score, producing a latent predictive sd of 1e-4 against a fitted noise of + 0.93, 68% interval coverage of 0.133, and a mean NLPD of 3.1e6. + + See docs/GP_MODEL_DECISION.md. + """ name: str min_noise: float min_lengthscale: float | None kernel_name: str = "matern_2.5_ard" + use_dim_scaled_prior: bool = False + use_lognormal_noise_prior: bool = False def __post_init__(self) -> None: - if self.name not in {DEFAULT_CURRENT_NAME, CONSERVATIVE_NAME}: + if self.name not in { + DIM_SCALED_PRIOR_NAME, + LEGACY_NO_PRIOR_NAME, + CONSERVATIVE_NAME, + }: raise ValueError( - "Model variant name must be 'default_current' or 'conservative'." + "Model variant name must be 'dim_scaled_prior', " + "'legacy_matern_no_prior' or 'conservative'." ) noise = _finite_positive(self.min_noise, field_name="min_noise") lengthscale = ( @@ -84,27 +116,56 @@ def __post_init__(self) -> None: ) if self.kernel_name != "matern_2.5_ard": raise ValueError("Only the audited matern_2.5_ard kernel is supported.") - if self.name == DEFAULT_CURRENT_NAME and ( - noise != DEFAULT_CURRENT_MIN_NOISE or lengthscale is not None + if self.name == DIM_SCALED_PRIOR_NAME and ( + noise != DIM_SCALED_PRIOR_MIN_NOISE + or lengthscale is not None + or not self.use_dim_scaled_prior + or not self.use_lognormal_noise_prior ): raise ValueError( - "default_current must preserve the Step 2B noise floor of 1e-3 " - "and must not add a lengthscale floor." + "dim_scaled_prior must use min_noise=1e-4, no lengthscale floor, " + "and BOTH the dimension-scaled lengthscale prior and the " + "LogNormal noise prior." + ) + if self.name == LEGACY_NO_PRIOR_NAME and ( + noise != LEGACY_NO_PRIOR_MIN_NOISE + or lengthscale is not None + or self.use_dim_scaled_prior + or self.use_lognormal_noise_prior + ): + raise ValueError( + "legacy_matern_no_prior must preserve the retired Step 2B contract: " + "min_noise=1e-3, no lengthscale floor, and no priors." ) if self.name == CONSERVATIVE_NAME and ( noise != CONSERVATIVE_MIN_NOISE or lengthscale != CONSERVATIVE_MIN_LENGTHSCALE + or self.use_dim_scaled_prior + or self.use_lognormal_noise_prior ): raise ValueError( - "conservative must use min_noise=0.01 and min_lengthscale=0.05." + "conservative must use min_noise=0.01, min_lengthscale=0.05 and " + "no priors." ) object.__setattr__(self, "min_noise", noise) object.__setattr__(self, "min_lengthscale", lengthscale) -DEFAULT_CURRENT = ModelVariantSpec( - DEFAULT_CURRENT_NAME, - min_noise=DEFAULT_CURRENT_MIN_NOISE, +#: The campaign default. Matern 2.5 ARD with BoTorch's dimension-scaled +#: LogNormal lengthscale prior and its LogNormal(-4, 1) noise prior. Both are +#: required; see ModelVariantSpec for what happens with only the former. +DIM_SCALED_PRIOR = ModelVariantSpec( + DIM_SCALED_PRIOR_NAME, + min_noise=DIM_SCALED_PRIOR_MIN_NOISE, + min_lengthscale=None, + use_dim_scaled_prior=True, + use_lognormal_noise_prior=True, +) +#: Retired. The prior-free contract used through Step 2C, kept only so archived +#: runs remain reproducible and interpretable. Do not select for new work. +LEGACY_NO_PRIOR = ModelVariantSpec( + LEGACY_NO_PRIOR_NAME, + min_noise=LEGACY_NO_PRIOR_MIN_NOISE, min_lengthscale=None, ) CONSERVATIVE = ModelVariantSpec( @@ -113,11 +174,16 @@ def __post_init__(self) -> None: min_lengthscale=CONSERVATIVE_MIN_LENGTHSCALE, ) +#: What new runs get unless a caller deliberately asks for something else. +PRIMARY_VARIANT = DIM_SCALED_PRIOR + def model_variant_spec(name: str) -> ModelVariantSpec: - """Return one of the two fixed Step 2C model contracts.""" - if name == DEFAULT_CURRENT_NAME: - return DEFAULT_CURRENT + """Return one of the fixed model contracts by name.""" + if name == DIM_SCALED_PRIOR_NAME: + return DIM_SCALED_PRIOR + if name == LEGACY_NO_PRIOR_NAME: + return LEGACY_NO_PRIOR if name == CONSERVATIVE_NAME: return CONSERVATIVE raise ValueError(f"Unsupported model variant {name!r}.") @@ -440,32 +506,115 @@ def _warning_rows( ] +class SignalCollapseError(RuntimeError): + """The fitted outputscale went to zero; the model has no signal component.""" + + +#: A fit whose latent (signal) sd falls below this multiple of the fitted noise +#: sd has explained the data as pure noise. Acquisition reads the latent +#: posterior, so such a model produces a near-deterministic score surface and a +#: meaningless exploration term, even though its predictive intervals look fine +#: because the inflated noise hides the collapse. +MINIMUM_LATENT_TO_NOISE_SD_RATIO = 1.0e-2 + + +def _assert_signal_not_collapsed( + gp: SingleTaskGP, + X: torch.Tensor, + *, + variant: ModelVariantSpec, + objective_index: int, + objective_name: str, + fit_key: str, + omitted_sample_id: Hashable | None, + fit_warnings: Sequence[ModelFitWarning], +) -> None: + """Fail loudly when the outputscale has collapsed to zero. + + This is a numerical guard, not a configuration check. Naming a variant + correctly cannot prevent a degenerate optimum: the same contract refitted on + different data -- more observations, replicate-derived ``train_Yvar``, a new + round -- can land there again. So the assertion runs on every fit. + + Observed instance: 10 of 15 leave-one-out folds of the thickness score fitted + a noise of 0.93 against a latent sd of 1e-4, a ratio of ~1e-4. + """ + with torch.no_grad(): + latent_sd = float(gp.posterior(X).variance.clamp_min(0.0).sqrt().min()) + noise_sd = float(gp.likelihood.noise.detach().reshape(-1)[0] ** 0.5) + if noise_sd <= 0.0: + return + ratio = latent_sd / noise_sd + if ratio < MINIMUM_LATENT_TO_NOISE_SD_RATIO: + raise ModelFitError( + variant_name=variant.name, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + objective_index=objective_index, + objective_name=objective_name, + stage="signal_collapse_guard", + cause=SignalCollapseError( + f"minimum latent sd {latent_sd:.3e} is {ratio:.3e} of the fitted " + f"noise sd {noise_sd:.3e}, below the " + f"{MINIMUM_LATENT_TO_NOISE_SD_RATIO:g} floor. The model has " + "explained the data as pure noise: its posterior mean is " + "effectively constant and its acquisition scores are meaningless. " + "Predictive intervals do NOT reveal this, because the inflated " + "noise masks the collapse." + ), + fit_warnings=tuple(fit_warnings), + ) + + def _build_single_task_gp( X: torch.Tensor, y: torch.Tensor, variant: ModelVariantSpec, + mean_module: Any = None, ) -> SingleTaskGP: - lengthscale_constraint = ( - None - if variant.min_lengthscale is None - else GreaterThan(variant.min_lengthscale) - ) - kernel_kwargs: dict[str, Any] = { - "nu": 2.5, - "ard_num_dims": X.shape[1], - } - if lengthscale_constraint is not None: - kernel_kwargs["lengthscale_constraint"] = lengthscale_constraint - base_kernel = MaternKernel(**kernel_kwargs) + if variant.use_dim_scaled_prior: + # BoTorch's dimension-scaled LogNormal lengthscale prior, the same one + # SingleTaskGP applies by default when no covar_module is supplied. We + # still pass an explicit module so the ScaleKernel wrapper (which the + # hyperparameter readout and plots depend on) stays in place. + base_kernel = get_covar_module_with_dim_scaled_prior( + ard_num_dims=X.shape[1], + use_rbf_kernel=False, + ) + else: + lengthscale_constraint = ( + None + if variant.min_lengthscale is None + else GreaterThan(variant.min_lengthscale) + ) + kernel_kwargs: dict[str, Any] = { + "nu": 2.5, + "ard_num_dims": X.shape[1], + } + if lengthscale_constraint is not None: + kernel_kwargs["lengthscale_constraint"] = lengthscale_constraint + base_kernel = MaternKernel(**kernel_kwargs) covar_module = ScaleKernel(base_kernel) - likelihood = GaussianLikelihood(noise_constraint=GreaterThan(variant.min_noise)) - return SingleTaskGP( + if variant.use_lognormal_noise_prior: + # LogNormal(-4, 1) with a GreaterThan(1e-4) floor. Without the prior the + # marginal likelihood is free to drive the outputscale to zero and call + # the data pure noise, which yields a degenerate near-zero latent + # variance. The floor alone does not prevent that. + likelihood = get_gaussian_likelihood_with_lognormal_prior() + else: + likelihood = GaussianLikelihood(noise_constraint=GreaterThan(variant.min_noise)) + model = SingleTaskGP( X, y, covar_module=covar_module, likelihood=likelihood, outcome_transform=Standardize(m=1), ) + if mean_module is not None: + # a frozen structured mean: its coefficients are registered as buffers, + # so the marginal likelihood still fits only the GP hyperparameters + model.mean_module = mean_module + return model def fit_model_variant( @@ -480,6 +629,7 @@ def fit_model_variant( omitted_sample_id: Hashable | None = None, cohort_fingerprint: str | None = None, cache: ModelFitCache | None = None, + mean_module: Any = None, ) -> FittedModelRecord: """Fit one strict independent GP per objective and return an audit record.""" ids, names = _validate_dataset(X, Y, sample_ids, objective_names, minimum_rows=2) @@ -493,6 +643,15 @@ def fit_model_variant( cohort_hash = training_hash if cohort_fingerprint is None else cohort_fingerprint if not isinstance(cohort_hash, str) or not cohort_hash.strip(): raise ValueError("cohort_fingerprint must be None or a non-empty string.") + if mean_module is not None and cache is not None: + # the cache key is built from the data and the variant, not the mean + # module, so a cached fit could be returned for a different trend. + # Refuse rather than silently serve the wrong model. + raise ValueError( + "A structured mean_module cannot be combined with a ModelFitCache: " + "the cache key does not capture the mean, so a fold could be served " + "a fit built from a different trend." + ) cache_key = ModelFitCacheKey( variant_name=variant.name, cohort_fingerprint=cohort_hash, @@ -523,7 +682,10 @@ def fit_model_variant( with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") gp = _build_single_task_gp( - X, Y[:, objective_index : objective_index + 1], variant + X, + Y[:, objective_index : objective_index + 1], + variant, + mean_module=mean_module, ) fit_warning_rows.extend( _warning_rows( @@ -599,6 +761,16 @@ def fit_model_variant( ) from exc gp.eval() gp.likelihood.eval() + _assert_signal_not_collapsed( + gp, + X, + variant=variant, + objective_index=objective_index, + objective_name=objective_name, + fit_key=fit_key, + omitted_sample_id=omitted_sample_id, + fit_warnings=fit_warning_rows, + ) models.append(gp) record = FittedModelRecord( @@ -987,7 +1159,11 @@ def validate_model_variant( __all__ = [ "CONSERVATIVE", - "DEFAULT_CURRENT", + "MINIMUM_LATENT_TO_NOISE_SD_RATIO", + "SignalCollapseError", + "DIM_SCALED_PRIOR", + "LEGACY_NO_PRIOR", + "PRIMARY_VARIANT", "ExactLOOCVResult", "FittedModelRecord", "HyperparameterRecord", diff --git a/src/mobo_kit/objectives.py b/src/mobo_kit/objectives.py index 5847b21..9725953 100644 --- a/src/mobo_kit/objectives.py +++ b/src/mobo_kit/objectives.py @@ -38,6 +38,11 @@ class ObjectiveSpec: name: str goal: Goal transform: TransformName + #: What the MODEL emits, relative to the physical quantity the utility is + #: defined on. ``log`` means the GP was fitted in log space (see + #: ``structured_mean``), so a model output must be exponentiated before the + #: utility applies, and a model *posterior* is lognormal rather than normal. + model_link: Literal["identity", "log"] = "identity" source_column: str | None = None lower_anchor: float | None = None upper_anchor: float | None = None @@ -183,7 +188,26 @@ def transform(self, Y: torch.Tensor) -> torch.Tensor: raise ValueError("Y must contain only finite values.") outputs: list[torch.Tensor] = [] for index, spec in enumerate(self.specs): - raw = Y[..., index] + outputs.append(self._utility(Y[..., index], spec)) + transformed = torch.stack(outputs, dim=-1) + if transformed.shape != Y.shape: + raise RuntimeError("Internal objective transform shape error.") + return transformed + + def _utility(self, model_output: torch.Tensor, spec: ObjectiveSpec): + """Utility for one objective, given that objective's MODEL output. + + The link decode happens exactly here and nowhere else. Quadrature and + Monte-Carlo paths must both route through this, or one of them will + exponentiate twice. + """ + raw = torch.exp(model_output) if spec.model_link == "log" else model_output + return self._utility_from_physical(raw, spec) + + @staticmethod + def _utility_from_physical(raw: torch.Tensor, spec: ObjectiveSpec): + """Utility for one objective from its PHYSICAL value (link already undone).""" + if True: if spec.transform == "identity": utility = raw elif spec.transform == "affine": @@ -203,14 +227,165 @@ def transform(self, Y: torch.Tensor) -> torch.Tensor: target = raw.new_tensor(spec.target) scale = raw.new_tensor(spec.scale) utility = -(raw - target).abs() / scale - outputs.append(utility) - transformed = torch.stack(outputs, dim=-1) - if transformed.shape != Y.shape: - raise RuntimeError("Internal objective transform shape error.") - return transformed + return utility __call__ = transform + def expected_transform( + self, mean: torch.Tensor, variance: torch.Tensor + ) -> torch.Tensor: + """Expected utility ``E[transform(Y)]`` for ``Y ~ N(mean, variance)``. + + Use this when the GP is trained on a *raw* measurement and the utility is + a nonlinear function of it. Applying :meth:`transform` to the posterior + mean is wrong in that case: it is biased by Jensen's inequality and it + discards the posterior variance entirely, which for a target-seeking + utility is precisely the information that matters. + + ``identity`` and ``affine`` are linear, so their expectation is just the + transform of the mean. The two target transforms are nonlinear and have + exact closed forms: + + * ``gaussian_target`` with target ``c`` and width ``s``:: + + E = s / sqrt(s^2 + v) * exp(-0.5 * (mu - c)^2 / (s^2 + v)) + + At ``mu == c`` this decays from 1 as the posterior widens, so a + confidently on-target candidate outranks an uncertain one. + + * ``negative_absolute_target`` uses the folded-normal mean. + + Both reduce to :meth:`transform` as ``variance -> 0``. + """ + if not isinstance(mean, torch.Tensor) or not isinstance(variance, torch.Tensor): + raise TypeError("mean and variance must be torch.Tensors.") + if not mean.is_floating_point() or not variance.is_floating_point(): + raise TypeError("mean and variance must use a floating dtype.") + if mean.shape != variance.shape: + raise ValueError( + f"mean and variance must share a shape; got {tuple(mean.shape)} " + f"and {tuple(variance.shape)}." + ) + if mean.ndim < 1 or mean.shape[-1] != self.objective_count: + raise ValueError( + f"mean final dimension must be {self.objective_count}; " + f"got shape {tuple(mean.shape)}." + ) + if not torch.isfinite(mean).all() or not torch.isfinite(variance).all(): + raise ValueError("mean and variance must contain only finite values.") + if (variance < 0).any(): + raise ValueError("variance must be non-negative.") + + outputs: list[torch.Tensor] = [] + for index, spec in enumerate(self.specs): + mu = mean[..., index] + var = variance[..., index] + if spec.model_link == "log": + # the posterior is lognormal, so no Gaussian closed form applies; + # integrate in log space by quadrature + utility = self._quadrature_expectation(mu, var, spec, nodes=20) + elif spec.transform in {"identity", "affine"}: + # linear in the physical value, and the link is identity in this + # branch, so E[f(Y)] = f(E[Y]) + utility = self._utility_from_physical(mu, spec) + elif spec.transform == "gaussian_target": + target = mu.new_tensor(spec.target) + s2 = mu.new_tensor(spec.sigma) ** 2 + denom = s2 + var + utility = torch.sqrt(s2 / denom) * torch.exp( + -0.5 * (mu - target).square() / denom + ) + else: + target = mu.new_tensor(spec.target) + scale = mu.new_tensor(spec.scale) + sd = var.clamp_min(0.0).sqrt() + delta = mu - target + # folded-normal mean; the sd == 0 branch degenerates to |delta| + safe_sd = torch.where(sd > 0, sd, torch.ones_like(sd)) + folded = safe_sd * np.sqrt(2.0 / np.pi) * torch.exp( + -0.5 * (delta / safe_sd).square() + ) + delta * torch.erf(delta / (safe_sd * np.sqrt(2.0))) + folded = torch.where(sd > 0, folded, delta.abs()) + utility = -folded / scale + outputs.append(utility) + expected = torch.stack(outputs, dim=-1) + if expected.shape != mean.shape: + raise RuntimeError("Internal expected-objective shape error.") + return expected + + def _quadrature_expectation( + self, + log_mean: torch.Tensor, + log_variance: torch.Tensor, + spec: ObjectiveSpec, + *, + nodes: int, + ) -> torch.Tensor: + """E[utility] for one log-link objective, by Gauss-Hermite in log space. + + E[g(Y)] = int g(exp(z)) N(z; m, s^2) dz + ~ (1/sqrt(pi)) sum_i w_i g(exp(m + sqrt(2) s x_i)) + + Exact for the Gaussian weight, deterministic, differentiable, and + cheaper than sampling. Moment-matching the lognormal to a Gaussian and + reusing the closed form is ~500x less accurate here, and its bias + changes sign across the range, which reorders candidates rather than + merely shifting them. + """ + raw_nodes, raw_weights = np.polynomial.hermite.hermgauss(nodes) + abscissa = log_mean.new_tensor(raw_nodes) + weights = log_mean.new_tensor(raw_weights) / float(np.sqrt(np.pi)) + sd = log_variance.clamp_min(0.0).sqrt() + shape = (-1, *([1] * log_mean.ndim)) + shifted = log_mean.unsqueeze(0) + np.sqrt(2.0) * sd.unsqueeze( + 0 + ) * abscissa.view(shape) + utilities = self._utility_from_physical(torch.exp(shifted), spec) + return (utilities * weights.view(shape)).sum(dim=0) + + def expected_transform_lognormal( + self, + log_mean: torch.Tensor, + log_variance: torch.Tensor, + *, + nodes: int = 20, + ) -> torch.Tensor: + """Expected utility treating EVERY objective as log-link. + + Prefer :meth:`expected_transform`, which dispatches per objective from + each spec's ``model_link``. This method is kept for the single-objective + case where the caller knows the posterior is lognormal. + """ + if not isinstance(log_mean, torch.Tensor) or not isinstance( + log_variance, torch.Tensor + ): + raise TypeError("log_mean and log_variance must be torch.Tensors.") + if not log_mean.is_floating_point() or not log_variance.is_floating_point(): + raise TypeError("log_mean and log_variance must use a floating dtype.") + if log_mean.shape != log_variance.shape: + raise ValueError( + f"log_mean and log_variance must share a shape; got " + f"{tuple(log_mean.shape)} and {tuple(log_variance.shape)}." + ) + if log_mean.ndim < 1 or log_mean.shape[-1] != self.objective_count: + raise ValueError( + f"log_mean final dimension must be {self.objective_count}; " + f"got shape {tuple(log_mean.shape)}." + ) + if not torch.isfinite(log_mean).all() or not torch.isfinite(log_variance).all(): + raise ValueError("log_mean and log_variance must be finite.") + if (log_variance < 0).any(): + raise ValueError("log_variance must be non-negative.") + if isinstance(nodes, bool) or not isinstance(nodes, int) or nodes < 2: + raise ValueError("nodes must be an integer of at least 2.") + columns = [ + self._quadrature_expectation( + log_mean[..., index], log_variance[..., index], spec, nodes=nodes + ) + for index, spec in enumerate(self.specs) + ] + return torch.stack(columns, dim=-1) + class ConfiguredMCMultiOutputObjective(MCMultiOutputObjective): """BoTorch MC objective backed by the exact same `ObjectiveTransform`.""" diff --git a/src/mobo_kit/step2c_artifacts.py b/src/mobo_kit/step2c_artifacts.py index bdbb076..53369c9 100644 --- a/src/mobo_kit/step2c_artifacts.py +++ b/src/mobo_kit/step2c_artifacts.py @@ -1214,7 +1214,7 @@ def _validate_manifest_provenance( model_variants = manifest["model_variants"] if not isinstance(model_variants, list) or { item.get("name") for item in model_variants if isinstance(item, Mapping) - } != {"default_current", "conservative"}: + } != {"dim_scaled_prior", "conservative"}: raise _error("run_manifest.json must record both GP model variants.") _validated_sha256( manifest["influence_common_pool_hash"], field="influence_common_pool_hash" diff --git a/src/mobo_kit/structured_mean.py b/src/mobo_kit/structured_mean.py new file mode 100644 index 0000000..8d14a18 --- /dev/null +++ b/src/mobo_kit/structured_mean.py @@ -0,0 +1,284 @@ +"""Physics-informed mean functions for small-data GPs. + +With 15 observations in 10 dimensions a zero-mean GP spends most of its capacity +rediscovering a trend that is already known from process physics. Giving it that +trend as a mean function, and letting the GP model only the residual, roughly +doubles the leave-one-out fit on this campaign's data. + +The two objectives want different shapes, which is the point of making this +declarative rather than hard-coded: + +* **thickness** -- ``log T ~ log(speed_1) + log(precur_conc)``. Spin-coating + theory gives ``T ~ omega^-0.5``; the measured exponent is -0.38. Neither term + alone is worth much (LOO R2 +0.159 and +0.187); the *pair* carries the signal + (+0.449). Modelled in log space, so the response is lognormal. +* **optoelectronic** -- a single linear term on ``anneal_temp`` and nothing else + (LOO R2 +0.244). Every addition tested made it worse, and an Arrhenius ``1/T`` + form bought nothing over plain linear temperature (+0.226). + +Do not assume the two-term shape generalises: these are opposite patterns. + +The linear coefficients are refit on the training rows of every fold, so +cross-validation stays honest. Feature *choice* is fixed in configuration from +physics, before fitting -- it is not selected against the outcome. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Literal, Mapping, Sequence + +import gpytorch +import numpy as np +import torch + +__all__ = [ + "MeanFeature", + "StructuredMeanSpec", + "StructuredPosterior", + "StructuredMean", + "build_structured_mean", + "fit_structured_mean", + "mean_spec_from_config", +] + +Link = Literal["identity", "log"] + + +@dataclass(frozen=True) +class MeanFeature: + """One column of the linear mean's design matrix.""" + + column: str + transform: Link = "identity" + + def evaluate(self, values: np.ndarray) -> np.ndarray: + if self.transform == "identity": + return values + if self.transform == "log": + if np.any(values <= 0): + raise ValueError( + f"Mean feature {self.column!r} uses a log transform but the " + "column contains non-positive values." + ) + return np.log(values) + raise ValueError(f"Unsupported mean-feature transform {self.transform!r}.") + + +@dataclass(frozen=True) +class StructuredMeanSpec: + """A linear trend removed before GP fitting and added back after. + + ``response`` is the space the GP works in. ``log`` means the GP models + ``log y``, which makes ``y`` lognormal -- the utility expectation must then + use Gauss-Hermite quadrature, not the Gaussian closed form. + """ + + response: Link + features: tuple[MeanFeature, ...] + + def __post_init__(self) -> None: + if self.response not in ("identity", "log"): + raise ValueError(f"Unsupported response link {self.response!r}.") + if not self.features: + raise ValueError("A structured mean needs at least one feature.") + names = [feature.column for feature in self.features] + if len(set(names)) != len(names): + raise ValueError(f"Mean features must be unique; got {names}.") + + def design_matrix( + self, X_phys: np.ndarray, input_names: Sequence[str] + ) -> np.ndarray: + columns = [] + for feature in self.features: + try: + index = list(input_names).index(feature.column) + except ValueError as exc: + raise ValueError( + f"Mean feature {feature.column!r} is not a declared input." + ) from exc + columns.append(feature.evaluate(np.asarray(X_phys, float)[:, index])) + return np.column_stack(columns) + + +@dataclass(frozen=True) +class StructuredPosterior: + """Posterior in the response space, plus the link needed to interpret it. + + When ``link == "log"`` these are the mean and variance of ``log y``, so the + utility expectation must integrate a lognormal. + """ + + mean: np.ndarray + variance: np.ndarray + link: Link + + +def _ols(F: np.ndarray, target: np.ndarray) -> np.ndarray: + """Least squares with an intercept, returned as coefficients on [1, F].""" + design = np.column_stack([np.ones(len(F)), F]) + coefficients, *_ = np.linalg.lstsq(design, target, rcond=None) + return coefficients + + +def fit_structured_mean( + X_phys: np.ndarray, + y: np.ndarray, + spec: StructuredMeanSpec, + input_names: Sequence[str], +) -> tuple[np.ndarray, np.ndarray]: + """Return the linear-mean coefficients and the residuals to hand the GP. + + Call this with the TRAINING rows only. Refitting per fold is what keeps + cross-validation honest; fitting once on everything and holding it fixed + leaks the held-out value into the trend. + """ + values = np.asarray(y, dtype=float) + if spec.response == "log": + if np.any(values <= 0): + raise ValueError("A log response requires strictly positive observations.") + target = np.log(values) + else: + target = values + F = spec.design_matrix(X_phys, input_names) + coefficients = _ols(F, target) + fitted = np.column_stack([np.ones(len(F)), F]) @ coefficients + return coefficients, target - fitted + + +def apply_structured_mean( + coefficients: np.ndarray, + X_phys: np.ndarray, + spec: StructuredMeanSpec, + input_names: Sequence[str], + residual_mean: np.ndarray, + residual_variance: np.ndarray, +) -> StructuredPosterior: + """Add the linear trend back to a GP residual posterior.""" + F = spec.design_matrix(X_phys, input_names) + trend = np.column_stack([np.ones(len(F)), F]) @ coefficients + return StructuredPosterior( + mean=np.asarray(residual_mean, float) + trend, + variance=np.asarray(residual_variance, float), + link=spec.response, + ) + + +def mean_spec_from_config(entry: Mapping[str, Any]) -> StructuredMeanSpec | None: + """Build a spec from one objective's ``mean_function`` block, if present.""" + block = entry.get("mean_function") + if block is None: + return None + if not isinstance(block, Mapping): + raise ValueError("mean_function must be a mapping.") + raw_features = block.get("features") + if not isinstance(raw_features, Sequence) or not raw_features: + raise ValueError("mean_function.features must be a non-empty list.") + features = [] + for item in raw_features: + if isinstance(item, str): + features.append(MeanFeature(item)) + elif isinstance(item, Mapping): + features.append( + MeanFeature(str(item["column"]), str(item.get("transform", "identity"))) + ) + else: + raise ValueError("Each mean feature must be a string or a mapping.") + return StructuredMeanSpec( + response=str(block.get("response", "identity")), + features=tuple(features), + ) + + +# --------------------------------------------------------------------------- # +# as a GPyTorch mean module +# --------------------------------------------------------------------------- # + + +class StructuredMean(gpytorch.means.Mean): + """The linear trend as a GP mean module, with the coefficients frozen. + + This is the two-stage pipeline -- OLS detrend, zero-mean GP on the residual -- + expressed as a single model. ``posterior()`` is then correct by + construction: there is no trend to add back afterwards and therefore no code + path that can forget to. + + Coefficients are registered as **buffers, not parameters**, so the marginal + likelihood fits only the GP hyperparameters and leaves the OLS fit alone. + + The module operates in the model's *standardized* outcome space, because + ``SingleTaskGP`` is built with ``Standardize(m=1)``. Getting that wrong is + silent: the trend comes out shifted and scaled, and the model still looks + plausible. :func:`build_structured_mean` handles the conversion. + """ + + def __init__( + self, + lowers: torch.Tensor, + uppers: torch.Tensor, + feature_columns: Sequence[int], + feature_logs: Sequence[bool], + coefficients: torch.Tensor, + bias: torch.Tensor, + ) -> None: + super().__init__() + self.register_buffer("lowers", lowers) + self.register_buffer("uppers", uppers) + self.register_buffer("coefficients", coefficients) + self.register_buffer("bias", bias) + self.feature_columns = tuple(int(c) for c in feature_columns) + self.feature_logs = tuple(bool(f) for f in feature_logs) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # the GP sees normalized inputs; the trend is defined on physical ones + physical = self.lowers + x * (self.uppers - self.lowers) + columns = [] + for position, use_log in zip(self.feature_columns, self.feature_logs): + value = physical[..., position] + columns.append(torch.log(value) if use_log else value) + features = torch.stack(columns, dim=-1) + return (features * self.coefficients).sum(dim=-1) + self.bias + + +def build_structured_mean( + X_phys: np.ndarray, + y: np.ndarray, + spec: StructuredMeanSpec, + input_names: Sequence[str], + lowers: np.ndarray, + uppers: np.ndarray, +) -> tuple["StructuredMean", np.ndarray]: + """Fit the trend on these rows and return it as a mean module. + + Returns the module plus the response-space target the GP should train on + (``log y`` for a log response, ``y`` otherwise). Pass only TRAINING rows; + refitting per fold is what keeps cross-validation honest. + """ + values = np.asarray(y, dtype=float) + if spec.response == "log": + if np.any(values <= 0): + raise ValueError("A log response requires strictly positive observations.") + target = np.log(values) + else: + target = values + + coefficients = _ols(spec.design_matrix(X_phys, input_names), target) + intercept, slopes = float(coefficients[0]), coefficients[1:] + + # SingleTaskGP standardizes the outcome, so express the trend in that space: + # m_std(x) = (m_raw(x) - mu) / sigma + mu = float(np.mean(target)) + sigma = float(np.std(target, ddof=1)) + if not np.isfinite(sigma) or sigma <= 0: + sigma = 1.0 + + names = list(input_names) + module = StructuredMean( + lowers=torch.tensor(np.asarray(lowers, float), dtype=torch.double), + uppers=torch.tensor(np.asarray(uppers, float), dtype=torch.double), + feature_columns=[names.index(f.column) for f in spec.features], + feature_logs=[f.transform == "log" for f in spec.features], + coefficients=torch.tensor(slopes / sigma, dtype=torch.double), + bias=torch.tensor((intercept - mu) / sigma, dtype=torch.double), + ) + return module, target diff --git a/src/mobo_kit/workbook_io.py b/src/mobo_kit/workbook_io.py new file mode 100644 index 0000000..58cb398 --- /dev/null +++ b/src/mobo_kit/workbook_io.py @@ -0,0 +1,347 @@ +"""Read the campaign workbook, write candidate sheets back. + +The experimentalist's side of the loop: open ``Summary Table.xlsx``, fill in ten +inputs and the measurement columns, press one button, get a new sheet of +conditions to run. + +Three rules this module keeps. + +**The source workbook is never opened for writing.** Candidate sheets go to a +sibling file, ``_R1_Candidates.xlsx``. + +That is not the original plan, which was to add sheets to ``Summary Table.xlsx`` +itself. It changed because of a measured fact: **openpyxl discards cached +formula values on save.** ``Uniformity score`` is a formula column +(``=L2*N2*O2``), so a single openpyxl round-trip turns it -- and every other +formula column -- into ``None`` for any reader that is not Excel, including this +one. Verified directly: Z2:Z4 read ``[0.657, 0.587, 0.561]`` before a save that +only added an empty sheet, and ``[None, None, None]`` after. + +Writing beside the workbook keeps the experimentalist's one-button flow (they +open the new file, fill it in, press the button again) and makes the read-only +invariant structural rather than merely asserted. + +**Which columns to collect comes from the config, not from here.** Thickness +trains on nanometres, not on its score, so ``R1_Candidates`` needs an entry +column for ``Thickness (avg)``. Driving that off ``model_source_columns(config)`` +means a future objective change updates the sheet automatically instead of +silently leaving the next round without its data. + +**Round detection is fail-closed.** A partially scored sheet is refused with a +plain sentence rather than being guessed at. +""" + +from __future__ import annotations + +import hashlib +import shutil +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any, Mapping, Sequence + +import numpy as np +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill +from openpyxl.utils import get_column_letter + +from .campaign import model_source_columns + +__all__ = [ + "CandidateSheetError", + "RoundState", + "WorkbookContents", + "backup_workbook", + "candidate_workbook_path", + "detect_round", + "read_campaign_workbook", + "sheet_name_for_round", + "workbook_digest", + "write_candidate_sheet", +] + +SOURCE_SHEET = "Sheet1" +SAMPLE_COLUMN = "Sample number" +ENTRY_FILL = PatternFill("solid", fgColor="FFF2CC") +HEADER_FONT = Font(bold=True) + + +class CandidateSheetError(RuntimeError): + """The workbook is not in a state this tool can act on.""" + + +@dataclass(frozen=True) +class WorkbookContents: + """Everything read out of the source sheet.""" + + inputs: pd.DataFrame + """Physical input values, columns in the config's declared order.""" + model_values: pd.DataFrame + """The columns the GP trains on, in objective order.""" + sample_ids: tuple[int, ...] + digest: str + + @property + def n_rows(self) -> int: + return len(self.inputs) + + +@dataclass(frozen=True) +class RoundState: + """Which round should be generated next, and why.""" + + next_round: str | None + reason: str + scored_rows: int = 0 + total_rows: int = 0 + + +def sheet_name_for_round(round_name: str) -> str: + return f"{round_name.upper()}_Candidates" + + +def workbook_digest(path: str | Path) -> str: + """SHA-256 of the whole file, for the unchanged-source proof.""" + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1 << 20), b""): + digest.update(chunk) + return digest.hexdigest() + + +def backup_workbook(path: str | Path, *, timestamp: str | None = None) -> Path: + """Copy the workbook next to itself before any write.""" + source = Path(path) + stamp = timestamp or datetime.now().strftime("%Y%m%d_%H%M%S") + destination = source.with_name(f"{source.stem}_backup_{stamp}{source.suffix}") + shutil.copy2(source, destination) + return destination + + +def _header_positions(sheet) -> dict[str, int]: + header = next(sheet.iter_rows(min_row=1, max_row=1, values_only=True)) + positions: dict[str, int] = {} + for index, value in enumerate(header): + if value is None: + continue + name = str(value).strip() + # duplicate headers exist in this workbook; first occurrence wins and the + # rest stay reachable by position + positions.setdefault(name, index) + # headers carry their unit inline ("precur_vol (uL)") while the config + # declares name and unit separately; accept both spellings + bare = name.split("(")[0].strip() + if bare and bare != name: + positions.setdefault(bare, index) + return positions + + +def read_campaign_workbook( + path: str | Path, config: Mapping[str, Any] +) -> WorkbookContents: + """Read measured rows, stopping at the first blank sample number. + + Rows below the data block are notes, not observations. + """ + workbook = load_workbook(Path(path), data_only=True, read_only=False) + if SOURCE_SHEET not in workbook.sheetnames: + raise CandidateSheetError( + f"Expected a sheet named {SOURCE_SHEET!r}; found " + f"{workbook.sheetnames}. If it was renamed, rename it back." + ) + sheet = workbook[SOURCE_SHEET] + positions = _header_positions(sheet) + + input_names = [item["name"] for item in config["inputs"]] + source_columns = list(model_source_columns(config)) + missing = [ + name + for name in [SAMPLE_COLUMN, *input_names, *source_columns] + if name not in positions + ] + if missing: + raise CandidateSheetError( + f"{SOURCE_SHEET} is missing required column(s): {missing}. " + "The optimizer trains on these, so it cannot proceed without them." + ) + + rows = [] + for row in sheet.iter_rows(min_row=2, values_only=True): + if row[positions[SAMPLE_COLUMN]] is None: + break + rows.append(row) + if not rows: + raise CandidateSheetError(f"{SOURCE_SHEET} contains no measured rows.") + + def column(name: str) -> list[Any]: + return [row[positions[name]] for row in rows] + + return WorkbookContents( + inputs=pd.DataFrame( + {name: pd.to_numeric(column(name), errors="coerce") for name in input_names} + ), + model_values=pd.DataFrame( + { + name: pd.to_numeric(column(name), errors="coerce") + for name in source_columns + } + ), + sample_ids=tuple(int(value) for value in column(SAMPLE_COLUMN)), + digest=workbook_digest(path), + ) + + +def detect_round(path: str | Path, config: Mapping[str, Any]) -> RoundState: + """Decide which round to generate. Fail closed on a partial sheet.""" + source_columns = list(model_source_columns(config)) + + for round_name, following in (("R1", "R2"), ("R2", None)): + candidate_path = candidate_workbook_path(path, round_name) + name = candidate_path.name + if not candidate_path.exists(): + return RoundState(round_name, f"{name} does not exist yet.") + sheet = load_workbook(candidate_path, data_only=True)[ + sheet_name_for_round(round_name) + ] + positions = _header_positions(sheet) + missing = [c for c in source_columns if c not in positions] + if missing: + raise CandidateSheetError( + f"{name} is missing entry column(s) {missing}. It was probably " + "created by an older version; delete the sheet and regenerate it." + ) + data = [ + row + for row in sheet.iter_rows(min_row=2, values_only=True) + if any(value is not None for value in row) + ] + filled = [ + all(row[positions[c]] is not None for c in source_columns) for row in data + ] + scored, total = sum(filled), len(filled) + if total and scored == 0: + return RoundState( + None, + f"{name} exists but no results have been entered yet. Run those " + f"{total} conditions and fill in {', '.join(source_columns)}.", + scored, + total, + ) + if scored < total: + return RoundState( + None, + f"{name} is partly filled in: {scored} of {total} rows have all " + "measurements. Complete the remaining rows, or clear them, then " + "try again.", + scored, + total, + ) + if following is None: + return RoundState( + None, + "R1 and R2 are both complete. The campaign is finished.", + scored, + total, + ) + return RoundState(None, "Nothing to do.") + + +def candidate_workbook_path(path: str | Path, round_name: str) -> Path: + """Where this round's candidates are written, beside the source workbook.""" + source = Path(path) + return source.with_name(f"{source.stem}_{sheet_name_for_round(round_name)}.xlsx") + + +def write_candidate_sheet( + path: str | Path, + config: Mapping[str, Any], + conditions: pd.DataFrame, + *, + round_name: str, + replicates: int = 3, + make_backup: bool = True, +) -> Path: + """Write this round's worklist to a sibling workbook. + + One row per physical film, three per condition sharing a ``replicate_group``. + Measurement columns are left blank and highlighted for entry -- including + thickness in nanometres, which the next round trains on directly. + + The source workbook is opened read-only and its Sheet1 digest is checked + afterwards, so the guarantee is enforced rather than assumed. + """ + source_path = Path(path) + source_before = _source_sheet_digest(source_path) + workbook_path = candidate_workbook_path(source_path, round_name) + sheet_name = sheet_name_for_round(round_name) + if workbook_path.exists(): + if make_backup: + backup_workbook(workbook_path) + raise CandidateSheetError( + f"{workbook_path.name} already exists. Rename or delete it first; " + "this tool does not overwrite a file that may hold measurements." + ) + from openpyxl import Workbook + + workbook = Workbook() + workbook.remove(workbook.active) + + input_names = [item["name"] for item in config["inputs"]] + source_columns = list(model_source_columns(config)) + headers = [ + "candidate_id", + "replicate_group", + "replicate_index", + "round", + *input_names, + *source_columns, + ] + + sheet = workbook.create_sheet(sheet_name) + sheet.append(headers) + for cell in sheet[1]: + cell.font = HEADER_FONT + + entry_start = len(headers) - len(source_columns) + 1 + for index, (_, condition) in enumerate(conditions.iterrows(), start=1): + candidate_id = f"{round_name.upper()}_C{index:02d}" + for replicate in range(1, replicates + 1): + sheet.append( + [ + candidate_id, + candidate_id, + replicate, + round_name.upper(), + *[float(condition[name]) for name in input_names], + ] + ) + for offset in range(len(source_columns)): + sheet.cell(row=sheet.max_row, column=entry_start + offset).fill = ( + ENTRY_FILL + ) + + for index, header in enumerate(headers, start=1): + sheet.column_dimensions[get_column_letter(index)].width = max( + 12, min(24, len(header) + 3) + ) + sheet.freeze_panes = "A2" + + workbook.save(workbook_path) + + # the invariant, actually enforced rather than asserted + if _source_sheet_digest(source_path) != source_before: + raise CandidateSheetError( + f"{SOURCE_SHEET} in {source_path.name} changed while writing " + f"{workbook_path.name}. It should not have been touched at all." + ) + return workbook_path + + +def _source_sheet_digest(path: str | Path) -> str: + """Digest of Sheet1's values only, so added sheets do not change it.""" + sheet = load_workbook(Path(path), data_only=True, read_only=False)[SOURCE_SHEET] + digest = hashlib.sha256() + for row in sheet.iter_rows(values_only=True): + digest.update(repr(row).encode("utf-8")) + return digest.hexdigest() diff --git a/tests/test_campaign.py b/tests/test_campaign.py new file mode 100644 index 0000000..1c2a386 --- /dev/null +++ b/tests/test_campaign.py @@ -0,0 +1,319 @@ +from __future__ import annotations + +import numpy as np +import pandas as pd +import pytest +import torch + +from mobo_kit.campaign import ( + FIXED_SCALING_MODES, + assert_scaling_is_campaign_fixed, + write_worklist_csv, + BatchValidityError, + CampaignConfigError, + build_objective_transform, + expand_replicates, + load_campaign_config, + model_source_columns, + run_r0_lhs, + validate_batch, +) +from mobo_kit.design import build_design_from_config + +CONFIG_PATH = "configs/FA0.9CS0.1PbI3_260407_Config.yaml" + + +@pytest.fixture(scope="module") +def config() -> dict: + return load_campaign_config(CONFIG_PATH) + + +def test_campaign_config_is_runnable(config: dict) -> None: + """The canonical config must no longer be a baseline-only stub.""" + assert config["campaign"]["status"] == "active" + assert len(config["inputs"]) == 10 + assert config["objectives"]["contract_version"] + assert len(config["objectives"]["specs"]) == 3 + assert config["constraints"] == [] + + +def test_thickness_trains_on_nanometres_not_the_score(config: dict) -> None: + """The whole point of the objective rework: source column != utility.""" + sources = model_source_columns(config) + assert sources == ( + "Uniformity score", + "Optoelectronic score", + "Thickness (avg)", + ) + transform = build_objective_transform(config) + thickness = transform.specs[2] + assert thickness.transform == "gaussian_target" + assert thickness.target == pytest.approx(650.0) + # workbook writes exp(-((T-650)/250)^2); this convention carries the 1/2 + assert thickness.sigma == pytest.approx(250.0 / np.sqrt(2.0)) + + +def test_transform_reproduces_the_workbook_thickness_score(config: dict) -> None: + """Thickness is a log-link objective: the GP emits log(nm), so the transform + exponentiates before applying the 650 nm Gaussian. Feeding it raw nm would + silently score exp(687) instead of 687.""" + transform = build_objective_transform(config) + assert transform.specs[2].model_link == "log" + nm = np.array([687.0, 1303.0]) + model_output = torch.tensor( + [[0.0, -8.0, np.log(nm[0])], [0.0, -8.0, np.log(nm[1])]], dtype=torch.double + ) + got = transform(model_output)[:, 2].numpy() + expected = np.exp(-(((nm - 650.0) / 250.0) ** 2)) + np.testing.assert_allclose(got, expected, atol=1e-12) + + +def test_reference_point_is_declared_in_utility_space(config: dict) -> None: + """A raw-scale reference silently weighted optoelectronic 4x; utility space + puts every axis on a comparable scale.""" + point = config["reference_point_utility"] + assert len(point) == 3 + assert all(abs(float(v)) < 1.0 for v in point) + + +def test_objectives_without_a_source_column_are_rejected() -> None: + bad = { + "objectives": { + "contract_version": "x", + "specs": [{"name": "a", "goal": "maximize", "transform": "identity"}], + } + } + with pytest.raises(CampaignConfigError, match="model_source_column"): + build_objective_transform(bad) + + +def test_empty_objective_list_cannot_propose() -> None: + with pytest.raises(CampaignConfigError, match="specs"): + build_objective_transform( + {"objectives": {"contract_version": "x", "specs": []}} + ) + + +# --------------------------------------------------------------------------- # +# validity gate +# --------------------------------------------------------------------------- # + + +def _valid_batch(config: dict) -> pd.DataFrame: + design = build_design_from_config(dict(config)) + rows = [[float(design.var_array[j][i * 2]) for j in range(10)] for i in range(3)] + return pd.DataFrame(rows, columns=list(design.names)) + + +def test_validate_batch_accepts_a_clean_batch(config: dict) -> None: + design = build_design_from_config(dict(config)) + report = validate_batch(_valid_batch(config), design, expected_count=3) + assert report["unique"] and report["on_grid"] and report["in_bounds"] + assert report["actual_count"] == 3 + + +@pytest.mark.parametrize( + "mutate, match", + [ + (lambda d: d.iloc[:2], "Expected exactly 3"), + (lambda d: pd.concat([d.iloc[:2], d.iloc[[0]]]), "unique"), + (lambda d: d.assign(anti_time=12.0), "grid"), + (lambda d: d.assign(speed_1=99999.0), "grid"), + (lambda d: d.assign(speed_1=float("nan")), "non-finite"), + ], +) +def test_validate_batch_refuses_real_defects(config: dict, mutate, match) -> None: + design = build_design_from_config(dict(config)) + with pytest.raises(BatchValidityError, match=match): + validate_batch(mutate(_valid_batch(config)), design, expected_count=3) + + +def test_validate_batch_enforces_minimum_spacing(config: dict) -> None: + design = build_design_from_config(dict(config)) + batch = _valid_batch(config) + with pytest.raises(BatchValidityError, match="pairwise distance"): + validate_batch(batch, design, expected_count=3, min_pairwise_distance=10.0) + + +def test_validity_report_carries_no_approval_flags(config: dict) -> None: + """The debug/production tiers are gone. Approval is a human decision recorded + outside the code, not something a validity check can compute.""" + design = build_design_from_config(dict(config)) + report = validate_batch(_valid_batch(config), design, expected_count=3) + for banned in ( + "debug_only", + "approved_for_experiment", + "approved_for_production", + "experimental_approval_false", + "production_approval_false", + ): + assert banned not in report + + +# --------------------------------------------------------------------------- # +# replicates +# --------------------------------------------------------------------------- # + + +def test_expand_replicates_groups_three_films_per_condition(config: dict) -> None: + batch = _valid_batch(config) + films = expand_replicates(batch, replicates=3, round_name="R1") + assert len(films) == 9 + assert films["replicate_group"].nunique() == 3 + assert set(films["replicate_index"]) == {1, 2, 3} + assert set(films["round"]) == {"R1"} + for _, group in films.groupby("replicate_group"): + inputs = group[list(batch.columns)].drop_duplicates() + assert len(inputs) == 1, "replicates must share identical inputs" + + +def test_expand_replicates_rejects_zero(config: dict) -> None: + with pytest.raises(ValueError, match="at least 1"): + expand_replicates(_valid_batch(config), replicates=0, round_name="R1") + + +# --------------------------------------------------------------------------- # +# R0 +# --------------------------------------------------------------------------- # + + +def test_run_r0_lhs_produces_a_valid_on_grid_worklist(config: dict) -> None: + result = run_r0_lhs(config, n=8, seed=7) + assert result.round_name == "R0" + assert result.n_conditions == 8 + assert list(result.conditions.columns) == [i["name"] for i in config["inputs"]] + assert result.diagnostics["validity"]["on_grid"] + assert ( + len(result.replicates) == 8 * config["rounds"]["r1"]["replicates_per_condition"] + ) + + +def test_run_r0_lhs_is_deterministic_for_a_seed(config: dict) -> None: + a = run_r0_lhs(config, n=6, seed=11).conditions + b = run_r0_lhs(config, n=6, seed=11).conditions + pd.testing.assert_frame_equal(a, b) + + +# --------------------------------------------------------------------------- # +# encoding +# --------------------------------------------------------------------------- # + +# Non-ASCII that breaks under a locale default codec such as GBK or cp1252. +NON_ASCII = "sigma \u2014 \u00b5m \u00b1 5% \u2013 caf\u00e9 \u4e2d\u6587" + + +def test_config_round_trips_non_ascii(tmp_path) -> None: + """Loaders must force UTF-8. + + The default codec is locale dependent -- on a Chinese-locale Windows install + it is GBK -- so a bare open() fails on the first non-ASCII byte, and it fails + on a different machine from the one the file was written on. + """ + import yaml + + path = tmp_path / "cfg.yaml" + payload = { + "campaign": {"name": NON_ASCII}, + "objectives": { + "contract_version": NON_ASCII, + "specs": [ + { + "name": "a", + "goal": "maximize", + "transform": "identity", + "model_source_column": NON_ASCII, + } + ], + }, + } + path.write_text(yaml.safe_dump(payload, allow_unicode=True), encoding="utf-8") + + loaded = load_campaign_config(path) + assert loaded["campaign"]["name"] == NON_ASCII + assert model_source_columns(loaded) == (NON_ASCII,) + assert build_objective_transform(loaded).version == NON_ASCII + + +def test_workbook_reader_round_trips_non_ascii(tmp_path) -> None: + """A non-ASCII column header or cell must survive the workbook boundary.""" + from openpyxl import Workbook, load_workbook + + path = tmp_path / "wb.xlsx" + book = Workbook() + sheet = book.active + sheet.title = "Sheet1" + sheet.append(["Sample number", NON_ASCII]) + sheet.append([1, NON_ASCII]) + book.save(path) + + reread = load_workbook(path, data_only=True)["Sheet1"] + rows = list(reread.iter_rows(values_only=True)) + assert rows[0][1] == NON_ASCII + assert rows[1][1] == NON_ASCII + + +def test_csv_round_trips_non_ascii(tmp_path) -> None: + """pandas defaults to UTF-8 for both directions; pin it with a test so a + future explicit encoding= cannot silently regress it.""" + path = tmp_path / "out.csv" + frame = pd.DataFrame({"label": [NON_ASCII], "value": [1.0]}) + frame.to_csv(path, index=False) + pd.testing.assert_frame_equal(pd.read_csv(path), frame) + + +# --------------------------------------------------------------------------- # +# campaign-fixed scaling +# --------------------------------------------------------------------------- # + + +def test_campaign_declares_fixed_scaling(config: dict) -> None: + assert config["objectives"]["scaling_mode"] in FIXED_SCALING_MODES + assert_scaling_is_campaign_fixed(config) + + +def test_data_derived_scaling_is_refused(config: dict) -> None: + """If the scale tracks the data, hypervolume stops being comparable between + rounds. The temptation arrives the moment R1 measurements land.""" + import copy + + bad = copy.deepcopy(dict(config)) + bad["objectives"]["scaling_mode"] = "observed_min_max" + with pytest.raises(CampaignConfigError, match="incomparable"): + assert_scaling_is_campaign_fixed(bad) + with pytest.raises(CampaignConfigError, match="incomparable"): + build_objective_transform(bad) + + +def test_affine_objective_without_declared_anchors_is_refused(config: dict) -> None: + """Two layers refuse this: ObjectiveSpec at construction, and + assert_scaling_is_campaign_fixed for anything that reaches it. Whichever + fires first, an affine objective can never end up with implicit anchors.""" + import copy + + bad = copy.deepcopy(dict(config)) + del bad["objectives"]["specs"][0]["upper_anchor"] + with pytest.raises(ValueError, match="upper_anchor"): + build_objective_transform(bad) + + +def test_inverted_anchors_are_refused(config: dict) -> None: + import copy + + bad = copy.deepcopy(dict(config)) + spec = bad["objectives"]["specs"][0] + spec["lower_anchor"], spec["upper_anchor"] = 1.0, 0.0 + with pytest.raises(ValueError, match="anchor"): + build_objective_transform(bad) + + +def test_worklist_csv_is_excel_safe(tmp_path) -> None: + """Excel does not detect plain UTF-8 and falls back to the system ANSI + codepage, mangling non-ASCII cells on the reader's machine rather than the + writer's. utf-8-sig writes the BOM; other readers strip it transparently.""" + path = tmp_path / "worklist.csv" + frame = pd.DataFrame({"label": [NON_ASCII], "speed_1": [1000.0]}) + write_worklist_csv(frame, path) + + assert path.read_bytes().startswith(b"\xef\xbb\xbf"), "missing UTF-8 BOM" + pd.testing.assert_frame_equal(pd.read_csv(path), frame) + pd.testing.assert_frame_equal(pd.read_csv(path, encoding="utf-8-sig"), frame) diff --git a/tests/test_d2d_baseline.py b/tests/test_d2d_baseline.py index 27bc503..76a7fb3 100644 --- a/tests/test_d2d_baseline.py +++ b/tests/test_d2d_baseline.py @@ -56,11 +56,12 @@ def test_canonical_d2d_yaml_has_exact_input_contract(): config = _load_canonical_config() design = build_design_from_config(config) - assert config["campaign"] == { - "name": "D2D_FA0.9Cs0.1PbI3", - "status": "baseline_only", - } - assert config["objectives"]["names"] == [] + assert config["campaign"]["name"] == "D2D_FA0.9Cs0.1PbI3" + # the config is no longer a baseline-only stub; it now carries a resolved + # objective contract and can propose candidates + assert config["campaign"]["status"] == "active" + assert config["objectives"]["contract_version"] + assert len(config["objectives"]["specs"]) == 3 assert config["constraints"] == [] assert design.names == [item[0] for item in EXPECTED_INPUTS] assert design.units == [item[1] for item in EXPECTED_INPUTS] diff --git a/tests/test_d2d_step2c_config.py b/tests/test_d2d_step2c_config.py index 09abbc3..5b760bd 100644 --- a/tests/test_d2d_step2c_config.py +++ b/tests/test_d2d_step2c_config.py @@ -44,7 +44,7 @@ def test_step2c_config_resolves_public_safe_debug_contract() -> None: assert config.bound_policies == ("none", "clip_ucb") assert config.primary_bound_policy == "clip_ucb" assert config.primary_penalty_variant == "radius_0_25" - assert config.model_variant_names == ("default_current", "conservative") + assert config.model_variant_names == ("dim_scaled_prior", "conservative") assert config.influence_sample_ids == config.expected_sample_ids assert config.shortlist_min == 8 assert config.shortlist_max == 12 diff --git a/tests/test_d2d_step2c_robustness_helpers.py b/tests/test_d2d_step2c_robustness_helpers.py index 227fc2d..7e5edec 100644 --- a/tests/test_d2d_step2c_robustness_helpers.py +++ b/tests/test_d2d_step2c_robustness_helpers.py @@ -54,7 +54,7 @@ def _study_batch( X_norm=normalized, base_scores=scores, penalized_scores=scores * penalty_factor, - model_variant="default_current", + model_variant="dim_scaled_prior", pool_seed=73, pool_size=32, pool_hash="A" * 64, @@ -125,7 +125,7 @@ def test_boundary_enrichment_separates_lower_and_upper_endpoints() -> None: def test_candidate_model_summary_counts_declared_support_extrapolation() -> None: summary = pd.DataFrame( { - "variant_name": ["default_current"] * 3, + "variant_name": ["dim_scaled_prior"] * 3, "objective_index": [0, 1, 2], "objective_name": ["uniformity", "optoelectronic", "thickness"], } @@ -133,7 +133,7 @@ def test_candidate_model_summary_counts_declared_support_extrapolation() -> None candidate_rows = pd.DataFrame( { "run_id": ["baseline"] * 5, - "model_variant": ["default_current"] * 5, + "model_variant": ["dim_scaled_prior"] * 5, "pred_mean_0": [-0.1, 0.2, 0.4, 0.8, 1.1], "pred_mean_1": [-20.0, -5.0, 0.0, 5.0, 20.0], "pred_mean_2": [0.1, 0.3, 0.5, 0.7, 0.9], diff --git a/tests/test_model_validation.py b/tests/test_model_validation.py index e9302f7..081ff82 100644 --- a/tests/test_model_validation.py +++ b/tests/test_model_validation.py @@ -11,7 +11,9 @@ import mobo_kit.models as models_module from mobo_kit.model_validation import ( CONSERVATIVE, - DEFAULT_CURRENT, + DIM_SCALED_PRIOR, + LEGACY_NO_PRIOR, + PRIMARY_VARIANT, ModelFitCache, ModelFitError, ModelVariantSpec, @@ -52,24 +54,88 @@ def _skip_optimization(mll: object) -> object: def test_model_variant_contracts_are_fixed() -> None: - assert model_variant_spec("default_current") is DEFAULT_CURRENT - assert DEFAULT_CURRENT.min_noise == pytest.approx(1.0e-3) - assert DEFAULT_CURRENT.min_lengthscale is None + assert model_variant_spec("dim_scaled_prior") is DIM_SCALED_PRIOR + assert DIM_SCALED_PRIOR.min_noise == pytest.approx(1.0e-4) + assert DIM_SCALED_PRIOR.min_lengthscale is None + assert DIM_SCALED_PRIOR.use_dim_scaled_prior is True + assert DIM_SCALED_PRIOR.use_lognormal_noise_prior is True + assert model_variant_spec("legacy_matern_no_prior") is LEGACY_NO_PRIOR + assert LEGACY_NO_PRIOR.use_dim_scaled_prior is False + assert LEGACY_NO_PRIOR.use_lognormal_noise_prior is False assert model_variant_spec("conservative") is CONSERVATIVE assert CONSERVATIVE.min_noise == pytest.approx(0.01) assert CONSERVATIVE.min_lengthscale == pytest.approx(0.05) - with pytest.raises(ValueError, match="default_current"): - ModelVariantSpec("default_current", min_noise=0.01, min_lengthscale=None) + with pytest.raises(ValueError, match="dim_scaled_prior"): + ModelVariantSpec("dim_scaled_prior", min_noise=0.01, min_lengthscale=None) + with pytest.raises(ValueError, match="dim_scaled_prior"): + # both priors are part of the contract, not options + ModelVariantSpec("dim_scaled_prior", min_noise=1.0e-4, min_lengthscale=None) + with pytest.raises(ValueError, match="dim_scaled_prior"): + # the lengthscale prior alone is the degenerate configuration + ModelVariantSpec( + "dim_scaled_prior", + min_noise=1.0e-4, + min_lengthscale=None, + use_dim_scaled_prior=True, + ) + with pytest.raises(ValueError, match="legacy_matern_no_prior"): + ModelVariantSpec( + "legacy_matern_no_prior", + min_noise=1.0e-3, + min_lengthscale=None, + use_dim_scaled_prior=True, + ) with pytest.raises(ValueError, match="conservative"): ModelVariantSpec("conservative", min_noise=0.01, min_lengthscale=0.01) with pytest.raises(ValueError, match="Unsupported"): model_variant_spec("mystery") -def test_default_current_matches_step2b_model_construction( +def test_primary_variant_is_the_prior_regularised_model() -> None: + """The fixed model is the default; the retired one must be asked for by name.""" + assert PRIMARY_VARIANT is DIM_SCALED_PRIOR + assert PRIMARY_VARIANT.name == "dim_scaled_prior" + + +def test_dim_scaled_prior_attaches_a_lengthscale_prior( monkeypatch: pytest.MonkeyPatch, ) -> None: + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", _skip_optimization) + X, Y, sample_ids = _training_data() + + primary = fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=DIM_SCALED_PRIOR, + ) + legacy = fit_model_variant( + X, + Y, + sample_ids=sample_ids, + objective_names=("one", "two"), + variant=LEGACY_NO_PRIOR, + ) + + for gp in primary.model.models: + base = gp.covar_module.base_kernel + assert base.lengthscale_prior is not None + # LogNormal(loc = sqrt(2) + log(d)/2, scale = sqrt(3)); d = 2 here + assert float(base.lengthscale_prior.loc) == pytest.approx( + np.sqrt(2.0) + np.log(X.shape[1]) / 2.0 + ) + for gp in legacy.model.models: + assert not hasattr(gp.covar_module.base_kernel, "lengthscale_prior") or ( + gp.covar_module.base_kernel.lengthscale_prior is None + ) + + +def test_legacy_variant_matches_step2b_model_construction( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The retired contract must still reproduce archived Step 2B/2C runs exactly.""" monkeypatch.setattr(validation_module, "fit_gpytorch_mll", _skip_optimization) monkeypatch.setattr(models_module, "fit_gpytorch_mll", _skip_optimization) X, Y, sample_ids = _training_data() @@ -80,7 +146,7 @@ def test_default_current_matches_step2b_model_construction( Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=LEGACY_NO_PRIOR, seed=73, ) torch.manual_seed(73) @@ -152,7 +218,7 @@ def test_lengthscale_flags_are_relative_to_normalized_domain( Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, ) for gp in record.model.models: gp.covar_module.base_kernel.lengthscale = torch.tensor( @@ -198,7 +264,7 @@ def fail_strictly(mll: object) -> object: Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, ) error = captured.value @@ -234,7 +300,7 @@ def fail_during_construction(*args: object, **kwargs: object) -> object: Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, ) assert captured.value.stage == "construct" @@ -259,7 +325,7 @@ def warn_and_succeed(mll: object) -> object: Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, fit_key="warning-test", ) @@ -290,7 +356,7 @@ def count_fit(mll: object) -> object: Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, seed=19, row_roles=("control", "r0", "r0", "r0", "r0"), control_sample_ids=(1,), @@ -302,7 +368,7 @@ def count_fit(mll: object) -> object: Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, seed=19, row_roles=("control", "r0", "r0", "r0", "r0"), control_sample_ids=(1,), @@ -336,7 +402,7 @@ def count_fit(mll: object) -> object: Y, sample_ids=sample_ids, objective_names=("one", "two"), - variant=DEFAULT_CURRENT, + variant=DIM_SCALED_PRIOR, seed=20, cache=cache, ) @@ -417,3 +483,102 @@ def test_prediction_metrics_report_undefined_r_squared_and_validate_uncertainty( with pytest.raises(ValueError, match="strictly positive"): compute_prediction_metrics([0.0], [0.0], [0.0]) + + +def test_dim_scaled_prior_carries_the_lognormal_noise_prior() -> None: + """Regression guard for the outputscale-collapse mode. + + With only the lengthscale prior, the marginal likelihood could drive the + outputscale to zero and explain the data as pure noise, leaving a latent + predictive sd near 1e-4 against a fitted noise near 0.93. Measured on the + real campaign data that produced 68% coverage of 0.133 and mean NLPD 3.1e6 + over the leave-one-out folds. The noise prior is what rules it out. + """ + X = torch.rand(12, 3, dtype=torch.double) + Y = torch.rand(12, 1, dtype=torch.double) + record = fit_model_variant( + X, + Y, + sample_ids=tuple(range(12)), + objective_names=("y",), + variant=DIM_SCALED_PRIOR, + ) + for gp in record.model.models: + assert gp.likelihood.noise_covar.noise_prior is not None + assert float(gp.likelihood.noise_covar.noise_prior.loc) == pytest.approx(-4.0) + assert float(gp.likelihood.noise_covar.noise_prior.scale) == pytest.approx(1.0) + floor = gp.likelihood.noise_covar.raw_noise_constraint.lower_bound + assert float(floor) == pytest.approx(1.0e-4) + + record.model.eval() + with torch.no_grad(): + latent_sd = record.model.posterior(X).variance.sqrt() + # a collapsed outputscale shows up here as a latent sd orders of magnitude + # below the outcome scale + assert float(latent_sd.min()) > 1e-3 + + +def test_legacy_variant_keeps_its_bare_noise_floor() -> None: + """The retired contract must not silently inherit the new noise prior.""" + X = torch.rand(12, 3, dtype=torch.double) + Y = torch.rand(12, 1, dtype=torch.double) + record = fit_model_variant( + X, + Y, + sample_ids=tuple(range(12)), + objective_names=("y",), + variant=LEGACY_NO_PRIOR, + ) + for gp in record.model.models: + # a prior-free HomoskedasticNoise has no noise_prior attribute at all + assert getattr(gp.likelihood.noise_covar, "noise_prior", None) is None + floor = gp.likelihood.noise_covar.raw_noise_constraint.lower_bound + assert float(floor) == pytest.approx(1.0e-3) + + +def test_signal_collapse_guard_fires_on_a_degenerate_fit( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A numerical guard, not a naming guard. + + Config-level naming cannot prevent a degenerate optimum: the same contract + refitted on new data can land there again. So the assertion runs on every + fit. Here the collapse is forced directly by zeroing the outputscale. + """ + X = torch.rand(10, 2, dtype=torch.double) + Y = torch.rand(10, 1, dtype=torch.double) + + real_fit = validation_module.fit_gpytorch_mll + + def collapse_outputscale(mll): + real_fit(mll) + # emulate the observed failure: no signal, all noise + mll.model.covar_module.outputscale = torch.tensor(1e-12, dtype=torch.double) + mll.model.likelihood.noise = torch.tensor(0.9, dtype=torch.double) + return mll + + monkeypatch.setattr(validation_module, "fit_gpytorch_mll", collapse_outputscale) + with pytest.raises(ModelFitError) as excinfo: + fit_model_variant( + X, + Y, + sample_ids=tuple(range(10)), + objective_names=("y",), + variant=DIM_SCALED_PRIOR, + ) + assert excinfo.value.stage == "signal_collapse_guard" + assert isinstance(excinfo.value.cause, validation_module.SignalCollapseError) + assert "pure noise" in str(excinfo.value.cause) + + +def test_signal_collapse_guard_passes_a_healthy_fit() -> None: + X = torch.rand(12, 3, dtype=torch.double) + Y = (X[:, :1] * 2.0 + 0.1 * torch.randn(12, 1, dtype=torch.double)).double() + record = fit_model_variant( + X, + Y, + sample_ids=tuple(range(12)), + objective_names=("y",), + variant=DIM_SCALED_PRIOR, + ) + assert record.model is not None diff --git a/tests/test_objectives.py b/tests/test_objectives.py index 2c3a58f..ebf1c07 100644 --- a/tests/test_objectives.py +++ b/tests/test_objectives.py @@ -1,3 +1,5 @@ +import math + import pytest import torch @@ -326,3 +328,318 @@ def test_wrong_dimension_integer_nonfinite_and_duplicate_names_fail(): ], version="TEST_ONLY-v1", ) + + +# --------------------------------------------------------------------------- # +# expected utility under a Gaussian posterior +# --------------------------------------------------------------------------- # + + +def _mc_expected(spec, mu, var, *, draws=2_000_000, seed=0): + """Monte-Carlo reference for E[transform(Y)], Y ~ N(mu, var).""" + transform = ObjectiveTransform([spec], version="TEST_ONLY-v1") + g = torch.Generator().manual_seed(seed) + y = mu + math.sqrt(var) * torch.randn(draws, 1, generator=g, dtype=torch.double) + return float(transform(y).mean()) + + +@pytest.mark.parametrize("mu", [400.0, 650.0, 900.0, 1300.0]) +@pytest.mark.parametrize("var", [0.0, 20.0**2, 100.0**2, 300.0**2]) +def test_gaussian_target_expected_matches_monte_carlo(mu, var): + """The closed form must agree with sampling, including at variance zero.""" + # workbook uses exp(-((T-650)/250)^2), i.e. sigma = 250/sqrt(2) here + spec = ObjectiveSpec( + "thickness", + "target", + "gaussian_target", + target=650.0, + sigma=250.0 / math.sqrt(2.0), + ) + transform = ObjectiveTransform([spec], version="TEST_ONLY-v1") + got = float( + transform.expected_transform( + torch.tensor([[mu]], dtype=torch.double), + torch.tensor([[var]], dtype=torch.double), + ) + ) + if var == 0.0: + assert got == pytest.approx( + float(transform(torch.tensor([[mu]], dtype=torch.double))), rel=1e-12 + ) + else: + assert got == pytest.approx(_mc_expected(spec, mu, var), abs=1e-3) + + +def test_gaussian_target_expected_penalises_uncertainty_at_the_target(): + """Identical predicted mean, wider posterior, strictly lower expected utility.""" + spec = ObjectiveSpec( + "thickness", + "target", + "gaussian_target", + target=650.0, + sigma=250.0 / math.sqrt(2.0), + ) + transform = ObjectiveTransform([spec], version="TEST_ONLY-v1") + mean = torch.full((3, 1), 650.0, dtype=torch.double) + var = torch.tensor([[20.0], [100.0], [300.0]], dtype=torch.double) ** 2 + scores = transform.expected_transform(mean, var).flatten().tolist() + assert scores[0] > scores[1] > scores[2] + assert scores == pytest.approx([0.993661, 0.870388, 0.507673], abs=1e-5) + + +def test_negative_absolute_target_expected_matches_monte_carlo(): + spec = ObjectiveSpec( + "t", "target", "negative_absolute_target", target=650.0, scale=250.0 + ) + transform = ObjectiveTransform([spec], version="TEST_ONLY-v1") + for mu, var in ((650.0, 100.0**2), (400.0, 50.0**2), (900.0, 300.0**2)): + got = float( + transform.expected_transform( + torch.tensor([[mu]], dtype=torch.double), + torch.tensor([[var]], dtype=torch.double), + ) + ) + assert got == pytest.approx(_mc_expected(spec, mu, var), abs=2e-3) + + +def test_linear_transforms_expectation_equals_transform_of_mean(): + transform = ObjectiveTransform( + [ + ObjectiveSpec("a", "maximize", "identity"), + ObjectiveSpec( + "b", "maximize", "affine", lower_anchor=0.0, upper_anchor=2.0 + ), + ], + version="TEST_ONLY-v1", + ) + mean = torch.tensor([[0.3, 1.1]], dtype=torch.double) + var = torch.tensor([[4.0, 9.0]], dtype=torch.double) + torch.testing.assert_close(transform.expected_transform(mean, var), transform(mean)) + + +def test_expected_transform_rejects_bad_input(): + transform = ObjectiveTransform( + [ObjectiveSpec("x", "maximize", "identity")], version="TEST_ONLY-v1" + ) + ok = torch.ones((2, 1), dtype=torch.double) + with pytest.raises(ValueError, match="share a shape"): + transform.expected_transform(ok, torch.ones((3, 1), dtype=torch.double)) + with pytest.raises(ValueError, match="non-negative"): + transform.expected_transform(ok, -ok) + with pytest.raises(ValueError, match="final dimension"): + transform.expected_transform( + torch.ones((2, 2), dtype=torch.double), + torch.ones((2, 2), dtype=torch.double), + ) + + +# --------------------------------------------------------------------------- # +# lognormal expectation (GP fitted in log space) +# --------------------------------------------------------------------------- # + + +def _thickness_utility(): + return ObjectiveTransform( + [ + ObjectiveSpec( + "thickness", + "target", + "gaussian_target", + target=650.0, + sigma=250.0 / math.sqrt(2.0), + ) + ], + version="TEST_ONLY-v1", + ) + + +@pytest.mark.parametrize("median_nm", [500.0, 700.0, 900.0]) +@pytest.mark.parametrize("s_log", [0.10, 0.20, 0.40]) +def test_lognormal_expectation_matches_monte_carlo(median_nm, s_log): + transform = _thickness_utility() + m = math.log(median_nm) + got = float( + transform.expected_transform_lognormal( + torch.tensor([[m]], dtype=torch.double), + torch.tensor([[s_log**2]], dtype=torch.double), + ) + ) + g = torch.Generator().manual_seed(0) + z = m + s_log * torch.randn(2_000_000, 1, generator=g, dtype=torch.double) + mc = float(transform(torch.exp(z)).mean()) + assert got == pytest.approx(mc, abs=1e-3) + + +def test_lognormal_expectation_beats_moment_matching_by_orders_of_magnitude(): + """Moment-matching a lognormal to a Gaussian and reusing the closed form is + an approximation whose error is large enough to reorder candidates.""" + transform = _thickness_utility() + m, s = math.log(700.0), 0.40 + gh = float( + transform.expected_transform_lognormal( + torch.tensor([[m]], dtype=torch.double), + torch.tensor([[s**2]], dtype=torch.double), + ) + ) + mu = math.exp(m + s * s / 2.0) + var = (math.exp(s * s) - 1.0) * math.exp(2 * m + s * s) + mm = float( + transform.expected_transform( + torch.tensor([[mu]], dtype=torch.double), + torch.tensor([[var]], dtype=torch.double), + ) + ) + g = torch.Generator().manual_seed(0) + z = m + s * torch.randn(2_000_000, 1, generator=g, dtype=torch.double) + mc = float(transform(torch.exp(z)).mean()) + assert abs(gh - mc) < 1e-3 + assert abs(mm - mc) > 50 * abs(gh - mc) + + +def test_moment_matching_error_changes_sign_across_the_range(): + """The reason moment-matching is not merely a constant offset: the bias + flips sign, so it permutes the candidate ordering.""" + transform = _thickness_utility() + s = 0.22 # the campaign's measured posterior width in log space + signed = [] + for median_nm in (550.0, 850.0): + m = math.log(median_nm) + gh = float( + transform.expected_transform_lognormal( + torch.tensor([[m]], dtype=torch.double), + torch.tensor([[s**2]], dtype=torch.double), + ) + ) + mu = math.exp(m + s * s / 2.0) + var = (math.exp(s * s) - 1.0) * math.exp(2 * m + s * s) + mm = float( + transform.expected_transform( + torch.tensor([[mu]], dtype=torch.double), + torch.tensor([[var]], dtype=torch.double), + ) + ) + signed.append(mm - gh) + assert signed[0] > 0 > signed[1], f"expected a sign change, got {signed}" + + +def test_lognormal_expectation_degenerates_to_the_plain_transform(): + transform = _thickness_utility() + m = torch.tensor([[math.log(650.0)]], dtype=torch.double) + got = float(transform.expected_transform_lognormal(m, torch.zeros_like(m))) + assert got == pytest.approx(1.0, abs=1e-9) + + +def test_lognormal_expectation_rejects_bad_input(): + transform = _thickness_utility() + ok = torch.zeros((2, 1), dtype=torch.double) + with pytest.raises(ValueError, match="share a shape"): + transform.expected_transform_lognormal( + ok, torch.zeros((3, 1), dtype=torch.double) + ) + with pytest.raises(ValueError, match="non-negative"): + transform.expected_transform_lognormal(ok, ok - 1.0) + with pytest.raises(ValueError, match="nodes"): + transform.expected_transform_lognormal(ok, ok, nodes=1) + + +# --------------------------------------------------------------------------- # +# model_link: the two acquisition paths must agree +# --------------------------------------------------------------------------- # + + +def _log_link_contract(): + """The campaign's shape: two identity-link objectives and one log-link.""" + return ObjectiveTransform( + [ + ObjectiveSpec( + "uniformity", "maximize", "affine", lower_anchor=0.0, upper_anchor=1.0 + ), + ObjectiveSpec( + "optoelectronic", + "maximize", + "affine", + lower_anchor=-10.0, + upper_anchor=-6.0, + ), + ObjectiveSpec( + "thickness", + "target", + "gaussian_target", + model_link="log", + target=650.0, + sigma=250.0 / math.sqrt(2.0), + ), + ], + version="TEST_ONLY-v1", + ) + + +def test_log_link_decodes_exactly_once(): + """The trap: quadrature and MC both route through one link decode. If either + exponentiates separately the utility is computed on exp(exp(x)).""" + transform = _log_link_contract() + nm = 687.0 + model_output = torch.tensor([[0.5, -8.0, math.log(nm)]], dtype=torch.double) + got = transform(model_output)[0, 2].item() + expected = math.exp(-(((nm - 650.0) / 250.0) ** 2)) + assert got == pytest.approx(expected, abs=1e-12) + + +def test_identity_link_objectives_are_untouched_by_the_link_machinery(): + transform = _log_link_contract() + model_output = torch.tensor([[0.5, -8.0, math.log(650.0)]], dtype=torch.double) + utilities = transform(model_output) + assert utilities[0, 0].item() == pytest.approx(0.5) + assert utilities[0, 1].item() == pytest.approx((-8.0 + 10.0) / 4.0) + + +def test_ucb_and_qlognehvi_paths_agree_on_the_same_posterior(): + """The guard that matters operationally. + + UCB reads analytic utility moments; qLogNEHVI transforms posterior samples. + Both are correct, and nothing else forces them to match. If they drift, the + two rounds silently optimise different objectives and it surfaces only as R1 + and R2 disagreeing for reasons nobody can trace. + """ + transform = _log_link_contract() + mean = torch.tensor( + [[0.4, -8.2, math.log(700.0)], [0.6, -7.5, math.log(480.0)]], + dtype=torch.double, + ) + variance = torch.tensor( + [[0.01, 0.04, 0.22**2], [0.02, 0.09, 0.30**2]], dtype=torch.double + ) + + analytic = transform.expected_transform(mean, variance) + + # the sampling path: draw in MODEL space, transform, average + g = torch.Generator().manual_seed(0) + draws = 400_000 + samples = mean.unsqueeze(0) + variance.sqrt().unsqueeze(0) * torch.randn( + (draws, *mean.shape), generator=g, dtype=torch.double + ) + sampled = transform(samples).mean(dim=0) + + # MC standard error over this many draws is ~1e-3 + torch.testing.assert_close(analytic, sampled, atol=4e-3, rtol=0.0) + + +def test_expected_transform_dispatches_per_objective_not_globally(): + """A contract mixing links must not apply one rule to every column.""" + transform = _log_link_contract() + mean = torch.tensor([[0.4, -8.0, math.log(650.0)]], dtype=torch.double) + zero = torch.zeros_like(mean) + + # at zero variance every path collapses to the plain transform + torch.testing.assert_close( + transform.expected_transform(mean, zero), transform(mean) + ) + + # widening only the log-link column must move only that utility + widened = zero.clone() + widened[0, 2] = 0.30**2 + expected = transform.expected_transform(mean, widened) + baseline = transform(mean) + assert expected[0, 0].item() == pytest.approx(baseline[0, 0].item()) + assert expected[0, 1].item() == pytest.approx(baseline[0, 1].item()) + assert expected[0, 2].item() < baseline[0, 2].item() diff --git a/tests/test_robustness_plots.py b/tests/test_robustness_plots.py index 9de9520..39fc9a3 100644 --- a/tests/test_robustness_plots.py +++ b/tests/test_robustness_plots.py @@ -58,7 +58,7 @@ def _convergence_frame() -> pd.DataFrame: def _hyperparameter_frame() -> pd.DataFrame: rows = [] - for variant_index, variant in enumerate(("default_current", "conservative")): + for variant_index, variant in enumerate(("dim_scaled_prior", "conservative")): for objective_index, objective in enumerate(("Uniformity", "Thickness")): for fold in range(2): rows.append( @@ -87,7 +87,7 @@ def _prediction_range_frame() -> pd.DataFrame: ranges = {"Uniformity": (0.1, 0.9), "Thickness": (-1.5, 1.2)} for objective_index, (objective, observed_range) in enumerate(ranges.items()): for candidate_index, candidate in enumerate(("C1", "C2")): - for model_index, model in enumerate(("default_current", "conservative")): + for model_index, model in enumerate(("dim_scaled_prior", "conservative")): rows.append( { "candidate_id": candidate, @@ -130,10 +130,10 @@ def _membership_frame() -> pd.DataFrame: "REGION-010", ], "model_variant": [ - "default_current", - "default_current", - "default_current", - "default_current", + "dim_scaled_prior", + "dim_scaled_prior", + "dim_scaled_prior", + "dim_scaled_prior", "conservative", ], "bound_policy": ["clip_ucb", "clip_ucb", "none", "clip_ucb", "clip_ucb"], diff --git a/tests/test_step2c_artifacts.py b/tests/test_step2c_artifacts.py index 9f8e71e..6cce657 100644 --- a/tests/test_step2c_artifacts.py +++ b/tests/test_step2c_artifacts.py @@ -401,7 +401,7 @@ def _build_bundle( ) ], model_variants=[ - {"name": "default_current"}, + {"name": "dim_scaled_prior"}, {"name": "conservative"}, ], influence_common_pool_hash="D" * 64, diff --git a/tests/test_structured_mean.py b/tests/test_structured_mean.py new file mode 100644 index 0000000..f31329e --- /dev/null +++ b/tests/test_structured_mean.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from mobo_kit.campaign import load_campaign_config +from mobo_kit.structured_mean import ( + MeanFeature, + StructuredMeanSpec, + apply_structured_mean, + fit_structured_mean, + mean_spec_from_config, +) + +NAMES = ["speed_1", "time_1", "precur_conc", "anneal_temp"] + + +def _X(n: int = 12) -> np.ndarray: + rng = np.random.default_rng(0) + return np.column_stack( + [ + rng.uniform(1000, 6000, n), + rng.uniform(5, 50, n), + rng.uniform(1.0, 2.0, n), + rng.uniform(100, 185, n), + ] + ) + + +def test_log_response_recovers_a_power_law_exactly() -> None: + """T = c * speed^a * conc^b is linear in log-log, so the mean should absorb + it completely and leave zero residual.""" + X = _X() + T = 5.0e5 * X[:, 0] ** -0.4 * X[:, 2] ** 1.3 + spec = StructuredMeanSpec( + response="log", + features=(MeanFeature("speed_1", "log"), MeanFeature("precur_conc", "log")), + ) + coefficients, residual = fit_structured_mean(X, T, spec, NAMES) + assert np.abs(residual).max() < 1e-9 + assert coefficients[1] == pytest.approx(-0.4, abs=1e-6) + assert coefficients[2] == pytest.approx(1.3, abs=1e-6) + + +def test_identity_response_recovers_a_linear_trend() -> None: + X = _X() + y = 3.0 - 0.05 * X[:, 3] + spec = StructuredMeanSpec("identity", (MeanFeature("anneal_temp"),)) + coefficients, residual = fit_structured_mean(X, y, spec, NAMES) + assert np.abs(residual).max() < 1e-9 + assert coefficients[1] == pytest.approx(-0.05, abs=1e-9) + + +def test_trend_round_trips_through_apply() -> None: + X = _X() + y = 2.0 + 0.01 * X[:, 3] + spec = StructuredMeanSpec("identity", (MeanFeature("anneal_temp"),)) + coefficients, residual = fit_structured_mean(X, y, spec, NAMES) + post = apply_structured_mean( + coefficients, X, spec, NAMES, residual, np.zeros(len(X)) + ) + np.testing.assert_allclose(post.mean, y, atol=1e-9) + assert post.link == "identity" + + +def test_log_response_reports_its_link() -> None: + """The link is what tells the utility layer to integrate a lognormal instead + of using the Gaussian closed form.""" + X = _X() + spec = StructuredMeanSpec("log", (MeanFeature("speed_1", "log"),)) + coefficients, residual = fit_structured_mean(X, np.full(len(X), 700.0), spec, NAMES) + post = apply_structured_mean( + coefficients, X, spec, NAMES, residual, np.zeros(len(X)) + ) + assert post.link == "log" + np.testing.assert_allclose(np.exp(post.mean), 700.0, atol=1e-8) + + +def test_coefficients_come_only_from_the_rows_passed_in() -> None: + """Guards the leakage property: fitting on a subset must not see the rest.""" + X = _X(14) + y = 2.0 + 0.01 * X[:, 3] + spec = StructuredMeanSpec("identity", (MeanFeature("anneal_temp"),)) + keep = list(range(13)) + a, _ = fit_structured_mean(X[keep], y[keep], spec, NAMES) + b, _ = fit_structured_mean(X[keep], y[keep] * 1.0, spec, NAMES) + np.testing.assert_allclose(a, b) + # perturbing the held-out row must not change the fitted trend + y_perturbed = y.copy() + y_perturbed[13] += 1000.0 + c, _ = fit_structured_mean(X[keep], y_perturbed[keep], spec, NAMES) + np.testing.assert_allclose(a, c) + + +def test_log_response_rejects_non_positive_observations() -> None: + X = _X() + spec = StructuredMeanSpec("log", (MeanFeature("speed_1", "log"),)) + y = np.full(len(X), 1.0) + y[0] = -1.0 + with pytest.raises(ValueError, match="strictly positive"): + fit_structured_mean(X, y, spec, NAMES) + + +def test_unknown_feature_column_is_rejected() -> None: + spec = StructuredMeanSpec("identity", (MeanFeature("not_an_input"),)) + with pytest.raises(ValueError, match="not a declared input"): + fit_structured_mean(_X(), np.ones(12), spec, NAMES) + + +@pytest.mark.parametrize( + "kwargs, match", + [ + ({"response": "sqrt", "features": (MeanFeature("speed_1"),)}, "response link"), + ({"response": "identity", "features": ()}, "at least one feature"), + ( + { + "response": "identity", + "features": (MeanFeature("speed_1"), MeanFeature("speed_1")), + }, + "unique", + ), + ], +) +def test_invalid_specs_are_rejected(kwargs, match) -> None: + with pytest.raises(ValueError, match=match): + StructuredMeanSpec(**kwargs) + + +# --------------------------------------------------------------------------- # +# the campaign's declared shapes +# --------------------------------------------------------------------------- # + + +def test_campaign_declares_the_two_measured_mean_functions() -> None: + """Opposite shapes by design: thickness needs a pair of log terms, + optoelectronic needs exactly one linear term. Neither generalises.""" + config = load_campaign_config("configs/FA0.9CS0.1PbI3_260407_Config.yaml") + by_name = {s["name"]: s for s in config["objectives"]["specs"]} + + assert mean_spec_from_config(by_name["uniformity"]) is None + + opto = mean_spec_from_config(by_name["optoelectronic"]) + assert opto.response == "identity" + assert [f.column for f in opto.features] == ["anneal_temp"] + assert [f.transform for f in opto.features] == ["identity"] + + thickness = mean_spec_from_config(by_name["thickness"]) + assert thickness.response == "log" + assert [f.column for f in thickness.features] == ["speed_1", "precur_conc"] + assert [f.transform for f in thickness.features] == ["log", "log"] diff --git a/tests/test_workbook_io.py b/tests/test_workbook_io.py new file mode 100644 index 0000000..832eb44 --- /dev/null +++ b/tests/test_workbook_io.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +import shutil + +import pandas as pd +import pytest +from openpyxl import load_workbook + +from mobo_kit.campaign import load_campaign_config, model_source_columns +from mobo_kit.workbook_io import ( + candidate_workbook_path, + CandidateSheetError, + detect_round, + read_campaign_workbook, + sheet_name_for_round, + write_candidate_sheet, +) + +CONFIG_PATH = "configs/FA0.9CS0.1PbI3_260407_Config.yaml" +SOURCE = "local_inputs/Summary Table.xlsx" + +pytestmark = pytest.mark.skipif( + not __import__("pathlib").Path(SOURCE).exists(), + reason="requires the ignored private campaign workbook", +) + + +@pytest.fixture(scope="module") +def config() -> dict: + return load_campaign_config(CONFIG_PATH) + + +@pytest.fixture +def workbook(tmp_path): + destination = tmp_path / "Summary Table.xlsx" + shutil.copy2(SOURCE, destination) + return destination + + +def _conditions(config: dict, n: int = 5) -> pd.DataFrame: + names = [item["name"] for item in config["inputs"]] + rows = [ + [float(item["start"]) + i * float(item["step"]) for item in config["inputs"]] + for i in range(n) + ] + return pd.DataFrame(rows, columns=names) + + +def test_reads_inputs_and_the_declared_model_columns(workbook, config) -> None: + contents = read_campaign_workbook(workbook, config) + assert contents.n_rows == 15 + assert list(contents.inputs.columns) == [i["name"] for i in config["inputs"]] + assert list(contents.model_values.columns) == list(model_source_columns(config)) + # thickness must arrive in nanometres, not as its score + assert contents.model_values["Thickness (avg)"].max() > 100.0 + + +def test_stops_at_the_first_blank_sample_number(workbook, config) -> None: + """Rows below the data block are notes, not observations.""" + contents = read_campaign_workbook(workbook, config) + assert contents.sample_ids == tuple(range(1, 16)) + + +def test_candidate_sheet_has_an_entry_column_per_model_source(workbook, config) -> None: + """The reader contract changed when thickness moved to nanometres: the sheet + needs an nm entry column or R2 has nothing to train on.""" + write_candidate_sheet(workbook, config, _conditions(config), round_name="R1") + sheet = load_workbook(candidate_workbook_path(workbook, "R1"))[ + sheet_name_for_round("R1") + ] + headers = [c.value for c in sheet[1]] + for column in model_source_columns(config): + assert column in headers + assert "Thickness (avg)" in headers + + +def test_writing_leaves_the_source_byte_identical(workbook, config) -> None: + """openpyxl discards cached formula values on save, and Uniformity score is a + formula column. Writing beside the workbook makes that impossible rather than + merely unlikely.""" + import hashlib + + before = hashlib.sha256(workbook.read_bytes()).hexdigest() + out = write_candidate_sheet(workbook, config, _conditions(config), round_name="R1") + assert out != workbook + assert out.exists() + assert hashlib.sha256(workbook.read_bytes()).hexdigest() == before + + +def test_formula_columns_survive_because_the_source_is_not_rewritten( + workbook, config +) -> None: + """The regression this design exists to prevent.""" + write_candidate_sheet(workbook, config, _conditions(config), round_name="R1") + contents = read_campaign_workbook(workbook, config) + # Uniformity score is =L*N*O; a rewritten workbook reads it as NaN + assert contents.model_values["Uniformity score"].notna().all() + assert contents.model_values["Uniformity score"].max() > 0.0 + + +def test_three_replicate_rows_per_condition(workbook, config) -> None: + write_candidate_sheet( + workbook, config, _conditions(config, 5), round_name="R1", replicates=3 + ) + sheet = load_workbook(candidate_workbook_path(workbook, "R1"))[ + sheet_name_for_round("R1") + ] + rows = [r for r in sheet.iter_rows(min_row=2, values_only=True) if r[0]] + assert len(rows) == 15 + assert len({r[1] for r in rows}) == 5 + + +def test_refuses_to_overwrite_an_existing_sheet(workbook, config) -> None: + write_candidate_sheet(workbook, config, _conditions(config), round_name="R1") + with pytest.raises(CandidateSheetError, match="already exists"): + write_candidate_sheet( + workbook, config, _conditions(config), round_name="R1", make_backup=False + ) + + +def test_round_detection_walks_r1_then_r2(workbook, config) -> None: + assert detect_round(workbook, config).next_round == "R1" + write_candidate_sheet(workbook, config, _conditions(config), round_name="R1") + state = detect_round(workbook, config) + assert state.next_round is None + assert "no results have been entered" in state.reason + + +def test_partially_scored_sheet_is_refused_with_a_readable_message( + workbook, config +) -> None: + """Fail closed: guessing at a half-filled sheet is how a round gets built on + data the experimentalist had not finished entering.""" + out = write_candidate_sheet(workbook, config, _conditions(config), round_name="R1") + book = load_workbook(out) + sheet = book[sheet_name_for_round("R1")] + headers = [c.value for c in sheet[1]] + for column in model_source_columns(config): + sheet.cell(row=2, column=headers.index(column) + 1).value = 1.0 + book.save(out) + + state = detect_round(workbook, config) + assert state.next_round is None + assert state.scored_rows == 1 and state.total_rows == 15 + assert "partly filled in" in state.reason + assert "1 of 15" in state.reason + + +def test_missing_source_sheet_is_a_plain_sentence(tmp_path, config) -> None: + from openpyxl import Workbook + + path = tmp_path / "wrong.xlsx" + book = Workbook() + book.active.title = "Renamed" + book.save(path) + with pytest.raises(CandidateSheetError, match="rename it back"): + read_campaign_workbook(path, config) From 0ab119683edf5d87c62ee65ccf676376ff46b1af Mon Sep 17 00:00:00 2001 From: Colin-Qi Date: Wed, 29 Jul 2026 04:07:24 -0400 Subject: [PATCH 03/39] Add a DTLZ2 end-to-end acceptance test for the campaign path Runs a synthetic 3-objective, 10-input problem with a known Pareto front through run_r0_lhs -> run_r1_ucb(5) -> run_r2_qlognehvi(3). Nothing touches the experimental data, so it answers "does the algorithm work" separately from "are the measurements right". Two sign conventions fail silently and are now covered: - DTLZ2 minimises by default; negate=True is mandatory or the test would measure the opposite of optimisation. - BoTorch's Hypervolume assumes maximisation and silently DROPS points that do not dominate the reference -- no warning, no exception, just a smaller number or 0.0. The helper asserts at least one point dominates first. On the optimisation claim: cumulative hypervolume rises monotonically by construction, so that alone would pass for random sampling. The test therefore compares against a random baseline at equal budget, and asserts the MEAN gain rather than a per-seed win: measured 5 of 8 seeds, mean gain +0.075 vs +0.056, a ratio of 1.35x. With 8 added points in 10 dimensions that is the honest expectation; asserting a per-seed win would be flaky and false. Structural invariants covered: exact batch sizes (5 and 3, 23 distinct conditions), uniqueness, on-grid, in-bounds, finiteness, batch spacing well above the configured floor (0.735 and 0.859 against 0.15), proposals distinct from observed points, and determinism at a fixed seed. Pool sizes are shrunk for runtime; R2 is the bottleneck and mc_samples is the cheapest lever. Fast tests ~12 s, the multi-seed comparison ~33 s behind a new `slow` marker. Also records a finding from the recon: metrics.compute_ref_pareto_hv builds its automatic reference as mins - 1e-8, giving a degenerate 6e-8 hypervolume against 1.448 from botorch's infer_reference_point on the same data, and recomputes it per call so values are not comparable across iterations. The production path passes an explicit reference and is unaffected. Tests: 528 passed, 2 skipped. Co-Authored-By: Claude Opus 5 --- docs/CAMPAIGN_STATUS.md | 53 +++++- pyproject.toml | 1 + tests/test_dtlz2_acceptance.py | 307 +++++++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 tests/test_dtlz2_acceptance.py diff --git a/docs/CAMPAIGN_STATUS.md b/docs/CAMPAIGN_STATUS.md index 05dd6dc..0141e25 100644 --- a/docs/CAMPAIGN_STATUS.md +++ b/docs/CAMPAIGN_STATUS.md @@ -89,6 +89,49 @@ Validated on the 15 R0 observations, exact leave-one-out, null R2 = -0.148: Uniformity is exploration-only by measurement, not by choice. The interface must not imply the model knows more than it does about it. +## Synthetic acceptance test + +`tests/test_dtlz2_acceptance.py` runs DTLZ2 (3 objectives, 10 inputs, known +Pareto front) end to end through `campaign.py`. It exercises the algorithm with +no dependence on whether the experimental measurements are right. + +```bash +pytest tests/test_dtlz2_acceptance.py -m "not slow" # 10 tests, ~12 s +pytest tests/test_dtlz2_acceptance.py -m slow # BO vs random, ~33 s +``` + +Measured on the negated DTLZ2 (max_hv = 0.807): + +| | R0 (15) | +R1 (5) | +R2 (3) | +|---|---:|---:|---:| +| hypervolume | 0.507 | 0.555 | 0.612 | + +Batch spacing: R1 min pairwise 0.735, R2 0.859, against a configured floor of +0.15 -- local penalization is separating candidates, not merely not failing. + +**Cumulative hypervolume rises monotonically by construction**, so that alone is +not evidence of optimisation -- it would hold for random sampling too. The +informative result is the baseline comparison at equal budget (8 extra points +from the same 15-point start): + +| | mean HV gain | +|---|---:| +| Bayesian optimisation | **+0.075** | +| random on-grid search | +0.056 | + +A ratio of **1.35x**, and BO wins on **5 of 8 seeds** -- on the mean, not every +seed. With 8 added points in 10 dimensions that is the honest expectation, so the +test asserts the mean and not a per-seed win. + +Two conventions that fail *silently* if got wrong, both now covered: + +* DTLZ2 minimises by default; `negate=True` is mandatory or the test measures the + opposite of optimisation. +* BoTorch's `Hypervolume` assumes maximisation and **silently drops points that + do not dominate the reference** -- no warning, no exception, just a smaller + number or 0.0. The helper asserts at least one point dominates before + trusting the result. + ## Open issues -- read before trusting a batch 1. **An unexplained 0.089 discrepancy on optoelectronic.** Two implementations of @@ -121,7 +164,15 @@ not imply the model knows more than it does about it. One specific thing to look for: whether anything lands near `speed_1 = 1000`, a region with two contradictory observations in it. -5. **Not started:** the tkinter launcher, replicate-variance pooling into +5. **`metrics.compute_ref_pareto_hv` has a degenerate auto-reference.** When + `ref_point_np=None` it uses `mins - 1e-8`, essentially the nadir itself, so + every slab is 1e-8 thick: measured HV 6e-8 against 1.448 from BoTorch's + `infer_reference_point` on the same data. It also recomputes the reference + from the current data each call, so hypervolumes are not comparable across + iterations. The production path passes an explicit reference and is + unaffected; any new plotting code must do the same. + +6. **Not started:** the tkinter launcher, replicate-variance pooling into `train_Yvar` (Phase 4), and removal of the legacy Step 2C debug ceremony. ## Reproducing the analysis diff --git a/pyproject.toml b/pyproject.toml index 07ce24b..203bd9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,4 +112,5 @@ python_functions = ["test_*"] addopts = "-v --tb=short" markers = [ "local_input: optional integration test requiring an ignored local_inputs artifact", + "slow: multi-seed acceptance test; deselect with -m 'not slow'", ] diff --git a/tests/test_dtlz2_acceptance.py b/tests/test_dtlz2_acceptance.py new file mode 100644 index 0000000..2bf4d71 --- /dev/null +++ b/tests/test_dtlz2_acceptance.py @@ -0,0 +1,307 @@ +"""End-to-end acceptance test on a synthetic problem with a known Pareto front. + +DTLZ2 with 3 objectives and 10 inputs, run through the real campaign path: +``run_r0_lhs`` -> ``run_r1_ucb(5)`` -> ``run_r2_qlognehvi(3)``. Nothing here +touches the experimental data, so it answers "does the algorithm work" separately +from "are the measurements right". + +Two conventions had to be got right, and both fail silently if you don't. + +**DTLZ2 minimises by default.** ``negate=True`` is mandatory. Without it the +objectives are positive, no point dominates the reference, and the test would +measure the opposite of optimisation. + +**BoTorch's Hypervolume assumes maximisation and silently drops points that do +not dominate the reference** -- there is no warning and no exception, you simply +get a smaller number, or 0.0. So :func:`_hypervolume` asserts that at least one +point dominates before trusting the result. + +The optimisation claim is deliberately weak, because the honest one is: +cumulative hypervolume rises monotonically *by construction* (adding points can +only grow the dominated region), so "HV increased" would pass for random +sampling too. The meaningful comparison is against a random baseline at the same +budget, and BO wins **on average, not on every seed** -- measured 5/8 seeds with +a mean gain ratio of 1.35x. Asserting a per-seed win would be a flaky test +asserting something untrue. +""" + +from __future__ import annotations + +import warnings + +import numpy as np +import pytest +import torch +from botorch.test_functions.multi_objective import DTLZ2 +from botorch.utils.multi_objective.hypervolume import Hypervolume +from botorch.utils.multi_objective.pareto import is_non_dominated + +from mobo_kit.campaign import ( + build_objective_transform, + run_r0_lhs, + run_r1_ucb, + run_r2_qlognehvi, +) +from mobo_kit.design import build_design_from_config + +INPUT_DIM = 10 +OBJECTIVES = 3 +R0_SIZE = 15 +R1_SIZE = 5 +R2_SIZE = 3 + + +def _problem() -> DTLZ2: + # negate=True -> maximisation, which is what the campaign and BoTorch's + # hypervolume both assume + return DTLZ2(dim=INPUT_DIM, num_objectives=OBJECTIVES, negate=True).to( + dtype=torch.double + ) + + +def _config(pool: int = 1024, mc_samples: int = 32) -> dict: + """A campaign config for DTLZ2. Pool sizes are shrunk for test runtime. + + The production config uses 32768, which costs ~124 s for one R0->R1->R2 pass. + R2 is the bottleneck and ``mc_samples`` is the cheapest lever, so that is what + is reduced most. + """ + return { + "inputs": [ + {"name": f"x{i}", "start": 0.0, "stop": 1.0, "step": 0.05} + for i in range(INPUT_DIM) + ], + "objectives": { + "contract_version": "TEST_ONLY-dtlz2-v1", + "scaling_mode": "fixed_affine", + "specs": [ + { + "name": f"f{i}", + "goal": "maximize", + "transform": "affine", + "model_source_column": f"f{i}", + # negated DTLZ2 lands in roughly [-1.9, 0] + "lower_anchor": -2.0, + "upper_anchor": 0.0, + } + for i in range(OBJECTIVES) + ], + }, + "reference_point_utility": [-0.01] * OBJECTIVES, + "rounds": { + "r1": { + "method": "ucb_hvi", + "batch_size": R1_SIZE, + "replicates_per_condition": 3, + "beta": 4.0, + "candidate_pool_size": pool, + "posterior_samples": 256, + "moment_method": "monte_carlo", + }, + "r2": { + "method": "qlognehvi", + "batch_size": R2_SIZE, + "replicates_per_condition": 3, + "candidate_pool_size": pool, + "mc_samples": mc_samples, + }, + }, + "local_penalization": { + "radius": 0.25, + "min_batch_distance": 0.15, + "min_observed_distance": 0.0, + "dimension_weights": None, + }, + "model": {"variant": "dim_scaled_prior"}, + "reproducibility": {"seed": 73}, + "constraints": [], + } + + +def _evaluate(problem: DTLZ2, X_phys: np.ndarray) -> np.ndarray: + """DTLZ2 lives on [0,1]^10, which is exactly the declared design domain.""" + return problem(torch.tensor(np.asarray(X_phys, float), dtype=torch.double)).numpy() + + +def _hypervolume(config: dict, Y_raw: np.ndarray) -> float: + """Hypervolume in UTILITY space against the fixed campaign reference. + + Computing it in utility space rather than raw space is what makes values + comparable across rounds: the campaign's scales are fixed, so the reference + does not drift as data arrives. + """ + transform = build_objective_transform(config) + reference = torch.tensor(config["reference_point_utility"], dtype=torch.double) + utility = transform(torch.tensor(np.asarray(Y_raw, float), dtype=torch.double)) + if not bool((utility >= reference).all(dim=-1).any()): + raise AssertionError( + "No point dominates the reference point. BoTorch would silently drop " + "every point and return 0.0 rather than raising, so this is checked " + "explicitly." + ) + return Hypervolume(ref_point=reference).compute(utility[is_non_dominated(utility)]) + + +def _run_campaign(config: dict, seed: int) -> dict: + """One full R0 -> R1 -> R2 pass, evaluating DTLZ2 at each proposed batch.""" + problem = _problem() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r0 = run_r0_lhs(config, n=R0_SIZE, seed=seed) + X0 = r0.conditions.to_numpy(float) + Y0 = _evaluate(problem, X0) + + r1 = run_r1_ucb(config, X0, Y0, seed=seed) + X1 = r1.conditions.to_numpy(float) + Y1 = _evaluate(problem, X1) + + X01, Y01 = np.vstack([X0, X1]), np.vstack([Y0, Y1]) + r2 = run_r2_qlognehvi(config, X01, Y01, seed=seed) + X2 = r2.conditions.to_numpy(float) + Y2 = _evaluate(problem, X2) + + return { + "r0": r0, + "r1": r1, + "r2": r2, + "hv": [ + _hypervolume(config, Y0), + _hypervolume(config, Y01), + _hypervolume(config, np.vstack([Y01, Y2])), + ], + "X": [X0, X1, X2], + } + + +@pytest.fixture(scope="module") +def campaign() -> dict: + return _run_campaign(_config(), seed=73) + + +# --------------------------------------------------------------------------- # +# the algorithm produces well-formed batches +# --------------------------------------------------------------------------- # + + +def test_each_round_proposes_exactly_the_requested_count(campaign: dict) -> None: + assert len(campaign["r0"].conditions) == R0_SIZE + assert len(campaign["r1"].conditions) == R1_SIZE + assert len(campaign["r2"].conditions) == R2_SIZE + # 23 distinct conditions, 3 films each + total = R0_SIZE + R1_SIZE + R2_SIZE + assert total == 23 + assert len(campaign["r2"].replicates) == R2_SIZE * 3 + + +@pytest.mark.parametrize("round_key", ["r0", "r1", "r2"]) +def test_every_batch_is_unique_on_grid_and_in_bounds( + campaign: dict, round_key: str +) -> None: + report = campaign[round_key].diagnostics["validity"] + assert report["unique"] + assert report["on_grid"] + assert report["in_bounds"] + assert report["finite"] + + +@pytest.mark.parametrize("round_key", ["r1", "r2"]) +def test_local_penalization_spreads_the_batch(campaign: dict, round_key: str) -> None: + """Without penalization a batch collapses onto the single best pool point. + + The floor is the configured 0.15; the measured values are far above it, which + is the signal that penalization is doing work rather than merely not failing. + """ + minimum = campaign[round_key].diagnostics["validity"]["min_pairwise_distance"] + assert minimum >= 0.15 + assert minimum > 0.5, f"batch is unexpectedly clustered: {minimum:.4f}" + + +def test_proposed_points_are_distinct_from_the_observed_set(campaign: dict) -> None: + """Re-proposing an already-measured recipe would waste a film.""" + X0, X1, X2 = campaign["X"] + seen = {tuple(np.round(row, 9)) for row in X0} + for row in np.vstack([X1, X2]): + assert tuple(np.round(row, 9)) not in seen + + +# --------------------------------------------------------------------------- # +# the algorithm optimises +# --------------------------------------------------------------------------- # + + +def test_cumulative_hypervolume_never_decreases(campaign: dict) -> None: + """True by construction -- adding points cannot shrink the dominated region. + + It is asserted anyway because a violation would mean something is broken in + the transform, the reference point, or the sign convention. It is NOT + evidence of optimisation; see the random-baseline test for that. + """ + hv0, hv1, hv2 = campaign["hv"] + assert hv0 <= hv1 <= hv2 + assert hv0 > 0.0 + + +def test_hypervolume_actually_improves_over_the_initial_design( + campaign: dict, +) -> None: + hv0, _, hv2 = campaign["hv"] + assert hv2 > hv0 + # DTLZ2's optimum against its own reference is 0.807; we are in the right + # order of magnitude rather than chasing a specific value + assert 0.0 < hv2 < 1.0 + + +def test_the_batches_are_deterministic_for_a_fixed_seed(campaign: dict) -> None: + """A reproducible campaign is a precondition for auditing one.""" + repeat = _run_campaign(_config(), seed=73) + np.testing.assert_allclose( + repeat["r1"].conditions.to_numpy(float), campaign["X"][1] + ) + np.testing.assert_allclose( + repeat["r2"].conditions.to_numpy(float), campaign["X"][2] + ) + + +@pytest.mark.slow +def test_bayesian_optimisation_beats_random_search_on_average() -> None: + """The test that makes the hypervolume numbers mean something. + + Cumulative HV rises for random sampling too, so the only informative + comparison is against a random baseline at the same budget (8 extra points + from the same 15-point start). + + BO wins on the MEAN, not on every seed: measured 5 of 8 seeds with a mean + gain ratio of 1.35x. With 8 added points in 10 dimensions that is the + honest expectation, and asserting a per-seed win would be a flaky test + asserting something false. + """ + config = _config() + design = build_design_from_config(config) + problem = _problem() + grids = [np.asarray(g, float) for g in design.var_array] + + bo_gains, random_gains, wins = [], [], 0 + for seed in (1, 2, 3, 4, 5): + result = _run_campaign(config, seed=seed) + hv0, _, hv_bo = result["hv"] + + rng = np.random.default_rng(seed) + X_random = np.column_stack( + [rng.choice(g, size=R1_SIZE + R2_SIZE) for g in grids] + ) + Y_random = np.vstack( + [_evaluate(problem, result["X"][0]), _evaluate(problem, X_random)] + ) + hv_random = _hypervolume(config, Y_random) + + bo_gains.append(hv_bo - hv0) + random_gains.append(hv_random - hv0) + wins += (hv_bo - hv0) > (hv_random - hv0) + + mean_bo, mean_random = float(np.mean(bo_gains)), float(np.mean(random_gains)) + assert mean_bo > 0, "BO made no hypervolume progress at all" + assert mean_random > 0, "the random baseline is broken, not a fair comparison" + assert mean_bo > mean_random, ( + f"BO mean gain {mean_bo:.4f} did not beat random {mean_random:.4f}; " + f"won {wins}/5 seeds" + ) From 33f101f8602533b918bdf4247292391895435256 Mon Sep 17 00:00:00 2001 From: Colin-Qi Date: Wed, 29 Jul 2026 15:42:17 -0400 Subject: [PATCH 04/39] Clean the branch: remove the retired Step 1/2A/2B/2C audit apparatus That machinery existed to audit a GP model this branch has since replaced. Its findings are recorded in docs/GP_MODEL_DECISION.md, so the code itself no longer earns its place: it was 16,442 lines that nothing on the campaign path imports. Removed 50 files -- 14 source modules, their tests, 5 examples, 4 configs, 9 docs and the Step 2B notebook -- plus stale demo/experiment output. Source drops from ~24k to ~9.6k lines; the campaign path itself is 7,073 lines across 16 modules. Everything is recoverable: tag pre-cleanup-2026-07-29 holds the full tree. git show pre-cleanup-2026-07-29:src/mobo_kit/.py Kept despite not being on the campaign path yet, because they are genuinely useful rather than historical: discrete_refinement (exact-grid local search), sobol_pool (nested pools), candidate_diagnostics and plotting (both wanted for the visualization work). production_gate.py went with the ceremony. Its one valuable check -- that objective scales are never re-derived from observed data, which would make hypervolume incomparable between rounds -- was already extracted into campaign.assert_scaling_is_campaign_fixed, where it runs inside build_objective_transform and cannot be bypassed. main.py's propose_candidates branch was permanently blocked by that gate, i.e. dead. It now raises a message pointing at mobo_kit.campaign, which is the real proposal path and carries the objective contract and batch validity checks that the legacy path never had. Naming made consistent: configs are campaign_d2d_perovskite.yaml, example_demo.yaml, example_from_csv.yaml. No file is named after a step number any more. README rewritten around the campaign loop, the DTLZ2 acceptance evidence, the current parameter values, and where the removed history lives. Tests: 280 passed (was 528; the difference is tests for removed modules). Co-Authored-By: Claude Opus 5 --- README.md | 387 +- ...nfig.yaml => campaign_d2d_perovskite.yaml} | 0 configs/d2d_step2a_provisional.yaml | 96 - configs/d2d_step2b_debug.yaml | 133 - configs/d2d_step2c_debug.yaml | 129 - configs/demo_config.yaml | 50 - .../{auto_config.yaml => example_demo.yaml} | 0 ...mple_config.yaml => example_from_csv.yaml} | 0 docs/CAMPAIGN_STATUS.md | 2 +- docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md | 154 - docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md | 97 - docs/D2D_STEP2A_COMPUTATIONAL_CORE.md | 204 - docs/D2D_STEP2C_ROBUSTNESS_METHOD.md | 209 - docs/GP_MODEL_DECISION.md | 2 +- docs/REPO_AUDIT_D2D.md | 142 - docs/STEP1_HANDOFF.md | 273 -- docs/STEP2A_HANDOFF.md | 319 -- docs/STEP2B_DEBUG_HANDOFF.md | 272 -- docs/STEP2C_ROBUSTNESS_HANDOFF.md | 298 -- examples/d2d_step2a_benchmark.py | 70 - examples/d2d_step2a_synthetic.py | 464 -- examples/d2d_step2b_debug.py | 61 - examples/d2d_step2c_robustness.py | 89 - examples/d2d_step2c_synthetic_ci.py | 55 - ...Global Distance Candidate generation.ipynb | 1013 ----- results/demo/batch_1.csv | 23 - results/demo/hv_demo_with_predictions.png | Bin 76677 -> 0 bytes results/demo/lhs_corr.png | Bin 172703 -> 0 bytes results/demo/lhs_dist.png | Bin 102955 -> 0 bytes results/demo/lhs_pca.png | Bin 55006 -> 0 bytes results/demo/parity_train.png | Bin 99004 -> 0 bytes results/demo/proposed_bar.png | Bin 114585 -> 0 bytes results/demo/shap.png | Bin 140915 -> 0 bytes results/experiment/next_batch.csv | 21 - results/experiment/parity_plots.png | Bin 68739 -> 0 bytes scripts/plot_dtlz2_report.py | 322 ++ scripts/validate_structured_means.py | 2 +- src/mobo_kit/batch_comparison.py | 180 - src/mobo_kit/d2d_campaign.py | 1182 ----- src/mobo_kit/d2d_r2_test.py | 162 - src/mobo_kit/d2d_scores.py | 663 --- src/mobo_kit/d2d_step2b_debug.py | 1533 ------- src/mobo_kit/d2d_step2c_config.py | 918 ---- src/mobo_kit/d2d_step2c_robustness.py | 3922 ----------------- src/mobo_kit/d2d_step2c_synthetic.py | 349 -- src/mobo_kit/main.py | 32 +- src/mobo_kit/observation_influence.py | 874 ---- src/mobo_kit/production_gate.py | 528 --- src/mobo_kit/robust_regions.py | 752 ---- src/mobo_kit/robustness_plots.py | 1322 ------ src/mobo_kit/step2c_artifacts.py | 2272 ---------- src/mobo_kit/workbook_schema.py | 829 ---- tests/test_batch_comparison.py | 123 - tests/test_campaign.py | 2 +- tests/test_d2d_baseline.py | 339 -- tests/test_d2d_campaign.py | 595 --- tests/test_d2d_notebook.py | 151 - tests/test_d2d_r2_test.py | 184 - tests/test_d2d_scores.py | 299 -- tests/test_d2d_step2b_debug.py | 308 -- tests/test_d2d_step2c_config.py | 156 - tests/test_d2d_step2c_robustness_helpers.py | 624 --- tests/test_d2d_step2c_synthetic.py | 113 - tests/test_observation_influence.py | 319 -- tests/test_production_gate.py | 281 -- tests/test_robust_regions.py | 313 -- tests/test_robustness_plots.py | 539 --- tests/test_step2a_synthetic.py | 81 - tests/test_step2c_artifacts.py | 917 ---- tests/test_structured_mean.py | 2 +- tests/test_workbook_io.py | 2 +- tests/test_workbook_schema.py | 681 --- 72 files changed, 458 insertions(+), 25976 deletions(-) rename configs/{FA0.9CS0.1PbI3_260407_Config.yaml => campaign_d2d_perovskite.yaml} (100%) delete mode 100644 configs/d2d_step2a_provisional.yaml delete mode 100644 configs/d2d_step2b_debug.yaml delete mode 100644 configs/d2d_step2c_debug.yaml delete mode 100644 configs/demo_config.yaml rename configs/{auto_config.yaml => example_demo.yaml} (100%) rename configs/{configCSV_example_config.yaml => example_from_csv.yaml} (100%) delete mode 100644 docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md delete mode 100644 docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md delete mode 100644 docs/D2D_STEP2A_COMPUTATIONAL_CORE.md delete mode 100644 docs/D2D_STEP2C_ROBUSTNESS_METHOD.md delete mode 100644 docs/REPO_AUDIT_D2D.md delete mode 100644 docs/STEP1_HANDOFF.md delete mode 100644 docs/STEP2A_HANDOFF.md delete mode 100644 docs/STEP2B_DEBUG_HANDOFF.md delete mode 100644 docs/STEP2C_ROBUSTNESS_HANDOFF.md delete mode 100644 examples/d2d_step2a_benchmark.py delete mode 100644 examples/d2d_step2a_synthetic.py delete mode 100644 examples/d2d_step2b_debug.py delete mode 100644 examples/d2d_step2c_robustness.py delete mode 100644 examples/d2d_step2c_synthetic_ci.py delete mode 100644 notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb delete mode 100644 results/demo/batch_1.csv delete mode 100644 results/demo/hv_demo_with_predictions.png delete mode 100644 results/demo/lhs_corr.png delete mode 100644 results/demo/lhs_dist.png delete mode 100644 results/demo/lhs_pca.png delete mode 100644 results/demo/parity_train.png delete mode 100644 results/demo/proposed_bar.png delete mode 100644 results/demo/shap.png delete mode 100644 results/experiment/next_batch.csv delete mode 100644 results/experiment/parity_plots.png create mode 100644 scripts/plot_dtlz2_report.py delete mode 100644 src/mobo_kit/batch_comparison.py delete mode 100644 src/mobo_kit/d2d_campaign.py delete mode 100644 src/mobo_kit/d2d_r2_test.py delete mode 100644 src/mobo_kit/d2d_scores.py delete mode 100644 src/mobo_kit/d2d_step2b_debug.py delete mode 100644 src/mobo_kit/d2d_step2c_config.py delete mode 100644 src/mobo_kit/d2d_step2c_robustness.py delete mode 100644 src/mobo_kit/d2d_step2c_synthetic.py delete mode 100644 src/mobo_kit/observation_influence.py delete mode 100644 src/mobo_kit/production_gate.py delete mode 100644 src/mobo_kit/robust_regions.py delete mode 100644 src/mobo_kit/robustness_plots.py delete mode 100644 src/mobo_kit/step2c_artifacts.py delete mode 100644 src/mobo_kit/workbook_schema.py delete mode 100644 tests/test_batch_comparison.py delete mode 100644 tests/test_d2d_baseline.py delete mode 100644 tests/test_d2d_campaign.py delete mode 100644 tests/test_d2d_notebook.py delete mode 100644 tests/test_d2d_r2_test.py delete mode 100644 tests/test_d2d_scores.py delete mode 100644 tests/test_d2d_step2b_debug.py delete mode 100644 tests/test_d2d_step2c_config.py delete mode 100644 tests/test_d2d_step2c_robustness_helpers.py delete mode 100644 tests/test_d2d_step2c_synthetic.py delete mode 100644 tests/test_observation_influence.py delete mode 100644 tests/test_production_gate.py delete mode 100644 tests/test_robust_regions.py delete mode 100644 tests/test_robustness_plots.py delete mode 100644 tests/test_step2a_synthetic.py delete mode 100644 tests/test_step2c_artifacts.py delete mode 100644 tests/test_workbook_schema.py diff --git a/README.md b/README.md index fc953a8..909f97b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -# README: MOBO-Kit +# MOBO-Kit + by Ethan Schwartz, Daniel Abdoue, Nicky Evans, and Tonio Buonassisi +

Slot-die optimization logo @@ -8,327 +10,164 @@ by Ethan Schwartz, Daniel Abdoue, Nicky Evans, and Tonio Buonassisi

-[![DOI](https://img.shields.io/badge/DOI-TBD-blue)](https://doi.org/TBD) -[![arXiv](https://img.shields.io/badge/arXiv-TBD-blue.svg?logo=arxiv&logoColor=white.svg)](https://arxiv.org/abs/TBD) [![Requires Python 3.11-3.12](https://img.shields.io/badge/Python-3.11--3.12-blue.svg?logo=python&logoColor=white)](https://python.org/downloads)

-**MOBO-Kit** is an open-source toolkit for accelerating design of experiments via **multi-objective Bayesian optimization**. Developed collaboratively across University of Washington, UC San Diego, and MIT, this toolkit enables rapid optimization of complex systems by balancing multiple objectives across any number of inputs and outputs (>2). While demonstrated for slot-die coating experiments (e.g., optimizing efficiency, repeatability, and stability), MOBO-Kit is generalizable to any multi-objective optimization problem. +**MOBO-Kit** accelerates design of experiments with **multi-objective Bayesian +optimization**. It proposes small batches of experimental conditions that trade +off several objectives at once, for problems with more than two inputs and more +than two outputs. Developed across the University of Washington, UC San Diego and +MIT, and demonstrated on slot-die coated perovskite films. -## Key Features +## The campaign loop -MOBO-Kit provides a complete package for multi-objective Bayesian optimization with: -- **Latin Hypercube Sampling** for initial experiment design -- **Gaussian Process models** with BoTorch -- **Multi-objective acquisition functions** (qNEHVI) -- **Batch candidate proposal** for efficient parallel experimentation -- **Comprehensive plotting and analysis tools** -- **Constraint handling** for complex design spaces -- **Command-line interface** and Python API +A campaign runs in three rounds. Each proposed condition is run in triplicate so +reproducibility can be measured. ---- +| Round | Method | Conditions | Films | +|---|---|---:|---:| +| R0 | Latin hypercube sampling | 15 | 45 | +| R1 | UCB-HVI + local penalization | 5 | 15 | +| R2 | qLogNEHVI | 3 | 9 | -## Table of Contents +```python +from mobo_kit.campaign import load_campaign_config, run_r0_lhs, run_r1_ucb, run_r2_qlognehvi -- [Key Features](#key-features) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Configuration](#configuration) -- [Package Structure](#package-structure) -- [Troubleshooting](#troubleshooting) -- [Next Steps](#next-steps) -- [Citation](#citation) -- [License](#license) -- [Get in Touch](#get-in-touch) +config = load_campaign_config("configs/campaign_d2d_perovskite.yaml") ---- +r0 = run_r0_lhs(config, n=15) # space-filling, no model +r1 = run_r1_ucb(config, X_phys, Y_model, n=5) # after R0 is measured +r2 = run_r2_qlognehvi(config, X_phys, Y_model, n=3) # after R1 is measured +``` -## Installation +Each call returns a `RoundResult` with `conditions` (distinct recipes, physical +units), `replicates` (one row per film, grouped), and `diagnostics` (seed, pool +size, and a validity report). -### Option 1: Install from Source (Recommended) +`docs/CAMPAIGN_STATUS.md` is the working guide: what to pass, what comes back, +how to plot it, and the current open issues. -We recommend creating a clean Python environment using `conda` or `venv`: +## Installation ```bash -# Create and activate environment -conda create -n mobo-kit python=3.11 +conda create -n mobo-kit python=3.12 conda activate mobo-kit - -# Or using venv -python -m venv mobo-env -source mobo-env/bin/activate # On Windows: mobo-env\Scripts\activate - -# Install MOBO-Kit git clone https://github.com/PV-Lab/MOBO-Kit.git cd MOBO-Kit python -m pip install -r requirements/dev.txt ``` -This installs the core scientific stack, the tested Torch/GPyTorch/BoTorch -combination, workbook auditing support, and the development test tools. - -### Optional GPU support - -The Step 1 campaign baseline is tested on CPU with PyTorch 2.8.0. GPU wheels -are platform- and CUDA-specific and are not part of that tested baseline. If a -later workflow requires a GPU, install a PyTorch **2.8.0** build using the -[official PyTorch previous-versions instructions](https://pytorch.org/get-started/previous-versions/), -then rerun the import and test smoke checks. Installing an arbitrary newer -Torch release invalidates the pinned BoTorch/GPyTorch compatibility claim. - -**Check your CUDA version:** -```bash -nvidia-smi -``` - -**Verify GPU support:** -```python -import torch -print("CUDA available:", torch.cuda.is_available()) -print("Device count:", torch.cuda.device_count()) -``` - -### Distribution status - -The source/editable installation above is the only installation path validated -for the Step 1 campaign baseline. A PyPI release and the historical Colab link -are not treated as production campaign environments until they have their own -versioned release and smoke-test workflow. - -### Dependencies +Tested on CPU with Python 3.12, PyTorch 2.8.0, BoTorch 0.15.1, GPyTorch 1.14. +The exact stack is pinned in `requirements/constraints.txt`. -MOBO-Kit requires: -- Python 3.11 or 3.12 -- PyTorch 2.8.x -- BoTorch 0.15.1 -- GPyTorch 1.14 -- NumPy, Pandas, Scikit-learn -- Matplotlib, Seaborn -- openpyxl for read-only campaign-workbook auditing -- Pillow for image-artifact validation +## Does the optimizer actually work? -The tested probabilistic stack is pinned in `requirements/constraints.txt`. -See `requirements.txt` for the runtime install and `requirements/dev.txt` for -the editable test environment. - -## Quick Start - -### 1. Command Line Interface +`tests/test_dtlz2_acceptance.py` runs the whole loop on **DTLZ2** — a synthetic +3-objective, 10-input problem with a known Pareto front — so the algorithm can be +checked independently of any experimental data. ```bash -# The runner accepts the repository's metadata-style campaign CSV format. -# Run model fitting and diagnostics without proposing candidates: -mobo-kit run --csv data/processed/configCSV_example.csv - -# Run with custom output directory -mobo-kit run --csv data/my_data.csv --out local_outputs/my_experiment - -# Run with verbose output -mobo-kit run --csv data/my_data.csv --verbose -``` - -### 2. Python API - -```python -import mobo_kit -from mobo_kit.main import main - -# Run the main workflow -main() -``` - -### 3. Advanced Usage Examples - -```python -# Generate initial experiments -from mobo_kit.main import generate_initial_experiments - -results = generate_initial_experiments( - config_path="configs/demo_config.yaml", - n_samples=20, - save_path="initial_experiments.csv" -) - -# Run MOBO optimization with custom parameters -from mobo_kit.main import run_mobo_experiment - -results = run_mobo_experiment( - csv_path="data/processed/configCSV_example.csv", - save_dir="local_outputs/experiment", - verbose=True, - propose_candidates=False, -) +pytest tests/test_dtlz2_acceptance.py -m "not slow" +pytest tests/test_dtlz2_acceptance.py -m slow ``` -`generate` writes an input-only R0 worklist. It is intentionally not accepted -directly by the legacy `run` proposal path. Campaign-specific workbook adapters -own that boundary and must validate their objective and state contracts first. - -### 4. Jupyter Notebooks - -See the `notebooks/` directory for interactive examples: -- `MOBO_demo_annotated.ipynb` - Complete workflow demonstration -- `D2D_MOBO_TEST Global Distance Candidate generation.ipynb` - D2D Step 2B - score validation and guarded debug-adapter interface - - *Note*: The LOOCV function may have trouble converging on small noisy datasets and is still in development. +Cumulative hypervolume rises monotonically **by construction**, so that alone +proves nothing — random sampling passes it too. The informative comparison is +against a random baseline at equal budget: mean hypervolume gain **+0.075 (BO) +against +0.056 (random)**, winning on 5 of 8 seeds. BO wins on the mean, not on +every seed, which is the honest expectation for 8 added points in 10 dimensions. -### 5. D2D Step 2B algorithm debugging +`python scripts/plot_dtlz2_report.py` renders the round-by-round GP fit, +uncertainty, acquisition surface and selected batch. -The D2D adapter requires explicit paths to an ignored private workbook and its -matching ignored private configuration. The tracked configuration is a -non-runnable public template with no workbook identity. The adapter reads the -workbook without saving it, uses the supplied Z/AA/AB final scores directly, -and writes only ignored, watermarked debug artifacts: +## Repository layout -```bash -python examples/d2d_step2b_debug.py \ - --workbook local_inputs/.xlsx \ - --config local_inputs/d2d_step2b_private.yaml ``` - -This command is deliberately **not** experimental approval. Its five-condition -proposal and 15-row replicate file are labelled `DEBUG ONLY - NOT APPROVED FOR -EXPERIMENT`. The legacy `run --propose-candidates` path remains blocked. - -### 6. D2D Step 2C robustness audit - -Step 2C validates the small-data GP models, measures all-observation influence, -checks nested Sobol search convergence, refines candidates on the exact discrete -grid, and summarizes persistent candidate regions. It remains read-only and -debug-only: - -```bash -# Generated sanitized workbook, small pools, and atomic artifact/ZIP CI coverage -python examples/d2d_step2c_synthetic_ci.py --overwrite - -# Quick integration smoke; never eligible to emit a consensus batch -python examples/d2d_step2c_robustness.py --mode fast - -# Declared 16,384/32,768/65,536/131,072 robustness study -python examples/d2d_step2c_robustness.py --mode full +src/mobo_kit/ + campaign.py the three rounds; start here + design.py input grid and bounds + lhs.py Latin hypercube sampling (R0) + candidate_pool.py discrete candidate sampling + sobol_pool.py nested Sobol pools (alternative sampler) + models.py GP construction + model_validation.py strict fitting, exact LOOCV, fit guards + structured_mean.py physics-informed GP mean functions + objectives.py raw measurement -> utility contract + ucb_hvi.py UCB hypervolume-improvement scoring (R1) + qlognehvi_batch.py qLogNEHVI batch selection (R2) + batch_selection.py local penalization, shared by both + discrete_refinement.py exact-grid local search + workbook_io.py Excel read / candidate-sheet write + metrics.py Pareto front and hypervolume + plotting.py diagnostic plots + candidate_diagnostics.py, acquisition.py, cli.py, main.py, + data.py, constraints.py, utils.py + +configs/ campaign_d2d_perovskite.yaml (the live campaign) + two examples +docs/ CAMPAIGN_STATUS.md, GP_MODEL_DECISION.md, D2D_CAMPAIGN_SPEC.md +scripts/ diagnostics and report figures +tests/ 280 tests ``` -Artifacts are written below the Git-ignored -`local_outputs/d2d_step2c_robustness/` directory. Every table and plot is -watermarked `DEBUG ONLY - NOT APPROVED FOR EXPERIMENT`; the source workbook is -hash/mtime checked before and after; no workbook writeback or real R2 proposal -is performed. A five-row consensus debug file is possible only in full mode and -only when every declared stability gate passes. Otherwise, the bundle records -the failed gates in `r1_no_stable_batch_reason.json`. - -The synthetic CI command creates no private fixture and cannot make the guarded -campaign command accept another workbook. It covers the real v3 read-only -adapter, GP/search orchestration, strict artifact validator, atomic replacement, -and an aggregate-only public summary ZIP on generated sanitized data. Private -campaign runs keep their complete evidence in a separately labelled -`*_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip` archive below the ignored output root. - ## Configuration -MOBO-Kit uses YAML configuration files. See `configs/` directory for examples: - -- `demo_config.yaml` - Basic configuration -- `configCSV_example_config.yaml` - Configuration from CSV metadata -- `d2d_step2b_debug.yaml` - Non-runnable public template; campaign runs require - a matching ignored private config and workbook -- `d2d_step2c_debug.yaml` - Public synthetic-CI template; private robustness - runs require explicit ignored campaign inputs - -### Configuration Structure +Objectives declare **what the model trains on** separately from **how that +becomes a utility**, because the two are not always the same column: ```yaml -inputs: - - name: "parameter1" - unit: "unit" - start: 0.0 - stop: 1.0 - step: 0.01 - objectives: - names: - - objective 1 - - objective 2 - - objective 3 - -constraints: - - clausius_clapeyron: true - ah_col: "absolute_humidity" - temp_c_col: "temperature_c" -``` - -Constraints are opt-in. A generic campaign configuration should use -`constraints: []`; the example above is only for a design that actually -contains the two named humidity/temperature inputs. - -## Package Structure - -``` -src/mobo_kit/ -├── main.py # Main API functions -├── cli.py # Command-line interface -├── design.py # Design space construction -├── data.py # Data loading and preprocessing -├── models.py # Gaussian Process models -├── acquisition.py # Acquisition functions and batch proposal -├── lhs.py # Latin Hypercube Sampling -├── plotting.py # Visualization tools -├── metrics.py # Performance metrics -├── constraints.py # Constraint handling -└── utils.py # Utility functions + contract_version: d2d-objectives-v2-nm-thickness + scaling_mode: fixed_affine + specs: + - name: thickness + model_source_column: "Thickness (avg)" # the GP trains on nanometres + transform: gaussian_target # utility peaks at the target + target: 650.0 + sigma: 176.7766952966369 + mean_function: # physics-informed trend + response: log + features: + - {column: speed_1, transform: log} + - {column: precur_conc, transform: log} ``` -## Troubleshooting +Objective scales are **fixed for the whole campaign** and must never be +re-derived from observed data — otherwise utility space moves between rounds and +hypervolume stops being comparable across them. +`assert_scaling_is_campaign_fixed` enforces this and runs inside +`build_objective_transform`, so no transform can bypass it. -### Common Installation Issues +## Current parameters -1. **Import errors**: Ensure all dependencies are installed: - ```bash - pip install -r requirements.txt - ``` +| Setting | Value | Config key | +|---|---|---| +| UCB beta (R1) | 4.0 | `rounds.r1.beta` | +| Local penalization radius | 0.25 | `local_penalization.radius` | +| Minimum batch spacing | 0.15 | `local_penalization.min_batch_distance` | +| Candidate pool | 32768 | `rounds.*.candidate_pool_size` | +| Posterior samples (R1) | 256 | `rounds.r1.posterior_samples` | +| MC samples (R2) | 128 | `rounds.r2.mc_samples` | +| GP variant | `dim_scaled_prior` | `model.variant` | +| Seed | 73 | `reproducibility.seed` | -2. **CUDA/GPU support**: Keep Torch at 2.8.0 and follow the official - platform-specific installation instructions linked above. GPU behavior was - not validated in the Step 1 baseline. +All of these are campaign configuration, not code. Tuning them does not require +touching the algorithm. -3. **Python version compatibility**: Python 3.11-3.12 is supported; the Step 1 - CPU baseline was tested with Python 3.12.10: - ```bash - conda create -n mobo-kit python=3.11 - conda activate mobo-kit - python -m pip install -r requirements/dev.txt - ``` +## History -4. **Jupyter notebook support**: - ```bash - pip install jupyter ipykernel - python -m ipykernel install --user --name=mobo-kit --display-name "Python (mobo-kit)" - ``` +The Step 1 / 2A / 2B / 2C audit apparatus was removed from this branch on +2026-07-29, once its findings were recorded in `docs/GP_MODEL_DECISION.md`. It +validated a GP model that has since been replaced. To recover any of it: -### Runtime Issues - -- **Memory issues**: For large datasets, consider using CPU instead of GPU or reducing batch sizes -- **Convergence issues**: The LOOCV function may have trouble converging on small noisy datasets -- **CUDA out of memory**: Reduce batch size or use CPU mode - -## Next Steps - -1. **Try the demo**: `mobo-kit run --csv data/processed/configCSV_example.csv --verbose` -2. **Generate initial experiments**: `mobo-kit generate --config configs/demo_config.yaml --n-samples 20 --out local_outputs/my_experiments.csv` -3. **Explore Jupyter notebooks** in the `notebooks/` directory -4. **Check configuration examples** in the `configs/` directory - -## Citation - -*Citation information will be added upon publication.* +```bash +git show pre-cleanup-2026-07-29:src/mobo_kit/.py +``` ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +MIT — see [LICENSE](LICENSE). -## Get in Touch +## Get in touch -For questions, issues, or contributions, please: -- Open an issue on GitHub -- Contact the development team -- Check the documentation in the `notebooks/` directory +Open an issue on GitHub, or contact the development team. diff --git a/configs/FA0.9CS0.1PbI3_260407_Config.yaml b/configs/campaign_d2d_perovskite.yaml similarity index 100% rename from configs/FA0.9CS0.1PbI3_260407_Config.yaml rename to configs/campaign_d2d_perovskite.yaml diff --git a/configs/d2d_step2a_provisional.yaml b/configs/d2d_step2a_provisional.yaml deleted file mode 100644 index b26447f..0000000 --- a/configs/d2d_step2a_provisional.yaml +++ /dev/null @@ -1,96 +0,0 @@ -# Step 2A configuration skeleton only. -# This file MUST NOT authorize a real candidate-generation run. - -schema_version: d2d-step2a-provisional-1 -campaign_id: PENDING -approved_for_production: false -approval: - approved_by: PENDING - approved_at: PENDING - decision_record: docs/D2D_MEETING_DECISIONS.md - -workbook: - profile: d2d_summary_v2 - sheet: Sheet1 - expected_content_range: A1:AC18 - expected_sha256: REQUIRED_IN_IGNORED_PRIVATE_CONFIG - input_aliases: - "precur_vol (uL)": precur_vol - -inputs: - - {name: speed_1, unit: rpm, start: 1000, stop: 6000, step: 500} - - {name: time_1, unit: s, start: 5, stop: 50, step: 5} - - {name: speed_2, unit: rpm, start: 0, stop: 5000, step: 500} - - {name: time_2, unit: s, start: 10, stop: 60, step: 5} - - {name: precur_conc, unit: M, start: 1.0, stop: 2.0, step: 0.05} - - {name: precur_vol, unit: uL, start: 40, stop: 200, step: 10} - - {name: anneal_temp, unit: C, start: 100, stop: 185, step: 5} - - {name: anneal_time, unit: min, start: 10, stop: 60, step: 5} - - {name: anti_vol, unit: uL, start: 100, stop: 200, step: 5} - - {name: anti_time, unit: s, start: 9, stop: 25, step: 2} - -objective_mapping_status: pending_meeting -objectives: - - name: uniformity_score - goal: maximize - model_source_column: PENDING_T_OR_OTHER - utility_transform: PENDING - scaling: PENDING - components: [Coverage, "1 - Uniformity", "Phase purity"] - - - name: optoelectronic_score - goal: maximize - model_source_column: PENDING_U_OR_OTHER - utility_transform: PENDING - scaling: PENDING - components: - - "PL - Implied Voc (Max) Normalized" - - "Photoconductance (Max) Normalized" - - - name: thickness - goal: target - model_source_column: PENDING_V_OR_W - target: 650.0 - target_transform: PENDING_GAUSSIAN_OR_OTHER - sigma: PENDING - scaling: PENDING - -reference_point_utility: PENDING - -qc_policy: - complete_case_rule: PENDING - failed_measurement_rule: PENDING - outlier_rule: PENDING - control_rule: PENDING - replicate_rule: PENDING - -constraints_status: pending_meeting -constraints: PENDING - -r1: - method: ucb_hvi - batch_size: 5 - beta: PENDING - posterior_samples: PENDING - candidate_pool_size: PENDING - -r2: - method: qlognehvi - batch_size: 3 - mc_samples: PENDING - candidate_pool_size: PENDING - sequential_pending: true - -local_penalization: - distance_metric: normalized_euclidean - radius: PENDING - min_batch_distance: PENDING - min_observed_distance: PENDING - dimension_weights: null - allow_hard_distance_relaxation: false - -reproducibility: - seed: PENDING - record_git_commit: true - record_environment_versions: true - record_resolved_config_hash: true diff --git a/configs/d2d_step2b_debug.yaml b/configs/d2d_step2b_debug.yaml deleted file mode 100644 index 4122782..0000000 --- a/configs/d2d_step2b_debug.yaml +++ /dev/null @@ -1,133 +0,0 @@ -# Public Step 2B algorithm-debugging template. This file never authorizes films. -# It intentionally has no private workbook identity and is not directly runnable. -schema_version: d2d-step2b-debug-1 -campaign_id: public-d2d-template-debug -template_only: true -run_mode: debug -debug_run_authorized: true -approved_for_production: false -approved_for_experiment: false - -decision_record: - source: public-template - status: runtime_private_config_required - known_data_issue: supplied_scores_must_be_validated_at_runtime - -workbook: - profile: d2d_summary_v3_scores - sheet: Sheet1 - expected_content_range: A1:AI20 - expected_sha256: REQUIRED_IN_IGNORED_PRIVATE_CONFIG - input_aliases: - "precur_vol (uL)": precur_vol - sample_id_column: Sample number - expected_sample_ids: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015] - -inputs: - - {name: speed_1, unit: rpm, start: 1000, stop: 6000, step: 500} - - {name: time_1, unit: s, start: 5, stop: 50, step: 5} - - {name: speed_2, unit: rpm, start: 0, stop: 5000, step: 500} - - {name: time_2, unit: s, start: 10, stop: 60, step: 5} - - {name: precur_conc, unit: M, start: 1.0, stop: 2.0, step: 0.05} - - {name: precur_vol, unit: uL, start: 40, stop: 200, step: 10} - - {name: anneal_temp, unit: C, start: 100, stop: 185, step: 5} - - {name: anneal_time, unit: min, start: 10, stop: 60, step: 5} - - {name: anti_vol, unit: uL, start: 100, stop: 200, step: 5} - - {name: anti_time, unit: s, start: 9, stop: 25, step: 2} - -objectives: - - name: uniformity_score - source_column: Uniformity score - excel_column: Z - direction: maximize - utility_transform: identity - support_formula: Coverage * (1 - Uniformity) * Phase purity - mismatch_policy: warn_use_supplied - - name: optoelectronic_score - source_column: Optoelectronic score - excel_column: AA - direction: maximize - utility_transform: identity - support_formula: log10((PL - Implied Voc (Max)) * Photoconductance (Max)) - mismatch_policy: error - - name: thickness_score - source_column: Thickness score - excel_column: AB - direction: maximize - utility_transform: identity - support_formula: exp(-((mean(valid T1:T4) - 650.0) / 250.0)^2) - target_nm: 650.0 - scale_nm: 250.0 - exponent_factor: 1.0 - exclude_columns: [T anom] - mismatch_policy: error - -ignored_model_columns: - - Stability score? - - Total combination - addition - - Total combination - multiplied - - Uniformity score absolute difference - - Optoelectronic score absolute difference - - Thickness absolute difference - - Total score absolute difference - -reference_point_utility: [-0.01, -10.0, -0.01] -constraints: [] - -r0: - condition_count: 15 - control_sample_ids: [1001] - include_control_in_debug_model: true - control_measurement_provenance_assumption: measured_in_synthetic_fixture - off_grid_observed_exceptions: - - sample_id: 1001 - input_name: speed_1 - observed_value_strategy: midpoint_between_first_two_grid_values - reason: algorithmic synthetic off-grid sentinel for public tests - retain_observed_value: true - -qc_policy: - final_scores_required: true - complete_case_rule: require_all_three_final_scores - failed_measurement_rule: block_row - outlier_rule: report_do_not_auto_remove - uniformity_known_mismatch: warn_and_continue_debug - optoelectronic_mismatch: fail - thickness_mismatch: fail - -r1: - method: ucb_hvi - batch_size_unique_conditions: 5 - replicates_per_condition: 3 - beta: 4.0 - posterior_samples: 256 - candidate_pool_size: 10000 - score_chunk_size: 512 - -r2_test_only: - method: qlognehvi - batch_size_unique_conditions: 3 - replicates_per_condition: 3 - mc_samples: 128 - candidate_pool_size: 5000 - sequential_pending: true - -local_penalization: - distance_metric: normalized_euclidean - radius: 0.25 - min_batch_distance: 0.15 - min_observed_distance: 0.0 - dimension_weights: null - allow_hard_distance_relaxation: false - -reproducibility: - seed: 73 - record_git_commit: true - record_environment_versions: true - record_resolved_config_hash: true - record_workbook_hash: true - -outputs: - root: local_outputs/d2d_step2b_debug - debug_watermark: DEBUG ONLY - NOT APPROVED FOR EXPERIMENT - write_source_workbook: false diff --git a/configs/d2d_step2c_debug.yaml b/configs/d2d_step2c_debug.yaml deleted file mode 100644 index 553aec7..0000000 --- a/configs/d2d_step2c_debug.yaml +++ /dev/null @@ -1,129 +0,0 @@ -# Public-safe Step 2C robustness template for deterministic synthetic debugging. -schema_version: d2d-step2c-robustness-debug-1 -campaign_id: d2d-step2c-public-debug -run_mode: debug -approved_for_experiment: false -approved_for_production: false -debug_watermark: "DEBUG ONLY - NOT APPROVED FOR EXPERIMENT" -base_step2b_config: configs/d2d_step2b_debug.yaml - -workbook: - source_kind: sanitized_synthetic_ci - -objectives: - order: [uniformity_score, optoelectronic_score, thickness_score] - source_columns: [Uniformity score, Optoelectronic score, Thickness score] - reference_point: [-0.01, -10.0, -0.01] - transforms: [identity, identity, identity] - directions: [maximize, maximize, maximize] - bounds: - uniformity_score: {lower: 0.0, upper: 1.0} - optoelectronic_score: {lower: null, upper: null} - thickness_score: {lower: 0.0, upper: 1.0} - known_uniformity_mismatch: - allowed_in_debug: true - blocks_experimental_approval: true - -r0: - inherit_from_base: true - -constraints: [] - -ucb_hvi: - moment_method: analytic_identity - beta_values: [1.0, 4.0, 9.0] - primary_beta: 4.0 - positive_hvi_threshold: 0.0 - numeric_tolerance: 1.0e-12 - score_chunk_size: 2048 - bound_policies: [none, clip_ucb] - primary_bound_policy: clip_ucb - mc_comparison_samples: 2048 - mc_comparison_seed: 73 - -candidate_search: - method: nested_sobol_grid_indices - scramble: true - primary_seed: 73 - secondary_seeds: [137, 911] - nested_unique_sizes: [16384, 32768, 65536, 131072] - primary_full_size: 131072 - preserve_accepted_prefix_nesting: true - -local_refinement: - enabled: true - anchors_per_selection_step: 64 - max_sweeps: 10 - improvement_tolerance: 1.0e-10 - coordinate_values: all_allowed_grid_values - stable_tie_break: lower_grid_index - -local_penalty_study: - hard_distance_relaxation: false - variants: - - {label: no_soft_no_hard, radius: null, min_batch_distance: 0.0} - - {label: no_soft_hard_0_15, radius: null, min_batch_distance: 0.15} - - {label: radius_0_15, radius: 0.15, min_batch_distance: 0.15} - - {label: radius_0_25, radius: 0.25, min_batch_distance: 0.15} - - {label: radius_0_35, radius: 0.35, min_batch_distance: 0.15} - primary_variant: radius_0_25 - min_observed_distance: 0.0 - dimension_weights: null - -models: - variants: - - {name: dim_scaled_prior, type: existing_default} - - name: conservative - type: explicit_conservative - min_noise: 0.01 - min_lengthscale: 0.05 - primary_for_debug: dim_scaled_prior - exact_leave_one_out: true - report_training_posterior_only_as_diagnostic: true - -observation_influence: - enabled: true - omitted_sample_ids: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015] - common_pool_size: 32768 - common_pool_seed: 73 - top_k: 100 - regional_thresholds: [0.10, 0.15, 0.20] - -robust_regions: - clustering: agglomerative_complete_link - primary_distance_threshold: 0.15 - sensitivity_thresholds: [0.10, 0.20] - shortlist_min: 8 - shortlist_max: 12 - consensus_batch_size: 5 - consensus_required_criteria: - largest_two_nested_regional_matches_within_0_15: 4 - largest_two_nested_mean_matched_distance_max: 0.10 - minimum_region_family_coverage: 3 - require_grid_valid: true - require_hard_distance_valid: true - -execution_modes: - fast: - nested_unique_sizes: [512, 1024, 2048, 4096] - anchors_per_selection_step: 8 - omitted_sample_ids: [1001, 1002, 1003] - mc_comparison_samples: 256 - full: - nested_unique_sizes: [16384, 32768, 65536, 131072] - anchors_per_selection_step: 64 - omitted_sample_ids: [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 1013, 1014, 1015] - mc_comparison_samples: 2048 - -reproducibility: - model_seed: 73 - record_git_commit: true - record_dirty_state: true - record_environment_versions: true - record_config_hash: true - record_workbook_hash_and_mtime_before_after: true - -outputs: - root: local_outputs/d2d_step2c_robustness - tracked_private_recipes: false - create_portable_ignored_zip: true diff --git a/configs/demo_config.yaml b/configs/demo_config.yaml deleted file mode 100644 index 96e8ecb..0000000 --- a/configs/demo_config.yaml +++ /dev/null @@ -1,50 +0,0 @@ -inputs: -- name: speed_inorg - unit: m/min - start: 0.25 - stop: 1.0 - step: 0.01 -- name: speed_org - unit: m/min - start: 0.25 - stop: 1.0 - step: 0.01 -- name: inkfl_inorg - unit: uL/min - start: 80.0 - stop: 240.0 - step: 1.0 -- name: inkfl_org - unit: uL/min - start: 100.0 - stop: 280.0 - step: 1.0 -- name: conc_inorg - unit: M - start: 0.8 - stop: 1.4 - step: 0.05 -- name: conc_org - unit: M - start: 0.4 - stop: 1.2 - step: 0.05 -- name: temperature_c - unit: F - start: 20.0 - stop: 50.0 - step: 1.0 -- name: absolute_humidity - unit: g/m^3 - start: 2.0 - stop: 37.0 - step: 1.0 -objectives: - names: - - PCE - - Stability - - Repeatability -constraints: -- clausius_clapeyron: true - ah_col: absolute_humidity - temp_c_col: temperature_c diff --git a/configs/auto_config.yaml b/configs/example_demo.yaml similarity index 100% rename from configs/auto_config.yaml rename to configs/example_demo.yaml diff --git a/configs/configCSV_example_config.yaml b/configs/example_from_csv.yaml similarity index 100% rename from configs/configCSV_example_config.yaml rename to configs/example_from_csv.yaml diff --git a/docs/CAMPAIGN_STATUS.md b/docs/CAMPAIGN_STATUS.md index 0141e25..91a31a9 100644 --- a/docs/CAMPAIGN_STATUS.md +++ b/docs/CAMPAIGN_STATUS.md @@ -8,7 +8,7 @@ R2 qLogNEHVI (3), three replicate films per condition, 23 distinct conditions. ```python from mobo_kit.campaign import load_campaign_config, run_r0_lhs, run_r1_ucb, run_r2_qlognehvi -config = load_campaign_config("configs/FA0.9CS0.1PbI3_260407_Config.yaml") +config = load_campaign_config("configs/campaign_d2d_perovskite.yaml") r0 = run_r0_lhs(config, n=15) # space-filling, no model r1 = run_r1_ucb(config, X_phys, Y_model, n=5) # UCB-HVI + local penalisation diff --git a/docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md b/docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md deleted file mode 100644 index 4537dbf..0000000 --- a/docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md +++ /dev/null @@ -1,154 +0,0 @@ -# D2D MOBO Meeting Decision Record - -**Meeting date:** -**Participants:** -**Recorded by:** -**Campaign/composition:** -**Decision-record version:** - -Complete this document after the experimental-team meeting. Replace every -`PENDING` entry and record who approved it. This document should become the -source for the frozen Step 2B production configuration. - -## 1. R0 dataset and row roles - -- Number of R0 recipe executions: `PENDING` -- Are the 15 workbook rows the complete R0 set? `PENDING` -- Control recipe(s) and sample ID(s): `PENDING` -- Do controls train the GP? `PENDING` -- Replicate rows/groups: `PENDING` -- Replicate aggregation/noise policy: `PENDING` -- Required status/QC fields before model fitting: `PENDING` -- Missing-one-objective rule: `PENDING` -- Failed-film rule: `PENDING` -- Outlier/exclusion approval owner: `PENDING` - -## 2. Final three model outputs and BO utilities - -### Objective 1 — Uniformity - -- Model source column: `PENDING` -- Utility/source column: `PENDING` -- Direction: `maximize` -- Is T supplied externally or calculated by Python? `PENDING` -- Exact equation using L/N/O: `PENDING` -- Component weights: `PENDING` -- Component normalization/clipping: `PENDING` -- Missing/failure semantics: `PENDING` -- Approved by: `PENDING` - -### Objective 2 — Optoelectronic - -- Model source column: `PENDING` -- Utility/source column: `PENDING` -- Direction: `maximize` -- Is U supplied externally or calculated by Python? `PENDING` -- Raw PL normalization anchor/formula: `PENDING` -- Raw photoconductance normalization anchor/formula: `PENDING` -- Exact Q/S combination and weights: `PENDING` -- Linear/log treatment: `PENDING` -- Missing/zero/failure semantics: `PENDING` -- Approved by: `PENDING` - -### Objective 3 — Thickness - -- Model source: `V raw thickness` / `W precomputed score` / other: `PENDING` -- Target: `650 nm` — confirm: `PENDING` -- Utility transform: `PENDING` -- Sigma/tolerance/scale: `PENDING` -- Symmetric about target? `PENDING` -- Clipping/bounds: `PENDING` -- Is W calculated by Python or supplied externally? `PENDING` -- Approved by: `PENDING` - -### Final objective-column decision - -- Final mapping: `T,U,W` / `T,U,V` / other: `PENDING` -- Rationale: `PENDING` - -## 3. Fixed utility scales and reference point - -- Are all final BO utilities on `[0,1]`? `PENDING` -- Fixed scale/anchor for Uniformity: `PENDING` -- Fixed scale/anchor for Optoelectronic: `PENDING` -- Fixed scale/anchor for Thickness utility: `PENDING` -- Out-of-range/clipping policy: `PENDING` -- Fixed utility-space hypervolume reference point: `PENDING` -- Reference-point rationale: `PENDING` -- Must remain fixed across R0/R1/R2? `PENDING` -- Approved by: `PENDING` - -## 4. Process and equipment constraints - -List every rule in physical units. Use `NONE — constraints: [] approved` only if -no rules apply. - -| ID | Rule | Variables | Hard/soft | Rationale | Approved by | -|---|---|---|---|---|---| -| C1 | PENDING | | | | | - -Specific checks: - -- Rule when `speed_2 = 0`: `PENDING` -- Antisolvent-time relation to spin time: `PENDING` -- Allowed antisolvent volume/time combinations: `PENDING` -- Anneal temperature/time restrictions: `PENDING` -- Concentration/volume restrictions: `PENDING` -- Equipment-resolution restrictions beyond configured steps: `PENDING` -- Resolution of the local off-grid input discrepancy versus the approved input - contract: `PENDING` - -## 5. R1 UCB-HVI settings - -- Method approved: `ucb_hvi` — `PENDING` -- Batch size: `5` — `PENDING` -- Beta: `PENDING` -- Equivalent kappa `sqrt(beta)`: `PENDING` -- Latent posterior or observation-noise posterior: `PENDING` -- Posterior MC samples for utility moments: `PENDING` -- Candidate pool size: `PENDING` -- Seed policy: `PENDING` -- Approval/delegation owner: `PENDING` - -## 6. R2 qLogNEHVI settings - -- Method approved: `qlognehvi` — `PENDING` -- Batch size: `3` — `PENDING` -- MC samples: `PENDING` -- Candidate pool size: `PENDING` -- Sequential pending-point selection approved? `PENDING` -- Native joint-q comparator required? `PENDING` -- Seed policy: `PENDING` -- Approval/delegation owner: `PENDING` - -## 7. Local penalization and diversity - -- Distance metric: normalized Euclidean / weighted / other: `PENDING` -- Soft penalty formula approved: `PENDING` -- Penalty radius: `PENDING` -- Minimum within-batch distance: `PENDING` -- Minimum distance from observed/pending points: `PENDING` -- May hard distances ever be relaxed? Recommended `No`: `PENDING` -- Required numerical diversity report: `PENDING` -- Required plots: `PENDING` -- Approval/delegation owner: `PENDING` - -## 8. Workbook and score ownership - -- Who enters raw characterization values? `PENDING` -- Who approves/enters final T/U/W scores? `PENDING` -- Should Python calculate any scores? `PENDING` -- Source-of-truth workbook location: `PENDING` -- One workbook for full campaign or one per round? `PENDING` -- Sample-ID convention: `PENDING` -- Backup/audit requirement: `PENDING` - -## 9. Production approval - -- Configuration version approved for dry-run: `PENDING` -- Configuration version approved for real R1: `PENDING` -- Scientific approver(s): `PENDING` -- Computational approver(s): `PENDING` -- Experimental operator approver(s): `PENDING` -- Approval date/time: `PENDING` -- Notes/conditions: `PENDING` diff --git a/docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md b/docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md deleted file mode 100644 index c83795b..0000000 --- a/docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md +++ /dev/null @@ -1,97 +0,0 @@ -# D2D Objective Contract — Provisional, Not Approved - -## Status - -This document records current information without turning it into a production -decision. `configs/d2d_step2a_provisional.yaml` deliberately contains -placeholders and `approved_for_production: false`; the production validator must -reject it. - -## Updated workbook profile - -The updated private workbook was audited read-only from an ignored, explicitly -supplied local path. Its filename and content identity are intentionally absent -from tracked documentation. - -- Sheet and content range: `Sheet1`, `A1:AC18` -- 29 columns; 15 recipe rows in Excel rows 2–16 -- Canonical optimizer inputs: B:K -- Explicit display alias: `precur_vol (uL)` to `precur_vol` -- No second annealing-temperature column -- Scores: T `Uniformity score`, U `Optoelectronic score` -- Thickness: V raw average, W normalized target-score candidate -- Y has a blank header and must remain visible in the audit -- No formulas or populated objective values - -The local workbook contains an off-grid input discrepancy relative to the -provisional design contract. The v2 auditor exposes the discrepancy and does -not snap or reinterpret private recipe values. The meeting must decide whether -the source is a transcription issue, an intentional off-grid execution, or -evidence that the approved input grid needs revision. - -The supplied instruction text states that the row-18 notes are in P18/R18. -Direct cell inspection of the supplied workbook instead finds the note text in -**Q18/S18**, under the normalized PL and normalized photoconductance columns; -P18/R18 are blank. The contract fixture follows the pack's P18/R18 locations, -while a separate sanitized anomaly fixture and the local audit report the -Q18/S18 deviation without changing the workbook. - -## Current scientific intent - -The current, still provisional goals are: - -1. maximize uniformity performance; -2. maximize optoelectronic performance; and -3. match thickness to 650 nm. - -Aleks identified L (`Coverage`), N (`1 - Uniformity`), and O (`Phase purity`) as -uniformity components, and Q/S as normalized optoelectronic components. This is -not enough to calculate T or U: equations, weights, anchors, clipping, and -failure semantics remain unresolved. - -## Unresolved objective-source choice - -Step 2A supports both possible architectures but approves neither: - -| Candidate mapping | Model outputs | Utility treatment | -|---|---|---| -| `T,U,W` | three precomputed scores | identity maximize only after the three columns and formulas are approved | -| `T,U,V` | two precomputed scores plus raw thickness | transform every raw thickness posterior sample through the approved target utility | - -The generic engine supports a Gaussian target utility and a negative-absolute -comparison utility. The workbook's `sigma = 250` header is not treated as final -approval for either the model-source choice or the production transform. - -## Objective specification requirements - -Each approved objective must ultimately provide: - -- a unique objective name and exact workbook/model source column; -- goal (`maximize`, `minimize`, or `target`); -- a versioned transform and fixed parameters; -- fixed affine anchors or a declaration that the source is an already-approved - utility; -- clipping behavior; -- formula ownership and missing/failure behavior. - -The transform outputs all-maximize utilities. Pareto analysis, UCB-HVI, -qLogNEHVI, and the reference point must all use that same transformed space. - -## Decisions required before production - -- final `T,U,W`, `T,U,V`, or other mapping; -- exact T and U formulas, or confirmation that externally approved values are - entered directly; -- final thickness model source, target transform, sigma/scale, and symmetry; -- fixed utility scales, clipping, and fixed utility-space reference point; -- R0 row inclusion, control, replicate, QC, missing-data, failure, and outlier - policies; -- complete physical/equipment constraints, including an explicitly approved - empty list if none apply; -- R1 beta, posterior policy/sample count, pool size, and seed policy; -- R2 sample count, pool size, comparator policy, and seed policy; -- local-penalty radius and hard distance settings; -- approval provenance and a frozen resolved-config hash. - -Until these are encoded and approved, only schema audits and explicitly -synthetic low-level examples are permitted. diff --git a/docs/D2D_STEP2A_COMPUTATIONAL_CORE.md b/docs/D2D_STEP2A_COMPUTATIONAL_CORE.md deleted file mode 100644 index 582293a..0000000 --- a/docs/D2D_STEP2A_COMPUTATIONAL_CORE.md +++ /dev/null @@ -1,204 +0,0 @@ -# D2D Step 2A Computational Core - -## Scope and safety boundary - -Step 2A supplies a generic, synthetic-tested engine for discrete R1 UCB-HVI -and R2 qLogNEHVI batch construction. It does **not** authorize a D2D campaign -proposal. The workbook objective mapping, utility formulas, fixed scales, -reference point, QC/control policy, physical constraints, and acquisition -settings remain subject to approval. - -The production gate is separate from the mathematical functions. Low-level -functions may run with visibly test-only synthetic settings; a workbook- or -campaign-facing run must pass `validate_production_config` first. During Step -2A, `run_mobo_experiment(..., propose_candidates=True)` is blocked before CSV -parsing, model fitting, or output creation. Even an approved configuration is -rejected there because the old runner does not use this objective contract, -discrete pool, or shared selector; wiring those pieces is a Step 2B task. - -## Coordinate and outcome spaces - -| Space | Representation | Used for | -|---|---|---| -| Physical input | Configured laboratory units on exact grids | worklists and physical constraints | -| Normalized input | Each input mapped to `[0, 1]` | GP inputs, candidate distances, local penalties | -| Raw/model outcome | Measurements fitted by the GP models | posterior sampling | -| Transformed utility | Every objective oriented so larger is better | Pareto filtering, hypervolume, UCB-HVI, qLogNEHVI | - -Raw outcomes and transformed utilities are never interchanged. In particular, -the fixed reference point is always expressed in transformed utility space. - -## Objective transformations - -`mobo_kit.objectives` defines an immutable `ObjectiveSpec`, a validated -`ObjectiveTransform`, and a BoTorch-compatible -`ConfiguredMCMultiOutputObjective`. Tensors use the shape contract -`[..., M] -> [..., M]`, preserving floating dtype and device. - -For fixed anchors `lo < hi`: - -```text -maximize: u(y) = (y - lo) / (hi - lo) -minimize: u(y) = (hi - y) / (hi - lo) -``` - -The target transformations are: - -```text -Gaussian: u(y) = exp(-0.5 * ((y - target) / sigma)^2) -Negative absolute u(y) = -abs(y - target) / scale -``` - -`sigma` or `scale` must be explicit, finite, and positive. Identity is valid -only for an already approved maximize utility. No anchor or reference point is -estimated from campaign observations. - -Nonlinear transformations are applied to every raw posterior Monte Carlo -sample. For candidate `x`: - -```text -Y_raw^(s)(x) ~ posterior(model, x) -U^(s)(x) = transform(Y_raw^(s)(x)) -mu_U(x) = mean_s U^(s)(x) -sigma_U(x) = population_std_s U^(s)(x) -``` - -The default is the latent posterior (`observation_noise=False`). Posterior -sampling uses a caller-supplied seed and Sobol QMC samples. - -## Discrete candidate pool - -`sample_discrete_candidate_pool` samples integer index tuples directly from the -configured axes. It never materializes the 177,816,994,740-point D2D Cartesian -product. Each accepted index tuple is converted exactly to a physical grid row -and then normalized. - -Observed, pending, and explicit avoid rows are converted back to integer grid -indices and excluded. Opt-in physical row constraints are evaluated only after -conversion to laboratory units. A request either returns the exact requested -pool in deterministic order for its seed or raises -`CandidatePoolSamplingError` with draw and rejection statistics. Constraints or -duplicate rules are never relaxed. - -## R1 UCB-HVI - -`posterior_utility_moments` evaluates candidates as singleton q-batches in -CPU-safe chunks. `score_ucb_hvi_pool` then forms, for each transformed utility -dimension, - -```text -kappa = sqrt(beta) -UCB_j(x) = mu_U,j(x) + kappa * sigma_U,j(x), beta >= 0 -``` - -For the non-dominated observed utility set `P` and explicit utility-space -reference `r`, the deterministic base score is - -```text -a_UCB-HVI(x) = HV(P union {UCB(x)}; r) - HV(P; r). -``` - -True dominated or non-contributing points retain a raw score of zero. A small -epsilon is used only to represent scores in log space. The proposal wrapper -requires enough candidates above its explicit positive-HVI threshold; it does -not fill a batch with arbitrary zero-HVI points. - -Public scoring APIs: - -- `posterior_utility_moments` -- `hypervolume_improvement_scores` -- `score_ucb_hvi_from_moments` -- `score_ucb_hvi_pool` -- `propose_ucb_hvi_batch` - -## Shared local-penalized selector - -Both acquisition methods use `select_local_penalized_batch`. For normalized -inputs and optional positive dimension weights `w`, distance is - -```text -d_w(x, z) = sqrt(sum_j w_j * (x_j - z_j)^2). -``` - -After selecting `x_i`, the soft exclusion factor applied to a remaining point -is - -```text -phi_i(x) = 1 - exp(-0.5 * (d_w(x, x_i) / rho)^2), rho > 0. -``` - -The selector operates in log space: - -```text -log a_pen(x) = log a_base(x) + sum_i log(max(phi_i(x), epsilon)). -``` - -Hard minimum selected-to-selected and optional selected-to-observed/pending -distances are masked before selection. Ties retain stable candidate-pool order. -If the exact batch is impossible, `UndersizedBatchError` reports the requested -and selected sizes, active thresholds, and remaining count; no fallback relaxes -the rules. - -## R2 qLogNEHVI - -`score_qlognehvi_singletons` constructs BoTorch 0.15.1 -`qLogNoisyExpectedHypervolumeImprovement` with: - -- the fitted `ModelListGP`; -- normalized `train_X` as `X_baseline`; -- the same configured Monte Carlo objective used by UCB-HVI; -- the same explicit transformed-utility reference point; -- a seeded Sobol sampler; and -- explicit pending points. - -The scorer requires `ConfiguredMCMultiOutputObjective`; passing `None` or an -unversioned arbitrary objective fails before BoTorch can silently operate in raw -outcome space. The utility reference dimension is checked against both the -objective contract and the model output count. - -The pool is evaluated in shape `N x 1 x D` and in caller-controlled chunks. -`propose_qlognehvi_penalized_batch` recomputes singleton base scores at every -selection step. Its pending set is the pre-existing pending set plus all points -already selected in the new batch. The shared selector then applies the same -soft penalty and hard distance policy used by UCB-HVI. - -## Diagnostics - -`mobo_kit.candidate_diagnostics` provides: - -- within-batch pairwise normalized distances and min/mean/max summaries; -- nearest observed/pending distance per candidate; -- duplicate, grid-membership, and normalized-boundary checks; -- PCA, selected-condition parallel coordinates, distance heatmap, and - base-versus-penalized acquisition plots. - -Selection results retain pool indices, order, base/log scores, penalty factors, -nearest distances, acquisition-specific diagnostics, seeds, and settings. -Plot functions use the headless `Agg` backend and write only to caller-supplied -paths. The Step 2A example uses ignored `local_outputs/` and writes a strict -JSON provenance report containing method/contract versions, every seed, -pool-draw statistics, beta/kappa or MC settings, local-penalty settings, -per-selection scores, UCB utility moments, qLogNEHVI pending counts, and runtime -versions. It contains no campaign recipes. - -## Determinism and performance - -- Pool sampling uses a seeded NumPy `Generator` and stable acceptance order. -- Posterior and qLogNEHVI Monte Carlo sampling use explicit Sobol seeds. -- Candidate-pool evaluation has explicit chunk sizes. -- Sequential distance work is `O(N*q*D)`; no pool-wide `N x N` matrix is made. -- Hypervolume work is singleton candidate scoring against the fixed observed - Pareto set. -- CPU floating-point reductions can differ below normal numerical tolerances - across chunk shapes; selected indices are tested for deterministic reruns. - -## Limitations before Step 2B - -Step 2A does not ingest completed R0 outcomes, write workbooks, manage campaign -state, or generate real candidates. The exact production objective contract and -all fields rejected by the gate must be resolved at the experimental-team -meeting and frozen in a versioned configuration before a dry run. The read-only -v2 workbook profile requires the exact 29-column header tuple, sample rows 2-16, -numeric sample identifiers 1-15, blank row 17, and content range A1:AC18. A -local off-grid discrepancy is reported without snapping or copying the private -condition into tracked fixtures. diff --git a/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md b/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md deleted file mode 100644 index d3fc8b3..0000000 --- a/docs/D2D_STEP2C_ROBUSTNESS_METHOD.md +++ /dev/null @@ -1,209 +0,0 @@ -# D2D Step 2C: R1 Robustness, Search Convergence, and Proposal Stabilization - -## Status and safety boundary - -Step 2C is a read-only computational audit. It does not approve fabrication, -change the campaign workbook, generate a real R2 proposal, or change the Sample -1 inclusion policy. All outputs carry: - -```text -DEBUG ONLY - NOT APPROVED FOR EXPERIMENT -``` - -The campaign runner accepts only the pinned private workbook and fail-closed -debug configuration. It verifies the workbook SHA-256 and modification -timestamp before computation, after computation, and after artifact export. -Output is limited to the configured Git-ignored directory and is published from -a fully validated staging directory. A separate synthetic-CI helper generates a -sanitized workbook under the ignored output root and exercises the same internal -orchestration without making the campaign command redirectable. - -The known supplied-Uniformity mismatch and the off-grid control exception remain -visible and continue to block experimental approval. - -## Objective contract - -The three supplied final scores in columns Z, AA, and AB are used directly, in -this order: - -1. Uniformity score; -2. Optoelectronic score; -3. Thickness score. - -All three are maximized. The objective transform is identity for Step 2C. The -declared UCB-support policy clips only Uniformity and Thickness UCB coordinates -to `[0, 1]`; it never clips training targets or the Optoelectronic score. - -## GP validation - -Two explicit model variants are evaluated: - -- `dim_scaled_prior`, matching the Step 2B model; -- `conservative`, with documented observation-noise and ARD-lengthscale floors. - -Every fit is strict: failed optimization raises rather than silently returning -an unfitted model. Exact leave-one-out validation fits 15 folds per variant and -reports per-observation predictive means, predictive uncertainty, residuals, -standardized residuals, interval inclusion, and objective-level accuracy and -calibration metrics. Full-fit and fold hyperparameters, optimizer warnings, and -leave-one-out hyperparameter stability summaries are exported separately. ARD -diagnostics flag normalized lengthscales at or below 0.05 as very small and at -or above 10.0 as extremely large/flat. Candidate posterior means outside the -observed range and outside declared objective bounds are reported separately. - -Training-posterior diagnostics are not substituted for leave-one-out results. - -## Analytic identity moments and UCB-HVI - -Because every Step 2C objective transform is identity, posterior mean and -standard deviation are obtained analytically from the GP posterior. The primary -search is therefore deterministic and does not depend on a Monte Carlo seed. - -A fixed-seed Monte Carlo comparison remains in the audit. It records moment -differences, selection correspondence, runtime, per-objective tolerances of -`max(0.02, 0.02 * max(observed_range, 1.0))`, and pass/fail flags. This -comparison is diagnostic and does not relax any consensus gate. - -Candidate utility is computed as: - -```text -UCB = posterior mean + sqrt(beta) * posterior standard deviation -``` - -followed by the selected UCB-bound policy and deterministic hypervolume -improvement relative to the declared reference point. - -## Nested Sobol search and exact-grid refinement - -Full mode uses exact accepted-unique scrambled Sobol prefixes of 16,384, -32,768, 65,536, and 131,072 points with primary seed 73. Every smaller accepted -pool is an exact prefix of the next. Full-size secondary scramble seeds 137 and -911 measure scramble sensitivity. Pool hashes and rejection counters prove the -search basis without materializing the full Cartesian grid. - -At each sequential batch step, the highest eligible pool anchors and eligible -previously discovered optima are refined by deterministic coordinate ascent. -Every allowed value of one grid dimension is evaluated at a time. Stable -lexicographic tie-breaking, positive-HVI eligibility, observed-row exclusion, -hard spacing, soft penalty, sweep limits, and termination reasons are recorded. -The trace includes anchor pool indices, start/end base HVI, start/end penalized -log score, accepted moves, changed dimensions, and aggregate sweeps. - -Each nested size exports both the unrefined pool batch and refined batch, -including refinement gain, HVI summaries, boundary counts, matched distances, -relative regret, and runtime. - -## One-factor robustness studies - -The core candidate-region registry contains 13 unique runs spanning: - -- four nested-search sizes; -- two alternate Sobol scrambles plus the primary reference; -- default and conservative models; -- unclipped and clipped UCB policies; -- beta values 1, 4, and 9; -- no-soft and three soft-radius penalty settings under fixed hard spacing. - -The primary run acts as the shared reference level. Family-level persistence is -weighted equally so families with more variants do not dominate. - -The local-penalty table separates the hard-spacing effect from the soft-penalty -effect: soft-radius variants are compared with the no-soft run that retains the -same hard spacing. Acquisition sacrifice, diversity, penalty factors, boundary -behavior, nearest-observed distance, runtime, and region change are retained. -Its human-readable activity classification considers regional correspondence -and minimum/mean pairwise-distance changes, not only score attenuation. The -overall soft-penalty interpretation is derived from `radius_*` variants only; -hard spacing is interpreted separately. - -## Observation influence - -The full model and every exact leave-one-out model are evaluated on the same -accepted pool and use the same local-refinement settings. The report keeps the -following components visible: - -- exact and regional batch displacement; -- prediction changes at the full-model batch and robust-region medoids; -- common-pool acquisition rank and top-K changes; -- observed Pareto membership changes; -- hyperparameter displacement; -- fit/proposal runtime and fitting-warning counts. - -The composite influence rank is a summary, not a replacement for these -components. Sample 1 remains included in the primary model regardless of its -diagnostic rank. - -## Robust regions and shortlist - -Core candidates are clustered in normalized input space with deterministic -agglomerative complete linkage. The primary distance threshold is 0.15, with -0.10 and 0.20 sensitivity counts. Each region exposes its medoid, diameter, -run/family coverage, equal-weight study-family persistence, within-run normalized -HVI statistics, prediction summaries, nearest-observed distance, boundary -frequency, and full-versus-omit-Sample-1 correspondence. - -The ignored robust shortlist contains 8-12 diverse medoids where geometry -permits. It includes predictions and uncertainty under both models, raw and -bounded UCB coordinates, observed-range flags, boundary flags, and omission -sensitivity at each medoid. - -Lower- and upper-boundary counts, rates, and enrichment are kept separate in -candidate, influence, shortlist, and plotting artifacts. This prevents opposite -boundary tendencies from cancelling in a combined statistic. - -## Future qLogNEHVI compatibility - -Step 2C does not generate a real R2 batch. The bounded posterior-sample objective -is nevertheless integrated with the singleton qLogNEHVI scorer and tested on a -small real BoTorch model with a fixed reference point and seed. Bounds apply only -to posterior utility samples; training targets remain unchanged. - -## Consensus gate - -A five-row `r1_consensus_debug_batch.csv` can be created only in full mode and -only when all declared checks pass on those exact five rows: - -1. the two largest nested searches match at least 4/5 regions within 0.15; -2. their mean matched distance is at most 0.10; -3. at least five regions, including every chosen region, cover at least three - non-baseline core study families; -4. the chosen medoids are finite, bounded, unique, exactly on-grid, and satisfy - the 0.15 hard pairwise distance; -5. debug-only and both approval-false flags are intact. - -Fast mode is categorically ineligible. If any check fails, the runner creates -`r1_no_stable_batch_reason.json` and does not create a consensus batch. - -Before publication, the validator independently checks required table schemas, -provenance fields, the canonical resolved/source config relationship, workbook -proofs, exact conditional artifacts, debug stamps, PNG metadata, and supported -file formats. It recomputes the convergence and family gates from their CSVs, -links robust regions to the shortlist and the shortlist to any consensus rows, -and requires the no-stable reason to reproduce the failed checks and observations -exactly. The ignored output directory remains the complete local evidence -surface. For private campaign runs, its full ZIP is explicitly named -`*_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip` and carries a warning because it contains -local provenance, sample-level data, and exact candidate recipes. The legacy -`.zip` sibling is now a separate, strict public-summary export: it contains -only allowlisted aggregate tables plus a sanitized manifest and excludes local -paths, profile names, workbook filenames, sample-level rows, recipes, and -recipe-coordinate plots. The directory and both requested ZIPs are published in -one rollback-safe transaction; prior artifacts are restored if publication or -either final validation fails. - -## Running the audit - -From the repository root: - -```bash -# Sanitized generated-data end-to-end CI coverage -python examples/d2d_step2c_synthetic_ci.py --overwrite - -python examples/d2d_step2c_robustness.py --mode fast -python examples/d2d_step2c_robustness.py --mode full -``` - -The synthetic command is the self-contained generated-data end-to-end CI path. -Campaign fast mode is a private-workbook integration smoke only. Full mode is the -declared robustness study. Every mode is ignored, read-only with respect to its -source workbook, and debug-only. diff --git a/docs/GP_MODEL_DECISION.md b/docs/GP_MODEL_DECISION.md index f1f655f..3910531 100644 --- a/docs/GP_MODEL_DECISION.md +++ b/docs/GP_MODEL_DECISION.md @@ -288,7 +288,7 @@ points that disagree. - Wire the structured mean into the campaign path before generating R1 candidates for fabrication. -- Hypervolume reference: fixed. `configs/FA0.9CS0.1PbI3_260407_Config.yaml` now +- Hypervolume reference: fixed. `configs/campaign_d2d_perovskite.yaml` now declares `reference_point_utility` in utility space after the transforms, so no axis dominates. The old raw-scale `[-0.01, -10.0, -0.01]` gave the optoelectronic axis 4.01x the uniformity axis. diff --git a/docs/REPO_AUDIT_D2D.md b/docs/REPO_AUDIT_D2D.md deleted file mode 100644 index f2064dc..0000000 --- a/docs/REPO_AUDIT_D2D.md +++ /dev/null @@ -1,142 +0,0 @@ -# MOBO-Kit Repository Audit for the D2D Campaign - -## Snapshot - -- Repository: `PV-Lab/MOBO-Kit` -- Verified default branch: `main` -- Baseline/main commit: `459afc6d06f66f4d5fb48c3f563e65c61f5df16f` -- Reference branch: `Nicky's-MOBO-Testground` -- Reference tip: `546a5bbdf8fc2f6f9fc6764ada5f5d587e1faa6f` -- Step 1 feature branch: `feature/d2d-mobo-step1-baseline` -- Reference comparison: 10 commits ahead, 0 behind, 59 changed paths, - 15,887 insertions, and 166 deletions - -The remote was fetched before branching. The reference branch was inspected by -explicit Git refs only and was not merged. - -## Architecture and current workflow - -The package separates design construction (`design.py`), normalization and grid -snapping (`data.py`), constraints (`constraints.py`), LHS (`lhs.py`), independent -GP fitting (`models.py`), qNEHVI/qLogNEHVI (`acquisition.py`), metrics -(`metrics.py`), plots (`plotting.py`), the high-level runner (`main.py`), and CLI -dispatch (`cli.py`). That is a workable engine boundary for a future thin Excel -adapter. - -The existing runner assumes all objectives are maximized, fits one `SingleTaskGP` -per output in a `ModelListGP`, continuously optimizes qNEHVI/qLogNEHVI in the -normalized cube, snaps to the physical grid, and writes an append-style CSV. It -has no campaign/round/sample/status state model and neither branch contains the -required R1 UCB. - -## Verified main-branch defects - -1. **Broken YAML-to-LHS path.** `generate_initial_experiments` passed a raw config - dictionary to `build_design`, which expects `InputSpec` objects. -2. **First experiment dropped.** `pandas.read_csv` had already consumed the - header, but `split_XY` sliced at `iloc[6:]`; the first demo experiment is at - DataFrame row 5. -3. **Fixed-offset parsing.** CSV metadata, separator, and data rows were assumed - rather than detected. Plain data and malformed metadata were accidental - behaviors. -4. **Implicit constraint.** Generic CSV conversion always injected the legacy - humidity/temperature Clausius-Clapeyron constraint. -5. **Unsafe metadata defaults.** Malformed start/stop/step cells could be replaced - with generic numeric defaults instead of failing. -6. **Inconsistent public types.** `split_XY` was annotated to return arrays but - returned DataFrames; `csv_to_config` was annotated as `str` but returned a - dictionary and wrote YAML as a side effect. -7. **Incomplete objectives accepted.** Partial/all-blank objective rows could - reach tensor/GP code as NaNs; blanks were not distinguished from real zeros. -8. **Non-deterministic/unsafe LHS.** Attempts were reseeded, subset selection used - a separate unseeded RNG, snapped duplicates were not removed, correlation - thresholds could be violated, and the final fallback bypassed constraints. -9. **Latent plotting failure.** LHS diagnostics referenced nonexistent - `design.labels`. -10. **Reference-point mismatch.** The high-level runner computed one reference - for reporting, then optimized with a hard-coded `[-0.01] * M` vector. -11. **Candidate failure hidden.** Proposal exceptions and short/empty batches - could still lead to a top-level `status: success`. -12. **Grid collisions/observed duplicates.** Continuous optima can snap to the - same recipe; the main path has no final deduplication or observed-point - exclusion. -13. **Round-trip mismatch.** LHS emits input-only CSV while the runner expected a - metadata-style CSV with objectives. -14. **CLI drift.** README examples omitted the required `run` subcommand and - `--num-restarts` was parsed but unused. -15. **Packaging drift.** Install URLs and the `all` extra still referenced - MOBO-FOM; dependency minimums admitted mutually incompatible future stacks. -16. **Stale tests.** Existing tests imported removed `src.*` modules and expected - APIs/files that no longer exist, so they did not validate the packaged code. -17. **Tracked caches.** Main tracked Python bytecode under `src/__pycache__` and - `tests/__pycache__` despite ignore rules. - -The main branch also contains pre-existing demonstration result images/CSVs. -Step 1 classifies them as published demo artifacts and does not regenerate or -expand them; generated campaign outputs and private inputs remain ignored. - -## D2D reference-branch findings - -The reference branch contains useful research ideas: large discrete candidate -pools, observed-point exclusion, deduplication, discrete qNEHVI/local search, -normalized linear constraints, posterior diagnostics, conservative noise/kernel -options, and encoding fallback. These need selective review and synthetic tests -in later steps. - -It is not an authoritative executable D2D pipeline: - -- its D2D YAML defines ten inputs but enables a Clausius-Clapeyron constraint on - `absolute_humidity` and `temperature_c`, neither of which exists in that design; -- its D2D CSV is inherited from the eight-input slot-die example, conflicts with - the YAML, and has declared/data field-count mismatches; -- notebooks import `MixedMCMultiOutputObjective`, which is absent from the active - package and exists only in a stale `src/mobo_kit OLD` copy; -- notebook objective directions include a 650-nm thickness match, but qNEHVI - calls omit that transform while using a transformed reference point; -- posterior non-domination calculations then compare raw outputs as if all were - maximized; -- the automatically derived reference changes with the observed dataset, so - round-to-round hypervolume would not be comparable; -- the diversity selector is post-hoc Euclidean reranking, not documented local - hypervolume penalization, and its fallback can bypass the configured minimum - distance; -- notebooks contain personal macOS Dropbox paths, undefined variables, - inconsistent `str`/`Path` operations, stale campaign names, embedded outputs, - and saved execution errors; -- caches, egg-info, a complete old package copy, and generated results are - committed; and -- no tests accompany the large acquisition/model changes. - -The changed `results/experiment/next_batch.csv` is legacy slot-die data, not a -D2D R1 result. No Excel integration, stable row identifiers, or round-state -adapter exists on the branch. - -## Workbook audit - -An ignored private workbook was inspected read-only and identity-checked against -the explicitly supplied source. Its filename and digest are not tracked. - -- one sheet: `Sheet1` -- non-empty range: `A1:AC16` -- 29 columns and 15 sample rows -- `Uniformity score` duplicates at 1-based positions 17 and 20 -- related/ambiguous `anneal_temp` and `Anneal Temp` at positions 8 and 12 -- blank header at position 25, after `Total combination - addition` -- measurements and derived scores are blank -- formatting extends beyond the non-empty range, so auditors must calculate the - range from cell content rather than styled dimensions alone - -No workbook cells, formulas, formats, or macros were changed. - -## Step 1 remediation boundary - -Step 1 fixes schema/execution ambiguity, parsing, opt-in constraints, design -construction, deterministic LHS, workbook auditing, tests, packaging, and -hygiene. It adds a canonical input-only D2D YAML with empty objective names and -no constraints. - -Step 1 deliberately does not implement UCB, local penalization, qNEHVI round -changes, objective formulas, a thickness transform, a campaign reference point, -real candidates, or an Excel button. The existing qNEHVI runner is made opt-in -and requires an explicit same-space reference point so it cannot silently invent -one. diff --git a/docs/STEP1_HANDOFF.md b/docs/STEP1_HANDOFF.md deleted file mode 100644 index dce77e5..0000000 --- a/docs/STEP1_HANDOFF.md +++ /dev/null @@ -1,273 +0,0 @@ -# Step 1 Handoff - -## 1. Status - -- Historical implementation branch: `feature/d2d-mobo-step1-baseline` -- Base commit: `459afc6d06f66f4d5fb48c3f563e65c61f5df16f` (`origin/main`) -- Reference branch inspected only: `Nicky's-MOBO-Testground` at - `546a5bbdf8fc2f6f9fc6764ada5f5d587e1faa6f` -- Original milestone state: Step 1 was reviewed locally before its checkpoint - commit. This handoff preserves that historical state; publication later uses a - clean squash containing the sanitized Step 1 through Step 2C tree. -- Overall result: **PASS**, with the documented deviations in section 11 - -No production R1/R2 candidates were generated. No UCB implementation, local -penalization, final objective transform, thickness target, or campaign reference -point was introduced. - -## 2. Summary of changes - -- Defined the exact ten-input D2D grid and campaign boundary without inventing - objectives or constraints. -- Replaced fixed-row CSV handling with a validated metadata-style parser that - preserves the first experiment and rejects ambiguous/incomplete model data. -- Corrected config-to-design construction and made grid validation explicit. -- Made constraints opt-in and removed the invalid D2D humidity constraint. -- Reworked R0 LHS generation to be deterministic, exactly sized, grid-valid, - unique after snapping, post-snap constrained, and fail-closed. -- Added a read-only raw-cell workbook schema auditor and a sanitized fixture. -- Pinned a CPU-tested dependency stack and repaired package/CLI documentation. -- Made candidate generation opt-in, require an explicit reference point, and - reject incomplete, duplicate, off-grid, non-finite, or observed recipes. -- Moved default generated output to ignored `local_outputs/`, enabled headless - plot generation, and made explicit missing config paths fail. -- Removed tracked bytecode caches and retired three obsolete experimental test - scripts whose active behavior is covered by the replacement test suite. - -## 3. Files changed - -| File or group | Purpose | -|---|---| -| `.gitignore` | Ignore private inputs, local outputs, caches, environments, coverage, and build artifacts. | -| `README.md` | Correct install, package, CLI, CPU/GPU, metadata-CSV, output, and round-trip guidance. | -| `pyproject.toml`, `setup.py`, `requirements.txt` | Normalize packaging, supported Python range, extras, dependency bounds, and pytest collection. | -| `requirements/constraints.txt`, `requirements/dev.txt` | Pin the tested direct stack and provide a reproducible editable development install. | -| `configs/FA0.9CS0.1PbI3_260407_Config.yaml` | Canonical ten-input D2D grid, empty objectives, and `constraints: []`. | -| `docs/D2D_CAMPAIGN_SPEC.md` | Canonical schema, workbook mapping guardrails, state model, and unresolved decisions. | -| `docs/REPO_AUDIT_D2D.md` | Verified main/reference-branch architecture, defects, risks, and workbook findings. | -| `docs/STEP1_HANDOFF.md` | This implementation and verification record. | -| `src/mobo_kit/design.py` | Strict `InputSpec`, endpoint-aligned grids, and supported config-to-design path. | -| `src/mobo_kit/utils.py` | Raw CSV metadata parser, explicit encodings, typed DataFrames, and strict model-boundary validation. | -| `src/mobo_kit/constraints.py` | Explicit-only constraint parsing and clear configuration/shape errors. | -| `src/mobo_kit/lhs.py` | Deterministic snapped-grid LHS with uniqueness, exact-size, constraint, and hard-correlation guarantees. | -| `src/mobo_kit/workbook_schema.py` | Read-only, content-range workbook audit preserving raw duplicate/blank headers. | -| `src/mobo_kit/main.py`, `src/mobo_kit/cli.py` | Correct design construction, safe defaults, CLI wiring, headless output, and proposal guards. | -| `tests/test_csv_parser.py` | Metadata boundary, first-row, encoding, objective, malformed-data, and return-type tests. | -| `tests/test_design.py`, `tests/test_lhs.py` | Grid/schema validation and deterministic/unique/constrained LHS regressions. | -| `tests/test_d2d_baseline.py` | Exact D2D contract/cardinality, 20-row smoke, constraints, imports, CLI, hygiene, and fail-closed guards. | -| `tests/test_workbook_schema.py` | Sanitized workbook fixture plus optional ignored-local-workbook audit. | -| `tests/test_acquisition.py`, `tests/test_models.py`, `tests/test_plotting.py` | Active-package, CPU-fast replacements for stale tests. | -| tracked `src/__pycache__/*`, `tests/__pycache__/*` | Removed 26 committed bytecode/cache artifacts. | -| `tests/smoke_test.py`, `tests/simple_gp_test.py`, `tests/synthetic_test.py` | Retired obsolete scripts using missing `src.*` APIs and uncontrolled experimental/candidate loops. | - -The private workbook remains only in ignored local storage. Its runtime identity -matches the explicitly supplied source, but neither filename nor digest is -tracked. - -## 4. Defects fixed - -1. `generate_initial_experiments` passed a raw dict to `build_design`. -2. CSV parsing used a fixed row offset and dropped the first experiment. -3. Duplicate headers could be mangled before ambiguity was reported. -4. Malformed metadata, missing/partial objectives, and nonnumeric model rows - could pass too far or be handled inconsistently. -5. Generic config conversion injected an unrelated Clausius-Clapeyron - constraint; the D2D config referenced nonexistent variables. -6. Public data-loading annotations and runtime return types disagreed. -7. LHS retries/subset choice were not fully deterministic and could return - duplicates, constraint violations, an undersized set, or a correlation-limit - violation. -8. LHS diagnostics referenced nonexistent `design.labels`. -9. The runner used an internally inconsistent hard-coded proposal reference - point, hid proposal failure, and did not wire `--num-restarts` through. -10. Proposal output could report success with an incomplete, duplicate, - off-grid, or already observed batch. -11. An explicitly supplied missing config path silently triggered schema - inference in the Python API. -12. Plot generation depended on Tcl/Tk even though the runner only writes files. -13. Default commands could overwrite tracked demonstration results. -14. README/package names, URLs, extras, CLI examples, and dependency policy had - drifted from MOBO-Kit. -15. Tracked caches and stale `src.*` test scripts obscured the actual package - test surface. - -## 5. Tests added - -1. Valid and invalid YAML/config to `Design`, including exact D2D input order, - bounds, steps, per-axis grids, and full product `177,816,994,740`. -2. Robust metadata CSV parsing: optional separator, first data row, encodings, - duplicate headers, missing objectives, malformed metadata, plain CSV, and - empty experimental data. -3. Model-boundary DataFrame behavior for missing, blank, partial, nonnumeric, - and non-finite values. -4. Empty/default, valid explicit, and missing-column constraint behavior. -5. Same-seed determinism, different-seed change, grid membership, bounds, - uniqueness, post-snap constraints, exact size, impossible requests, and hard - correlation limits for LHS. -6. A deterministic unique 20-by-10 D2D R0 smoke generation; the sample count is - still caller-configurable. -7. Sanitized and optional local workbook audits for range, sample count, - duplicate headers, blank header, anneal ambiguity, and file immutability. -8. Direct Torch/GPyTorch/BoTorch/package CPU imports and all three CLI help - surfaces. -9. Production source/notebook personal-path scan. -10. Fail-closed proposal batch validation and explicit missing-config behavior. -11. Active-package acquisition, model, and headless plotting smoke tests. - -## 6. Commands run - -```powershell -git -c http.sslBackend=openssl fetch --all --prune -git switch -c feature/d2d-mobo-step1-baseline main - -# Clean environment installation used the committed constraints. -uv pip install -c requirements/constraints.txt -e ".[dev]" - -.\.venv\Scripts\python.exe -m pytest -q -.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider ` - --basetemp - -.\.venv\Scripts\python.exe -m black --check -git diff --check -git ls-files - -.\.venv\Scripts\mobo-kit.exe --help -.\.venv\Scripts\mobo-kit.exe generate --help -.\.venv\Scripts\mobo-kit.exe run --help -.\.venv\Scripts\mobo-kit.exe run ` - --csv data/processed/configCSV_example.csv ` - --config configs/demo_config.yaml --device cpu --seed 42 --verbose ` - --out - -.\.venv\Scripts\python.exe -c "import torch, gpytorch, botorch, mobo_kit" -.\.venv\Scripts\python.exe -c "from mobo_kit.workbook_schema import audit_campaign_workbook; ..." -``` - -GitHub access, branch tips, and comparison were also verified against the -connected repository before implementation. The reference branch was never -checked out over or merged into the feature branch. - -## 7. Test results - -Final complete test command (after all code changes): - -```text -79 passed, 14 third-party Matplotlib/PyParsing deprecation warnings, -0 skipped, 0 failed -``` - -The literal `pytest -q` command also passes. This Windows sandbox denies pytest's -default cache directory, producing one additional cache warning; the recorded -clean result disables only the cache provider and uses an explicit writable -temporary base. `git diff --check`, `pip check`, CLI help, direct imports, the -safe CPU demo run, workbook hash/mtime checks, and the scoped Black check pass. - -The safe demo run loaded 12 rows with 8 inputs and 3 synthetic/demo objectives, -fit the CPU models, wrote `parity_plots.png`, wrote no `next_batch.csv`, and -exited 0. - -## 8. Tested environment - -- OS: Microsoft Windows NT `10.0.26200` -- Python: `3.12.10` -- torch: `2.8.0+cpu` -- gpytorch: `1.14` -- botorch: `0.15.1` -- linear_operator: `0.6` -- numpy: `2.2.6` -- scipy: `1.16.0` -- pandas: `2.3.1` -- scikit-learn: `1.7.1` -- matplotlib: `3.10.3` -- seaborn: `0.13.2` -- PyYAML: `6.0.2` -- Excel reader: openpyxl `3.1.5` -- Image handling: Pillow `12.3.0` -- pytest: `8.4.1` -- black: `25.1.0` -- CUDA available: `False` - -The committed package range is Python `>=3.11,<3.13`. Python 3.10 was removed -from the advertised baseline because the tested SciPy 1.16 stack requires -Python 3.11 or newer. - -## 9. Workbook audit - -- Input: explicitly supplied ignored private workbook -- Identity: verified locally before and after the audit; not tracked -- Sheet: `Sheet1` -- Used/non-empty range: `A1:AC16` -- Columns: 29 -- Sample rows: 15 -- Duplicate headers: `Uniformity score` at one-based columns 17 (`Q`) and 20 - (`T`) -- Ambiguous headers: `anneal_temp` at column 8 (`H`) and `Anneal Temp` at - column 12 (`L`) -- Other warning: blank header at column 25 (`Y`), immediately after - `Total combination - addition` -- Modification result: SHA-256 and modification time unchanged; no save was - performed - -No duplicate or related workbook headers were merged, renamed, or assigned a -scientific meaning. - -## 10. Unresolved scientific decisions - -1. R0 candidate/control counts and whether controls/replicates train the GP. -2. The three authoritative BO objective columns, formulas, and directions. -3. Uniformity definition and meanings of both `Uniformity score` columns. -4. Thickness target, tolerance, and utility transformation. -5. Optoelectronic score formula and missing/failed/zero semantics. -6. Fixed campaign objective scaling and out-of-range policy. -7. Fixed hypervolume reference point in original and transformed units. -8. Meanings of `anneal_temp` and `Anneal Temp` and which is the optimizer input. -9. Physical/equipment/safety constraints and control recipes. -10. Literature-backed multi-objective UCB definition and beta/noise policy. -11. Local-penalization metric, radius/weight, observed-point treatment, and hard - minimum distance. -12. Missing-data, failed-film, QC, outlier, and replicate aggregation policy. -13. Excel deployment environment, macro policy, Python installation, signing, - and trusted-location requirements. - -## 11. Deviations from specification - -- The pack preferred Python 3.10/3.11. The usable managed local runtime was - Python 3.12.10, so that exact CPU environment was tested. The resulting - package supports 3.11-3.12, but Python 3.11 still needs CI verification. -- Native pip dependency resolution repeatedly consumed CPU without terminating - cleanly in this managed environment. The environment was installed with `uv` - against the committed pip-compatible constraints; `pip check` and a local - no-dependency editable-install dry run pass. -- All changed/new Python files pass Black. A repository-wide Black check still - reports five untouched legacy source modules (`acquisition.py`, `data.py`, - `metrics.py`, `models.py`, and `plotting.py`) as style-only reformat targets. - They were not mass-formatted to avoid an unrelated full-source rewrite. -- The real-workbook integration test ran locally. It remains optional/skipped - for clean clones where the ignored private workbook is unavailable. - -## 12. Risks and limitations - -- The D2D YAML deliberately has no objective names; it is valid for R0 design - generation but cannot authorize a D2D model/acquisition run. -- R0 `generate` emits an input-only CSV. It does not round-trip directly into - the metadata-style `run` parser; the future workbook adapter must implement - that state transition. -- The existing qNEHVI path remains a legacy, explicit opt-in capability. Its - output is guarded for size/grid/uniqueness/observed recipes, but Step 1 does - not add round logic or local penalization. -- Only CPU/Python 3.12 was executed locally. GPU and Python 3.11 need separate - CI coverage before being claimed as tested campaign environments. -- The 14 test warnings are third-party Matplotlib/PyParsing deprecations. - -## 13. Recommended Step 2 - -- Obtain and encode the approved objective-transform contract, fixed scales, - QC policy, process constraints, and fixed reference point. -- Implement and mathematically document the approved R1 multi-objective UCB - acquisition using synthetic tests first. -- Add one reusable, fail-closed local-penalized batch-selection policy for R1 - and R2, including distance diagnostics and exact batch-size guarantees. -- Design the workbook round-trip/state adapter only after the scientific schema - is approved; keep optimization logic in the Python engine. -- Do not generate real R1 candidates until those approvals and a frozen campaign - configuration are present. diff --git a/docs/STEP2A_HANDOFF.md b/docs/STEP2A_HANDOFF.md deleted file mode 100644 index 5608edd..0000000 --- a/docs/STEP2A_HANDOFF.md +++ /dev/null @@ -1,319 +0,0 @@ -# Step 2A Handoff - -## 1. Status - -- Step 1 checkpoint commit: `648efd0c81631726afed91e493b143e66e5f25c1` -- Historical Step 2A branch: `feature/d2d-mobo-step2a-core` -- Original milestone state: the implementation was reviewed before the Step 2A - checkpoint. This handoff preserves that history; publication later uses a - clean squash containing the sanitized Step 1 through Step 2C tree. -- Overall result: **PASS** -- Production candidate generation: **BLOCKED / NOT RUN** -- Production pull request or candidate release at this milestone: **NOT RUN** - -## 2. Summary of implementation - -- Added a versioned raw-output-to-utility framework, including nonlinear target - transforms applied to posterior samples. -- Added integer-index sampling of exact-size, discrete candidate pools without - materializing the 177.8-billion-row D2D Cartesian product. -- Added R1 UCB-HVI scoring and exact five-candidate, positive-HVI-only batch - selection. -- Added one sequential, log-space local-penalized selector shared by UCB-HVI and - qLogNEHVI. It enforces stable ties and hard distances without fallback. -- Added singleton-pool qLogNEHVI with a required configured MC objective and - pending-point reconstruction at every selection step. -- Added numerical/grid/distance diagnostics and four headless plots per method. -- Extended the read-only workbook auditor to preserve the historical Step 1 - profile and recognize the updated v2 profile. -- Added an explicit production approval gate. The supplied provisional config - is tested and rejected. The existing campaign proposal runner is also - blocked before CSV parsing, model fitting, or output creation; even an - otherwise approved config cannot enter that incompatible legacy path. -- Added a deterministic ten-input synthetic CPU example and optional - 10,000-point scoring benchmark. No workbook is read by either example. The - synthetic JSON report now records seeds, pool draws/rejections, method - versions, beta/kappa, MC settings, local penalties, per-selection scores, - UCB utility moments, pending counts, and runtime versions. - -## 3. Mathematical definitions implemented - -### Objective transforms - -All objective outputs are transformed into an all-maximize utility space. - -```text -affine maximize: u(y) = (y - lo) / (hi - lo) -affine minimize: u(y) = (hi - y) / (hi - lo) -Gaussian target: u(y) = exp(-0.5 * ((y - target) / sigma)^2) -negative absolute target: u(y) = -abs(y - target) / scale -``` - -Affine anchors, target, sigma, and scale are explicit and validated. The -nonlinear transforms are applied to every posterior MC sample before computing -utility mean and population standard deviation (`correction=0`). Latent -posterior sampling is the default and observation-noise inclusion is explicit. - -### UCB-HVI - -```text -kappa = sqrt(beta) -UCB_j(x) = mean_U,j(x) + kappa * std_U,j(x) -a_UCB-HVI(x) = HV(P union {UCB(x)}; r) - HV(P; r) -``` - -`P` is the non-dominated observed utility set and `r` is a fixed, caller-supplied -reference in the same utility space. True zero HVI remains zero. Only log-score -stabilization uses epsilon. Negative HVI beyond numerical tolerance raises. -Batch proposal requires enough candidates above a positive-HVI threshold. - -### Local penalization - -```text -d_w(x,z) = sqrt(sum_j w_j * (x_j - z_j)^2) -phi_i(x) = 1 - exp(-0.5 * (d_w(x,x_i) / rho)^2) -log a_pen(x) = log a_base(x) + sum_i log(max(phi_i(x), epsilon)) -``` - -Distances use normalized input space. Hard selected-to-selected and optional -selected-to-observed/pending thresholds are applied before stable `argmax`. -Exact observed/pending duplicates are always ineligible. An impossible exact -batch raises `UndersizedBatchError` and does not relax settings. - -### qLogNEHVI sequential selection - -Each remaining candidate is evaluated with shape `N x 1 x D`. At selection step -`t`, `X_pending` contains pre-existing pending points plus selections `1..t-1`. -The seeded qLogNEHVI acquisition is rebuilt and rescored before the shared local -penalty is applied. `ConfiguredMCMultiOutputObjective` is mandatory, preventing -an accidental identity transform in raw outcome space. - -## 4. Public APIs - -| API | Purpose | Input/output spaces | -|---|---|---| -| `ObjectiveSpec`, `ObjectiveTransform` | Validate and apply a versioned objective contract | raw/model outcome to all-maximize utility | -| `ConfiguredMCMultiOutputObjective` | Use the same transform inside BoTorch MC acquisition | posterior samples to utility samples | -| `sample_discrete_candidate_pool` | Sample exact unique grid tuples with exclusions/constraints | grid indices, physical inputs, normalized inputs | -| `physical_rows_to_grid_indices` | Validate exact grid membership | physical inputs to integer indices | -| `score_ucb_hvi_pool` | MC utility moments and singleton optimistic HVI | normalized pool/raw posterior to utility-space scores | -| `propose_ucb_hvi_batch` | Select an exact positive-HVI batch | candidate pool to selected normalized/physical rows | -| `select_local_penalized_batch` | Shared sequential diversity selector | log acquisition + normalized distances to batch | -| `score_qlognehvi_singletons` | Chunked discrete singleton qLogNEHVI | normalized pool to utility-space log acquisition | -| `propose_qlognehvi_penalized_batch` | Exact sequential qLogNEHVI batch with pending updates | candidate pool to selected normalized/physical rows | -| `summarize_candidate_batch` and plot helpers | Numeric and headless selection diagnostics | normalized/physical inputs to summaries/PNG files | -| `audit_campaign_workbook` | Recognize historical/v2 schemas without saving | workbook cells to structured audit | -| `validate_production_config` | Block unresolved campaign-facing proposals | resolved config to approval receipt/hash | -| `block_legacy_campaign_proposal` | Prevent an approved config from entering the incompatible old proposal runner | resolved config to an explicit Step 2B migration error | - -## 5. Files changed - -| File | Purpose | -|---|---| -| `configs/d2d_step2a_provisional.yaml` | Deliberately unapproved D2D contract skeleton | -| `docs/D2D_STEP2A_COMPUTATIONAL_CORE.md` | Equations, APIs, determinism, safety, limitations | -| `docs/D2D_OBJECTIVE_CONTRACT_PROVISIONAL.md` | Current workbook facts and unresolved objective choices | -| `docs/D2D_MEETING_DECISION_RECORD_TEMPLATE.md` | Step 2B scientific decision record | -| `docs/STEP2A_HANDOFF.md` | This implementation and verification record | -| `src/mobo_kit/objectives.py` | Objective contract and BoTorch MC adapter | -| `src/mobo_kit/candidate_pool.py` | Integer-index discrete-pool sampling | -| `src/mobo_kit/batch_selection.py` | Shared local-penalized selector | -| `src/mobo_kit/ucb_hvi.py` | Posterior utility moments, HVI, R1 proposal | -| `src/mobo_kit/qlognehvi_batch.py` | Singleton qLogNEHVI and R2 proposal | -| `src/mobo_kit/candidate_diagnostics.py` | Numerical diagnostics and headless plots | -| `src/mobo_kit/workbook_schema.py` | Historical/v2 read-only workbook audit | -| `src/mobo_kit/production_gate.py` | Fail-closed production approval validator | -| `src/mobo_kit/main.py` | Enforce the gate and Step 2A proposal block before campaign data is parsed | -| `src/mobo_kit/cli.py` | Expose the Step 2A proposal-disabled status without pre-creating output | -| `examples/d2d_step2a_synthetic.py` | Synthetic-only ten-input 5/3 smoke run | -| `examples/d2d_step2a_benchmark.py` | Optional synthetic 10,000-point benchmark | -| `tests/test_*.py` (Step 2A modules) | Mathematical, safety, reproducibility, integration tests | - -## 6. Tests added - -1. Objective identity/affine/target equations, shapes, dtype/device, validation, - nonlinear sample-before-mean behavior, and BoTorch MC equivalence. -2. Candidate-pool exact size/order, grid membership, normalization, exclusions, - physical constraints, rejection statistics, impossible requests, and proof - that Cartesian allocation helpers are not used. -3. UCB-HVI beta/kappa behavior, exact two-/three-objective HVI, Pareto filtering, - chunk/seed consistency, reference validation, and zero-HVI batch refusal. -4. Local-penalty formula, weights, stable ties, diversity, hard distances, - duplicate exclusion, and explicit undersized-batch failure. -5. qLogNEHVI singleton shape/chunks, configured objective requirement, reference - dimensions, observed/pending exclusion, sequential pending counts, exact - three-candidate batch, and hard-distance failure. -6. Candidate distance/grid/boundary diagnostics and all four headless plots. -7. Historical and v2 sanitized workbook fixtures, aliases, notes, formulas, - input-grid validation, local file integrity, and unapproved objective status. -8. Production-gate missing/false/nested-placeholder cases; fixed-scaling and - integer-count validation; distance-weight consistency; rejection of the - exact supplied provisional config; and pre-I/O campaign-runner blocking. -9. End-to-end CPU synthetic GP fitting, five UCB-HVI candidates, three - qLogNEHVI candidates, deterministic reruns, spacing, grids, and temporary-only - outputs. - -The suite increased from the protected Step 1 baseline of 79 tests to 170 tests. - -## 7. Commands run - -```powershell -# Step 1 reproduction before checkpoint -.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider ` - --basetemp \step2a-pytest-step1-20260724 - -# Final full suite -.\.venv\Scripts\python.exe -m pytest -q -p no:cacheprovider ` - --basetemp \pytest-step2a-full-final - -# Formatting and dependency checks -.\.venv\Scripts\python.exe -m black --check -.\.venv\Scripts\python.exe -m pip check -git diff --check -git diff --no-index --check NUL -git status --short - -# Synthetic verification and optional performance benchmark -.\.venv\Scripts\python.exe examples\d2d_step2a_synthetic.py -.\.venv\Scripts\python.exe examples\d2d_step2a_benchmark.py -``` - -Focused objective, pool, selector, UCB-HVI, qLogNEHVI, diagnostics, workbook, -gate, and synthetic tests were also run during development. - -## 8. Test results - -```text -Step 1 checkpoint reproduction: 79 passed, 14 warnings, 18.19 s -Step 2A final full suite: 170 passed, 20 warnings, 7.64 s -Black 25.1.0 check: PASS -pip check: PASS -git diff --check: PASS -Untracked no-index check: PASS (22 files) -Private/generated artifact check: PASS (ignored and untracked) -``` - -Warnings are from the pinned third-party Matplotlib/PyParsing stack and -BoTorch/NumPy array-compatibility path during synthetic GP fitting. There are no -test failures or project-code warnings. - -## 9. Performance - -- Synthetic smoke elapsed time: 2.72 s for 15 observations, a 128-point R1 - pool, five-candidate UCB-HVI, a 96-point R2 pool, three-candidate qLogNEHVI, - deterministic reruns, diagnostics, and eight plots on CPU. -- Optional 10k-pool benchmark: - - pool sampling: 0.1750 s; - - deterministic three-objective UCB-HVI scoring: 2.2931 s; - - 6,986 positive-HVI points; - - full Cartesian grid not materialized. -- Tested environment: Windows 11, CPython 3.12.10, 24 logical processors, - Torch 2.8.0 CPU (`cuda_available=False`). - -| Dependency | Version | -|---|---:| -| NumPy | 2.2.6 | -| pandas | 2.3.1 | -| SciPy | 1.16.0 | -| scikit-learn | 1.7.1 | -| Matplotlib | 3.10.3 | -| seaborn | 0.13.2 | -| PyYAML | 6.0.2 | -| Torch | 2.8.0 | -| GPyTorch | 1.14 | -| BoTorch | 0.15.1 | -| SHAP | 0.48.0 | -| openpyxl | 3.1.5 | -| pytest | 8.4.1 | -| Black | 25.1.0 | - -- Observed limitation: singleton hypervolume evaluation scales linearly with - pool size but has a nontrivial per-point cost; qLogNEHVI construction and GP - posterior work should continue to use explicit chunks for production-sized - pools. - -## 10. Updated workbook audit - -- Input: explicitly supplied ignored private workbook -- Identity and modification time: verified unchanged locally; not tracked -- Sheet/range: `Sheet1`, `A1:AC18` -- Rows/columns: 15 sample rows (Excel 2–16), 29 columns, row 17 blank -- Header mapping: canonical inputs B:K; explicit - `precur_vol (uL)` to `precur_vol` alias; T uniformity, U optoelectronic, V raw - thickness, W normalized thickness; Y blank -- No duplicate annealing-temperature field, formulas, or populated objective - measurements -- Actual note locations: Q18/S18. P18/R18 are blank, contrary to the pack text. -- Grid warning: the local workbook contains an off-grid input discrepancy - relative to the provisional design contract. No private condition was - snapped, changed, or copied into tracked fixtures. -- Modification result: file hash and modification time were unchanged by the - read-only audit and tests; the workbook remains ignored and untracked. - -## 11. Production gate - -- Required fields include explicit production approval; approved objective and - constraint statuses; campaign/schema/input - versions; approver/time/decision record; three unique source/formula/transform - contracts; fixed scaling and reference point; QC/control/replicate policy; - explicit constraints list; R1/R2 method, batch, sample, pool, and beta values; - local radius/hard distances; seed and provenance recording. -- Null, blank, false approval, nested `PENDING`/`TBD`/`provisional`, dynamic - observed-data scaling, fractional sample/pool counts, inconsistent distance - weights, invalid numeric, unsupported distance, missing objective, and - missing policy cases are rejected. -- `allow_hard_distance_relaxation` must be false and R2 sequential pending must - be true. -- The exact `configs/d2d_step2a_provisional.yaml` template is tested and rejected - with more than ten independent unresolved reasons. -- `run_mobo_experiment(..., propose_candidates=True)` evaluates the gate before - CSV parsing or output creation. A fully resolved test config passes the gate - but is then deliberately rejected because the legacy runner does not use the - Step 2A transforms, discrete pool, or shared local selector. -- Provisional config status: `approved_for_production: false`. - -## 12. Deviations - -1. The instruction pack says the workbook notes are at P18/R18. Direct workbook - inspection and artifact-tool rendering show Q18/S18. The contract fixture - follows P18/R18; a separate sanitized anomaly fixture and the local audit - report Q18/S18 without modifying the source file. -2. The pack expects all 15 real workbook recipes to be on-grid. The supplied - workbook contains a local off-grid discrepancy. The sanitized v2 fixture - proves valid-grid behavior; the local audit fails closed without exposing, - snapping, or modifying the private condition. -3. Step 2A was originally reviewed as a feature diff before checkpointing. The - current public-review tree includes its sanitized implementation in a clean - Step 1 through Step 2C squash. - -## 13. Remaining decisions after the meeting - -- Final model/utility mapping: `T,U,W`, `T,U,V`, or another approved mapping. -- Exact T and U ownership, equations, weights, fixed normalization anchors, - clipping, and missing/failure semantics. -- Final raw thickness versus W model source, 650-nm confirmation, transform, - sigma/scale, symmetry, and output ownership. -- Fixed utility scales and fixed utility-space reference point. -- R0 row roles, controls, replicates, GP inclusion, QC, failure, outlier, and - missing-objective policies. -- Complete physical/equipment constraints or explicitly approved `constraints: []`. -- Resolution of the local off-grid discrepancy versus the approved input - contract. -- R1 beta/posterior samples/pool/seed and R2 MC samples/pool/seed. -- Local-penalty radius, within-batch distance, observed/pending distance, and - required review plots. -- Workbook/score ownership, campaign state, backup, and audit workflow. - -## 14. Recommended Step 2B - -- Encode the approved objective mapping and exact versioned formulas. -- Freeze fixed scales and the transformed-utility reference point. -- Encode controls, replicates, QC rules, and every process constraint. -- Resolve the off-grid workbook row or approve a revised input contract. -- Run a read-only dry-run audit on completed R0 objective data. -- Review the resulting model diagnostics and synthetic-equivalent proposal - audit without writing to the workbook. -- Implement the reviewed Step 2B campaign adapter; do not re-enable the legacy - raw-objective proposal path. -- Only after the resolved config passes the production gate should a real R1 - proposal be authorized. diff --git a/docs/STEP2B_DEBUG_HANDOFF.md b/docs/STEP2B_DEBUG_HANDOFF.md deleted file mode 100644 index 0fc0b91..0000000 --- a/docs/STEP2B_DEBUG_HANDOFF.md +++ /dev/null @@ -1,272 +0,0 @@ -# Step 2B Debug Handoff - -## 1. Git status - -- Step 1 checkpoint: `648efd0c81631726afed91e493b143e66e5f25c1` -- Step 2A checkpoint: `749c9de` (`Implement D2D MOBO Step 2A computational core`) -- Historical Step 2B branch: `feature/d2d-mobo-step2b-debug` -- Original milestone state: implemented and reviewed locally before the Step 2B - checkpoint; publication later uses a clean, sanitized Step 1 through Step 2C - squash. -- Experimental or production pull request at this milestone: not run - -Step 2A was reproduced before checkpointing: 170 tests passed, its scoped Black -check passed, `pip check` passed, and `git diff --check` passed. - -## 2. Overall result - -- Result: **PASS for read-only algorithm debugging** -- Debug R1 proposal: generated -- Experimental approval: **false** -- Production approval: **false** -- Source workbook write-back: not implemented and not run - -The generated conditions are not a lab worklist. Every candidate artifact is -watermarked `DEBUG ONLY - NOT APPROVED FOR EXPERIMENT`. - -## 3. Source workbook - -- Input: explicitly supplied ignored private workbook -- Identity and modification time: verified unchanged locally; not tracked -- Sheet/range: `Sheet1`, `A1:AI20` -- Workbook profile: `d2d_summary_v3_scores` -- Sample rows: 15 unique numeric Sample IDs at Excel rows 2-16 -- Note rows excluded: 17-20 -- Objective columns: Z, AA, AB -- Workbook modified: no - -The previous Step 2A workbook was preserved under an ignored private name. All -`local_inputs/` and `local_outputs/` files remain ignored and untracked. - -## 4. Resolved objective contract - -Ordered GP outputs and acquisition utilities: - -1. Z `Uniformity score` -2. AA `Optoelectronic score` -3. AB `Thickness score` - -All three use identity transforms and are maximized. No clipping or observed-data -min/max normalization is applied. GP outcome standardization remains internal to -the existing model implementation. Input normalization uses fixed configured -bounds. - -- Fixed utility-space reference: `[-0.01, -10.0, -0.01]` -- All 15 observations strictly dominate the reference component-wise. -- Ignored: AC `Stability score?` and all summary fields AD:AI. - -## 5. Score validation - -- Required final scores: 15/15 complete and finite for Z, AA, and AB. -- Authoritative Uniformity scores: 15/15 within the required `[0,1]` range; - out-of-range values now fail validation and model ingestion. -- Uniformity support `L*N*O`: 4/15 match at workbook precision; 11/15 structured - warnings. Z remains unchanged and authoritative. -- Optoelectronic support `log10(P*Q)`: 15/15 pass against R and AA. -- Thickness support - `exp(-((mean(valid T1:T4)-650.0)/250.0)^2)`: 15/15 pass against Y and AB. -- Thickness uses the unrounded valid T1:T4 mean, excludes `T anom`, normalizes - blank/whitespace/NBSP cells to missing, and has no `0.5` exponent factor. -- Missing/non-finite score errors: 0. - -The known uniformity discrepancy is recorded in the config, score table, run -manifest, and debug status. It remains a production blocker. - -## 6. Control and grid handling - -- Control identity: supplied only by the ignored private configuration -- Primary-model inclusion: yes; all 15 R0 observations train the debug GP. -- Measurement provenance assumption: outcomes were measured in the current - campaign; only the recipe was literature-derived. -- Observed-only exception: supplied only by the ignored private configuration. -- On-grid observed conditions: 14 -- Off-grid observed conditions: 1 -- Control value changed/snapped: no -- Search grid changed: no -- New candidates all finite, bounded, unique, and exactly on-grid: yes - -Strict Step 2A grid conversion still rejects the control. The adapter partitions -the row before grid-index exclusion, but includes all normalized observations in -GP fitting and distance diagnostics. - -## 7. GP and R1 debug run - -- Training rows: 15 -- Model: one CPU `SingleTaskGP` per direct score with internal `Standardize(m=1)` -- Observed Pareto count: 6 -- Current fixed-reference hypervolume: `1.709278134536184` -- Candidate pool: 10,000 accepted from 10,000 draws; no duplicate, avoid, or - constraint rejections in the sampled pool -- Beta/kappa: `4.0 / 2.0` -- Posterior samples: 256 -- Local radius: 0.25 -- Hard within-batch minimum: 0.15 -- Selected unique conditions: exactly 5, each with positive UCB-HVI -- Observed selected-batch minimum normalized distance: `0.8493280824045194` -- Minimum selected-to-observed normalized distance: `0.887172581912801` -- Complete full-study runtime: `73.89 s` on CPU - -Training-posterior diagnostics are near-interpolating (`R^2 > 0.99998` for each -objective). These are not cross-validation scores and must not be read as evidence -of out-of-sample accuracy with only 15 observations. - -Ignored debug bundle: - -```text -local_outputs/d2d_step2b_debug/baseline_seed73/ -``` - -Artifact provenance hashes were recorded in the ignored local audit bundle and -are intentionally not reproduced in tracked documentation. - -The tracked handoff deliberately does not reproduce private candidate recipes. - -## 8. Replicate worklist - -- Unique R1 conditions: 5 -- Replicates per condition: 3 -- Physical execution rows: 15 -- Candidate IDs: `R1-C01` through `R1-C05` -- Replicate numbers: 1, 2, 3 -- Inputs are identical within every replicate group. -- Measurement/final-score fields remain blank. -- Replicate aggregation records condition mean, sample standard deviation, count, - standard error, completeness, and source execution IDs. -- One-row standard deviation/SEM remain missing rather than zero; input mismatch - inside a group fails. - -The synthetic-only R2 boundary aggregates five triplicate R1 groups, combines -them with 15 R0 condition observations (20 total), and returns exactly three -on-grid qLogNEHVI conditions in tests. Its returned metadata is explicitly -synthetic/test-only and not approved for experiment. No real R2 batch was -generated. - -## 9. Sensitivity and control ablation - -All 13 requested baseline/one-factor/control runs completed without relaxing a -rule, but the debug batch is not robust enough for experimental approval. Pool -randomness and Monte Carlo randomness are independently seeded; the two pool-seed -comparisons below keep the MC seed fixed at 73. - -| Comparison | Exact overlap with baseline | -|---|---:| -| beta 1.0 | 5/5 | -| beta 9.0 | 2/5 | -| pool 5,000 | 1/5 | -| pool 20,000 | 4/5 | -| pool seed 137 | 0/5 | -| pool seed 911 | 0/5 | -| radius 0.15 | 5/5 | -| radius 0.35 | 3/5 | -| hard batch distance 0.10 | 5/5 | -| hard batch distance 0.20 | 5/5 | -| posterior samples 128 | 5/5 | -| control excluded | 0/5 | - -Control exclusion changed all five selections. Its mean absolute posterior change -on the baseline candidate locations was `0.3748446` across the three raw score -dimensions. The observed Pareto set changed from Samples -`[1, 4, 6, 9, 10, 15]` to `[4, 6, 9, 10, 15]`; the removed control was Pareto -nondominated in the primary fit. Alternative random candidate-pool seeds also -changed all selections. The computational pipeline is working, but the current -15-point campaign fit and finite random pool produce a configuration-sensitive -proposal. - -The summary has 13 run-level rows. `sensitivity_candidates_long.csv` records all -65 selected rows with exact inputs, prediction means/stds, raw/log/final -acquisition values, full pairwise-distance rows, nearest-observed distances, -boundaries, settings, and runtimes. `control_ablation.csv` records five aligned -baseline-candidate prediction comparisons and Pareto diagnostics. All are ignored, -watermarked debug artifacts. - -## 10. Notebook update - -- Added `notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb` from - the supplied download. -- Inserted `2.5 Calculate and Validate D2D Scores` between sections 2 and 3. -- Uses package score helpers and explicit Z/AA/AB name selection. -- Includes the exact D2D no-half-factor thickness equation. -- Replaced personal paths with repository-relative/configurable paths. -- Replaced unavailable legacy acquisition/model APIs in the active path. -- Retired the undefined legacy global-distance sandbox cells and contradictory - PCE/stability/log-nEHVI/reference-point guidance. -- Candidate generation is opt-in and routes through the guarded adapter. -- Notebook diagnostics and the guarded candidate bundle use separate output - directories, so diagnostic plots cannot make the adapter destination nonempty. -- All notebook execution counts and outputs are cleared. -- Programmatic notebook smoke tests pass. - -## 11. Tests and quality checks - -```text -Step 2A pre-checkpoint: 170 passed, 20 dependency warnings -Step 2B focused suite: 94 passed, 68 dependency warnings -Final complete suite: 251 passed, 74 dependency warnings -Black direct API check: PASS (14 changed/new Python files) -Python compilation: PASS -pip check: PASS -git diff --check: PASS -untracked whitespace check: PASS (13 files) -debug script --help: PASS -private/generated status: ignored and untracked -``` - -Warnings are from the pinned third-party Matplotlib/PyParsing stack and the -BoTorch/NumPy array-compatibility path. No project warning or test failure was -reported. - -## 12. Files changed - -| File | Purpose | -|---|---| -| `README.md` | Document the guarded Step 2B command and debug boundary. | -| `configs/d2d_step2b_debug.yaml` | Freeze the debug-only v3, objective, control, grid, replicate, and acquisition contract. | -| `src/mobo_kit/workbook_schema.py` | Recognize and audit exact v3 structure plus explicitly configured observed-only exceptions. | -| `src/mobo_kit/candidate_diagnostics.py` | Support visible, metadata-backed debug watermarks on every generated candidate plot. | -| `src/mobo_kit/d2d_scores.py` | Compute and structurally validate D2D support scores without overwriting supplied objectives. | -| `src/mobo_kit/d2d_campaign.py` | Validate config, read workbook rows, prepare control-aware training data, and handle replicates. | -| `src/mobo_kit/d2d_step2b_debug.py` | Run the read-only R1 debug proposal, diagnostics, sensitivity study, and artifact bundle. | -| `src/mobo_kit/d2d_r2_test.py` | Expose only a synthetic-test future R2 qLogNEHVI boundary. | -| `examples/d2d_step2b_debug.py` | Provide the local one-command debug entry point. | -| `notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb` | Add the supplied research notebook with the resolved Section 2.5 and guarded APIs. | -| `tests/test_workbook_schema.py` | Add sanitized v3/profile/immutability/error tests and update the optional local audit. | -| `tests/test_candidate_diagnostics.py` | Verify headless plot generation and embedded debug watermark metadata. | -| `tests/test_d2d_scores.py` | Test equations, missing semantics, tolerances, and severity policy. | -| `tests/test_d2d_campaign.py` | Test the pinned config/grid, objective order, control partition, row metadata, strict triplicates, and 20-condition aggregation. | -| `tests/test_d2d_step2b_debug.py` | Test fail-before-output, safe output containment, independent seeds, deterministic batch, detailed artifacts, watermarks, and workbook integrity. | -| `tests/test_d2d_r2_test.py` | Test synthetic triplicate aggregation, pool-coordinate consistency, test-only status, and exact three-condition qLogNEHVI. | -| `tests/test_d2d_notebook.py` | Test executable thickness helpers, section order, resolved narrative, active APIs, portability, and cleared output. | -| `docs/STEP2B_DEBUG_HANDOFF.md` | This handoff and production-blocker record. | - -## 13. Deviations and local issues - -1. The pack allowed the reference point to be chosen; its resolved - `[-0.01, -10.0, -0.01]` value was used exactly. -2. The workbook contains formulas only in R2:R3; R4:R16 are cached/static numeric - checks. Validation accepts either formulas with numeric cached values or static - numeric checks. -3. The supplied notebook required more than a single inserted cell because its - active path referenced unavailable APIs, undefined variables, and personal - paths. The research notes were retained where safe, but the legacy sandbox was - visibly retired. -4. Black's normal Windows worker CLI left non-responsive worker processes after - reporting completion. The final formatting/check used Black's direct serial - API, passed, and the task-owned stale workers were terminated. - -## 14. Remaining blockers before experimental R1 - -- Correct or explicitly approve the 11 inconsistent uniformity scores. -- Confirm the configured control's measurement provenance through the private - campaign record. -- Review the strong control-ablation and random-pool-seed sensitivity. -- Choose and freeze a reviewed pool construction/seed and acquisition configuration. -- Review the five-condition proposal with experimental and computational owners. -- Approve a production provenance/config record; both approval flags remain false. -- Define the final workbook write-back/interface and signing/trust policy. - -## 15. Recommended next step - -Hold a bounded computational/experimental review of control provenance and the -seed/pool sensitivity. After resolving the uniformity values, freeze the reviewed -configuration and rerun one production-gated dry run for sign-off. Do not fabricate -from the current debug bundle. diff --git a/docs/STEP2C_ROBUSTNESS_HANDOFF.md b/docs/STEP2C_ROBUSTNESS_HANDOFF.md deleted file mode 100644 index 0fb13fd..0000000 --- a/docs/STEP2C_ROBUSTNESS_HANDOFF.md +++ /dev/null @@ -1,298 +0,0 @@ -# Step 2C Robustness Handoff - -## 1. Baseline and review status - -- Step 1 checkpoint: `648efd0c81631726afed91e493b143e66e5f25c1` -- Step 2A checkpoint: `749c9de` (`Implement D2D MOBO Step 2A computational core`) -- Step 2B checkpoint: `1c6a83a9dfd7e6ed5e69ce66765b8dc0fd8a86af` -- Step 2C implementation and read-only robustness evidence: complete for review - -## 2. Overall result - -- Implementation and read-only robustness study: **PASS** -- Full stable consensus criteria: **FAIL** -- Experimental approval: **false** -- Production approval: **false** -- Real R2 proposal: not run - -Step 2C completed the declared full study and produced a validated robust-region -bundle. It did not release an R1 consensus batch. This is the correct fail-closed -outcome: search convergence was strong, but only four regions met the required -study-family coverage and the GP validation remains poor. - -## 3. Source workbook - -- Input class: Git-ignored private campaign workbook -- Content identity and unchanged-file proof: verified before, during, after, and - at independent validation; exact identifiers are intentionally omitted -- Adapter/schema contract: verified locally; private workbook metadata is not - tracked -- Workbook modified: no -- Known Uniformity mismatch: retained as warning-only input for debugging and as - an experimental-approval blocker -- Control observation: retained in the primary model under the reviewed - off-grid policy; its exact override remains in the ignored private config - -No code path saves or writes back to this workbook. - -## 4. Objective and baseline contract - -The direct supplied scores in Z/AA/AB remain ordered as Uniformity, -Optoelectronic, and Thickness. All three use identity transforms and are -maximized. The fixed utility reference is `[-0.01, -10.0, -0.01]`. - -Step 2B was checkpointed before Step 2C. Step 2C intentionally replaces the -finite random-pool/MC-moment debug search with analytic identity moments, exact -nested Sobol prefixes, and deterministic grid refinement. The control inclusion, -score-source, input-grid, reference-point, and approval boundaries did not -change. - -## 5. Analytic identity moments - -- Primary API: `posterior_identity_moments` -- Full diagnostic comparison: 2,048 MC samples, seed 73, 2,048 candidates -- Maximum absolute mean difference: `0.00006279358632210741` -- Maximum absolute standard-deviation difference: `0.00036946014787431203` -- Exact selected overlap: 5/5 -- Regional matches within 0.15: 5/5 -- Analytic/MC debug gate: pass -- Observed runtime ratio, MC/analytic: `1.3323` -- Production Step 2C moment method: deterministic `analytic_identity` - -The per-objective comparison tolerance is -`max(0.02, 0.02 * max(observed_range, 1.0))`. Monte Carlo remains diagnostic; -it does not drive the full search. - -## 6. Nested Sobol pools - -Primary accepted prefixes were exact and nested: - -| Seed | Accepted size | Draws | Duplicate/avoid/constraint rejects | Prefix SHA-256 | -|---:|---:|---:|---:|---| -| 73 | 16,384 | 16,384 | 0/0/0 | `76B069D765949EDF045E8ACDBA0866EB27125FF84CB2AC8A3F874E38B9CBC3B2` | -| 73 | 32,768 | 32,768 | 0/0/0 | `896A8483D182BEC14584149F657E7C9EADE07C3207A4CAB28F877EB7E3CE6833` | -| 73 | 65,536 | 65,536 | 0/0/0 | `84ABB5185246D20336CB90BD1E32836E8C0EDCF73AA5FAF288353D97770B9EB7` | -| 73 | 131,072 | 131,072 | 0/0/0 | `848B418477DEF6DF9FF04287F4FA902AF8FE15490AFD6AC2801069799FA33DBC` | -| 137 | 131,072 | 131,072 | 0/0/0 | `4AEE3ECE6A3F181B4D0A1E7A1A4F6F6E4613C4C2B52E258DCC5820CB901665D9` | -| 911 | 131,072 | 131,072 | 0/0/0 | `F8CC95A722C309FB1B1BB21C62B1F8DC9AFBBD60E8227FB283E15C0B52BE9BD2` | - -The full Cartesian grid was never materialized. Observed on-grid recipes were -excluded by exact grid index; the off-grid control remained a continuous GP and -distance reference only. - -## 7. Local refinement and convergence - -- Full settings: 64 anchors per selection step, at most 10 sweeps, - `1e-10` improvement tolerance, all allowed coordinate values -- Mean nested-batch acquisition gain: `1.73963296072043` -- Gain range across the four prefixes: `1.6522526527608` to - `1.86429022721892` -- Baseline anchor summaries: 358 -- Distinct baseline converged optima: 21 -- Accepted baseline coordinate moves: 2,845 -- Study candidate rows: 75 -- Invalid grid, bounds, hard-distance, or duplicate rows: 0 - -All adjacent nested-prefix comparisons from 16,384 onward matched 5/5 within -0.15 with mean matched distance 0.0. Both secondary scramble seeds reproduced -the same five refined optima exactly; their acquisition regrets were numerical -roundoff (`-6.1e-14` and `2.4e-13`). Refinement is deterministic in regression -tests and never accepts a score decrease. - -## 8. Model validation - -Exact leave-one-out results for all 15 observations: - -| Variant | Objective | MAE | RMSE | R2 | 68% coverage | 95% coverage | Mean NLPD | Max abs. standardized residual | -|---|---|---:|---:|---:|---:|---:|---:|---:| -| default | Uniformity | 0.2062 | 0.2472 | -0.9102 | 0.400 | 0.533 | 4.096 | 9.420 | -| default | Optoelectronic | 0.3853 | 0.5266 | -0.2473 | 0.467 | 0.667 | 2.821 | 6.571 | -| default | Thickness | 0.3559 | 0.4299 | -0.4578 | 0.467 | 0.600 | 1.354 | 3.935 | -| conservative | Uniformity | 0.2045 | 0.2478 | -0.9188 | 0.467 | 0.667 | 2.384 | 7.851 | -| conservative | Optoelectronic | 0.3988 | 0.5414 | -0.3185 | 0.467 | 0.667 | 3.088 | 6.384 | -| conservative | Thickness | 0.3604 | 0.4328 | -0.4779 | 0.467 | 0.600 | 1.316 | 3.804 | - -Every one of the 96 full/fold objective fits had at least one normalized ARD -lengthscale at or above the declared flatness threshold of 10.0. One fit also -had a lengthscale at or below 0.05. Flat flags were most frequent for `time_2` -(90/96), `precur_conc` (89/96), `anneal_time` (87/96), `anti_vol` (86/96), and -`anti_time` (83/96). - -All 96 fits also reached their configured likelihood-noise lower bound: all 48 -default fits were at `0.001`, and all 48 conservative fits were at `0.01`. This -is a major calibration diagnostic, not evidence that either noise policy is -validated. - -The 576 recorded optimizer warnings are the same NumPy `copy=` deprecation -warning, not fit failures. During the full influence study, the console also -showed one GPyTorch numerical warning that clamped a tiny negative posterior -variance to `1e-10`. That console-observed warning was not suppressed, but it is -not present in the aggregate warning CSV. - -For both model variants, all five selected Optoelectronic posterior means were -above the observed maximum. No selected posterior mean violated the declared -Uniformity or Thickness bounds. These diagnostics reinforce that the models are -not ready to justify fabrication. - -## 9. Observation influence - -All 15 exact omission runs completed on the common accepted pool. The five most -influential observations were Samples 1, 9, 4, 6, and 10. - -Sample 1 was rank 1/15 at the 100th percentile. Omitting it produced 0/5 exact -or 0.15-regional matches, mean matched batch distance `2.1692`, prediction-mean -change `0.6183`, normalized acquisition-rank change `0.3345`, and mean absolute -hyperparameter log-ratio `2.8595`. Its omission removed Sample 1 from the -observed Pareto set (Pareto Jaccard `0.8333`). The control remains included in -the primary model; this result is a sensitivity warning, not an automatic -exclusion decision. - -## 10. Bounded-utility and qLogNEHVI compatibility - -The declared clip-UCB and unbounded-UCB policies had 0/5 exact and 0/5 regional -correspondence. The clip policy affected 77,286 of 131,072 pool rows (58.965%) -and all five selected rows; the largest selected-coordinate clip was `0.04911`. -The bounded and unbounded acquisition sums were `7.6932` and `13.2048`, -respectively. These policy-specific HVI sums should not be interpreted as a -shared-scale regret. - -Training targets were not mutated. A real synthetic BoTorch qLogNEHVI test now -uses the same bounded posterior-sample objective with a fixed reference and -seed. No real R2 qLogNEHVI proposal was generated. - -## 11. Local-penalty study - -| Variant | Comparator | Exact/regional overlap | Minimum batch distance | Acquisition sacrifice | Interpretation | -|---|---|---:|---:|---:|---| -| no soft, no hard spacing | self | 5/5 | 0.2828 | 0 | Base optima already separated | -| no soft, hard 0.15 | no soft, no hard | 5/5 | 0.2828 | 0 | Hard spacing inactive | -| radius 0.15 | no soft, hard 0.15 | 4/5 | 0.5657 | `2.21e-7` | Material diversity change | -| radius 0.25 | no soft, hard 0.15 | 3/5 | 0.7141 | 0.00378 | Material diversity change | -| radius 0.35 | no soft, hard 0.15 | 3/5 | 1.0000 | 0.11789 | Material diversity change | - -The primary radius 0.25 is active in the converged full search even though its -mean penalty factor remains close to one (`0.9865`). The classification uses -regional and pairwise-distance changes, not score attenuation alone. Hard -spacing did not relax. - -## 12. Beta, model, and boundary robustness - -Beta 1 and beta 9 each had 0/5 correspondence with beta 4 after refinement. The -conservative model retained 4/5 exact clustered regions, whereas unbounded UCB -retained 0/5. - -The baseline batch remains strongly boundary-seeking: - -- `speed_2`: 5/5 at the upper bound; -- `precur_conc` and `precur_vol`: 5/5 at the upper bound; -- `anneal_temp`, `anti_vol`, and `anti_time`: 5/5 at the lower bound; -- `time_2` and `anneal_time`: 2/5 at each lower and upper bound. - -Lower and upper rates and enrichment are reported separately. The combination -of beta sensitivity, bound-policy sensitivity, flat ARD directions, and boundary -seeking remains a fabrication blocker. - -## 13. Robust regions and shortlist - -- Clustering: deterministic complete-link agglomerative clustering -- Primary normalized distance threshold: 0.15 -- Sensitivity thresholds: 0.10 and 0.20 -- Robust regions: 18 -- Debug shortlist: 12 medoids -- Regions covering one, two, and four non-baseline families: 9, 5, and 4 -- Regions meeting the required at-least-three-family criterion: 4 - -The tracked handoff contains no private recipe rows. Those remain only in the -ignored local artifacts. - -## 14. Conditional consensus result - -- `r1_consensus_debug_batch.csv`: not created -- Full-mode eligibility: pass -- Largest-two nested regional match gate: pass, 5/5 -- Largest-two mean-distance gate: pass, 0.0 -- Five family-qualified regions: fail, only four available -- Exact five consensus candidates: fail, only four eligible medoids - -All candidate-row gates are consequently false because no exact five-row set -exists; this does not mean the global debug/approval flags were weakened. Both -approval flags remain false throughout. The explicit reason artifact is -`r1_no_stable_batch_reason.json`. - -## 15. Local and public artifacts - -Full campaign evidence remains below the ignored `local_outputs/` boundary and -must not be committed or shared. Private runs label their complete archive -`*_PRIVATE_EVIDENCE_DO_NOT_SHARE.zip`. The sibling `.zip` is now a separate, -strictly allowlisted public summary containing aggregate tables and sanitized -metadata only. Input-specific artifact hashes are intentionally not tracked. - -## 16. Verification - -- Sanitized synthetic end-to-end suite: 2 passed, including the real workbook - adapter, strict GP/search orchestration, resolved-config provenance, - rollback-safe publication, and validation of the aggregate-only public ZIP -- Ignored private Step 2B/2C configuration compatibility smoke: pass -- Historical pre-sanitization private fast/full runs: pass with 46 validated - artifacts; these private bundles were not published -- Historical full-mode runtime: `1508.32 s` on CPU -- Complete pytest: 427 passed, 2 expected private-input skips, 270 dependency - warnings (`48.67 s` with single-threaded numerical libraries) -- Black: pass on all 67 changed/new Python files -- Python compilation: pass -- `pip check`: pass (`No broken requirements found`) -- `git diff --check`: pass -- Private/generated outputs: confirmed ignored by `.gitignore` - -Largest full-mode phase runtimes were observation influence `707.34 s`, -secondary/model/bound/beta studies `413.08 s`, primary moments and scores -`285.17 s`, and nested refinement `64.39 s`. - -## 17. Files changed - -| Area | Files/purpose | -|---|---| -| Config/docs | `configs/d2d_step2c_debug.yaml`, this handoff, the Step 2C method document, and README command/safety guidance | -| Entry points | Private fast/full runner and separate sanitized synthetic-CI example | -| Search | Analytic UCB-HVI moments, nested Sobol prefixes, regional batch comparison, and deterministic discrete refinement | -| Models | Strict GP variants, exact LOOCV, hyperparameter/flatness diagnostics, and all-observation influence | -| Policies | Bounded UCB, bounded qLogNEHVI objective compatibility, beta and five-variant penalty studies | -| Stabilization | Complete-link robust regions, diverse shortlist, exact consensus gates, and lower/upper boundary diagnostics | -| Safety | Strict artifact schemas/provenance, CSV evidence-chain validation, watermark and format checks, exact consensus/no-stable linkage, aggregate-only public ZIP validation, and rollback-safe public/private publication | -| Tests | Unit, regression, artifact-negative, real qLogNEHVI, pending-row, normalization, plotting, and sanitized end-to-end coverage | - -## 18. Deviations and limitations - -1. A separate sanitized synthetic runner was added because campaign fast mode is - correctly pinned to the private workbook and therefore cannot be portable CI. -2. The qLogNEHVI work is compatibility testing only; Step 2C deliberately did - not generate a real R2 proposal. -3. Region-threshold sensitivity exports counts at 0.10 and 0.20; full membership - rows are exported for the declared primary 0.15 threshold. -4. One console-observed posterior-variance clamp warning occurred in the full - study and is retained as a model-stability limitation; it was not captured in - the aggregate warning CSV. - -## 19. Remaining blockers before experimental R1 - -- Correct or explicitly approve the 11 inconsistent supplied Uniformity scores. -- Confirm Sample 1 outcome provenance with the experimental team. -- Resolve poor LOOCV calibration/accuracy, all 96 likelihood-noise fits reaching - their configured floor, and the pervasive flat ARD directions. -- Review the posterior-variance numerical warning. -- Choose and justify the bounded-versus-unbounded utility policy. -- Review the strong beta, control, and boundary sensitivity. -- Obtain at least five regions satisfying the declared family-coverage gate, or - explicitly redesign that gate through a new reviewed specification. -- Freeze a production configuration and approval record. -- Define workbook write-back/interface and signing policy only after candidate - sign-off. - -## 20. Recommended next step - -Do not fabricate from either Step 2B or Step 2C artifacts. First correct/approve -the Uniformity scores and confirm the control provenance. Then perform a focused -model review—kernel/priors, noise treatment, objective policy, and whether more -R0 information is required—before rerunning this same fail-closed full study. -Proceed to Excel button/write-back integration only after a five-row consensus -set and experimental sign-off both exist. diff --git a/examples/d2d_step2a_benchmark.py b/examples/d2d_step2a_benchmark.py deleted file mode 100644 index 15631b0..0000000 --- a/examples/d2d_step2a_benchmark.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Optional TEST_ONLY 10,000-point CPU scoring benchmark for Step 2A.""" - -from __future__ import annotations - -import json -from pathlib import Path -import sys -from time import perf_counter - -import numpy as np - - -REPOSITORY_ROOT = Path(__file__).resolve().parents[1] -for source in (REPOSITORY_ROOT / "src", Path(__file__).resolve().parent): - if str(source) not in sys.path: - sys.path.insert(0, str(source)) - -from d2d_step2a_synthetic import build_test_only_d2d_design # noqa: E402 -from mobo_kit.candidate_pool import sample_discrete_candidate_pool # noqa: E402 -from mobo_kit.ucb_hvi import score_ucb_hvi_from_moments # noqa: E402 - - -def main() -> int: - design = build_test_only_d2d_design() - pool_started = perf_counter() - pool = sample_discrete_candidate_pool(design, 10_000, seed=99173) - pool_seconds = perf_counter() - pool_started - - X = pool.X_norm - utility_mean = np.column_stack( - [ - 0.15 + 0.65 * X[:, 0], - 0.10 + 0.70 * X[:, 4], - np.exp(-0.5 * ((X[:, 7] - 0.55) / 0.22) ** 2), - ] - ) - utility_std = 0.02 + 0.04 * np.column_stack([X[:, 1], 1.0 - X[:, 5], X[:, 9]]) - observed = np.array( - [ - [0.35, 0.75, 0.60], - [0.55, 0.55, 0.80], - [0.75, 0.35, 0.65], - ] - ) - score_started = perf_counter() - result = score_ucb_hvi_from_moments( - utility_mean, - utility_std, - observed, - np.array([-0.05, -0.05, -0.05]), - beta=1.0, - chunk_size=512, - objective_contract_version="TEST_ONLY-benchmark-utilities-v1", - ) - score_seconds = perf_counter() - score_started - report = { - "status": "TEST_ONLY_BENCHMARK", - "production_candidate_generation": "NOT_RUN", - "pool_size": pool.size, - "pool_sampling_seconds": round(pool_seconds, 4), - "ucb_hvi_scoring_seconds": round(score_seconds, 4), - "positive_hvi_count": int(np.count_nonzero(result.base_score > 0)), - "full_cartesian_grid_materialized": False, - } - print(json.dumps(report, indent=2)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/d2d_step2a_synthetic.py b/examples/d2d_step2a_synthetic.py deleted file mode 100644 index e17fca0..0000000 --- a/examples/d2d_step2a_synthetic.py +++ /dev/null @@ -1,464 +0,0 @@ -"""CPU-only TEST_ONLY exercise of the Step 2A computational core. - -This script never reads a campaign workbook and never authorizes or emits real -D2D R1/R2 recipes. All observations, objective settings, reference values, and -acquisition parameters below are synthetic fixtures for software verification. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from importlib.metadata import version as package_version -import json -import math -from pathlib import Path -import platform -import sys -from time import perf_counter -from typing import Any, Mapping - -import numpy as np -import torch -from botorch.fit import fit_gpytorch_mll -from botorch.models import SingleTaskGP -from botorch.models.model_list_gp_regression import ModelListGP -from botorch.models.transforms.outcome import Standardize -from gpytorch.mlls import ExactMarginalLogLikelihood - - -REPOSITORY_ROOT = Path(__file__).resolve().parents[1] -SOURCE_ROOT = REPOSITORY_ROOT / "src" -if str(SOURCE_ROOT) not in sys.path: - sys.path.insert(0, str(SOURCE_ROOT)) - -from mobo_kit.batch_selection import LocalPenalizationConfig # noqa: E402 -from mobo_kit.candidate_diagnostics import ( # noqa: E402 - plot_candidate_pca, - plot_distance_heatmap, - plot_parallel_coordinates, - plot_selection_scores, - summarize_candidate_batch, -) -from mobo_kit.candidate_pool import ( # noqa: E402 - CandidatePool, - sample_discrete_candidate_pool, -) -from mobo_kit.design import InputSpec, build_design # noqa: E402 -from mobo_kit.objectives import ( # noqa: E402 - ConfiguredMCMultiOutputObjective, - ObjectiveSpec, - ObjectiveTransform, -) -from mobo_kit.qlognehvi_batch import ( # noqa: E402 - propose_qlognehvi_penalized_batch, -) -from mobo_kit.ucb_hvi import propose_ucb_hvi_batch # noqa: E402 - - -TEST_ONLY_REFERENCE_POINT_UTILITY = np.array([-0.05, -0.05, -0.05]) -TEST_ONLY_SEED = 20260724 - - -@dataclass(frozen=True) -class SyntheticRunSummary: - elapsed_seconds: float - observed_count: int - ucb_pool_size: int - qlognehvi_pool_size: int - ucb_selected_pool_indices: np.ndarray - qlognehvi_selected_pool_indices: np.ndarray - ucb_minimum_distance: float - qlognehvi_minimum_distance: float - ucb_metadata: dict[str, Any] - qlognehvi_metadata: dict[str, Any] - plot_paths: tuple[Path, ...] - - -def _json_safe(value: Any) -> Any: - """Convert nested NumPy/PyTorch metadata to strict JSON-compatible values.""" - if isinstance(value, Mapping): - return {str(key): _json_safe(item) for key, item in value.items()} - if isinstance(value, (list, tuple)): - return [_json_safe(item) for item in value] - if isinstance(value, np.ndarray): - return _json_safe(value.tolist()) - if isinstance(value, np.generic): - return _json_safe(value.item()) - if isinstance(value, torch.Tensor): - return _json_safe(value.detach().cpu().tolist()) - if isinstance(value, Path): - return str(value) - if isinstance(value, float) and not math.isfinite(value): - return None - return value - - -def _selection_table(selection: Any) -> list[dict[str, Any]]: - """Return all score, penalty, distance, and order fields for a batch.""" - return [ - { - "order": step.order, - "pool_index": step.pool_index, - "base_score": step.base_score, - "base_log_score": step.base_log_score, - "penalty_factor": step.penalty_factor, - "log_penalty": step.log_penalty, - "penalized_log_score": step.penalized_log_score, - "nearest_selected_distance_before": (step.nearest_selected_distance_before), - "nearest_observed_distance": step.nearest_observed_distance, - } - for step in selection.steps - ] - - -def build_test_only_d2d_design(): - """Build the exact ten-dimensional D2D grid without campaign semantics.""" - return build_design( - [ - InputSpec("speed_1", 1000, 6000, 500, unit="rpm"), - InputSpec("time_1", 5, 50, 5, unit="s"), - InputSpec("speed_2", 0, 5000, 500, unit="rpm"), - InputSpec("time_2", 10, 60, 5, unit="s"), - InputSpec("precur_conc", 1.0, 2.0, 0.05, unit="M"), - InputSpec("precur_vol", 40, 200, 10, unit="uL"), - InputSpec("anneal_temp", 100, 185, 5, unit="C"), - InputSpec("anneal_time", 10, 60, 5, unit="min"), - InputSpec("anti_vol", 100, 200, 5, unit="uL"), - InputSpec("anti_time", 9, 25, 2, unit="s"), - ] - ) - - -def _synthetic_raw_outcomes(X_norm: np.ndarray) -> np.ndarray: - """Create two bounded maximize outcomes and one raw target outcome.""" - first = 0.15 + 0.35 * X_norm[:, 0] + 0.25 * X_norm[:, 4] + 0.15 * X_norm[:, 7] - second = ( - 0.10 + 0.30 * X_norm[:, 1] + 0.20 * (1.0 - X_norm[:, 2]) + 0.25 * X_norm[:, 8] - ) - raw_target = 430.0 + 420.0 * (0.55 * X_norm[:, 3] + 0.45 * X_norm[:, 6]) - return np.column_stack([first, second, raw_target]) - - -def _fit_synthetic_model(train_X: torch.Tensor, train_Y: torch.Tensor) -> ModelListGP: - models = [] - for objective_index in range(train_Y.shape[1]): - output = train_Y[:, objective_index : objective_index + 1] - output_variance = output.var(correction=0).clamp_min(1e-8) - # Standardize(m=1) scales this to a stable TEST_ONLY variance of 1e-4. - known_noise = torch.full_like(output, float(output_variance * 1e-4)) - model = SingleTaskGP( - train_X, - output, - train_Yvar=known_noise, - outcome_transform=Standardize(m=1), - ) - mll = ExactMarginalLogLikelihood(model.likelihood, model) - fit_gpytorch_mll( - mll, - optimizer_kwargs={"options": {"maxiter": 25, "ftol": 1e-7}}, - ) - model.eval() - models.append(model) - return ModelListGP(*models) - - -def _objective_contract() -> ObjectiveTransform: - return ObjectiveTransform( - [ - ObjectiveSpec("synthetic_maximize_1", "maximize", "identity"), - ObjectiveSpec("synthetic_maximize_2", "maximize", "identity"), - ObjectiveSpec( - "synthetic_target", - "target", - "gaussian_target", - target=650.0, - sigma=120.0, - ), - ], - version="TEST_ONLY-synthetic-objectives-v1", - ) - - -def _plots_for_method( - method: str, - output_dir: Path, - design, - observed_norm: np.ndarray, - pool: CandidatePool, - selection, -) -> tuple[Path, ...]: - steps = selection.steps - return ( - plot_candidate_pca( - observed_norm, - selection.X_norm, - output_dir / f"{method}_pca.png", - pool_norm=pool.X_norm, - seed=TEST_ONLY_SEED, - ), - plot_parallel_coordinates( - selection.X_norm, - design.names, - output_dir / f"{method}_parallel_coordinates.png", - ), - plot_distance_heatmap( - selection.X_norm, output_dir / f"{method}_distance_heatmap.png" - ), - plot_selection_scores( - [step.order for step in steps], - [step.base_log_score for step in steps], - [step.penalized_log_score for step in steps], - output_dir / f"{method}_selection_scores.png", - ), - ) - - -def run_synthetic_step2a( - output_dir: str | Path, - *, - ucb_pool_size: int = 128, - qlognehvi_pool_size: int = 96, - posterior_samples: int = 32, - qlognehvi_samples: int = 16, -) -> SyntheticRunSummary: - """Fit synthetic GPs and deterministically propose TEST_ONLY 5/3 batches.""" - started = perf_counter() - output = Path(output_dir) - output.mkdir(parents=True, exist_ok=True) - torch.manual_seed(TEST_ONLY_SEED) - np.random.seed(TEST_ONLY_SEED) - - design = build_test_only_d2d_design() - observed_pool = sample_discrete_candidate_pool(design, 15, seed=TEST_ONLY_SEED) - train_X = torch.as_tensor(observed_pool.X_norm, dtype=torch.double) - train_Y = torch.as_tensor( - _synthetic_raw_outcomes(observed_pool.X_norm), dtype=torch.double - ) - model = _fit_synthetic_model(train_X, train_Y) - transform = _objective_contract() - mc_objective = ConfiguredMCMultiOutputObjective(transform) - - ucb_pool = sample_discrete_candidate_pool( - design, - ucb_pool_size, - seed=TEST_ONLY_SEED + 1, - observed_phys=observed_pool.X_phys, - ) - local_config = LocalPenalizationConfig( - radius=0.65, - min_batch_distance=0.20, - min_observed_distance=0.08, - ) - ucb_kwargs = dict( - q=5, - beta=1.0, - local_penalization_config=local_config, - observed_pending_norm=observed_pool.X_norm, - positive_score_tolerance=1e-12, - mc_samples=posterior_samples, - seed=TEST_ONLY_SEED + 2, - posterior_chunk_size=32, - hvi_chunk_size=64, - ) - ucb = propose_ucb_hvi_batch( - ucb_pool, - model, - train_Y, - transform, - TEST_ONLY_REFERENCE_POINT_UTILITY, - **ucb_kwargs, - ) - ucb_repeat = propose_ucb_hvi_batch( - ucb_pool, - model, - train_Y, - transform, - TEST_ONLY_REFERENCE_POINT_UTILITY, - **ucb_kwargs, - ) - if not np.array_equal( - ucb.selection.selected_pool_indices, - ucb_repeat.selection.selected_pool_indices, - ): - raise RuntimeError("TEST_ONLY UCB-HVI rerun was not deterministic.") - - qlog_pool = sample_discrete_candidate_pool( - design, - qlognehvi_pool_size, - seed=TEST_ONLY_SEED + 3, - observed_phys=observed_pool.X_phys, - pending_phys=ucb.selection.X_phys, - ) - ucb_pending = torch.as_tensor(ucb.selection.X_norm, dtype=torch.double) - qlog_kwargs = dict( - q=3, - local_penalization_config=local_config, - X_pending_norm=ucb_pending, - mc_samples=qlognehvi_samples, - seed=TEST_ONLY_SEED + 4, - chunk_size=32, - ) - qlog = propose_qlognehvi_penalized_batch( - qlog_pool, - model, - train_X, - mc_objective, - TEST_ONLY_REFERENCE_POINT_UTILITY, - **qlog_kwargs, - ) - qlog_repeat = propose_qlognehvi_penalized_batch( - qlog_pool, - model, - train_X, - mc_objective, - TEST_ONLY_REFERENCE_POINT_UTILITY, - **qlog_kwargs, - ) - if not np.array_equal( - qlog.selection.selected_pool_indices, - qlog_repeat.selection.selected_pool_indices, - ): - raise RuntimeError("TEST_ONLY qLogNEHVI rerun was not deterministic.") - - ucb_diagnostics = summarize_candidate_batch( - ucb.selection.X_norm, - observed_pending_norm=observed_pool.X_norm, - X_phys=ucb.selection.X_phys, - design=design, - metadata=ucb.metadata, - ) - qlog_diagnostics = summarize_candidate_batch( - qlog.selection.X_norm, - observed_pending_norm=np.vstack([observed_pool.X_norm, ucb.selection.X_norm]), - X_phys=qlog.selection.X_phys, - design=design, - metadata=qlog.metadata, - ) - if not np.all(ucb_diagnostics.grid_valid_rows) or not np.all( - qlog_diagnostics.grid_valid_rows - ): - raise RuntimeError("Synthetic proposal contained an off-grid row.") - if ucb_diagnostics.duplicate_row_pairs or qlog_diagnostics.duplicate_row_pairs: - raise RuntimeError("Synthetic proposal contained a duplicate row.") - - plot_paths = _plots_for_method( - "ucb_hvi", - output, - design, - observed_pool.X_norm, - ucb_pool, - ucb.selection, - ) + _plots_for_method( - "qlognehvi", - output, - design, - observed_pool.X_norm, - qlog_pool, - qlog.selection, - ) - elapsed = perf_counter() - started - summary = SyntheticRunSummary( - elapsed_seconds=elapsed, - observed_count=observed_pool.size, - ucb_pool_size=ucb_pool.size, - qlognehvi_pool_size=qlog_pool.size, - ucb_selected_pool_indices=ucb.selection.selected_pool_indices, - qlognehvi_selected_pool_indices=qlog.selection.selected_pool_indices, - ucb_minimum_distance=float(ucb_diagnostics.minimum_within_batch_distance), - qlognehvi_minimum_distance=float( - qlog_diagnostics.minimum_within_batch_distance - ), - ucb_metadata=dict(ucb_diagnostics.metadata), - qlognehvi_metadata=dict(qlog_diagnostics.metadata), - plot_paths=plot_paths, - ) - ucb_selected = ucb.selection.selected_pool_indices - ucb_utility_diagnostics = [ - { - "pool_index": int(pool_index), - "utility_mean": ucb.scoring.utility_mean[pool_index], - "utility_std": ucb.scoring.utility_std[pool_index], - "utility_ucb": ucb.scoring.utility_ucb[pool_index], - "raw_hvi": ucb.scoring.base_score[pool_index], - } - for pool_index in ucb_selected - ] - report = { - "status": "TEST_ONLY_SYNTHETIC_PASS", - "production_candidate_generation": "NOT_RUN", - "elapsed_seconds": round(elapsed, 3), - "observed_count": summary.observed_count, - "seeds": { - "global": TEST_ONLY_SEED, - "observed_pool": TEST_ONLY_SEED, - "ucb_pool": TEST_ONLY_SEED + 1, - "ucb_posterior": TEST_ONLY_SEED + 2, - "qlognehvi_pool": TEST_ONLY_SEED + 3, - "qlognehvi_mc": TEST_ONLY_SEED + 4, - }, - "ucb_hvi": { - "pool_size": summary.ucb_pool_size, - "batch_size": len(summary.ucb_selected_pool_indices), - "selected_pool_indices": summary.ucb_selected_pool_indices.tolist(), - "minimum_normalized_distance": summary.ucb_minimum_distance, - "metadata": summary.ucb_metadata, - "selection_steps": _selection_table(ucb.selection), - "selected_utility_diagnostics": ucb_utility_diagnostics, - "nearest_observed_pending_distance": ( - ucb_diagnostics.nearest_observed_pending_distance - ), - "boundary_flags": ucb_diagnostics.boundary_flags, - "grid_valid_rows": ucb_diagnostics.grid_valid_rows, - }, - "qlognehvi": { - "pool_size": summary.qlognehvi_pool_size, - "batch_size": len(summary.qlognehvi_selected_pool_indices), - "selected_pool_indices": summary.qlognehvi_selected_pool_indices.tolist(), - "minimum_normalized_distance": summary.qlognehvi_minimum_distance, - "metadata": summary.qlognehvi_metadata, - "selection_steps": _selection_table(qlog.selection), - "pending_counts_by_selection_step": [ - item.pending_count for item in qlog.score_history - ], - "nearest_observed_pending_distance": ( - qlog_diagnostics.nearest_observed_pending_distance - ), - "boundary_flags": qlog_diagnostics.boundary_flags, - "grid_valid_rows": qlog_diagnostics.grid_valid_rows, - }, - "objective_contract_version": transform.version, - "reference_point": { - "space": "TEST_ONLY transformed utility", - "value": TEST_ONLY_REFERENCE_POINT_UTILITY.tolist(), - }, - "runtime": { - "platform": platform.platform(), - "python": platform.python_version(), - "numpy": package_version("numpy"), - "torch": package_version("torch"), - "botorch": package_version("botorch"), - "gpytorch": package_version("gpytorch"), - "device": "cpu", - }, - "plots": [path.name for path in plot_paths], - } - (output / "synthetic_summary.json").write_text( - json.dumps(_json_safe(report), indent=2, allow_nan=False), encoding="utf-8" - ) - return summary - - -def main() -> int: - output = REPOSITORY_ROOT / "local_outputs" / "step2a_synthetic" - summary = run_synthetic_step2a(output) - print( - "TEST_ONLY synthetic Step 2A PASS: " - f"R1={len(summary.ucb_selected_pool_indices)}, " - f"R2={len(summary.qlognehvi_selected_pool_indices)}, " - f"elapsed={summary.elapsed_seconds:.2f}s, output={output}" - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/d2d_step2b_debug.py b/examples/d2d_step2b_debug.py deleted file mode 100644 index 3340fd0..0000000 --- a/examples/d2d_step2b_debug.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Run the read-only D2D Step 2B algorithm-debug adapter from the repo root.""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -from mobo_kit.d2d_step2b_debug import run_d2d_step2b_debug - - -def main() -> int: - repository_root = Path(__file__).resolve().parents[1] - parser = argparse.ArgumentParser( - description=( - "Generate a watermarked D2D R1 debug bundle. This never approves or " - "writes an experimental worklist to Excel." - ) - ) - parser.add_argument( - "--workbook", - type=Path, - required=True, - help="Explicit path to the ignored private campaign workbook.", - ) - parser.add_argument( - "--config", - type=Path, - required=True, - help="Explicit path to the matching ignored private debug configuration.", - ) - parser.add_argument( - "--output", - type=Path, - default=( - repository_root / "local_outputs" / "d2d_step2b_debug" / "baseline_seed73" - ), - ) - parser.add_argument("--overwrite", action="store_true") - parser.add_argument( - "--skip-sensitivity", - action="store_true", - help="Run only the configured baseline (useful for a quick local smoke test).", - ) - args = parser.parse_args() - result = run_d2d_step2b_debug( - args.workbook, - args.config, - args.output, - overwrite=args.overwrite, - run_sensitivity=not args.skip_sensitivity, - ) - print("DEBUG ONLY - NOT APPROVED FOR EXPERIMENT") - print(f"Output directory: {result.output_dir}") - print(f"Unique R1 conditions: {len(result.candidates_unique)}") - print(f"Replicate execution rows: {len(result.replicate_worklist)}") - print(f"Sensitivity rows: {len(result.sensitivity_summary)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/d2d_step2c_robustness.py b/examples/d2d_step2c_robustness.py deleted file mode 100644 index 086710e..0000000 --- a/examples/d2d_step2c_robustness.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Run the read-only D2D Step 2C robustness study from the repository root.""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -from mobo_kit.d2d_step2c_robustness import run_d2d_step2c_robustness - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description=( - "Generate a watermarked, read-only D2D R1 robustness audit. " - "This command never writes the workbook or approves fabrication." - ) - ) - parser.add_argument( - "--workbook", - type=Path, - required=True, - help="Path to the Git-ignored campaign workbook.", - ) - parser.add_argument( - "--config", - type=Path, - required=True, - help="Path to the matching Git-ignored private Step 2C configuration.", - ) - parser.add_argument( - "--output", - type=Path, - default=None, - help=( - "Run directory below local_outputs/d2d_step2c_robustness. " - "Defaults to _seed73." - ), - ) - parser.add_argument( - "--mode", - choices=("fast", "full"), - default="full", - help="Use fast for a smoke audit or full for the declared Step 2C study.", - ) - parser.add_argument("--overwrite", action="store_true") - parser.add_argument( - "--no-portable-zip", - action="store_true", - help="Skip the ignored portable ZIP while retaining the validated directory.", - ) - return parser - - -def main() -> int: - repository_root = Path(__file__).resolve().parents[1] - args = build_parser().parse_args() - output = args.output or ( - repository_root - / "local_outputs" - / "d2d_step2c_robustness" - / f"{args.mode}_seed73" - ) - result = run_d2d_step2c_robustness( - args.workbook, - args.config, - output, - mode=args.mode, - overwrite=args.overwrite, - create_portable_zip=not args.no_portable_zip, - ) - print("DEBUG ONLY - NOT APPROVED FOR EXPERIMENT") - print(f"Mode: {result.mode}") - print(f"Output directory: {result.output_dir}") - print(f"Robust regions: {len(result.robust_regions)}") - print(f"Debug shortlist rows: {len(result.shortlist)}") - print(f"Consensus criteria passed: {result.consensus.passed}") - print( - "Sample 1 influence rank: " - f"{result.run_manifest['sample_1_influence_rank']} " - f"({result.run_manifest['sample_1_influence_percentile']:.1f} percentile)" - ) - print(f"Validated artifacts: {len(result.artifact_hashes)}") - print("Workbook writeback performed: false") - print("Real R2 proposal generated: false") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/examples/d2d_step2c_synthetic_ci.py b/examples/d2d_step2c_synthetic_ci.py deleted file mode 100644 index dbd10f2..0000000 --- a/examples/d2d_step2c_synthetic_ci.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Run the sanitized synthetic Step 2C fast end-to-end fixture.""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -from mobo_kit.d2d_step2c_synthetic import run_synthetic_step2c_fast_ci - - -def build_parser(repository_root: Path) -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description=( - "Run Step 2C fast orchestration on generated sanitized data. " - "This is CI/debug coverage only and cannot approve an experiment." - ) - ) - parser.add_argument( - "--config", - type=Path, - default=repository_root / "configs" / "d2d_step2c_debug.yaml", - ) - parser.add_argument( - "--output", - type=Path, - default=( - repository_root / "local_outputs" / "d2d_step2c_robustness" / "synthetic_ci" - ), - ) - parser.add_argument("--overwrite", action="store_true") - parser.add_argument("--no-portable-zip", action="store_true") - return parser - - -def main() -> int: - repository_root = Path(__file__).resolve().parents[1] - args = build_parser(repository_root).parse_args() - result = run_synthetic_step2c_fast_ci( - args.config, - args.output, - overwrite=args.overwrite, - create_portable_zip=not args.no_portable_zip, - ) - print("DEBUG ONLY - NOT APPROVED FOR EXPERIMENT") - print("Input data: sanitized synthetic CI fixture") - print(f"Output directory: {result.output_dir}") - print(f"Validated artifacts: {len(result.artifact_hashes)}") - print(f"Consensus criteria passed: {result.consensus.passed}") - print("Private campaign workbook read: false") - print("Experimental approval enabled: false") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb b/notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb deleted file mode 100644 index 5ec30f9..0000000 --- a/notebooks/D2D_MOBO_TEST Global Distance Candidate generation.ipynb +++ /dev/null @@ -1,1013 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "84796167", - "metadata": {}, - "source": [ - "# D2D MOBO Debug Notebook\n", - "\n", - "This notebook is a research/debug interface for the package implementation. The\n", - "authoritative Step 2B run is `mobo_kit.d2d_step2b_debug.run_d2d_step2b_debug`,\n", - "which reads the completed workbook without modifying it and watermarks every\n", - "candidate artifact as **DEBUG ONLY - NOT APPROVED FOR EXPERIMENT**.\n", - "\n", - "## Outline\n", - "- 0. Setup and portable paths\n", - "- 1. Read the completed R0 workbook\n", - "- 2. Normalize inputs\n", - "- 2.5. Calculate and validate D2D scores\n", - "- 3. Fit GP models\n", - "- 4-5. Optional model diagnostics\n", - "- 6. Run the guarded Step 2B debug adapter\n", - "- 7-9. Review the adapter's watermarked outputs" - ] - }, - { - "cell_type": "markdown", - "id": "c22b92d9", - "metadata": {}, - "source": [ - "## 0. Setup & Imports\n" - ] - }, - { - "cell_type": "markdown", - "id": "24c9601d", - "metadata": {}, - "source": [ - "**Imports** Bring in libraries for data handling, modeling, and plotting. \n", - "If CUDA isn't available, the CPU path still works for a workshop-scale demo.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "69eaaa20", - "metadata": {}, - "outputs": [], - "source": [ - "# --- Imports & setup ---\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "# Core\n", - "import os, sys\n", - "from pathlib import Path\n", - "import numpy as np\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "\n", - "parent_dir = os.path.dirname(os.getcwd())\n", - "if parent_dir not in sys.path:\n", - " sys.path.insert(0, parent_dir)\n", - "\n", - "# Torch / BoTorch / GPyTorch\n", - "import torch\n", - "from mobo_kit.utils import csv_to_config, split_XY, get_objective_names, np_to_torch\n", - "from mobo_kit.design import build_design_from_config\n", - "\n", - "from mobo_kit.lhs import lhs_dataframe, lhs_dataframe_optimized\n", - "from mobo_kit.constraints import constraints_from_config\n", - "\n", - "from mobo_kit.plotting import plot_distribution, plot_correlation_heatmap, plot_PCA\n", - "\n", - "from mobo_kit.data import x_normalizer_np\n", - "\n", - "from mobo_kit.models import fit_gp_models\n", - "from mobo_kit.constraints import check_clausius_clapeyron_np\n", - "from mobo_kit.lhs import lhs_dataframe\n", - "#from mobo_kit.plotting import plot_pareto, plot_hypervolume_trajectory\n", - "\n", - "# if torch.cuda.is_available():\n", - "# device = torch.device(\"cuda\")\n", - "# elif torch.backends.mps.is_available():\n", - "# device = torch.device(\"mps\")\n", - "# else:\n", - "device = torch.device(\"cpu\")\n", - "\n", - "print(f\"Using device: {device}\")\n" - ] - }, - { - "cell_type": "markdown", - "id": "18716c1e", - "metadata": {}, - "source": [ - "## Portable workbook, configuration, and debug-output paths" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "83c88e1e", - "metadata": {}, - "outputs": [], - "source": [ - "# Supply private inputs explicitly through environment variables; no private path is tracked.\n", - "import os\n", - "\n", - "cwd = Path.cwd().resolve()\n", - "repo_root = cwd.parent if cwd.name == \"notebooks\" else cwd\n", - "private_workbook = os.environ.get(\"MOBO_KIT_D2D_PRIVATE_WORKBOOK\")\n", - "private_config = os.environ.get(\"MOBO_KIT_D2D_PRIVATE_CONFIG\")\n", - "if not private_workbook or not private_config:\n", - " raise RuntimeError(\n", - " \"Set MOBO_KIT_D2D_PRIVATE_WORKBOOK and MOBO_KIT_D2D_PRIVATE_CONFIG \"\n", - " \"to explicit ignored local files before running campaign cells.\"\n", - " )\n", - "workbook_path = Path(private_workbook).expanduser().resolve()\n", - "config_path = Path(private_config).expanduser().resolve()\n", - "save_path = repo_root / \"local_outputs\" / \"d2d_step2b_debug\" / \"notebook_seed73\"\n", - "diagnostics_path = repo_root / \"local_outputs\" / \"d2d_step2b_debug\" / \"notebook_diagnostics_seed73\"\n", - "diagnostics_path.mkdir(parents=True, exist_ok=True)\n", - "\n", - "print(\"Workbook:\", workbook_path)\n", - "print(\"Config:\", config_path)\n", - "print(\"Notebook diagnostics:\", diagnostics_path)\n", - "print(\"Debug bundle (created only when enabled):\", save_path)" - ] - }, - { - "cell_type": "markdown", - "id": "155a087c", - "metadata": {}, - "source": [ - "## 1. Read the completed R0 workbook" - ] - }, - { - "cell_type": "markdown", - "id": "8f41089d", - "metadata": {}, - "source": [ - "The Step 2B reader selects the 15 numeric `Sample number` rows from the\n", - "35-column v3 workbook. Inputs are mapped from B:K, the final objectives are\n", - "selected by their exact Z/AA/AB headers, and Stability plus AD:AI are excluded.\n", - "The source workbook is opened read-only and is never saved." - ] - }, - { - "cell_type": "markdown", - "id": "459b2ca0", - "metadata": {}, - "source": [ - "### Completed R0 data\n", - "\n", - "Control identities and any observed-only off-grid exceptions are supplied only\n", - "by the ignored private configuration. They are retained for GP training and\n", - "distance diagnostics, while every new candidate remains on the configured grid." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9525ed10", - "metadata": {}, - "outputs": [], - "source": [ - "from mobo_kit.d2d_campaign import (\n", - " D2D_INPUT_COLUMNS,\n", - " D2D_OBJECTIVE_COLUMNS,\n", - " load_d2d_debug_config,\n", - " load_d2d_workbook_frame,\n", - " prepare_d2d_training_data,\n", - ")\n", - "\n", - "resolved_config = load_d2d_debug_config(config_path)\n", - "design = resolved_config.design\n", - "data_rows, workbook_audit = load_d2d_workbook_frame(\n", - " workbook_path,\n", - " expected_profile=resolved_config.workbook_profile,\n", - " expected_sample_ids=resolved_config.expected_sample_ids,\n", - " allowed_input_exceptions=resolved_config.off_grid_exceptions,\n", - ")\n", - "training_data = prepare_d2d_training_data(data_rows, resolved_config)\n", - "\n", - "X_df = pd.DataFrame(training_data.X_phys_all, columns=D2D_INPUT_COLUMNS)\n", - "Y_df = pd.DataFrame(training_data.Y_objectives, columns=D2D_OBJECTIVE_COLUMNS)\n", - "obj_names = list(D2D_OBJECTIVE_COLUMNS)\n", - "\n", - "print(\"Workbook profile:\", workbook_audit.profile)\n", - "print(\"Rows:\", len(data_rows), \"Inputs:\", X_df.shape, \"Objectives:\", Y_df.shape)\n", - "display(X_df.head())\n", - "display(Y_df.head())" - ] - }, - { - "cell_type": "markdown", - "id": "e719d6af", - "metadata": {}, - "source": [ - "### Optional R0 LHS reference (not used by Step 2B)\n", - "\n", - "The completed workbook already contains R0 observations, so the guarded debug\n", - "path does not regenerate LHS points. Set the flag below only for a separate,\n", - "input-only design exercise." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c94998c8", - "metadata": {}, - "outputs": [], - "source": [ - "run_optional_lhs = False\n", - "row_constraints = []\n", - "max_abs_corr = 0.32\n", - "lhs_batch_size = 14\n", - "\n", - "if run_optional_lhs:\n", - " lhs_df = lhs_dataframe_optimized(\n", - " design,\n", - " n=lhs_batch_size,\n", - " seed=42,\n", - " row_constraints=row_constraints,\n", - " max_abs_corr=max_abs_corr,\n", - " verbose=True,\n", - " )\n", - " display(lhs_df)\n", - "else:\n", - " lhs_df = pd.DataFrame(columns=design.names)\n", - " print(\"Skipping LHS because completed R0 data are available.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb722d98", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "\n", - "# 1. Pull the pre-calculated choices from the Design object\n", - "# 'var_array' was created by build_design using the 'make_linspace' function\n", - "summary_data = []\n", - "for i, name in enumerate(design.names):\n", - " # .size gives the number of valid grid points for this parameter\n", - " n_choices = design.var_array[i].size \n", - " summary_data.append({\n", - " \"Parameter\": name,\n", - " \"Choices\": n_choices,\n", - " \"Min\": design.lowers[i],\n", - " \"Max\": design.uppers[i],\n", - " \"Step\": design.steps[i]\n", - " })\n", - "\n", - "# 2. Display the table\n", - "space_summary_df = pd.DataFrame(summary_data)\n", - "display(space_summary_df)\n", - "\n", - "# 3. Calculate Total Conditions (Product of all choices)\n", - "total_conditions = np.prod(space_summary_df[\"Choices\"].values)\n", - "print(f\"\\nTOTAL UNIQUE CONDITIONS IN SEARCH SPACE: {total_conditions:,.0f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f62c8fdd", - "metadata": {}, - "outputs": [], - "source": [ - "if run_optional_lhs:\n", - " round_worklist_path = diagnostics_path / \"Round_0_Input_Only_Reference.csv\"\n", - " round_worklist_path.parent.mkdir(parents=True, exist_ok=True)\n", - " input_reference = lhs_df.copy()\n", - " for objective in D2D_OBJECTIVE_COLUMNS:\n", - " input_reference[objective] = \"\"\n", - " input_reference.to_csv(round_worklist_path, index=False)\n", - " print(\"Input-only reference saved to:\", round_worklist_path)" - ] - }, - { - "cell_type": "markdown", - "id": "e15c0ed2", - "metadata": {}, - "source": [ - "### Visualize data distribution\n" - ] - }, - { - "cell_type": "markdown", - "id": "eb7ce27a", - "metadata": {}, - "source": [ - "#### Standardize inputs to visualize correlation (NE addition 260407)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8ffd61aa", - "metadata": {}, - "outputs": [], - "source": [ - "if run_optional_lhs:\n", - " scaler = StandardScaler()\n", - " lhs_standardized = scaler.fit_transform(lhs_df)\n", - " lhs_df_std = pd.DataFrame(lhs_standardized, columns=lhs_df.columns)\n", - "else:\n", - " lhs_df_std = pd.DataFrame(columns=design.names)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fd4e947c", - "metadata": {}, - "outputs": [], - "source": [ - "if run_optional_lhs:\n", - " _ = plot_distribution(lhs_df, title=\"LHS Feature Distributions\", save=diagnostics_path / \"lhs_dist.png\")\n", - " _ = plot_correlation_heatmap(lhs_df_std, title=\"LHS Pearson Correlation\", save=diagnostics_path / \"lhs_corr.png\")\n", - " _ = plot_PCA(lhs_df_std, title=\"LHS PCA (2D)\", save=diagnostics_path / \"lhs_pca.png\")" - ] - }, - { - "cell_type": "markdown", - "id": "ddd5fa6f", - "metadata": {}, - "source": [ - "## 2. Normalize Data and Create Tensors/Arrays\n" - ] - }, - { - "cell_type": "markdown", - "id": "3cf47096", - "metadata": {}, - "source": [ - "**Inputs.** Scale to [0, 1] per dimension. \n", - "\n", - "This helps GP hyperparameters learn sensibly. There are utility functions to normalize and standardize the outputs, but this is currently handled internally in `fit_gp_models`.\n", - "\n", - "**Tensors.** Some Botorch functions use tensor objects rather than arrays and vice versa. It's good to have both!\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bf09e4d7", - "metadata": {}, - "outputs": [], - "source": [ - "# Inputs -> fixed [0, 1] bounds from the configured design.\n", - "X_np = X_df.to_numpy(dtype=float)\n", - "X_norm = x_normalizer_np(X_np, design)\n", - "X_t, _ = np_to_torch(X_norm, device=device, return_device=True)\n", - "print(\"Normalized input shape:\", X_t.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "d2d-score-contract", - "metadata": {}, - "source": [ - "## 2.5 Calculate and Validate D2D Scores\n", - "\n", - "The final BO objectives are Excel Z `Uniformity score`, AA\n", - "`Optoelectronic score`, and AB `Thickness score`. All three are maximized and\n", - "used directly; they are not clipped to `[0, 1]`. Experimental analysis owns the\n", - "final scores, while the support equations below are validation only. The known\n", - "uniformity inconsistency is a debug warning, and Stability is ignored.\n", - "\n", - "- Uniformity support: `Coverage * (1 - Uniformity) * Phase purity`\n", - "- Optoelectronic support: `log10(P * Q)`\n", - "- Thickness support: `exp(-((mean(valid T1:T4) - 650.0) / 250.0) ** 2)`\n", - "\n", - "The thickness equation has **no factor 0.5**, uses the unrounded valid T1:T4\n", - "mean, and excludes `T anom`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2d-score-imports", - "metadata": {}, - "outputs": [], - "source": [ - "from mobo_kit.d2d_scores import (\n", - " compute_thickness_average,\n", - " compute_thickness_score,\n", - " validate_supplied_d2d_scores,\n", - ")\n", - "\n", - "def normalize_missing_thickness_cell(value):\n", - " # Convert blank/whitespace/NBSP thickness cells to NaN.\n", - " if value is None:\n", - " return np.nan\n", - " if isinstance(value, str) and not value.replace(\"\\u00a0\", \" \").strip():\n", - " return np.nan\n", - " return value\n", - "\n", - "thickness_columns = [\"T1\", \"T2\", \"T3\", \"T4\"]\n", - "normalized_thickness = data_rows[thickness_columns].map(\n", - " normalize_missing_thickness_cell\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2d-thickness-score", - "metadata": {}, - "outputs": [], - "source": [ - "# Package helper implements exp(-((mean_t - 650.0) / 250.0) ** 2).\n", - "thickness_means = normalized_thickness.apply(\n", - " lambda row: compute_thickness_average(*row.tolist()), axis=1\n", - ")\n", - "calculated_thickness_scores = normalized_thickness.apply(\n", - " lambda row: compute_thickness_score(row.tolist(), target=650.0, scale=250.0),\n", - " axis=1,\n", - ")\n", - "display(\n", - " pd.DataFrame(\n", - " {\n", - " \"Sample number\": data_rows[\"Sample number\"],\n", - " \"unrounded T1:T4 mean\": thickness_means,\n", - " \"calculated thickness score\": calculated_thickness_scores,\n", - " \"supplied Thickness score\": data_rows[\"Thickness score\"],\n", - " }\n", - " )\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2d-score-validation", - "metadata": {}, - "outputs": [], - "source": [ - "score_validation = validate_supplied_d2d_scores(data_rows)\n", - "display(score_validation.frame)\n", - "print(\"Uniformity warnings:\", score_validation.uniformity_warning_count)\n", - "score_validation.raise_for_errors() # fatal for missing, opto, or thickness errors" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d2d-objective-selection", - "metadata": {}, - "outputs": [], - "source": [ - "objective_columns = [\n", - " \"Uniformity score\",\n", - " \"Optoelectronic score\",\n", - " \"Thickness score\",\n", - "]\n", - "Y = data_rows[objective_columns].astype(float)\n", - "Y_df = Y.copy()\n", - "Y_np = Y.to_numpy(dtype=float)\n", - "(X_t, Y_t), _ = np_to_torch(X_norm, Y_np, device=device, return_device=True)\n", - "obj_names = objective_columns\n", - "print(\"Tensor shapes:\", X_t.shape, Y_t.shape)" - ] - }, - { - "cell_type": "markdown", - "id": "ee5a6060", - "metadata": {}, - "source": [ - "## 3. Fit Gaussian Process (GP) Models\n" - ] - }, - { - "cell_type": "markdown", - "id": "cef36573", - "metadata": {}, - "source": [ - "We fit one **Gaussian Process (GP)** for each authoritative final score: Excel Z\n", - "`Uniformity score`, AA `Optoelectronic score`, and AB `Thickness score`. The\n", - "three models preserve the direct score scales and the resolved optimization\n", - "direction is **max/max/max**. This gives an uncertainty-aware model of how each\n", - "final score varies with the process parameters.\n", - "\n", - "- **Defaults:** \n", - " - Kernel: `MaternKernel(nu=2.5, ard_num_dims=d)` (smooth, ARD per feature) \n", - " - Likelihood: `GaussianLikelihood` with an inferred homoskedastic noise level \n", - " (includes a small positive floor to prevent overfitting)\n", - "\n", - "- **Optional overrides:** \n", - " - You may pass your own **kernel function(s)** (e.g., RBF, Matern with different ν, etc.) \n", - " - You may pass **noise priors** (e.g., LogNormal or Gamma) to guide the noise estimate.\n", - "\n", - "This example shows passing custom priors and a kernel to demonstrate how it works, but you can omit them entirely to use the defaults.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb83dfde", - "metadata": {}, - "outputs": [], - "source": [ - "from mobo_kit.models import fit_gp_models\n", - "\n", - "# The model internally standardizes each output but returns posterior values on\n", - "# the original Z/AA/AB scales.\n", - "model = fit_gp_models(X_t, Y_t)" - ] - }, - { - "cell_type": "markdown", - "id": "b11f931a", - "metadata": {}, - "source": [ - "## 4. LOOCV Model Selection (optional)\n", - "\n", - "Instead of hand-choosing kernel functions and noise priors, you can let the repo \n", - "**automatically compare candidates** using **Leave-One-Out Cross-Validation (LOOCV)**.\n", - "\n", - "- The function `loocv_select_models` tries multiple `(kernel × noise)` combinations.\n", - "- By default, it uses:\n", - " - **Kernel options:** RBF, Matern(ν=0.5), Matern(ν=1.5), Matern(ν=2.5) \n", - " - **Noise priors:** `None` (free noise level) and `LogNormal(-4.0, 0.5)`\n", - "- For each fold (leave one point out), it re-fits the GP, predicts the held-out point, \n", - " and records **R²** and **RMSE**. \n", - "- After sweeping all combinations, it selects the best configuration per objective \n", - " and re-fits on the full dataset.\n", - "\n", - "**Pros:** \n", - "- removes guesswork.\n", - "- gives metrics for each option. \n", - "\n", - "**Cons:** \n", - "- expensive when you have many data points (since it re-fits N×(#kernels×#priors) times).\n", - "- poor fits when handling very small and noisy datasets and have convergence errors.\n", - "\n", - "You can skip this step if you are happy with the defaults from Step 3.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d88052f8", - "metadata": {}, - "outputs": [], - "source": [ - "# Optional LOOCV model selection. Disabled by default because it refits many GPs.\n", - "run_loocv = False\n", - "if run_loocv:\n", - " from mobo_kit.models import loocv_select_models\n", - " model_cv, results_df = loocv_select_models(\n", - " X_t,\n", - " Y_t,\n", - " objective_names=obj_names, # list of objective column names\n", - " device=X_t.device, # optional; inferred from X_t if omitted\n", - " )\n", - " display(results_df.sort_values([\"objective\", \"rmse\"]))\n", - "else:\n", - " results_df = pd.DataFrame()\n", - " model_cv = None\n", - " print(\"Skipping optional LOOCV. Set run_loocv = True to enable it.\")" - ] - }, - { - "cell_type": "markdown", - "id": "1d73d832", - "metadata": {}, - "source": [ - "### Display LOOCV Results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "206e169a", - "metadata": {}, - "outputs": [], - "source": [ - "# Display fitted model details from the default model (in source code).\n", - "for i, gp in enumerate(model.models):\n", - " print(f\"--- Model {i+1} ({obj_names[i]}) ---\")\n", - " kernel = gp.covar_module.base_kernel\n", - " print(\"Kernel:\", type(kernel).__name__)\n", - " if hasattr(kernel, \"nu\"):\n", - " print(\"Matern nu:\", kernel.nu)\n", - " prior_type = type(getattr(gp.likelihood.noise_covar, \"noise_prior\", None)).__name__\n", - " print(\"Noise prior:\", prior_type)\n", - " print(\"Lengthscales:\", gp.covar_module.base_kernel.lengthscale.detach().cpu().numpy().flatten())\n", - " print(\"Outputscale:\", gp.covar_module.outputscale.item())\n", - " print(\"Noise:\", gp.likelihood.noise.item())\n", - " print()" - ] - }, - { - "cell_type": "markdown", - "id": "91798dcb", - "metadata": {}, - "source": [ - "## 5. Posterior Predictions & Diagnostics\n" - ] - }, - { - "cell_type": "markdown", - "id": "bdaa215f", - "metadata": {}, - "source": [ - "With a fitted GP model (`model` from Step 3 or 4), we can now evaluate how well it explains the data. \n", - "This step does two things:\n", - "\n", - "1. **Posterior predictions:** \n", - " - Use `posterior_report(model, X_t)` to get the **predicted mean** and **uncertainty (std)** for each objective at the training points. \n", - " - These predictions are automatically converted back into the original units (because the model internally standardizes outputs).\n", - "\n", - "2. **Diagnostics:** \n", - " - `plot_parity_np` shows **predicted vs. true values** for each objective, with optional error bars from the GP’s predictive uncertainty. \n", - " - `plot_shap` estimates **feature importance** (mean absolute SHAP values per input dimension), so you can see which process parameters most influence each objective. \n", - " - `compute_metrics` calculates **R²** and **RMSE**, and can also return per-point residuals and z-scores to help check model fit quality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bc509011", - "metadata": {}, - "outputs": [], - "source": [ - "from mobo_kit.models import posterior_report\n", - "from mobo_kit.plotting import plot_parity_np, plot_shap\n", - "from mobo_kit.metrics import compute_metrics\n", - "pred_mean, pred_std = posterior_report(model, X_t)\n", - "fig_train, metrics_train = plot_parity_np(\n", - " Y_np,\n", - " pred_mean,\n", - " pred_std,\n", - " objective_names=obj_names,\n", - " save=str(diagnostics_path / \"parity_train.png\"),\n", - ")\n", - "run_shap = False\n", - "if run_shap:\n", - " # plot_shap calls the fitted GP posterior directly, so explain the same\n", - " # normalized input space used to train the model.\n", - " fig_shap = plot_shap(\n", - " design,\n", - " X_norm,\n", - " model,\n", - " objective_names=obj_names,\n", - " save=str(diagnostics_path / \"shap.png\"),\n", - " )\n", - "else:\n", - " fig_shap = None\n", - " print(\"Skipping SHAP by default to keep memory usage low. Set run_shap = True to enable it.\")\n", - "metrics_df = compute_metrics(\n", - " true_Y=Y_np,\n", - " pred_mean=pred_mean,\n", - " pred_std=pred_std,\n", - " objective_names=obj_names,\n", - " add_residuals=True,\n", - " add_zscores=True,\n", - ")\n", - "display(metrics_df)" - ] - }, - { - "cell_type": "markdown", - "id": "4ebd9ea3", - "metadata": {}, - "source": [ - "## 6. R1 UCB-HVI Debug Candidate Generation\n", - "\n", - "The guarded Step 2B path ranks grid-valid conditions with **R1 UCB-HVI** and\n", - "shared local penalization. The optimization contract is Excel Z `Uniformity\n", - "score`, AA `Optoelectronic score`, and AB `Thickness score`, with\n", - "**max/max/max** directions and the fixed reference point\n", - "`[-0.01, -10.0, -0.01]`. The reference point is a campaign constant; it is not\n", - "derived from the observed worst values.\n", - "\n", - "The adapter selects exactly five unique conditions, expands each condition to\n", - "three replicate rows, and watermarks every artifact **DEBUG ONLY - NOT APPROVED\n", - "FOR EXPERIMENT**. The batch is for algorithm diagnostics, not experimental\n", - "execution.\n", - "\n", - "### Pareto front and hypervolume diagnostics\n", - "- The **Pareto front** consists of non-dominated points: no other point is strictly better across *all* objectives. \n", - "- The **hypervolume** measures the space dominated by the Pareto front relative to the fixed campaign reference point. \n", - "- As we add better candidates, the hypervolume increases." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e54ea0a6", - "metadata": {}, - "outputs": [], - "source": [ - "from botorch.utils.multi_objective.pareto import is_non_dominated\n", - "from mobo_kit.d2d_campaign import (\n", - " D2D_REFERENCE_POINT_UTILITY,\n", - " build_d2d_objective_transform,\n", - ")\n", - "from mobo_kit.metrics import compute_ref_pareto_hv\n", - "\n", - "directions = [\"max\", \"max\", \"max\"]\n", - "objective_transform = build_d2d_objective_transform()\n", - "Y_t_transformed = objective_transform(Y_t) # identity; values remain Z/AA/AB\n", - "transformed_ref_point_t = torch.as_tensor(\n", - " D2D_REFERENCE_POINT_UTILITY,\n", - " dtype=Y_t.dtype,\n", - " device=Y_t.device,\n", - ")\n", - "if not torch.all(Y_t_transformed > transformed_ref_point_t):\n", - " raise ValueError(\"Every observed score must strictly dominate the fixed reference point.\")\n", - "\n", - "_, pareto_Y_t_transformed, hv_val = compute_ref_pareto_hv(\n", - " Y_t_transformed,\n", - " D2D_REFERENCE_POINT_UTILITY.copy(),\n", - ")\n", - "pareto_mask = is_non_dominated(Y_t_transformed)\n", - "pareto_Y_t_real_world = Y_t[pareto_mask]\n", - "print(\"Fixed reference point:\", transformed_ref_point_t.cpu().numpy())\n", - "print(\"Observed hypervolume:\", hv_val)\n", - "print(\"Pareto count:\", pareto_Y_t_real_world.shape[0])" - ] - }, - { - "cell_type": "markdown", - "id": "c5b6f156", - "metadata": {}, - "source": [ - "### Guarded R1 algorithm-debug adapter\n", - "\n", - "The package adapter calls the Step 2A discrete pool, UCB-HVI, and shared local\n", - "penalization APIs directly. It selects five unique conditions and expands them\n", - "to three physical replicates only after acquisition. It never invokes the\n", - "legacy raw-objective proposal path and never writes to Excel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5d929ef1", - "metadata": {}, - "outputs": [], - "source": [ - "from mobo_kit.d2d_step2b_debug import run_d2d_step2b_debug\n", - "\n", - "# Deliberate opt-in: candidate files are always watermarked debug-only.\n", - "run_campaign_debug = False\n", - "debug_result = None\n", - "if run_campaign_debug:\n", - " debug_result = run_d2d_step2b_debug(\n", - " workbook_path,\n", - " config_path,\n", - " save_path,\n", - " overwrite=False,\n", - " run_sensitivity=True,\n", - " )\n", - " display(debug_result.candidates_unique)\n", - "else:\n", - " print(\"Debug proposal not run. Set run_campaign_debug=True to create the guarded bundle.\")" - ] - }, - { - "cell_type": "markdown", - "id": "81c1e56f", - "metadata": {}, - "source": [ - "### Legacy global-distance sandbox retired\n", - "\n", - "The earlier experimental cells used APIs and constraints that are not part of\n", - "the active package. The Step 2A shared normalized-distance selector and the\n", - "Step 2B adapter above are authoritative." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "69f2e4d5", - "metadata": {}, - "outputs": [], - "source": [ - "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", - "pass" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1eeac466", - "metadata": {}, - "outputs": [], - "source": [ - "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", - "pass" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "35dff6b8", - "metadata": {}, - "outputs": [], - "source": [ - "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", - "pass" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b3ac1ffc", - "metadata": {}, - "outputs": [], - "source": [ - "# Retired legacy sandbox cell. See run_d2d_step2b_debug above.\n", - "pass" - ] - }, - { - "cell_type": "markdown", - "id": "fb3517fb", - "metadata": {}, - "source": [ - "### Review the guarded adapter result" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "626723c3", - "metadata": {}, - "outputs": [], - "source": [ - "if debug_result is not None:\n", - " X_next_df = debug_result.candidates_unique.copy()\n", - " display(X_next_df)\n", - "else:\n", - " X_next_df = pd.DataFrame()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3b3dfb4c", - "metadata": {}, - "outputs": [], - "source": [ - "if debug_result is not None:\n", - " print(\"Debug artifacts:\", debug_result.output_dir)\n", - " print(\"Unique conditions:\", len(debug_result.candidates_unique))\n", - " print(\"Replicate rows:\", len(debug_result.replicate_worklist))" - ] - }, - { - "cell_type": "markdown", - "id": "d94c5fd8", - "metadata": {}, - "source": [ - "## 7. Review R1 UCB-HVI Debug Diagnostics\n", - "\n", - "When `debug_result` is available, review the guarded adapter outputs directly:\n", - "\n", - "1. `debug_result.candidates_unique` contains five grid-valid conditions in\n", - " physical units, plus predicted means and standard deviations for Z\n", - " `Uniformity score`, AA `Optoelectronic score`, and AB `Thickness score`.\n", - "2. `base_ucb_hvi`, `penalty_factor`, and `penalized_log_score` document the R1\n", - " UCB-HVI ranking and local-penalization effect. These values are R1 algorithm\n", - " diagnostics from the guarded adapter.\n", - "3. `debug_result.model_diagnostics` and the saved sensitivity summary expose\n", - " model-fit and batch-robustness checks.\n", - "\n", - "The objectives remain direct-score **max/max/max**, the reference point remains\n", - "fixed at `[-0.01, -10.0, -0.01]`, and every displayed or saved candidate is\n", - "**DEBUG ONLY - NOT APPROVED FOR EXPERIMENT**.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6f7cb13e", - "metadata": {}, - "outputs": [], - "source": [ - "if debug_result is not None:\n", - " display(debug_result.candidates_unique)\n", - " display(debug_result.model_diagnostics)\n", - "else:\n", - " print(\"Run the guarded adapter to review predictions and diagnostics.\")" - ] - }, - { - "cell_type": "markdown", - "id": "a6014a48", - "metadata": {}, - "source": [ - "## 8. Visualize Your Progress (Work in Progress...)\n", - "\n", - "We track learning progress by plotting the **hypervolume (HV)** after each batch of **observed** results. \n", - "As you collect new data and the Pareto front improves, HV should **monotonically increase** (or stay flat).\n", - "\n", - "**Key points**\n", - "- Use **observed outcomes** (not predictions). \n", - "- Keep the campaign reference point fixed at `[-0.01, -10.0, -0.01]`; do not recompute it from observed minima or worst values. \n", - "- Compute HV on the **cumulative dataset** up through each batch." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bc1980a3", - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"Current observed hypervolume: {hv_val:.6f}\")\n", - "if debug_result is not None:\n", - " print(\"Candidate and sensitivity diagnostics are stored in the guarded bundle.\")" - ] - }, - { - "cell_type": "markdown", - "id": "1230729a", - "metadata": {}, - "source": [ - "This plot shows the **current Pareto front** (green) in 3D objective space and an optional **predicted next batch**. \n", - "\n", - "The **reference point** is drawn as a red star." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "99ec1de4", - "metadata": {}, - "outputs": [], - "source": [ - "# All three displayed axes are higher-is-better final scores.\n", - "directions = [\"max\", \"max\", \"max\"]\n", - "print(\"Pareto objectives:\", list(D2D_OBJECTIVE_COLUMNS))\n", - "print(\"Reference point:\", D2D_REFERENCE_POINT_UTILITY.tolist())" - ] - }, - { - "cell_type": "markdown", - "id": "e07896ef", - "metadata": {}, - "source": [ - "## 9. Save Outputs\n" - ] - }, - { - "cell_type": "markdown", - "id": "0e533308", - "metadata": {}, - "source": [ - "Only the guarded adapter writes Step 2B outputs, under `local_outputs`. It never\n", - "modifies the source workbook, and every candidate artifact remains **DEBUG ONLY\n", - "- NOT APPROVED FOR EXPERIMENT**.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9292a4fc", - "metadata": {}, - "outputs": [], - "source": [ - "if debug_result is not None:\n", - " print(\"The adapter has already saved the watermarked candidate and replicate CSV files.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "88ff7744", - "metadata": {}, - "outputs": [], - "source": [ - "if debug_result is not None:\n", - " display(debug_result.replicate_worklist)\n", - "else:\n", - " print(\"No outputs written; the debug adapter remains opt-in.\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "mobo-fom", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.20" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/results/demo/batch_1.csv b/results/demo/batch_1.csv deleted file mode 100644 index 087ded1..0000000 --- a/results/demo/batch_1.csv +++ /dev/null @@ -1,23 +0,0 @@ -Unnamed: 0,speed_inorg,speed_org,inkfl_inorg,inkfl_org,conc_inorg,conc_org,temperature_c,absolute_humidity,Unnamed: 9,PCE,Stability,Repeatability -units,m/min,m/min,uL/min,uL/min,M,M,F,g/m^3,,,, -start,0.25,0.25,80,100,0.8,0.4,20,2,,,, -stop,1,1,240,280,1.4,1.2,50,37,,,, -step,0.01,0.01,1,1,0.05,0.05,1,1,,,, -,,,,,,,,,,,, -,0.58,0.3,190,134,0.85,0.75,24.7,3,,0.0,0.0,10.0 -,0.95,0.58,170,246,0.9,0.85,44,19,,0.0,0.0,10.0 -,0.67,0.95,150,179,1.05,1.15,31.8,6,,0.0,0.0,10.0 -,0.77,0.39,130,269,1.15,0.65,28.5,6,,0.0,0.0,10.0 -,0.39,0.67,230,201,1.2,0.55,41.6,15,,10.69,1.35,3.333333333 -,0.3,0.86,90,224,1.3,0.95,41.6,19,,0.0,0.0,10.0 -,0.86,0.48,210,111,1.35,1.05,36.3,16,,16.43,5.68,0.952380952 -,0.3,0.3,82.5,120,1.4,0.4,23.2,2,,17.22,0.9,0.735294118 -,0.92,0.39,213,181,1.35,1,45.1,9,,0.54,1.09,0.392156863 -,0.89,0.36,228,280,1.4,0.65,46.5,15,,0.0,0.0,10.0 -,0.3,0.67,240,213,1.4,1.05,38.2,2,,16.83,4.68,1.282051282 -,0.43,0.85,213,254,1.35,1.15,42,9,,2.18,0.0,3.125 -,0.72,1.0,107.0,114.0,0.8,1.15,36.0,17.0,,17.35706971953076,5.219312558290997,8.984170432675196 -,0.67,1.0,80.0,106.0,1.4,0.95,35.0,16.0,,15.976962026424701,5.193390711635139,7.455737159814987 -,1.0,1.0,163.0,124.0,0.8,0.65,34.0,17.0,,16.11921708962232,4.583029335484693,8.897908609111669 -,0.26,1.0,240.0,104.0,0.8,1.1,46.0,18.0,,14.729113480381507,5.0374920889118515,7.303781029245997 -,0.99,0.79,80.0,131.0,0.8,0.6,43.0,19.0,,14.511073685461238,3.8791072174518737,8.652695960741147 diff --git a/results/demo/hv_demo_with_predictions.png b/results/demo/hv_demo_with_predictions.png deleted file mode 100644 index df3b12b7d308a25396bca322a927a8261d17e01a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76677 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

7nw9-902x0JG*1?jpKKWwJ0hY0I- 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
@(rj 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

3Z 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%0Al436>s@7jE%MVQ&?gciGTQ+e#1fa9+75M*2Fcq zVhsa-roUlvuAKhgx;~y-Q}=*AOT%&Mj9y4Ix^lXIGNUWfyDOqtny(9=W>kkyz7_fK zUgejUzM8+}9r0AuI%C}~EmvSk) zXo>EQ(<#QCZ1}YuX(pR=&!y>%X9r)D)t`hpl-4yU9PIs9r@dOcs7h!W==f5hTS*zz z&u>O^wUoD$Q|GEFls`C%r9*4jr=;_IQ*4UFzU>ZrRVp4m$=LP6a%zFz6i#^M`B-Bk zR`5`W9MJ+3lCVs^oq6_~8N=KG~&1|%`XHyBi5Cq_;{1Bu4Bww#jLdB2KVd&0J zQWQ#mzF+~{v&|oH<>Xp4RmT91$99L zr-f6DU+C#_XsgkH9xL*!M&7r7Lvl-(Yx3NYlYwW?a+$lJRHpwlgxCS`=GQRyd+=cY zru{qfzljZF33Y0X(IX3Hx@7YaA72qK7qRcYEz!i9SKy%!ZUGXsrL);r|9;s@z)e1` z8bRvNb3`JIv$ZpX%~{=Ycwjh1iPbL1ca$0-OTxomZEn{<=Gy3FeuW$JEzoP55*l%%3`i{&g~K zy8rkkd_IV@CQQ3M_`fpIVKM9u@CV#0UPkfhgx*UByK?2iE>7pFv6I6(D-Ycx{j8xn z`g_fZ38`g0@-dg6+fs2kKY4>C?1%$aaq$39-xPz`Vc6COqQ@_S$`2GgDADp%!?-^= z5cSdIFEG1<&PZ7=O|wooMKo0T#DXlxV4j!w)4jH{+X9P)vEmlT8E4z~Wv1BfEqY$| zHw1iw@W{rB@TRz1=IhQ{W#+Hwjg zUP4L=$^=T4QXwf)VaV^c_(rmv1+g$xAIlKZIp189q7UsKbSTaZ{#tjJA*)i>T8u~j zil4zi&QXr!dg)&rxY$o{(3FG!Q+SGMc2fwhy2GvljDV>mFIi*lF4-ezYAXUNee`tD zPR?wuOPcv_YCSnY@5CYY^l&xZ*x5Yb#~%x*I@ej8MTf1ycabOx*?=2VJV&Xnqi8fN z?{!3bb082gG!gy_tO(dbmtYiQ>cEXwfl0;!heZES`af2Tgz?1u5W*$9VOHKQnO-Yv z6NS{c^=UcTQ2t2w8k1?t->>pKQTyM2H#E4vb;55Vr;p6Q9(Jyy0FU{MA=W(hzVtQ4 z(Kdg;TWB5ho!g^##;+X`v$$v;ubQ=G(;ozAhn|neEvUPQ-#5BDOCz=CAf%Z$`LM2qRo|{m#YoX>{uuBdPR*}@TK*d`thhg5a|lF2f&}lJdzXi}rA?bR!7zCz%A8=( z%jLlko9Y~d5H3{riRTcKI4hN?NVTbW5ew!E@Sik=?rr+s`+PRfaXOE4{g3e%OUreV@6)`H83LsZvw1vg_`{sY{`uU8U!r;$KL9*)}|)dsRKbe(S!<|KYuRl(;N{rYx4t)Ftt$xXZi8FHII^ zfQyVQTh#F{9mj=d$U#c)=kC|H1DqvT+q=9F#pqS&c8-^!&hUrgvZ7M)O&e>0nlcE(L2fx&0 zD`3^QViZB$@xo9Ds!#;lABbONDxfWy+cSw?Du_8+oYahtX^X`ts)XoPaQC2X`l^o# zD8$&A+5&&h=OSL_8Xcx|ROE7#4pYCWF@>nD@+}FK*Zum?mA_tR*ubaPw%{aMB@STt zk>@!@a~AUY2H@QZ9kKj-o7Z64}5BRYXh-;Tk7* z2jYwgf6svOXVpbyovgj9$~v4#t}4u2oz^p$e!bjanqu1E^qb0&Qx3a`vuH)o4-tVX zUS|&azr%Kriqo2vj(!hnr39Q}o^lW0Z@#NK3KHl1x55~jZQm(L!BvQPy3!$!Zx%M^ z%C)&4h(mpoZ$H4Tw1pC$OzS7sp-+q2wz-eejlS!m1<|lqf_u_Zx?-_C01^i5p zcROKQo48q_`b6JPoS)FYOSU%F4CQ5s4w^V^xC&B3$NLa(z_a@C1`Rm`r=8@t?Z>ud zvckr#y!va%RE%nSxJT5cUFU1*i-&phe@6&BUVkJ(qr83HkARA^yYf~Y3fNt5Hkwr! zNt)O{Cu$^k7xAAXRtF^JzQBTE(+7V5)+)!4o7mrd9Vd4;m#^z8*%@TaBiyqpO+_wh z`$0Pxa-|^QiJ%Wlo0Y%Z{u2wl2E~`TFuY>?VeQqDPc9FsmZd?a+9QKgYJ|+S zZZccaarjQyi7<@AD`738U7RV8o*%!g2y%1@URU;azh|AY^`xEKrki8Iz$A>;A>KBn zjUm($r)kx@?|qzXvkm!MA?ur_-3|jiZEA0iCUe2$TamC5am-v;e1bC7D#b$}19?Lc zmo0G`h1Nt~DD&il)|BkO3U$5S{Ly6mxN&6ZgX;^_jML@cPT`K8R>;L3Y@ z8K?^ZD)m@+tU1|CoE;0N!&+#!w~<^1VKBSrkB$4QUU|i4GeNdy zcg1sKgs3ixcD>d^%Q>xvcD=kf8%Sn6hGR>0ztCArm5K$r?S^La)* zP8UyGR@sU%(&yF;VJAwJS1wRF8BwurUU!tpf&W+xq4xGXTXG2M;sF`;zhLC-zn@*v z$EUetd()V&fhY2h$_TdFHZJ=JqQz2>W#W7VUKqW6dZ2Q)YiN-1j6pxy!d0z{?cpdYC!&jZNMm_#8vnt+FgG@@%Ou+W56 zm!KHu+~jVJl1wX< zA7FY&)!udDBX*5qMOCLFn~Xz#8KR~LVbyaUR2WA`rFI@!nQ`VLZfmVrJJby`Mif#;Z%KfE3iwzPUEmM*GpPiq9{xX$tKmlH`< zhlhX!`?N6;XoM`#2{3>{$kA&JJS_9c2%d@H-=Gsu$J{~FDuEoif+@Du#yAQNA^DuI zgLkk9I<5oa&k5=LbSvR?D1kapE4}9Tvuo z(h5;Pg8zjdpom*s)NUk{eU_G5#iBQ(p36yqxHiARN$lT3Aq8r|Kv`D4dtJ-TIK3Nl zx8?}tLF#ca_8{|e3K0^A_lwAdSnRUG;w;PRxaGsI48~@W#2u27jrTmsLpG0y4e3t_ z*~_ztd8`=9`@&&`{JJLgVS8Hsk~VBe+Q@!snq-VwD@dj=QV9%&w>)Mji*0b7a*!K< zlpI)qQa4*$40z@}=ELouC~Y7Ub*#9XsqBb3%dO`v_mVGQUoYD5#*4U>Sl0(GO`<&; zx>poy;4S$oz}*1n0Tp6nj1zoSgIDYlDTLov`5-)m6u1sS$J|UnV1k?X{=)OgXhD<2ITN@ zSjcDs%N5P)StLo#X;tJ!W82IxEt@X(`MUD1@?EOs?jSCS{)nPT+c~>jhnBQEh)Ql5 z4-1Seo*+~-#WiS3yy-i0hI%*;f`O@=eHohU$;W70iRFhEjQfA zA$MUTkPn17#50-w*;=knnC|#1agF=D*SY#{i+z%`L0uoF@tUZ?9uV{TefQ0hBTKm7 zSkX2P@fv6>)0MrTvu0!H8pz7#AQ>iDYX~2xUQ1oTx4r`*4LEhxJKYAxR=aARZfo9nuBRCK?lj?WN4Jul zZ+dxL>58CX$ufzp8`*Vw&=gt#pSu4?XG4QF@NID8o7*+^ZV) z=_#BJ6nhrK1n)7w;Kho##>7z1Dp*8p%i5J9M;WK%#EjLNm)9@^flx>K&Wuw+tmIa# ztmGy(==Af>I+vRM1Xsi!Q}FuurZ6c3_Ej`H{A(lURYsnwXGxWx<)!ZLFF)04clvs= z6vy=irU6_E#0kTG5wqF2=9a6hD6)#W6h7{zx?z(_P-+r7dL3Cn8%{nYy7KV@8&&6W zp-h}w3+boThxk^{%K;v*+uo8KsfOdbSe|~-3orv3FH1<*BeA1B^PNg1hg$MKO##(m9Vb2p}N_-0OWU?=I z5Q~`a(;G7K9Iapwr@x7R!1dF=%vB(rF*=(oB;k_!3u3!)Mw+3A8$VGz54;NntyWt= zPdbwAXBfe?X==&$Hgq9i+TNrIYt|8~2ISf%ycs_$*@|@%vSd2PL2^qPg@)j^!d^z@ zJhShWalN=4a2A1 z?{++%3kr(3@#gu`X7N2lAH`}&Q9b*4^V(J`t&tM&>E*9_kcH=~)nm<&<09g>M^Rj7 zr6EkU>Vw*}62X#jwBCzxYJOrO&SuaFsC<))j%@Mrhba+XfT*muiyaXbIu<-m>#HRg zTvFHvhvHX3@wYrcj=My8>)|T-Mc`J<({Lq#gDScYL1wRAH2>~ivD{kqAn_Dumsp|c zq%I3hRn+gQrcfH82(ocBW>ZQy4caY;;)$Vw%c6OYa5b~Q#nEBTtLqP*10DrZa1k@v z_D0NoierjTr!S^6;w`nE{YyNb^L5hRcR5n|S|?W%Qny;PSdm?$3z-*^|8|rVQ@`R= zi@D8(*VauOkX-dVk3q2#LVob41P3QeH}8pHqz#YUq9PI*(LCV9ou_@|pkWfDOl8e2 zye}Vl|CPJm#_~qTuU8jDhoW~h595wJixsAwi^TgjD&NnX|$H|X>(SWmx@ zBl&OQEq2?Gii)!=kVn&esyOmD#+F0lf_Zas%lcgy=Ww-L#kjPV>#XChG%?j`A?(&L z9ltnz0%_RuK&mRv!W0XMT!{KLplMdnEU{o%o|@`t&DFUtuYq|2lZ=AVI2kg=E;6&2 z=>a3na>3+LR(qIMADPYXFdcKxL}72}5&kc}brfk!O~qLvhPWie2)y2jNZ&0r-^#ti zHL)e#m5jQuVwn0T-lwqu{q~&&PeNV|W8oJ!cdr~4d0wkEKxAv=davD^oPwB3TIH7R zmK=`PdO;}=Z6PrwQ2sm|A*{EW0K4x!!}XT8Wof%lq5yHlfoUyO6fXkIm=++mI&LDE z9ujsG&&C%Dr{$feo}}f_lDWA@d8?2J zaW^XbiZII)Sdp@#%7t;l-{wiRMrCsH3D{pG=InY_7IB%kO!JVkotNOkGWMM}HFs>Z zxPk-ekXACUEm5{!0EiCF z*x}z5+B<*-Z42P)j&QXfYo|cWj1}D19`v%INA;l!EjGohd!tHIBg{8&^vlN65IU#( zE#t;E#Mw?&C+;bPlX*wp+q=A)3eXZ4^CTK4@7Jn?#4EB6f%oi5#=Cq`{>hQ24}~O! zFwFB0x)|d#cCfA}VW<3nCC!T#9zKZu=T(p_u3#p$usT;5GLK(y16_xvTM}(fy!kfh zw2+>!JnGO1FDi-)LJo2JOFwP!atG_QeFja7N@c34a1lD7lLdf$#W{%l$sj=^Si3(_ zPT#k~T8jPqmbcH);TZRnR|ysOFWv;=uuIDk{#KUtOX~|7Q_@@0e(G#$Q zVQT*e-<0Nl&OJ>|z?;vgGJh}uLXNA?$N54e* zh>_!rN6_0Bdg=Vq5};v_A3CrbaYL3vQC>nnZbD2JFbQF$h^(oN*Hg`=xM)Xf`ZrDV zFP#J*1)_#CoA)b!#rzB>;m^WLY6Qk=EYoGEkE&m8T5%%ZF0z(yLQ>P)mAj2=<-ib# zOdMld=E@bsXf%*f*MzE^FCD{9NkYCF0$b6r1@@!-N7d`>0R@s?J&M1ajdENv9;aKV zTRmOm;b<7coPW9$wC5)R_;l5BF}Cv@$mfilVp?p8N@d|xC{jdh4UUbh?WwzWh(}6X zp64isBzt+hPcp%E`kK6P)r0;gA=iB{2M2?pt&-^iit{9~DyMjrnICevm$O>t!_rY} z*eceYi$LbI2v^;>)0z*0uw%aiFY{Qa>5qj@*Qequ+Fs4wNRZIfB*uR7Q%eG}TC`56 zCB^rheDA&c1zKnSx07Y~StCV%k5A@!JELvNxSdzSMhPaX7ZJY2nk!ah%k37)5|LCX zPFnSbyy8kX&ivBi{j;^umgLL4d6Ai3GXcv6yK1143w+${u0K29Y@DRF5uYy&%Tl-3gUhD zLPI^X-j*dVL&*ED?QEsU3pYRSueF`7#cxP-5e&pSYQIX(=k|&6_zh&V!PH_9B}Br< zGtRzucl^GJy#7s5q?=F_94-lsw1sK`H9DaG54jp} zU%W1HgE|_jtSkrbK8j?Jpg3{60P7FcrPEbP3m~Vg*l8X=`89j`i}%x9f?qTKPfsmT zTPV=lHG#VLI=?L05bt0jo)sy+qBMEtS#&%DX5w`l?bVzA&Z_eN=}$gxc|hvZEokak zfFKIHxMlmY&%O!q7Be5k@`je7$9ob2X<|W z@$&wZRFNz{K(weu5JcYesD#-Vf-r~`pS~^*HIj@T1(4N+g=VkVU+#WQE5Rjhg@F{$ z7k_tB-iKteu@LBn1eTdbD~bbEsX}_^OZ~^$m-ETY5jknz%sho(CL;bOBL)&PD76Wb zKV)m(WN6#RE`Utb?LS_K%L@@TIgViM{D>=i`kGV;IQ-_XKL2T0botZlasR9+G+)%m z-u?<#UCn^0$AyZZWZ79o&raIT_M8>F@=_ij zf~^SIEiEa(vKNuw1Yv>ADxiIzVgmVl;+&9ohe_!2N^yU*VRQ$UkC z1`YK26q@!!6f2}|Q^R_7-y8ZKo&-9s&fVOg?cVRq%Z+R`!6&%)^|AtXVL{y^Q zQN}&qw1gNL@rK0|H;z>$R!&;8>JD;1 zf*x)`2%cxbTT?ajxO|d4a6fgIp_asFyxO45MDNms|CQx z&-7O!dJe{So}09*1lr7H0|Km+Y;We9g<3>Bx*sNn;v;?A3eD6fx81QS$I)jVfWE60 zgMVwS=6wJSl?}VcDDJgeu=x>-?u1qTTQ(mT&JIG?HUT@Mx_~Nq5sEWffFuQi&zRfs z_fJYgrR5bNz)uH#i%>f9*4B9F$#^WNb1hB#sof%czUj1&eFt;#RQ1I#>Xg>5qM>_! zq4oQvU^^<==shopi~8ko=Qrf(0kO2f=c_{)BT~`q`A(@rjDUcIs&kXuehLIt67JGQi< z_30Ksqt2$Zn}7^BAGyHWmhA789RTIpW@Mw{)t_7IYlIRA6;=t1TH6^8jt~#_18<%+t_lYa2PgAhRNXz}U=hV#ZIcA$q~2?+ z6Eq6BS_CcxMNQsIH0i*UwUM|(tgdDkMJ!!@} z<9*%uuAqeSw0yqNC?l;1eD)reT7ELUd-qN^?B7pOhX>)k91iZkA2uBE_y2src*w^8 zv5|b22V#K{naQBnW%;Xr%F{_ocB>5M2p|GRnLYs~Kd*kM|50MWl&Qz!iI?L53f zn-#YAL<*^Bvba{StNz`ILfSWPWW0TRXh9(P+}zynDtVHs1@dWKOA@)%`{Vg?dw0&) zCq^>T(y>se3mO(y(%I&K+5*zBrFR)8Y8^-@UGmrsq*JKn86Kqs}AW-qEWSDkdRe5EQ{hmadQ0+Z#0A z9X02x7ptyWlJeNZb_Su4`nIH`q@WWZp`gg^j_2p=)LJ<$y3WX?u;_jN@I!}uS(w_Z zxVV_Zeo5fwV#dJqd}}B^Ep7LtYHV!GY5SX7zu?uv&ulaJ$Hi1OqrkVrA+)r#5=gcC zGZhA)%ao$;#+G=%LwG5(>~r;P!DT|>Y%|563A0NJLFKshDii&7FiY3~UH8+cy8gFe zF)^ALSxHQq{msX1Ou)TMrqA+LRs8l=W42kEcJ^Sdddh8eO4A@Iij?;)7O;zgr@Q5+ zjjeS1WD39gv6_e1=l7?*6yv3utlCX3mPW_uW85*su!pU_(Q?{D%3=G5EB!83W4y*v ztyrhFBse&@h_d%r1mWhg-&2yH*JV)$O82i{zl&7zHfF0#7!wMMI(;7>?~Wn1^E@}e zVNzd-lP9I7GK>h_mEGTLXK9*ROckq1k?`0Qap>RQ-%nnd&y?#yysqr_uTV_oDq1hU z9xODf^~6wAK$?o&#ougmjtDqU@r%9>3H8$s4+|@jPJ8E8u7E?w!J*m{Nt_2UA!A05 zp%7-lr&m;`CAvK;`O0lwV&IP zVvmEW-btamyF0=@q4C-o6tliqD#ixm)%A6nt0k`rMqW`IPF|@Pa*a7Q2Kbs(ZtG;v zqy36@yZ$z{&hV%xQ=HF5VgC$(<-h5fL$%i6bK*fEp;%QLt31Sj9u1YqY+Dqm?u8xky6q!`YPj@FBTB zcnrnpoU!vmw_foJp1dnMDr)7D7y(~~jSEs|fM8a=yz)a`0FTdREA+_hKSp=pn?FzrU!+OSRqu(7m4GuX# zWeM*&OE_-lH)6cXjAA#@b{Z5q@fMH3B1EVh6y3CP9_$ z4Tb^5*xi)=0dEAgy641|pAmB9{iU&fS4Cm7N zc;a3{QT_}mK6+-H9Z?f)#9f@Q-&hDFJFh|v$+MTSWEFweK zsOW2fCPJ=z>MwD~c5njTVr&bsTrc?mE5cs&!10X| z7&(~^Scz2P0t|{TPW)C*ndp6SM{FJSKh8bLM#;x47zP9&msm7eQ3U_SA~0uQN)z&F z8t65yy6VPP;=}CPK+b}PL{$n)t`ge%;7DS(8|&UfCzP!AV#Y>@JE^#mhip5`>TTvY z8rP%WIbq>#Aa9eAFz$0Af9zZ%6Gfn*33LRF_M%p-qxFHuttX4(`rorf0QU=C3jARg znAiFcouQ2_CtVoRl#%2JANm-ogp+~ta`8~qF4L}-M8+h_=CBiBzd-Krh)nEVZJ@%yPV;#$ z@ro1HAQknA+`jdl>tP)xNX>;_&fAw7sIlw3j1eV3R6ReGOZ)N+^!(~z&cr#(cRT@! z;G35J&NEtuUqnLQ4d_}qEoc!h60Psg23Q&A;UXFb2jwp?xC7rYg#5B5T?BUM;$nSeE+6KPNCO?@v_zV6^N%hmiG+7AL6Vh3{#d2wGnl zF3qGEF)cnreODm<=0^kX(~yg6Blgg%+j;x;;*eUwL>WW)m@`U@7GXq(Sngm5GMLa% z824kB7ZQk3r9be>eEFWAcNQfv8crzP>$p`jX>Em%I`y*+T;!YHZ6tRPq!kBqfc1`& zP_};1c>46$%{GHW$V{n5>4);C&(36Xtc{(^I2q;ycbA815$*%#h}W1Z1wnYP6Zxr| zpB*CwD9YhG*idZmEi^j6O6DgX3LZ;IHClGCiRBMIU`3p%wC|f09YxAF;j8mMe(-xd zr?~JLds6t==G}N}a2L;vXb7}~Z*ByS$$hVl(mO1FeR(-$9s}aN|IxL~2Iinj$6i*6 zAm%hH!pluD@SWS-r!+Lbv0FVS;sA#+reI)WYX-bR3=q0I;N0JibjDql0;A%+H2vGP zs>H1x9x@-d-IezvR8gcwMB;!pKoR_jG&NRJvPkB!U!sNRe!&x9p(bl(zef^%=_Oq? zgJM)kDGv9Z;$B1spCG&;+Yf^S3#6cXI>1T5vO{mAL;)3veb$L}hev^ki}T_qQa)np zC0HACy;}4V#WS4G1g$H(#%8ie#VX))2sHv@Uv}jIsqdlW^s{U~RD!g=gVCSVgoKFp zIFS&rVKy;mzY=tg+y5|%T<{N_wEq>m@4b6^HkPNBVcZvtKqReEV*2OwQ4L@Kmtcy!>4T-&K?VPZp6G9u<)@E+<}Bxt=n+MV=y96b>(d z^`QCj7$3o@A)8{-c|@=fp~Zik#hayEwgetbDLb z<0R+a&qi2=OiQQ1)FiCj^JxQa$ahA!1 zs4Th&&;19&<31qns#1RN2rIIevm1 zi<08weE$_(y6_Ex$}#gHLtJCXF;=o~ zoyNow7I`?qPLmkj2-2rjw{S%UYZkq=(3!fC;hGw>2)UM zJJjAD3r^j;IH^Zwt0VREt872&aO>{7>Yj2_HK6;T9Q*70 z$VS(8^qF#=vSP?OH~=`j0S)3eJZ}p#)mJFNWpE|j$rWj0R!m8LGLv+LmqZ_ z!!s;Tl_UQpd=KVk5t*>KU&LSMbZ&hNb3#Qh5ZJZ+X>dZ2So<>~D-m;{5!P-yezhox zMtm4Ahy>dMgMw`BNWYP-4;wFx2jx+w`FN`oB&667(QK0s+sciQi0sC{!-=d#G{%_} zHK2Fn=vWAGVnfudS`J}y+{l!pHsQFET~=C_2^$E_XwGYcq1?|5|!NX>UaS(e#({Cs(IEd zNJb;?+nKAj>@tNbujzE9X@)o)DON!O%3${-h*0!mXzu7U{5YCO-1&w}Pr@GSP9oL! zYee&1-2A$p51k~ZD>{NC={}YJhtWZyHjhIulMRo|qf%o(n*}TLE_L;{u2C#BZ83iLz-eVgjSg&gf6GreB^_g zBQ&4s=7D>m4Lhz`P66IF4GWQ-f8T`A*`y)4N-5ae&9iT*Z4MjA$?)RI-2-4xx$yY9E zCQ9kdS3iXit0f8D?$E?M8Vh@0JEX^BLN!I;5Cyq$yo*)xK3n+~8@x_)#T*-~b>1G{ zGk%jUWhn z>RJ2Z^7L=1kkID+TK#W>fEs>CCvMa5^q_h~b`D~CUc(GE8bdMuGJ%M7cXdz_8rTc~ z0kw2WOoB5lmlP(=;oWm+1buaOq3qAkSW`$*DY)6<31*R`l68?n>u6Zf%u!zsmj_mV zhAw*QfiZV?dzlHj-78W%Lhu9z8l!vq0i4-)&f}Od=7knbE?B~H`$mIy_nH8DH5BHy z>@!1$8nm)cYTvR75p^{3ra!-%%iE3;!YF3&s}DoRbMAyS&eD|>Pj^?#e%l_pmsrN< ze@t@O3=J;w{8g;htibgTVt8{*(Xy{&J-`8F;lOGQP+f2Y>qv8$_;*ofnC4NsDvUuO zc<5LI020U)&#EX!q7Iv$Y4dp2T20004*ISs`+5;l;Y%*>BW_Ej^VrI6oYqpmFn{pHSFn2EDC&7+(IUHz6BB7jX?N(F5icQ{{txTpcsOw9Hz3F z^`=h|Ui!VfgamR=C<7HsN0G%o0P{A5-QoZr5XkF@5L{3tczsU6h5($pRS2IP*p)iurti+mHSa&+;02v^ZbT zNaxGX-NaBII{xy3`n&l^rfLQz8Q{n5!NnN&6j;1L$!2ueY%ciikW>{;X4m-L2Zn> zrA27y=uIV-Op*cbta}bnWp9;*az{qHTo}E8!r5EK;P{O*$Yl(071L!eGOBm7;&q)( z%+GGWb$4^#9lVlSmI3CoNf0mVD68}_0QyxoZOZR&_bYhj)EML#SrjJ21)~0C0P>2C z+n;>X?<8Z$ZSl3(n)t?_)}rBJ+9nJ6Ix6-VI>*efv*D|%RVuVxgipKic|`OX4Nj+Wu?+$vs?X=WELeT_#5-yNTwiDmfvIJ1rLI|1&@vJ zbJFcZBs%uivwkMUXVOHQ5A9D6BU^28G*YsT{>RTDZ=l3tsM!D?@N@LsajE6ua{g>W zNmy^Hg{Ph)E#Ac&H*Dp-biAaU%0-e)2uFWk1fO$?u6=DIb}vc*HmIO7R!&w{RnQC|3G;gV+X0llC#0ER zfA$Q69ryJbQ3diKqUXY zYu#bt!r`OW#a`0Bv+Ve(MD{Ll$1C1`CRp}9kJ@VuQY9Ji>TC=%xd15Aewr8o-r%ioCKi$Yh%`)zGs1oLv#egDfL5387PFnvo zxt8AQ8|}q4&)HtT3mhR|uek_#lfks36bzy|;C&QYbqCmb=d+!>m_J+!Qpway7-O|x z)DnPsO&0ZmHStJfLJhPLfxs2>3;G%~dU-f+-b$fn=pef12atzOEl3)1Z>GypRc|k(Xb3tZc?mgRKZn;9_G{s=NF<77`eatn9+%< z2U-A`GI_l5a{P1@{p#HZJy053AG$*_v7zI76mLmLW?q|kADR2HB76isq7ja~Xj*(e7&(ho_QJt(G&_JqH0l6*!S85+-!;;C-m}b16B!D)ORGQr#0_gRtbs2GB92JUOw2Oemasz{ z!hYhvlG;Xw@zAh<6_)dx<*|n{@Aug)Ukuyf@1%75Hf^MYuyWHun2eN5P3V6O(El1i z$`7oQ{s#8IvZ*vBh1gECKBEbZtHSQehJVfRw(BE6!2`C?wZ^vN*ji4^+1C+urqEYgHO{YKRgcS~|$% z`m>!q$^q3zybdhfhS#mx#VnAM*nBgC$MxsXzD_qhZJVQvJ3t|-wR|6az$djSikiI3 zsRfP2N+H}i4m>ZGOJVs;9+S-nsb(u?rlh1)s*q7vWut*fW?#raT(PeL;NS>6ljWO6(Lh6(tUVP|MO!uMO6bfosoyhwO$F)eSC(=Z4QxV#}Sk{F60 zioYZgLGXL{+dGXqtbVjgE%7?suRA>UOCMp>ujHPN?{CT2L4=vAj`KuvLC+$6)>+oX zx7iDr+N4?^sddpY_**nV_sYn*$NQ`DuztWJI1�jOq?pk8t?5l94wtAO@jX`O)hj>Y%BS061rIc8s>;xIs_S=A0JG5J*mAST0a=hUp1QID%Q@~sezl^_y*{nVt<=G(R^Pn

5_V{PR9P9G|5(vR5Ezspf36^4f*svhHlN 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

*eVw~A8n~~~eI#tznMeNV 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