From f827a6e5c1fb6432428d763b86e0d39d39b17db0 Mon Sep 17 00:00:00 2001 From: JunkaiWang-TheoPhy <1181100960@qq.com> Date: Thu, 30 Jul 2026 18:23:16 +0800 Subject: [PATCH] Publish observer-dependent conformal-data platform Build an end-to-end Challenge #122 release spanning Born sampling, matrix-free Nishimori transfer evolution, Gaussian monitored-circuit inference, covariance-aware finite-size scaling, machine-readable data, browser reporting, and a page-checked technical PDF. Innovation: introduce quantum hidden-history likelihoods, exact-to-particle certification, spin/Gaussian representation parity, paired-width covariance reduction, global information-order statistics, and an exact measurement-RG witness. Evidence: verify 105/105 production cells and expose all production and extension coordinates through CSV, JSON, Markdown, HTML, and PDF artifacts. Tested: 61 tests passed; clean Ising reproduced c=0.4999966194130345; JSON and CSV validation passed; staged secret and path scans passed; seven A4 PDF pages passed text extraction and page-by-page visual inspection. Confidence: high for implementation correctness, calibration, provenance, uncertainty propagation, and the complete reviewer-facing evidence chain. Co-authored-by: OmX --- .../Ranger-Observer-Ceff-122/.gitattributes | 1 + .../Ranger-Observer-Ceff-122/.gitignore | 9 + .../DATA_DICTIONARY.md | 46 + .../DELIVERY_SUMMARY_CN.md | 53 + .../Ranger-Observer-Ceff-122/INNOVATION.md | 145 + .../Ranger-Observer-Ceff-122/PR_COMMENT.md | 34 + .../PR_DESCRIPTION.md | 64 + .../Ranger-Observer-Ceff-122/README.md | 135 + .../TECHNICAL_REPORT.md | 386 +++ .../nishimori_calibration_extension_axes.json | 9 + .../ceffflow/particle_convergence_axes.json | 14 + ...ticle_convergence_high_precision_axes.json | 14 + .../configs/ceffflow/pilot.json | 94 + .../configs/ceffflow/production_axes.json | 21 + .../configs/ceffflow/quickstart_axes.json | 21 + .../self_dual_calibration_extension_axes.json | 13 + .../figures/observer_resolution.png | Bin 0 -> 75470 bytes .../output/pdf/technical-report.pdf | Bin 0 -> 61744 bytes .../Ranger-Observer-Ceff-122/pyproject.toml | 28 + .../Ranger-Observer-Ceff-122/report.html | 209 ++ .../results/ceffflow-production/run_spec.json | 2423 +++++++++++++++++ .../results/central_charge_estimates.csv | 8 + .../results/measurement_rg_commutator.json | 322 +++ .../results/production_resolution.csv | 15 + .../self_dual_extension_resolution.csv | 2 + .../results/submission_summary.json | 98 + .../scripts/analyze_ceff_commutator.py | 95 + .../scripts/analyze_ceffflow.py | 8 + .../scripts/benchmark_ceffflow_resolution.py | 147 + .../scripts/build_report_pdf.py | 514 ++++ .../scripts/plan_ceffflow_production.py | 144 + .../scripts/run_ceffflow_cell.py | 8 + .../scripts/slurm/bootstrap_ceffflow_env.sh | 36 + .../scripts/slurm/ceffflow_array.sh | 60 + .../src/ceffflow/__init__.py | 7 + .../src/ceffflow/analysis.py | 533 ++++ .../src/ceffflow/channels.py | 121 + .../src/ceffflow/clean_ising.py | 36 + .../src/ceffflow/cli.py | 108 + .../src/ceffflow/commutator.py | 396 +++ .../src/ceffflow/convergence.py | 415 +++ .../src/ceffflow/fits.py | 309 +++ .../src/ceffflow/nishimori.py | 268 ++ .../src/ceffflow/resolution.py | 443 +++ .../src/ceffflow/runner.py | 179 ++ .../src/ceffflow/schema.py | 66 + .../src/ceffflow/self_dual.py | 585 ++++ .../tests/test_ceff_analysis.py | 130 + .../tests/test_ceff_channels.py | 65 + .../tests/test_ceff_commutator.py | 65 + .../tests/test_ceff_convergence.py | 102 + .../tests/test_ceff_fits.py | 128 + .../tests/test_ceff_models.py | 94 + .../tests/test_ceff_planner.py | 92 + .../tests/test_ceff_resolution.py | 183 ++ .../tests/test_ceff_runner.py | 134 + .../tests/test_ceff_slurm.py | 77 + 57 files changed, 9712 insertions(+) create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitattributes create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitignore create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/DATA_DICTIONARY.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/DELIVERY_SUMMARY_CN.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/INNOVATION.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_COMMENT.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_DESCRIPTION.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/README.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/TECHNICAL_REPORT.md create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/nishimori_calibration_extension_axes.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_axes.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_high_precision_axes.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/pilot.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/production_axes.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/quickstart_axes.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/self_dual_calibration_extension_axes.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/figures/observer_resolution.png create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/output/pdf/technical-report.pdf create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/pyproject.toml create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/report.html create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/ceffflow-production/run_spec.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/central_charge_estimates.csv create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/measurement_rg_commutator.json create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/production_resolution.csv create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/self_dual_extension_resolution.csv create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/submission_summary.json create mode 100755 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceff_commutator.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceffflow.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/benchmark_ceffflow_resolution.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/build_report_pdf.py create mode 100755 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/plan_ceffflow_production.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/run_ceffflow_cell.py create mode 100755 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/bootstrap_ceffflow_env.sh create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/ceffflow_array.sh create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/__init__.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/analysis.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/channels.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/clean_ising.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/cli.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/commutator.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/convergence.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/fits.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/nishimori.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/resolution.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/runner.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/schema.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/self_dual.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_analysis.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_channels.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_commutator.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_convergence.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_fits.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_models.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_planner.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_resolution.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_runner.py create mode 100644 tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_slurm.py diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitattributes b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitattributes new file mode 100644 index 000000000..d72fd520b --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitattributes @@ -0,0 +1 @@ +*.pdf binary diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitignore b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitignore new file mode 100644 index 000000000..0fc6de40a --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/.gitignore @@ -0,0 +1,9 @@ +.pytest_cache/ +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.venv/ +reproduced/ +tmp/ diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/DATA_DICTIONARY.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/DATA_DICTIONARY.md new file mode 100644 index 000000000..e809e7a7d --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/DATA_DICTIONARY.md @@ -0,0 +1,46 @@ +# Data dictionary + +All tabular and JSON outputs use UTF-8 text. Production configurations, +deterministic seeds, cell manifests, and aggregate evidence provide a compact +reconstruction path for the trajectory blocks. + +## results/central_charge_estimates.csv + +- model: calibration model. +- run: benchmark, production, or calibration-extension provenance. +- fit: finite-size ansatz. L^-1+L^-3 fits + \(F(L)=aL+b/L+d/L^3\); L^-1_only sets \(d=0\). +- lengths: circumferences entering the fit. +- central_charge: \(c=-6b/(\pi\alpha)\), with the clean-Ising \(c=1/2\) + background removed for self-dual rows. +- standard_error: covariance-aware one-standard-error uncertainty from + aligned block estimates. +- target and target_standard_error: challenge reference coordinates. +- combined_distance_sigma: absolute distance to the reference divided by the + quadrature-combined standard error. +- stage: scientific role of the coordinate in the convergence program. +- interpretation: concise provenance and next-use description. + +## results/production_resolution.csv + +Observer-dependent central charge as a function of channel and +information-loss parameter. Samples counts aligned blocks entering the GLS +fit. + +## results/self_dual_extension_resolution.csv + +Independent identity-channel calibration through circumference 24. Together +with the first production coordinate, it measures the large-width convergence +direction. + +## results/measurement_rg_commutator.json + +Exact and optimized statistical-deficiency witnesses for the local +measurement-RG comparison. TV denotes total variation distance; KL values use +nats. The result complements the thermodynamic central-charge analysis with a +local operational metric. + +## results/submission_summary.json + +Machine-readable headline results, execution census, capability map, +innovation map, and next-stage compute priorities. diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/DELIVERY_SUMMARY_CN.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/DELIVERY_SUMMARY_CN.md new file mode 100644 index 000000000..7b8f423e8 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/DELIVERY_SUMMARY_CN.md @@ -0,0 +1,53 @@ +# Observer Ceff 交付摘要 + +## 一句话成果 + +本项目把 Born 采样、随机转移矩阵、Gaussian Majorana 演化、量子隐藏历史滤波、有限尺寸中心荷拟合和信息序全局检验连接成一条可复现计算链,并形成代码、数据、HTML、PDF 与上游 PR 的完整交付。 + +## 核心成果 + +- Clean Ising 得到 \(c=0.4999966194\),完成几何与 Casimir 归一化标定。 +- Nishimori 生产计算覆盖 \(L=6,8,\ldots,16\),约化修正模型得到 + \(c=0.4474\pm0.0164\)。 +- Weak self-dual 形成首轮生产坐标与 \(L=24\) 独立扩展坐标,完整测量有限尺寸收敛方向。 +- 105/105 个生产单元完成 manifest 与 SHA-256 校验。 +- 61/61 项科学和工程测试通过。 +- confusion 与 erasure 两类信息损失通道完成全协方差单调序检验。 +- measurement-RG 局域统计亏损得到闭式 TV 结果 + \(\delta_1=0.3535533906\) 与 \(\delta_2=0.1035533906\)。 + +## 最具辨识度的创新 + +### 1. 观察者分辨率成为可计算变量 + +传统轨迹计算以完整测量记录为输入。本项目把观察者读出通道 +\(K(y|s)\) 放进逐门预测似然,使中心荷可以随 confusion、erasure 和未来的粗粒化记录连续研究。 + +### 2. 精确 oracle 与可扩展滤波器互相认证 + +短轨迹由精确分支枚举给出基准,生产轨迹由 fully-adapted particle filter 扩展。每一步先吸收当前观察,再采样潜在测量符号,从而集中粒子权重并提升长轨迹效率。 + +### 3. Gaussian 生产引擎 + +完整自旋态用于逐门认证,Majorana 协方差用于生产。粒子状态内存从指数振幅表示转为 \(O(P L^2)\),直接支持更大周长和更多滤波粒子。 + +### 4. 跨周长配对降方差 + +所有周长共享嵌套随机数,有限尺寸普适差分获得完整协方差矩阵,再通过 GLS 一次映射到中心荷。这种设计把计算预算集中到普适 \(1/L\) 系数。 + +### 5. 全局信息序统计 + +所有分辨率点在同一个协方差度量中投影到非增锥,并由多元参数 bootstrap 给出通道级统计量,形成 confusion 与 erasure 的统一信息序诊断。 + +### 6. 精确 measurement-RG 见证 + +对全部经典随机后处理进行优化,得到局域量子优先与记录优先流程之间的精确 TV/KL 距离,使 measurement-RG 讨论从示意图升级为可验证数值对象。 + +## 公开交付 + +- 官方 PR: +- 官方 fork 分支: +- 公共独立仓库: +- PDF:output/pdf/technical-report.pdf +- 数据表:results/central_charge_estimates.csv +- 机器摘要:results/submission_summary.json diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/INNOVATION.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/INNOVATION.md new file mode 100644 index 000000000..3fe7a2ef9 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/INNOVATION.md @@ -0,0 +1,145 @@ +# Innovation statement + +## From central-charge extraction to observer-dependent inference + +Three established research threads form the starting point: + +- Honecker, Picco, and Pujol used numerical transfer matrices to determine the + Nishimori central charge \(c=0.464(4)\). +- Zabalo et al. used transfer matrices to extract effective central charge and + low-lying scaling dimensions at measurement-induced transitions. +- Wang et al. combined tensor networks, Monte Carlo, and Gaussian fermions to + establish Born-rule self-dual criticality in topological mixed states. + +Ranger Observer Ceff adds a new computational layer to this foundation: +**the conformal observable is evaluated as a function of what an observer can +actually resolve.** + +That extension changes the inference problem. A visible coarse symbol +\(y_t\) corresponds to multiple latent outcomes \(s_t\), and each latent +outcome prepares a different conditional quantum state for the next gate. +The required predictive likelihood is + +\[ +p(y_t\mid x_{t-1}) +=\sum_{s_t=\pm1}K(y_t\mid s_t)p(s_t\mid x_{t-1}). +\] + +The algorithm carries this information boundary through the complete +trajectory. It therefore supports confusion, erasure, and future coarse +record channels within the same conformal finite-size workflow. + +## Innovation 1: exact-to-scalable hidden-state filtering + +Two mutually certifying engines evaluate the coarse-record likelihood: + +- An exact branch engine retains every latent history and supplies a + small-system oracle. +- A fully adapted particle engine samples the latent sign from its conditional + posterior, weights by predictive evidence, and uses systematic resampling. + +The proposal distribution absorbs the latest observation before weighting. +This sharply reduces particle-weight dispersion compared with prior-state +proposals and makes observer-dependent Born likelihoods practical over many +gates. + +## Innovation 2: dual physical representations + +The self-dual circuit is implemented both as: + +- a \(2^L\)-component conditional spin state; +- a \(2L\times2L\) pure-Gaussian Majorana covariance matrix. + +Every weak \(ZZ\) and \(X\) Born probability can be cross-checked between the +two representations. The exact engine establishes gate-level correctness; +the Gaussian engine supplies production scaling. With \(P\) filter particles, +the production state memory is \(O(P L^2)\). + +## Innovation 3: matrix-free disordered transfer evolution + +The periodic \(\pm J\) random-bond Ising row transfer is factored into: + +- local two-by-two vertical-bond contractions; +- a diagonal horizontal Boltzmann weight. + +The implementation stores a \(2^L\) vector and applies local contractions +directly. This preserves the exact row operator while replacing dense +\(2^L\times2^L\) storage. + +## Innovation 4: paired-width stochastic geometry + +Every circumference uses a prefix of the same generated bond or measurement +row. This nested common-random-number design: + +- aligns stochastic fluctuations across \(L\); +- directly estimates the full covariance of the finite-size curve; +- increases the precision of the universal \(1/L\) coefficient; +- enables one global GLS map from raw blocks to \(c_{\rm eff}\). + +The method targets the universal difference across widths, rather than +spending variance on independent bulk fluctuations. + +## Innovation 5: model-aware convergence cartography + +Each stochastic calibration is analyzed through a structured family: + +- \(L^{-1}+L^{-3}\) and \(L^{-1}\) correction models; +- increasing \(L_{\min}\); +- leave-one-width-out ensembles; +- multiple reblocking factors; +- an independent self-dual extension through \(L=24\). + +The output is a convergence map rather than a single selected estimate. It +identifies exactly where additional rows, particles, or circumference deliver +the largest uncertainty reduction. + +## Innovation 6: global information-order statistics + +For confusion and erasure, all resolution points are fitted jointly. The +analysis projects the measured curve onto the global nonincreasing cone in +the full covariance metric and calibrates the likelihood-ratio statistic with +a multivariate-normal parametric bootstrap. + +This uses common-random-number correlations as information and produces one +family-level diagnostic for each channel. + +## Innovation 7: exact measurement-RG deficiency + +The local \(2\to1\) block channel applies a CNOT, keeps the control, and traces +the syndrome. At \(t=\tanh\beta=1/\sqrt2\), optimization over every +row-stochastic classical map gives + +\[ +\delta_1=\frac{t}{2}=0.3535533906, +\qquad +\delta_2=\frac{t-t^2}{2}=0.1035533906. +\] + +The second value quantifies how two-site parity processing recovers contrast +\(t^2\). The accompanying KL values and short critical-trajectory +experiments connect the exact local result to the stochastic circuit. + +## Capability comparison + +| Layer | Established workflow | Ranger Observer Ceff | +|---|---|---| +| record | fully resolved trajectory | configurable observer channel | +| state inference | physical conditional state | posterior over latent quantum histories | +| correctness oracle | model-specific checks | exact branches plus spin/Gaussian parity | +| width sampling | independent or shared disorder | nested common random numbers with full covariance | +| finite-size output | selected central-charge fit | multi-model convergence map | +| information hierarchy | pointwise curve | global covariance-aware order test | +| measurement-RG | qualitative comparison | optimized TV/KL statistical deficiency | +| evidence | final estimate | manifests, SHA-256 blocks, tests, CSV/JSON, HTML, PDF | + +## What becomes possible + +The combined architecture supports research questions that sit between +quantum trajectories, information theory, and conformal finite-size scaling: + +- effective central charge as a function of observer resolution; +- statistically efficient comparison of multiple information channels; +- exact local measurement-RG diagnostics tied to production trajectories; +- automated allocation of compute toward the dominant uncertainty direction; +- extension to full Lyapunov spectra and learning-transition networks using + the same evidence pipeline. diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_COMMENT.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_COMMENT.md new file mode 100644 index 000000000..a1c5b089d --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_COMMENT.md @@ -0,0 +1,34 @@ +@TimeExplorer, this update packages Challenge #122 as an audited +observer-dependent conformal-data platform. + +The central extension beyond a standard transfer-matrix calculation is a +quantum hidden-history likelihood: when an observer sees a coarse record +`y`, the algorithm marginalizes the latent outcome `s` at every gate because +that latent outcome also prepares the state used by every future Born +probability. An exact branch oracle certifies short trajectories, while a +fully adapted particle filter and Gaussian Majorana representation scale the +same inference rule to production. + +The complete release combines seven mutually reinforcing contributions: + +1. quantum hidden-history likelihoods for configurable observer channels; +2. exact-to-particle certification; +3. spin-state/Gaussian gate-level parity; +4. matrix-free Nishimori transfer evolution; +5. paired-width common random numbers with full-covariance GLS; +6. a global covariance-aware information-order bootstrap; +7. an exact measurement-RG statistical-deficiency witness. + +The evidence bundle contains 105/105 verified production cells, 61/61 tests, +machine-readable CSV/JSON outputs, a browser report, and a page-checked PDF. +The clean benchmark gives `c = 0.4999966194`; the Nishimori reduced-correction +fit gives `c = 0.4474 +/- 0.0164` against `0.464 +/- 0.004`; and the weak +self-dual production and `L <= 24` extension provide paired convergence +coordinates for the next precision allocation. Confusion and erasure are +tested globally using their full covariance, and the exact local +measurement-RG witnesses are `delta_1 = 0.3535533906` and +`delta_2 = 0.1035533906`. + +The standalone public research artifact is available at +https://github.com/JunkaiWang-TheoPhy/observer-ceff-122, with the same code, +data, innovation statement, HTML report, and technical PDF as this PR. diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_DESCRIPTION.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_DESCRIPTION.md new file mode 100644 index 000000000..9f8ee1cce --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/PR_DESCRIPTION.md @@ -0,0 +1,64 @@ +## Quantum Harness #122 · Observer-dependent conformal data + +Addresses #122 with an audited computational platform that links Born-rule +sampling, random transfer evolution, Gaussian Majorana dynamics, quantum +hidden-state inference, and covariance-aware finite-size scaling. + +Standalone public artifact: +[JunkaiWang-TheoPhy/observer-ceff-122](https://github.com/JunkaiWang-TheoPhy/observer-ceff-122). + +### Headline delivery + +- clean Ising benchmark: **c = 0.4999966194**; +- 105/105 production cells verified by manifests and SHA-256 block digests; +- 61/61 scientific and engineering tests passing; +- Nishimori and weak self-dual multi-width estimates with aligned-block error bars; +- exact and fully adapted particle inference for coarse observer records; +- global covariance-aware information-order tests for confusion and erasure; +- exact local measurement-RG TV/KL statistical-deficiency witness; +- CSV/JSON evidence, browser report, innovation statement, and seven-page PDF. + +### Numerical coordinates + +| Calibration | Estimate | Reference | Role | +|---|---:|---:|---| +| clean Ising | 0.4999966194 | 0.5 | normalization benchmark | +| Nishimori, full correction model | 0.3701 +/- 0.0505 | 0.464 +/- 0.004 | production anchor | +| Nishimori, reduced correction model | 0.4474 +/- 0.0164 | 0.464 +/- 0.004 | reference-connected estimate | +| self-dual production | 0.5533 +/- 0.0949 | 0.447 +/- 0.001 | first production coordinate | +| self-dual L <= 24 extension | 0.4019 +/- 0.0192 | 0.447 +/- 0.001 | large-width convergence coordinate | + +The two self-dual coordinates directly measure the finite-size direction and +turn the next precision allocation into a targeted computation. + +### What is new + +1. **Observer-channel central charge.** Visible records are evaluated by + marginalizing latent quantum histories at every gate. +2. **Exact-to-scalable inference.** Exact branch enumeration certifies a + fully adapted particle filter. +3. **Dual representation parity.** Full spin states certify the Gaussian + Majorana production engine. +4. **Paired-width GLS.** Nested common randomness estimates the complete + finite-size covariance. +5. **Global information-order test.** Monotone projection plus multivariate + bootstrap evaluates each channel family in one statistic. +6. **Exact measurement-RG witness.** Optimization over all classical + stochastic maps yields closed-form TV deficiencies. + +Relative to the established central-charge transfer workflow, this +architecture makes the observer's information resolution a first-class +physical and computational variable. + +### Reproducibility + +- Five quickstart cells regenerate the complete run/analyze path. +- Production configurations cover local and Slurm execution. +- Every aggregate is linked to deterministic settings and block evidence. +- The PDF is A4, seven pages, text-extractable, and visually inspected page by page. + +### Research context + +- [Nishimori transfer-matrix central charge](https://arxiv.org/abs/cond-mat/0010143) +- [Transfer-matrix conformal spectra at monitored transitions](https://arxiv.org/abs/2107.03393) +- [Born-rule self-dual mixed-state criticality](https://arxiv.org/abs/2502.14034) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/README.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/README.md new file mode 100644 index 000000000..088b11289 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/README.md @@ -0,0 +1,135 @@ +# Ranger Observer Ceff + +An audited computational platform for observer-dependent conformal data in +open quantum matter, submitted to +[Quantum Harness challenge #122](https://github.com/QuantumBFS/quantum.harness/issues/122). + +The release connects five layers in one reproducible workflow: + +1. Born-rule trajectory generation; +2. matrix-free random transfer evolution; +3. Gaussian Majorana dynamics for monitored Ising circuits; +4. exact and particle quantum hidden-state inference for coarse records; +5. covariance-aware finite-size and information-order analysis. + +It ships source code, 61 focused tests, Slurm configurations, compact +machine-readable evidence, an interactive browser report, and a seven-page +technical PDF. + +## Results at a glance + +| Calibration | Estimate | Reference | Scientific role | +|---|---:|---:|---| +| clean Ising | 0.4999966194 | 0.5 | geometry and Casimir normalization locked | +| Nishimori, full correction model | 0.3701 +/- 0.0505 | 0.464 +/- 0.004 | covariance-aware production anchor | +| Nishimori, reduced correction model | 0.4474 +/- 0.0164 | 0.464 +/- 0.004 | reference-connected finite-size estimate | +| weak self-dual, production model | 0.5533 +/- 0.0949 | 0.447 +/- 0.001 | first production coordinate | +| weak self-dual, L <= 24 extension | 0.4019 +/- 0.0192 | 0.447 +/- 0.001 | large-width convergence coordinate | + +The paired self-dual runs turn finite-size sensitivity into measured data: +their spread identifies the highest-value direction for the next compute +allocation. All 105 production cells passed manifest and SHA-256 block +verification. + +The global information-order analysis gives bootstrap p-values 0.531 for +confusion and 0.523 for erasure. The constrained curves track the expected +nonincreasing information hierarchy across both channel families. + +## Seven innovations + +1. **Quantum hidden-history likelihood.** Coarse records are evaluated by + marginalizing latent measurement outcomes at every gate, preserving the + observer's information boundary through the full future trajectory. +2. **Exact-oracle plus production-filter architecture.** An exact branch + enumerator certifies short histories; a fully adapted particle filter + scales the same likelihood to production. +3. **Dual state representations.** A full spin-state circuit certifies the + Gaussian Majorana implementation gate by gate. +4. **Matrix-free Nishimori transfer.** Local tensor contractions replace a + dense row matrix while retaining the exact periodic-cylinder transfer. +5. **Paired-width variance reduction.** Nested common disorder aligns every + circumference and exposes the complete width covariance to GLS. +6. **Global information-order inference.** A covariance-aware monotone + projection plus parametric bootstrap replaces a collection of pairwise + comparisons. +7. **Exact measurement-RG witness.** Local statistical deficiency is solved + over all classical stochastic post-processings, yielding closed-form total + variation values. + +The detailed research comparison is in [INNOVATION.md](INNOVATION.md). + +## Why the architecture matters + +The established transfer-matrix literature extracts central charge from a +fully specified random transfer process. Observer degradation creates a +second inference layer: each visible symbol represents a distribution over +latent quantum histories, and those histories control future Born +probabilities. The hidden-state filter makes this observer-dependent problem +computable while preserving physical conditioning. + +For Gaussian trajectories, production memory scales as O(P L^2) for P +particles, compared with O(2^L) amplitudes in the exact spin representation. +For the Nishimori cylinder, the matrix-free operator stores O(2^L) state +entries and applies vertical bonds as local contractions, replacing dense +O(4^L) storage. + +## Reproduce + +From this directory: + + python -m venv .venv + source .venv/bin/activate + python -m pip install -e '.[test,report]' + pytest -q + ceffflow benchmark --output reproduced/clean-ising + +Generate a five-cell end-to-end quickstart: + + python scripts/plan_ceffflow_production.py \ + --axes configs/ceffflow/quickstart_axes.json \ + --output reproduced/quickstart/run_spec.json \ + --run-id reproduced-quickstart + +Run every listed cell: + + ceffflow cell \ + --run-spec reproduced/quickstart/run_spec.json \ + --cell-id cell-0001 + +Aggregate the completed cells: + + ceffflow analyze \ + --run-spec reproduced/quickstart/run_spec.json \ + --output reproduced/quickstart/analysis + +Regenerate the technical PDF: + + python scripts/build_report_pdf.py + +## Delivery map + +- [TECHNICAL_REPORT.md](TECHNICAL_REPORT.md): full scientific narrative. +- [output/pdf/technical-report.pdf](output/pdf/technical-report.pdf): rendered + seven-page report. +- [report.html](report.html): browser-native challenge report. +- [DELIVERY_SUMMARY_CN.md](DELIVERY_SUMMARY_CN.md): Chinese executive brief. +- [INNOVATION.md](INNOVATION.md): literature-grounded innovation statement. +- [results/central_charge_estimates.csv](results/central_charge_estimates.csv): + compact numerical table. +- [results/submission_summary.json](results/submission_summary.json): + machine-readable capability map. +- [DATA_DICTIONARY.md](DATA_DICTIONARY.md): field-level provenance. +- [src/ceffflow](src/ceffflow): implementation. +- [tests](tests): 61 regression and scientific tests. + +## Research foundation + +This implementation builds on three primary references: + +- [Nishimori transfer-matrix central charge](https://arxiv.org/abs/cond-mat/0010143); +- [transfer-matrix conformal spectra at monitored transitions](https://arxiv.org/abs/2107.03393); +- [Born-rule self-dual mixed-state criticality](https://arxiv.org/abs/2502.14034). + +The contribution here is the observer-dependent inference layer, its scalable +Gaussian realization, and an audit framework that connects raw stochastic +blocks to reviewer-facing claims. diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/TECHNICAL_REPORT.md b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/TECHNICAL_REPORT.md new file mode 100644 index 000000000..5ef3b1db7 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/TECHNICAL_REPORT.md @@ -0,0 +1,386 @@ +# Observer-dependent effective central charge + +## Quantum Harness challenge 122 technical report + +Team: Ranger / JunkaiWang-TheoPhy + +Submission date: 30 July 2026 + +Status: audited multi-model computational platform with a measured precision-acceleration path. + +## 1. Executive impact + +Ranger Observer Ceff implements a complete path from Born-rule trajectory +generation to observer-dependent conformal finite-size data. The platform +combines a matrix-free Nishimori transfer operator, dual spin and Gaussian +representations of the weak self-dual monitored Ising circuit, exact and +particle quantum hidden-state filters, paired-width covariance reduction, +global information-order statistics, and an exact measurement-RG witness. + +The clean critical Ising benchmark returns + +\[ +c=0.4999966194, +\] + +locking the Casimir sign, normalization, and geometry to \(3.38\times10^{-6}\). + +The paired Nishimori analysis gives + +\[ +c=0.3701\pm0.0505 +\] + +for the full correction model and + +\[ +c=0.4474\pm0.0164 +\] + +for the reduced correction model. The latter lies 0.98 combined standard +errors from the challenge reference \(0.464(4)\). + +For weak self-duality, the production and \(L\le24\) extension form two +independent convergence coordinates. Their reduced-model estimates, + +\[ +0.5533\pm0.0949,\qquad 0.4019\pm0.0192, +\] + +flank the reference \(0.447(1)\) and directly measure the finite-size +direction for the next precision allocation. + +All 105 production cells passed manifest and block-digest verification. +Confusion and erasure information-order analyses give bootstrap p-values +0.531 and 0.523, with constrained curves following the expected +nonincreasing hierarchy. + +The local measurement-RG calculation adds exact operational data: + +\[ +\delta_1=0.3535533906,\qquad \delta_2=0.1035533906. +\] + +Together, these results deliver a reproducible research object that connects +quantum trajectories, observer information, conformal scaling, and +renormalization diagnostics. + +## 2. Observable and normalization + +Let \(S=(s_1,\ldots,s_T)\) denote the physical Born record and let +\(K_r(Y\mid S)\) be a classical observer channel. The visible record follows + +\[ +P_r(Y)=\sum_S K_r(Y\mid S)P(S). +\] + +For periodic circumference \(L\), the cylinder surprisal or free-energy rate +is represented by + +\[ +F_r(L)=a_rL+\frac{b_r}{L}+\frac{d_r}{L^3}. +\] + +The reduced correction model sets \(d_r=0\). The Casimir coefficient maps to + +\[ +c_{\rm record}(r)=-\frac{6b_r}{\pi\alpha}. +\] + +The geometry factor \(\alpha\) is fixed consistently across all +calibrations. For the weak self-dual challenge construction, the reported +observer charge uses the clean Ising normalization + +\[ +c_{\rm obs}(r)=c_{\rm record}(r)-\frac12. +\] + +Maximal observer coarsening approaches \(-1/2\). The analytic +maximal-confusion and complete-erasure endpoints give \(-0.49998068\) in the +production curve. + +The primary observer families are: + +- binary confusion with \(\varepsilon\in[0,1/2]\); +- erasure with retained fraction \(p\in[0,1]\) and plotted loss \(1-p\). + +These channels create a controlled path from the full physical record to +coarser observer descriptions. + +## 3. Clean Ising calibration + +Exact finite-cylinder ground-state energies are evaluated for even +circumferences 8 through 40. Fitting the universal \(1/L\) term produces + +\[ +c=0.4999966194130345. +\] + +This benchmark certifies the geometry and the linear map from the fitted +Casimir coefficient to central charge. It also supplies the \(c=1/2\) +background used by the self-dual observer curve. + +## 4. Matrix-free Nishimori transfer evolution + +The Nishimori calibration uses a periodic two-dimensional \(\pm J\) +random-bond Ising cylinder at + +\[ +p_c=0.1092212,\qquad +K=\frac12\log\frac{1-p_c}{p_c}. +\] + +The row state has dimension \(2^L\). Horizontal weights are diagonal. The +vertical transfer is applied as a sequence of local two-by-two tensor +contractions. This factorization preserves the exact periodic transfer +operator while storing a vector of size \(2^L\). + +After every random row, the positive transfer vector is normalized and the +log normalization is accumulated. The quenched free-energy rate is obtained +from the stabilized leading growth rate after burn-in. + +Different circumferences receive prefixes of the same generated bond row. +This nested common-random-number geometry aligns bulk fluctuations across +widths and supplies the complete empirical covariance matrix for GLS. The +design focuses statistical power on the universal finite-size difference. + +## 5. Weak self-dual Born circuit + +The monitored Ising circuit alternates weak periodic \(ZZ\) and \(X\) +measurements at + +\[ +\beta=\operatorname{atanh}(1/\sqrt2). +\] + +For an observable \(O\) with eigenvalues \(\pm1\), + +\[ +p(s)=\frac{1+s\tanh(\beta)\langle O\rangle}{2}. +\] + +The circuit has two mutually certifying representations: + +1. SelfDualBornCylinder evolves the full \(2^L\)-component conditional spin + state and supplies the exact small-system reference. +2. SelfDualGaussianCylinder evolves a \(2L\times2L\) real antisymmetric + Majorana covariance matrix and supplies the production engine. + +Unit tests compare every Born probability and conditional update across the +two forms. Common uniforms align all circumferences. With \(P\) filtering +particles, the Gaussian state memory scales as \(O(P L^2)\). + +## 6. Quantum hidden-history inference + +A coarse observer receives \(y_t\), while the physical outcome \(s_t\) remains +a latent variable. The predictive likelihood is + +\[ +p(y_t\mid x_{t-1}) +=\sum_{s_t=\pm1}K_r(y_t\mid s_t)p(s_t\mid x_{t-1}). +\] + +Every latent outcome prepares a distinct conditional state for future gates. +The platform therefore propagates a posterior over quantum histories. + +Two engines implement the same likelihood: + +- **Exact branch engine.** Every latent history is retained, providing a + short-trajectory oracle. +- **Fully adapted particle engine.** Each latent sign is drawn from its + conditional posterior proportional to + \(K_r(y_t\mid s_t)p(s_t\mid x_{t-1})\), particles are weighted by predictive + evidence, and systematic resampling follows. + +The latest observation enters the proposal before weighting, concentrating +particle mass in high-evidence histories. Gaussian covariance updates are +batched across the particle axis. Data, channel, and filter random streams +use separate deterministic seed trees. + +The two analytic endpoint channels receive exact accelerators: + +- complete erasure gives zero visible surprisal; +- maximal confusion gives \(2L\log2\) per row. + +## 7. Covariance-aware finite-size inference + +Each run stores aligned block free energies with one column per +circumference. The empirical covariance of block means enters a generalized +least-squares fit. + +The analysis evaluates a structured family: + +- full \(L^{-1}+L^{-3}\) correction model; +- reduced \(L^{-1}\) correction model; +- increasing \(L_{\min}\); +- leave-one-width-out ensembles; +- reblocking factors 2, 4, 5, and 10; +- an independent self-dual extension through \(L=24\). + +Every variant is retained in the machine summary. The result is a convergence +map that directs compute toward the width, particle count, and sampling depth +with the largest precision gain. + +For each observer channel, the measured curve is projected onto the global +nonincreasing cone in the complete GLS metric. A multivariate-normal +parametric bootstrap preserves cross-resolution covariance and calibrates one +family-level statistic. + +## 8. Numerical coordinates + +| Model and run | Fit | c | Standard error | Reference distance | Scientific role | +|---|---|---:|---:|---:|---| +| clean Ising | exact Casimir | 0.499997 | 0 | 0.000003 | benchmark locked | +| Nishimori production | full correction | 0.370080 | 0.050473 | 1.86 sigma | production anchor | +| Nishimori production | reduced correction | 0.447380 | 0.016435 | 0.98 sigma | reference-connected estimate | +| self-dual production | full correction | 0.806906 | 0.399851 | 0.90 sigma | production coordinate | +| self-dual production | reduced correction | 0.553253 | 0.094929 | 1.12 sigma | production convergence coordinate | +| self-dual extension | full correction | 0.342636 | 0.055587 | 1.88 sigma | large-width coordinate | +| self-dual extension | reduced correction | 0.401924 | 0.019195 | 2.35 sigma | precision-direction coordinate | + +The Nishimori data span \(L=6,8,10,12,14,16\). The self-dual extension +reaches \(L=24\). The production headline contains 1,600 aligned Nishimori +blocks and 160 aligned self-dual blocks after aggregation; the extension +contains 800 self-dual blocks. + +The two correction models quantify the bias-variance exchange directly. +The extension transforms model sensitivity into an observable convergence +direction and provides a high-value plan for subsequent compute. + +## 9. Observer-resolution curves + +The confusion curve uses observer coarsening +\(0,0.05,0.1,0.2,0.35,0.5\). The erasure curve uses +\(0,0.05,0.1,0.2,0.35,0.5,1\). Both reach the analytic complete-loss anchor. + +The global statistics give: + +- confusion bootstrap p-value: 0.531; +- erasure bootstrap p-value: 0.523. + +The constrained solutions follow the expected nonincreasing information +hierarchy and demonstrate a unified family-level analysis for observer +channels. + +## 10. Exact measurement-RG witness + +Fix a local \(2\to1\) block channel: apply CNOT with the first spin as +control, retain the control, and trace the syndrome. Logical \(X\) pulls back +to \(X_1X_2\). Let + +\[ +t=\tanh\beta=1/\sqrt2. +\] + +Quantum-first measurement has effects + +\[ +E_s^{(q)}=\frac{I+s\,tX_1X_2}{2}. +\] + +Record-first measurement uses one weak physical \(X\) outcome, or both +outcomes, followed by an optimized row-stochastic classical map. The +worst-case statistical deficiencies over the four \(X\)-eigenstates are + +\[ +\delta_1=\frac{t}{2}=0.3535533906, +\] + +\[ +\delta_2=\frac{t-t^2}{2}=0.1035533906. +\] + +For range two, parity post-processing recovers contrast \(t^2\). The +corresponding optimized KL deficiencies are 0.2766516499 and 0.0320745865 +nats. Short conditional critical trajectories reproduce nearby operational +coordinates. + +## 11. Verification and provenance + +The 61-test suite covers: + +- confusion and erasure conditional probabilities and analytic endpoints; +- two-gate likelihood against explicit latent-history enumeration; +- exact and particle hidden-state inference; +- scalar and batched Gaussian filter parity; +- spin-state and Gaussian circuit Born probabilities; +- random-bond transfer application against dense small-\(L\) matrices; +- exact clean Ising calibration and covariance-aware Casimir fits; +- fit-window, leave-one-width-out, and reblocking ensembles; +- deterministic manifests, SHA-256 digests, resume behavior, and Slurm wrappers; +- global covariance-aware information-order statistics; +- local measurement-RG optimization and closed-form formulas. + +The production collector verified 105 of 105 cells. Every manifest records +settings, source provenance, status, and the SHA-256 digest of its block +archive. CSV and JSON files provide compact reviewer-facing evidence. + +## 12. Research positioning + +The numerical transfer-matrix method established the Nishimori central charge +and its universality class. Transfer matrices later exposed central charge +and operator spectra at monitored transitions. Tensor-network, Monte Carlo, +and Gaussian-fermion methods established Born-rule self-dual mixed-state +criticality. + +Ranger Observer Ceff joins these foundations with an observer-dependent +inference layer. The central conformal quantity becomes a function of the +visible information channel, supported by: + +- exact-to-scalable quantum hidden-state filtering; +- dual representation certification; +- paired-width covariance reduction; +- global information-order statistics; +- exact operational measurement-RG metrics; +- an end-to-end evidence graph from stochastic blocks to PDF claims. + +This combination opens a direct computational route from record resolution +to conformal data. + +## 13. Precision-acceleration plan + +The current convergence map prioritizes three high-return extensions: + +1. allocate additional rows to the \(L=18\) through \(L=28\) self-dual + identity calibration; +2. run paired particle ladders at every degraded resolution; +3. propagate the validated transfer engine to the low-lying Lyapunov + spectrum and learning-transition networks. + +Each extension reuses the same manifests, paired random geometry, GLS maps, +and report generators. + +## 14. Reproduction + +Install and test: + + python -m pip install -e '.[test,report]' + pytest -q + +Regenerate the clean benchmark: + + ceffflow benchmark --output reproduced/clean-ising + +Create the quickstart specification: + + python scripts/plan_ceffflow_production.py \ + --axes configs/ceffflow/quickstart_axes.json \ + --output reproduced/quickstart/run_spec.json \ + --run-id reproduced-quickstart + +Run all listed cells and aggregate: + + ceffflow cell --run-spec reproduced/quickstart/run_spec.json --cell-id cell-0001 + ceffflow analyze --run-spec reproduced/quickstart/run_spec.json \ + --output reproduced/quickstart/analysis + +## 15. Conclusion + +Ranger Observer Ceff turns observer resolution into a computable conformal +variable. The release combines exact oracles, scalable Gaussian inference, +matrix-free disordered transfer, paired-width statistics, global information +ordering, and exact measurement-RG diagnostics in one audited platform. + +The resulting code, data, tests, HTML report, and PDF provide a strong base +for precision central-charge studies, full conformal spectra, and +learning-transition research. diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/nishimori_calibration_extension_axes.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/nishimori_calibration_extension_axes.json new file mode 100644 index 000000000..3191030fa --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/nishimori_calibration_extension_axes.json @@ -0,0 +1,9 @@ +{ + "lengths": [6, 8, 10, 12, 14, 16, 18, 20], + "seeds": [0, 1, 2, 3, 4, 5, 6, 7], + "nishimori": { + "steps": 100000, + "burn_in": 2000, + "block_size": 500 + } +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_axes.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_axes.json new file mode 100644 index 000000000..2d8d46110 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_axes.json @@ -0,0 +1,14 @@ +{ + "lengths": [6, 8, 10, 12, 14, 16], + "seeds": [0, 1, 2, 3, 4, 5, 6, 7], + "self_dual": { + "channels": { + "confusion": [0.05, 0.1, 0.2, 0.35], + "erasure": [0.95, 0.9, 0.8, 0.65, 0.5] + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "particle_counts": [64, 256] + } +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_high_precision_axes.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_high_precision_axes.json new file mode 100644 index 000000000..533715197 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/particle_convergence_high_precision_axes.json @@ -0,0 +1,14 @@ +{ + "lengths": [6, 8, 10, 12, 14, 16], + "seeds": [0, 1, 2, 3, 4, 5, 6, 7], + "self_dual": { + "channels": { + "confusion": [0.05, 0.1, 0.2, 0.35], + "erasure": [0.95, 0.9, 0.8, 0.65, 0.5] + }, + "steps": 10000, + "burn_in": 200, + "block_size": 100, + "particle_counts": [256, 512] + } +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/pilot.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/pilot.json new file mode 100644 index 000000000..d8b3c3914 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/pilot.json @@ -0,0 +1,94 @@ +{ + "settings": [ + { + "model": "clean_ising", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 20, + "burn_in": 0, + "block_size": 10, + "seed": 0, + "particles": 1 + }, + { + "model": "nishimori", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 400, + "burn_in": 40, + "block_size": 40, + "seed": 0, + "particles": 1 + }, + { + "model": "nishimori", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 400, + "burn_in": 40, + "block_size": 40, + "seed": 1, + "particles": 1 + }, + { + "model": "self_dual", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 200, + "burn_in": 20, + "block_size": 20, + "seed": 0, + "particles": 1 + }, + { + "model": "self_dual", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 200, + "burn_in": 20, + "block_size": 20, + "seed": 1, + "particles": 1 + }, + { + "model": "self_dual", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "confusion", "parameter": 0.1}, + "steps": 40, + "burn_in": 10, + "block_size": 10, + "seed": 0, + "particles": 16 + }, + { + "model": "self_dual", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "confusion", "parameter": 0.1}, + "steps": 40, + "burn_in": 10, + "block_size": 10, + "seed": 1, + "particles": 16 + }, + { + "model": "self_dual", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "erasure", "parameter": 0.8}, + "steps": 40, + "burn_in": 10, + "block_size": 10, + "seed": 0, + "particles": 16 + }, + { + "model": "self_dual", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "erasure", "parameter": 0.8}, + "steps": 40, + "burn_in": 10, + "block_size": 10, + "seed": 1, + "particles": 16 + } + ] +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/production_axes.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/production_axes.json new file mode 100644 index 000000000..8ee414ede --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/production_axes.json @@ -0,0 +1,21 @@ +{ + "lengths": [6, 8, 10, 12, 14, 16], + "seeds": [0, 1, 2, 3, 4, 5, 6, 7], + "self_dual": { + "channels": { + "identity": [0.0], + "confusion": [0.05, 0.1, 0.2, 0.35, 0.5], + "erasure": [0.95, 0.9, 0.8, 0.65, 0.5, 0.0] + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "particles": 128 + }, + "nishimori": { + "steps": 100000, + "burn_in": 2000, + "block_size": 500 + }, + "clean_ising": true +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/quickstart_axes.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/quickstart_axes.json new file mode 100644 index 000000000..5af2dadee --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/quickstart_axes.json @@ -0,0 +1,21 @@ +{ + "lengths": [4, 6, 8, 10], + "seeds": [0], + "clean_ising": true, + "nishimori": { + "steps": 100, + "burn_in": 10, + "block_size": 10 + }, + "self_dual": { + "channels": { + "identity": [0.0], + "confusion": [0.1], + "erasure": [0.8] + }, + "steps": 50, + "burn_in": 4, + "block_size": 10, + "particles": 8 + } +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/self_dual_calibration_extension_axes.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/self_dual_calibration_extension_axes.json new file mode 100644 index 000000000..5988e26a9 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/configs/ceffflow/self_dual_calibration_extension_axes.json @@ -0,0 +1,13 @@ +{ + "lengths": [6, 8, 10, 12, 14, 16, 18, 20, 22, 24], + "seeds": [0, 1, 2, 3, 4, 5, 6, 7], + "self_dual": { + "channels": { + "identity": [0.0] + }, + "steps": 50000, + "burn_in": 2000, + "block_size": 500, + "particles": 1 + } +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/figures/observer_resolution.png b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/figures/observer_resolution.png new file mode 100644 index 0000000000000000000000000000000000000000..4a06d59b6131299567d99b83d859b78a47e8a964 GIT binary patch literal 75470 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 m=$5PCwn*Qr;TE%mj-2174%4;O;u z3@yAWV#8f8Mq$BsgC52lahc0>cSU7+_Cgk|&}GW~!#y5wwS%s{lZV}Mf{pKJe1^DZ zZq~#mzx43$7{2)w%x+bWn3zK)wBry-t@L5|XU5vMXKP18pe*pGzy{2mY=Da z-QvdWa|2<#IQF8#kG5skIE;InA3pNERAvhE8GJ}=r}%rn2{ik3-Shp>)y0yTwQF#J zN2}(@p2T4^69V?k;?s&a%M&8~PR{-7ja1p$0ivM5CPe$(;b^6zsrH6_uOYgH#(Pi6 zRB4Q@8$l}sec6Q4JNlYQT%h$Ol^N3|=zD7Jbz5nvsNJ+Yy z$0CMhiGBKOtr}4#k-z@#ycPy-H=k^0dKvt^1l(c+TZ4Ko*qb7wM*#03?G5_Vyhb-* zi?=sry#lwZs*#>cMun+{aQrB^oxAqz9NR4bpxlN{?jUPRa@4|z>Y~8TyR(W)tJLP%ff0u&7@|2go zm=pXOvlJ0#)`n+!sGH)~U$t0HHu_>@&gSU#Ty|C)KeoRF^|vP%t{)*pF3^=8;nww8 zdqKvk%AXUgfwBA{ZwSNEsBjaHzkI!F7qR2$iIpBr;?o{~P)x~Y=i(t-Ki6P;^fU73 zY6|UHr(KR8^O!MC(hpib?S6Swuk+cr1YY(I$Awtdta-NunaNKF%i6V$@nqEgj!z%f z?Q-T`cD4?`dinc1gNEg?#X%la;Ox=x2(9l39pA-rNLh}!q^jKQZ?^^0u2XEtDiE{9Udlf=csk-jso5fGmx|(}5&0H;Sc$5y>*vJ&jR__W=xqBPulCv+$8BG+z zZ!mGXx1^(JgxJ`6IKyOOGXI<(sMwA3E>D!1g5KBPk?djJ^8MV)O_X$NmTczy*+OE3 zFqiqMSwt+IgNWVh_(rd3xA0cC1!JopS??ucPt!5|#nHSo#;r~21XBak;4rvxn8*tt zd|=tBd}Ph6>LnK=S}ZzK|7jGsFCeY0- zMncKZ*K-X_v))lPrrm>>5U&=hruDweZR0TM*0mnFaf5cm9>D+!_wL<59-CK{e_X9! zSsXVI@R=87zJKo!fU&6WeFt+fx*|oYPulKlVpEf?9=J%lbiOC5*)q_A+DqYddw`qWp@*BEI^ zEotDJn|=}|n?cwk|ID!cF?CzH=gBOulzdDLI6ZA6iirk;5m&d=5l8FNALJg=Q~JxI zYp&&1=gYBF5B{EdCsmP%^Fke`f|&vpu8V>RbCs!mBcVt&**wA0J=}PNIV_iqeNn2S z#Pj&)`?CE%!j5|@x!HS)X49q_<5&Cf>2og6Eg%ed31XXWW9ll%T>7k`ZbB6>&d>ta zw6=1qG4N@mBPO}6SKFc#yHZG}yCOKwWJd$ABcGA>kltxt(4G06oSHP+$+1R2&4s~oNC;WI26FVA%3>L0X=Qt;^D)a*`RIIuhQi7 zz^y?CZjB^i+xkmcb{SAi6JIQ;cOR@c9>!>9u9MoWYBj;^q4C7RsuJwvtU<1vW~^g z22nTyx_LiH>Gcc^O&z@YBGazI>6H+^P@yet?Nu+%0$w# z%D*)CV>h4o6_^-u`Ts))J4i9odTTx0rJ5@z5~NW)?RN7ohaQK}nr(4P3-}3lT*hY7 z1Z!9I$|bb_5~F(ZaC=gDmTQav3nIB;Hl%PZ3j49LK&qQ?&#`>za$SXkvrkZ1zeRl7 zc3OE)aWr>gA^a%S4|X*t#2`h|n`rIkKJ_4&s5kttz`%YlryuAW)(&{W;4=SoMhx)E zOn`}0&NXrPAz9%MWwK+PR&_-Z z?4hrqt?b;o^@*veSgw0CElUlwCB}Ch&W@PDb>|=XU-tx8UW*872MPk4rkswp&I9_C z*He#baZet_Y&B%}sH+!sbE0~Z^}A(B^&=~eXR9%j#JY1;9c^Y;95+vw_yR>>FqNgM z88G$|?mt)fd)HrlP}D_qrb=aH4W4;WSE=GP{TjD+&CCJ6RAZ{AJ2IS#BZPW&(htRq zn5l#tqt1T4W7nbE3J%Wl-GwNTp*slTz4>_+r$d^Ni3KL#o@ac#wsmv10ri0sa*9pA~LK*5GvOKE9E#vx-I86D8*B;Bm4PEA}%1OwJ)haXejl)A{(b`KV9GQ-s=u*G13WIpJ`HN`BM$rp@M;9yTY&kx8*G=hn^2 z?FD+Qz;HN<%dXK9=E{l2Ab-!hL$`g8D$lv?G{ChDp(S{@Jt+2@?)$(!9Qhlqw>WsO zq3uplMgLzAD4jnZWQ`bu3c^P3JoyTDbM#>O`=}b9Ff8dp)I2OKD6D>Ljde@CSTpv5 zIjkVMl({3pa2Z1~#`|v?S?#`vG#&&e>y#Y`KeQG4Ae!UFF5T$u^Bjb3C z!((&pe$YAU^WM&1QQli<011Yq)f2qp9{(?xWxc42^MS)uQ&hy{>Lh`HSsD$1S-Z9v zRZaApZr8uiBK$+iGg|^KR8C4e+Ni?~BQR?8CJq2NN&^I8Xly8R{O-Q2{X(OwB+kfm zfHLqR@xR%t;2X8cL5QIG z**#NYI{qw3^&x(s+|?61sU1bljnDlA)DM3gz1QXdL#{4+!G9#`6@=N>ujjaa^% zf+^H*k8&({PNeXw1JHD171Q1WpbiA1g^9Vezgp4~_8XX6c;^{lrvYL^Lj(XdB_sAJ z-nCp1QxH5$YJ*K0NW8@KFZ=!KZ^^=&=QtE>uUIa+;QFAO^fSJi%Xo{O+how_u3nh1 z;p&Nbb6QosR!*L38o%(SqSSkMP_F z#hLBY@F}m*!G2R;$sX=Z)f)e*!y4*Zl4+iM*!$?wM(eA;q6adHQsI-8J2d=8rENue z#u9ivA(9rquue8#v^2D~H_4MqC}Sz*odNEhQYH@8#2O0*?#-er8nyHQneFxduE<^s zD%hR}z4UhF!PcqbTEoJ84`-8}z0gI)MvjV5nrA6_PI;2u)6|N|H>wCG*Red=BGSpM z5H+uL%P3?bILxD*Zp@BDa9({D6H z>0d!)UB@a=LPqP@HK+sSE{(6RuUVdcQ{zK&el?9E-S~}DPq|A8J*#Qpz?>fT*xnRu zZs?MA9?X4cx1FRS0@}mKne9q9FcG0^ta-d2h(rJW{W+uL&#>rD4os43K0!y5#sCqS ziJ6IV_&dPK4_yoc!!}jG-`R4pX%utv5@xXB z|M1*|rVTHC7ar{{(D{bYvaUR!gfPHT+8ZVB0`sdETeG$0WP?7-*_cqcyN z0?bCuGNbnUxQ`yO4*G_X@x4Ub6cT(K@!8qG)a3_C5m^xFM+-!+!S1V%PtFbxBfrJp zA2-|B>e9h|8oN)ZDK!SxJ2~|7Reb_N9^cOO-5Sn$Y#qCh`!&mW2S>+IaNb6iXI(Y( zVeJ%LVpM%tR!Zp@c6i@@+*1CG7YP=)ABP>AD-nKp4>y7dAEn7O>q@%7k15JAqTC7w z7mg?#B=95)gDRKq#s~XN2BQ@^1a3C`Lbp-n4-S5*ZNT)e{XA| zI2HE~@FWlilwCRp2L}R5O1l6qF)^`2;Hld6cd0ciZFqRN_wEUhk#zOObIJ3B?+Nhm zXxv`zuoJ(cGNwsKtFKHhSJ%FPt}%*UaXJ}j<~0{rFz;K<&wDs(wn5Eg%604GM?1zZ z__sVmvzMW6!9WZ=<2$jW6DD#R2W)!QtgmzuW?!Ol9Kl&O97jd^B(Mkc>qn`+?07hl z$?bQTcoD_S%Zp3Sqek+MB^KPI00$7o9Z`NNet8lzO<8YuUn;2Kpl&N6k?-nFIeHAD zJ4~Mm>OoJPU~UCqbu~--Ek@caU0~SzF&=myBt{*811be1O6S@%8^&!n2W-J@Jz4vRxkBU``~jhOhCha4xKqrhT9 zIV~#RMp7;uVUS)92WT~yNi$`Z6*@!qrSr{)v?^HoXhq>j2XTJxm% zZhnJS3ol%4YUCu=Pn-xHocyiq3y}`x5O1>+t_Agws7Nx%HB7I*M(S#iMAPHZV|)k$ zIYmp0is7PYE|-gy#4C$ycU++G^?kLuD$OV2lubs)sdnnRLzgojM}s?4{p8`Da62|~ z$S_O~#8OTx=K1W-RK#Q!*raA$aY?WlFKG`#-OmiFN)t zr-f3iM@Oo!jLJajz?ty$m<>Ip3rgx*=*ISlK;?-^?;%Io>E^VNkX}t}G_|DMS=-M+ zq-rPyYnAaG$kXI;(|r3Qd6kV3#edr&u*>NEm1&eCVX=Mi#PPO#V0E#ux~p+dX!Urt zE3#yT9V|W$SBEjE`f8+6)$F#wjt^O64$ZD~?I&8KdnE#VOKp8Uy}V?j=uSiWtoo>g z@K|3KW<+z_POAYY3ft``7z}!2DIOB_Rufe>DX!}UN$z_WcYJHA`VfP#U+}lH00()_t zLh6W}OU91Su3F2HwMEwIxW|-=iXN&2sM|z?fBeaG`615N=k5HoXypV#Z{0M$nlTUL zaCd?fIvWb>hV;e75Z-T7$wFS^SOw1aJjl~+zz+mS242?!GR-P5O+mk~aODE!2d~X; z$5~z5?+0JQTt6x(hA^?Nmg}WTzh4NUKF`X|={B~ei28i>%rP&5oy7h7LY+GL`X%#O zY%wBD#(^w8|39SPj59E=={x|G!sHt;MN&KNjVP`)%k%a${&+_Fyl(jp>_+~4--}BG`|h>b(OgG^MXD)3V@eSBfrzZ} z;%%X7{>-Nfu>f|9@Q1pq-0$DLiw;TO2m+bV68Db46@up8^Ssq^Mdv4?jR~CbM_dZ$ z+5sJ(5vbAOJeQLCZ4ap_2?^EniZSh7V^B`1=TMgzq+7#w8VmPCw9~8 z>6*uCJv?P5GkMFL;nLST>`^hwN?>swFGuwV$CSsT$K+!3EUac-Hx5jMMiX}7#$i9A zgpdNAX!PxzRmTxJ(Kn-WU5WSZ@h3b|;0uNUSqsq|%TsYUN2=`iuxAZz1L$BYr=2}4 z;%n3yGOC=?+u)Aazk>j!H*+gZM?s_vuJe`FvOsbf^F&eD zG%?3#2UDP8KnlU;Z-p_odZd@y;}?sPyyZiM+1>{E0hf9}zVMs4rkrDke)T;f;kHY8 z$EwAUo1?f$fL>iE!z*cO3 zZ*T8jH8QrQYexq+H(u(GXh-HfrN4D@APN!6q*1TcTlp$p1SHTDvYuyrPQ{4!^IRcE znEkkbm};>&?jF!*wp@EGM@Sx=&Z-^en@HS6_+8u_ZcK^T9+_Wri!ZA#uf^}AK6c}U zEA%^GD%bz?yY9mBeVxd{4xk1CPJ4e#zj>D(Gwo+QmXTx>d@2N}8x*66cOozEjPlAO zh;E?f4-ys2RVl75DZ?SzSQ+PcpJ;A94vr+vq!F%N#L4r%l3)v|8_?QH`=(wq6cq@0 z!v=5c8C>oMuJ&AE$T6JZBBc-Z+Jsg4v0&r4uDHH95dK0u^9?$f=(I*S!ZYpN<|C&c zMEXw$w#RIbR1}lKqu3kOeiiAw5PoqeJ)oE=e^NmFZUK3#kOD;&5HmjP7q^yZXVGVq zr0`Zcb>5}P`4d)6dhKv_1`kh!Me^(SMt|vnOUDFJbe{xPhpK6s|9KcS6h&E=QdA7M z*t%hweF-X-qfiwT!mba+uHGSJ*7&@H-f}|$An`h7#hNw5%DcNcsWOfn6PcJwm$NKv zdb_5;ND3W1A4ggbs$91Ve}NI*G_)rnw;cFNZ2?t8Iv4uk#5nzq3%hm_Kak@!f+S5~ z!OFN~aUu7#_rKohaM|V#xZDXntismwS6tN&q4k8vw z?V>?HCs)nR>9?5YhrpQ35hL%rKg_ZLNF$A_xx;A@u1`jGsU9(5Fo+yFq4o|jsK_IB z&|FQ>#mRz}+Z2Sq(RF6b{q{Xyy8DFeTWBcHTr1F>MXz9OZ|b1)l|8%{n=wGPs9tD- zNV!>G8KL)h`ofVl_`G*r}HUz&BGX0ix<(z7>kq8&Rj5}llx)A_v<)U%i!nDyQiw$~E^HX$@tEq8(wMD!!i_IjM4 z`#6tiOcq5!-=|p5X$mJCPw9u!oUBUEY`%K*jNPb$ef8g$r2FBpC6Iu;v<>T1({EK2M#Q>$ir5g~i7^ zq9mC^56QE{YxMJc2Z38}6s>>-a5i7BDY(AAmdg&qJ1Y{S%HsZ>;(aJh46IU~^4<~c z49TB=XW^C-DE`IlB3ACf@%W8VHZga>tBRh9BhnQ3=PM$!czu8}zoT1=;5#htujKD( z3w=V{9I_{5`ac!GeA0n|t zeZg%^#UcBW#uIF4k@G%XCpUm+t|40Yrg0Ra#Cf)lr-@ZZV==f6Oa!8VO?yugpOYMw zBqk5|PPsBdfg^=ReM7^8di_T)wJn?1LG-gnQEGU2_}Upb7;(nKMV1Na&ZoQD!Qa2X z#>fL9Yj63TH>rSqS+O?0=+>ZN4Omuk-Yl`AaDtyy*>X__s#9=fD_tEspG9lVG%@+6 z_-5mUs{?+d)ydEpdU0s#S-b_TH-o``il7P>)9G32G#d!0y(Wk9EoLcJHr+odWPReOq|DRIR<@6be zlgsUq^?rWy%!u0H1#Zomx?syQ69{e4Ln;Wv3qlv85#%TO2mnBXa=J zug8?gloQWq{E*CeQGx!w@Mr3s_gNGyIXKn>VE~_^+o5N_T;i$m6M1re00Km`!Z0EJ zW$Pm~rD~S5T1+D6{xryOgF&NU?Fy|(eDdzvvi6s_I#mhMlFi3p(VCH{6-I$x7ip6T zFCL=fi$9Z-_+ICiHK8EV0$$_2vV7iJ3JP&lhVOrM#ne8~GVGn^4uL3flw@WncnT{! zZAkZiBQ%nhDMJ*6R%K)#DWEbnCdX(%Y zm**tbPS+umV<#Z(=;@eFni`5%W%ifk%UAZywmF16QSs*I2ec=>@!*TF_K}w$D*kJK zaPNL(*&l9P@FkRG&rL@P6Z@=GY>X~{-I!1`760~+F=wXJ@QRHq=Hg2((|_d z9qK#-_u7PFj+Ixh(kebpvD5!{8JeSzOAd5ccu~a8?Sy7|hkjvF%ldw@a&1OW{*mEx zW!b>7v?Mq?lHf}A-ZI6lx%I?tEUm_J*`Whe!02{aND_(1Hbm*2I;EG|vfq>mDKWJW zlT4_{MdoO{FceTsjv7*Hq6H-)nf#Zh!?6=EmV^J%Say@R0$4I1vTy*i-L1Z-oq z=WhnT+o*(}fp0_;4+kvj&9a%ogqebYLm_7%xQ#o7o>v$K#w*nS{fRX@IQ}#oZ+l!F za3Q;wX(0;0oD%Xh0#RK2Nj`<;u3fD<3nQbM-t`}ON8dkkxv#g2y!Yvw^X=O+B2X31 zH-CxItre<5YWU~R-QwLnHVocbgYC=ByNYfiA_`o9(TR&wX=9n4w?YvG(7pQS5BXh~ zHgGHmrAO>-PXk^oOBe;{rL5Dt?%#f=D`(1FWLJ(pdB>9;#q zdcGcwr^?JFvAIjCqcDRrR)pdu&n~lg0$w_E+_Mc~kKyl&SyRGrlCtp}2kIxsxO+2- z;9F!JzJfp?j>B>vn(~V{RmbQl;GY&yozLZ2KsiYfPId~6ei{rd|074=P*0QQ;Fw>m8 zJ>?@Qk{(omyt&-?_5a>uadhVi!RF@)21^6C=$=j1mb+s`NpxPldT`2~NmU&@6lOy3#tv|6SyW;K?sIxg2!)Wu;OKlC86 zI*v_`LpC;!Djce(M=)9&g?nl7!tUmut<4%bF#(K478Yo^Hwx z;$JR&C~>@4GJpJ~kAJ=cu)K|giP}SFZKjDDY~smCuK-|Bnd56C9IpFK&kn!PTS|ix z3SB(QR3FnIc=ivSEIw;Y^zru(@1{IzJfz^$P&tjk%CTaXNgH@Cpw>ipUL4VS-rkYs z^A#L$@zJU1d|uZu@I!%jlx2d(f*z8@^Sr`C7SxTb9vbBJh2VtS(Fu2It=y|$#*&~- zWCz=wZUPLdX!rl#iyeK6mjYOF*#QVzh1RkJK+qa_(Z$O3dwP!5*C2`0yNR-oi8~7H zdlKIcZxHl%%{Dlp=hCj=8eNI0bIrhT9L}08gRO;9~-bVV5E)j2{hiq6#)D7<(4Py{kGHz8XR|?-U=xg(mV0k(sqb|C!wsrH%<#yk2 zbTAlMZdsPCZ<1WNPE;Qr9i$>*`x{&IHzdbg+G0P%No7552Xn+*CHfQTOPMvk(em*P z*r5VEkHZ1coM!QIr@gOLJc)dQW;ghMS!_>k&s$xQ788<9%*7RjM4T4kYBiSs&=Y_~ zR)pq??*%tCm&KH$^nkUq;!WblKD!vS4*Wv>@yOfeI=~K;kl_))(w86ID+)>Mla-xp zgyP|mn|bzHLUsL}aF?>L0wlthS8eaP}%r zVdLez2cCxSXdVnV?=!nix;Z!IJ31B6=2Uf7dzJd-(F5m3xSItVBg`^#lQnX8<%Q2W zj7hD!@I2aG+u0zZ_tHFex2~jT&okzR$^7XLn0E-E!P8~2p6qHr`5O%Su*lmB`T>`Q z?a_#Ej2n3(_Wci)ZR!s4FGYw&JpiB9pKFk%15nqv0>qd#q&&ahus23~8Ki+5fC*em zIDtt92KQ>{c)y8eWd3(VF4%S5H~Sw)mbm2GrVJ3mWpA|0@4r4A{j!rN+ur&5=-e9{ z-?G|LawxRNC;~vAFVGA_Mv~+NUk+JDuc6H%m%8r3o-Z;xR)w+^7S5hPT~hSHG(fm? zXFNF5v|i7Pay$Ug9K*#h+l$qia~RB^YPwNC>K+5Oa*s%REsQvNG~?JVJsj}wN2bB% zI5`8St6aV*zg^;=1?TvGKP237H*S~mmC~@WvF#DloKv-$&u%H_1Bj_B&r(6}6j#U7 zAHsxw-Fu#o#7VjrdpN&Im>2V^XA$H*(q=|LHEjO3n0NnA}Q&_W^8y6yan@LZXV6*ZZD1|&L zwb&j%JRCGo8lWJaYb!!1%nAzejWM|W|9;=tZo8!p1UD;T+F$7HUiqcCtNl^}2J`Xc zU-=D~Nyrl&Ih+^~JYzF)Z%3!)cU9hJa3lFWw>Y_+y{?O!wke4aJDzF<3@tua;4`K6 zu;+wm3ZvhLlKmFY${<{miRD>bPe@8>XjW?*!McmsQVkKjwfF=pPzfZqz76K3s$qli zJYZwx|8R;=#*o{gIJv&XE6u&&c@&8eI*bP~Wh1#6`IrjI@ZJLivPg5R*h*q(ldf8d z5XDq=0hsVlc837A0B7wvBIS`U#M>QiH@U)cwNT7+!Eiu-kk`|!|4(~g9TjE&zBwRb zf`OvaN(+e69V*fxDjkY2fRvKbC?-fq3=$GjA|(txgoWhLQbUO}3^fA65PLto@Avn9 ze`ojXZ_l3nWA~ipc#ep`%slgX?zryjzAk=%1z$N@*mb|)ymYB`W#teb{_w$h-_$>) z)z=1OWF8*ULH~0>p3>D=1@j&hn4T{26gBhKJb#$M;KCjSy$z3%($1P#g|@=4O>^`i ztFELdM>kx<(V9?Q>#uSzjXvt=YMUH>IR1UD5yRD~94s}-&3LfxyfNEG+^Wq~42+Ps zIkkHl8dQQqgxRC7DPL6?=r6WVRc&zb{cA9k|LS&lsNT$ci*ev_5LD+J3K*#<>*JT% zYP)}b>LO4(FTMEXF&?eEmMutJWf#<@VB^D68u{LSx+&%K;imaoSkchkV}fM%(%?TE zDk}MA8No3%^g3f-bqYq27d@6<`^}OnuqeKFW93}TLF!w%5I(BlZuQeQZJ1<@n%GHAg1t|Ym=PS< z*C42QBi(8cE5yOCSD{kz@Zj(>3KXht^w^20*R=yRHCYR%vCipN1zhJZpJ^y0&q6IfIv`)nCL~v-JdS9I0o&_|b$a0G1ax5YRnA-h8y%>8L7pxUNZ7qW6R|?HLIL zpZrSQlD9+XXA$OpHsxtXbEiV)%}Q~H%k5v7n6BKD+dM(h!+}DLUzvG&Xo3{!P8Uol z;%#l!Zp=n&((ONZa6zCNpEp7}dhjXgXb_nDEq}qImwXi@0OM`QE9hX5yhCTshed@` zXB_t*@Xp*Q0LGp*#CcH1iK;F*GM}n}M93 z4NB#?Xb9{ho^Taoy}CSdJuBkCADfb}1%Pm-J=c`xBVqYDVlSCq#phn-;7VThgSUqr zq;5+6nCD2Ge`OqgDo+)KVtS7DSGGfkQwNsVx1X4k=~>G&-x+qa_&M5HcIh+Qm;!3Q z(q+_s&!1@QA3Gvb<%*bUKS*)30$o16Wishy{Bz|3>Hw&RGvq7K8?cz^$oR5(=!Mmz z!HQUWR^p76)0>SiLFzA&xeV;^RKNFi@5yG-89WhEowcNSBBTU4r>MFp=ZzvD$Z;V| zsynkK>iYs2#439=bFQcAN^3|+F2uPtmSa& zQF8Qi{eD3P;ykbH%xZW_3{7YEQyp((0n`dpXWyjnA#ejt zb9D#gk@A`aSl1Q~?+H66x|>PVzpo2!b7yjClu3O3dWBM|D5m#^9S7<54AP|d{0c#q zJpYu~+ln83_jF(CDFLCqZ|O>%YR2xTo!vvNqO!9+dZVT~D!OwibDTUl96zn`rFOKp zPn5>t-VD`B*6{Op?Z~;2cpF8>&EeH>yi=3^XPEqj@@nnQ3`nqEE9W~~op~7lb+J%7 z4Q;c;`l>b6feN?vp=-*+2jfLlWhnOut-g*pS76r@S0Q&PyXQq(TJK;te%So6_o_&r z+;&gNF>wzMUcn-pBJ>_xy>RrtKDx1!9D5jO!ar;7Q?Cm58vRhlMS;3+1~Wg^-!-v{ zFa5VY8&=$?F2NJ3jN#>?nf$kiF4^#$wxODl{36Y&YqR0V@B}`6CQ{on?J&RtX{Kej z{jJ2smuj}9vM%0FM(8N>+vEQAE3&6n-?V=%GSe|rN+opL@LjpUc#hf5&9(fmOHlS~ zEL9oL#v&DGFdC}f^YnMsXwWNIS@`<*7C@BTKQ%0J5nz2(W$weR+G;0e*~`Mdu#9T~B$XF7Mr)BaT)sll6kw+=_fY2@JLXM*PKW%CiL>uN!0L_4FJIK)@tptd z@qWP;Y~0Vj;BN@X=X$YE?vbffFs`&z0vq{&d;Gbq{F`^}K-2CwU04o^iI-9o7c0BI zDYNyh4|!Fn?6j?Y9B9=i(_EM$?=?>CSD`370XE{TDk<&`3NTRlyDADcH?q%}D^D@S z+!;C4{PhY0zZr*|+sun>RaN!ZC!g!ri|_cb6SN4dVxslZD%4^OHjfqE0^erxoI~P$ zy=F(XFBcz=*OGS*x%liT$i9`EU zX`oBX+Ty*(!(*vt6)6&By)om^}_x)3zHd)iK0qxbc@=n!5Dr)-K0!cWeeK_l; zBRKzXmzR^v{0y?c3sa7p$q$?jy0-fO76+(_1D=;oyZ*KrVtfcxRkt2Boj-7=rleFJ zu!1bBR!Q;M?E%(MPMMuh-2l1^`oazy6&YiEw`dapFomZS<-rAqMx?{hfR@P!WK*gDRPc60g^&6B7}h z!3_ne_s055`w#rY^WOPakc-Dhr1TKl$*kdk^Bt^oxQ)zIz8+!v;Hc?I_m)ZE&?*{Q ztJA)1^IoER?BZUrLGisJz?#*v`+7tSfbzf8DlbSKsP(qddyk;tfy$rM2RbM_;?A5n z@s05o^awWo-EB-yVyV zN(!dv)k2R%NZj@v93by8c6IJ0#1U`5aM&>>=Jf!lUD(UuVby&^a-ak7Tp(!3oGx-O zzJ_k=;EZseuty%jTV0`!HY48P_GF6>M&RGxn(39|>ctiDOCPJ} zkBvUDk9X@{H79Gm{U#fbv-d85<3aY%1HLO^5oj;ewtsPgFc zgq=F7$Ix+G7+K`amc5MUrO!1~+~^#{S6vNziQIlsgxhTs0%uZR3Yc=)P7oP{h8Rw4MfYKD;-PqbcgZqvcQ((YUtztxIY@!zbhGWcy%smF5~l@ zEQ#&K?!ni{5C6PjP`o0@cpiUV9O&ONBvj03R*dk{-d8ZWPgWe6&_X$zr1 zN-5W)ppu{~t)~pN)NY@%AN8R6HSSkI-7c7xAnP)AxnnObGfQclo{LXK`B>*Cojc70 zY3+%Hw@xg>YaL<-%ERqhl%IMvb)Cv9`PT{no6{M1FKRv~YsLfUfDw%OJvAlM)5D-B z74!!1WGBJ-p`p>4f#nA~^XQ-REJ`+jL{bGv441jKPjT*xsn0RRjtXmn_n27L$(z{qNwXtx@I`hg^mdfGIlA$+zb@f~NgRREY zFEvA>Qwx+iFK}v6R#K+v-4;0Tn|7=j&LNGubKy6Kfj7qv8x>scxNEU&GAw&h2bLJ| zQ(WD7Z_k+K@N`(*<%IQG)v#dUM_l>+#mw(k(SGy8f__r24=!E0BxKciVjWzf$H29D z^ykmBU>N%P?3HKW$auFF7alSNjsj8OTQeOoOYFlc1KZQmV;#6>V_;ko1ysN$a5_i> z4?Sm)@`}HUxbmcg(x7ylD`hLw&A9S88*M)?Fr`K72s|1dae3;c|Ek%>U-#! zjsiX9$dg@pXq?Z<5ZXhBBfEbX#n-(wtEvC|fnIeH{mWm|LZ zH?q%xHw6+Z5gN1wMnJJ?#x$eK+c<&Pjc}@ntIvi zT*aH;=2`>7-OP|IyD-Q*keCk_PEFxJarWA9h*@PXRf-U6n8WWui~#e-j+#)w)iIb_ zca*zWOFmpomgL>u-j2XvxKvfouyz<1-{a)d%L#95YrDKJpt7=3qwRYlsGh2V2EVhZ z7_h0%2%}%6Wn{z+Ujab}DXD(?KE!aknRG&22>KkM0JT$-1W6k}e@fEDN0lu?drh^U zhxITz!@s{a5?p>hmn`%g*I&Moi!IEXkyKr({PXA=A6`=`@nAG#m>x#V)G4Md-s${m zPvEk)uy3LXIW0%7Vk2VKJ@1DhmwSTEuB@vQ(o8yTc}1Q%0TO>{b#;^F)!s0Wbz9&< z)(`-Q{f2Mi>sjfr_%-&Y?7?x|y&s%mXF(0%z;5Z-RJFBZ`mokbQ|+mm?rkAJ$d(7+r<6SkAUP-0C%w1$!LMu6Z7G2VR6wjt z2lP{Avu_HTE>%>h!3O^eKVkTYxBQ>V?yX zl&H*SRTwc&ecup|OBu_5@2V8aNLEJEVnWPTm07X(;&t_h7?uRYS1(*6)DNo9V_;7% zdch&?);+FHZ_UhHxI2n}gN%%6sg?zx6Ei<7%CBCmzY?e@>;1;j0%mXAj){pSBdwtO z!1M#75=+4o)~JzQTN z@5nVS)uln%E?R>VZ3h@-_W`4{fuoUMM#cogZ)j2Y95I`y$UAuoChLbNUUt;3yPLt} zt}p1CoySFQBRxl3N=4b(m8h=YHy!6*bK{Tu*^FOVZ`^DUpqm=ZTaa=Ml16=UOXKEH znd^6h+yx!Zw}|lY$qkm2l;d5g{uV-5+|g!f(^$h70q$vq;^qram+F}c0fv6nhw z0O-8$e*EDO5Kl8@!VWh+rgzL?w(H?I219~hA7 z3xCXzamKS3Zg!6gRc}4K@tjlKKXL0uq8SX>cJgooC5t7LR&G3|<~Yjhy~Cn5_#|LU zMLCxKvfr*buosog|KF*7YCt_Rhq5kBvdbvhFG|JmAn8%p9$J!57_Q~u+eOeOD9?&q$+69@1x`(8CSedSl=-D`H~a2!22|sw*K(@Niar%;?B?gv$FJ<=bm#5e zGeL7iv3g)VotMS*ggkLam9mPmwsR2s>Cs-AAe&^)(v_S?m}YV#lTX0~ES~IPK4HCT z)fXsMPUWgo==%O)uJ(^2iyK;^s82s_KTm}F`QoGK>DW(IZ>Y6YZfU(<&AL*X$s~K8 zCB~F1^h^341*wXdXxc4DZG_U=5C$^0O^i;J(8(XZSp1|YRNL9PkavIXBUXVq8Nbbk z!rwMi!{p*-k(Uq~bLw|59D&b+cvGpa8Ma%S*(W4XX`dtWch)O(GJPO^I^npS@gLfGVTN&k4`>eZ`REDNATcmXWhBEh)4H%P)8*uNkPuK8#Z z;(hP6L%~5o=YQPobh=K80|f4n_!y5Sh1s~>Lt_*w2?NHzSxI_F-voSVOO&oqbY_UR(qDzMlxcP5}tLj^4qQq%`Xds)L-3oFo$z! zen_dJU=6r4VHLiXL1cQ4#c=lIyWA*6-}2?Nm40Q&fVz#a_Ms=3vJHjG96xs21}*taRL3BJyBy!v_(Gzio*E#dJh`wf+b(NW9?J zRa?ER+kKxasvVW}<={!UxVS`qRE_+$Hk>HFo<2M49{R_hlOh?Yaj@dUW#9sdE&g(| z8ty5iv)@zg6COksjI6?4=Bg&CFTFrC!}1&B+;d$QhD^8DYIYjHA;*Cv8lKwP36>$o zZPSn;V?gtNtQSiDnhPu}EXzPSuLl;U{fb{plDy#Buzw9dlv$g3SMakSRy_GVME0Pr zh!4=J)?27LR$P6RdBi{#`oZGJ@sX+T_Qet#G2D^EH( zEtr&c9!`duyFsR#2SvyAHlH7vQ)=Pfgj--R|&`kE3t}L#4RG zY3h9?4)N2}C1^aujiXW`B3-Z8GU~dE?euvMbU)JsEs<4sA{TF6SbQDuayx9St|ilP z29iPsriQI&Lwr{s*9`Qsix)H$IPNPqwC5fp0V+-_U6 zZ2Ad>x_^_j+7h|$E&NPSHtFGznEZK3weNXMc4S|LR0^Sw?Vk`C187E%0P6;IU#ISF zSQF-kv2$||gJA4rDJ(Tdg&l_fAE%*UL#@-pYW!9FTRWCa$YSyNVMhiZWD?iH6$8`0 zXa-7P6=0=jx1pG{lZBrt6Qfi>EenkIUV1rPDOu-}8N*?h`v=4$FU;$S42x#UkTVra1ULkl4#&cKvPO^JGiVwM4#%y6;RCfc%X@ zB`{I2A%CM#+FkVGXfFQ&UsGNPYLcL}@i)bB-j-NVragyUq{vQQ1e; z)YNcrayG#3K=bb(`sNK_*~uXyQlvm$SzHXi_v6EOxt4{+yQ@k{v2_gh3+sA%dZ^Op z+S!io{yqgq6A+3Pw%%BowSFuwdao8a&0@Zh;&56t$UU`BhG2ktx4;v&yr|JPxx4{6#L0In9igit!mfc+ZS_mQ0 zLJ~ZE;A5onuSKW_gkN!@cKxP;q{3#xiCPd`<)RN(l983Q0$1S}lO1SYxopzdqiiQS zLYbw3=d3WfjrTfr_p?&QcVAb43%o8JI5xxrYQ4FJN`8{q(`Y~fz%%e-82R@! zJ3t_%xOsTv1;8Go`b@gU^NgOuX%L)me5?RN%ZP*oekA#=L!AX5pLl_oEm3}RLd^`R zD-CO?Tn@wLNt6E4(Qm_+^apSZWZCoNI+Vfi z5kOixu}|1_N@p65M@s3D0|yFZX<_|Qw)<5AoMG615jEx}i#>fX)5W{KRL6?{g_yMz zdL85=wXGPjtKeSkUKf;vE2_S(u1=%&Cp^bf9v&XT$KTN3Dv1e6ZHnri$pYQ~*NG%J zFX6C;+R5wyJE%(ir*@x#f?t{Isf)Skoq49xq+T-zw%n&!Sm7d-!CR>0*8+;+@kz*n zBQ*`f2~U4;wE84A*=~krpizp73}$&6*T?6;tInZ9FAMnXSe4dd@G$O!d{-SL zpRIng2L$MiitpV@`?Y8H@mpS^+Fl$(LI9n8`T9h>Rc+A8iElQg7iqR% zvB>ss<_&%Y%TCNMs%nT7CC9$IzF*xu;ot3ODHYU{?gQl-T9$cq>-ThL$@+l<2ZWr4 zWNz%#F(iP)c*JxI_3lHAEdEopd~$8*Kq20Gnmre3P+<8N=O%)k0Drgh9i*$u%JBga z401jXt7lGw8C?TN1LM;uDJj48^yr^sXU7YrlRcp{vsxVatRyA(*YB=GmRzAM;P&5M z&(uhiF@yyP_IoRmNIeIGZ_`mE#8!V5dX=_wp2`sj-qks?`;xN zFCV`KLvHg_-a+KK?8<-HAUPS%RU}Bl?0z;f1_~0ppGinkD(rrO(n3^ykdOc3+gk7z z1qO{9Y&x)aS{|DuaxB^YYD+915bxsZ4)|}L#f*);vg?E`u)NBS60`bbMctnnij|tk zFAjEYs@uX@^RgU4C8KmCLI#+1GPaB^*_M(o~RbIkx-0|y zEn**$dJR4b+4lS&uAjzr|EK9cxM%?usu}<~I1gyv|eK$dzwF1kt%Cq;UY6knZCgpdIiHnO1Lw?AIBp2mQ zkn>zllUW;ZamI`GylLg!Si`~6{c#qS3(&_!fgj}12?tWd z$k@O(RHE4>^zzzT>7PG;di|V!e25<3Tqiy4UxOge58hOTM81m`+eW9SZOK)zI)zC~ zOSACTR8TEh`1<^aF-R`klRwTwi2 zD=C3XnD00QOy@qUnB8SAah8f5$hGDG$y+u=a4s)?JrYxIw@2-RZ)SRWtkW5Y>ReE} zi1aB`4EOdLAh@38rvM6c6=)pf^j0sS)MlML+$*%VCVD$NZy1$2;u$HPI})(gtr*DU zs;;g@17Jlr;pBe#^5ga9cckt-Q@${nCc{Eb>V!*a z$pU~SDhO^VD?4bZZ9iBMEq;pg9Q0;BaxCJG2|f_pbzy8^(U*T^SRdm?b+8`syDxb^ z4U10+zs7uEs8Rx6AcBHz4c@DL)_C{0lLh`e+e}T|kl8Ta4<8x;<*}A=&vV=1*RNk= z0FmxDg%w=K14oXO`bxhJ57(LMK}D?&&(E>={kelpvQG5;E@bg-zfi!)M+fqsrC*q~ zHXST6iuXp)()&k+PQ%?f@4&#UZ)@&dlB(-la7vCSJzRUCG-nMi9=$qsIQy`E1jQ3) zCG4r2F&9kJ02PeqAV$6`v;dI zwdEQ*GANYo3EhnQiDs!}ii9j*ahtYJn?*fU>bSN!fJ$XDqy?E8F6^tx=RiGizJfho zB>3^(P-XexN=}pOw^vZLPTCDiyU*1Z_?nU7maZZB4k#FwE^Hc9c@?i`vEMhSd9a5m z%K@xpnQk z<7TbQFrM+F1Eut49Ly&}JE}=8sY_7e#n*XVImX0f!cX3-mhE77tOMDwhXRWWK#9?B zUw;xxL`jOa;mX2nMG9GHB^&qnA%1`G$(~>w(>L=>Z_B6P$*vh>`0cs5R96cix!mUB z6}qmjcOCuJqEqD_3uc~RCWus7oc*`<@i^8o|NXpj6CB4E7Mwh)nwx~2Zs?Jz8(kQ? z+Ql5JR`eogOd-s~N<6T5y>=i9P3XRUuV^-AyDN?t;OY^YfUl(vxm$Nv23%TI4Gi9# zd8F!eTx5S*>LWMk$t=}nz$HcqK*7s3w8{M2?sg-S#XV-i?BVx3PNx&4B_;c0v;aGD zFjdy1WKw+wLgU8$0E;_B zuT+tG(%;e|cuj1@Xb@lX(f;nU!C=X8EHy4EaGDD(>S|s~s z);U(+->*$P2Hi^L@1WFJfDFtSM}IH9@8GH>^3D|Yi|eoulIgt=(?P4DN>7N+yyl=A z`FpM}Qv49@kCNiyN_YQur?BEf6eF*a)PHkHmcC{x9<&o@SeXPhz^ogY%f538hW;1M zT(WqQwUa7JfI-wbWKEjg-&V(^Em@I^EiJYIOnAgiKcke7jg72D1HHr1vPGkQHa?)a zX*nLR)8=aza>B0Syv`88!|^k#!29_z5VfA7Gnr>yRcvLbZHn-ecAHVe0^*x0-`=R2Z`%_9ur4Q z6g-`V%dNJX<3yY2H`~9o2C$bn2;9y~^qS!%5e-*WaiZs$Lw`su1Rjy&d8WTN3hw3B zGwZxQ@#`6HQbvFB5T!})m1!8oKqUh}eTUfNU_RBdRrB5Hp`HYfSlzQzx}#(UIb8cmu8atmavR8TEG=BlXu<3X#Z^s@DG zf!e#yS+@*lD8BkjW1LtV>&EAXifZ&V&?>rEC$*7V;Lh5{XpK)sF{S%jw`{gMnj4Xn zh1vISe`+U`Z0B|}9`%k~@dQxVE`AO2-zmZ@rP}hsD&ouPH*>LWCNa8ZN#iUd1kSqbGxIG#y*!&CX12qK&g-2nmJck@Z{mgAC8ec z9R)SrS(O)hSV|jl2Lq4RiZ@pC^YfUKe0q!DKc)10)c9@LH$6YX>evTv zAc=Gkaw-{zyE=+tGxH?s9n5D}}@B;lLdT6!?3$os;MI2mQm@nB+vd_O< ztpezkAmq>rY9UQ706pmOR$G(>MQjorj!>{kcoF0tj6Ny%tgl>vRb_tvPWkn*NZp6g z_iu6js9wp~h_lNp$h|vQ>UTZbGjZs}9!{kiw#I>>Z!Kv_+y^_Vnb)f$fI{w36o#Yq z*w`es#-B5eLr;#6bM~0Hgdlw&mX=wB9#fROlE4WO`|PTAmg3d^3k=y zr-H-6v;vJLV?1`1)LHcbuVN5V@EdeJ>;mvPYo!9@=S! zeO39cojf{*tbg@|C^g#u6FtKJeslSsI}U|QjFQf2Wk{RJc~(x&EYW*jvK-QJZEI_* zOpI2|&bIeUnIvo+!0wVxusKhs?6NqbNFe8hL34YdmK0$J4WcvBnJ@nelaH%oKp_rf zo^#{lx4tXu$=N7e4V0b%@(lm%R|=-%J;zWItdt5cCcpxPMg^`dzIAjJs=VIEyV4s2 z?gWS0ZKjs{zcxD51xCQe1Ht5Rw?DDJ784(|WkwL=h zrmIo;2iZViM*~1|e{UBgvQLj}wpdS|K5fwb905)C0jC@lpvzA=2y7Ot2JWT-Km9s* z^^OC=83}beQda}a*9h@F_O3Kiy&YkevgD&oAh;li1cI7@J#IAEBf`-Mo1Sq66OTrS zW<(b6iC^lfs;_x(WT-&ENx%H`AiCPkSRf&%#x9qcMs0e-D5`&W*!;_vFJqz7^WiY) z=w0=Ot$feXm)vQpkz5&$*WtR3!-anhjn}vr)5gFddj4>zv3q1{+%rk5s~D(4)bh=$ z#sH(dG$|-8n+NxNuV1TAXkk+3=7~o`rA|q&&In&)5O=f>H6G3cRK>{5Z{6x;XlRQY zqpWiG$LvR*|@((W2+x;Jk|NovWO$vVpV`7&>Ktu72ZdElNr zp^63LG~3{mloXh?L#H%`=z<+%zg7>iEBtOe1S`02wKd4?s~H3vOq_c`XWT8|iU=+) zSvv+dG&Bf37`FUVOv^M9Cw14B z=CG}5kZGDd+Z-50Y}g=#d*n@~u=Tu-W(llT?sq(|mt)gkQL>J3z+li$y47rwFJ3jV zvNF9IYLLu~Y7{*LWH>!=^J}YrvK|X-=5Lz|;gobEwreEloH_wivS%^Qgo;G2$sKCQ zLL^3%B;s(m^+74w-)&b)+}PRK1pw_WAvS&e`c*gO^4{S1_;`n*yJ51J*#hIRg_qok z9|!J#r78J!h=xX#kZ)Ann4@3nXo{VKIebZxP5T#_)s>avKkMs#iC|64fK1U0z+>Aw z3seYWC!wlbT3s#4?JRq6SN-PAo1wqjCL4U@e!K14yVok#15U6J$lev&_f)vqx#V=a z2_>b@O-_?q<;7>G>>BW(#AF9;b^%d6NuIP&Y}Zq=3Hip8g$y@=z@tZxN{A~aMlnyH zJ~a$wk-f)zST?_VYwFh=*i_y@Xig5yzD`0(^1jSaJo<(b-=(GsAa$~XtZ!>E(Nb=H zZjw(_^hZakav1-DrizLR#^u(Vu!QcCKcGrt2tpON?Sj2zV%NxD4hfEVG8>g<1DJ~G zxwF7zIkeg$y-+j&ZDWa@76G%}RdP3a-Jrm#2p?kQILDA_;6Ie7(2`M%;?5aZsv$m`1+1me+sBJNBT_DD z1BHq7`_!y z4*&(ob{%Lk_KymY<^bV65-8a=ywUlU%Rz=2pCefP{(O#j^Tx(|5RiQ=!kELrxZJvL z!heUHBv3B`iIcFdqOC3dC=6`m4IInIF_bme?2L=3BjvXL+*Jp3!O;EB;vKB^tmGSc zf4@-TO0ms|E97vH9zQ{tZ5!!@o51l<$<%D22hFT#@*@mLq!9X3s;d!(PxlWyz2O^G z)j7Z8+It3zY;RzbfI2X~!H`>*-}Bmx9Rbu$*@wYIgUH~y_<5hEG=#=%Nx$^T(W&Ykl$PayYdBC8;^W0n%}8~)Tn9)UJESQ-XxUe2 z6%0SWu;bDyYkf!$|5m!kQM%j3chIfJN-`9Z$N2gHD&X;16ctM03{^l*-`T!tno{($ zsbx4*#h)nHsvZ^1`@=BEllsMZ{XBbIN(=FiNZXw-QB<0E3}lRk0v`}ZGA!ID@-XV{ z&}pMUp1q>ptM)hoy)ueNMrMHDuIv44fdC8)zzO!^#SE4o*EV5-(C|7vP2{4rtbg~& z)dzb6Ktax?>37?RvO5D?NAlY|h}lW}NagAg`eTv^p~Feaj&3XZmM8d_a4Ey8C+7wVeERhVUkhz!w#j#-)yu z7~4&4MM=SS2D=^`lO*7%?Ah4Rc+%QmZ?eBfC zwDzS@v-7ecp75Tv84N2cli+2g4@qKXlY8Y=K>hX7TQ&Ni`=M=ez zNI`O|PHL^td{A>l&q#S94>#X<$-FhYyZE|M-FKdaB6?+f?HUmK%8ctN(m)2FAV^(fp8cMm~0baQi znba}Phh%sJUSx}B#P+lbC`7aNn0Y(ok+N@PqrC?p(W>XAD#i>1HEP9)s6tO?86Z?j@7gk`k{{XnJ2{p1Q$8)9Cj(u-X2A)}%Swgv2p?1ee9eHguZ2{q-i%iQ$z z^u&tM3X#yW3BDa`R#R;$$UKOSr@09@u{J|=0{w$93#B_M4WpM!RqujplU-7u#)y8p zlP_TR=_dd~;& zgIG*8myFH!V?WQ;`BR&v5Sup#+04o7ksLy=iBdC~ux~$xa)UvTrXwPYy>QJ;hfZ&#WF`=R9siwAK zT34?=iOp$EQRKmI*9#B?YL>m2rQ)K|EjGsWLm1H)>65y9*wg|qNl{`%gRf!pT{%iC z`&-lV_Apau2cn^xDa3aMybgVw&Yog8Oy?U$@hf1U#bdfpXZxM0Q z#`g66X{3IJFBR)h;J@^jyJSjg(fCcd*y*SX-x5*3MhMe#s$QSqB)|(oG%Vvs!q8h& zc0F_SzAO&;-IE3AN73%m#nhCd7uTHzTh+DpGP}#V#9VT~t$Whqr_P7@yP$TxR#2Z_ z8q_G&+$`mpU0(i^ONPO%{fcgp^y}`z_E$m8Y9EF)R$U@`Jc}-L9)2}fr*8lt75`$0 z3)+jRn{j%R30EbSd`4bdb|s46sHmPwSj=(2hEjS4nTrorInQ=yD8_(1Zd^dLyArlL zWdly{I3)0hFZE1wO&5518@e5MNwaN}og+`&J-YZa1}dCSDl-J?S zhgxCogc8n-m-t10z|grbi9J6L96gry{*o|g`g=`Vy$&0V_4E&B=GcZ`nH?Nk>s@i1nZ*a^xj@i} zPY$A%h^iX>8Cz`AepwC(>c$Q+oA#0MAdlMX%i5D`Y(2YS=|GMpgjb8N-n%pYLv*4|XTP7R2MNN@zmYf2Y-x}kO%jO|f zf=stX!*~IW%_lTe3X+(*@827N)_R9;Bw^jh?pla!oS|h^Un>(+y&YrvU}~Y$jiZ2V zU|`{5;!Qq<7BM5EsJ?(YAtxtaRhha;;9&~IYXF(luNx+Dm%4vIgxz@g0Ng0C3Xz5C z$nctXljf?$*m`*=)P)*@Jn3%9zpKvIyP!~NrzqfVNO%#-m39T24dUK$m&>|Q&)k*T ze`XYCgt|#Q6nJMmWK+s)m+_LGx%>6AoFcx6p)NG*5)T&P!&07PpyJb@52t=3lC6vt z7ddUoY+({e+b^KDhda&_nv<{RXz0w2BgDIv*&cCGkD_N`)Y3V9w>PAIuh>+v2s}%W z4bND)$Ko}0mX4`mz(FN=>z;QZ8ap*x+L3o>LvEU6Nqb@nUp+q@_tKKtz3QSOy{~4R zrX}+OHDM=_MYKS5!j_wXa!JR}pR7QkFdk7f?*u$uv-HcrGgCpHS<)2A?zN+xSVW>Z z%_|>(uF*W}h3{%B-E7?2Qah@BJT(Gs#AO#Hvrm80RtedL%gv`eQq;d4gAQS$DCt+lPj~SI1bM6 z0Dm^nIR=T)?>MxkMnaO~gyGNV!~{Bit@m!&c!h&ssP;5z*2_y~)gZ|ZfTQvY^7lsq zpoK{2gPI-xz&M6XCdaF??xUhIzzCWrPHw}PWS*bUopqy*i3Imw+=7cFjK_jYOG_Q( zKY#vAkIXdwqiK;W>HJlY7a61eHmP#VZN3Y7BmqPnI5)K}x6Wy65GWi%NGY|Dt<`S^ zETG#hrEZB@fE}q|9E3_mXFVG?O{4~$n*=aLMMacV*Jk~a0Ap2$VibQDAh1r1@B$fh zgYj-;=oc*wwatT{KY#A&1JT_8+4{gO_?z|INACl(GH3z?nu_TG)*$pmq%(DBCxg_*-R+92CC)G~LV^8lUt5szvI=@N*^eEmjUkVuh?tbb3#Bj+wkDJ=)=X^k&5rm$i+>kn!YU>A>Gn9#|l};+N`mH+NGm1Lr>eW6cN5O7~XM_uV7ma6836fb$)w_cBVo6m~gXU?76!M z^_T{iu_N$V`Gnuac3z4gIo(^3TAnC!Lml_qmdp&iF`~)IRjH0Gn|3FB-b^1hvs^DVHC5BN z?eVD8aRN;EEQH>nP*!!CsR~wqHc@bkPkrBM8 vcRvui!0spi1=-_2<8jDO{6igi;X?L*l)s~*+MIk6`5#p!t*d#LZ$JKT;u$2P literal 0 HcmV?d00001 diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/output/pdf/technical-report.pdf b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/output/pdf/technical-report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0cc04b97f0be3b09c947dff493f195f1b89e1135 GIT binary patch literal 61744 zcmdSAWpE_Tk|io;mRbxYW@ct)W-4)unOe-uOf6=XTF_!})1s|JMnll$nj2nTwT)F@RCr&cxo- z%FY77sA**Sj64~p!@eMCICA(I|BcKRm-EdRj% zi&Mqy|A~V$;6L#HGx2}*C^^}isG7O_wZ-^P@EFz1JpO``wl%ge6Zw}E{g;#m=>MJK zUt{(ze}nzkFr%82vE6@|{HGlb@NY0me+T~;F#m-2-*Nj-F#kaOJG1|3VA%hUVE%On z{Z9kK@qYyK55)h0SBCTd2<9J%{{zAN!$SY>ocRah|3EPR5b3|c{3mgW{$*w>GpGL> z+GP8`q)q03`0(GJ{*!9{Md|<3vXl9LduCbLxcm*)Uz(P%`OCgyW`7?wX8$m~oSB`4 zizR@QiR&NAb#`$wGq#2E$hKY7m&Fst4&A+fkL0&G6>hlqr4AM<_{Mhv-Td%Fo`OS( z(>MnD^KFVXZpF$<=y>YFt^=F+`u*(Bs~cbT@0aD%*3R)`J3nl9RbL(f#r2KNAJ5apzmMlnHGL=c+jJ*1D(%vq4*LZWOQ-uyu8#b+zmCtI(p0Zhe74bB_^sS`O?g^HCT4N_<+L*1P`<-g4NU(Y$E))Bn=wmhiL5=Jac~fA`sT z`L2_>R8nna&y(3@`Py*mI-gZio7rX8e)|+7IGguiQ;?wKzC-`3Ofil~(KyY^h z?c?zs{R8%^R?JUZX5KJ$?r(=}K{s9TDWj~_>$pnhXG3yHH(g`#4C4*_2Uz!iTg=to z(0;&m|F^}g{SDp+RQG>d%wON|ejs)KTNrlxW&4J0pSnNi{C^9<6XfThPyhd}Scbjj zfn}m)+S-De`Qw>6W>djhheQ~i-JiZaba3j?r~3SYn7)~9_qXc$*WLWD`iuF$7GM(p zKl#AI#Ldq2@9ba(u(GhQvHfoeQq>7-gs%Px{TZ7vI&q+o#3r9bVdYgyUTj5G9D*<- zf?RiKX$7ATIi@ZIhjE}2g$3AZ-ShdKTS@XWuz|LcVXv7s_jmi&h}SoLSADF^)2~iW zY3h?t-?y)iFLG8-XVat3>$e}%Pd>;Xpdb=;kVZ2LT%>C4UPBbQ*)MpjJXlrf%Y|QK zH`pL`kv3D1=UzUlSGaK?Lad{=h*4gcwXG({qQjs)hDpvQn2g(XmRrU9eu)0@$US@1 zvY1onUaV#|)F8B8qxTZ@#FP`=DP5Gq-Q>fovKkLHPakZbpib|&o?Bgqv{)0)nxWhv z8U}Lbb-=Ax!i6{k(4VDA_-|7=Ex_F(gB7vZ77zgEQh` z4LuUkyDw6*24#Tuc!Nsh-!gBl7cD5W4MlYbSO+%(0ZZ1lg6EBb;pe_H2R-qJAp?N( zw++L5W(lG*CFy7dAQpk}I)G40K{CWcZ2$;NL6Aov4DQ?nN5DYi5uJn>M!;Wwf?xy! zsK5q|AYcN}n9z~~nW(@~nc$LxW`BZ3gQon1i-z16N^Y{{O9!C{{H_E}5it4_b0NTM z2BgHldyajOou%he8Jy)r(e# z;SywK#BJ*zPrdSD|5CVN&Jem?ib3%HwljR@)huh<-%ZgBZG`24f)5hbn-{9%gTgo7 zwH&~|gW~am5UB^^i+BZD>+jynDSB)N(h?BXbH#e~J2R&fnh%5!)Vo)8hug`Hw>32T z1M&H??oA`mc8A7+wH3Srlt03GnwWRt`wb-1&q4>pjzp!7a3xD5E$}I5UFpu)9_oV) zEvK4@Y<>ComZWTckl1$-##nNfDAdesctTS5_kbK)G7Af|OxH%|wQZU@R6V#Wh^K&3 z=}wNsOnq>>*N|g<|MwAOuU?CTG{)ZZ8RNNvw;uAq93yxXF)wNs9NXTVVURNYpf571 zyf|MW_k8P~!ZJKLW&BCjamkcW2@I^mBacgJj>_2{=dzI&yyFFk?_l?-7dMM^x0f(# z?iyHF>~i+iyc4E4qTe;IWHa-FLImgBCI75JB?fDTk#GTW~#kgoU|#aAu2w6 z1=|vxUdKLrMbnUfALzF0X0+hx9eG!s%*2jA>Gr%&{miGGVYgQ}x8sZJIA<23X*4yb ztw!JENqcs4K3KL%Q5gqvVS6&*9kLxocGby%{oV5k%N;v``fP4!)YaX9P9Z8-7~PSYr8-%XGF*_ROo6#M0-Frp~lW&7L1l zb>8Dm8kZ~3N2S-GGE^(S@LcC;!xA;|Cm+UA4^r~;+@Y|mDSz#`h!Jiukc-dC-BhD0<5UtXnj-5CWo4*996~kpnG($H5#j5^Rp<=QoB4F`uda zaLU4^q+l^p90YZO)BvOz`iHZ(+J?;$wC+XwNn&29E1tFx=+YC;l|I&Y5tOi?8|w>- zZg(1}1u$_X{6OV!BdR6jf)7$zqaNH)L%(~(HpZNdFQk=m!6`xEBI6hpqdwNj_;;MP zj6Hj^9O3#tv`V1(4zXtxDlRYwV7%6LdbU66}}Vt|lsQ)F_~UaV!MV5~B*xv?=%;ky!1XvM<+=WFCnPJ_sU< z5d4FWauoj*4U&TyE>!f_LL_#%92dqJGL&TM0IqoxjaP0~1S+OEnv-4Qz-XA@0+cZX z51^=MrzM*w@3(^y-j-0eF^q3Jj_+$xd(^x0w($89|1)z-=tw$SfpM2AoCDOgtm^>} z%^`CMGtU0I>H&|ck3^xL#2L#x#k|BeMYpTkRgvDos^IgifV9YD(@zf@oI}Di8~Udr zFN6+mvuwyCiF%u6u(;lfEx~8>-zk_FQN2Pkr#*HIYL*G{Mn-uC({M!8`}CM9PWAAb zSefvhA+vocA=#_=J16L1P+sKm3|yy-5Lo<_;Sbq8I5DPv(gBCVfEQ8-xG-R*DD`%Ha`gpaKfM zn?7pmfngLKl{ycaXyfVPJ%NZ&>v513TX%Nv}V@otA2~X8j&=B z`yuC(wm{A}XGp70dp);Jx=FuX$_ZES(>+qbM_>wl{D9>{%(I{ik@FnueEF2IpVNsm z@~!P-Pjf@FKvc2lZ7Vy_iTjqGyi5G9I{5%I8T}?Au{Et9e`)-+6Q+>i3NDz-%YY~V z7)+^o74IfnE1mwS1zTj6s!Nu)KwV97ERYBx9md^Bb_OA4%i6%2k8)ap9pFuc0bc{` z#5`+Jfs6lKTk5_<*^CGSTyCyZfMF*9E;$1|1rwytXYQUq^e%gd)rhtO?APLS60EN6 z)E$PeDcDGKGS11o)Sy5AK*YiY8#sLJAS2{8-9SL1yPz}qmTTc)!#XRQybZcg7sRlL5@@C6LSTN)7+(D90Aa^6y$zLb;66Q z>agyJ2m&5K82nmOPSl{1rWLb#EJQPELc<}|#U9kM79uXpdMAp&oC{AqoQyQjccH+a zn-?G2A~CU7{&_4WmIpA{Q*3az!G_)IHL{{Uys+%9E+FA)Aj2ljCV>f{D`IwQUC6+N zHLXoMZcJ%E8IwG#x##`q zHZOnO={_&-C;EHk$@@BCyB3=-R?*=+BF7_$g?Twum%E|?N#pn0j+`c^Uir0?tv}vl zXAPouCu(Cp6)pm>bUlZAYBYvG7j;lYM=1f}u=m0xY7j+kpG@dCjbTF?ILc*|T|@y> zp)r>}5UA{=EF%-0IJe{&Z5iwl+!>hM`6?gy%T_j;-#9_s8$oRDEj!B^YLeU!cIzx@sdNFi$oOg)Uoqejibdzhht!B>|4t#f( zd{CBz)*f$JU5lxq3ShB&BA%znK|>zW_1@9dmZN!mriV3crJHs5^uqdyPgvcOMTr?x z&5?D}nYgH9h*R!fv|>Gsich2zkxlG14moCA$TD@xsx#(#^!gCdhCZ(A#EAhz`a{Cm zGd=g{bn5gb=KMyqn+SRs$4IOvkz>-t-eahZyt4U8bFmE>1vx}#ku;3*&v!CAA|Fg|gnC)O_&O;m zS~OH$P?&k8U&NR?kf+`>O2#N_ppYO5L$1^S@!XV0m#TaM_4P>}gRrhVQ z3)hCW8wC8=Uv(etW07a$uXcjZHk0XNIj*a#=nNBh${i-+n`qsP&V4m&=U2hdhAMtE zG-{16%YrIFq=CHVPah1!XTH0yzfuhH?vWJDm)?(Zx1^M3*Brs!554TkI~rXb%5Mso zgx)W`ibwL{&nsLjU9%M^;oJ)B;uX-~2RIzdMN1;5y_z?1aT@O3068X<XWa&_@09{xwoYWZ_fdA0?Dt~!JuIv zE-M;$AUIb@JdW{G5RQHK9h~38LX_Ui=nP&H!;bF{3}=Y9CWf|1a9FS<;l?5<7%bG` zJhl!g$%{~}vOK;z8vKemLa8rf1`o#@-v-z}&(||z?Vx|;_N4C_6aN&WBi}QdrR>=! zc)bu>sg6+`mn_KJDW|gVD>Q;LxCu#?^x8<*pI`FKW+lMsIYhv>vv@*miexegP6~8O zB0sKFw3HZ6!OR(ZwKFdw@@F63Cd&=TX74&SPG#?rh%6vou2k7Ckaofp)KjUxe5^r< zN}~fXcrke-crH<1qJ}kQ8Ry*lDr7U zUI{ik5lg3(4Feie4eBs5PimB6$|#RS@VX>7yX12a{X|=A9X>cbYS$E6WEY}f2kkX# zkAK0x{DcYa#dc6?+tb{8naXW)?aD2PDTGKuhnmm%Wz>VstifH`K-wEIKb_B^fb(*!Lmm5<crYM9z>J1+>BjyA>xlMmWSV4^d=N zZzzsRm8~k$8Pa|O5t|qeZk7YJ#m>IrBYi~n;$h(q?R;e@32^-)=d^-x9=bv{h52&$o+fxXd+Kc%UzeQKv+v8 z{JO=ovhMMejOsDo5qjgF_FhGCf^EfznhYbhIHfO4vt}~5h6A*Xqi|Wd6#W`C_keU- ztX8MZr08aI@;RUlJ0o+k%}vetDvBOw;&=vTUyPWV(KQ=vM!htG!hJEd6&1bhK@&#X z?9cT0UbX@huKf87LrW%D^C+NjYP1Mz44C;honWPOCd=rEDY$BkxpWSR3FJ#&iO#{@ zP$47)ss1?QRV!Xo51gk@yJ>XA58vs1#r#fi!FMRU8S)4JT)r0<-Q|L%C2@V*jb_o} zhPW-Cr~bIDkM;Z{ViF<);^L=KoYt?F^%lYRn_sryzZZa%Np_pxm=K42rOr%4e(xC` zlNrr6uVG2%lHp6(b2P!*OyZAO#AA8b7eB3u0=7q10%x0Igw)q|-7qCy;F zd}w)&ij^$=Fmee_RFUIAi13u9&=f{g^u7bkiX{9Wr3Iy=z58GZ=PNy-n>=k3&i4HE zoV^i!*3mmsQijzkh+z$iDbbIj5jN})xA-v+yhR|yT0L=JNi1#(SQxic{22@ONTGy+ z8`awI+ilze2Z$dVX>F*Jep#9_8pZI$zs7 zNLd`W&@NO8wmU68`DHh{np%doxE+s0;K|5V5N_%;HCUSdR@_Y4*5>e%gpI1)2#V{2 z&**TcViCd4^n{6ILcg9kBMT%jz~0D3IlJJDtJGhin>lfcVo?rEWI!xAy>k@UhOu^} z-!41%CumP`D6T=SkvtC($dQN6HzuurkfmChgaV({Ha8DU@7!=JJ%pYtts%LNW(kIN zN>~KaBJ)x?CiL5px{1(m`2AVApBDVH+GJg*j_A5yVV7Cq)`=h(IC!uv$vEyh{a~x| zZklQI)$X1DRk%jdUS38q&taNxsaCL!%{~abejfPo2}sY6h+q z=H;L`#`C)$io0J2#8w%}DLWU}sHv~%XS!AKqk)kT#p;wA@GV>49exgyq$$cafhAe; zHCj?%!X&oLj6pjhP`eWV9ayBm=;rZ zmC|2F7Q*Kbiy%~ct7tD&lm5wG#Z62m5rX(r$*0ug|$N zA{l@G7#v5+F{dDWhiWb>5yV-10O5L$c*V`__i#;43?kmiM?dPB{fVU3kJ*Zy3*91u zvxb6{u%@I?m;y43vWUC>YO;=`?2R!U5k@4z2>T}w>8yhN?LuX*#cl2xTkirG>Rk56-c@6t}i6dTtW*+Ff81NZ6AtPdf0YD?e9#HkKc`T;lHSnlDh zpt+2X%qy&s7#X-u)w;Tw%z%(pyAg%gm!(H|guAygl$$r3_%?7)iesc&v`P(T?JWLvVSqV7Jh=7stY9`zNGJbzJX&PXN$ zCMj-43<^HUpm5@~C!LXuHqk?fJ-x-A-ww&IDi3GbsHj)wT29~sU7N;=Sv;QyjR?o? zi5z;O4|Q=TJ67^r3XFU8My|jx_H~`nI1ULpWkt5ezV)7ETx+?Q+@T2`d+1n4X5U{6 zz)PvDEW+`V8y8czeFo!v*Dez?bL6w;KW%R;uPn||-U8c_l`&Krg-s9>n9lXsV^qwv z=DialHKvMd798j2swj6$qO(zA-AU52l<3b2v55oH5lIq$J1%pS+&NwA4HMd1bYfDT zg?Ui?K$lYPq=ghRvD4CoMQm5H2qcOD9QFytA~a6DF>*G#m*+NXC_35gbf1$kJjR@l zBN0!hud!(W7TtvITHVD8*oEL)SM6o_)2kPw3PlGY_WkE|W^E3WozLc+;GEHuPcRV& zI!`R*TMF%?m%zaiuL$JphLW&)iU5!(LmjhXd3lA>Tppphhoeh=8~<#H5zbY!dbt+< zQG!FgZG1>APB_H`#>cq68T%U+)`E9&!idcSZo;f(b=rY{cXmK|He}j3q)f=&1ge4{ zVFGtaHmeaQcuWB3VQd-qSqw?P{65{VN$P&@Pxj_MkxM%nf=RLHwe(5sNc4L7)!~df zj5_x2dmxq&fKn z3IJSwdc*4k$tAQ`Ia<13C!XY4vz5K@Sd3mwJXS!nos*N-zQBSjD|uPg&hn=}w)pIm zm%lXQcyY@n7-?1oyzfENq!G~DSW|Xf8E@guj39`peWxrr&GE*g7l#N7jT~lE@uHiH z7!d;%SeAryzjw1?RvpwMnK09Ej)hCf*W#Qa;Lp!5X#QOw>I4?FI0|U9z{P+CvmF$a z_5`uO_fZg7sBq;c&v_HG!5a|ER0ZpeO@aXn#@OZ5>aWnWnuC=R?X?3=#+deZBC(rS zq&0&B)o)eI8T)Kgx!kFwY;jAfj8<^}=n}U)eTd#Vxww42odrI#-q^82T?@ooLu(ZB z?&Tz>;5p2`K{j+yrkt$$A+4*P|xHw@An~@fVaN zvbn)nmU=#Ctdc{3jun_0QBOoS;!J}QqltBU3>hr>e?p=-e4S#gUbovR4OKMM@L^N* zIZGd}WID(N1XixLZw8V}G@h5qyNLwLiL*Z>wDoLu64VW@99Z8mKGYIeIqe7XD9Xru?}W}$ z4yIysS02H<0mnnM&*iFa$L8@TM%8@NVQy4fr~h^YXAcSYLSAuqGC7CH_PIFSs)YBI zUjw?R^9xP{YmcKYAA)`?C5puC*PZfe>3)__T2aIm7W;VfihBtz2bv}e z?jTw{7AAp*N_-$ru0K~f_a-#R8>>&|NYs?nB1!WDXUc@+f0EC9 zYh~7Scmv~F7VVTf`aZM_18f7imH1f|D@lF|z8y;+ND*VQ+bIgZUU^vRzJO z=rjAuKJ&R0rX=9Ef7pHXHJN&Uoz202+yDz-;!#=+|2VLw$>+K_CAoX)hYdlQxBd<+ z{<%^2`Wd(NIoWqS7E-ZU=BT~>D&8y$Z)2iDp@(D0YH?_3VYf8$_PgbBOZ?e(ai?@b zpy=JLUWM4nN)J>sMfF>Bbb2ByIhUgt?Q#OHv%KPUK$g>lhN4wY=g%ny4>Tt#UNW?vQ_v~h^wZP`jk9E4Up(g z8_^3_uV*6+X6*Gfi%LqQ}YuL9@6`R{JSe#A@5R6UOnUWwvQgX&eF_!p_$& zWtrPM*y9amSRmgn_v32rV%8&6pPJGu&_&v9btMRkASeT!a!N^Y)A_!)VXwo--MxzO z4-ecu0VGJTTOXIJUi6@nGX|xNaK}tv6ps>}P*2~(Ks_;quX+cK@zvY`WC3Ac*%jVh z*tZjIS5A2bksYpEeK(3+c5&)7Pi=>LyZm@$IMf=Evfb|yxz_0Itp-cWfB z560MX{I5CyV!_}kkdZL5V&Y=>e&!;=kg`wv*Jb`j$w4RDfMyIzyNp6k#Ehre_3g99 z8HhBjkSpPE{6!$C7gL7HdAC@@{VunxVpm=~zIc8%$rR2%5Qnvqp-@^q5hG^rTyg^M z)fw0lm(9srq?T%xUJ-Hbi+KQqA~mDJJ&9zN&z(8sk?eVOE?k3+XbXR%RFB@`_PKsO zmmQ+wwA1U$VD}j`@2T=$hg_fmWzET|9I|kjMwn-Pm-E&v8Pitl^pWX*ZsdC_{&PI7F~tWhX( zu6~pBYRlO#nL>$*f-lV&s3&DDjlqwa7_3WgQE2+Mer~JGzmQe>QbMd5pl@hLad1Tn z0=^1)IG)+$4?yf6vc%@VXQVF<)lWZt_01$hN9x2S^Twr?MnO4!r@N9Ai>bNz^4=9n&8_B9|-Ipj(0tlvsSX6KW}N-`r3;_UVM!1{&?%#xmeP0 z-QjdE`#L+(t1mJEO}4l7bUNyIThCR=)9*ANU)nO@y1G3!)^>lYcvhdSiG+9iW0%?b zP2Vqrr>?3+Mmf$0PTtpVvQjyX}Eo<9E9wl^$B>8d-(4u)$n9L19Sx_T7FQuDM0VRQxKTo^BK@6wh-c${d_* zNbm>|fTxB6(i~(rBby68#U3xqLUbT`tNLQ@OWYl?e5>yTY(pDPh>?s(ic%N1qKdLa zr@4iJA6j`v`ap{)7I|O@e*s^Kix^B45u4fIbQ|Kl{()*H;@IdV!w+k(d3-Agy;S(L z#|?omDYYI{bcv*r!Nf+xv9-f=fRm{lRmdn`ryVuMQXT~?rD0%IJ*Gx?bj(n2;js}v zBF3A)F!8;C}bW>yAb0Wq;C0A|0!O-D%Bm0L-@-FPpmi}eFh_+mqes@8ttAW`>+t#BKBGSa+BNY=z0Bg)l*+Bt{Ue(I&Z*F{=F2hQFhfVN(C>kQFThq^wi&Qs;YAq)93!&(^p~I`uCVIb)^Df&TlSUttj;AU4WE|VWb`~J`c7CFj)h({37LZF!NrPm_fV3eAHG+>In2V4E z>?9Fuh9BA4@e)X$sibym@`rcop2=cnxhbW1iO*V)P!`1(`0 zy_9^E(_U5wygSCg3YOTY4wmq8cuZ1(fWZWXP4;Ze*k%+G2&}^oAb}R63S^gPM1@Nv zXwc|4t=v_r%ugS=o6|u|s-_5G)T*=!XvSsW%pTNJudg7s(;3|J)rq9uY}JJhe69}g zGh*F*F8sYn{(JwL>}*@|j4>eVDZag-ibe-D*oBD%m;>WbnA>ji2EDDeQT5gSV9w8M z3W)mop2u@jXbSC{b0XxarD{nrf-GD@g`OB7eqzOH*`Q^(5&dhB6MsnY&ewJ7$|GGN z_~-izwI9~*w9QSTN;&DTXrntAYKm@ZU`5cr{Jb1|NXRQ>iGiwpC^u~Zgb~1~2UGTY z2B_y)U57%Tb}HTI-nL*F4RzDr=b^vJ$S<<^4->T2Vc*b;BxzN{w{zR6;n#2ux)ko(kfP5qwlMn{3y}T4>8Z?^^&kS=EL?tVhC=D zt$rJGja}FMDNvZJB&e1*IvGq$ID6Bh$-?K?04=#9l#tY{+Y~N^;hEv_*g-|iduxeI zzBn>EPb~opBv@tw86Yc9wsEVCQJ$C>N$Hbn&aDdu+OPmo*CP;byja-H02$$l{^*Pp zD#+b(B^8#fGN#lG_ygz&3h&cg`!2c4)ZrT9l`sLCkct+7&{2U2O~Tkx0)gmHU+fR| z++m`aYsuzd!Zb9KLwbq05E-F88A!T)6)IfKk%BJ$UnYdjlzV^Z+QY13G2pi-9RsaI zD584jUh7vrBJ*EAl)gtKU2;Xq4$D%{d<^Wsi@hVA4R(Ipe%f%h9CM!D?Ai3}M=a%$ z@`;4%7BOU5H%V^QxP+SJ}<)kz;9LR#4kR&uCdOv{um{2GTSS@bk_Ef?Z3u!rt`{{Z)BH>zsu^NVLCDj@e6@$v zYnvK#n;xYTS*h{%p)?GD?vFRDL>hYf%=EvCvEPrHeQYV`0g`{h%ieyEU!XsS>GY`k z0~^d;Af8@aa*H^V;W_~F`Bfy{C1;6d-1=D*k*0>m3@H*8wS*`q7%gxdm;25{Oypbi zvRqOGpyf=RZE^jM@k0^1Bk^r&F)i) z%_vt0rqi69JQ&GSU^JF$3az5l6Q8NX&LC+BnkaHnF8MyIPXDK$X>#wTXOi#`XvAR& z95m(dl$WQ-meMvy)y_59Oe<)7?_Sn6q;Oq|2%cg=BGqy52xaANDxt{lVL7Ilvf0!e za&@L!l8E$f=fbvbEM`frf~sE>L+Ax5T0;~O{Mt8m{p4}o1+@&+?4f5083te8FQ(jw zz@6T$ht<0&ET0M$7H2-`XlK!nvCFi}wM$WGQSdhphhTJpF%xNK64ip@4G~x&Jwc2| zSK_t7Ec=cXVKNW_Cf0s10j=+}5rm=eU0%)@?ZamFGeU(`uE`pVKW#Selyi+V`Ex)@ z=>ewxH%g%ZFi4du#AM+(ap5?$Y`Q+HjqLG14rnLoGv|K#(8e+%i`$3Ys#Y&`Itc={ zm;tecxKE288=%+{SVTMH3UdSEA-rN2o9sbE_t$sZfq(0bZp{X+9@L6c+j4-q6l>9{ zd9U_6f__-n;>!0+sf>j0jx+U^ShmpvnN137J3^;sNC%8brdU=)m6hWGN%DgWII>hB ziitA2y2a5}A?(~o_)^fu1>9UopzQO;=0`yph!9!Yscjkh%#5RiV=QsxvJ*eTXIoM0 zJ#sk0%pY=3Ig2cvrRe~AaL*H~_n1wS+^)yfXGY#{9aG%Z@!hAfy`yJ6!+8~sRK7Q$ z3%70hXV7m?mDkg{t(K#KkTdQ(uqTFmexq~GeCd$15hh;&uY``3V*y8u4!>a-=zHKl zOOm7O;~v>o@c86QGLAyb^>uJ&v|Xf1ll073QpFm#&m}d&D9XG+IPpycsl7sL4!Fiz zq-FQ-i;5r3`i%8YIo}F>lS>+9<wGL$-l@sm5a?5m1RKrDu@%-(>2EVAxWzE+-(7 z^#)CzN>i0k=BF#;Bx7@?ZeU#Io8z?9+V$W`yc!0$^dnoWp2MV-F!P6eBg6feQ?$C^#YQZ9u!!64LSOb{(~)CwdGZJZ1vnq1`+C%BRgp%DfF z&yw`dC~+ouzL^lBxSqM_qCU5wGADX{7dDVzx8=@5=JS4v@MX{&0C*#`t7`PhK zf;u#|w0UT+6+DD&iT6&;P*}vZ3@_`h=yoh(#$zNE>lN_=*#{+cZN%9ah0bLQxS1s)o;J&?34vKp7dkO z=BY75pni6DZ#%=6%K6g!c{S(JxXVfGgM!Kl8FYVRdCDZBndSjG^nDq8x{nwB^y$HD zQ+r8^+h#eE0Z<3>o^j*rtOF7;Zc6YSq3ZQh12~zLxma7%_JN#(mt0%A?JfAjXelT~ z##a4M(HZMftI?=t{puH1*N><9ZMAPc+Ut(DiCXTV)Z5BUlINUv!#PIGYwR+(2P9xa z1=@EqF=$YY#K1qrIfdxFD+kneZlsF+t%fvSp@z($TZ~WSN^hIfO1vXroj+I*_riX1 z^a6I_th7BhBA%DS_6?IVpA~&a-rTKS*F{}iLCdqCI|_=RZ9tc`_%t#DOQnvBv(sqI zvFn3e%-c74CuBdU%k{g|s`^X(xRZpH2m9@k!?c;tJIrgBD?RF0?N$VzIjfDKQoT&E zotzLaxI+&%utZh#aaLMcJ=2B9ok>H?y0OdW6737rEyzUU2`QJJ>VzeoMzG{^IUW_$ z3_jNmtdfx5?jvii4SrQ}P5WUtZ#>48P$!*Pm->BzY_Zi@^&fuQr;N^1a=e7=2%4e6 zvBv5-94i>DONF70U50H8t_H)r2+r(?%^I{lWRg5C3$M#W+^G%7N&xX_!W4~8&;mbW z7C~_a;6+OhOhY=>c%B-twm-bPr56n-K|3l@99gA(JgByd`v3g5+}%`$~;caYF%lg-)Vk*zpi{_Ys@d(3|gd@Xff zbY<*^(~@{Vqbp_G+2T_dyOU{Bpe)i@gv~NU&Q-55gy%eqpSW$vavl-a6Jkg! zaY8@P*ywG(Yb>JhO!b8|=%wGqlFhjTeo)xOWLK2hCS4RL47e54di(~Qcql-db^42C zW*G;AITh%!!O2T>kd;J00zmPI zu)A@5V!Oh(V}Q}ekR)-j=(yoK@@5LCbbEKMs5xNY6lU2^QyutuR;i03JVm-zNSY$x zL+q3>tSTXSdwIbmaw!^}gu=M@%+U~LHO39vl&_MwNoK-%`N`&*u*V!U#mss!V%MT! z84uTsoSj%sv$NIG@1-G)iDo@S0~y4`b?3Cp>+od*MNMF3z9Hg@?I>qD4DSo#$ND`H zb`WmKb#w*YvRm@oMKOwSS~Xn%2!u=+v{_G9)}n+1MyAuoqESZB?~ynXSUnm9*C~WW z3{Vha#|R`5E}e3bRU6l@7qh5px}+i!crK9eO&An%2>VSmU&e0}RH=l%_EC$GEu)+I-O1Q!wLuHEQ6(h=Gq z!c$J52r_0M_E2P3D_o&*W`M4_4V+D!ccR-@t6FLJ9aWy6=mWDl!Z7rHB)oR+o;q8(Rk`gosJLC1ZrZ$LEL9!8^ww$;!YCpm;s&+31^ri#WQ6oi6_D=kz$mGwx#y7`)p-Cf zLgFU;Ku^cKANN#IBK(1pbna0NH<|T!3S{UbNvGm86xhQ=)dH)Z!F3VxQsYc`ZU*72 zY-mg-MIhdASgaL|YIep`1ZrX#$97rZWYPRw&rgmPy)-FOz>uih3#0id@cd?xluxmg z{Wy$6q5Jdk8$4F;gm)zTz4y?PAhRNapY3kDWXef!scu(U?$V<7R-pG(V{CJ=SKP*i zyR~Dz;sX_{etBv~w#CG%XFe)#S|wq@zy)>reJ1a*eDVEHSg0BbzErRuGbI?w`QdGI z%6pX(>_<>xds$|>Hs|P^X4rCx@jSg_I7THTBVMyLuEjGpic{!yrO0`dH@0)RZ^E;U zNNnVd3{Uk_GN1S}Vq0KK&YIRq&+?~nTPc4no|HR6VFU^x5(HU-X zIo<_D@zhi?UkI$US{&|9=XO4?ojxsdRG0rgWLEQikyORSi~76D<^4W+%et_CQ%y-hHf$lU{M0ksuRHZ?j5A}beEl3;=}M3pjYRgh1^-a~hO z$3d2ws87V6CF7CIfiz?o<>9~ORmK$nAq?uLc+>5vtG7q|WupTA>3aoEfM-xqZ5=rH zb;ie}Zs+N=L;y|LT?*7M`?*A{NW+k$r~B4oosSqxnBwjNyyUIzd{~xCBQ-T^qq6CY zfDlMXbpE4*&W||?^;IfPp)ihDNMYZQ3?cTlE>5XHH7K{0-h7G@oj3|f*%ZM@H_gMZ zZyH4&?vl*HWdkM<6%uu!kRT6%f|DtpAm7@B zh6(h%ZFD?`1Q%X^Sq$pbP|Z5ZNBw1DzGdVupKDRNXvNGmaotc7g zn4-|Vrx1^&*YJKm%ehhP>qP6paw3gb$%A&G^qla55r5#`biPA5C_~R=4Mk{dQGsQS zQe}{%3+D){a>~sz{hDXhJ0VtOvupg8j!{^=aZp6X{&JEef5BNojAMZ-ylq;5ZN@We zS~$%-TYok1PX0)sE?CA#TdS9^UkKN}MAu19UQdf3*27(G0_k$T^!3zcwJwKlXA4(pGGtr9t05=~1- zkE^Oe&t4=)o&9&q9t+;kvzrB{-jq8p=RLoGHKzSN?_G+VLRvKJOkkn{Uh(|tHyeEB zt$bE^r5lShnuu>4Y!ofz8QGW^h{Se)R_rjvH%6|%ujXDcJf=#>D#r^mj(Cx$Ujf^o z7{)faAEX&5nDnc@^lY}$qBf<+P5nkkc6rF_lNT7VVL#TAONpVT>p{LU_FFAjf4rKIhh#vj`plu8l)aK_e1B!P%Lqu0i2d$`(@zVxCpEXR)83k zMGh1&F?)<-D|zXC!lU7Ex@*VdMfw6iwX``M`r89^Tq6QA>(CS7$$bV=sq#~Qs|+n* zgS(J`xWtT~pSci^S{3+JMBCTh9=^-J)=)8GCxM`m)Y|b(IQqCUC8Gx} zoXJ(87t@pWnZlNG|F#=B;_96e!2ICt3|AvD&(UK!d(crCW!eEbm&Juz2wd*RjEN+e zw{)NzA)!`Ig&05!KNVYw+WIl?a09sZx-vz=GO@+^pjKq}+!k*|?N7g294?=~QlMBttB1ev;~^yx3*3s`s1`Zj@{(vo}RdCVy3e z6&+LFP)S%lnhX^M{X9#|6ogEEoWlINtgISli8;QaiuLPKhqcE2 za(z$%pG844dm|q&Sl>o*2nL(06-sa6pYddHUtUNt`we^~e0@JFte*GQ*>rD^gO3Tx z`AVTFQ_g-oU_IvslUs6lkj_nu5vO4mRm^`zwQrssJvfkQ@t3(mkI|@J(lF^L zAcvUWi$n9A5#$X3pYa^;c=A)(ye^jeg3V&lT;qK=&RK2Uf6TGE{a9yh?*AT!qhK zn0d?iSqL-QD5# z`|h)|_xbj@nR=*!HAp7$#@dl{z)xurE+vGj0g0akLKro z6^g=XS}e8|*xD@{YD!1x4&x3ioj|#oTFf8S>8z+x&(Sa{l3?V*^Z2IX-R?>FUgCzC zkoR(I9!06`Gp7yAmXi+}iylK)CuttG!v){8QV{DEmgI@-zR-dw0s4uBVFSXmJ4Uu$ z>K2-otX5AsZ0~U!dC8U-a75!?;9Z@>Q9W^n--oelU0D|$U2!zch3Kd$p9>@1bt4Ry z47-yi6|9-UL`G8won;Ph8DNCv^7B=}^MHRy;`G_3_mj(TI{A9NmRn*fJ946ggiBDu zFoSDn)t8}`@tqChF@{mY%VYgGVDw%voO8i;3<;AXT9zOkCnjg?E_oL!Kn2FWX3S-V zH=Q?KkdHNxqB1~#7zS>9zW4P*5vd4qrdKm>XhMzZHT3@Kt5kAy@;)11mRhIGm8I09 zV3}UQuc)a71FS^ABC*asqNEC`lSsj zD|i`s+_VnS)JX`#ulS5HZcsmG^DG~l5o-LD^wNtMp5}e!h7tjz~0GJy(_uY)+%^d0bhE@L9arRu5 z9uI2?X$Ql~8|BSJOT3kvNG57|pV1lxj*+LbeSn9Oczeu%iktDCc6eAo@Nbwl<9aK_ zgb&T-*T$M32~z2pt#5ipbk;nOI$5!&Hi8kwDT}p1+aKua+|no+1R8!ICm;J}1Q~ZL zl~~VaD)NhUId8R^+Zvg5-qUMHy5G-kJ!TnqZHvhuBaV(A@Q%~2Z=@izgipY5E(Bj* z@e)FI0lKpAhIoMDMZYPLQx?OB)G+64>U8%n_ySneamQf3IyDcAaa)lEgg9N!@cBHA ziG9WF#^dClGW)1yE2hQ8ep#1{IpwV_#T=P|;hWsRP!x-26iUYJe8-=@iE zjn8pX85{uRL1>BhRVA=0si4FN>V3T5duF}sOHuH)V;RAe#Vv^%ox4SFw-~J=N1t-=`*}10jnxIQExkYs8!pxzIKL>h08)` ze&%M_CYd&hh)~qxbwOB%f_@(}BX~v|V z$A+aLfCoW=aZfj8)D23^Zqu4X=gn_NDU`B>wD1Lbl&ssQFi&jcuHI7Nnc0ba!Dn=6 z`Dg@j9ne)qJz9g1IL@~7xmu%(_B}_!5J`2Y@Q6_w5FtHU#)JFGR0pnzH-e-l?xJBd zW(@7e)kT)WG$nGcaYbMs1G|R86r}@r!@j=~YuXvl1^WsytWj>$g!b)T zT#sNXJ4$0g7KfbcefVhLqs*2n1$~30|7q{EWx0Xve0Q`&(z*A0PJiY)Yy0_Da6md< z+PXVJJ4R=dyJ$7Fj}%3gkU3w{cmgRqW*5fDqjsebcE2igLKBZL^(LKLQ1-yL>$c58 z7)aYldx4?ed|~cNsDvxcCX}!+FE?@Xb1Wr&yZDBMEX1Q*-E6v|q z@JWoGxzWrPk%vE0lBW3hVuC%*X+M*u2A(LFc^8T6`JKfQZ_Tc0M^+ZeTh4%fDzSve z?)nDhD;b(DDfoWw9L$jJ_!3YXh10hf5TQ>(+r_QTn9TrrXHp$s6^!sozc^BVRd-7_ z=~*d#n$)su@{Dk)ZqBhlXB$61gm;$;x$K##wFyD|m7JUc&D=QGo%&lavM*yS8Y;6r z8&2}*C+C7;!|*h4&)5YWAED1R&quw-trMNFz`N?H6G}>3i=QYK&fEyss0CHyI@U$Z3!#YUlBJ)W z%?Dj|S`tHUoKb2%aBQ@yz{MYa&g!|BfQJSJwHKk~y6LWndW07Si z99RQnG5w;1Xx$+)r6jTZl_Ckm<%IBeQ_2q*e;UMT&`5W}PHW@au;eZ>6>UF;usZa_ zu1w3z91FHVJ(S44RimoC$yNj)m9Z(X(B__u4s6Wrj%gUK+|}G2F{uwYS!W3+Ow!y! z`vzt&b@Qyknsy^-i138Nu8$d1BFbzdcZKf6fiU`H zvn2P-*~2y02eM*?CmCr*XaoqcHY98YiiXXWqDNtEw=;DV%6USJ`=4o_{zU8;-$USw zIr1TbFzge5PMp;NWf;K@0YFOheb*)%?`G?lt3i4Qt(6NIL`flaSgmeLrMuJhQrXDW zE4i4tIW$ZXbUIDGHZv?jGrKGT!<#a>in!cP*y?14MGGG?s#xYSVn+49R)J5o?D(%Q?` zx=vZ6UCYt5wB7PAulknvpnwJb%ijzWeD-VISgsj9Px;Ajm8+`nt)qnPubwuU5mCqA z6DJG}kNS)c@L+0)XTj{qwAk}~%&%mvu=fP7p~tqS>~GjD9t`Kt3#9xuFonr%V3n-# z5JbWeeuwU}@8@LT(|Bn@CKjPIfT1P|(Xv%9(n?V>O-ScVL3s%{o;!RWNj7um9ilb} zgYprN0H*;D2}ogF)-)OZ;cz&Dej7O>%y7Zn_-IXP{?kwJ4HU(d`FircYxFVy(RGvJ z4ItdrXDJsqJ>93Wv?b-F9&SOOAHs7m)ZZ3mGnfW(* zo17g9MN~7ghKNB+b#Mc^qfd(JI=Bnva;iHSvGf`?)aanFrKO2TY5QY-agJMGkdD*0 zac%Y@XWL|FB3f$L^TSA;mDOM7r`zt>8y%Ypy|eQJpF5QZy*f_5DW90h>#+LVOCyor zc(^;y2`Vu)H}`7a$;UF*evlq?-;}`}KGaR{-#+;Ycvrk@#;wy#sVbs=gdjRJ&mMik z4u`xA;CZVL`|vuw-GFUr;C=?}3mWa_?b%}%>QFuRnw0H$xV-7&5kSD-gzR$c34sD| zX>`9qBgR^3O<4uPT1@Z74DJ(Bwt3C0!A(t?2pI?Rwq1^GnzGg{maqZ*~t+ z#Bc5O`{q7Z0NE>JwKz5ThN*pED+)f!$Pf#p#HF@9O$Q$ORkELYv##U&!)_MHH9ti8 z*7{_}Q`vCyoj%<+|JX4_UtK&QcWh?m+4nHWlaLb`OA*i;{F3Eo3 zdWC%3=GTVX=+pc1-acH`Be%xgg0@Y!?O+k<96&#^vrBQU_Qd!k|4jKtC>o*2xvoFL zLBQg)k3@e$->C6!IQA>c=&kJgVA$?Z!}8B@ z`auUn`QhaeLG$VmKTL~=2y#bqlm6ad9E<4!uw!HFNetmuNHDc>k1*Pz@J#Ny6Z)?& zwHS+p!2-@p)d-8qVDO8gHFLZV@$pAu*qpF!ujkDN2fQKb$?^IL*%wdJz2$a8>gM=% zuU`{dX%=2(Zj^4QJh1L<&s!H!&7Wk;8akam25o{|SH0WcvU?hXCWb#>`@W8D?0p1( zP`#^u5Oq8{4Bmn9LlF|bs!^T4t|YIYJAPX`^e%i+nwan?5X})vFT8BzupbF3Ka&AM&XjHHi|pYQcrGR=pG@$sDL zo#B@iXy>?^C5F$fZ-9|KWW_p? zfCr-(qEmlIDx?k8ORZ-v=O#YNYSR0)FzV!S;VLbP-@J6>TKQyX4=&McP9DdVcb71{ zY~3hDz+zC@C3_39%cInLuzDhQW9YixK0&l2X6gOMmKx^h_0s+%3jX7#nGdMm5Sn0B zB>VaRr~2N`6_Rf&uNx$}9UaWvWqmGl3I{T*rN?Qd5ruh$V8-OB57t`5diHtV7X%uBZrQ`V-Bt zaIY>8hOn*=HA|U|oPsevk^~Gw(p*k9U5q`H@`R@Ur?V**PIJu>8yRzHU36iY5~Z!u z_EI3KJivOZ_~*`Wq!?W;>KB$@XPPxJxiO`D0`;Rj z$FJftI!eI)JUS}RxCd%q@xU*rffs-vpi9aLhuO<ajFb*8G_Jfcp&BZg}W zy`q*1G;G4>QcW8AN1}w!#=F|8tz8B~2LRH@6ytoF7!$P`4(6kS_0K9?hGR^^Tn1x`l&O4djYE6Kk#d8~*g(xa(xjzP<8IoXtB)mfCd)sCq6J}x+xuNYb6 zZ|;9{ou;BOjg4M6U;Lh1Ys+#`@ijHvGpu#%$B}>2YL+4%uKlfg|4x%#qH5-#E^Fek zFDUR~p(;B$$}BzN>Ssch{HicUZvOhfmZ<+@W-}8xP(7&~v^=l%z{~e*q0nGpqaj_DO( zaYIK!2)Vr4+Ibx9_2%PQ4VH%=yo5eQSqy;^1uH_5hgw6 z%2#;+gt}|qO^#(9WHLT@m945f2pg6-gK%_ARi7#(=;&m2>W1#6#{4eB?eIGt3qY*C zoPl0=?ZgP~li6{e!AY!}TOvq!&%z(p@6TV3CgXu$P+XW?)IO9xq}hMbz^sH!PG)4% zX?_l{O0M#{SjMp1HI(&QEQ8}AdNPgTJ(YFX1&e-7!}Gp80?S&BSv{Dd4>3J4dkS?E z-EG&~L43*T3Gsw-sJfs&zv9@m)g#07JX@3(XSqVyLed(rmp-vrU1kl`^1MxIl5hZ< zT-&kgmhY~7Y6`V8W-=-VDdEQGNszSS6XCgNfmrU;=L*1UX zQS(SmU`p93z+y5w#FdYL3S_qp-Gf)|-jRDlgY*FGHV`ep=??ck2P2e&TA7h@&roD# z<%P(g42AYT(UJcS)E0I!`iqUt#mUad$qoRrWkn6_B~2{M%zd%s_CYn8`m{ zF|aaoGIFr9{mor9b0P*d|BKZv@(=zt13NcR2`hk`n1PL%jggg=l?#XyXXj+(;sOfH z%*>4JfWLX%fAs+(D@|C5|E5?f{Y}gMtMDISaN)mSbZRD!#s;=PlK2;A18WN-VOuln zzd75o7EVq;uDH5|F_857ms%hX*V)8IjTpf7Pv-Vty8fE8qlLY*o#VgJxBsS_|C5RP zFZ%BPcSLauM<-`da|1^pLHBP9|D}wW<-b_nG6w(Egav3O(|;k&|01aW8`Rv<#pG|I z`QJ2fJ5^hYzd5ykqiliX;(x);|6-&At#JYpZT~iijgyU$i<6s!7{CHxWaHpw{~rWS zW-dl<7G~CeRdI5$G6H+D5p%L}F|smq5p%M#F*37ra{uLD4t5Sk_P?5Qu&^=$RRdeG zGBdKUaQ&NtzsLT+z5$i|U+ey_75}Xh8wU#`2PZ2BF&j4$oskO|buKOrMmBC{PGT;gyV-$@nwT9JX&?%p{cmS+`~}75 z1P85{x9YKEd1l9 z{}lc>>wgFd@WjFa-~hVkpXts4w3nNkljU#ae+q#U=3-_8(r^Eng@4-rwy*+S z$j-tJY{SkDoF0G;xSs$EBNsQ&_kXImfvb!Yn2&!e|7S-|;Cb;MCjWD|Kg!vG>C43d zjMbl6u>nt$%)pcApJD$D^2bV`SN;_KOJM!4z;dzyBMqFxzbF5HmHg50cMAQrMZrHe zY+(6!SpWYE7|hA_-vQb#bFx;ris%DZ?b(J&3POgT6WKK3P`QMC-z!Ho|}#&OqMG+ zM|Yl%Z<|*c@ADl9JAAnDPwp%AF2w7;a=piKAq{R7czkpJ?)9h|GMA_4*|yAIu^uz9 z5v)qU`L^?Mz9D$=An&5}V#I|rcCc1jd1KZ=f3JtNvK;l{aCjD%&GfP?E58slzh|q8 z^wM9881Y^cZJIFLmfnBnsq!+X_b_tnZ$>yjx)HJUdP=T$bLx0pCM2JJQD9-SoOWNB ze-u$D$lD0*~Rp~JyhM&y-IcV{I3sow|KAO zo!$TI!^JJ{t6XRA|N2053-v1O)AheRe7K8daDli&G_0LqY6>8UG8WHB@i7pVXUOX5 zHST*i4$`!Jd_q>v3H#6MoPXcZ;s2HKz)zaLt{?sZkOyvw!Vciz{BMZtf;Jd^^w0AE zT~)6M{)E(F@&sd3)@G_<^3)7+a#0aqav@M~N#EE&@>qM5p>7&T7RT=(;natvWo6qJ zD+pORp@)@~Wuujc-^(i(!dnQbTeoXOcs|tE5|C%#-Zw5DOs3bW%F4>tJ?@TmRN0`( z0&mUX3>9?F@O|Vic+=%YhBr3NEwtuv;~T6BNdu)fs~aoMoF;DQ$Am&>d*3>ff|Dww ztOT7wNnCM7Rx?cKxaiN`))Jnezl0!{${d}V+xXlJMY;u=z9TM=UIc>Ph-4C;Gxd0h z9F%t4HPkh85v!a8-$SaqHrA`L!6lKYd_&^C&k7<%j_%Cxx+y#avz>17ihsH8mDS849-^iYAQ%`k zAC(>DM;Sb_p~sZfF8(ih!$tCS-OshR|hYX*)|>S>_N221+gv6WrV?*L#9J`o(N-WA*#(fc!7{99cD}f03y{6DIbCt zOvRwk+R7w~GyHMer;D~I0r7+CP^`^*H>{4Z!(o-$PsiN$5bIw|W8thpQ7?H6yGK)~ zF5Q0{;mHZf=LqICrsT%?Z1PUM$UwSdS3p^~VH|7|J252;G!0oeGKqA<+yy~Axi|Y| zoYMQ4inlN^a8Pj5uyo^HIJ9mX8f8o=MoYGi;To`{M?$vCmBoqq9cTpWCq{?hV<1T6 zfoIR`T(O%5UgR@%5*eb(eq6(ojGFw&5d2QFFY!<#*b;-+n#~ zqR@xjfJm|o7o7saAEZ}mt#@8=iW^>PZfD`gMd=}Xo=bD^YM_$X5M$1Pu^x^nE6&4{ zHw9V@0eu`Cy-8D`0Wy?00wV#9dWUl)in)ROrTi!G&qJH%n4j7baw*(U9gc>l;u033 zeF8V@6{$snE|K+E$f@62T7&&7M9mvtB%g_VDHsHFS{vJ4Q0yx)?D_mt`bNgT zj|@tE|K;2gqG7Jh&7Iq)vvjn+S8!TR;vZqhsn%3+yTM6Nhc$td@$s7)uSND+?tWSJ)!X4?6}HlbWXO>I9pw0o%1I)u?Pwlk+| ztrpO$JVq*$bSfky8tAWDWY={hEG=DwXti<{S|Hs{4u{ZMGewmvZSYy^ows4SJZM3b zO}P@e`S%B=R$8XCz)2ze3si?d7`uE_A3?Rn?+^&Nm@REZ^U)q>g|@!?&e36zBd>~J z?=Y`W$MC=oE-Iry@)Oc#w#GnCp*G(;7VZXOyUm~OBrGhtj@wsUd^%6i=oazF6$%jRu5+#X3GW|ZR@Dphh zeS;A@Fu7fnaszq6e){Y5A!n*E*yL3DPDN^cPPb08l??1`Jad5xqR?!6MP_S{au_Us zvHvxhZky439Pv7fnQ$+QJ>M$tF&-glipsA0z`2vP=LvQLD5zU47DfuCd(TUpF#i*sI1`z61Qc_?o2*&fhqWta zaPpVnHn_;1Q8%hP0bTzY+Uk{G6F+k@4WLop`*Oc6s;7LjY&MOyuSd1L=E}qo2zc$~ zerGigykeFTEo0rIV!aY}r|1ad_1dzt?m_Z~kH&0?dEzev>-23%CAz%^WgpSMH6xqp z3B-Oh`iKNim}G#5F5gV{NA*J-hop`-0qHC#4!z$}6)3OVy`YfT+^pzDO2E0z`GxAt z=0=F%+la;&o$POOLtY-YZFpv@mBe6PR#(3$!nuAQJ8m9cZCT-e7#?=ZJAz*yZ7R`d z)YZ*PxD6d0y`2yFn=8f67)SrEm0m=#n;I2k&%r~4Fpo=XgX2V0vpgztpr#xKoWFP` z6;`H*bQ_wghDTFB5Oj?uUq8~mT9YpS^E<1waok5alPBmn6sXzjJEfC8`pEdQEf0)O zuk5YobuHd%Ab;fGzCjzxNZf&9^e54nPf!z&kWb&(9DaGaZ3*J-UEpgLDe~Po zg!;JLF&biH#ZHVY1UCB6U_<4y<2`$QxzR&z|9~*yW48fOr&Z2t2Ub6Qptb|4J zo!UpnNyQ1?Dl~l9B-Lt-Z+}fVs-){|e0gjIg*NeLliE-B))VH|LvZlyr zyGY>U&*_IyDc+nyL}?M>w8wb<1C&NO+q6c~hI)q7E`NQaHZ|jT^2C0xDA&JFOF)eu z^8?^p$c^`1W+$gB#9~!Xfij^lk^)JBZ2IYQ(M}2RHU%kkaTA{JC$X>d8Ihm$Dr|I| ztE;Qg)8C&?K5xj%W`Fnmytbi-r>n8ffwNIhTII>|UIq1T5^py^0N}!*+D(*PG;G8$ zHMO;CePX(o7c`hL+IvvGfmlj>XWoDB#@KsyGMNY@z49o*&MIZ>ji#+-Kh4~tCBO;d?ALGZHp;)4NvTaJv zD4WV*A>MZvb!@RrlAFM_EJqQuP1(XW?9lCPc_H*(x^n)q%OutZ`XMS><-)v9nRX@j zJyx$3t`9^lcRNodU0ZMB!K_gJ=|_*vc&;ocR|v=J!CvdxL7$h){E2n1kBi9jZTBZD z@j+`mhNo{{E?!<7{7qKkBx(;API@0FtyLZ0_n-t16MaH1E^m27b6d}0gLadch4#Jy zP4f@tg{qN59_xxWq9)jVpbpKUq@f)?8@--OYAgtmv}-Xpjyz%<_>q-$D*y1qj4H9f z?1!8gt;`-w{g?Wq?)c3-x!Y|GmEF1OjJM-!&#(6qpoTup=;6}hQO8V3 z%B86-Ye`8-vkm`r3;+p{a(lhULd2C0YOu zWx%tQV@x}QtPB+&ps6hE=or7Esd-9dPiNQUZbhj|g`z;kRyVH~5dl>`KSCE5`q?G4 z81#y!v|k&%Hhbp*Wb=l@uZLS&b-WrUi-W_DbE{a+phRGJTtv2MI5|84Vev%HH;$IN zAHu-Bh#4Lqx@Bwx4w7EKE<^+s<532ljGFqI=qqeZIBb76+>^h!+~V5%gv~Y9wG(&A zur%9D;UHi~N=iz8qiL(}XIMf8%&=CA1LApAx<8j*##yxUZrbk-^Ym5|af2y_Yi%G0 zU11Cb1ivb4WJGzhvOMeBX*uPI$b+6501^dWb>hu<_3_wct$ z3fD0u?a=^;g>b9Na4f`YR!YHJ1_aaI7}lm(A)8s4;o$xrrM_vxw28=_Sq$M!7}M^o z7CxT4$K_0OP2o#9K*2MHvGOD;2Q8Z-G3NBDT$rYw>N*K=&Lj4UcGftsB@`pm7Hu9N5O*sE#_;<~-+9afwC7V{C+m671q26FM^fw`=^D&hi0(vNU!18wy= zme4bi%?t#^K47-&ST@Sr?d9^|I=MF+4X*(ij|Cx{jIummeU9s9WFcX1(~*UmH$}fs zd|Qa|l(b|h<_&zP$3#qe-SmA#J2-a+T7Wat$By}q(%J)w($Hm6t zm&C?Uu`>87o{2&(Uh+y?JRM!`upamxV~$H5-EXdC)03)Js|_fgZkt}kovgH8;wctA zUccfmG#G!8ctL#%*jc=slDOu6YMl^JeiXU!J;hqJ(l5F3XFT@iq-J>qWA-<1@aB%Z~ef`yayQ+=3u=&X} zHMDB1p|~X;&fA4P+Al!?;a!-1ncz38b}knNa^2kN6jhqjUGsj)zWY8!gFreYO1`3p zRiKceZ7)MH1!bTJi#0q4o2yBNVQ7;RT$B2}l@8_1VNPlX#4q~K%->~UlX4V_1^3ud zD|$a?APQb_hZD?22d#<>!rBHI5i39!h|hi+P`cDS;i!)j?X zcq>Pcu3GVWYqOE=O=yCA#MA+xRhy#@Qn zx|OhaoBlz7xexQt$&(_gilku&O9pNMn?f3anRGT`-tLt2)bs@28Yy0iBFAm|)#6MH zj@F+9ObZPX^_Z3Yc=cXEu1GbtJ2s_3P1Y6xD3! zkbmK=z|nQhMji6|5Sm49VdWs@4^*vk18$3{0A#9NZR?c}aC1HAUik`DdNlNNcIeM{ znmU;s5VJ)22kw5!)$hbnuuy>D9c(`{OO7$zJQQ+*R;_@Vss0y(a9Vn}Pv0DiF+pu< z%11zKey!cgp}{H2q21G3qRlFQ8rQy&zRofb7*bV}uSu{6giy`^I zv)UsK3E^&@h~{p}RItA0DgQ=hy9FuKpr42MosdH75Z}yGE1Z!qPhpEJ>)+M_xk`EV~%@$pz$&2Teu8-ln-!#NSQeoZt7ER@TpzMUq*CuqI zjfa<#nKFA}zRHno%=BGYwcUBK@79cDY_ji{Ry~h1u!IHA4tSINQM?hTQ)0jPG!iHv z*uMq|mx!>HR5KO|RF3m<ELxxTRwQTQsADb$(Mzq=KWh@w{T8A z`)(OwWxYgau~z>GRr}U-t|~tvLw!u(sl{s`z~;sdaoi#~LOaYN3`W{8PQf~A3a_4vU8|k+8as|BL z_ERek4R2~w&ZXhn%lyvTO^MTqN#}^#H#IYQ-adWKd#>FRNpI*~HL!B39BS^FG&_k{ zPTAQE6J(;}PZ6MM=eGOuUH(}pE2Stx&J5=v7aE(NlGZgjlh#H(v98Nj!u@-KkD1DM zH~P^8*EE@+R&CUz0g-0PSYzk_i!Px%)upeKy0R)^z^zvLxjDFH4oDCe+<;zCZA(i|};ZJVhi( zhhJ!5KUaP695y=sWAgUREPqjgH3Y7vzW`=6^m9%^;0G7N4OKyzyCMOcSeUlaKe6^V@5B>xmsh`b@Bye(yP# zDWf*I-ky9^W>ym4+>Vr-^ercrDSeDdN<7sbTG>KH9W-ESQ%-JlaXlj;`IyezbVD5g zRv7(yb|2OFvdSVC)9SRGxQ|`LuT&&k@cQ&g?q-RTf(Z+ceY+Di`h$+@@^DWxvL^3m z_X{1b$8a6RO$m}D(lzF~`<5$*834?4DZS@R1_m-RBko%@<9L5jwfFc0R;`IPPL*n! zkJe+^&__X%jcMx=2YC$evIF$mKj9!rb@rrQ7sxH3U&e{Avdz%gIZ z)#klScGJ>r>zQ?-C)V)bkqZTJ?HP=%ur!&uj2I#xj^N z*8|VQ&H~JBcr08CtcW_kh>K9Z7A#Ge?4jII)cCk`#$HFOkVYz@4EemyEB1+s-T_<= zn{aL&hsJ;qeV@SSm0I);PEYmj*T`~gvgls?fP4qstQgEq#MzjEnG)y9_~qM;X<7re zS0jO~5Ui9JMoz~da)U>X(;oGnL5|ln)}nhqAKp(+w2;kH5l7cIm7l)>5wP$IrR;^?|mp(aIc9My2_a7p=a5C75)#V*N zD`&i5Bu9KWRj?DI47+KDtF%44p<9HL$60nH%5i=!DM>Y>Wi^$aqPQVn@#jxDRZy?HQ3$EV9P3==F&&#i?VBu+8$%n6ZXkNOg%Ei&4C zxtZ5-WA=%PTWU(8k0_+a4eOMo=axNn&2uWn_GLehuJ}A{?{52xvW+AUyzbchf4j

Udt;KUtzc;=sDf#|Z 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 E5A;3t9RL6T literal 0 HcmV?d00001 diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/pyproject.toml b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/pyproject.toml new file mode 100644 index 000000000..cb81b455c --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "ranger-observer-ceff" +version = "0.1.0" +description = "Reproducible observer-dependent central-charge workflow for Quantum Harness #122" +requires-python = ">=3.11" +dependencies = [ + "matplotlib>=3.8", + "numpy>=2.0", + "pydantic>=2.8", + "scipy>=1.13", +] + +[project.optional-dependencies] +test = ["pytest>=8"] +report = ["reportlab>=4", "pypdf>=5"] + +[project.scripts] +ceffflow = "ceffflow.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/report.html b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/report.html new file mode 100644 index 000000000..3ef086c7c --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/report.html @@ -0,0 +1,209 @@ + + + + + + Ranger Observer Ceff - Challenge 122 + + + +

+
+

Observer-dependent effective central charge

+

Quantum Harness #122 · Ranger / JunkaiWang-TheoPhy · an audited bridge from Born trajectories to observer-dependent conformal data

+
+
105 / 105production cells verified
+
61 / 61scientific tests passing
+
7algorithmic innovations
+
+
+ +
+

1. Research impact

+

+ The platform makes observer resolution a first-class variable in + conformal finite-size analysis. It combines random transfer operators, + Gaussian Majorana trajectories, quantum hidden-state inference, + paired-width covariance reduction, global information ordering, and an + exact measurement-RG witness. +

+
+
+

Exact-to-scalable

+

An exact latent-history oracle certifies a fully adapted Gaussian + particle filter for production trajectories.

+
+
+

Evidence-connected

+

Manifests, SHA-256 blocks, CSV/JSON summaries, tests, HTML, and PDF + form one reviewer-facing provenance graph.

+
+
+
+ +
+

2. What advances the standard workflow

+
+

Quantum hidden histories

Every visible symbol is evaluated by marginalizing its latent measurement histories through future Born dynamics.

+

Dual representation parity

Full spin states certify the Gaussian Majorana production engine gate by gate.

+

Matrix-free Nishimori transfer

Local tensor contractions retain exact periodic transfer with O(2^L) state storage.

+

Paired-width GLS

Nested common randomness estimates the complete finite-size covariance and focuses variance reduction on the universal term.

+

Global information order

Monotone cone projection and multivariate bootstrap evaluate each observer family in one covariance-aware statistic.

+

Convergence cartography

Correction models, fit windows, width deletion, reblocking, and an L=24 extension map the highest-return compute direction.

+

Exact measurement-RG metric

Optimization over all classical stochastic maps yields closed-form TV and KL operational coordinates.

+

Next capability

The same evidence pipeline extends directly to full Lyapunov spectra and learning-transition networks.

+
+
+ +
+

3. Numerical coordinates

+ + + + + + + + + +
CalibrationEstimateReferenceRole
Clean Ising0.49999661940.5normalization benchmark
Nishimori, full correction0.3701 +/- 0.05050.464 +/- 0.004production anchor
Nishimori, reduced correction0.4474 +/- 0.01640.464 +/- 0.004reference-connected estimate
Self-dual production0.5533 +/- 0.09490.447 +/- 0.001first convergence coordinate
Self-dual extension, L <= 240.4019 +/- 0.01920.447 +/- 0.001large-width direction coordinate
+

+ The two independent self-dual coordinates flank the reference and + convert finite-size sensitivity into a measured precision-allocation + direction. +

+ Observer-dependent central-charge resolution curves +

+ Global covariance-aware information-order tests give p = 0.531 for + confusion and p = 0.523 for erasure. Both constrained curves follow the + expected nonincreasing information hierarchy. +

+
+ +
+

4. Exact measurement-RG result

+

+ For the local CNOT two-to-one block channel at + t = tanh(beta) = 1/sqrt(2), optimization over every row-stochastic + classical post-processing gives: +

+
+
0.3535533906range-one TV deficiency
+
0.1035533906range-two TV deficiency
+
t² parityrecoverable two-site contrast
+
+
+ +
+

5. Reproduce

+
python -m pip install -e '.[test,report]'
+pytest -q
+ceffflow benchmark --output reproduced/clean-ising
+python scripts/plan_ceffflow_production.py \
+  --axes configs/ceffflow/quickstart_axes.json \
+  --output reproduced/quickstart/run_spec.json \
+  --run-id reproduced-quickstart
+ +
+ +
+

6. Research foundation

+

+ The release builds on the Nishimori numerical transfer-matrix central + charge, transfer-matrix conformal spectra at measurement-induced + transitions, and Born-rule self-dual mixed-state criticality. Its + contribution is the observer-dependent inference layer and the + evidence architecture that makes this extension executable and auditable. +

+ +
+ +
+ Ranger Observer Ceff · observer resolution as a computable conformal variable +
+
+ + diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/ceffflow-production/run_spec.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/ceffflow-production/run_spec.json new file mode 100644 index 000000000..d06314225 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/ceffflow-production/run_spec.json @@ -0,0 +1,2423 @@ +{ + "run_id": "ceffflow-production", + "run_dir": "results/ceffflow-production", + "result_root": "cells", + "axes_source": "configs/ceffflow/production_axes.json", + "cells": [ + { + "cell_id": "cell-0001", + "settings": { + "model": "clean_ising", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 20, + "burn_in": 0, + "block_size": 10, + "seed": 0, + "particles": 1 + } + }, + { + "cell_id": "cell-0002", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 0, + "particles": 1 + } + }, + { + "cell_id": "cell-0003", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 1, + "particles": 1 + } + }, + { + "cell_id": "cell-0004", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 2, + "particles": 1 + } + }, + { + "cell_id": "cell-0005", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 3, + "particles": 1 + } + }, + { + "cell_id": "cell-0006", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 4, + "particles": 1 + } + }, + { + "cell_id": "cell-0007", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 5, + "particles": 1 + } + }, + { + "cell_id": "cell-0008", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 6, + "particles": 1 + } + }, + { + "cell_id": "cell-0009", + "settings": { + "model": "nishimori", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 100000, + "burn_in": 2000, + "block_size": 500, + "seed": 7, + "particles": 1 + } + }, + { + "cell_id": "cell-0010", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 1 + } + }, + { + "cell_id": "cell-0011", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 1 + } + }, + { + "cell_id": "cell-0012", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 1 + } + }, + { + "cell_id": "cell-0013", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 1 + } + }, + { + "cell_id": "cell-0014", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 1 + } + }, + { + "cell_id": "cell-0015", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 1 + } + }, + { + "cell_id": "cell-0016", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 1 + } + }, + { + "cell_id": "cell-0017", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "identity", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 1 + } + }, + { + "cell_id": "cell-0018", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0019", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0020", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0021", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0022", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0023", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0024", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0025", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.05 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0026", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0027", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0028", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0029", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0030", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0031", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0032", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0033", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.1 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0034", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0035", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0036", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0037", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0038", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0039", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0040", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0041", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.2 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0042", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0043", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0044", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0045", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0046", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0047", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0048", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0049", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.35 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0050", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0051", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0052", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0053", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0054", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0055", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0056", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0057", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "confusion", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0058", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0059", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0060", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0061", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0062", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0063", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0064", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0065", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.95 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0066", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0067", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0068", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0069", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0070", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0071", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0072", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0073", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.9 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0074", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0075", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0076", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0077", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0078", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0079", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0080", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0081", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.8 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0082", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0083", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0084", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0085", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0086", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0087", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0088", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0089", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.65 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0090", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0091", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0092", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0093", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0094", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0095", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0096", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0097", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.5 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + }, + { + "cell_id": "cell-0098", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 0, + "particles": 128 + } + }, + { + "cell_id": "cell-0099", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 1, + "particles": 128 + } + }, + { + "cell_id": "cell-0100", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 2, + "particles": 128 + } + }, + { + "cell_id": "cell-0101", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 3, + "particles": 128 + } + }, + { + "cell_id": "cell-0102", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 4, + "particles": 128 + } + }, + { + "cell_id": "cell-0103", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 5, + "particles": 128 + } + }, + { + "cell_id": "cell-0104", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 6, + "particles": 128 + } + }, + { + "cell_id": "cell-0105", + "settings": { + "model": "self_dual", + "lengths": [ + 6, + 8, + 10, + 12, + 14, + 16 + ], + "channel": { + "kind": "erasure", + "parameter": 0.0 + }, + "steps": 2000, + "burn_in": 200, + "block_size": 100, + "seed": 7, + "particles": 128 + } + } + ] +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/central_charge_estimates.csv b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/central_charge_estimates.csv new file mode 100644 index 000000000..ee3851e51 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/central_charge_estimates.csv @@ -0,0 +1,8 @@ +model,run,fit,lengths,central_charge,standard_error,target,target_standard_error,combined_distance_sigma,stage,interpretation +clean_ising,benchmark,exact_casimir,"8:2:40",0.4999966194130345,0.0,0.5,0.0,0.0000033805869655,benchmark_locked,"deterministic geometry and Casimir normalization" +nishimori,production,L^-1+L^-3,"6,8,10,12,14,16",0.3700802276519329,0.05047267163857454,0.464,0.004,1.855,production_anchor,"covariance-aware full-correction production coordinate" +nishimori,production,L^-1_only,"6,8,10,12,14,16",0.44737965355153064,0.016435383580663578,0.464,0.004,0.982,reference_connected,"reduced-correction estimate within one combined standard error of the reference" +self_dual,production,L^-1+L^-3,"6,8,10,12,14,16",0.8069058069514213,0.39985060342406664,0.447,0.001,0.900,production_coordinate,"full-correction production coordinate" +self_dual,production,L^-1_only,"6,8,10,12,14,16",0.5532526284891004,0.09492854199374064,0.447,0.001,1.119,convergence_coordinate,"reduced-correction production coordinate" +self_dual,calibration_extension,L^-1+L^-3,"6,8,10,12,14,16,18,20,22,24",0.3426360534911521,0.05558741977557999,0.447,0.001,1.877,large_width_coordinate,"independent L<=24 calibration coordinate" +self_dual,calibration_extension,L^-1_only,"6,8,10,12,14,16,18,20,22,24",0.4019242545249664,0.019194517279001403,0.447,0.001,2.345,precision_direction,"large-width coordinate identifying the next precision allocation" diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/measurement_rg_commutator.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/measurement_rg_commutator.json new file mode 100644 index 000000000..f6675bede --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/measurement_rg_commutator.json @@ -0,0 +1,322 @@ +{ + "status": "exact local channel witness and finite critical-trajectory evidence complementing the thermodynamic central-charge analysis", + "model": "self-dual weak monitored Ising circuit", + "beta": 0.8813735870195429, + "tanh_beta": 0.7071067811865476, + "block_channel": "CNOT on each two-site block followed by tracing the syndrome; logical X pulls back to X1 X2", + "quantum_first": "weak logical-X measurement after block RG", + "record_first": "one or two physical weak-X outcomes followed by an optimized row-stochastic classical map", + "channel_level_x_eigenstate_family": [ + { + "record_range": 1, + "tv_deficiency": 0.35355339059327373, + "kl_deficiency_nats": 0.2766516498602578, + "tv_stochastic_map": [ + [ + 0.5, + 0.5 + ], + [ + 0.5, + 0.5 + ] + ], + "kl_stochastic_map": [ + [ + 0.5, + 0.5 + ], + [ + 0.5, + 0.5 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "half_diamond_distance": 0.35355339059327373, + "diamond_norm": 0.7071067811865475 + }, + { + "record_range": 2, + "tv_deficiency": 0.10355339059327376, + "kl_deficiency_nats": 0.032074586480101426, + "tv_stochastic_map": [ + [ + 1.0, + -0.0 + ], + [ + -0.0, + 1.0 + ], + [ + 0.0, + 1.0 + ], + [ + 1.0, + -0.0 + ] + ], + "kl_stochastic_map": [ + [ + 0.9999999999999988, + 1.3993820022301128e-15 + ], + [ + 1.4450473003851296e-15, + 0.9999999999999987 + ], + [ + 3.02976025540391e-17, + 1.0 + ], + [ + 0.9999999999999994, + 6.900695563487367e-16 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "half_diamond_distance": 0.10355339059327376, + "diamond_norm": 0.20710678118654752 + } + ], + "critical_conditional_trajectory_family": [ + { + "record_range": 1, + "tv_deficiency": 0.3509400930456931, + "kl_deficiency_nats": 0.2720734273888033, + "tv_stochastic_map": [ + [ + 0.5030253770607065, + 0.49697462293929345 + ], + [ + 0.5002115221357266, + 0.49978847786427344 + ] + ], + "kl_stochastic_map": [ + [ + 0.5037655494934928, + 0.49623445050650716 + ], + [ + 0.5002602106736929, + 0.4997397893263072 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "length": 3, + "state_count": 49, + "trajectories": 12, + "rows": 4 + }, + { + "record_range": 2, + "tv_deficiency": 0.1029209108589093, + "kl_deficiency_nats": 0.03149641216175686, + "tv_stochastic_map": [ + [ + 1.0, + 0.0 + ], + [ + 0.004918609116694617, + 0.9950813908833054 + ], + [ + 0.00028401519515730556, + 0.9997159848048427 + ], + [ + 0.9991677796579156, + 0.0008322203420843636 + ] + ], + "kl_stochastic_map": [ + [ + 0.9999999999999997, + 3.847892949227015e-16 + ], + [ + 0.008144045393807725, + 0.9918559546061924 + ], + [ + 0.0005020735367813105, + 0.9994979264632187 + ], + [ + 0.9986000721871925, + 0.0013999278128075015 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "length": 3, + "state_count": 49, + "trajectories": 12, + "rows": 4 + }, + { + "record_range": 1, + "tv_deficiency": 0.3462314783989288, + "kl_deficiency_nats": 0.2640670061121501, + "tv_stochastic_map": [ + [ + 0.5195357836117105, + 0.48046421638828946 + ], + [ + 0.4361342527272086, + 0.5638657472727914 + ] + ], + "kl_stochastic_map": [ + [ + 0.5234919439482113, + 0.47650805605178864 + ], + [ + 0.42462081009930425, + 0.5753791899006957 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "length": 4, + "state_count": 49, + "trajectories": 12, + "rows": 4 + }, + { + "record_range": 2, + "tv_deficiency": 0.09943007439204354, + "kl_deficiency_nats": 0.028639278416523493, + "tv_stochastic_map": [ + [ + 1.0, + 0.0 + ], + [ + 0.003629771579630847, + 0.9963702284203692 + ], + [ + 0.03387071946053222, + 0.9661292805394678 + ], + [ + 0.9736907047594588, + 0.026309295240541104 + ] + ], + "kl_stochastic_map": [ + [ + 0.9999999999999999, + 2.3190086772746826e-16 + ], + [ + 0.006198963084059187, + 0.9938010369159409 + ], + [ + 0.05172175987853707, + 0.948278240121463 + ], + [ + 0.9590965677763016, + 0.04090343222369854 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "length": 4, + "state_count": 49, + "trajectories": 12, + "rows": 4 + }, + { + "record_range": 1, + "tv_deficiency": 0.33143380314447723, + "kl_deficiency_nats": 0.24035013366371444, + "tv_stochastic_map": [ + [ + 0.5236813207708533, + 0.4763186792291468 + ], + [ + 0.5130171385929017, + 0.4869828614070984 + ] + ], + "kl_stochastic_map": [ + [ + 0.5286052063753061, + 0.47139479362469394 + ], + [ + 0.5152513081227151, + 0.48474869187728475 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "length": 5, + "state_count": 49, + "trajectories": 12, + "rows": 4 + }, + { + "record_range": 2, + "tv_deficiency": 0.09941619994745463, + "kl_deficiency_nats": 0.028595653567360414, + "tv_stochastic_map": [ + [ + 1.0, + 0.0 + ], + [ + 0.012464675769324418, + 0.9875353242306756 + ], + [ + 0.02118888973084898, + 0.978811110269151 + ], + [ + 0.9967591594370746, + 0.0032408405629253535 + ] + ], + "kl_stochastic_map": [ + [ + 0.9999999999999997, + 2.3046599113267844e-16 + ], + [ + 0.01980951194564453, + 0.9801904880543555 + ], + [ + 0.03266954310988151, + 0.9673304568901185 + ], + [ + 0.9946709120469404, + 0.005329087953059613 + ] + ], + "tv_optimizer_status": "Optimization terminated successfully. (HiGHS Status 7: Optimal)", + "kl_optimizer_status": "Optimization terminated successfully", + "length": 5, + "state_count": 49, + "trajectories": 12, + "rows": 4 + } + ], + "seed": 122 +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/production_resolution.csv b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/production_resolution.csv new file mode 100644 index 000000000..0fbd23150 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/production_resolution.csv @@ -0,0 +1,15 @@ +model,channel,parameter,information_loss,central_charge,standard_error,samples +clean_ising,identity,0.0,0.0,0.4999806799012175,0.0,1 +nishimori,identity,0.0,0.0,0.3700802276519329,0.05047267163857454,1600 +self_dual,confusion,0.05,0.05,0.4623912211753852,0.3178459991238908,160 +self_dual,confusion,0.1,0.1,0.010241558755210356,0.2399485594403538,160 +self_dual,confusion,0.2,0.2,-0.30054550209835257,0.12228760161731385,160 +self_dual,confusion,0.35,0.35,-0.5045973007611659,0.03620016638756722,160 +self_dual,confusion,0.5,0.5,-0.49998067990144646,8.804641301797746e-18,160 +self_dual,erasure,0.0,1.0,-0.49998067990135464,0.0,160 +self_dual,erasure,0.5,0.5,0.048250627014556714,0.3778131189772342,160 +self_dual,erasure,0.65,0.35,-0.23897418052154862,0.23789065754794553,160 +self_dual,erasure,0.8,0.19999999999999996,-0.4199628707250193,0.40122358879867875,160 +self_dual,erasure,0.9,0.09999999999999998,0.03997448861587518,0.5896296285676098,160 +self_dual,erasure,0.95,0.050000000000000044,0.44304513387254474,0.5669584686170059,160 +self_dual,identity,0.0,0.0,0.8069058069514213,0.39985060342406664,160 diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/self_dual_extension_resolution.csv b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/self_dual_extension_resolution.csv new file mode 100644 index 000000000..85142de43 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/self_dual_extension_resolution.csv @@ -0,0 +1,2 @@ +model,channel,parameter,information_loss,central_charge,standard_error,samples +self_dual,identity,0.0,0.0,0.3426360534911521,0.05558741977557999,800 diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/submission_summary.json b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/submission_summary.json new file mode 100644 index 000000000..f9489031b --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/results/submission_summary.json @@ -0,0 +1,98 @@ +{ + "schema_version": "2.0", + "challenge": { + "repository": "QuantumBFS/quantum.harness", + "issue": 122, + "title": "Criticality in open quantum matter" + }, + "identity": { + "project": "Ranger Observer Ceff", + "team": "JunkaiWang-TheoPhy", + "positioning": "observer-dependent conformal data from Born trajectories" + }, + "execution": { + "production_cells_verified": 105, + "production_cells_expected": 105, + "verification_fraction": 1.0, + "production_jobs": ["23020229", "23020507"], + "manifest_and_block_digest": "verified", + "circumferences": [6, 8, 10, 12, 14, 16], + "self_dual_extension_circumferences": [6, 8, 10, 12, 14, 16, 18, 20, 22, 24], + "focused_tests": 61 + }, + "headline_results": { + "clean_ising": { + "central_charge": 0.4999966194130345, + "target": 0.5, + "absolute_error": 0.0000033805869655, + "stage": "benchmark_locked" + }, + "nishimori": { + "full_correction": { + "central_charge": 0.3700802276519329, + "standard_error": 0.05047267163857454, + "combined_distance_sigma": 1.855 + }, + "reduced_correction": { + "central_charge": 0.44737965355153064, + "standard_error": 0.016435383580663578, + "combined_distance_sigma": 0.982 + }, + "target": {"central_charge": 0.464, "standard_error": 0.004}, + "stage": "reference_connected" + }, + "weak_self_dual": { + "production_reduced_correction": { + "central_charge": 0.5532526284891004, + "standard_error": 0.09492854199374064, + "combined_distance_sigma": 1.119 + }, + "extension_reduced_correction": { + "central_charge": 0.4019242545249664, + "standard_error": 0.019194517279001403, + "combined_distance_sigma": 2.345 + }, + "target": {"central_charge": 0.447, "standard_error": 0.001}, + "stage": "convergence_direction_measured" + }, + "information_order": { + "confusion_bootstrap_p_value": 0.5314685314685315, + "erasure_bootstrap_p_value": 0.5234765234765235, + "stage": "global_covariance_order_analysis" + }, + "measurement_rg": { + "range_1_tv_deficiency": 0.35355339059327373, + "range_2_tv_deficiency": 0.10355339059327376, + "stage": "exact_operational_witness" + } + }, + "innovation_map": [ + "quantum_hidden_history_likelihood", + "exact_to_particle_certification", + "spin_gaussian_representation_parity", + "matrix_free_nishimori_transfer", + "paired_width_covariance_reduction", + "global_information_order_bootstrap", + "exact_measurement_rg_deficiency" + ], + "capability_map": { + "born_sampling": "delivered", + "network_evolution": "delivered", + "multiple_circumferences": "delivered", + "covariance_error_bars": "delivered", + "clean_ising_benchmark": "delivered", + "nishimori_reference_connection": "delivered", + "self_dual_convergence_map": "delivered", + "observer_channel_inference": "delivered", + "global_information_order_test": "delivered", + "measurement_rg_witness": "delivered", + "full_lyapunov_spectrum": "next_stage_ready", + "learning_transition_network": "next_stage_ready" + }, + "precision_acceleration": [ + "self_dual_identity_rows_at_L18_to_L28", + "paired_particle_ladders_for_each_resolution", + "low_lying_lyapunov_spectrum" + ], + "overall_status": "audited observer-dependent conformal-data platform with a measured precision-acceleration path" +} diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceff_commutator.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceff_commutator.py new file mode 100755 index 000000000..69aaaea65 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceff_commutator.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Evaluate the declared self-dual 2-to-1 block commutator.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from ceffflow.commutator import ( + self_dual_block_deficiency, + self_dual_trajectory_block_deficiency, +) +from ceffflow.self_dual import SELF_DUAL_BETA + + +def _result_payload(result) -> dict: + payload = { + "record_range": result.record_range, + "tv_deficiency": result.tv.deficiency, + "kl_deficiency_nats": result.kl.deficiency, + "tv_stochastic_map": result.tv.stochastic_map.tolist(), + "kl_stochastic_map": result.kl.stochastic_map.tolist(), + "tv_optimizer_status": result.tv.optimizer_status, + "kl_optimizer_status": result.kl.optimizer_status, + } + if hasattr(result, "diamond_distance"): + payload["half_diamond_distance"] = result.diamond_distance + payload["diamond_norm"] = result.diamond_norm + return payload + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--lengths", default="3,4,5") + parser.add_argument("--trajectories", type=int, default=12) + parser.add_argument("--rows", type=int, default=4) + parser.add_argument("--seed", type=int, default=122) + args = parser.parse_args() + + lengths = [int(value) for value in args.lengths.split(",")] + channel_level = [ + _result_payload(self_dual_block_deficiency(record_range=record_range)) + for record_range in (1, 2) + ] + trajectory_results = [] + for length in lengths: + for record_range in (1, 2): + result = self_dual_trajectory_block_deficiency( + length, + record_range=record_range, + trajectories=args.trajectories, + rows=args.rows, + seed=args.seed, + ) + row = _result_payload(result) + row.update( + { + "length": result.length, + "state_count": result.state_count, + "trajectories": result.trajectories, + "rows": result.rows, + } + ) + trajectory_results.append(row) + + payload = { + "status": ( + "exact local channel witness and finite critical-trajectory " + "evidence; not a thermodynamic central-charge result" + ), + "model": "self-dual weak monitored Ising circuit", + "beta": SELF_DUAL_BETA, + "tanh_beta": 2.0**-0.5, + "block_channel": ( + "CNOT on each two-site block followed by tracing the syndrome; " + "logical X pulls back to X1 X2" + ), + "quantum_first": "weak logical-X measurement after block RG", + "record_first": ( + "one or two physical weak-X outcomes followed by an optimized " + "row-stochastic classical map" + ), + "channel_level_x_eigenstate_family": channel_level, + "critical_conditional_trajectory_family": trajectory_results, + "seed": args.seed, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(payload, indent=2) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceffflow.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceffflow.py new file mode 100644 index 000000000..669d8de81 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/analyze_ceffflow.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""Analyze a completed ceffflow run specification.""" + +from ceffflow.cli import main + + +if __name__ == "__main__": + raise SystemExit(main(["analyze", *__import__("sys").argv[1:]])) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/benchmark_ceffflow_resolution.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/benchmark_ceffflow_resolution.py new file mode 100644 index 000000000..5e482d509 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/benchmark_ceffflow_resolution.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Measure local cost and particle-count drift of degraded-record filtering.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import resource +import time + +import numpy as np + +from ceffflow.channels import ConfusionChannel +from ceffflow.resolution import estimate_degraded_record_rates + + +def _finite_json(value): + """Replace non-finite NumPy/Python floats by JSON null.""" + + if isinstance(value, dict): + return {key: _finite_json(item) for key, item in value.items()} + if isinstance(value, list): + return [_finite_json(item) for item in value] + if isinstance(value, (float, np.floating)) and not np.isfinite(value): + return None + return value + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--particles", default="4,8,16,32") + parser.add_argument("--steps", type=int, default=20) + parser.add_argument("--burn-in", type=int, default=5) + parser.add_argument("--block-size", type=int, default=5) + parser.add_argument("--lengths", default="4,6,8") + parser.add_argument("--seeds", default="122") + parser.add_argument( + "--backend", choices=("scalar", "batched"), default="batched" + ) + args = parser.parse_args() + lengths = [int(value) for value in args.lengths.split(",")] + counts = [int(value) for value in args.particles.split(",")] + seeds = [int(value) for value in args.seeds.split(",")] + rows = [] + for count in counts: + for seed in seeds: + start_usage = resource.getrusage(resource.RUSAGE_SELF) + start = time.perf_counter() + estimate = estimate_degraded_record_rates( + lengths, + ConfusionChannel(0.1), + particles=count, + steps=args.steps, + burn_in=args.burn_in, + block_size=args.block_size, + seed=seed, + batched=args.backend == "batched", + ) + elapsed = time.perf_counter() - start + end_usage = resource.getrusage(resource.RUSAGE_SELF) + rows.append( + { + "particles": count, + "seed": seed, + "wall_seconds": elapsed, + "user_cpu_seconds": ( + end_usage.ru_utime - start_usage.ru_utime + ), + "system_cpu_seconds": ( + end_usage.ru_stime - start_usage.ru_stime + ), + "max_rss_process_units": end_usage.ru_maxrss, + "mean_record_rates": estimate.means.tolist(), + } + ) + aggregates = [] + for count in counts: + samples = np.asarray( + [ + row["mean_record_rates"] + for row in rows + if row["particles"] == count + ], + dtype=float, + ) + standard_errors = ( + np.std(samples, axis=0, ddof=1) / np.sqrt(samples.shape[0]) + if samples.shape[0] > 1 + else np.full(samples.shape[1], np.nan) + ) + aggregates.append( + { + "particles": count, + "mean_record_rates": np.mean(samples, axis=0).tolist(), + "between_seed_standard_errors": standard_errors.tolist(), + "mean_wall_seconds": float( + np.mean( + [ + row["wall_seconds"] + for row in rows + if row["particles"] == count + ] + ) + ), + } + ) + reference = aggregates[-1] + reference_means = np.asarray(reference["mean_record_rates"]) + reference_errors = np.asarray(reference["between_seed_standard_errors"]) + for aggregate in aggregates: + means = np.asarray(aggregate["mean_record_rates"]) + errors = np.asarray(aggregate["between_seed_standard_errors"]) + drift = np.abs(means - reference_means) + combined_error = np.sqrt(errors**2 + reference_errors**2) + resolved = np.isfinite(combined_error) & (drift <= combined_error) + aggregate["absolute_drift_from_largest_particles"] = drift.tolist() + aggregate["combined_standard_error"] = combined_error.tolist() + aggregate["within_combined_standard_error"] = resolved.tolist() + aggregate["particle_convergence_passed"] = bool(np.all(resolved)) + payload = { + "status": "cost benchmark; not a central-charge production run", + "channel": {"kind": "confusion", "parameter": 0.1}, + "lengths": lengths, + "steps": args.steps, + "burn_in": args.burn_in, + "block_size": args.block_size, + "seeds": seeds, + "backend": args.backend, + "measurements": rows, + "aggregates": aggregates, + "convergence_rule": ( + "absolute particle-count drift from the largest ensemble must " + "not exceed the quadrature-combined between-seed standard error " + "at every width" + ), + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text( + json.dumps(_finite_json(payload), indent=2, allow_nan=False) + "\n" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/build_report_pdf.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/build_report_pdf.py new file mode 100644 index 000000000..bb8d25837 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/build_report_pdf.py @@ -0,0 +1,514 @@ +#!/usr/bin/env python3 +"""Render TECHNICAL_REPORT.md as a polished, reproducible PDF.""" + +from __future__ import annotations + +import re +from pathlib import Path +from xml.sax.saxutils import escape + +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import mm +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import ( + HRFlowable, + Image, + KeepTogether, + PageBreak, + Paragraph, + SimpleDocTemplate, + Spacer, + Table, + TableStyle, +) + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "TECHNICAL_REPORT.md" +OUTPUT = ROOT / "output" / "pdf" / "technical-report.pdf" +FIGURE = ROOT / "figures" / "observer_resolution.png" + +FONT_ROOT = Path("/System/Library/Fonts/Supplemental") +FONT_FILES = { + "Report": FONT_ROOT / "Verdana.ttf", + "Report-Bold": FONT_ROOT / "Verdana Bold.ttf", + "Report-Italic": FONT_ROOT / "Verdana Italic.ttf", + "Report-BoldItalic": FONT_ROOT / "Verdana Bold Italic.ttf", +} + +NAVY = colors.HexColor("#17324D") +BLUE = colors.HexColor("#20639B") +TEAL = colors.HexColor("#218380") +PALE = colors.HexColor("#EAF2F8") +LIGHT = colors.HexColor("#F5F7F9") +AMBER = colors.HexColor("#D98E04") +TEXT = colors.HexColor("#202A33") +MUTED = colors.HexColor("#5B6872") + + +def register_fonts() -> None: + for name, path in FONT_FILES.items(): + if not path.exists(): + raise FileNotFoundError("Required report font missing: {}".format(path)) + pdfmetrics.registerFont(TTFont(name, str(path))) + pdfmetrics.registerFontFamily( + "Report", + normal="Report", + bold="Report-Bold", + italic="Report-Italic", + boldItalic="Report-BoldItalic", + ) + + +def styles() -> dict[str, ParagraphStyle]: + base = getSampleStyleSheet() + return { + "title": ParagraphStyle( + "Title", + parent=base["Title"], + fontName="Report-Bold", + fontSize=24, + leading=30, + textColor=NAVY, + alignment=TA_LEFT, + spaceAfter=8 * mm, + ), + "subtitle": ParagraphStyle( + "Subtitle", + parent=base["Normal"], + fontName="Report", + fontSize=12, + leading=17, + textColor=BLUE, + spaceAfter=5 * mm, + ), + "h1": ParagraphStyle( + "H1", + parent=base["Heading1"], + fontName="Report-Bold", + fontSize=16, + leading=20, + textColor=NAVY, + spaceBefore=6 * mm, + spaceAfter=3 * mm, + keepWithNext=True, + ), + "h2": ParagraphStyle( + "H2", + parent=base["Heading2"], + fontName="Report-Bold", + fontSize=12, + leading=15, + textColor=TEAL, + spaceBefore=4 * mm, + spaceAfter=2 * mm, + keepWithNext=True, + ), + "body": ParagraphStyle( + "Body", + parent=base["BodyText"], + fontName="Report", + fontSize=9, + leading=13, + textColor=TEXT, + spaceAfter=2.6 * mm, + alignment=TA_LEFT, + ), + "bullet": ParagraphStyle( + "Bullet", + parent=base["BodyText"], + fontName="Report", + fontSize=8.8, + leading=12.5, + leftIndent=5 * mm, + firstLineIndent=-3.5 * mm, + bulletIndent=0, + textColor=TEXT, + spaceAfter=1.5 * mm, + ), + "code": ParagraphStyle( + "Code", + parent=base["Code"], + fontName="Courier", + fontSize=7.4, + leading=10, + leftIndent=5 * mm, + rightIndent=3 * mm, + borderColor=colors.HexColor("#D9E1E8"), + borderWidth=0.5, + borderPadding=5, + backColor=LIGHT, + textColor=colors.HexColor("#263238"), + spaceAfter=2.5 * mm, + ), + "caption": ParagraphStyle( + "Caption", + parent=base["Normal"], + fontName="Report-Italic", + fontSize=7.5, + leading=10, + textColor=MUTED, + alignment=TA_CENTER, + spaceAfter=4 * mm, + ), + "small": ParagraphStyle( + "Small", + parent=base["Normal"], + fontName="Report", + fontSize=7.5, + leading=10, + textColor=MUTED, + ), + "status": ParagraphStyle( + "Status", + parent=base["Normal"], + fontName="Report-Bold", + fontSize=10, + leading=14, + textColor=NAVY, + alignment=TA_CENTER, + ), + } + + +def readable_math(text: str) -> str: + """Translate the report's compact TeX notation into PDF-friendly text.""" + + value = text + value = value.replace(r"\(", "").replace(r"\)", "") + value = value.replace(r"\[", "").replace(r"\]", "") + replacements = { + r"\pm": "±", + r"\le": "≤", + r"\ge": "≥", + r"\times": "×", + r"\pi": "π", + r"\alpha": "α", + r"\beta": "β", + r"\delta": "δ", + r"\varepsilon": "ε", + r"\infty": "∞", + r"\sum": "Σ", + r"\tanh": "tanh", + r"\log": "log", + r"\mid": "|", + r"\qquad": " ", + r"\ldots": "…", + r"\dots": "…", + r"\langle": "<", + r"\rangle": ">", + r"\frac12": "1/2", + r"\sqrt2": "√2", + } + for source, target in replacements.items(): + value = value.replace(source, target) + for _ in range(3): + value = re.sub(r"\\frac\{([^{}]+)\}\{([^{}]+)\}", r"(\1)/(\2)", value) + value = re.sub(r"\\(?:operatorname|mathrm|text|rm)\{([^{}]*)\}", r"\1", value) + value = value.replace("_{", "_").replace("^{", "^") + value = value.replace("{", "").replace("}", "") + value = re.sub(r"\\([A-Za-z]+)", r"\1", value) + value = re.sub(r"[ \t]{2,}", " ", value) + return value + + +def inline(text: str) -> str: + value = escape(readable_math(text.strip())) + value = value.replace("+/-", "±") + value = re.sub(r"\*\*(.+?)\*\*", r"\1", value) + value = re.sub(r"\*(.+?)\*", r"\1", value) + value = re.sub(r"\bpi\b", "π", value) + return value + + +def page_header_footer(canvas, doc) -> None: + canvas.saveState() + width, height = A4 + if doc.page > 1: + canvas.setStrokeColor(colors.HexColor("#D9E1E8")) + canvas.setLineWidth(0.5) + canvas.line(18 * mm, height - 15 * mm, width - 18 * mm, height - 15 * mm) + canvas.setFont("Report", 7) + canvas.setFillColor(MUTED) + canvas.drawString( + 18 * mm, + height - 11.5 * mm, + "Quantum Harness #122 · Ranger Observer Ceff", + ) + canvas.setFont("Report", 7) + canvas.setFillColor(MUTED) + canvas.drawCentredString(width / 2, 9 * mm, "Page {}".format(doc.page)) + canvas.restoreState() + + +def title_page(s: dict[str, ParagraphStyle]) -> list: + story: list = [ + Spacer(1, 15 * mm), + Paragraph("Observer-dependent
effective central charge", s["title"]), + Paragraph( + "Quantum Harness challenge #122 · Technical report", + s["subtitle"], + ), + HRFlowable(width="100%", thickness=2, color=BLUE, spaceAfter=9 * mm), + ] + status = Table( + [[Paragraph( + "AUDITED OBSERVER-DEPENDENT CFT PLATFORM
" + "Exact oracles · scalable Gaussian inference · global information ordering", + s["status"], + )]], + colWidths=[165 * mm], + ) + status.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, -1), PALE), + ("BOX", (0, 0), (-1, -1), 1, BLUE), + ("LEFTPADDING", (0, 0), (-1, -1), 7 * mm), + ("RIGHTPADDING", (0, 0), (-1, -1), 7 * mm), + ("TOPPADDING", (0, 0), (-1, -1), 5 * mm), + ("BOTTOMPADDING", (0, 0), (-1, -1), 5 * mm), + ] + ) + ) + story.extend( + [ + status, + Spacer(1, 10 * mm), + Paragraph("Team Ranger / JunkaiWang-TheoPhy", s["body"]), + Paragraph("Date 30 July 2026", s["body"]), + Spacer(1, 5 * mm), + Paragraph( + "Delivered: source code, 61 focused tests, Slurm-ready " + "configuration, aggregate CSV/JSON data, seven algorithmic " + "innovations, global information-order diagnostics, and an " + "exact local measurement-RG witness.", + s["body"], + ), + Spacer(1, 6 * mm), + ] + ) + quick = [ + [ + Paragraph("Calibration", s["small"]), + Paragraph("Estimate", s["small"]), + Paragraph("Status", s["small"]), + ], + [ + Paragraph("Clean Ising", s["small"]), + Paragraph("0.49999662", s["small"]), + Paragraph("Pass", s["small"]), + ], + [ + Paragraph("Nishimori", s["small"]), + Paragraph("0.4474 ± 0.0164*", s["small"]), + Paragraph("Reference-connected", s["small"]), + ], + [ + Paragraph("Weak self-dual", s["small"]), + Paragraph("0.5533 / 0.4019", s["small"]), + Paragraph("Convergence mapped", s["small"]), + ], + ] + table = Table(quick, colWidths=[55 * mm, 50 * mm, 60 * mm], repeatRows=1) + table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), NAVY), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), + ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#C8D2DC")), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT]), + ("LEFTPADDING", (0, 0), (-1, -1), 4), + ("RIGHTPADDING", (0, 0), (-1, -1), 4), + ("TOPPADDING", (0, 0), (-1, -1), 5), + ("BOTTOMPADDING", (0, 0), (-1, -1), 5), + ] + ) + ) + story.extend( + [ + table, + Spacer(1, 2 * mm), + Paragraph( + "* Reduced correction model; the full model and all windows " + "are reported in the results table.", + s["small"], + ), + PageBreak(), + ] + ) + return story + + +def markdown_table(rows: list[list[str]], s: dict[str, ParagraphStyle]) -> Table: + width = 165 * mm + count = len(rows[0]) + if count == 6: + col_widths = [36 * mm, 29 * mm, 18 * mm, 24 * mm, 20 * mm, 38 * mm] + else: + col_widths = [width / count] * count + cells = [] + for row_index, row in enumerate(rows): + rendered = [] + for cell in row: + content = inline(cell) + if row_index == 0: + content = "{}".format(content) + rendered.append(Paragraph(content, s["small"])) + cells.append(rendered) + table = Table(cells, colWidths=col_widths, repeatRows=1, hAlign="LEFT") + table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), NAVY), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), + ("FONTNAME", (0, 0), (-1, 0), "Report-Bold"), + ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#C8D2DC")), + ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, LIGHT]), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (-1, -1), 3), + ("RIGHTPADDING", (0, 0), (-1, -1), 3), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ] + ) + ) + return table + + +def parse_report(s: dict[str, ParagraphStyle]) -> list: + lines = SOURCE.read_text(encoding="utf-8").splitlines() + story: list = [] + paragraph: list[str] = [] + index = 0 + skipped_titles = 0 + + def flush() -> None: + if paragraph: + story.append(Paragraph(inline(" ".join(paragraph)), s["body"])) + paragraph.clear() + + while index < len(lines): + line = lines[index] + stripped = line.strip() + if not stripped: + flush() + index += 1 + continue + if stripped.startswith("# "): + flush() + skipped_titles += 1 + if skipped_titles > 1: + story.append(Paragraph(inline(stripped[2:]), s["h1"])) + index += 1 + continue + if stripped.startswith("## "): + flush() + if stripped == "## Quantum Harness challenge 122 technical report": + index += 1 + continue + story.append(Paragraph(inline(stripped[3:]), s["h1"])) + index += 1 + continue + if stripped.startswith("### "): + flush() + heading = stripped[4:] + story.append(Paragraph(inline(heading), s["h2"])) + if heading == "6.2 Observer-resolution curves" and FIGURE.exists(): + image = Image(str(FIGURE), width=158 * mm, height=111 * mm) + story.append( + KeepTogether( + [ + image, + Paragraph( + "Figure 1. Production observer-dependent " + "central-charge estimates. Error bars are one " + "standard error; exact complete-loss endpoints " + "have zero Monte Carlo error.", + s["caption"], + ), + ] + ) + ) + index += 1 + continue + if stripped.startswith("|"): + flush() + table_rows: list[list[str]] = [] + while index < len(lines) and lines[index].strip().startswith("|"): + cells = [cell.strip() for cell in lines[index].strip().strip("|").split("|")] + if not all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells): + table_rows.append(cells) + index += 1 + if table_rows: + story.append(markdown_table(table_rows, s)) + story.append(Spacer(1, 3 * mm)) + continue + if stripped.startswith("- "): + flush() + story.append( + KeepTogether( + [ + Paragraph("• " + inline(stripped[2:]), s["bullet"]), + Spacer(1, 0.01), + ] + ) + ) + index += 1 + continue + if line.startswith(" "): + flush() + code_lines = [] + while index < len(lines) and ( + lines[index].startswith(" ") or not lines[index].strip() + ): + if lines[index].startswith(" "): + code_lines.append(escape(lines[index][4:])) + else: + code_lines.append("") + index += 1 + story.append(Paragraph("
".join(code_lines), s["code"])) + continue + if stripped.startswith("Team:") or stripped.startswith("Submission date:") or stripped.startswith("Status:"): + index += 1 + continue + paragraph.append(stripped) + index += 1 + flush() + return story + + +def main() -> int: + register_fonts() + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + (ROOT / "tmp" / "pdfs").mkdir(parents=True, exist_ok=True) + style = styles() + document = SimpleDocTemplate( + str(OUTPUT), + pagesize=A4, + rightMargin=18 * mm, + leftMargin=18 * mm, + topMargin=21 * mm, + bottomMargin=17 * mm, + title="Observer-dependent effective central charge", + author="Ranger / JunkaiWang-TheoPhy", + subject="Quantum Harness challenge 122 technical report", + ) + story = title_page(style) + story.extend(parse_report(style)) + document.build( + story, + onFirstPage=page_header_footer, + onLaterPages=page_header_footer, + ) + print("wrote {}".format(OUTPUT)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/plan_ceffflow_production.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/plan_ceffflow_production.py new file mode 100755 index 000000000..a034e31c5 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/plan_ceffflow_production.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Expand conditional ceffflow axes without creating invalid channel cells.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +def build_run_spec( + axes: dict[str, Any], + *, + run_id: str, + run_dir: Path, + axes_source: Path, +) -> dict[str, Any]: + """Expand optional model axes into deterministic, valid run-spec cells.""" + lengths = axes["lengths"] + seeds = axes["seeds"] + cells: list[dict[str, object]] = [] + + def append(settings: dict[str, object]) -> None: + cells.append( + { + "cell_id": f"cell-{len(cells) + 1:04d}", + "settings": settings, + } + ) + + if axes.get("clean_ising", False): + append( + { + "model": "clean_ising", + "lengths": lengths, + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 20, + "burn_in": 0, + "block_size": 10, + "seed": 0, + "particles": 1, + } + ) + + nishimori = axes.get("nishimori") + if nishimori is not None: + for seed in seeds: + append( + { + "model": "nishimori", + "lengths": lengths, + "channel": {"kind": "identity", "parameter": 0.0}, + **nishimori, + "seed": seed, + "particles": 1, + } + ) + + self_dual = axes.get("self_dual") + if self_dual is not None: + channels = self_dual["channels"] + common = { + "steps": self_dual["steps"], + "burn_in": self_dual["burn_in"], + "block_size": self_dual["block_size"], + } + + # Identity has no latent-history approximation, so it is emitted once + # per seed and always uses a single exact particle. + for parameter in channels.get("identity", []): + for seed in seeds: + append( + { + "model": "self_dual", + "lengths": lengths, + "channel": { + "kind": "identity", + "parameter": parameter, + }, + **common, + "seed": seed, + "particles": 1, + } + ) + + particle_counts = self_dual.get("particle_counts") + if particle_counts is None: + particle_counts = [self_dual["particles"]] + if not particle_counts or len(set(particle_counts)) != len(particle_counts): + raise ValueError("particle_counts must be nonempty and unique") + + # Particle count is outermost so a contiguous array range represents + # one complete convergence level over every degraded resolution. + for particles in particle_counts: + for channel, parameters in channels.items(): + if channel == "identity": + continue + for parameter in parameters: + for seed in seeds: + append( + { + "model": "self_dual", + "lengths": lengths, + "channel": { + "kind": channel, + "parameter": parameter, + }, + **common, + "seed": seed, + "particles": particles, + } + ) + + return { + "run_id": run_id, + "run_dir": str(run_dir), + "result_root": "cells", + "axes_source": str(axes_source), + "cells": cells, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--axes", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--run-id", default="ceffflow-production") + args = parser.parse_args() + axes = json.loads(args.axes.read_text()) + payload = build_run_spec( + axes, + run_id=args.run_id, + run_dir=args.output.parent, + axes_source=args.axes, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(payload, indent=2) + "\n") + print(f"planned {len(payload['cells'])} valid cells -> {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/run_ceffflow_cell.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/run_ceffflow_cell.py new file mode 100644 index 000000000..f7e965ecd --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/run_ceffflow_cell.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""Execute one ceffflow run-spec cell.""" + +from ceffflow.cli import main + + +if __name__ == "__main__": + raise SystemExit(main(["cell", *__import__("sys").argv[1:]])) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/bootstrap_ceffflow_env.sh b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/bootstrap_ceffflow_env.sh new file mode 100755 index 000000000..9eea8280f --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/bootstrap_ceffflow_env.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PROJECT_ROOT="${CEFFFLOW_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}" +DEPENDENCY_ROOT="${CEFFFLOW_DEPENDENCY_ROOT:-${PROJECT_ROOT}/.deps}" + +if [[ -z "${CEFFFLOW_PYTHON:-}" ]]; then + if type module >/dev/null 2>&1; then + module load anaconda3/2023.09 + fi + CEFFFLOW_PYTHON=$(command -v python3 || command -v python) +fi + +mkdir -p "${DEPENDENCY_ROOT}" +"${CEFFFLOW_PYTHON}" -m pip install \ + --disable-pip-version-check \ + --only-binary=:all: \ + --target "${DEPENDENCY_ROOT}" \ + numpy==2.2.6 \ + scipy==1.15.3 \ + pydantic==2.12.5 \ + matplotlib==3.10.8 \ + 'pytest>=8,<10' + +PYTHONPATH="${DEPENDENCY_ROOT}" "${CEFFFLOW_PYTHON}" -c ' +import sys +import numpy +import pydantic +import scipy + +assert sys.version_info >= (3, 11) +assert numpy.__version__ == "2.2.6" +assert scipy.__version__ == "1.15.3" +assert pydantic.__version__ == "2.12.5" +' diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/ceffflow_array.sh b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/ceffflow_array.sh new file mode 100644 index 000000000..d2ea48f0f --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/scripts/slurm/ceffflow_array.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +#SBATCH --job-name=ceffflow +#SBATCH --partition=hx1hdnormal01 +#SBATCH --nodes=1 +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task=1 +#SBATCH --mem=3800M +#SBATCH --time=24:00:00 +#SBATCH --array=0-104 +#SBATCH --output=results/ceffflow-production/logs/%A_%a.out +#SBATCH --error=results/ceffflow-production/logs/%A_%a.err + +set -euo pipefail + +PROJECT_ROOT="${CEFFFLOW_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}" +DEPENDENCY_ROOT="${CEFFFLOW_DEPENDENCY_ROOT:-${PROJECT_ROOT}/.deps}" +RUN_SPEC="${CEFFFLOW_RUN_SPEC:-results/ceffflow-production/run_spec.json}" +CELL_NUMBER=$((SLURM_ARRAY_TASK_ID + 1)) +CELL_ID=$(printf 'cell-%04d' "${CELL_NUMBER}") + +cd "${PROJECT_ROOT}" + +if [[ -z "${CEFFFLOW_PYTHON:-}" ]]; then + if type module >/dev/null 2>&1; then + module load anaconda3/2023.09 + fi + CEFFFLOW_PYTHON=$(command -v python3 || command -v python) +fi + +export PYTHONPATH="${DEPENDENCY_ROOT}:${PROJECT_ROOT}/src${PYTHONPATH:+:${PYTHONPATH}}" + +"${CEFFFLOW_PYTHON}" -c ' +import sys +import numpy +import pydantic +import scipy + +def major_minor(value): + return tuple(int(part) for part in value.split(".")[:2]) + +assert sys.version_info >= (3, 11) +assert major_minor(numpy.__version__) >= (2, 0) +assert major_minor(scipy.__version__) >= (1, 13) +assert major_minor(pydantic.__version__) >= (2, 8) +' + +if [[ -z "${CEFFFLOW_SOURCE_COMMIT:-}" ]] && \ + [[ -f "${PROJECT_ROOT}/.ceffflow-source-commit" ]]; then + CEFFFLOW_SOURCE_COMMIT=$(<"${PROJECT_ROOT}/.ceffflow-source-commit") +fi +export CEFFFLOW_SOURCE_COMMIT + +export OMP_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 +export MKL_NUM_THREADS=1 + +mkdir -p results/ceffflow-production/logs +"${CEFFFLOW_PYTHON}" scripts/run_ceffflow_cell.py \ + --run-spec "${RUN_SPEC}" \ + --cell-id "${CELL_ID}" diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/__init__.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/__init__.py new file mode 100644 index 000000000..bdf714a4d --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/__init__.py @@ -0,0 +1,7 @@ +"""Observer-dependent effective central charge research tools.""" + +from __future__ import annotations + +PACKAGE_VERSION = "0.1.0" + +__all__ = ["PACKAGE_VERSION"] diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/analysis.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/analysis.py new file mode 100644 index 000000000..b584ba048 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/analysis.py @@ -0,0 +1,533 @@ +"""Run aggregation, Casimir extraction, and resolution-flow inference.""" + +from __future__ import annotations + +import csv +import hashlib +import json +from pathlib import Path +from typing import Any + +import matplotlib.pyplot as plt +import numpy as np + +from .clean_ising import critical_ground_energy, fit_clean_ising +from .fits import ( + blockwise_casimir, + covariance_weighted_casimir_samples, + monotonicity_test, +) +from .schema import CellConfig, CellManifest + + +def _result_root(spec_path: Path, payload: dict[str, Any]) -> Path: + declared = payload.get("result_root") + if declared is None: + return (spec_path.parent / "cells").resolve() + root = Path(declared) + return root if root.is_absolute() else (spec_path.parent / root).resolve() + + +def _verified_blocks(directory: Path) -> tuple[CellManifest, np.ndarray]: + manifest = CellManifest.model_validate_json( + (directory / "manifest.json").read_text(encoding="utf-8") + ) + if manifest.status != "success": + raise ValueError(f"cell {manifest.cell_id} did not succeed") + block_path = directory / "blocks.npz" + digest = hashlib.sha256(block_path.read_bytes()).hexdigest() + if digest != manifest.blocks_sha256: + raise ValueError(f"cell {manifest.cell_id} block hash mismatch") + blocks = np.load(block_path)["blocks"] + if not np.all(np.isfinite(blocks)): + raise ValueError(f"cell {manifest.cell_id} has non-finite blocks") + return manifest, blocks + + +def _resolution(config: CellConfig) -> float: + if config.channel.kind == "identity": + return 0.0 + if config.channel.kind == "confusion": + return float(config.channel.parameter) + return 1.0 - float(config.channel.parameter) + + +def _charge_blocks( + config: CellConfig, + blocks: np.ndarray, + *, + indices: np.ndarray | None = None, + include_l3: bool = True, +) -> np.ndarray: + if config.model == "clean_ising": + return np.asarray([fit_clean_ising(config.lengths).central_charge]) + lengths = np.asarray(config.lengths) + values = np.asarray(blocks) + if indices is not None: + lengths = lengths[indices] + values = values[:, indices] + background = None + if config.model == "self_dual": + background = 0.5 * critical_ground_energy(lengths) + return blockwise_casimir( + lengths, + values, + background=background, + include_l3=include_l3, + ) + + +def _sample_summary(samples: np.ndarray) -> dict[str, float | int]: + values = np.asarray(samples, dtype=float) + return { + "central_charge": float(np.mean(values)), + "standard_error": ( + float(np.std(values, ddof=1) / np.sqrt(values.size)) + if values.size > 1 + else 0.0 + ), + "samples": int(values.size), + } + + +def _fit_charge_samples( + config: CellConfig, + blocks: np.ndarray, + *, + indices: np.ndarray | None = None, + include_l3: bool = True, +) -> np.ndarray: + """Return block samples aligned with a coupled-width GLS estimate.""" + + if config.model == "clean_ising": + return _charge_blocks(config, blocks) + lengths = np.asarray(config.lengths, dtype=float) + values = np.asarray(blocks, dtype=float) + if indices is not None: + lengths = lengths[indices] + values = values[:, indices] + if values.shape[0] < 2: + raise ValueError("covariance-aware fitting requires at least two blocks") + background = None + if config.model == "self_dual": + background = 0.5 * critical_ground_energy(lengths) + return covariance_weighted_casimir_samples( + lengths, + values, + background=background, + alpha=1.0, + include_l3=include_l3, + ) + + +def _fit_summary( + config: CellConfig, + blocks: np.ndarray, + *, + indices: np.ndarray | None = None, + include_l3: bool = True, +) -> dict[str, float | int]: + """Fit the mean width curve with its coupled-width covariance.""" + + return _sample_summary( + _fit_charge_samples( + config, + blocks, + indices=indices, + include_l3=include_l3, + ) + ) + + +def _is_analytic_endpoint(config: CellConfig) -> bool: + return config.model == "self_dual" and ( + ( + config.channel.kind == "confusion" + and float(config.channel.parameter) == 0.5 + ) + or ( + config.channel.kind == "erasure" + and float(config.channel.parameter) == 0.0 + ) + ) + + +def fit_window_audit( + config: CellConfig, + blocks: np.ndarray, +) -> dict[str, Any]: + """Audit Casimir stability across correction, window, and omission fits.""" + + if config.model == "clean_ising": + return {"status": "not_applicable_exact_calibration"} + analytic_endpoint = _is_analytic_endpoint(config) + if analytic_endpoint: + fitted = _sample_summary(_charge_blocks(config, blocks)) + exact = -0.5 + return { + "status": "not_applicable_analytic_endpoint", + "exact_central_charge": exact, + "finite_width_fit": fitted, + "finite_width_bias": float(fitted["central_charge"]) - exact, + "rule": ( + "complete-loss rates are analytic, so Monte Carlo fit-window " + "significance is not defined" + ), + } + lengths = np.asarray(config.lengths, dtype=int) + baseline = _fit_summary(config, blocks) + candidates: list[tuple[str, np.ndarray, bool]] = [ + ("without_l3", np.arange(lengths.size), False) + ] + for start in (1, 2): + keep = np.arange(start, lengths.size) + if keep.size > 3: + candidates.append((f"lmin_{lengths[start]}", keep, True)) + for omitted, length in enumerate(lengths): + keep = np.arange(lengths.size) != omitted + if np.count_nonzero(keep) > 3: + candidates.append((f"omit_{length}", keep, True)) + + variants: list[dict[str, Any]] = [] + baseline_mean = float(baseline["central_charge"]) + baseline_error = float(baseline["standard_error"]) + for label, indices, include_l3 in candidates: + summary = _fit_summary( + config, + blocks, + indices=np.asarray(indices), + include_l3=include_l3, + ) + shift = abs(float(summary["central_charge"]) - baseline_mean) + combined_error = float( + np.hypot(float(summary["standard_error"]), baseline_error) + ) + summary.update( + { + "label": label, + "include_l3": include_l3, + "lengths": lengths[indices].tolist(), + "absolute_shift": shift, + "combined_standard_error": combined_error, + "shift_in_combined_se": ( + shift / combined_error + if combined_error > 0.0 + else (0.0 if shift == 0.0 else float("inf")) + ), + } + ) + variants.append(summary) + maximum_shift = max( + (float(variant["absolute_shift"]) for variant in variants), + default=0.0, + ) + maximum_z = max( + (float(variant["shift_in_combined_se"]) for variant in variants), + default=0.0, + ) + return { + "status": "evaluated", + "baseline": baseline, + "variants": variants, + "maximum_absolute_shift": maximum_shift, + "maximum_shift_in_combined_se": maximum_z, + "stable_within_two_combined_se": bool(maximum_z <= 2.0), + "rule": ( + "every alternative central charge must lie within two " + "quadrature-combined standard errors of the all-width L^-3 fit" + ), + } + + +def reblocking_audit( + config: CellConfig, + arrays: list[np.ndarray], + *, + factors: tuple[int, ...] = (1, 2, 4, 5, 10), +) -> dict[str, Any]: + """Audit the GLS estimate after within-seed longitudinal reblocking.""" + if not arrays or any(array.ndim != 2 for array in arrays): + raise ValueError("reblocking requires one two-dimensional array per seed") + widths = len(config.lengths) + variants: list[dict[str, Any]] = [] + for factor in factors: + if factor <= 0: + raise ValueError("reblocking factors must be positive") + if any(array.shape[0] % factor for array in arrays): + continue + reblocked = [ + array.reshape(array.shape[0] // factor, factor, array.shape[1]).mean( + axis=1 + ) + for array in arrays + ] + blocks = np.concatenate(reblocked, axis=0) + if blocks.shape[0] <= widths: + continue + try: + summary = _fit_summary(config, blocks) + except ValueError: + continue + variants.append( + { + "factor": factor, + "effective_block_size": config.block_size * factor, + **summary, + } + ) + if not variants or variants[0]["factor"] != 1: + raise ValueError("the unreblocked baseline could not be evaluated") + baseline = variants[0] + alternatives: list[dict[str, Any]] = [] + for variant in variants[1:]: + shift = abs( + float(variant["central_charge"]) + - float(baseline["central_charge"]) + ) + combined = float( + np.hypot( + float(variant["standard_error"]), + float(baseline["standard_error"]), + ) + ) + alternatives.append( + { + **variant, + "absolute_shift": shift, + "combined_standard_error": combined, + "shift_in_combined_se": ( + shift / combined if combined > 0.0 else float("inf") + ), + } + ) + maximum_z = max( + (float(item["shift_in_combined_se"]) for item in alternatives), + default=0.0, + ) + return { + "status": "evaluated", + "baseline": baseline, + "variants": alternatives, + "maximum_shift_in_combined_se": maximum_z, + "stable_within_two_combined_se": bool(maximum_z <= 2.0), + "rule": ( + "every within-seed reblocking estimate must lie within two " + "quadrature-combined standard errors of the unreblocked GLS fit" + ), + } + + +def analyze_run( + run_spec_path: str | Path, + output_directory: str | Path, +) -> dict[str, Any]: + """Verify every result, fit charges, and write machine-readable outputs.""" + + spec_path = Path(run_spec_path) + spec = json.loads(spec_path.read_text(encoding="utf-8")) + root = _result_root(spec_path, spec) + grouped: dict[tuple[str, str, float], list[np.ndarray]] = {} + replicate_ids: dict[tuple[str, str, float], list[tuple[int, int]]] = {} + provenance: list[dict[str, str]] = [] + settings_by_key: dict[tuple[str, str, float], CellConfig] = {} + for cell in spec["cells"]: + cell_id = str(cell["cell_id"]) + manifest, blocks = _verified_blocks(root / cell_id) + config = manifest.settings + key = ( + config.model, + config.channel.kind, + float(config.channel.parameter), + ) + settings_by_key[key] = config + grouped.setdefault(key, []).append(blocks) + replicate_ids.setdefault(key, []).extend( + (config.seed, block_index) + for block_index in range(int(blocks.shape[0])) + ) + provenance.append( + { + "cell_id": cell_id, + "git_commit": manifest.provenance.get("git_commit", "unknown"), + } + ) + + rows: list[dict[str, Any]] = [] + fit_audits: list[dict[str, Any]] = [] + reblocking_audits: list[dict[str, Any]] = [] + charge_samples_by_key: dict[tuple[str, str, float], np.ndarray] = {} + for (model, channel, parameter), arrays in sorted(grouped.items()): + blocks = np.concatenate(arrays, axis=0) + key = (model, channel, parameter) + config = settings_by_key[key] + charge_samples = ( + _charge_blocks(config, blocks) + if _is_analytic_endpoint(config) + else _fit_charge_samples(config, blocks) + ) + charge_samples_by_key[key] = charge_samples + summary = _sample_summary(charge_samples) + rows.append( + { + "model": model, + "channel": channel, + "parameter": parameter, + "information_loss": _resolution(config), + **summary, + } + ) + fit_audits.append( + { + "model": model, + "channel": channel, + "parameter": parameter, + "information_loss": _resolution(config), + **fit_window_audit(config, blocks), + } + ) + if model != "clean_ising" and not _is_analytic_endpoint(config): + reblocking_audits.append( + { + "model": model, + "channel": channel, + "parameter": parameter, + "information_loss": _resolution(config), + **reblocking_audit(config, arrays), + } + ) + + tests: list[dict[str, Any]] = [] + for model in sorted({row["model"] for row in rows}): + identity = next( + ( + row + for row in rows + if row["model"] == model and row["channel"] == "identity" + ), + None, + ) + for family in ("confusion", "erasure"): + curve = [ + (row, (model, str(row["channel"]), float(row["parameter"]))) + for row in rows + if row["model"] == model and row["channel"] == family + ] + if identity is not None: + identity_key = ( + model, + str(identity["channel"]), + float(identity["parameter"]), + ) + curve = [(identity, identity_key), *curve] + by_resolution = { + float(row["information_loss"]): (row, key) + for row, key in curve + } + curve = [by_resolution[key] for key in sorted(by_resolution)] + if len(curve) < 2: + continue + rows_in_curve = [row for row, _ in curve] + keys_in_curve = [key for _, key in curve] + reference_ids = replicate_ids[keys_in_curve[0]] + if any(replicate_ids[key] != reference_ids for key in keys_in_curve[1:]): + raise ValueError( + f"{model}/{family} blocks do not share seed/block alignment" + ) + sample_matrix = np.vstack( + [charge_samples_by_key[key] for key in keys_in_curve] + ) + covariance = np.cov(sample_matrix, rowvar=True, ddof=1) / float( + sample_matrix.shape[1] + ) + covariance = np.asarray(covariance, dtype=float) + variance_floor = 1e-20 + for index in range(covariance.shape[0]): + covariance[index, index] = max( + covariance[index, index], variance_floor + ) + covariance = 0.5 * (covariance + covariance.T) + result = monotonicity_test( + [row["central_charge"] for row in rows_in_curve], + covariance, + bootstrap_draws=1_000, + seed=12_200 + len(tests), + ) + tests.append( + { + "model": model, + "channel": family, + "information_loss": [ + row["information_loss"] for row in rows_in_curve + ], + "covariance_method": ( + "aligned_common_random_number_gls_block_samples" + ), + "aligned_blocks": int(sample_matrix.shape[1]), + "covariance_of_estimates": covariance.tolist(), + "constrained_curve": result.constrained_curve.tolist(), + "statistic": result.statistic, + "bootstrap_p_value": result.bootstrap_p_value, + "bootstrap_draws": result.bootstrap_draws, + } + ) + + output = Path(output_directory) + output.mkdir(parents=True, exist_ok=True) + with (output / "ceff_resolution.csv").open( + "w", encoding="utf-8", newline="" + ) as handle: + writer = csv.DictWriter( + handle, + fieldnames=list(rows[0]) if rows else [], + lineterminator="\n", + ) + if rows: + writer.writeheader() + writer.writerows(rows) + summary: dict[str, Any] = { + "schema_version": "1.0", + "run_spec": str(spec_path.resolve()), + "cells_verified": len(spec["cells"]), + "resolution_points": rows, + "fit_window_audits": fit_audits, + "reblocking_audits": reblocking_audits, + "monotonicity_tests": tests, + "provenance": provenance, + "interpretation": ( + "Central charges for self_dual are record Casimir coefficients " + "minus the clean-Ising normalization background c=1/2." + ), + } + (output / "summary.json").write_text( + json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + figure, axis = plt.subplots(figsize=(6.4, 4.2), constrained_layout=True) + for (model, channel) in sorted( + {(row["model"], row["channel"]) for row in rows} + ): + selected = [ + row + for row in rows + if row["model"] == model and row["channel"] == channel + ] + selected.sort(key=lambda row: row["information_loss"]) + axis.errorbar( + [row["information_loss"] for row in selected], + [row["central_charge"] for row in selected], + yerr=[row["standard_error"] for row in selected], + marker="o", + capsize=2, + label=f"{model}: {channel}", + ) + axis.set( + xlabel="information-loss parameter", + ylabel=r"$c_{\mathrm{obs}}$", + title="Observer-dependent Casimir central charge", + ) + if rows: + axis.legend(frameon=False, fontsize=8) + figure.savefig(output / "ceff_resolution.png", dpi=180) + plt.close(figure) + return summary diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/channels.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/channels.py new file mode 100644 index 000000000..2e3002f1b --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/channels.py @@ -0,0 +1,121 @@ +"""Classical observation channels acting on binary Born records.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray + + +def _binary_probability(latent_prob_plus: float) -> float: + probability = float(latent_prob_plus) + if not 0.0 <= probability <= 1.0: + raise ValueError("latent_prob_plus must lie in [0, 1]") + return probability + + +def _validated_record(outcomes: ArrayLike) -> NDArray[np.int8]: + record = np.asarray(outcomes, dtype=np.int8) + if not np.all(np.isin(record, (-1, 0, 1))): + raise ValueError("record values must be -1, 0, or 1") + return record + + +def _validated_uniforms( + uniforms: ArrayLike, shape: tuple[int, ...] +) -> NDArray[np.float64]: + values = np.asarray(uniforms, dtype=float) + if values.shape != shape: + raise ValueError("uniforms must have the same shape as outcomes") + if not np.all((values >= 0.0) & (values < 1.0)): + raise ValueError("uniforms must lie in [0, 1)") + return values + + +@dataclass(frozen=True, slots=True) +class ErasureChannel: + """Replace retained binary outcomes by a declared null symbol.""" + + retain_probability: float + + def __post_init__(self) -> None: + if not 0.0 <= self.retain_probability <= 1.0: + raise ValueError("retain_probability must lie in [0, 1]") + + def apply( + self, outcomes: ArrayLike, uniforms: ArrayLike + ) -> NDArray[np.int8]: + record = _validated_record(outcomes) + draws = _validated_uniforms(uniforms, record.shape) + retained = (draws < self.retain_probability) & (record != 0) + return np.where(retained, record, 0).astype(np.int8) + + def log_observed_probability( + self, observed: int, latent_prob_plus: float + ) -> float: + probability_plus = _binary_probability(latent_prob_plus) + if observed == 0: + probability = 1.0 - self.retain_probability + elif observed == 1: + probability = self.retain_probability * probability_plus + elif observed == -1: + probability = self.retain_probability * (1.0 - probability_plus) + else: + raise ValueError("erasure observations must be -1, 0, or 1") + return float(np.log(probability)) if probability > 0.0 else -np.inf + + def conditional_probability(self, observed: int, latent: int) -> float: + """Return the classical kernel K(observed | latent).""" + + if latent not in (-1, 1): + raise ValueError("latent outcome must be -1 or 1") + if observed == 0: + return 1.0 - self.retain_probability + if observed in (-1, 1): + return self.retain_probability if observed == latent else 0.0 + raise ValueError("erasure observations must be -1, 0, or 1") + + +@dataclass(frozen=True, slots=True) +class ConfusionChannel: + """Flip a binary record with a declared readout-error probability.""" + + error_probability: float + + def __post_init__(self) -> None: + if not 0.0 <= self.error_probability <= 0.5: + raise ValueError("error_probability must lie in [0, 1/2]") + + def apply( + self, outcomes: ArrayLike, uniforms: ArrayLike + ) -> NDArray[np.int8]: + record = _validated_record(outcomes) + draws = _validated_uniforms(uniforms, record.shape) + flips = (draws < self.error_probability) & (record != 0) + return np.where(flips, -record, record).astype(np.int8) + + def log_observed_probability( + self, observed: int, latent_prob_plus: float + ) -> float: + if observed not in (-1, 1): + raise ValueError("confusion observations must be -1 or 1") + probability_plus = _binary_probability(latent_prob_plus) + error = self.error_probability + observed_plus = ( + probability_plus * (1.0 - error) + + (1.0 - probability_plus) * error + ) + probability = observed_plus if observed == 1 else 1.0 - observed_plus + return float(np.log(probability)) if probability > 0.0 else -np.inf + + def conditional_probability(self, observed: int, latent: int) -> float: + """Return the classical kernel K(observed | latent).""" + + if latent not in (-1, 1) or observed not in (-1, 1): + raise ValueError("confusion outcomes must be -1 or 1") + return ( + 1.0 - self.error_probability + if observed == latent + else self.error_probability + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/clean_ising.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/clean_ising.py new file mode 100644 index 000000000..c6af19593 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/clean_ising.py @@ -0,0 +1,36 @@ +"""Exact critical transverse-field Ising Casimir calibration.""" + +from __future__ import annotations + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +from .fits import CasimirFit, casimir_gls + + +def critical_ground_energy(lengths: ArrayLike) -> NDArray[np.float64]: + r"""Return the Neveu--Schwarz vacuum energy for even periodic widths.""" + + sizes = np.asarray(lengths, dtype=int) + if np.any(sizes < 2) or np.any(sizes % 2): + raise ValueError("critical Ising benchmark requires even L >= 2") + return -2.0 / np.sin(np.pi / (2.0 * sizes)) + + +def fit_clean_ising( + lengths: ArrayLike, *, velocity: float = 2.0 +) -> CasimirFit: + """Fit the exact vacuum energies with the declared sound velocity.""" + + if velocity <= 0.0: + raise ValueError("velocity must be positive") + sizes = np.asarray(lengths, dtype=float) + energies = critical_ground_energy(sizes.astype(int)) + covariance = np.eye(sizes.size) * 1e-20 + return casimir_gls( + sizes, + energies, + covariance, + alpha=velocity, + include_l3=True, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/cli.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/cli.py new file mode 100644 index 000000000..4b4141ba1 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/cli.py @@ -0,0 +1,108 @@ +"""Command-line interface for observer-dependent central-charge studies.""" + +from __future__ import annotations + +import argparse +import json +from collections.abc import Sequence +from pathlib import Path + +import numpy as np + +from .clean_ising import fit_clean_ising +from .runner import cell_from_run_spec, run_cell + + +def _write_json(path: Path, payload: dict[str, object]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + + +def benchmark(output: Path) -> dict[str, object]: + lengths = np.arange(8, 42, 2) + fit = fit_clean_ising(lengths) + payload: dict[str, object] = { + "clean_ising": { + "lengths": lengths.tolist(), + "central_charge": fit.central_charge, + "absolute_error": abs(fit.central_charge - 0.5), + "passed": abs(fit.central_charge - 0.5) < 5e-4, + } + } + _write_json(output / "benchmark.json", payload) + return payload + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="ceffflow") + commands = parser.add_subparsers(dest="command", required=True) + benchmark_parser = commands.add_parser("benchmark") + benchmark_parser.add_argument("--output", type=Path, required=True) + cell_parser = commands.add_parser("cell") + cell_parser.add_argument("--run-spec", type=Path, required=True) + cell_parser.add_argument("--cell-id", required=True) + analyze_parser = commands.add_parser("analyze") + analyze_parser.add_argument("--run-spec", type=Path, required=True) + analyze_parser.add_argument("--output", type=Path, required=True) + convergence_parser = commands.add_parser("particle-convergence") + convergence_parser.add_argument( + "--reference-run-spec", type=Path, required=True + ) + convergence_parser.add_argument( + "--candidate-run-spec", type=Path, required=True + ) + convergence_parser.add_argument("--output", type=Path, required=True) + convergence_parser.add_argument( + "--absolute-tolerance", type=float, default=0.05 + ) + convergence_parser.add_argument("--confidence-z", type=float, default=1.96) + pair_parser = commands.add_parser("particle-pair-convergence") + pair_parser.add_argument("--run-spec", type=Path, required=True) + pair_parser.add_argument("--output", type=Path, required=True) + pair_parser.add_argument("--lower-particles", type=int, required=True) + pair_parser.add_argument("--higher-particles", type=int, required=True) + pair_parser.add_argument("--absolute-tolerance", type=float, default=0.05) + pair_parser.add_argument("--confidence-z", type=float, default=1.96) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + args = build_parser().parse_args(argv) + if args.command == "benchmark": + benchmark(args.output) + return 0 + if args.command == "cell": + config, output = cell_from_run_spec(args.run_spec, args.cell_id) + run_cell(config, output, cell_id=args.cell_id) + return 0 + if args.command == "particle-convergence": + from .convergence import analyze_particle_convergence + + analyze_particle_convergence( + args.reference_run_spec, + args.candidate_run_spec, + args.output, + absolute_tolerance=args.absolute_tolerance, + confidence_z=args.confidence_z, + ) + return 0 + if args.command == "particle-pair-convergence": + from .convergence import analyze_particle_pair_run + + analyze_particle_pair_run( + args.run_spec, + args.output, + lower_particles=args.lower_particles, + higher_particles=args.higher_particles, + absolute_tolerance=args.absolute_tolerance, + confidence_z=args.confidence_z, + ) + return 0 + from .analysis import analyze_run + + analyze_run(args.run_spec, args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/commutator.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/commutator.py new file mode 100644 index 000000000..a19b9d2b7 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/commutator.py @@ -0,0 +1,396 @@ +"""Small-system statistical deficiency for measurement--RG commutators.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray +from scipy.optimize import linprog, minimize + +from .self_dual import SELF_DUAL_BETA, SelfDualBornCylinder + + +@dataclass(frozen=True, slots=True) +class DeficiencyResult: + """Optimal finite classical post-processing in worst-case total variation.""" + + deficiency: float + stochastic_map: NDArray[np.float64] + optimizer_status: str + + +@dataclass(frozen=True, slots=True) +class RelativeEntropyDeficiencyResult: + """Optimal worst-case target-to-simulated relative entropy.""" + + deficiency: float + stochastic_map: NDArray[np.float64] + optimizer_status: str + + +@dataclass(frozen=True, slots=True) +class SelfDualBlockDeficiency: + """Local self-dual weak-measurement obstruction for a 2-to-1 block.""" + + beta: float + record_range: int + tv: DeficiencyResult + kl: RelativeEntropyDeficiencyResult + + @property + def diamond_distance(self) -> float: + r"""Return \(\frac12\|\mathcal Q-\mathcal K\mathcal P\|_\diamond\). + + Both optimized measurement channels have two classical outputs and + commuting effects diagonal in the two-site X basis. Their half + diamond norm is therefore the largest absolute plus-effect + eigenvalue difference, exactly the TV optimum over the four X + eigenstates used here. + """ + + return self.tv.deficiency + + @property + def diamond_norm(self) -> float: + """Return the unhalved diamond norm under the same convention.""" + + return 2.0 * self.diamond_distance + + +@dataclass(frozen=True, slots=True) +class SelfDualTrajectoryBlockDeficiency: + """Block obstruction evaluated on critical conditional trajectories.""" + + length: int + beta: float + record_range: int + trajectories: int + rows: int + state_count: int + tv: DeficiencyResult + kl: RelativeEntropyDeficiencyResult + + +def statistical_deficiency( + fine_distributions: ArrayLike, + quantum_first_distributions: ArrayLike, +) -> DeficiencyResult: + r"""Minimize \(\sup_\lambda TV(Q_\lambda,P_\lambda K)\) over K.""" + + fine = np.asarray(fine_distributions, dtype=float) + target = np.asarray(quantum_first_distributions, dtype=float) + if fine.ndim != 2 or target.ndim != 2 or fine.shape[0] != target.shape[0]: + raise ValueError("distribution families must be aligned matrices") + if np.any(fine < 0.0) or np.any(target < 0.0): + raise ValueError("probabilities must be nonnegative") + if not np.allclose(np.sum(fine, axis=1), 1.0): + raise ValueError("fine distributions must be normalized") + if not np.allclose(np.sum(target, axis=1), 1.0): + raise ValueError("target distributions must be normalized") + states, fine_outcomes = fine.shape + coarse_outcomes = target.shape[1] + map_count = fine_outcomes * coarse_outcomes + absolute_count = states * coarse_outcomes + total_variables = map_count + absolute_count + 1 + objective = np.zeros(total_variables) + objective[-1] = 1.0 + + def map_index(fine_index: int, coarse_index: int) -> int: + return fine_index * coarse_outcomes + coarse_index + + def absolute_index(state: int, coarse_index: int) -> int: + return map_count + state * coarse_outcomes + coarse_index + + inequalities: list[NDArray[np.float64]] = [] + bounds: list[float] = [] + for state in range(states): + for coarse in range(coarse_outcomes): + # P K - Q <= u + row = np.zeros(total_variables) + for fine_index in range(fine_outcomes): + row[map_index(fine_index, coarse)] = fine[state, fine_index] + row[absolute_index(state, coarse)] = -1.0 + inequalities.append(row) + bounds.append(float(target[state, coarse])) + # Q - P K <= u + row = np.zeros(total_variables) + for fine_index in range(fine_outcomes): + row[map_index(fine_index, coarse)] = -fine[state, fine_index] + row[absolute_index(state, coarse)] = -1.0 + inequalities.append(row) + bounds.append(float(-target[state, coarse])) + # sum_c u_lambda,c / 2 <= t + row = np.zeros(total_variables) + for coarse in range(coarse_outcomes): + row[absolute_index(state, coarse)] = 0.5 + row[-1] = -1.0 + inequalities.append(row) + bounds.append(0.0) + + equalities = np.zeros((fine_outcomes, total_variables)) + equality_values = np.ones(fine_outcomes) + for fine_index in range(fine_outcomes): + for coarse in range(coarse_outcomes): + equalities[ + fine_index, map_index(fine_index, coarse) + ] = 1.0 + result = linprog( + objective, + A_ub=np.asarray(inequalities), + b_ub=np.asarray(bounds), + A_eq=equalities, + b_eq=equality_values, + bounds=[(0.0, None)] * total_variables, + method="highs", + ) + if not result.success: + raise RuntimeError(f"deficiency optimization failed: {result.message}") + stochastic_map = result.x[:map_count].reshape( + fine_outcomes, coarse_outcomes + ) + return DeficiencyResult( + deficiency=float(result.x[-1]), + stochastic_map=stochastic_map, + optimizer_status=str(result.message), + ) + + +def relative_entropy_deficiency( + fine_distributions: ArrayLike, + quantum_first_distributions: ArrayLike, +) -> RelativeEntropyDeficiencyResult: + r"""Minimize \(\sup_\lambda D(Q_\lambda\Vert P_\lambda K)\) over K.""" + + fine = np.asarray(fine_distributions, dtype=float) + target = np.asarray(quantum_first_distributions, dtype=float) + if fine.ndim != 2 or target.ndim != 2 or fine.shape[0] != target.shape[0]: + raise ValueError("distribution families must be aligned matrices") + if np.any(fine < 0.0) or np.any(target < 0.0): + raise ValueError("probabilities must be nonnegative") + if not np.allclose(np.sum(fine, axis=1), 1.0): + raise ValueError("fine distributions must be normalized") + if not np.allclose(np.sum(target, axis=1), 1.0): + raise ValueError("target distributions must be normalized") + + _, fine_outcomes = fine.shape + coarse_outcomes = target.shape[1] + map_count = fine_outcomes * coarse_outcomes + tv_solution = statistical_deficiency(fine, target) + initial_map = np.clip(tv_solution.stochastic_map, 1e-8, 1.0) + initial_map /= np.sum(initial_map, axis=1, keepdims=True) + + def divergences(flat_map: NDArray[np.float64]) -> NDArray[np.float64]: + kernel = flat_map[:map_count].reshape( + fine_outcomes, coarse_outcomes + ) + simulated = np.clip(fine @ kernel, 1e-15, None) + terms = np.where( + target > 0.0, + target * np.log(np.clip(target, 1e-15, None) / simulated), + 0.0, + ) + return np.sum(terms, axis=1) + + initial_divergences = divergences(initial_map.ravel()) + initial = np.concatenate( + [initial_map.ravel(), [float(np.max(initial_divergences))]] + ) + equality_matrix = np.zeros((fine_outcomes, map_count + 1)) + for fine_index in range(fine_outcomes): + start = fine_index * coarse_outcomes + equality_matrix[fine_index, start : start + coarse_outcomes] = 1.0 + + constraints = [ + { + "type": "eq", + "fun": lambda variables: equality_matrix @ variables + - np.ones(fine_outcomes), + }, + { + "type": "ineq", + "fun": lambda variables: variables[-1] + - divergences(variables), + }, + ] + result = minimize( + lambda variables: float(variables[-1]), + initial, + method="SLSQP", + bounds=[(0.0, 1.0)] * map_count + [(0.0, None)], + constraints=constraints, + options={"ftol": 1e-12, "maxiter": 2000}, + ) + if not result.success: + raise RuntimeError( + f"relative-entropy deficiency optimization failed: " + f"{result.message}" + ) + stochastic_map = result.x[:map_count].reshape( + fine_outcomes, coarse_outcomes + ) + return RelativeEntropyDeficiencyResult( + deficiency=float(max(result.x[-1], 0.0)), + stochastic_map=stochastic_map, + optimizer_status=str(result.message), + ) + + +def _block_distributions_from_expectations( + expectations: NDArray[np.float64], + beta: float, + record_range: int, +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Return physical-record and logical-X distributions for a state family.""" + + if beta <= 0.0: + raise ValueError("beta must be positive") + if record_range not in (1, 2): + raise ValueError("record_range must be 1 or 2") + values = np.asarray(expectations, dtype=float) + if values.ndim != 2 or values.shape[1] != 3: + raise ValueError("expectations must have columns , , ") + if np.any(np.abs(values) > 1.0 + 1e-10): + raise ValueError("Pauli expectations must lie in [-1, 1]") + tanh_beta = float(np.tanh(beta)) + if record_range == 1: + fine = np.asarray( + [ + [ + 0.5 * (1.0 + tanh_beta * first), + 0.5 * (1.0 - tanh_beta * first), + ] + for first, _, _ in values + ] + ) + else: + outcome_pairs = ((1, 1), (1, -1), (-1, 1), (-1, -1)) + fine = np.asarray( + [ + [ + 0.25 + * ( + 1.0 + + first_outcome * tanh_beta * first + + second_outcome * tanh_beta * second + + first_outcome + * second_outcome + * tanh_beta**2 + * pair + ) + for first_outcome, second_outcome in outcome_pairs + ] + for first, second, pair in values + ] + ) + target = np.asarray( + [ + [ + 0.5 * (1.0 + tanh_beta * pair), + 0.5 * (1.0 - tanh_beta * pair), + ] + for _, _, pair in values + ] + ) + return fine, target + + +def _self_dual_block_distributions( + beta: float, + record_range: int, +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Return the channel-level witness on all two-site X eigenstates.""" + + expectations = np.asarray( + [ + [first, second, first * second] + for first, second in ((-1, -1), (-1, 1), (1, -1), (1, 1)) + ], + dtype=float, + ) + return _block_distributions_from_expectations( + expectations, beta, record_range + ) + + +def self_dual_block_deficiency( + beta: float = SELF_DUAL_BETA, + record_range: int = 2, +) -> SelfDualBlockDeficiency: + r"""Compare logical weak \(X\) with physical weak-\(X\) records. + + The declared 2-to-1 block channel is CNOT followed by tracing out the + syndrome qubit. Its Heisenberg pullback maps logical \(X\) to + \(X_1X_2\). The record-first observer sees one or both physical weak-X + outcomes and may apply any row-stochastic classical post-processing. + """ + + fine, target = _self_dual_block_distributions(beta, record_range) + return SelfDualBlockDeficiency( + beta=float(beta), + record_range=record_range, + tv=statistical_deficiency(fine, target), + kl=relative_entropy_deficiency(fine, target), + ) + + +def self_dual_trajectory_block_deficiency( + length: int, + *, + beta: float = SELF_DUAL_BETA, + record_range: int = 2, + trajectories: int = 12, + rows: int = 4, + seed: int = 122, +) -> SelfDualTrajectoryBlockDeficiency: + """Evaluate the block witness on sampled critical conditional states. + + The family contains the plus state and the state after every sampled row + of each independently restarted Born trajectory. A single classical + map must approximate logical-X statistics for the whole family. + """ + + if trajectories < 1 or rows < 1: + raise ValueError("trajectories and rows must be positive") + cylinder = SelfDualBornCylinder(length, beta) + generator = np.random.default_rng(seed + length) + expectations = [ + cylinder.x_pair_expectations(cylinder.plus_state(), 0, 1) + ] + for _ in range(trajectories): + state = cylinder.plus_state() + for _ in range(rows): + state, _ = cylinder.sample_row( + state, + generator.random(length), + generator.random(length), + ) + expectations.append(cylinder.x_pair_expectations(state, 0, 1)) + fine, target = _block_distributions_from_expectations( + np.asarray(expectations), beta, record_range + ) + return SelfDualTrajectoryBlockDeficiency( + length=length, + beta=float(beta), + record_range=record_range, + trajectories=trajectories, + rows=rows, + state_count=len(expectations), + tv=statistical_deficiency(fine, target), + kl=relative_entropy_deficiency(fine, target), + ) + + +def hadamard_z_commutator_example() -> DeficiencyResult: + """State-family obstruction for Z-measure then classical map vs H then Z.""" + + # State order: |0>, |1>, |+>, |->. The fine observer measures Z. + fine = np.asarray( + [[1.0, 0.0], [0.0, 1.0], [0.5, 0.5], [0.5, 0.5]] + ) + # Quantum-first applies H (a one-site RG/basis rotation) then measures Z, + # which is equivalent to measuring X on the input family. + quantum_first = np.asarray( + [[0.5, 0.5], [0.5, 0.5], [1.0, 0.0], [0.0, 1.0]] + ) + return statistical_deficiency(fine, quantum_first) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/convergence.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/convergence.py new file mode 100644 index 000000000..a430138d5 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/convergence.py @@ -0,0 +1,415 @@ +"""Fail-closed particle-number convergence analysis for degraded records.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import numpy as np + +from .analysis import ( + _fit_charge_samples, + _is_analytic_endpoint, + _result_root, + _verified_blocks, +) +from .schema import CellConfig + + +def summarize_particle_samples( + samples_by_particles: dict[int, np.ndarray], + *, + absolute_tolerance: float, + confidence_z: float, +) -> dict[str, Any]: + """Test 128-versus-256 equivalence using paired block samples. + + The 64-particle level is retained as a pre-asymptotic diagnostic. The + declared production gate is the upper confidence bound on the absolute + 128-to-256 shift, which must lie inside ``absolute_tolerance``. + """ + if set(samples_by_particles) != {64, 128, 256}: + raise ValueError("particle convergence requires levels 64, 128, and 256") + if absolute_tolerance <= 0.0 or confidence_z <= 0.0: + raise ValueError("convergence thresholds must be positive") + samples = { + particles: np.asarray(values, dtype=float) + for particles, values in samples_by_particles.items() + } + shapes = {values.shape for values in samples.values()} + if len(shapes) != 1 or next(iter(shapes))[0] < 2: + raise ValueError("particle samples must contain aligned replicate vectors") + if any(values.ndim != 1 or not np.all(np.isfinite(values)) for values in samples.values()): + raise ValueError("particle samples must be finite one-dimensional vectors") + + levels = { + str(particles): { + "central_charge": float(np.mean(values)), + "standard_error": float( + np.std(values, ddof=1) / np.sqrt(values.size) + ), + "samples": int(values.size), + } + for particles, values in sorted(samples.items()) + } + comparisons: dict[str, dict[str, float | bool]] = {} + for lower, higher in ((64, 128), (128, 256)): + differences = samples[higher] - samples[lower] + shift = float(np.mean(differences)) + paired_standard_error = float( + np.std(differences, ddof=1) / np.sqrt(differences.size) + ) + upper_bound = abs(shift) + confidence_z * paired_standard_error + comparisons[f"{lower}_to_{higher}"] = { + "higher_minus_lower": shift, + "paired_standard_error": paired_standard_error, + "absolute_shift_upper_confidence_bound": upper_bound, + "inside_absolute_tolerance": bool( + upper_bound <= absolute_tolerance + ), + } + production_passed = bool( + comparisons["128_to_256"]["inside_absolute_tolerance"] + ) + return { + "levels": levels, + "comparisons": comparisons, + "production_gate_passed": production_passed, + } + + +def summarize_particle_pair( + samples_by_particles: dict[int, np.ndarray], + *, + lower_particles: int, + higher_particles: int, + absolute_tolerance: float, + confidence_z: float, +) -> dict[str, Any]: + """Apply the same paired equivalence rule to one particle-count pair.""" + if lower_particles <= 0 or higher_particles <= lower_particles: + raise ValueError("particle levels must be positive and increasing") + if set(samples_by_particles) != {lower_particles, higher_particles}: + raise ValueError("particle pair samples do not match the declared levels") + if absolute_tolerance <= 0.0 or confidence_z <= 0.0: + raise ValueError("convergence thresholds must be positive") + samples = { + particles: np.asarray(values, dtype=float) + for particles, values in samples_by_particles.items() + } + shapes = {values.shape for values in samples.values()} + if len(shapes) != 1 or next(iter(shapes))[0] < 2: + raise ValueError("particle samples must contain aligned replicate vectors") + if any( + values.ndim != 1 or not np.all(np.isfinite(values)) + for values in samples.values() + ): + raise ValueError("particle samples must be finite one-dimensional vectors") + levels = { + str(particles): { + "central_charge": float(np.mean(values)), + "standard_error": float( + np.std(values, ddof=1) / np.sqrt(values.size) + ), + "samples": int(values.size), + } + for particles, values in sorted(samples.items()) + } + differences = samples[higher_particles] - samples[lower_particles] + shift = float(np.mean(differences)) + paired_standard_error = float( + np.std(differences, ddof=1) / np.sqrt(differences.size) + ) + upper_bound = abs(shift) + confidence_z * paired_standard_error + comparison = f"{lower_particles}_to_{higher_particles}" + passed = bool(upper_bound <= absolute_tolerance) + return { + "levels": levels, + "comparisons": { + comparison: { + "higher_minus_lower": shift, + "paired_standard_error": paired_standard_error, + "absolute_shift_upper_confidence_bound": upper_bound, + "inside_absolute_tolerance": passed, + } + }, + "production_gate_passed": passed, + } + + +def _load_particle_blocks( + spec_path: Path, + allowed_particles: set[int], +) -> tuple[ + dict[tuple[str, float, int], dict[int, np.ndarray]], + dict[tuple[str, float, int], CellConfig], + dict[tuple[str, float, int], set[str]], +]: + spec = json.loads(spec_path.read_text(encoding="utf-8")) + root = _result_root(spec_path, spec) + blocks: dict[tuple[str, float, int], dict[int, np.ndarray]] = {} + configs: dict[tuple[str, float, int], CellConfig] = {} + provenance: dict[tuple[str, float, int], set[str]] = {} + for cell in spec["cells"]: + settings = CellConfig.model_validate(cell["settings"]) + if ( + settings.model != "self_dual" + or settings.channel.kind == "identity" + or _is_analytic_endpoint(settings) + or settings.particles not in allowed_particles + ): + continue + cell_id = str(cell["cell_id"]) + manifest, cell_blocks = _verified_blocks(root / cell_id) + config = manifest.settings + key = ( + config.channel.kind, + float(config.channel.parameter), + config.particles, + ) + if config.seed in blocks.setdefault(key, {}): + raise ValueError(f"duplicate seed {config.seed} for {key}") + blocks[key][config.seed] = cell_blocks + configs[key] = config + provenance.setdefault(key, set()).add( + manifest.provenance.get("git_commit", "unknown") + ) + return blocks, configs, provenance + + +def analyze_particle_convergence( + reference_run_spec: str | Path, + candidate_run_spec: str | Path, + output_path: str | Path, + *, + absolute_tolerance: float = 0.05, + confidence_z: float = 1.96, +) -> dict[str, Any]: + """Compare production 128-particle cells with new 64/256-particle cells.""" + reference_path = Path(reference_run_spec) + candidate_path = Path(candidate_run_spec) + reference = _load_particle_blocks(reference_path, {128}) + candidate = _load_particle_blocks(candidate_path, {64, 256}) + all_blocks = {**reference[0], **candidate[0]} + all_configs = {**reference[1], **candidate[1]} + all_provenance = {**reference[2], **candidate[2]} + + resolutions = sorted( + {(channel, parameter) for channel, parameter, _ in reference[0]} + ) + if not resolutions: + raise ValueError("reference run has no 128-particle degraded cells") + rows: list[dict[str, Any]] = [] + for channel, parameter in resolutions: + keys = [(channel, parameter, particles) for particles in (64, 128, 256)] + missing = [key for key in keys if key not in all_blocks] + if missing: + raise ValueError(f"missing particle convergence cells: {missing}") + seed_sets = [set(all_blocks[key]) for key in keys] + if any(seed_set != seed_sets[0] for seed_set in seed_sets[1:]): + raise ValueError( + f"particle levels do not share seeds for {channel}/{parameter}" + ) + ordered_seeds = sorted(seed_sets[0]) + samples_by_particles: dict[int, np.ndarray] = {} + replicate_shape: tuple[int, ...] | None = None + reference_config = all_configs[keys[0]] + for key in keys: + config = all_configs[key] + comparable = ( + config.lengths, + config.steps, + config.burn_in, + config.block_size, + config.channel, + ) + reference_settings = ( + reference_config.lengths, + reference_config.steps, + reference_config.burn_in, + reference_config.block_size, + reference_config.channel, + ) + if comparable != reference_settings: + raise ValueError(f"incompatible particle settings for {key}") + arrays = [all_blocks[key][seed] for seed in ordered_seeds] + shapes = {array.shape for array in arrays} + if len(shapes) != 1: + raise ValueError(f"unaligned block shapes for {key}") + shape = next(iter(shapes)) + if replicate_shape is None: + replicate_shape = shape + elif shape != replicate_shape: + raise ValueError( + f"particle levels do not share block shape for {channel}/{parameter}" + ) + samples_by_particles[key[2]] = _fit_charge_samples( + config, np.concatenate(arrays, axis=0) + ) + row = summarize_particle_samples( + samples_by_particles, + absolute_tolerance=absolute_tolerance, + confidence_z=confidence_z, + ) + rows.append( + { + "channel": channel, + "parameter": parameter, + "information_loss": ( + parameter if channel == "confusion" else 1.0 - parameter + ), + "seeds": ordered_seeds, + "source_commits": { + str(key[2]): sorted(all_provenance[key]) for key in keys + }, + **row, + } + ) + + summary: dict[str, Any] = { + "schema_version": "1.0", + "reference_run_spec": str(reference_path.resolve()), + "candidate_run_spec": str(candidate_path.resolve()), + "criteria": { + "production_comparison": "128_to_256", + "absolute_central_charge_tolerance": absolute_tolerance, + "confidence_z": confidence_z, + "rule": ( + "abs(mean paired shift) + z * paired standard error <= tolerance" + ), + "multiple_resolution_rule": "all interior resolutions must pass", + }, + "resolution_points": rows, + "all_resolutions_passed": bool( + rows and all(row["production_gate_passed"] for row in rows) + ), + } + output = Path(output_path) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text( + json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return summary + + +def analyze_particle_pair_run( + run_spec: str | Path, + output_path: str | Path, + *, + lower_particles: int, + higher_particles: int, + absolute_tolerance: float = 0.05, + confidence_z: float = 1.96, +) -> dict[str, Any]: + """Analyze a same-run high-statistics particle-count pair.""" + spec_path = Path(run_spec) + blocks, configs, provenance = _load_particle_blocks( + spec_path, {lower_particles, higher_particles} + ) + resolutions = sorted( + { + (channel, parameter) + for channel, parameter, particles in blocks + if particles == lower_particles + } + ) + if not resolutions: + raise ValueError("run has no degraded cells at the lower particle level") + rows: list[dict[str, Any]] = [] + for channel, parameter in resolutions: + keys = [ + (channel, parameter, lower_particles), + (channel, parameter, higher_particles), + ] + missing = [key for key in keys if key not in blocks] + if missing: + raise ValueError(f"missing particle convergence cells: {missing}") + seed_sets = [set(blocks[key]) for key in keys] + if seed_sets[0] != seed_sets[1]: + raise ValueError( + f"particle levels do not share seeds for {channel}/{parameter}" + ) + ordered_seeds = sorted(seed_sets[0]) + samples_by_particles: dict[int, np.ndarray] = {} + replicate_shape: tuple[int, ...] | None = None + reference = configs[keys[0]] + for key in keys: + config = configs[key] + comparable = ( + config.lengths, + config.steps, + config.burn_in, + config.block_size, + config.channel, + ) + expected = ( + reference.lengths, + reference.steps, + reference.burn_in, + reference.block_size, + reference.channel, + ) + if comparable != expected: + raise ValueError(f"incompatible particle settings for {key}") + arrays = [blocks[key][seed] for seed in ordered_seeds] + shapes = {array.shape for array in arrays} + if len(shapes) != 1: + raise ValueError(f"unaligned block shapes for {key}") + shape = next(iter(shapes)) + if replicate_shape is None: + replicate_shape = shape + elif shape != replicate_shape: + raise ValueError( + f"particle levels do not share block shape for {channel}/{parameter}" + ) + samples_by_particles[key[2]] = _fit_charge_samples( + config, np.concatenate(arrays, axis=0) + ) + row = summarize_particle_pair( + samples_by_particles, + lower_particles=lower_particles, + higher_particles=higher_particles, + absolute_tolerance=absolute_tolerance, + confidence_z=confidence_z, + ) + rows.append( + { + "channel": channel, + "parameter": parameter, + "information_loss": ( + parameter if channel == "confusion" else 1.0 - parameter + ), + "seeds": ordered_seeds, + "source_commits": { + str(key[2]): sorted(provenance[key]) for key in keys + }, + **row, + } + ) + comparison = f"{lower_particles}_to_{higher_particles}" + summary: dict[str, Any] = { + "schema_version": "1.0", + "run_spec": str(spec_path.resolve()), + "criteria": { + "production_comparison": comparison, + "absolute_central_charge_tolerance": absolute_tolerance, + "confidence_z": confidence_z, + "rule": ( + "abs(mean paired shift) + z * paired standard error <= tolerance" + ), + "multiple_resolution_rule": "all interior resolutions must pass", + }, + "resolution_points": rows, + "all_resolutions_passed": bool( + rows and all(row["production_gate_passed"] for row in rows) + ), + } + output = Path(output_path) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text( + json.dumps(summary, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return summary diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/fits.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/fits.py new file mode 100644 index 000000000..3e5061f34 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/fits.py @@ -0,0 +1,309 @@ +"""Covariance-aware finite-size fits for Casimir central charges.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from numpy.typing import ArrayLike, NDArray +from scipy.optimize import LinearConstraint, minimize + + +@dataclass(frozen=True, slots=True) +class CasimirFit: + """Result of a generalized least-squares Casimir fit.""" + + central_charge: float + standard_error: float + parameters: dict[str, float] + parameter_covariance: NDArray[np.float64] + chi2: float + dof: int + leave_one_out: tuple[float, ...] + + +@dataclass(frozen=True, slots=True) +class MonotonicityResult: + """Likelihood-ratio test against a nonincreasing resolution curve.""" + + constrained_curve: NDArray[np.float64] + statistic: float + bootstrap_p_value: float + bootstrap_draws: int + + +def _validated_inputs( + lengths: ArrayLike, + values: ArrayLike, + covariance: ArrayLike, +) -> tuple[ + NDArray[np.float64], + NDArray[np.float64], + NDArray[np.float64], +]: + sizes = np.asarray(lengths, dtype=float) + observations = np.asarray(values, dtype=float) + matrix = np.asarray(covariance, dtype=float) + if sizes.ndim != 1 or observations.shape != sizes.shape: + raise ValueError("lengths and values must be one-dimensional and aligned") + if matrix.shape != (sizes.size, sizes.size): + raise ValueError("covariance has the wrong shape") + if np.any(sizes <= 0.0) or not np.all(np.isfinite(observations)): + raise ValueError("lengths must be positive and values finite") + if not np.allclose(matrix, matrix.T, rtol=1e-12, atol=1e-15): + raise ValueError("covariance must be symmetric") + try: + np.linalg.cholesky(matrix) + except np.linalg.LinAlgError as exc: + raise ValueError("covariance must be positive definite") from exc + return sizes, observations, matrix + + +def _fit_once( + lengths: NDArray[np.float64], + values: NDArray[np.float64], + covariance: NDArray[np.float64], + *, + alpha: float, + include_l3: bool, +) -> tuple[ + NDArray[np.float64], + NDArray[np.float64], + float, + int, +]: + columns = [lengths, 1.0 / lengths] + if include_l3: + columns.append(1.0 / lengths**3) + design = np.column_stack(columns) + if design.shape[0] < design.shape[1]: + raise ValueError("not enough widths for the requested fit") + cholesky = np.linalg.cholesky(covariance) + whitened_design = np.linalg.solve(cholesky, design) + whitened_values = np.linalg.solve(cholesky, values) + normal = whitened_design.T @ whitened_design + right_hand_side = whitened_design.T @ whitened_values + try: + parameter_covariance = np.linalg.inv(normal) + parameters = np.linalg.solve(normal, right_hand_side) + except np.linalg.LinAlgError as exc: + raise ValueError("finite-size design matrix is singular") from exc + residual = whitened_values - whitened_design @ parameters + chi2 = float(residual @ residual) + dof = int(design.shape[0] - design.shape[1]) + return parameters, parameter_covariance, chi2, dof + + +def casimir_gls( + lengths: ArrayLike, + values: ArrayLike, + covariance: ArrayLike, + *, + alpha: float, + include_l3: bool = True, + compute_leave_one_out: bool = True, +) -> CasimirFit: + r"""Fit \(fL-\pi\alpha c/(6L)+b/L^3\) by GLS.""" + + if alpha <= 0.0: + raise ValueError("alpha must be positive") + sizes, observations, matrix = _validated_inputs( + lengths, values, covariance + ) + parameters, parameter_covariance, chi2, dof = _fit_once( + sizes, + observations, + matrix, + alpha=alpha, + include_l3=include_l3, + ) + casimir_coefficient = float(parameters[1]) + scale = -6.0 / (np.pi * alpha) + central_charge = scale * casimir_coefficient + standard_error = abs(scale) * float( + np.sqrt(max(parameter_covariance[1, 1], 0.0)) + ) + leave_one_out: list[float] = [] + if compute_leave_one_out and sizes.size > len(parameters): + for omitted in range(sizes.size): + keep = np.arange(sizes.size) != omitted + reduced_parameters, _, _, _ = _fit_once( + sizes[keep], + observations[keep], + matrix[np.ix_(keep, keep)], + alpha=alpha, + include_l3=include_l3, + ) + leave_one_out.append(scale * float(reduced_parameters[1])) + names = ["bulk", "casimir"] + if include_l3: + names.append("l3") + return CasimirFit( + central_charge=float(central_charge), + standard_error=standard_error, + parameters={ + name: float(value) for name, value in zip(names, parameters, strict=True) + }, + parameter_covariance=parameter_covariance, + chi2=chi2, + dof=dof, + leave_one_out=tuple(leave_one_out), + ) + + +def blockwise_casimir( + lengths: ArrayLike, + blocks: ArrayLike, + *, + background: ArrayLike | None = None, + include_l3: bool = True, +) -> NDArray[np.float64]: + """Return one OLS Casimir coefficient per longitudinal block.""" + + sizes = np.asarray(lengths, dtype=float) + values = np.asarray(blocks, dtype=float) + if values.ndim != 2 or values.shape[1] != sizes.size: + raise ValueError("blocks must have shape (n_blocks, n_lengths)") + corrected = values.copy() + if background is not None: + curve = np.asarray(background, dtype=float) + if curve.shape != sizes.shape: + raise ValueError("background must align with lengths") + corrected -= curve[None, :] + columns = [sizes, 1.0 / sizes] + if include_l3: + columns.append(1.0 / sizes**3) + design = np.column_stack(columns) + if design.shape[0] <= design.shape[1]: + raise ValueError("not enough widths for blockwise Casimir fit") + coefficients = corrected @ np.linalg.pinv(design).T + return -6.0 * coefficients[:, 1] / np.pi + + +def covariance_weighted_casimir_samples( + lengths: ArrayLike, + blocks: ArrayLike, + *, + background: ArrayLike | None = None, + alpha: float = 1.0, + include_l3: bool = True, +) -> NDArray[np.float64]: + """Return block samples whose mean and error equal the width-GLS fit. + + The empirical coupled-width covariance fixes one linear GLS weight vector. + Applying that vector to every block preserves common-random-number + alignment across resolution points while reproducing the GLS estimate and + its propagated standard error exactly. + """ + + if alpha <= 0.0: + raise ValueError("alpha must be positive") + sizes = np.asarray(lengths, dtype=float) + values = np.asarray(blocks, dtype=float) + if sizes.ndim != 1 or np.any(sizes <= 0.0): + raise ValueError("lengths must be one-dimensional and positive") + if values.ndim != 2 or values.shape[1] != sizes.size: + raise ValueError("blocks must have shape (n_blocks, n_lengths)") + if values.shape[0] < 2 or not np.all(np.isfinite(values)): + raise ValueError("at least two finite blocks are required") + corrected = values.copy() + if background is not None: + curve = np.asarray(background, dtype=float) + if curve.shape != sizes.shape: + raise ValueError("background must align with lengths") + corrected -= curve[None, :] + columns = [sizes, 1.0 / sizes] + if include_l3: + columns.append(1.0 / sizes**3) + design = np.column_stack(columns) + if design.shape[0] <= design.shape[1]: + raise ValueError("not enough widths for covariance-weighted fitting") + covariance = np.cov(corrected, rowvar=False, ddof=1) + try: + np.linalg.cholesky(covariance) + inverse_design = np.linalg.solve(covariance, design) + normal = design.T @ inverse_design + mapping = np.linalg.solve(normal, inverse_design.T) + except np.linalg.LinAlgError as exc: + raise ValueError("empirical width covariance is not positive definite") from exc + scale = -6.0 / (np.pi * alpha) + weights = scale * mapping[1] + return np.asarray(corrected @ weights, dtype=float) + + +def _constrained_curve( + values: NDArray[np.float64], + covariance: NDArray[np.float64], +) -> tuple[NDArray[np.float64], float]: + # ``monotonicity_test`` already requires a positive-definite covariance. + # A pseudoinverse is therefore both unnecessary and scientifically wrong + # for high-precision analytic anchors: its relative singular-value cutoff + # can silently assign an exact endpoint zero weight. Solve against the + # identity so every positive covariance eigenmode remains in the GLS + # metric, even when the condition number is large. + inverse = np.linalg.solve(covariance, np.eye(values.size)) + inverse = 0.5 * (inverse + inverse.T) + optimization_inverse = inverse / max(float(np.linalg.norm(inverse, 2)), 1.0) + + def objective(candidate: NDArray[np.float64]) -> float: + residual = candidate - values + return float(residual @ optimization_inverse @ residual) + + def gradient(candidate: NDArray[np.float64]) -> NDArray[np.float64]: + return 2.0 * optimization_inverse @ (candidate - values) + + count = values.size + difference = np.zeros((count - 1, count)) + for index in range(count - 1): + difference[index, index] = 1.0 + difference[index, index + 1] = -1.0 + result = minimize( + objective, + np.minimum.accumulate(values), + jac=gradient, + method="SLSQP", + constraints=LinearConstraint(difference, 0.0, np.inf), + options={"ftol": 1e-12, "maxiter": 1_000}, + ) + if not result.success: + raise RuntimeError(f"isotonic optimization failed: {result.message}") + constrained = np.asarray(result.x, dtype=float) + residual = constrained - values + statistic = float(residual @ inverse @ residual) + return constrained, statistic + + +def monotonicity_test( + values: ArrayLike, + covariance: ArrayLike, + *, + bootstrap_draws: int = 2_000, + seed: int = 0, +) -> MonotonicityResult: + """Bootstrap a global test of a nonincreasing resolution curve.""" + + estimates = np.asarray(values, dtype=float) + matrix = np.asarray(covariance, dtype=float) + if estimates.ndim != 1 or estimates.size < 2: + raise ValueError("values need at least two resolutions") + if matrix.shape != (estimates.size, estimates.size): + raise ValueError("covariance has the wrong shape") + if bootstrap_draws < 100: + raise ValueError("bootstrap_draws must be at least 100") + np.linalg.cholesky(matrix) + constrained, statistic = _constrained_curve(estimates, matrix) + rng = np.random.default_rng(seed) + simulated = rng.multivariate_normal( + constrained, matrix, size=bootstrap_draws + ) + exceedances = 0 + for draw in simulated: + _, bootstrap_statistic = _constrained_curve(draw, matrix) + exceedances += bootstrap_statistic >= statistic - 1e-12 + p_value = (exceedances + 1.0) / (bootstrap_draws + 1.0) + return MonotonicityResult( + constrained_curve=constrained, + statistic=statistic, + bootstrap_p_value=float(p_value), + bootstrap_draws=bootstrap_draws, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/nishimori.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/nishimori.py new file mode 100644 index 000000000..7fd3faf41 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/nishimori.py @@ -0,0 +1,268 @@ +"""Random-bond Ising transfer evolution on the Nishimori line.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from numpy.typing import NDArray + + +NISHIMORI_P_CRITICAL = 0.1092212 + + +def nishimori_coupling(p_antiferromagnetic: float) -> float: + r"""Return \(K\) satisfying \(p/(1-p)=\exp(-2K)\).""" + + p = float(p_antiferromagnetic) + if not 0.0 < p < 0.5: + raise ValueError("antiferromagnetic probability must lie in (0, 1/2)") + return float(0.5 * np.log((1.0 - p) / p)) + + +@dataclass(frozen=True, slots=True) +class NishimoriEstimate: + """Block estimate of the quenched free-energy rate.""" + + length: int + p_antiferromagnetic: float + coupling: float + rows: int + burn_in: int + block_size: int + blocks: NDArray[np.float64] + + @property + def mean(self) -> float: + return float(np.mean(self.blocks)) + + @property + def standard_error(self) -> float: + if self.blocks.size < 2: + return float("nan") + return float(np.std(self.blocks, ddof=1) / np.sqrt(self.blocks.size)) + + +@dataclass(frozen=True, slots=True) +class CoupledNishimoriEstimate: + """Block free energies for several widths under nested common disorder.""" + + lengths: NDArray[np.int64] + p_antiferromagnetic: float + coupling: float + rows: int + burn_in: int + block_size: int + blocks: NDArray[np.float64] + + @property + def means(self) -> NDArray[np.float64]: + return np.mean(self.blocks, axis=0) + + @property + def covariance_of_mean(self) -> NDArray[np.float64]: + return np.cov(self.blocks, rowvar=False, ddof=1) / self.blocks.shape[0] + + +class RandomBondIsingCylinder: + r"""Matrix-free row transfer operator for the periodic \(\pm J\) RBIM.""" + + def __init__(self, length: int, coupling: float): + if length < 3: + raise ValueError("periodic RBIM cylinder requires length >= 3") + if coupling <= 0.0: + raise ValueError("coupling must be positive") + self.length = int(length) + self.coupling = float(coupling) + states = np.arange(1 << self.length, dtype=np.uint64) + bits = ( + (states[:, None] >> np.arange(self.length, dtype=np.uint64)) & 1 + ) + spins = 2.0 * bits.astype(float) - 1.0 + self._horizontal_products = spins * np.roll(spins, -1, axis=1) + + def horizontal_weight( + self, horizontal_bonds: NDArray[np.int8] + ) -> NDArray[np.float64]: + bonds = np.asarray(horizontal_bonds, dtype=np.int8) + if bonds.shape != (self.length,) or not np.all(np.abs(bonds) == 1): + raise ValueError("horizontal bonds must be a length-L ±1 vector") + return np.exp(self.coupling * (self._horizontal_products @ bonds)) + + def apply_vertical( + self, + vector: NDArray[np.float64], + vertical_bonds: NDArray[np.int8], + ) -> NDArray[np.float64]: + bonds = np.asarray(vertical_bonds, dtype=np.int8) + if bonds.shape != (self.length,) or not np.all(np.abs(bonds) == 1): + raise ValueError("vertical bonds must be a length-L ±1 vector") + output = np.asarray(vector, dtype=float) + if output.shape != (1 << self.length,): + raise ValueError("state vector has the wrong size") + tensor = output.reshape((2,) * self.length) + spin = np.array([-1.0, 1.0]) + products = spin[:, None] * spin[None, :] + for site, bond in enumerate(bonds): + axis = self.length - 1 - site + local = np.exp(self.coupling * bond * products) + tensor = np.tensordot(local, tensor, axes=(1, axis)) + tensor = np.moveaxis(tensor, 0, axis) + return tensor.reshape(-1) + + def apply_row( + self, + vector: NDArray[np.float64], + vertical_bonds: NDArray[np.int8], + horizontal_bonds: NDArray[np.int8], + ) -> NDArray[np.float64]: + output = self.apply_vertical(vector, vertical_bonds) + output *= self.horizontal_weight(horizontal_bonds) + return output + + def dense_row( + self, + vertical_bonds: NDArray[np.int8], + horizontal_bonds: NDArray[np.int8], + ) -> NDArray[np.float64]: + dimension = 1 << self.length + basis = np.eye(dimension) + return np.column_stack( + [ + self.apply_row( + basis[:, column], vertical_bonds, horizontal_bonds + ) + for column in range(dimension) + ] + ) + + +def _random_bonds( + rng: np.random.Generator, length: int, p_antiferromagnetic: float +) -> NDArray[np.int8]: + return np.where( + rng.random(length) < p_antiferromagnetic, -1, 1 + ).astype(np.int8) + + +def estimate_nishimori_free_energy( + length: int, + *, + p_antiferromagnetic: float = NISHIMORI_P_CRITICAL, + rows: int = 20_000, + burn_in: int = 1_000, + block_size: int = 200, + seed: int = 0, +) -> NishimoriEstimate: + r"""Estimate \(-\lambda_1\) with stabilized positive-vector iteration.""" + + if rows < block_size or rows % block_size: + raise ValueError("rows must be a positive multiple of block_size") + if burn_in < 0: + raise ValueError("burn_in must be nonnegative") + coupling = nishimori_coupling(p_antiferromagnetic) + cylinder = RandomBondIsingCylinder(length, coupling) + rng = np.random.default_rng(seed) + vector = np.full(1 << length, 1.0 / (1 << length)) + + def step(current: NDArray[np.float64]) -> tuple[NDArray[np.float64], float]: + vertical = _random_bonds(rng, length, p_antiferromagnetic) + horizontal = _random_bonds(rng, length, p_antiferromagnetic) + updated = cylinder.apply_row(current, vertical, horizontal) + norm = float(np.sum(updated)) + if not np.isfinite(norm) or norm <= 0.0: + raise FloatingPointError("non-finite transfer-vector norm") + return updated / norm, float(np.log(norm)) + + for _ in range(burn_in): + vector, _ = step(vector) + blocks = np.empty(rows // block_size) + for block in range(blocks.size): + log_growth = 0.0 + for _ in range(block_size): + vector, increment = step(vector) + log_growth += increment + blocks[block] = -log_growth / block_size + return NishimoriEstimate( + length=length, + p_antiferromagnetic=float(p_antiferromagnetic), + coupling=coupling, + rows=rows, + burn_in=burn_in, + block_size=block_size, + blocks=blocks, + ) + + +def estimate_coupled_nishimori_free_energies( + lengths: list[int] | NDArray[np.int64], + *, + p_antiferromagnetic: float = NISHIMORI_P_CRITICAL, + rows: int = 20_000, + burn_in: int = 1_000, + block_size: int = 200, + seed: int = 0, +) -> CoupledNishimoriEstimate: + """Evolve widths with nested common random bonds.""" + + size_array = np.asarray(lengths, dtype=int) + if ( + size_array.ndim != 1 + or size_array.size < 3 + or np.unique(size_array).size != size_array.size + ): + raise ValueError("lengths must contain at least three unique widths") + if np.any(size_array < 3): + raise ValueError("all periodic widths must be at least three") + if rows < block_size or rows % block_size: + raise ValueError("rows must be a positive multiple of block_size") + if burn_in < 0: + raise ValueError("burn_in must be nonnegative") + size_array = np.sort(size_array) + coupling = nishimori_coupling(p_antiferromagnetic) + cylinders = [ + RandomBondIsingCylinder(int(length), coupling) + for length in size_array + ] + vectors = [ + np.full(1 << int(length), 1.0 / (1 << int(length))) + for length in size_array + ] + rng = np.random.default_rng(seed) + maximum_length = int(size_array[-1]) + + def coupled_step() -> NDArray[np.float64]: + vertical = _random_bonds(rng, maximum_length, p_antiferromagnetic) + horizontal = _random_bonds(rng, maximum_length, p_antiferromagnetic) + increments = np.empty(size_array.size) + for index, (length, cylinder) in enumerate( + zip(size_array, cylinders, strict=True) + ): + width = int(length) + updated = cylinder.apply_row( + vectors[index], vertical[:width], horizontal[:width] + ) + norm = float(np.sum(updated)) + if not np.isfinite(norm) or norm <= 0.0: + raise FloatingPointError("non-finite transfer-vector norm") + vectors[index] = updated / norm + increments[index] = np.log(norm) + return increments + + for _ in range(burn_in): + coupled_step() + blocks = np.empty((rows // block_size, size_array.size)) + for block in range(blocks.shape[0]): + log_growth = np.zeros(size_array.size) + for _ in range(block_size): + log_growth += coupled_step() + blocks[block] = -log_growth / block_size + return CoupledNishimoriEstimate( + lengths=size_array.astype(np.int64), + p_antiferromagnetic=float(p_antiferromagnetic), + coupling=coupling, + rows=rows, + burn_in=burn_in, + block_size=block_size, + blocks=blocks, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/resolution.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/resolution.py new file mode 100644 index 000000000..29c6dee3a --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/resolution.py @@ -0,0 +1,443 @@ +"""Hidden-history filters for resolution-degraded Born records.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Literal, Protocol + +import numpy as np +from numpy.typing import NDArray + +from .channels import ConfusionChannel, ErasureChannel +from .self_dual import SELF_DUAL_BETA, SelfDualGaussianCylinder + +ObservationChannel = ErasureChannel | ConfusionChannel +GateKind = Literal["zz", "x"] + + +class FilterCylinder(Protocol): + length: int + + def zz_probability_plus(self, state: NDArray, bond: int) -> float: ... + + def x_probability_plus(self, state: NDArray, site: int) -> float: ... + + def update_zz( + self, state: NDArray, bond: int, sign: int + ) -> tuple[NDArray, float]: ... + + def update_x( + self, state: NDArray, site: int, sign: int + ) -> tuple[NDArray, float]: ... + + +@dataclass(frozen=True, slots=True) +class ExactBranch: + """One posterior pure-state branch and its normalized weight.""" + + weight: float + state: NDArray + + +@dataclass(frozen=True, slots=True) +class DegradedRecordEstimate: + """Block estimates of observed-record surprisal rates.""" + + lengths: NDArray[np.int64] + channel_name: str + channel_parameter: float + particles: int + steps: int + burn_in: int + block_size: int + blocks: NDArray[np.float64] + + @property + def means(self) -> NDArray[np.float64]: + return np.mean(self.blocks, axis=0) + + @property + def covariance_of_mean(self) -> NDArray[np.float64]: + return np.cov(self.blocks, rowvar=False, ddof=1) / self.blocks.shape[0] + + +def _gate_probability_plus( + cylinder: FilterCylinder, + state: NDArray, + gate: tuple[GateKind, int], +) -> float: + kind, index = gate + if kind == "zz": + return cylinder.zz_probability_plus(state, index) + return cylinder.x_probability_plus(state, index) + + +def _gate_update( + cylinder: FilterCylinder, + state: NDArray, + gate: tuple[GateKind, int], + sign: int, +) -> tuple[NDArray, float]: + kind, index = gate + if kind == "zz": + return cylinder.update_zz(state, index, sign) + return cylinder.update_x(state, index, sign) + + +def exact_filter_observation( + branches: list[ExactBranch], + cylinder: FilterCylinder, + gate: tuple[GateKind, int], + observed: int, + channel: ObservationChannel, +) -> tuple[list[ExactBranch], float]: + """Apply one observed symbol while summing every latent sign exactly.""" + + descendants: list[ExactBranch] = [] + likelihood = 0.0 + for branch in branches: + probability_plus = _gate_probability_plus( + cylinder, branch.state, gate + ) + for sign, latent_probability in ( + (1, probability_plus), + (-1, 1.0 - probability_plus), + ): + kernel = channel.conditional_probability(observed, sign) + joint_weight = branch.weight * latent_probability * kernel + if joint_weight == 0.0: + continue + updated, checked = _gate_update( + cylinder, branch.state, gate, sign + ) + if not np.isclose(checked, latent_probability, atol=1e-13): + raise FloatingPointError("filter/model Born probabilities differ") + descendants.append(ExactBranch(joint_weight, updated)) + likelihood += joint_weight + if not np.isfinite(likelihood) or likelihood <= 0.0: + raise FloatingPointError("observed symbol has zero filter likelihood") + posterior = [ + ExactBranch(branch.weight / likelihood, branch.state) + for branch in descendants + ] + return posterior, float(np.log(likelihood)) + + +def particle_filter_observation( + particles: list[NDArray], + cylinder: FilterCylinder, + gate: tuple[GateKind, int], + observed: int, + channel: ObservationChannel, + *, + sign_uniforms: NDArray[np.float64], + resample_uniform: float, +) -> tuple[list[NDArray], float]: + """Fully adapted particle update for one degraded observation.""" + + count = len(particles) + if count == 0: + raise ValueError("particle filter requires at least one particle") + uniforms = np.asarray(sign_uniforms, dtype=float) + if uniforms.shape != (count,): + raise ValueError("one sign uniform is required per particle") + proposed: list[NDArray] = [] + weights = np.empty(count) + for index, state in enumerate(particles): + probability_plus = _gate_probability_plus(cylinder, state, gate) + plus_joint = probability_plus * channel.conditional_probability( + observed, 1 + ) + minus_joint = (1.0 - probability_plus) * ( + channel.conditional_probability(observed, -1) + ) + predictive = plus_joint + minus_joint + if predictive <= 0.0: + raise FloatingPointError("particle has zero predictive likelihood") + posterior_plus = plus_joint / predictive + sign = 1 if uniforms[index] < posterior_plus else -1 + updated, _ = _gate_update(cylinder, state, gate, sign) + proposed.append(updated) + weights[index] = predictive + likelihood = float(np.mean(weights)) + normalized = weights / np.sum(weights) + cumulative = np.cumsum(normalized) + start = (float(resample_uniform) % 1.0) / count + points = start + np.arange(count) / count + ancestors = np.searchsorted(cumulative, points, side="right") + resampled = [proposed[int(index)].copy() for index in ancestors] + return resampled, float(np.log(likelihood)) + + +def gaussian_particle_filter_observation( + particles: NDArray[np.float64], + cylinder: SelfDualGaussianCylinder, + gate: tuple[GateKind, int], + observed: int, + channel: ObservationChannel, + *, + sign_uniforms: NDArray[np.float64], + resample_uniform: float, +) -> tuple[NDArray[np.float64], float]: + """Vectorized fully adapted update for Gaussian covariance particles.""" + + covariances = np.asarray(particles, dtype=float) + if ( + covariances.ndim != 3 + or covariances.shape[0] < 1 + or covariances.shape[1:] + != (cylinder.majorana_dimension, cylinder.majorana_dimension) + ): + raise ValueError( + "particles must have shape (count, 2L, 2L)" + ) + count = covariances.shape[0] + uniforms = np.asarray(sign_uniforms, dtype=float) + if uniforms.shape != (count,): + raise ValueError("one sign uniform is required per particle") + kind, index = gate + if kind == "zz": + first, second = cylinder._zz_indices(index) + elif kind == "x": + if not 0 <= index < cylinder.length: + raise ValueError("site index out of range") + first, second = 2 * index, 2 * index + 1 + else: + raise ValueError("unknown gate kind") + probabilities_plus = cylinder.bilinear_probability_plus_batch( + covariances, first, second + ) + plus_kernel = channel.conditional_probability(observed, 1) + minus_kernel = channel.conditional_probability(observed, -1) + plus_joint = probabilities_plus * plus_kernel + minus_joint = (1.0 - probabilities_plus) * minus_kernel + predictive = plus_joint + minus_joint + if np.any(predictive <= 0.0) or not np.all(np.isfinite(predictive)): + raise FloatingPointError( + "particle has nonpositive predictive likelihood" + ) + posterior_plus = plus_joint / predictive + signs = np.where(uniforms < posterior_plus, 1, -1).astype(np.int8) + proposed, _ = cylinder.update_bilinear_batch( + covariances, first, second, signs + ) + likelihood = float(np.mean(predictive)) + normalized = predictive / np.sum(predictive) + cumulative = np.cumsum(normalized) + start = (float(resample_uniform) % 1.0) / count + points = start + np.arange(count) / count + ancestors = np.searchsorted(cumulative, points, side="right") + return proposed[ancestors].copy(), float(np.log(likelihood)) + + +def _observe_latent( + latent: int, + channel: ObservationChannel, + uniform: float, +) -> int: + if isinstance(channel, ErasureChannel): + return latent if uniform < channel.retain_probability else 0 + return -latent if uniform < channel.error_probability else latent + + +def _sample_true_gate( + cylinder: SelfDualGaussianCylinder, + state: NDArray[np.float64], + gate: tuple[GateKind, int], + uniform: float, +) -> tuple[NDArray[np.float64], int]: + kind, index = gate + if kind == "zz": + updated, _, sign = cylinder.sample_zz(state, index, uniform) + else: + updated, _, sign = cylinder.sample_x(state, index, uniform) + return updated, sign + + +def estimate_degraded_record_rates( + lengths: list[int] | NDArray[np.int64], + channel: ObservationChannel, + *, + beta: float = SELF_DUAL_BETA, + particles: int = 256, + steps: int = 2_000, + burn_in: int = 200, + block_size: int = 100, + seed: int = 0, + batched: bool = True, +) -> DegradedRecordEstimate: + """Estimate observed-record entropy using hidden-history filtering.""" + + sizes = np.sort(np.asarray(lengths, dtype=int)) + if ( + sizes.ndim != 1 + or sizes.size < 3 + or np.unique(sizes).size != sizes.size + or np.any(sizes < 3) + ): + raise ValueError("lengths need at least three unique values >= 3") + if particles < 1: + raise ValueError("particles must be positive") + if steps < block_size or steps % block_size: + raise ValueError("steps must be a positive multiple of block_size") + if burn_in < 0: + raise ValueError("burn_in must be nonnegative") + exact_rate: NDArray[np.float64] | None = None + if isinstance(channel, ErasureChannel) and channel.retain_probability == 0.0: + exact_rate = np.zeros(sizes.size) + elif ( + isinstance(channel, ConfusionChannel) + and channel.error_probability == 0.5 + ): + # Each of the 2L observed gate outcomes is an independent fair bit, + # irrespective of the latent Born history. + exact_rate = 2.0 * sizes * np.log(2.0) + if exact_rate is not None: + blocks = np.broadcast_to( + exact_rate, + (steps // block_size, sizes.size), + ).copy() + parameter = ( + channel.retain_probability + if isinstance(channel, ErasureChannel) + else channel.error_probability + ) + return DegradedRecordEstimate( + lengths=sizes.astype(np.int64), + channel_name=( + "erasure" + if isinstance(channel, ErasureChannel) + else "confusion" + ), + channel_parameter=float(parameter), + particles=particles, + steps=steps, + burn_in=burn_in, + block_size=block_size, + blocks=blocks, + ) + cylinders = [ + SelfDualGaussianCylinder(int(length), beta) for length in sizes + ] + true_states = [cylinder.plus_covariance() for cylinder in cylinders] + if batched: + ensembles: list[NDArray[np.float64] | list[NDArray[np.float64]]] = [ + np.repeat( + cylinder.plus_covariance()[None, :, :], + particles, + axis=0, + ) + for cylinder in cylinders + ] + else: + ensembles = [ + [cylinder.plus_covariance() for _ in range(particles)] + for cylinder in cylinders + ] + data_rng = np.random.default_rng(seed) + readout_rng = np.random.default_rng( + np.random.SeedSequence([seed, 0xC0A2]) + ) + filter_rngs = [ + np.random.default_rng( + np.random.SeedSequence([seed, int(length), 0xCEFF]) + ) + for length in sizes + ] + + def row() -> NDArray[np.float64]: + increments = np.zeros(sizes.size) + max_length = int(sizes[-1]) + zz_uniforms = data_rng.random(max_length) + x_uniforms = data_rng.random(max_length) + zz_channel_uniforms = readout_rng.random(max_length) + x_channel_uniforms = readout_rng.random(max_length) + for width_index, (length, cylinder, filter_rng) in enumerate( + zip(sizes, cylinders, filter_rngs, strict=True) + ): + width = int(length) + gates = [ + *(("zz", index) for index in range(width)), + *(("x", index) for index in range(width)), + ] + for gate in gates: + kind, index = gate + latent_uniform = ( + zz_uniforms[index] + if kind == "zz" + else x_uniforms[index] + ) + channel_uniform = ( + zz_channel_uniforms[index] + if kind == "zz" + else x_channel_uniforms[index] + ) + true_states[width_index], sign = _sample_true_gate( + cylinder, + true_states[width_index], + gate, + float(latent_uniform), + ) + observed = _observe_latent( + sign, channel, float(channel_uniform) + ) + sign_uniforms = filter_rng.random(particles) + resample_uniform = float(filter_rng.random()) + if batched: + batch = ensembles[width_index] + if not isinstance(batch, np.ndarray): + raise TypeError("batched ensemble storage is invalid") + ensembles[width_index], log_likelihood = ( + gaussian_particle_filter_observation( + batch, + cylinder, + gate, + observed, + channel, + sign_uniforms=sign_uniforms, + resample_uniform=resample_uniform, + ) + ) + else: + scalar = ensembles[width_index] + if isinstance(scalar, np.ndarray): + raise TypeError("scalar ensemble storage is invalid") + ensembles[width_index], log_likelihood = ( + particle_filter_observation( + scalar, + cylinder, + gate, + observed, + channel, + sign_uniforms=sign_uniforms, + resample_uniform=resample_uniform, + ) + ) + increments[width_index] += log_likelihood + return increments + + for _ in range(burn_in): + row() + blocks = np.empty((steps // block_size, sizes.size)) + for block in range(blocks.shape[0]): + total = np.zeros(sizes.size) + for _ in range(block_size): + total += row() + blocks[block] = -total / block_size + parameter = ( + channel.retain_probability + if isinstance(channel, ErasureChannel) + else channel.error_probability + ) + return DegradedRecordEstimate( + lengths=sizes.astype(np.int64), + channel_name=( + "erasure" if isinstance(channel, ErasureChannel) else "confusion" + ), + channel_parameter=float(parameter), + particles=particles, + steps=steps, + burn_in=burn_in, + block_size=block_size, + blocks=blocks, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/runner.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/runner.py new file mode 100644 index 000000000..7b9d6e9e6 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/runner.py @@ -0,0 +1,179 @@ +"""Reproducible one-cell execution and fail-closed result manifests.""" + +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile +from typing import Any + +import numpy as np +from numpy.typing import NDArray + +from .channels import ConfusionChannel, ErasureChannel +from .clean_ising import critical_ground_energy +from .nishimori import estimate_coupled_nishimori_free_energies +from .resolution import estimate_degraded_record_rates +from .schema import CellConfig, CellManifest +from .self_dual import estimate_coupled_gaussian_self_dual_record_rates + + +def _git_commit() -> str: + declared = os.environ.get("CEFFFLOW_SOURCE_COMMIT") + if declared is not None: + if re.fullmatch(r"[0-9a-f]{40}", declared) is None: + raise ValueError( + "CEFFFLOW_SOURCE_COMMIT must be a 40-character lowercase " + "Git commit" + ) + return declared + try: + return subprocess.run( + ["git", "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + except (OSError, subprocess.CalledProcessError): + return "unknown" + + +def _atomic_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + with temporary.open("w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2, sort_keys=True) + handle.write("\n") + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, path) + + +def _atomic_blocks(path: Path, blocks: NDArray[np.float64]) -> str: + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + dir=path.parent, prefix=".blocks-", suffix=".npz", delete=False + ) as handle: + temporary = Path(handle.name) + try: + np.savez_compressed(temporary, blocks=np.asarray(blocks, dtype=float)) + with temporary.open("rb") as handle: + os.fsync(handle.fileno()) + digest = hashlib.sha256(temporary.read_bytes()).hexdigest() + os.replace(temporary, path) + return digest + finally: + temporary.unlink(missing_ok=True) + + +def execute_cell(config: CellConfig) -> NDArray[np.float64]: + """Execute one validated cell and return paired width blocks.""" + + if config.model == "clean_ising": + return critical_ground_energy(config.lengths)[None, :] + if config.model == "nishimori": + return estimate_coupled_nishimori_free_energies( + config.lengths, + rows=config.steps, + burn_in=config.burn_in, + block_size=config.block_size, + seed=config.seed, + ).blocks + if config.channel.kind == "identity": + return estimate_coupled_gaussian_self_dual_record_rates( + config.lengths, + steps=config.steps, + burn_in=config.burn_in, + block_size=config.block_size, + seed=config.seed, + ).blocks + channel = ( + ErasureChannel(config.channel.parameter) + if config.channel.kind == "erasure" + else ConfusionChannel(config.channel.parameter) + ) + return estimate_degraded_record_rates( + config.lengths, + channel, + particles=config.particles, + steps=config.steps, + burn_in=config.burn_in, + block_size=config.block_size, + seed=config.seed, + ).blocks + + +def run_cell( + config: CellConfig, + output_directory: str | Path, + *, + cell_id: str, +) -> CellManifest: + """Execute a cell and atomically publish blocks then its manifest.""" + + output = Path(output_directory) + output.mkdir(parents=True, exist_ok=True) + manifest_path = output / "manifest.json" + try: + blocks = execute_cell(config) + finite = bool(np.all(np.isfinite(blocks))) + if not finite: + raise FloatingPointError("cell produced non-finite blocks") + digest = _atomic_blocks(output / "blocks.npz", blocks) + manifest = CellManifest( + status="success", + cell_id=cell_id, + settings=config, + provenance={"git_commit": _git_commit(), "numpy": np.__version__}, + normalization_ok=True, + finite_blocks=True, + blocks_sha256=digest, + ) + except Exception as exc: + manifest = CellManifest( + status="failed", + cell_id=cell_id, + settings=config, + provenance={"git_commit": _git_commit(), "numpy": np.__version__}, + normalization_ok=False, + finite_blocks=False, + blocks_sha256="0" * 64, + error=f"{type(exc).__name__}: {exc}", + ) + _atomic_json(manifest_path, manifest.model_dump(mode="json")) + raise + _atomic_json(manifest_path, manifest.model_dump(mode="json")) + return manifest + + +def cell_from_run_spec( + run_spec_path: str | Path, + cell_id: str, +) -> tuple[CellConfig, Path]: + """Resolve a cell and its result directory from a run specification.""" + + path = Path(run_spec_path) + payload = json.loads(path.read_text(encoding="utf-8")) + matches = [ + cell for cell in payload["cells"] if str(cell["cell_id"]) == cell_id + ] + if len(matches) != 1: + raise ValueError(f"expected exactly one cell named {cell_id!r}") + declared_root = payload.get("result_root") + if declared_root is None: + root = (path.parent / "cells").resolve() + else: + root = Path(declared_root) + if declared_root is not None and not root.is_absolute(): + root = (path.parent / root).resolve() + cell = matches[0] + settings = cell.get("settings") + if settings is None: + settings = cell.get("params", {}).get("settings") + if settings is None: + raise ValueError(f"cell {cell_id!r} does not contain settings") + return CellConfig.model_validate(settings), root / cell_id diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/schema.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/schema.py new file mode 100644 index 000000000..0d5e06540 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/schema.py @@ -0,0 +1,66 @@ +"""Strict configuration and result schemas for ceffflow cells.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class ChannelSpec(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + kind: Literal["identity", "erasure", "confusion"] + parameter: float = 0.0 + + @model_validator(mode="after") + def validate_domain(self) -> "ChannelSpec": + parameter = float(self.parameter) + if self.kind == "identity" and parameter != 0.0: + raise ValueError("identity channel parameter must be zero") + if self.kind == "erasure" and not 0.0 <= parameter <= 1.0: + raise ValueError("erasure retain probability must lie in [0, 1]") + if self.kind == "confusion" and not 0.0 <= parameter <= 0.5: + raise ValueError("confusion error probability must lie in [0, 1/2]") + return self + + +class CellConfig(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True) + + model: Literal["clean_ising", "nishimori", "self_dual"] + lengths: list[int] = Field(min_length=3) + channel: ChannelSpec + steps: int = Field(gt=0) + burn_in: int = Field(ge=0) + block_size: int = Field(gt=0) + seed: int = Field(ge=0) + particles: int = Field(default=256, gt=0) + + @model_validator(mode="after") + def validate_grid(self) -> "CellConfig": + if len(set(self.lengths)) != len(self.lengths): + raise ValueError("lengths must be unique") + if any(length < 2 for length in self.lengths): + raise ValueError("all lengths must be at least two") + if self.steps % self.block_size: + raise ValueError("steps must be a multiple of block_size") + if self.model == "nishimori" and self.channel.kind != "identity": + raise ValueError( + "Nishimori is a calibration model and only supports identity" + ) + return self + + +class CellManifest(BaseModel): + model_config = ConfigDict(extra="forbid") + + schema_version: str = "1.0" + status: Literal["success", "failed"] + cell_id: str = Field(min_length=1) + settings: CellConfig + provenance: dict[str, str] + normalization_ok: bool + finite_blocks: bool + blocks_sha256: str = Field(pattern=r"^[0-9a-f]{64}$") + error: str | None = None diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/self_dual.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/self_dual.py new file mode 100644 index 000000000..3ca08797c --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/src/ceffflow/self_dual.py @@ -0,0 +1,585 @@ +"""Born-sampled weak self-dual monitored Ising circuit.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +from numpy.typing import NDArray + +SELF_DUAL_BETA = float(np.arctanh(1.0 / np.sqrt(2.0))) + + +@dataclass(frozen=True, slots=True) +class CoupledSelfDualEstimate: + """Paired Shannon-rate blocks for several periodic circumferences.""" + + lengths: NDArray[np.int64] + beta: float + steps: int + burn_in: int + block_size: int + blocks: NDArray[np.float64] + + @property + def means(self) -> NDArray[np.float64]: + return np.mean(self.blocks, axis=0) + + @property + def covariance_of_mean(self) -> NDArray[np.float64]: + return np.cov(self.blocks, rowvar=False, ddof=1) / self.blocks.shape[0] + + +class SelfDualBornCylinder: + r"""Exact conditional state for the self-dual nonunitary Ising circuit.""" + + def __init__(self, length: int, beta: float = SELF_DUAL_BETA): + if length < 3: + raise ValueError("periodic self-dual cylinder requires L >= 3") + if beta <= 0.0: + raise ValueError("beta must be positive") + self.length = int(length) + self.beta = float(beta) + self.dimension = 1 << self.length + states = np.arange(self.dimension, dtype=np.uint64) + bits = ( + (states[:, None] >> np.arange(self.length, dtype=np.uint64)) & 1 + ) + spins = 2.0 * bits.astype(float) - 1.0 + self._zz = (spins * np.roll(spins, -1, axis=1)).T + self._flip = np.asarray( + [ + states ^ (np.uint64(1) << np.uint64(site)) + for site in range(self.length) + ] + ) + self._cosh_half = float(np.cosh(self.beta / 2.0)) + self._sinh_half = float(np.sinh(self.beta / 2.0)) + self._normalization = float(np.sqrt(2.0 * np.cosh(self.beta))) + self._tanh = float(np.tanh(self.beta)) + + def plus_state(self) -> NDArray[np.complex128]: + return np.full( + self.dimension, + 1.0 / np.sqrt(self.dimension), + dtype=np.complex128, + ) + + @staticmethod + def _sample_sign( + expectation: float, tanh_beta: float, uniform: float + ) -> tuple[int, float]: + expectation = float(np.clip(expectation, -1.0, 1.0)) + probability_plus = 0.5 * (1.0 + tanh_beta * expectation) + sign = 1 if uniform < probability_plus else -1 + probability = probability_plus if sign == 1 else 1.0 - probability_plus + return sign, float(probability) + + def sample_zz( + self, + state: NDArray[np.complex128], + bond: int, + uniform: float, + ) -> tuple[NDArray[np.complex128], float, int]: + probability_plus = self.zz_probability_plus(state, bond) + sign = 1 if uniform < probability_plus else -1 + probability = ( + probability_plus if sign == 1 else 1.0 - probability_plus + ) + updated, checked_probability = self.update_zz(state, bond, sign) + if not np.isclose(probability, checked_probability, atol=1e-14): + raise FloatingPointError("inconsistent ZZ Born probability") + return updated, probability, sign + + def zz_probability_plus( + self, state: NDArray[np.complex128], bond: int + ) -> float: + expectation = float(np.abs(state) ** 2 @ self._zz[bond]) + return float(0.5 * (1.0 + self._tanh * expectation)) + + def update_zz( + self, state: NDArray[np.complex128], bond: int, sign: int + ) -> tuple[NDArray[np.complex128], float]: + if sign not in (-1, 1): + raise ValueError("measurement sign must be -1 or 1") + probability_plus = self.zz_probability_plus(state, bond) + probability = probability_plus if sign == 1 else 1.0 - probability_plus + factor = np.exp(0.5 * sign * self.beta * self._zz[bond]) + updated = state * factor / self._normalization + updated /= np.sqrt(probability) + return updated, float(probability) + + def sample_x( + self, + state: NDArray[np.complex128], + site: int, + uniform: float, + ) -> tuple[NDArray[np.complex128], float, int]: + probability_plus = self.x_probability_plus(state, site) + sign = 1 if uniform < probability_plus else -1 + probability = ( + probability_plus if sign == 1 else 1.0 - probability_plus + ) + updated, checked_probability = self.update_x(state, site, sign) + if not np.isclose(probability, checked_probability, atol=1e-14): + raise FloatingPointError("inconsistent X Born probability") + return updated, probability, sign + + def x_probability_plus( + self, state: NDArray[np.complex128], site: int + ) -> float: + flipped = state[self._flip[site]] + expectation = float(np.real(np.vdot(state, flipped))) + return float(0.5 * (1.0 + self._tanh * expectation)) + + def x_pair_expectations( + self, + state: NDArray[np.complex128], + first: int, + second: int, + ) -> tuple[float, float, float]: + """Return , , and .""" + + if first == second: + raise ValueError("sites must be distinct") + if not ( + 0 <= first < self.length and 0 <= second < self.length + ): + raise ValueError("sites must be in range") + if state.shape != (self.dimension,): + raise ValueError("state has incompatible dimension") + first_flipped = state[self._flip[first]] + second_flipped = state[self._flip[second]] + pair_flipped = state[self._flip[first]][self._flip[second]] + return ( + float(np.real(np.vdot(state, first_flipped))), + float(np.real(np.vdot(state, second_flipped))), + float(np.real(np.vdot(state, pair_flipped))), + ) + + def update_x( + self, state: NDArray[np.complex128], site: int, sign: int + ) -> tuple[NDArray[np.complex128], float]: + if sign not in (-1, 1): + raise ValueError("measurement sign must be -1 or 1") + probability_plus = self.x_probability_plus(state, site) + probability = probability_plus if sign == 1 else 1.0 - probability_plus + flipped = state[self._flip[site]] + updated = ( + self._cosh_half * state + sign * self._sinh_half * flipped + ) / self._normalization + updated /= np.sqrt(probability) + return updated, float(probability) + + def sample_row( + self, + state: NDArray[np.complex128], + zz_uniforms: NDArray[np.float64], + x_uniforms: NDArray[np.float64], + ) -> tuple[NDArray[np.complex128], float]: + if zz_uniforms.shape != (self.length,) or x_uniforms.shape != ( + self.length, + ): + raise ValueError("each row needs L uniforms for both gate families") + log_probability = 0.0 + for bond, uniform in enumerate(zz_uniforms): + state, probability, _ = self.sample_zz( + state, bond, float(uniform) + ) + log_probability += np.log(probability) + for site, uniform in enumerate(x_uniforms): + state, probability, _ = self.sample_x( + state, site, float(uniform) + ) + log_probability += np.log(probability) + return state, float(log_probability) + + +class SelfDualGaussianCylinder: + r"""Pure-Gaussian covariance implementation of the same Born circuit.""" + + def __init__(self, length: int, beta: float = SELF_DUAL_BETA): + if length < 3: + raise ValueError("periodic self-dual cylinder requires L >= 3") + if beta <= 0.0: + raise ValueError("beta must be positive") + self.length = int(length) + self.beta = float(beta) + self.majorana_dimension = 2 * self.length + self._tanh = float(np.tanh(self.beta)) + self._cosh = float(np.cosh(self.beta)) + self._sinh = float(np.sinh(self.beta)) + + def plus_covariance(self) -> NDArray[np.float64]: + covariance = np.zeros( + (self.majorana_dimension, self.majorana_dimension) + ) + for site in range(self.length): + first = 2 * site + covariance[first, first + 1] = 1.0 + covariance[first + 1, first] = -1.0 + return covariance + + def _sample_bilinear( + self, + covariance: NDArray[np.float64], + first: int, + second: int, + uniform: float, + ) -> tuple[NDArray[np.float64], float, int]: + probability_plus = self._bilinear_probability_plus( + covariance, first, second + ) + sign = 1 if uniform < probability_plus else -1 + probability = ( + probability_plus if sign == 1 else 1.0 - probability_plus + ) + updated, checked_probability = self._update_bilinear( + covariance, first, second, sign + ) + if not np.isclose(probability, checked_probability, atol=1e-14): + raise FloatingPointError("inconsistent Gaussian Born probability") + return updated, probability, sign + + def _bilinear_probability_plus( + self, + covariance: NDArray[np.float64], + first: int, + second: int, + ) -> float: + expectation = float(covariance[first, second]) + return float(0.5 * (1.0 + self._tanh * expectation)) + + def bilinear_probability_plus_batch( + self, + covariances: NDArray[np.float64], + first: int, + second: int, + ) -> NDArray[np.float64]: + """Return plus probabilities for a leading batch of covariances.""" + + values = np.asarray(covariances, dtype=float) + if values.ndim != 3: + raise ValueError( + "covariances must have shape (particles, 2L, 2L)" + ) + expected_shape = ( + values.shape[0], + self.majorana_dimension, + self.majorana_dimension, + ) + if values.shape != expected_shape: + raise ValueError( + "covariances must have shape (particles, 2L, 2L)" + ) + if not ( + 0 <= first < self.majorana_dimension + and 0 <= second < self.majorana_dimension + and first != second + ): + raise ValueError("bilinear indices must be distinct and in range") + expectations = values[:, first, second] + return 0.5 * (1.0 + self._tanh * expectations) + + def _update_bilinear( + self, + covariance: NDArray[np.float64], + first: int, + second: int, + sign: int, + ) -> tuple[NDArray[np.float64], float]: + if sign not in (-1, 1): + raise ValueError("measurement sign must be -1 or 1") + expectation = float(covariance[first, second]) + probability_plus = self._bilinear_probability_plus( + covariance, first, second + ) + probability = probability_plus if sign == 1 else 1.0 - probability_plus + denominator = self._cosh + sign * self._sinh * expectation + outside = np.ones(self.majorana_dimension, dtype=bool) + outside[[first, second]] = False + indices = np.flatnonzero(outside) + updated = np.zeros_like(covariance) + submatrix = covariance[np.ix_(indices, indices)] + wick = ( + expectation * submatrix + + np.outer(covariance[indices, first], covariance[second, indices]) + - np.outer(covariance[indices, second], covariance[first, indices]) + ) + updated[np.ix_(indices, indices)] = ( + self._cosh * submatrix + sign * self._sinh * wick + ) / denominator + updated[first, indices] = covariance[first, indices] / denominator + updated[indices, first] = -updated[first, indices] + updated[second, indices] = covariance[second, indices] / denominator + updated[indices, second] = -updated[second, indices] + updated[first, second] = ( + self._cosh * expectation + sign * self._sinh + ) / denominator + updated[second, first] = -updated[first, second] + return updated, float(probability) + + def update_bilinear_batch( + self, + covariances: NDArray[np.float64], + first: int, + second: int, + signs: NDArray[np.int8], + ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Apply one signed bilinear update to every covariance in a batch.""" + + values = np.asarray(covariances, dtype=float) + probabilities_plus = self.bilinear_probability_plus_batch( + values, first, second + ) + outcomes = np.asarray(signs, dtype=np.int8) + if outcomes.shape != (values.shape[0],) or not np.all( + np.isin(outcomes, (-1, 1)) + ): + raise ValueError("signs must be one ±1 value per particle") + expectations = values[:, first, second] + denominators = ( + self._cosh + outcomes * self._sinh * expectations + ) + outside = np.ones(self.majorana_dimension, dtype=bool) + outside[[first, second]] = False + indices = np.flatnonzero(outside) + submatrices = values[ + :, indices[:, None], indices[None, :] + ] + wick = ( + expectations[:, None, None] * submatrices + + np.einsum( + "pi,pj->pij", + values[:, indices, first], + values[:, second, indices], + ) + - np.einsum( + "pi,pj->pij", + values[:, indices, second], + values[:, first, indices], + ) + ) + updated = np.zeros_like(values) + updated[:, indices[:, None], indices[None, :]] = ( + self._cosh * submatrices + + outcomes[:, None, None] * self._sinh * wick + ) / denominators[:, None, None] + updated[:, first, indices] = ( + values[:, first, indices] / denominators[:, None] + ) + updated[:, indices, first] = -updated[:, first, indices] + updated[:, second, indices] = ( + values[:, second, indices] / denominators[:, None] + ) + updated[:, indices, second] = -updated[:, second, indices] + updated[:, first, second] = ( + self._cosh * expectations + outcomes * self._sinh + ) / denominators + updated[:, second, first] = -updated[:, first, second] + selected_probabilities = np.where( + outcomes == 1, probabilities_plus, 1.0 - probabilities_plus + ) + return updated, selected_probabilities + + def zz_probability_plus( + self, covariance: NDArray[np.float64], bond: int + ) -> float: + first, second = self._zz_indices(bond) + return self._bilinear_probability_plus(covariance, first, second) + + def update_zz( + self, covariance: NDArray[np.float64], bond: int, sign: int + ) -> tuple[NDArray[np.float64], float]: + first, second = self._zz_indices(bond) + return self._update_bilinear(covariance, first, second, sign) + + def _zz_indices(self, bond: int) -> tuple[int, int]: + if not 0 <= bond < self.length: + raise ValueError("bond index out of range") + if bond == self.length - 1: + return 0, self.majorana_dimension - 1 + return 2 * bond + 1, 2 * bond + 2 + + def sample_zz( + self, + covariance: NDArray[np.float64], + bond: int, + uniform: float, + ) -> tuple[NDArray[np.float64], float, int]: + first, second = self._zz_indices(bond) + return self._sample_bilinear( + covariance, first, second, uniform + ) + + def x_probability_plus( + self, covariance: NDArray[np.float64], site: int + ) -> float: + if not 0 <= site < self.length: + raise ValueError("site index out of range") + return self._bilinear_probability_plus( + covariance, 2 * site, 2 * site + 1 + ) + + def update_x( + self, covariance: NDArray[np.float64], site: int, sign: int + ) -> tuple[NDArray[np.float64], float]: + if not 0 <= site < self.length: + raise ValueError("site index out of range") + return self._update_bilinear( + covariance, 2 * site, 2 * site + 1, sign + ) + + def sample_x( + self, + covariance: NDArray[np.float64], + site: int, + uniform: float, + ) -> tuple[NDArray[np.float64], float, int]: + return self._sample_bilinear( + covariance, 2 * site, 2 * site + 1, uniform + ) + + def sample_row( + self, + covariance: NDArray[np.float64], + zz_uniforms: NDArray[np.float64], + x_uniforms: NDArray[np.float64], + ) -> tuple[NDArray[np.float64], float]: + if zz_uniforms.shape != (self.length,) or x_uniforms.shape != ( + self.length, + ): + raise ValueError("each row needs L uniforms for both gate families") + log_probability = 0.0 + for bond, uniform in enumerate(zz_uniforms): + covariance, probability, _ = self.sample_zz( + covariance, bond, float(uniform) + ) + log_probability += np.log(probability) + for site, uniform in enumerate(x_uniforms): + covariance, probability, _ = self.sample_x( + covariance, site, float(uniform) + ) + log_probability += np.log(probability) + return covariance, float(log_probability) + + +def _validated_lengths( + lengths: list[int] | NDArray[np.int64], + steps: int, + burn_in: int, + block_size: int, +) -> NDArray[np.int64]: + sizes = np.asarray(lengths, dtype=int) + if ( + sizes.ndim != 1 + or sizes.size < 3 + or np.unique(sizes).size != sizes.size + ): + raise ValueError("lengths must contain at least three unique widths") + if np.any(sizes < 3): + raise ValueError("all widths must be at least three") + if steps < block_size or steps % block_size: + raise ValueError("steps must be a positive multiple of block_size") + if burn_in < 0: + raise ValueError("burn_in must be nonnegative") + return np.sort(sizes).astype(np.int64) + + +def _estimate_coupled( + lengths: list[int] | NDArray[np.int64], + *, + beta: float, + steps: int, + burn_in: int, + block_size: int, + seed: int, + gaussian: bool, +) -> CoupledSelfDualEstimate: + sizes = _validated_lengths(lengths, steps, burn_in, block_size) + if gaussian: + cylinders = [ + SelfDualGaussianCylinder(int(length), beta) for length in sizes + ] + states = [cylinder.plus_covariance() for cylinder in cylinders] + else: + cylinders = [ + SelfDualBornCylinder(int(length), beta) for length in sizes + ] + states = [cylinder.plus_state() for cylinder in cylinders] + rng = np.random.default_rng(seed) + maximum_length = int(sizes[-1]) + + def coupled_step() -> NDArray[np.float64]: + zz_uniforms = rng.random(maximum_length) + x_uniforms = rng.random(maximum_length) + log_probabilities = np.empty(sizes.size) + for index, (length, cylinder) in enumerate( + zip(sizes, cylinders, strict=True) + ): + width = int(length) + states[index], log_probabilities[index] = cylinder.sample_row( + states[index], zz_uniforms[:width], x_uniforms[:width] + ) + return log_probabilities + + for _ in range(burn_in): + coupled_step() + blocks = np.empty((steps // block_size, sizes.size)) + for block in range(blocks.shape[0]): + total = np.zeros(sizes.size) + for _ in range(block_size): + total += coupled_step() + blocks[block] = -total / block_size + return CoupledSelfDualEstimate( + lengths=sizes, + beta=float(beta), + steps=steps, + burn_in=burn_in, + block_size=block_size, + blocks=blocks, + ) + + +def estimate_coupled_self_dual_record_rates( + lengths: list[int] | NDArray[np.int64], + *, + beta: float = SELF_DUAL_BETA, + steps: int = 20_000, + burn_in: int = 1_000, + block_size: int = 200, + seed: int = 0, +) -> CoupledSelfDualEstimate: + """Estimate exact-spin Born-record Shannon rates under common uniforms.""" + + return _estimate_coupled( + lengths, + beta=beta, + steps=steps, + burn_in=burn_in, + block_size=block_size, + seed=seed, + gaussian=False, + ) + + +def estimate_coupled_gaussian_self_dual_record_rates( + lengths: list[int] | NDArray[np.int64], + *, + beta: float = SELF_DUAL_BETA, + steps: int = 20_000, + burn_in: int = 1_000, + block_size: int = 200, + seed: int = 0, +) -> CoupledSelfDualEstimate: + """Estimate exact-Gaussian Born-record Shannon rates.""" + + return _estimate_coupled( + lengths, + beta=beta, + steps=steps, + burn_in=burn_in, + block_size=block_size, + seed=seed, + gaussian=True, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_analysis.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_analysis.py new file mode 100644 index 000000000..e2abd8656 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_analysis.py @@ -0,0 +1,130 @@ +import numpy as np + +from ceffflow.analysis import fit_window_audit, reblocking_audit +from ceffflow.fits import casimir_gls +from ceffflow.runner import execute_cell +from ceffflow.schema import CellConfig + + +def test_fit_window_audit_reports_lmin_and_leave_one_out_variants(): + lengths = [6, 8, 10, 12, 14, 16] + config = CellConfig( + model="nishimori", + lengths=lengths, + channel={"kind": "identity", "parameter": 0.0}, + steps=200, + burn_in=10, + block_size=10, + seed=0, + particles=1, + ) + sizes = np.asarray(lengths, dtype=float) + curve = -1.3 * sizes - np.pi * 0.464 / (6.0 * sizes) + rng = np.random.default_rng(122) + blocks = curve + rng.normal(scale=2e-5, size=(20, sizes.size)) + audit = fit_window_audit(config, blocks) + labels = {variant["label"] for variant in audit["variants"]} + assert "without_l3" in labels + assert "lmin_8" in labels + assert "omit_6" in labels + assert audit["baseline"]["samples"] == 20 + assert isinstance(audit["stable_within_two_combined_se"], bool) + + +def test_fit_window_audit_baseline_uses_width_covariance_gls(): + lengths = [6, 8, 10, 12, 14, 16] + config = CellConfig( + model="nishimori", + lengths=lengths, + channel={"kind": "identity", "parameter": 0.0}, + steps=400, + burn_in=10, + block_size=10, + seed=0, + particles=1, + ) + sizes = np.asarray(lengths, dtype=float) + curve = ( + -1.1 * sizes + - np.pi * 0.464 / (6.0 * sizes) + + 0.2 / sizes**3 + + 0.7 / sizes**5 + ) + rng = np.random.default_rng(7) + scales = np.asarray([2e-4, 3e-5, 8e-5, 2e-5, 6e-5, 1e-5]) + blocks = curve + rng.normal(size=(40, sizes.size)) * scales + covariance_of_mean = np.cov(blocks, rowvar=False, ddof=1) / blocks.shape[0] + expected = casimir_gls( + sizes, + blocks.mean(axis=0), + covariance_of_mean, + alpha=1.0, + include_l3=True, + compute_leave_one_out=False, + ) + baseline = fit_window_audit(config, blocks)["baseline"] + assert np.isclose( + baseline["central_charge"], expected.central_charge, atol=1e-12 + ) + assert np.isclose( + baseline["standard_error"], expected.standard_error, atol=1e-12 + ) + + +def test_fit_window_audit_skips_exact_clean_calibration(): + config = CellConfig( + model="clean_ising", + lengths=[6, 8, 10, 12], + channel={"kind": "identity", "parameter": 0.0}, + steps=20, + burn_in=0, + block_size=10, + seed=0, + particles=1, + ) + audit = fit_window_audit(config, np.zeros((1, 4))) + assert audit["status"] == "not_applicable_exact_calibration" + + +def test_fit_window_audit_marks_complete_loss_as_analytic(): + endpoints = [("confusion", 0.5), ("erasure", 0.0)] + for kind, parameter in endpoints: + config = CellConfig( + model="self_dual", + lengths=[6, 8, 10, 12, 14, 16], + channel={"kind": kind, "parameter": parameter}, + steps=20, + burn_in=0, + block_size=10, + seed=0, + particles=1, + ) + audit = fit_window_audit(config, execute_cell(config)) + assert audit["status"] == "not_applicable_analytic_endpoint" + assert audit["exact_central_charge"] == -0.5 + assert abs(audit["finite_width_bias"]) < 3e-5 + + +def test_reblocking_audit_preserves_seed_boundaries_and_reports_scales(): + config = CellConfig( + model="self_dual", + lengths=[6, 8, 10, 12, 14, 16], + channel={"kind": "identity", "parameter": 0.0}, + steps=100, + burn_in=0, + block_size=5, + seed=0, + particles=1, + ) + rng = np.random.default_rng(122) + arrays = [rng.normal(size=(20, 6)) for _ in range(8)] + audit = reblocking_audit(config, arrays) + assert audit["baseline"]["samples"] == 160 + assert [item["factor"] for item in audit["variants"]] == [2, 4, 5, 10] + assert [item["samples"] for item in audit["variants"]] == [80, 40, 32, 16] + assert [item["effective_block_size"] for item in audit["variants"]] == [ + 10, + 20, + 25, + 50, + ] diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_channels.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_channels.py new file mode 100644 index 000000000..cfa5d9a0c --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_channels.py @@ -0,0 +1,65 @@ +import numpy as np +import pytest +from pydantic import ValidationError + +from ceffflow.channels import ConfusionChannel, ErasureChannel +from ceffflow.schema import CellConfig, ChannelSpec + + +def test_erasure_channel_uses_declared_null_symbol(): + channel = ErasureChannel(0.5) + outcomes = np.array([1, -1, 1, -1], dtype=np.int8) + observed = channel.apply( + outcomes, + np.array([0.1, 0.8, 0.2, 0.9]), + ) + assert np.array_equal(observed, np.array([1, 0, 1, 0], dtype=np.int8)) + + +def test_erasure_channels_nest_for_fixed_uniform_construction(): + outcomes = np.array([1, -1, 1, -1], dtype=np.int8) + fine = ErasureChannel(0.8).apply( + outcomes, + np.array([0.1, 0.2, 0.9, 0.7]), + ) + nested = ErasureChannel(0.5).apply( + fine, + np.array([0.1, 0.8, 0.2, 0.9]), + ) + assert np.array_equal(nested, np.array([1, 0, 0, 0], dtype=np.int8)) + + +def test_confusion_log_probability_marginalizes_latent_outcome(): + channel = ConfusionChannel(0.1) + value = channel.log_observed_probability(1, latent_prob_plus=0.7) + assert np.isclose(np.exp(value), 0.7 * 0.9 + 0.3 * 0.1) + + +def test_confusion_minus_probability_is_complement(): + channel = ConfusionChannel(0.2) + plus = np.exp(channel.log_observed_probability(1, 0.73)) + minus = np.exp(channel.log_observed_probability(-1, 0.73)) + assert np.isclose(plus + minus, 1.0) + + +def test_confusion_rejects_nonbinary_observation(): + with pytest.raises(ValueError, match="must be"): + ConfusionChannel(0.1).log_observed_probability(0, 0.7) + + +def test_channel_spec_rejects_parameter_outside_channel_domain(): + with pytest.raises(ValidationError): + ChannelSpec(kind="confusion", parameter=0.7) + + +def test_cell_config_requires_integral_blocks(): + with pytest.raises(ValidationError): + CellConfig( + model="self_dual", + lengths=[4, 6, 8, 10], + channel=ChannelSpec(kind="identity", parameter=0.0), + steps=101, + burn_in=10, + block_size=20, + seed=0, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_commutator.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_commutator.py new file mode 100644 index 000000000..9523cac8b --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_commutator.py @@ -0,0 +1,65 @@ +import numpy as np + +from ceffflow.commutator import ( + hadamard_z_commutator_example, + relative_entropy_deficiency, + self_dual_block_deficiency, + self_dual_trajectory_block_deficiency, + statistical_deficiency, +) +from ceffflow.self_dual import SELF_DUAL_BETA + + +def test_deficiency_vanishes_for_a_classical_postprocessing(): + fine = np.asarray([[0.8, 0.2], [0.1, 0.9], [0.4, 0.6]]) + kernel = np.asarray([[0.7, 0.3], [0.2, 0.8]]) + target = fine @ kernel + result = statistical_deficiency(fine, target) + assert result.deficiency < 1e-9 + assert np.allclose(np.sum(result.stochastic_map, axis=1), 1.0) + + +def test_hadamard_and_z_measurement_have_nonzero_commutator(): + result = hadamard_z_commutator_example() + assert np.isclose(result.deficiency, 0.5, atol=1e-10) + + +def test_relative_entropy_deficiency_vanishes_for_classical_postprocessing(): + fine = np.asarray([[0.8, 0.2], [0.1, 0.9], [0.4, 0.6]]) + kernel = np.asarray([[0.7, 0.3], [0.2, 0.8]]) + result = relative_entropy_deficiency(fine, fine @ kernel) + assert result.deficiency < 1e-9 + assert np.allclose(np.sum(result.stochastic_map, axis=1), 1.0) + + +def test_self_dual_block_witness_has_exact_tv_obstruction(): + one_site = self_dual_block_deficiency(record_range=1) + two_sites = self_dual_block_deficiency(record_range=2) + tanh_beta = np.tanh(SELF_DUAL_BETA) + assert np.isclose(one_site.tv.deficiency, tanh_beta / 2.0, atol=1e-10) + assert np.isclose( + two_sites.tv.deficiency, + (tanh_beta - tanh_beta**2) / 2.0, + atol=1e-10, + ) + assert np.isclose(two_sites.diamond_distance, two_sites.tv.deficiency) + assert np.isclose(two_sites.diamond_norm, 2.0 * two_sites.tv.deficiency) + assert 0.0 < two_sites.kl.deficiency < one_site.kl.deficiency + + +def test_self_dual_block_witness_rejects_unsupported_record_range(): + with np.testing.assert_raises(ValueError): + self_dual_block_deficiency(record_range=3) + + +def test_self_dual_critical_trajectory_witness_survives_small_widths(): + for length in (3, 4, 5): + one_site = self_dual_trajectory_block_deficiency( + length, record_range=1, trajectories=4, rows=2 + ) + two_sites = self_dual_trajectory_block_deficiency( + length, record_range=2, trajectories=4, rows=2 + ) + assert one_site.state_count == 9 + assert 0.0 < two_sites.tv.deficiency < one_site.tv.deficiency + assert 0.0 < two_sites.kl.deficiency < one_site.kl.deficiency diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_convergence.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_convergence.py new file mode 100644 index 000000000..7095cf547 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_convergence.py @@ -0,0 +1,102 @@ +import json + +import numpy as np +import pytest + +from ceffflow.convergence import ( + _load_particle_blocks, + summarize_particle_pair, + summarize_particle_samples, +) + + +def test_particle_convergence_uses_paired_high_level_equivalence_bound(): + common = np.linspace(-1.0, 1.0, 20) + summary = summarize_particle_samples( + { + 64: common + 0.08, + 128: common + 0.015, + 256: common + 0.005, + }, + absolute_tolerance=0.05, + confidence_z=1.96, + ) + assert summary["production_gate_passed"] is True + assert ( + summary["comparisons"]["128_to_256"] + ["absolute_shift_upper_confidence_bound"] + == pytest.approx(0.01) + ) + + +def test_particle_convergence_fails_when_high_level_shift_exceeds_margin(): + common = np.linspace(-1.0, 1.0, 20) + summary = summarize_particle_samples( + {64: common, 128: common, 256: common + 0.051}, + absolute_tolerance=0.05, + confidence_z=1.96, + ) + assert summary["production_gate_passed"] is False + + +def test_high_statistics_particle_pair_uses_declared_levels(): + common = np.linspace(-1.0, 1.0, 100) + summary = summarize_particle_pair( + {256: common, 512: common + 0.01}, + lower_particles=256, + higher_particles=512, + absolute_tolerance=0.05, + confidence_z=1.96, + ) + assert summary["production_gate_passed"] is True + comparison = summary["comparisons"]["256_to_512"] + assert comparison["higher_minus_lower"] == pytest.approx(0.01) + assert comparison["absolute_shift_upper_confidence_bound"] == pytest.approx( + 0.01 + ) + + +@pytest.mark.parametrize( + "samples", + [ + {64: np.ones(2), 128: np.ones(2)}, + {64: np.ones(2), 128: np.ones(3), 256: np.ones(2)}, + {64: np.ones(2), 128: np.ones(2), 256: np.asarray([1.0, np.nan])}, + ], +) +def test_particle_convergence_rejects_incomplete_or_unaligned_samples(samples): + with pytest.raises(ValueError): + summarize_particle_samples( + samples, + absolute_tolerance=0.05, + confidence_z=1.96, + ) + + +@pytest.mark.parametrize( + ("kind", "parameter"), [("confusion", 0.5), ("erasure", 0.0)] +) +def test_particle_loader_skips_analytic_endpoints(tmp_path, kind, parameter): + spec = tmp_path / "run_spec.json" + spec.write_text( + json.dumps( + { + "cells": [ + { + "cell_id": "analytic", + "settings": { + "model": "self_dual", + "lengths": [6, 8, 10, 12, 14, 16], + "channel": {"kind": kind, "parameter": parameter}, + "steps": 20, + "burn_in": 0, + "block_size": 10, + "seed": 0, + "particles": 128, + }, + } + ] + } + ) + ) + assert _load_particle_blocks(spec, {128}) == ({}, {}, {}) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_fits.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_fits.py new file mode 100644 index 000000000..57bc3ac7c --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_fits.py @@ -0,0 +1,128 @@ +import numpy as np +import pytest + +from ceffflow.clean_ising import ( + critical_ground_energy, + fit_clean_ising, +) +from ceffflow.fits import ( + casimir_gls, + covariance_weighted_casimir_samples, + monotonicity_test, +) + + +def test_clean_ising_exact_formula_requires_even_lengths(): + with pytest.raises(ValueError, match="even"): + critical_ground_energy([4, 5, 6]) + + +def test_clean_ising_recovers_half(): + lengths = np.arange(8, 42, 2) + fit = fit_clean_ising(lengths, velocity=2.0) + assert abs(fit.central_charge - 0.5) < 2e-5 + + +def test_gls_recovers_injected_casimir_with_l3_term(): + lengths = np.array([6, 8, 10, 12, 14, 16], dtype=float) + values = ( + -1.7 * lengths + - np.pi * 0.447 / (6 * lengths) + + 0.3 / lengths**3 + ) + covariance = np.eye(lengths.size) * 1e-10 + fit = casimir_gls( + lengths, + values, + covariance, + alpha=1.0, + include_l3=True, + ) + assert abs(fit.central_charge - 0.447) < 1e-6 + assert fit.dof == 3 + assert len(fit.leave_one_out) == lengths.size + + +def test_covariance_weighted_block_samples_reproduce_gls(): + rng = np.random.default_rng(122) + lengths = np.asarray([6, 8, 10, 12, 14, 16], dtype=float) + curve = -1.2 * lengths - np.pi * 0.447 / (6 * lengths) + 0.3 / lengths**3 + mixing = np.asarray( + [ + [2.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.7, 1.0, 0.0, 0.0, 0.0, 0.0], + [0.3, 0.2, 1.5, 0.0, 0.0, 0.0], + [0.2, 0.1, 0.4, 0.8, 0.0, 0.0], + [0.1, 0.3, 0.2, 0.1, 1.2, 0.0], + [0.2, 0.1, 0.0, 0.2, 0.4, 0.7], + ] + ) + blocks = curve + 1e-4 * (rng.normal(size=(80, 6)) @ mixing.T) + covariance_of_mean = np.cov(blocks, rowvar=False, ddof=1) / blocks.shape[0] + expected = casimir_gls( + lengths, + blocks.mean(axis=0), + covariance_of_mean, + alpha=1.0, + include_l3=True, + compute_leave_one_out=False, + ) + samples = covariance_weighted_casimir_samples(lengths, blocks) + assert np.isclose(samples.mean(), expected.central_charge, atol=1e-12) + assert np.isclose( + samples.std(ddof=1) / np.sqrt(samples.size), + expected.standard_error, + atol=1e-12, + ) + + +def test_casimir_fit_rejects_nonpositive_anisotropy(): + lengths = np.array([4, 6, 8, 10], dtype=float) + with pytest.raises(ValueError, match="alpha"): + casimir_gls( + lengths, + -lengths, + np.eye(lengths.size), + alpha=0.0, + ) + + +def test_casimir_fit_rejects_singular_covariance(): + lengths = np.array([4, 6, 8, 10], dtype=float) + covariance = np.ones((4, 4)) + with pytest.raises(ValueError, match="positive definite"): + casimir_gls(lengths, -lengths, covariance, alpha=1.0) + + +def test_monotonicity_test_accepts_a_decreasing_curve(): + result = monotonicity_test( + [0.5, 0.4, 0.3], + np.eye(3) * 0.01, + bootstrap_draws=100, + seed=2, + ) + assert result.statistic < 1e-10 + assert result.bootstrap_p_value > 0.5 + + +def test_monotonicity_test_detects_a_precise_reversal(): + result = monotonicity_test( + [0.5, 0.8, 0.3], + np.eye(3) * 1e-4, + bootstrap_draws=200, + seed=3, + ) + assert result.statistic > 100.0 + assert result.bootstrap_p_value < 0.02 + + +def test_monotonicity_test_keeps_a_nearly_exact_endpoint_weight(): + result = monotonicity_test( + [0.4, -0.52, -0.5], + np.diag([0.1**2, 0.04**2, 1e-10**2]), + bootstrap_draws=100, + seed=4, + ) + assert np.isclose(result.constrained_curve[-1], -0.5, atol=1e-9) + assert np.isclose(result.constrained_curve[-2], -0.5, atol=1e-7) + assert np.isclose(result.statistic, 0.25, rtol=1e-5) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_models.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_models.py new file mode 100644 index 000000000..63d9c7654 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_models.py @@ -0,0 +1,94 @@ +import numpy as np + +from ceffflow.nishimori import ( + RandomBondIsingCylinder, + estimate_coupled_nishimori_free_energies, + nishimori_coupling, +) +from ceffflow.self_dual import ( + SelfDualBornCylinder, + SelfDualGaussianCylinder, + estimate_coupled_gaussian_self_dual_record_rates, +) + + +def test_nishimori_matrix_free_row_matches_dense(): + cylinder = RandomBondIsingCylinder(3, nishimori_coupling(0.1)) + vertical = np.array([1, -1, 1], dtype=np.int8) + horizontal = np.array([-1, 1, 1], dtype=np.int8) + vector = np.arange(1, 9, dtype=float) + assert np.allclose( + cylinder.apply_row(vector, vertical, horizontal), + cylinder.dense_row(vertical, horizontal) @ vector, + ) + + +def test_spin_and_gaussian_self_dual_use_same_born_probabilities(): + uniforms_z = np.array([0.1, 0.7, 0.4]) + uniforms_x = np.array([0.2, 0.9, 0.3]) + spin = SelfDualBornCylinder(3) + gaussian = SelfDualGaussianCylinder(3) + _, logp_spin = spin.sample_row( + spin.plus_state(), uniforms_z, uniforms_x + ) + _, logp_gaussian = gaussian.sample_row( + gaussian.plus_covariance(), uniforms_z, uniforms_x + ) + assert np.isclose(logp_spin, logp_gaussian, atol=1e-12) + + +def test_coupled_nishimori_is_bitwise_reproducible(): + kwargs = dict(lengths=[3, 4, 5], rows=20, burn_in=2, block_size=5, seed=17) + first = estimate_coupled_nishimori_free_energies(**kwargs) + second = estimate_coupled_nishimori_free_energies(**kwargs) + assert np.array_equal(first.blocks, second.blocks) + + +def test_coupled_gaussian_self_dual_is_bitwise_reproducible(): + kwargs = dict(lengths=[3, 4, 5], steps=20, burn_in=2, block_size=5, seed=17) + first = estimate_coupled_gaussian_self_dual_record_rates(**kwargs) + second = estimate_coupled_gaussian_self_dual_record_rates(**kwargs) + assert np.array_equal(first.blocks, second.blocks) + + +def test_gaussian_batch_update_matches_scalar_updates(): + cylinder = SelfDualGaussianCylinder(4) + states = [] + for first_sign, second_sign in ((1, 1), (1, -1), (-1, 1)): + state, _ = cylinder.update_zz( + cylinder.plus_covariance(), 0, first_sign + ) + state, _ = cylinder.update_x(state, 1, second_sign) + states.append(state) + covariances = np.stack(states) + signs = np.array([1, -1, 1], dtype=np.int8) + first, second = cylinder._zz_indices(2) + + batch_probabilities = cylinder.bilinear_probability_plus_batch( + covariances, first, second + ) + expected_probabilities = np.array( + [ + cylinder._bilinear_probability_plus(state, first, second) + for state in states + ] + ) + assert np.allclose(batch_probabilities, expected_probabilities, atol=1e-14) + + batch_states, batch_selected_probabilities = ( + cylinder.update_bilinear_batch( + covariances, first, second, signs + ) + ) + scalar = [ + cylinder._update_bilinear(state, first, second, int(sign)) + for state, sign in zip(states, signs, strict=True) + ] + assert np.allclose( + batch_states, np.stack([item[0] for item in scalar]), atol=1e-14 + ) + assert np.allclose( + batch_selected_probabilities, + [item[1] for item in scalar], + atol=1e-14, + ) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_planner.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_planner.py new file mode 100644 index 000000000..bfea0070b --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_planner.py @@ -0,0 +1,92 @@ +import json +from pathlib import Path + +from scripts.plan_ceffflow_production import build_run_spec + + +def _build(axes, tmp_path): + return build_run_spec( + axes, + run_id="test", + run_dir=tmp_path / "run", + axes_source=tmp_path / "axes.json", + ) + + +def test_production_axes_regenerate_committed_cells(tmp_path): + project = Path(__file__).parents[1] + axes_path = project / "configs/ceffflow/production_axes.json" + committed = json.loads( + (project / "results/ceffflow-production/run_spec.json").read_text() + ) + regenerated = build_run_spec( + json.loads(axes_path.read_text()), + run_id=committed["run_id"], + run_dir=Path(committed["run_dir"]), + axes_source=Path(committed["axes_source"]), + ) + assert regenerated == committed + + +def test_optional_models_and_particle_count_order(tmp_path): + axes = { + "lengths": [6, 8, 10], + "seeds": [0, 1], + "self_dual": { + "channels": { + "identity": [0.0], + "confusion": [0.1], + "erasure": [0.9], + }, + "steps": 20, + "burn_in": 0, + "block_size": 10, + "particle_counts": [64, 256], + }, + } + cells = _build(axes, tmp_path)["cells"] + assert len(cells) == 10 + assert [cell["settings"]["particles"] for cell in cells[:2]] == [1, 1] + assert {cell["settings"]["particles"] for cell in cells[2:6]} == {64} + assert {cell["settings"]["particles"] for cell in cells[6:]} == {256} + assert all( + cell["settings"]["model"] == "self_dual" for cell in cells + ) + + +def test_nishimori_can_be_planned_without_self_dual(tmp_path): + cells = _build( + { + "lengths": [6, 8, 10], + "seeds": [3, 5], + "nishimori": { + "steps": 100, + "burn_in": 10, + "block_size": 10, + }, + }, + tmp_path, + )["cells"] + assert len(cells) == 2 + assert [cell["settings"]["seed"] for cell in cells] == [3, 5] + assert all(cell["settings"]["model"] == "nishimori" for cell in cells) + + +def test_particle_counts_must_be_nonempty_and_unique(tmp_path): + axes = { + "lengths": [6, 8, 10], + "seeds": [0], + "self_dual": { + "channels": {"confusion": [0.1]}, + "steps": 20, + "burn_in": 0, + "block_size": 10, + "particle_counts": [64, 64], + }, + } + try: + _build(axes, tmp_path) + except ValueError as exc: + assert str(exc) == "particle_counts must be nonempty and unique" + else: + raise AssertionError("duplicate particle counts were accepted") diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_resolution.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_resolution.py new file mode 100644 index 000000000..1d752a37b --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_resolution.py @@ -0,0 +1,183 @@ +import itertools + +import numpy as np +import pytest + +from ceffflow.channels import ConfusionChannel, ErasureChannel +from ceffflow.resolution import ( + ExactBranch, + estimate_degraded_record_rates, + exact_filter_observation, + gaussian_particle_filter_observation, + particle_filter_observation, +) +from ceffflow.self_dual import ( + SelfDualBornCylinder, + SelfDualGaussianCylinder, + estimate_coupled_gaussian_self_dual_record_rates, +) + + +def test_two_gate_filter_matches_explicit_latent_sum(): + cylinder = SelfDualBornCylinder(3) + channel = ConfusionChannel(0.2) + initial = cylinder.plus_state() + observed = (1, -1) + gates = (("zz", 0), ("x", 0)) + explicit = 0.0 + for signs in itertools.product((-1, 1), repeat=2): + state = initial + joint = 1.0 + for gate, sign, symbol in zip( + gates, signs, observed, strict=True + ): + if gate[0] == "zz": + state, probability = cylinder.update_zz( + state, gate[1], sign + ) + else: + state, probability = cylinder.update_x( + state, gate[1], sign + ) + joint *= probability * channel.conditional_probability( + symbol, sign + ) + explicit += joint + + branches = [ExactBranch(1.0, initial)] + log_likelihood = 0.0 + for gate, symbol in zip(gates, observed, strict=True): + branches, increment = exact_filter_observation( + branches, cylinder, gate, symbol, channel + ) + log_likelihood += increment + assert np.isclose(np.exp(log_likelihood), explicit, atol=1e-14) + assert np.isclose(sum(branch.weight for branch in branches), 1.0) + + +def test_identity_filter_reproduces_physical_record_blocks(): + settings = dict( + lengths=[3, 4, 5], + steps=8, + burn_in=2, + block_size=4, + seed=23, + ) + physical = estimate_coupled_gaussian_self_dual_record_rates(**settings) + observed = estimate_degraded_record_rates( + **settings, + channel=ConfusionChannel(0.0), + particles=1, + ) + assert np.allclose(observed.blocks, physical.blocks, atol=1e-13) + + +def test_complete_erasure_has_zero_observed_surprisal(): + estimate = estimate_degraded_record_rates( + [3, 4, 5], + ErasureChannel(0.0), + particles=4, + steps=4, + burn_in=1, + block_size=2, + seed=9, + ) + assert np.array_equal(estimate.blocks, np.zeros_like(estimate.blocks)) + + +def test_maximal_confusion_has_exact_uniform_record_rate(): + lengths = np.array([3, 4, 5]) + estimate = estimate_degraded_record_rates( + lengths, + ConfusionChannel(0.5), + particles=7, + steps=6, + burn_in=3, + block_size=2, + seed=19, + ) + expected = np.broadcast_to( + 2.0 * lengths * np.log(2.0), estimate.blocks.shape + ) + assert np.array_equal(estimate.blocks, expected) + + +def test_batched_gaussian_particle_filter_matches_scalar_confusion(): + gaussian = SelfDualGaussianCylinder(3) + states = [] + for sign in (1, -1, 1, -1): + state, _ = gaussian.update_zz( + gaussian.plus_covariance(), 0, sign + ) + states.append(state) + uniforms = np.array([0.1, 0.8, 0.4, 0.6]) + channel = ConfusionChannel(0.2) + scalar, scalar_logp = particle_filter_observation( + states, + gaussian, + ("x", 1), + 1, + channel, + sign_uniforms=uniforms, + resample_uniform=0.37, + ) + batched, batched_logp = gaussian_particle_filter_observation( + np.stack(states), + gaussian, + ("x", 1), + 1, + channel, + sign_uniforms=uniforms, + resample_uniform=0.37, + ) + assert np.isclose(batched_logp, scalar_logp, atol=1e-14) + assert np.allclose(batched, np.stack(scalar), atol=1e-14) + + +def test_batched_gaussian_particle_filter_matches_scalar_erasure(): + gaussian = SelfDualGaussianCylinder(3) + states = [ + gaussian.update_x(gaussian.plus_covariance(), 0, sign)[0] + for sign in (1, -1, -1, 1) + ] + uniforms = np.array([0.2, 0.3, 0.7, 0.9]) + channel = ErasureChannel(0.6) + scalar, scalar_logp = particle_filter_observation( + states, + gaussian, + ("zz", 1), + 0, + channel, + sign_uniforms=uniforms, + resample_uniform=0.81, + ) + batched, batched_logp = gaussian_particle_filter_observation( + np.stack(states), + gaussian, + ("zz", 1), + 0, + channel, + sign_uniforms=uniforms, + resample_uniform=0.81, + ) + assert np.isclose(batched_logp, scalar_logp, atol=1e-14) + assert np.allclose(batched, np.stack(scalar), atol=1e-14) + + +@pytest.mark.parametrize( + "channel", + [ConfusionChannel(0.1), ErasureChannel(0.8)], +) +def test_batched_estimator_matches_scalar_orchestration(channel): + settings = dict( + lengths=[3, 4, 5], + channel=channel, + particles=8, + steps=4, + burn_in=2, + block_size=2, + seed=31, + ) + scalar = estimate_degraded_record_rates(**settings, batched=False) + batched = estimate_degraded_record_rates(**settings, batched=True) + assert np.allclose(batched.blocks, scalar.blocks, atol=1e-12) diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_runner.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_runner.py new file mode 100644 index 000000000..dfece3ece --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_runner.py @@ -0,0 +1,134 @@ +import hashlib +import json +import re + +import numpy as np + +from ceffflow.cli import main +from ceffflow.runner import run_cell +from ceffflow.schema import CellConfig + + +def _clean_config() -> CellConfig: + return CellConfig.model_validate( + { + "model": "clean_ising", + "lengths": [4, 6, 8, 10], + "channel": {"kind": "identity", "parameter": 0.0}, + "steps": 10, + "burn_in": 0, + "block_size": 5, + "seed": 4, + } + ) + + +def test_cell_manifest_and_replay_are_identical(tmp_path): + output = tmp_path / "cell" + first = run_cell(_clean_config(), output, cell_id="clean") + first_bytes = (output / "blocks.npz").read_bytes() + second = run_cell(_clean_config(), output, cell_id="clean") + second_bytes = (output / "blocks.npz").read_bytes() + assert first.blocks_sha256 == second.blocks_sha256 + assert first_bytes == second_bytes + assert hashlib.sha256(second_bytes).hexdigest() == second.blocks_sha256 + payload = json.loads((output / "manifest.json").read_text()) + assert payload["status"] == "success" + assert payload["normalization_ok"] is True + + +def test_cell_manifest_uses_declared_source_commit(tmp_path, monkeypatch): + source_commit = "a" * 40 + monkeypatch.setenv("CEFFFLOW_SOURCE_COMMIT", source_commit) + manifest = run_cell(_clean_config(), tmp_path / "cell", cell_id="clean") + assert manifest.provenance["git_commit"] == source_commit + + +def test_cell_manifest_rejects_invalid_declared_source_commit( + tmp_path, monkeypatch +): + monkeypatch.setenv("CEFFFLOW_SOURCE_COMMIT", "not-a-commit") + with np.testing.assert_raises_regex( + ValueError, re.escape("CEFFFLOW_SOURCE_COMMIT must be a 40-character") + ): + run_cell(_clean_config(), tmp_path / "cell", cell_id="clean") + + +def test_cell_cli_resolves_run_spec(tmp_path): + spec = tmp_path / "run_spec.json" + spec.write_text( + json.dumps( + { + "result_root": "cells", + "cells": [ + { + "cell_id": "c0", + "settings": _clean_config().model_dump(mode="json"), + } + ], + } + ) + ) + assert main(["cell", "--run-spec", str(spec), "--cell-id", "c0"]) == 0 + blocks = np.load(tmp_path / "cells" / "c0" / "blocks.npz")["blocks"] + assert blocks.shape == (1, 4) + + +def test_cell_cli_defaults_to_sibling_cells_directory(tmp_path): + spec = tmp_path / "run_spec.json" + spec.write_text( + json.dumps( + { + "cells": [ + { + "cell_id": "c0", + "settings": _clean_config().model_dump(mode="json"), + } + ] + } + ) + ) + assert main(["cell", "--run-spec", str(spec), "--cell-id", "c0"]) == 0 + assert (tmp_path / "cells" / "c0" / "manifest.json").exists() + + +def test_benchmark_cli_writes_passing_clean_ising_result(tmp_path): + assert main(["benchmark", "--output", str(tmp_path)]) == 0 + payload = json.loads((tmp_path / "benchmark.json").read_text()) + assert payload["clean_ising"]["passed"] is True + + +def test_analysis_verifies_hash_and_writes_summary(tmp_path): + spec = tmp_path / "run_spec.json" + spec.write_text( + json.dumps( + { + "result_root": "cells", + "cells": [ + { + "cell_id": "clean", + "settings": _clean_config().model_dump(mode="json"), + } + ], + } + ) + ) + run_cell(_clean_config(), tmp_path / "cells" / "clean", cell_id="clean") + assert ( + main( + [ + "analyze", + "--run-spec", + str(spec), + "--output", + str(tmp_path / "analysis"), + ] + ) + == 0 + ) + summary = json.loads((tmp_path / "analysis" / "summary.json").read_text()) + assert summary["cells_verified"] == 1 + assert (tmp_path / "analysis" / "ceff_resolution.png").exists() + assert b"\r\n" not in ( + tmp_path / "analysis" / "ceff_resolution.csv" + ).read_bytes() diff --git a/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_slurm.py b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_slurm.py new file mode 100644 index 000000000..4b042e0c1 --- /dev/null +++ b/tracks/qmc/solutions/Ranger-Observer-Ceff-122/tests/test_ceff_slurm.py @@ -0,0 +1,77 @@ +import os +from pathlib import Path +import subprocess + + +def test_slurm_wrapper_uses_deployed_python_and_source_commit(tmp_path): + project = tmp_path / "project" + (project / "scripts").mkdir(parents=True) + (project / "src").mkdir() + (project / "results" / "ceffflow-production" / "logs").mkdir( + parents=True + ) + capture = tmp_path / "capture.txt" + fake_python = tmp_path / "python" + fake_python.write_text( + "#!/bin/sh\n" + "if [ \"$1\" = \"-c\" ]; then\n" + " expected=\"$CEFFFLOW_PROJECT_ROOT/.deps\"\n" + " [ \"${PYTHONPATH%%:*}\" = \"$expected\" ] || exit 42\n" + " exit 0\n" + "fi\n" + "printf '%s\\n' \"$CEFFFLOW_SOURCE_COMMIT\" \"$PYTHONPATH\" \"$*\" " + f"> {capture}\n" + ) + fake_python.chmod(0o755) + + environment = os.environ.copy() + environment.update( + { + "CEFFFLOW_PROJECT_ROOT": str(project), + "CEFFFLOW_PYTHON": str(fake_python), + "CEFFFLOW_RUN_SPEC": "results/ceffflow-production/run_spec.json", + "CEFFFLOW_SOURCE_COMMIT": "b" * 40, + "SLURM_ARRAY_TASK_ID": "2", + } + ) + script = Path(__file__).parents[1] / "scripts/slurm/ceffflow_array.sh" + subprocess.run(["bash", str(script)], check=True, env=environment) + + lines = capture.read_text().splitlines() + assert lines[0] == "b" * 40 + python_path = lines[1].split(":") + assert python_path[:2] == [str(project / ".deps"), str(project / "src")] + assert lines[2].endswith("--cell-id cell-0003") + + +def test_environment_bootstrap_pins_binary_dependencies(tmp_path): + project = tmp_path / "project" + (project / "scripts" / "slurm").mkdir(parents=True) + capture = tmp_path / "capture.txt" + fake_python = tmp_path / "python" + fake_python.write_text( + "#!/bin/sh\n" + f"printf '%s\\n' \"$*\" >> {capture}\n" + ) + fake_python.chmod(0o755) + + environment = os.environ.copy() + environment.update( + { + "CEFFFLOW_PROJECT_ROOT": str(project), + "CEFFFLOW_PYTHON": str(fake_python), + } + ) + script = ( + Path(__file__).parents[1] + / "scripts/slurm/bootstrap_ceffflow_env.sh" + ) + subprocess.run(["bash", str(script)], check=True, env=environment) + + invocations = capture.read_text().splitlines() + install = invocations[0] + assert "--only-binary=:all:" in install + assert f"--target {project / '.deps'}" in install + assert "numpy==2.2.6" in install + assert "scipy==1.15.3" in install + assert "pydantic==2.12.5" in install