diff --git a/MANIFEST.in b/MANIFEST.in
index 9d56eac..747d9a8 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -9,3 +9,7 @@ include RELEASE_NOTES*.md
recursive-include docs/joss *.md *.json
include scripts/check_joss_artifacts.py
+
+recursive-include assets/readme *.svg *.png *.json
+
+recursive-include paper *.pdf *.json
diff --git a/README.md b/README.md
index 7ca98a2..eef047c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
# StrainTrace
@@ -66,6 +66,8 @@ Full-field 2D DIC: Load images → draw field ROI → IC-GN/IC-LM → dic/
## Full-field output contract
+
+
Full-field analysis uses the first frame in the selected analysis range as one fixed reference. Each later frame is correlated to that same reference; the workflow does not silently switch to a frame-to-frame reference. The rectangular ROI is sampled at points of interest (POIs), so the result is a POI grid rather than a value at every image pixel. It is not a 3D measurement.
All images in the selected full-field range must have the same dimensions; a mismatch is a validation failure.
diff --git a/assets/readme/hero-generation.json b/assets/readme/hero-generation.json
new file mode 100644
index 0000000..a091282
--- /dev/null
+++ b/assets/readme/hero-generation.json
@@ -0,0 +1,12 @@
+{
+ "name": "StrainTrace",
+ "subtitle": "Image-based displacement and strain",
+ "palette": "Deep forest navy, jade, pale mint and restrained orange",
+ "motif": "A clear speckled reference specimen plane next to a slightly deformed plane, linked by fine tracking guides. A few tasteful displacement arrows and a sparse deformation grid communicate digital image correlation. No machinery, no human hands, no invented colorbar or numerical measurement.",
+ "tool": "image_gen built-in; model identifier not exposed",
+ "revision": null,
+ "prompt": "Create one exceptionally polished scientific software README cover banner, wide landscape approximately 2.4:1 aspect ratio, premium editorial art direction, high resolution. Project: StrainTrace. Palette: Deep forest navy, jade, pale mint and restrained orange. Concept: A clear speckled reference specimen plane next to a slightly deformed plane, linked by fine tracking guides. A few tasteful displacement arrows and a sparse deformation grid communicate digital image correlation. No machinery, no human hands, no invented colorbar or numerical measurement. Compose generous negative space and sharply legible refined sans-serif typography integrated with the scientific motif. Exact text, no other copy: \"StrainTrace\" large, \"Image-based displacement and strain\" smaller, and a discreet \"Conceptual illustration\". Make name/subtitle read immediately at GitHub width. Restrained three-dimensional material, carefully controlled highlights, precise linework, sophisticated hierarchy and balanced composition. No generic AI neon clouds, no random particles, no badges, no claims, no photoreal experimental data, no fake application screenshot, no watermarks or publisher branding. This is an editorial cover, not a quantitative paper figure.",
+ "created_on": "2026-09-27",
+ "purpose": "README conceptual cover only, not experimental data or software output",
+ "sha256": "65a5cbe82226578358162b5ce0c6bc65174593761a9e4f80622c70c8f713b8b5"
+}
diff --git a/assets/readme/hero.png b/assets/readme/hero.png
new file mode 100644
index 0000000..7e86de6
Binary files /dev/null and b/assets/readme/hero.png differ
diff --git a/assets/readme/section-01-why.svg b/assets/readme/section-01-why.svg
index 9980fe3..84466a1 100644
--- a/assets/readme/section-01-why.svg
+++ b/assets/readme/section-01-why.svg
@@ -1,9 +1,7 @@
-
+
diff --git a/assets/readme/section-02-science.svg b/assets/readme/section-02-science.svg
index b02e986..ff79231 100644
--- a/assets/readme/section-02-science.svg
+++ b/assets/readme/section-02-science.svg
@@ -1,9 +1,7 @@
-
+
diff --git a/docs/joss/submission-record.json b/docs/joss/submission-record.json
index c5bbdb6..a42b72b 100644
--- a/docs/joss/submission-record.json
+++ b/docs/joss/submission-record.json
@@ -45,7 +45,8 @@
},
"ai_assistance": {
"current_round": "OpenAI Codex, GPT-6, 2026-09-27: repository audit, packaging, tests, documentation, manuscript and bibliography preparation, validation and Git integration.",
- "historical_disclosure": "Retain project-specific disclosures; AbsSAXS AI history is not copied to these projects."
+ "historical_disclosure": "Retain project-specific disclosures; AbsSAXS AI history is not copied to these projects.",
+ "visual_revision": "2026-09-27: Codex revised reproducible manuscript figure scripts, captured real application widgets and prepared conceptual README covers using the built-in image-generation tool (model identifier not exposed). Prompt and image provenance are retained in the repository. Final author review remains pending."
},
"submission_date": null,
"related_publications": [
diff --git a/docs/joss/visual-provenance.md b/docs/joss/visual-provenance.md
new file mode 100644
index 0000000..ddae3a9
--- /dev/null
+++ b/docs/joss/visual-provenance.md
@@ -0,0 +1,16 @@
+# Visual sources and reproduction
+
+The September 2026 visual revision separates editorial illustrations from research-software evidence.
+
+- `assets/readme/hero.png` is an AI-generated **conceptual illustration** for the README. It is not detector data, a calculated result, or an application screenshot. The prompt, tool description and file hash are in `hero-generation.json` beside it. The built-in image tool did not expose a model identifier; no specific model-version claim is made.
+- The editable section headers use repository-owned SVG. The previous SVG cover remains as an alternative source asset.
+- Manuscript diagrams and numerical plots are generated by the repository scripts below, with SVG and PDF vector exports and 450 dpi PNGs. Scientific arrays, reflection positions, and solver observations are not modified by an image-generation model.
+- Numerical demonstrations use only the bundled synthetic fixtures and actual software calculations. AnisoScope's interface figure, where applicable, is captured from real Qt widgets; the capture JSON records its inputs and hash.
+
+From the repository root, with the project's dependencies and Matplotlib installed:
+
+```bash
+python paper/make_figures.py
+```
+
+Inspect the generated images and recompile `paper/paper.md` using the official draft-PDF workflow after any figure change. The README cover is not included as a scientific manuscript figure. Synthetic verification is not independent reproduction of the Acta Materialia experimental study.
diff --git a/paper/README.md b/paper/README.md
index d266553..13a072c 100644
--- a/paper/README.md
+++ b/paper/README.md
@@ -9,3 +9,7 @@ The source date is a preparation date. Before submission, complete the author re
and research-use mapping in [the submission guide](../docs/joss/README.md), use the
actual submission date, rebuild and inspect the PDF. The synthetic verification
suite is documented in [validation](../docs/joss/validation.md).
+
+## Reproduce the figures
+
+Run `python paper/make_figures.py` from the repository root. It regenerates the workflow and reruns the locked synthetic benchmark before plotting the clean small-translation case. Vector SVG/PDF and 450 dpi PNG exports are saved under `paper/figures`, together with plotted observations and the full benchmark JSON. This is not experimental-data validation.
diff --git a/paper/figures/benchmark_report.json b/paper/figures/benchmark_report.json
new file mode 100644
index 0000000..5cad688
--- /dev/null
+++ b/paper/figures/benchmark_report.json
@@ -0,0 +1,984 @@
+{
+ "report_version": "ezdic-benchmark-report-v5",
+ "cases_version": "ezdic-benchmark-cases-v3",
+ "migration": {
+ "previous_report_version": "ezdic-benchmark-report-v4",
+ "previous_cases_version": "ezdic-benchmark-cases-v2",
+ "reason": "quality threshold was reclassified as NOT_CALIBRATED and rejected outcomes entered the ranking population"
+ },
+ "locked_cases_hash": "3dbe0dae3fdf8f30ec32c9fd8f036f0a53b4a705380626e7860773f62f31cb20",
+ "app": {
+ "name": "ezDIC",
+ "benchmark": "quality_to_known_error",
+ "version": "v0.2.0-dev"
+ },
+ "code": {
+ "module": "benchmarks.run_benchmark",
+ "source_sha256": "fe6b69a46bd356c426b811b2236889a2247b22407470478a75803c6c8ef3996d",
+ "benchmark_runner_source_sha256": "fe6b69a46bd356c426b811b2236889a2247b22407470478a75803c6c8ef3996d",
+ "benchmark_source_sha256": "500166303fc7570738eb73507e817d02e44f6ab710cb182c4fb3c8a01fb5851c",
+ "synthetic_cases_source_sha256": "152737174ed7e1375e8548ddcb668acfafe81ded6b71b0543833b1b0ae5ed92f",
+ "cases_json_sha256": "1937ce09f8c7c532769d24ab488051777d7a13486b16e099d8e46abab79219af",
+ "core_source_sha256": "c48d289020cf28f7ec0ae262dddb83caa6df67f937f7c1fbcdb99b774f9179e6",
+ "cli_source_sha256": "a97692774cda2d44916c1b1f2b49f72b16786450086e72328babbb48eac433b5",
+ "benchmark_facade": {
+ "module": "ezdic_benchmark",
+ "source_sha256": "500166303fc7570738eb73507e817d02e44f6ab710cb182c4fb3c8a01fb5851c"
+ },
+ "core": {
+ "module": "ezdic_core",
+ "source_sha256": "c48d289020cf28f7ec0ae262dddb83caa6df67f937f7c1fbcdb99b774f9179e6"
+ },
+ "cli": {
+ "module": "ezdic_cli",
+ "source_sha256": "a97692774cda2d44916c1b1f2b49f72b16786450086e72328babbb48eac433b5"
+ }
+ },
+ "environment": {
+ "python": "3.13.0",
+ "platform": "Windows",
+ "image_shape": [
+ 192,
+ 192
+ ]
+ },
+ "contract": {
+ "image_shape": [
+ 192,
+ 192
+ ],
+ "roi": [
+ 40,
+ 60,
+ 88,
+ 88
+ ],
+ "subset_size": 21,
+ "step": 8,
+ "strain_window": 5,
+ "coordinate_order": "row-major y then x; exactly 81 locked POIs",
+ "reference_frame": "fixed clean reference; each deformed image is compared to it",
+ "solver": {
+ "subset_size": 21,
+ "step": 8,
+ "zncc_min": 0.75,
+ "strain_window": 5,
+ "smooth_sigma": 0.0
+ }
+ },
+ "texture_preflight_contract": {
+ "version": "texture_preflight_v2",
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9
+ },
+ "quality_contract": {
+ "version": "quality_score_v1",
+ "error_tolerance_px": 0.25,
+ "quality_validity_required": true,
+ "illustrative_quality_threshold": {
+ "status": "NOT_CALIBRATED",
+ "quality_accept_score_min": 0.5
+ },
+ "roc_auc_min": 0.9,
+ "minimum_bad_label_count": 2,
+ "minimum_corruption_row_count": 4,
+ "score_components": {
+ "zncc": 0.25,
+ "second_peak_margin": 0.15,
+ "second_peak_ratio_best_over_second": 0.15,
+ "residual_rms": 0.15,
+ "hessian_condition_number": 0.15,
+ "iterations": 0.05,
+ "converged": 0.1
+ },
+ "score_normalization": {
+ "zncc_floor": 0.5,
+ "zncc_span": 0.5,
+ "second_peak_margin_span": 0.25,
+ "second_peak_ratio_floor": 1.0,
+ "second_peak_ratio_span": 0.75,
+ "residual_rms_scale": 0.1,
+ "hessian_condition_ceiling": 1000.0,
+ "iterations_free_ceiling": 8.0,
+ "iterations_span": 40.0
+ },
+ "texture_preflight": {
+ "version": "texture_preflight_v2",
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9
+ },
+ "corruption_panel": {
+ "version": "image_corruption_panel_v1",
+ "apply_to": "deformed_image_only",
+ "target_coordinate": "oracle_deformed_poi_center",
+ "variants": [
+ {
+ "variant_id": "gaussian_noise_target_0",
+ "case_ids": [
+ "small_translation",
+ "large_translation"
+ ],
+ "target_point_indices": [
+ 0
+ ],
+ "radius_px": 3,
+ "sigma_gray": 160.0,
+ "seed": 1701
+ },
+ {
+ "variant_id": "gaussian_noise_target_80",
+ "case_ids": [
+ "small_translation",
+ "large_translation"
+ ],
+ "target_point_indices": [
+ 80
+ ],
+ "radius_px": 4,
+ "sigma_gray": 160.0,
+ "seed": 1701
+ }
+ ]
+ }
+ },
+ "quality_error": {
+ "version": "quality_score_v1",
+ "threshold_status": "NOT_CALIBRATED",
+ "quality_threshold_evaluated": false,
+ "quality_threshold_pass": null,
+ "error_tolerance_px": 0.25,
+ "illustrative_quality_accept_score_min": 0.5,
+ "point_count": 565,
+ "finite_error_label_count": 565,
+ "ranking_point_count": 567,
+ "ranking_label_basis": "numeric solver rows; accepted finite error <= tolerance is good, rejected or nonfinite is bad; near-1D preflight excluded",
+ "all_row_count": 648,
+ "rejected_point_count": 83,
+ "good_label_count": 563,
+ "bad_label_count": 2,
+ "ranking_good_label_count": 563,
+ "ranking_bad_label_count": 4,
+ "ranking_rejected_bad_count": 2,
+ "minimum_bad_label_count": 2,
+ "corruption_row_count": 4,
+ "minimum_corruption_row_count": 4,
+ "roc_auc": 0.9942273534635879,
+ "roc_auc_min": 0.9,
+ "roc_auc_gate": true,
+ "false_accept_count": 2,
+ "false_reject_count": 0,
+ "false_accept_rate": 1.0,
+ "false_reject_rate": 0.0,
+ "ranking_false_accept_count": 2,
+ "ranking_false_reject_count": 0,
+ "ranking_false_accept_rate": 0.5,
+ "ranking_false_reject_rate": 0.0,
+ "class_conditional_rates": {
+ "finite_error_labels": {
+ "false_accept_count": 2,
+ "bad_label_count": 2,
+ "false_accept_rate": 1.0,
+ "false_reject_count": 0,
+ "good_label_count": 563,
+ "false_reject_rate": 0.0
+ },
+ "ranking_outcomes": {
+ "false_accept_count": 2,
+ "bad_label_count": 4,
+ "false_accept_rate": 0.5,
+ "false_reject_count": 0,
+ "good_label_count": 563,
+ "false_reject_rate": 0.0
+ }
+ }
+ },
+ "quality_score": {
+ "version": "quality_score_v1",
+ "components": {
+ "zncc": 0.25,
+ "second_peak_margin": 0.15,
+ "second_peak_ratio_best_over_second": 0.15,
+ "residual_rms": 0.15,
+ "hessian_condition_number": 0.15,
+ "iterations": 0.05,
+ "converged": 0.1
+ },
+ "normalization": {
+ "zncc_floor": 0.5,
+ "zncc_span": 0.5,
+ "second_peak_margin_span": 0.25,
+ "second_peak_ratio_floor": 1.0,
+ "second_peak_ratio_span": 0.75,
+ "residual_rms_scale": 0.1,
+ "hessian_condition_ceiling": 1000.0,
+ "iterations_free_ceiling": 8.0,
+ "iterations_span": 40.0
+ },
+ "validity_required": true,
+ "ratio_direction": "best_over_second",
+ "threshold_status": "NOT_CALIBRATED",
+ "quality_threshold_evaluated": false,
+ "quality_threshold_pass": null,
+ "illustrative_accept_score_min": 0.5,
+ "error_tolerance_px": 0.25,
+ "roc_auc": 0.9942273534635879,
+ "roc_auc_min": 0.9
+ },
+ "quality_auc": 0.9942273534635879,
+ "thresholds": {
+ "small_translation": {
+ "valid_fraction_min": 0.95,
+ "rmse_px_max": 0.05,
+ "p95_error_px_max": 0.1,
+ "max_error_px_max": 0.15
+ },
+ "large_translation": {
+ "valid_fraction_min": 0.95,
+ "rmse_px_max": 0.05,
+ "p95_error_px_max": 0.1,
+ "max_error_px_max": 0.15
+ },
+ "small_affine_strain": {
+ "valid_fraction_min": 0.95,
+ "rmse_px_max": 0.05,
+ "p95_error_px_max": 0.1,
+ "max_error_px_max": 0.15,
+ "strain_component_abs_error_max": 0.0005,
+ "strain_valid_fraction_min": 0.8,
+ "strain_consistency_abs_error_max": 0.0005
+ },
+ "near_1d_periodic": {
+ "expected_failure_code": "AMBIGUOUS_TEXTURE",
+ "max_successful_export_artifacts": 0
+ }
+ },
+ "cases": [
+ {
+ "case_id": "small_translation",
+ "kind": "translation",
+ "seed": 17,
+ "case_hash": "3e798dbedd361b30fbff33638de665a588189a1fa5772de167d669b876c4408c",
+ "config_hash": "3e798dbedd361b30fbff33638de665a588189a1fa5772de167d669b876c4408c",
+ "input_sha256": "3e804075a1c0fbe3bc2a61d5bfc33184065613263cc05adb328edb1abaad74bc",
+ "thresholds": {
+ "valid_fraction_min": 0.95,
+ "rmse_px_max": 0.05,
+ "p95_error_px_max": 0.1,
+ "max_error_px_max": 0.15
+ },
+ "status": "PASS",
+ "benchmark_pass": true,
+ "scientific_ok": true,
+ "failure_code": null,
+ "texture_preflight": {
+ "ok": true,
+ "code": null,
+ "metrics": {
+ "std_gray": 77.69232940673828,
+ "contrast_p95_p5": 224.2419493675232,
+ "low_frac": 0.0,
+ "high_frac": 0.16206095041322313,
+ "structure_tensor_ratio": 0.9352860600952614,
+ "texture_rank_ratio": 0.9352860600952614,
+ "minor_to_major_gradient_eigenvalue_ratio": 0.9352860600952614,
+ "gradient_energy": 1745.5391597462635,
+ "directional_gradient_coherence": 0.00932645688962488,
+ "structure_tensor_eigenvalues": [
+ 843.5850787767855,
+ 901.954080969478
+ ],
+ "metrics_version": "structure_tensor_rank1_periodic_v2",
+ "periodicity_score": 0.6333981857811681,
+ "periodicity_period_px": 2,
+ "periodicity_axis": "x",
+ "discriminator_version": "rank_one_periodic_v1",
+ "rank_one_ratio": 0.9352860600952614,
+ "rank_one_score": 0.06471393990473862
+ },
+ "details": {},
+ "thresholds": {
+ "version": "texture_preflight_v2",
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9
+ }
+ },
+ "metrics": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.019939074470495494,
+ "p95_error_px": 0.029262027232242572,
+ "max_error_px": 0.032582835504919486,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true,
+ "valid_fraction": true,
+ "rmse_px": true,
+ "p95_error_px": true,
+ "max_error_px": true,
+ "corruption_rows": true
+ },
+ "runs": [
+ {
+ "run_id": "clean",
+ "panel": null,
+ "status": "PASS",
+ "strict_gate_evaluated": true,
+ "observed": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.019939074470495494,
+ "p95_error_px": 0.029262027232242572,
+ "max_error_px": 0.032582835504919486,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true,
+ "valid_fraction": true,
+ "rmse_px": true,
+ "p95_error_px": true,
+ "max_error_px": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ },
+ {
+ "run_id": "gaussian_noise_target_0",
+ "panel": {
+ "panel_version": "image_corruption_panel_v1",
+ "variant_id": "gaussian_noise_target_0",
+ "target_point_indices": [
+ 0
+ ],
+ "radius_px": 3,
+ "sigma_gray": 160.0,
+ "seed": 1701,
+ "applied_to": "deformed_image_only"
+ },
+ "status": "PANEL_OBSERVATION",
+ "strict_gate_evaluated": false,
+ "observed": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.04628106565291975,
+ "p95_error_px": 0.032582835504919486,
+ "max_error_px": 0.25739955822182586,
+ "false_accept_count": 1,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 1,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ },
+ {
+ "run_id": "gaussian_noise_target_80",
+ "panel": {
+ "panel_version": "image_corruption_panel_v1",
+ "variant_id": "gaussian_noise_target_80",
+ "target_point_indices": [
+ 80
+ ],
+ "radius_px": 4,
+ "sigma_gray": 160.0,
+ "seed": 1701,
+ "applied_to": "deformed_image_only"
+ },
+ "status": "PANEL_OBSERVATION",
+ "strict_gate_evaluated": false,
+ "observed": {
+ "valid_fraction": 0.9876543209876543,
+ "strain_valid_fraction": 0.9876543209876543,
+ "raw_finite_count": 80,
+ "rejected_count": 1,
+ "rmse_px": 0.02361856001201188,
+ "p95_error_px": 0.03072241002094009,
+ "max_error_px": 0.0933221265812201,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ }
+ ],
+ "corruption_panel_version": "image_corruption_panel_v1"
+ },
+ {
+ "case_id": "large_translation",
+ "kind": "translation",
+ "seed": 17,
+ "case_hash": "5ec5a9372697df0cdf63d9e3e27e78f72c8a5a8f95561157d2535a1c582c3c12",
+ "config_hash": "5ec5a9372697df0cdf63d9e3e27e78f72c8a5a8f95561157d2535a1c582c3c12",
+ "input_sha256": "64f19d9dfa3b21e9cc1811ddab0eeaf60d2e4260df0963124971690f32dc5ef3",
+ "thresholds": {
+ "valid_fraction_min": 0.95,
+ "rmse_px_max": 0.05,
+ "p95_error_px_max": 0.1,
+ "max_error_px_max": 0.15
+ },
+ "status": "PASS",
+ "benchmark_pass": true,
+ "scientific_ok": true,
+ "failure_code": null,
+ "texture_preflight": {
+ "ok": true,
+ "code": null,
+ "metrics": {
+ "std_gray": 77.69232940673828,
+ "contrast_p95_p5": 224.2419493675232,
+ "low_frac": 0.0,
+ "high_frac": 0.16206095041322313,
+ "structure_tensor_ratio": 0.9352860600952614,
+ "texture_rank_ratio": 0.9352860600952614,
+ "minor_to_major_gradient_eigenvalue_ratio": 0.9352860600952614,
+ "gradient_energy": 1745.5391597462635,
+ "directional_gradient_coherence": 0.00932645688962488,
+ "structure_tensor_eigenvalues": [
+ 843.5850787767855,
+ 901.954080969478
+ ],
+ "metrics_version": "structure_tensor_rank1_periodic_v2",
+ "periodicity_score": 0.6333981857811681,
+ "periodicity_period_px": 2,
+ "periodicity_axis": "x",
+ "discriminator_version": "rank_one_periodic_v1",
+ "rank_one_ratio": 0.9352860600952614,
+ "rank_one_score": 0.06471393990473862
+ },
+ "details": {},
+ "thresholds": {
+ "version": "texture_preflight_v2",
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9
+ }
+ },
+ "metrics": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.011529723845911357,
+ "p95_error_px": 0.02398089477028113,
+ "max_error_px": 0.026990196683557104,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true,
+ "valid_fraction": true,
+ "rmse_px": true,
+ "p95_error_px": true,
+ "max_error_px": true,
+ "pyramid_levels": true,
+ "corruption_rows": true
+ },
+ "runs": [
+ {
+ "run_id": "clean",
+ "panel": null,
+ "status": "PASS",
+ "strict_gate_evaluated": true,
+ "observed": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.011529723845911357,
+ "p95_error_px": 0.02398089477028113,
+ "max_error_px": 0.026990196683557104,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true,
+ "valid_fraction": true,
+ "rmse_px": true,
+ "p95_error_px": true,
+ "max_error_px": true,
+ "pyramid_levels": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ },
+ {
+ "run_id": "gaussian_noise_target_0",
+ "panel": {
+ "panel_version": "image_corruption_panel_v1",
+ "variant_id": "gaussian_noise_target_0",
+ "target_point_indices": [
+ 0
+ ],
+ "radius_px": 3,
+ "sigma_gray": 160.0,
+ "seed": 1701,
+ "applied_to": "deformed_image_only"
+ },
+ "status": "PANEL_OBSERVATION",
+ "strict_gate_evaluated": false,
+ "observed": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.0490516427705283,
+ "p95_error_px": 0.026990196683557104,
+ "max_error_px": 0.3178957323053715,
+ "false_accept_count": 1,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 1,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ },
+ {
+ "run_id": "gaussian_noise_target_80",
+ "panel": {
+ "panel_version": "image_corruption_panel_v1",
+ "variant_id": "gaussian_noise_target_80",
+ "target_point_indices": [
+ 80
+ ],
+ "radius_px": 4,
+ "sigma_gray": 160.0,
+ "seed": 1701,
+ "applied_to": "deformed_image_only"
+ },
+ "status": "PANEL_OBSERVATION",
+ "strict_gate_evaluated": false,
+ "observed": {
+ "valid_fraction": 0.9876543209876543,
+ "strain_valid_fraction": 0.9876543209876543,
+ "raw_finite_count": 80,
+ "rejected_count": 1,
+ "rmse_px": 0.03753840100543573,
+ "p95_error_px": 0.025428296855793377,
+ "max_error_px": 0.22923298645005563,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0
+ },
+ "gates": {
+ "diagnostics_complete": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ }
+ ],
+ "corruption_panel_version": "image_corruption_panel_v1"
+ },
+ {
+ "case_id": "small_affine_strain",
+ "kind": "affine",
+ "seed": 23,
+ "case_hash": "ceb0a63aa61ab37a76f463e55bb47127aba190ef219fd3313721aac727abba71",
+ "config_hash": "ceb0a63aa61ab37a76f463e55bb47127aba190ef219fd3313721aac727abba71",
+ "input_sha256": "bd6e4656149339e3e0e3ebbe0ad23c8d9eb77ec1815ed5829ff0683bb5a90d5f",
+ "thresholds": {
+ "valid_fraction_min": 0.95,
+ "rmse_px_max": 0.05,
+ "p95_error_px_max": 0.1,
+ "max_error_px_max": 0.15,
+ "strain_component_abs_error_max": 0.0005,
+ "strain_valid_fraction_min": 0.8,
+ "strain_consistency_abs_error_max": 0.0005
+ },
+ "status": "PASS",
+ "benchmark_pass": true,
+ "scientific_ok": true,
+ "failure_code": null,
+ "texture_preflight": {
+ "ok": true,
+ "code": null,
+ "metrics": {
+ "std_gray": 75.73677825927734,
+ "contrast_p95_p5": 221.67289905548097,
+ "low_frac": 0.0,
+ "high_frac": 0.16270661157024793,
+ "structure_tensor_ratio": 0.9382107030336673,
+ "texture_rank_ratio": 0.9382107030336673,
+ "minor_to_major_gradient_eigenvalue_ratio": 0.9382107030336673,
+ "gradient_energy": 1633.1240329258662,
+ "directional_gradient_coherence": 0.005519447542641189,
+ "structure_tensor_eigenvalues": [
+ 790.5303817971642,
+ 842.5936511287022
+ ],
+ "metrics_version": "structure_tensor_rank1_periodic_v2",
+ "periodicity_score": 0.6201619387536572,
+ "periodicity_period_px": 2,
+ "periodicity_axis": "y",
+ "discriminator_version": "rank_one_periodic_v1",
+ "rank_one_ratio": 0.9382107030336673,
+ "rank_one_score": 0.06178929696633273
+ },
+ "details": {},
+ "thresholds": {
+ "version": "texture_preflight_v2",
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9
+ }
+ },
+ "metrics": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.0036344039490451537,
+ "p95_error_px": 0.006512649134610679,
+ "max_error_px": 0.010373674753570926,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0,
+ "estimated_F": [
+ [
+ 1.0119850289355974,
+ 0.006974982837275979
+ ],
+ [
+ -0.004021334173409178,
+ 0.989015622083158
+ ]
+ ],
+ "oracle_F": [
+ [
+ 1.012,
+ 0.007
+ ],
+ [
+ -0.004,
+ 0.989
+ ]
+ ],
+ "estimated_E": [
+ 0.012064934959158058,
+ -0.01089972444494186,
+ 0.001540707944643743
+ ],
+ "oracle_E": [
+ 0.01207999999999998,
+ -0.010915000000000008,
+ 0.001564
+ ],
+ "fit_count": 81,
+ "fit_rank": 3,
+ "strain_component_abs_error_max": 0.0002738786399401061,
+ "strain_consistency_abs_error_max": 0.00027090769474797413
+ },
+ "gates": {
+ "diagnostics_complete": true,
+ "valid_fraction": true,
+ "rmse_px": true,
+ "p95_error_px": true,
+ "max_error_px": true,
+ "affine_fit": true,
+ "affine_metadata": true,
+ "strain_component_abs_error": true,
+ "strain_consistency_abs_error": true,
+ "strain_valid_fraction": true,
+ "corruption_rows": true
+ },
+ "runs": [
+ {
+ "run_id": "clean",
+ "panel": null,
+ "status": "PASS",
+ "strict_gate_evaluated": true,
+ "observed": {
+ "valid_fraction": 1.0,
+ "strain_valid_fraction": 1.0,
+ "raw_finite_count": 81,
+ "rejected_count": 0,
+ "rmse_px": 0.0036344039490451537,
+ "p95_error_px": 0.006512649134610679,
+ "max_error_px": 0.010373674753570926,
+ "false_accept_count": 0,
+ "false_reject_count": 0,
+ "quality_false_accept_count": 0,
+ "quality_false_reject_count": 0,
+ "estimated_F": [
+ [
+ 1.0119850289355974,
+ 0.006974982837275979
+ ],
+ [
+ -0.004021334173409178,
+ 0.989015622083158
+ ]
+ ],
+ "oracle_F": [
+ [
+ 1.012,
+ 0.007
+ ],
+ [
+ -0.004,
+ 0.989
+ ]
+ ],
+ "estimated_E": [
+ 0.012064934959158058,
+ -0.01089972444494186,
+ 0.001540707944643743
+ ],
+ "oracle_E": [
+ 0.01207999999999998,
+ -0.010915000000000008,
+ 0.001564
+ ],
+ "fit_count": 81,
+ "fit_rank": 3,
+ "strain_component_abs_error_max": 0.0002738786399401061,
+ "strain_consistency_abs_error_max": 0.00027090769474797413
+ },
+ "gates": {
+ "diagnostics_complete": true,
+ "valid_fraction": true,
+ "rmse_px": true,
+ "p95_error_px": true,
+ "max_error_px": true,
+ "affine_fit": true,
+ "affine_metadata": true,
+ "strain_component_abs_error": true,
+ "strain_consistency_abs_error": true,
+ "strain_valid_fraction": true
+ },
+ "diagnostic_keys": [
+ "converged",
+ "hessian_condition_number",
+ "iterations",
+ "residual_rms",
+ "second_peak_margin",
+ "stop_reason",
+ "zncc"
+ ],
+ "point_count": 81
+ }
+ ],
+ "corruption_panel_version": "image_corruption_panel_v1"
+ },
+ {
+ "run_id": "preflight",
+ "status": "REJECTED",
+ "benchmark_pass": true,
+ "scientific_ok": false,
+ "failure_code": "AMBIGUOUS_TEXTURE",
+ "outcome": "AMBIGUOUS_TEXTURE",
+ "solver_calls": 0,
+ "successful_export_artifacts": 0,
+ "texture_preflight": {
+ "ok": false,
+ "code": "AMBIGUOUS_TEXTURE",
+ "metrics": {
+ "std_gray": 70.47953033447266,
+ "contrast_p95_p5": 200.0,
+ "low_frac": 0.0,
+ "high_frac": 0.0,
+ "structure_tensor_ratio": 0.0,
+ "texture_rank_ratio": 0.0,
+ "minor_to_major_gradient_eigenvalue_ratio": 0.0,
+ "gradient_energy": 729.9887920199736,
+ "directional_gradient_coherence": 0.017320472616451612,
+ "structure_tensor_eigenvalues": [
+ 0.0,
+ 729.9887920199736
+ ],
+ "metrics_version": "structure_tensor_rank1_periodic_v2",
+ "periodicity_score": 0.8157926182583469,
+ "periodicity_period_px": 16,
+ "periodicity_axis": "x",
+ "discriminator_version": "rank_one_periodic_v1",
+ "rank_one_ratio": 0.0,
+ "rank_one_score": 1.0
+ },
+ "details": {
+ "roi": [
+ 40,
+ 60,
+ 88,
+ 88
+ ],
+ "metrics": {
+ "std_gray": 70.47953033447266,
+ "contrast_p95_p5": 200.0,
+ "low_frac": 0.0,
+ "high_frac": 0.0,
+ "structure_tensor_ratio": 0.0,
+ "texture_rank_ratio": 0.0,
+ "minor_to_major_gradient_eigenvalue_ratio": 0.0,
+ "gradient_energy": 729.9887920199736,
+ "directional_gradient_coherence": 0.017320472616451612,
+ "structure_tensor_eigenvalues": [
+ 0.0,
+ 729.9887920199736
+ ],
+ "metrics_version": "structure_tensor_rank1_periodic_v2",
+ "periodicity_score": 0.8157926182583469,
+ "periodicity_period_px": 16,
+ "periodicity_axis": "x",
+ "discriminator_version": "rank_one_periodic_v1",
+ "rank_one_ratio": 0.0,
+ "rank_one_score": 1.0
+ },
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9,
+ "metrics_version": "structure_tensor_rank1_periodic_v2",
+ "discriminator_version": "rank_one_periodic_v1",
+ "preflight_version": "texture_preflight_v2",
+ "message": "AMBIGUOUS_TEXTURE"
+ },
+ "thresholds": {
+ "version": "texture_preflight_v2",
+ "min_std": 8.0,
+ "min_contrast": 25.0,
+ "max_saturated_frac": 0.2,
+ "min_structure_ratio": 0.02,
+ "max_directional_coherence": 0.85,
+ "min_periodicity_score": 0.9
+ }
+ },
+ "gates": {
+ "ambiguous_texture_rejected": true,
+ "zero_successful_export_artifacts": true
+ },
+ "point_count": 81,
+ "case_id": "near_1d_periodic",
+ "kind": "near_1d_periodic",
+ "seed": 29,
+ "case_hash": "39e68dd5016afde118fde61cc62f50eba3fffce320580f3e18c00a6a8fd737cb",
+ "config_hash": "39e68dd5016afde118fde61cc62f50eba3fffce320580f3e18c00a6a8fd737cb",
+ "thresholds": {
+ "expected_failure_code": "AMBIGUOUS_TEXTURE",
+ "max_successful_export_artifacts": 0
+ },
+ "input_sha256": "6f77ebe6d64aead15f0c2c7e0797305b4e9dea71f2813b57b1e4f6eec00d5ec4"
+ }
+ ],
+ "artifacts": {
+ "benchmark_report_csv": "benchmark_report.csv",
+ "benchmark_report_csv_sha256": "55898d18291c1de595e6442a5ee7ad9fe4fc479954bdbc1fcefcd349b87a7939"
+ },
+ "gate_summary": {
+ "numeric_baseline_pass": true,
+ "near_1d_preflight_pass": true,
+ "quality_ranking_pass": true,
+ "quality_threshold_evaluated": false,
+ "quality_threshold_pass": null,
+ "code_provenance": true,
+ "csv_rows": true,
+ "csv_exists": true
+ },
+ "gates": {
+ "numeric_baseline_pass": true,
+ "near_1d_preflight_pass": true,
+ "quality_ranking_pass": true,
+ "quality_threshold_evaluated": false,
+ "quality_threshold_pass": null,
+ "code_provenance": true,
+ "csv_rows": true,
+ "csv_exists": true
+ },
+ "overall_pass": true,
+ "exit_code": 0
+}
diff --git a/paper/figures/synthetic_verification.data.json b/paper/figures/synthetic_verification.data.json
new file mode 100644
index 0000000..d163ba1
--- /dev/null
+++ b/paper/figures/synthetic_verification.data.json
@@ -0,0 +1,827 @@
+{
+ "case": {
+ "case_id": "small_translation",
+ "kind": "translation",
+ "seed": 17,
+ "translation": [
+ 2.3,
+ -1.2
+ ],
+ "pyramid_levels": 1,
+ "search_radius": 8
+ },
+ "input_sha256": "3e804075a1c0fbe3bc2a61d5bfc33184065613263cc05adb328edb1abaad74bc",
+ "points": [
+ {
+ "point_index": "0",
+ "x": "50",
+ "y": "70",
+ "u_raw": "2.3280588388442993",
+ "v_raw": "-1.2017752677202225",
+ "error_u_px": "0.028058838844299494",
+ "error_v_px": "-0.0017752677202225176",
+ "valid": "1"
+ },
+ {
+ "point_index": "1",
+ "x": "58",
+ "y": "70",
+ "u_raw": "2.3137023838628847",
+ "v_raw": "-1.1899362244062688",
+ "error_u_px": "0.013702383862884915",
+ "error_v_px": "0.010063775593731172",
+ "valid": "1"
+ },
+ {
+ "point_index": "2",
+ "x": "66",
+ "y": "70",
+ "u_raw": "2.3139380270712446",
+ "v_raw": "-1.193211086406359",
+ "error_u_px": "0.013938027071244807",
+ "error_v_px": "0.0067889135936409772",
+ "valid": "1"
+ },
+ {
+ "point_index": "3",
+ "x": "74",
+ "y": "70",
+ "u_raw": "2.3241243539321208",
+ "v_raw": "-1.1925076053875425",
+ "error_u_px": "0.024124353932120979",
+ "error_v_px": "0.0074923946124574314",
+ "valid": "1"
+ },
+ {
+ "point_index": "4",
+ "x": "82",
+ "y": "70",
+ "u_raw": "2.3097293893352178",
+ "v_raw": "-1.1897483759647698",
+ "error_u_px": "0.0097293893352179417",
+ "error_v_px": "0.010251624035230167",
+ "valid": "1"
+ },
+ {
+ "point_index": "5",
+ "x": "90",
+ "y": "70",
+ "u_raw": "2.321580544444108",
+ "v_raw": "-1.1987581059459276",
+ "error_u_px": "0.021580544444108174",
+ "error_v_px": "0.0012418940540723789",
+ "valid": "1"
+ },
+ {
+ "point_index": "6",
+ "x": "98",
+ "y": "70",
+ "u_raw": "2.3169958121892229",
+ "v_raw": "-1.1838928085636629",
+ "error_u_px": "0.016995812189223081",
+ "error_v_px": "0.016107191436337098",
+ "valid": "1"
+ },
+ {
+ "point_index": "7",
+ "x": "106",
+ "y": "70",
+ "u_raw": "2.3165865525530993",
+ "v_raw": "-1.1827204366813953",
+ "error_u_px": "0.016586552553099487",
+ "error_v_px": "0.017279563318604696",
+ "valid": "1"
+ },
+ {
+ "point_index": "8",
+ "x": "114",
+ "y": "70",
+ "u_raw": "2.3131421166593955",
+ "v_raw": "-1.1904948925052987",
+ "error_u_px": "0.013142116659395686",
+ "error_v_px": "0.0095051074947012371",
+ "valid": "1"
+ },
+ {
+ "point_index": "9",
+ "x": "50",
+ "y": "78",
+ "u_raw": "2.3132158700668444",
+ "v_raw": "-1.1932865565103536",
+ "error_u_px": "0.013215870066844548",
+ "error_v_px": "0.0067134434896463091",
+ "valid": "1"
+ },
+ {
+ "point_index": "10",
+ "x": "58",
+ "y": "78",
+ "u_raw": "2.3031551837921143",
+ "v_raw": "-1.2031277418136597",
+ "error_u_px": "0.0031551837921144354",
+ "error_v_px": "-0.0031277418136597124",
+ "valid": "1"
+ },
+ {
+ "point_index": "11",
+ "x": "66",
+ "y": "78",
+ "u_raw": "2.3232978572848357",
+ "v_raw": "-1.1877596371876054",
+ "error_u_px": "0.02329785728483591",
+ "error_v_px": "0.012240362812394601",
+ "valid": "1"
+ },
+ {
+ "point_index": "12",
+ "x": "74",
+ "y": "78",
+ "u_raw": "2.3051706898679716",
+ "v_raw": "-1.1946433221348975",
+ "error_u_px": "0.0051706898679717561",
+ "error_v_px": "0.0053566778651024105",
+ "valid": "1"
+ },
+ {
+ "point_index": "13",
+ "x": "82",
+ "y": "78",
+ "u_raw": "2.3138577745224622",
+ "v_raw": "-1.192285108621481",
+ "error_u_px": "0.013857774522462396",
+ "error_v_px": "0.0077148913785189954",
+ "valid": "1"
+ },
+ {
+ "point_index": "14",
+ "x": "90",
+ "y": "78",
+ "u_raw": "2.3061080753435217",
+ "v_raw": "-1.1895719591496019",
+ "error_u_px": "0.0061080753435218682",
+ "error_v_px": "0.010428040850398057",
+ "valid": "1"
+ },
+ {
+ "point_index": "15",
+ "x": "98",
+ "y": "78",
+ "u_raw": "2.3170922683528419",
+ "v_raw": "-1.1868530729181987",
+ "error_u_px": "0.017092268352842055",
+ "error_v_px": "0.013146927081801207",
+ "valid": "1"
+ },
+ {
+ "point_index": "16",
+ "x": "106",
+ "y": "78",
+ "u_raw": "2.312508677063049",
+ "v_raw": "-1.1882836246379271",
+ "error_u_px": "0.012508677063049145",
+ "error_v_px": "0.011716375362072817",
+ "valid": "1"
+ },
+ {
+ "point_index": "17",
+ "x": "114",
+ "y": "78",
+ "u_raw": "2.3138498498223954",
+ "v_raw": "-1.1875753471880868",
+ "error_u_px": "0.013849849822395566",
+ "error_v_px": "0.012424652811913184",
+ "valid": "1"
+ },
+ {
+ "point_index": "18",
+ "x": "50",
+ "y": "86",
+ "u_raw": "2.307450738772729",
+ "v_raw": "-1.1875088725309879",
+ "error_u_px": "0.0074507387727291707",
+ "error_v_px": "0.012491127469012042",
+ "valid": "1"
+ },
+ {
+ "point_index": "19",
+ "x": "58",
+ "y": "86",
+ "u_raw": "2.3217108249664307",
+ "v_raw": "-1.1997828930616379",
+ "error_u_px": "0.021710824966430842",
+ "error_v_px": "0.00021710693836207717",
+ "valid": "1"
+ },
+ {
+ "point_index": "20",
+ "x": "66",
+ "y": "86",
+ "u_raw": "2.3146043195745705",
+ "v_raw": "-1.188764555028432",
+ "error_u_px": "0.014604319574570646",
+ "error_v_px": "0.011235444971567921",
+ "valid": "1"
+ },
+ {
+ "point_index": "21",
+ "x": "74",
+ "y": "86",
+ "u_raw": "2.3191977721025538",
+ "v_raw": "-1.1917061200133032",
+ "error_u_px": "0.019197772102554023",
+ "error_v_px": "0.0082938799866967283",
+ "valid": "1"
+ },
+ {
+ "point_index": "22",
+ "x": "82",
+ "y": "86",
+ "u_raw": "2.3111139960023173",
+ "v_raw": "-1.1890948226061915",
+ "error_u_px": "0.011113996002317439",
+ "error_v_px": "0.010905177393808474",
+ "valid": "1"
+ },
+ {
+ "point_index": "23",
+ "x": "90",
+ "y": "86",
+ "u_raw": "2.3084205882390334",
+ "v_raw": "-1.1858267243418092",
+ "error_u_px": "0.0084205882390335773",
+ "error_v_px": "0.014173275658190709",
+ "valid": "1"
+ },
+ {
+ "point_index": "24",
+ "x": "98",
+ "y": "86",
+ "u_raw": "2.3126976469056784",
+ "v_raw": "-1.1844609437985971",
+ "error_u_px": "0.012697646905678539",
+ "error_v_px": "0.015539056201402834",
+ "valid": "1"
+ },
+ {
+ "point_index": "25",
+ "x": "106",
+ "y": "86",
+ "u_raw": "2.3082904649461873",
+ "v_raw": "-1.1853603453826287",
+ "error_u_px": "0.0082904649461874591",
+ "error_v_px": "0.014639654617371267",
+ "valid": "1"
+ },
+ {
+ "point_index": "26",
+ "x": "114",
+ "y": "86",
+ "u_raw": "2.3222131389100213",
+ "v_raw": "-1.199765052179774",
+ "error_u_px": "0.022213138910021524",
+ "error_v_px": "0.00023494782022592808",
+ "valid": "1"
+ },
+ {
+ "point_index": "27",
+ "x": "50",
+ "y": "94",
+ "u_raw": "2.3101715192596091",
+ "v_raw": "-1.1772551497305612",
+ "error_u_px": "0.010171519259609241",
+ "error_v_px": "0.022744850269438732",
+ "valid": "1"
+ },
+ {
+ "point_index": "28",
+ "x": "58",
+ "y": "94",
+ "u_raw": "2.3066274096771853",
+ "v_raw": "-1.1802703746783303",
+ "error_u_px": "0.0066274096771854474",
+ "error_v_px": "0.019729625321669664",
+ "valid": "1"
+ },
+ {
+ "point_index": "29",
+ "x": "66",
+ "y": "94",
+ "u_raw": "2.3019005704854165",
+ "v_raw": "-1.2018810354616039",
+ "error_u_px": "0.0019005704854166972",
+ "error_v_px": "-0.0018810354616038971",
+ "valid": "1"
+ },
+ {
+ "point_index": "30",
+ "x": "74",
+ "y": "94",
+ "u_raw": "2.3192162590859424",
+ "v_raw": "-1.1811555359589627",
+ "error_u_px": "0.0192162590859426",
+ "error_v_px": "0.018844464041037234",
+ "valid": "1"
+ },
+ {
+ "point_index": "31",
+ "x": "82",
+ "y": "94",
+ "u_raw": "2.3166946044773842",
+ "v_raw": "-1.1859461284741546",
+ "error_u_px": "0.016694604477384356",
+ "error_v_px": "0.014053871525845363",
+ "valid": "1"
+ },
+ {
+ "point_index": "32",
+ "x": "90",
+ "y": "94",
+ "u_raw": "2.317094201411213",
+ "v_raw": "-1.1934276771966301",
+ "error_u_px": "0.017094201411213206",
+ "error_v_px": "0.0065723228033698344",
+ "valid": "1"
+ },
+ {
+ "point_index": "33",
+ "x": "98",
+ "y": "94",
+ "u_raw": "2.3200629200934326",
+ "v_raw": "-1.1813434507575016",
+ "error_u_px": "0.020062920093432801",
+ "error_v_px": "0.018656549242498377",
+ "valid": "1"
+ },
+ {
+ "point_index": "34",
+ "x": "106",
+ "y": "94",
+ "u_raw": "2.3127166176038463",
+ "v_raw": "-1.1851382452390054",
+ "error_u_px": "0.012716617603846458",
+ "error_v_px": "0.014861754760994605",
+ "valid": "1"
+ },
+ {
+ "point_index": "35",
+ "x": "114",
+ "y": "94",
+ "u_raw": "2.3142698374174007",
+ "v_raw": "-1.1892267497726907",
+ "error_u_px": "0.014269837417400844",
+ "error_v_px": "0.010773250227309239",
+ "valid": "1"
+ },
+ {
+ "point_index": "36",
+ "x": "50",
+ "y": "102",
+ "u_raw": "2.3148286080779261",
+ "v_raw": "-1.1898343919268952",
+ "error_u_px": "0.014828608077926297",
+ "error_v_px": "0.010165608073104737",
+ "valid": "1"
+ },
+ {
+ "point_index": "37",
+ "x": "58",
+ "y": "102",
+ "u_raw": "2.3148180548585953",
+ "v_raw": "-1.1931608181754989",
+ "error_u_px": "0.014818054858595442",
+ "error_v_px": "0.0068391818245010505",
+ "valid": "1"
+ },
+ {
+ "point_index": "38",
+ "x": "66",
+ "y": "102",
+ "u_raw": "2.3000170977457017",
+ "v_raw": "-1.1807265177229438",
+ "error_u_px": "1.7097745701910583e-05",
+ "error_v_px": "0.019273482277056164",
+ "valid": "1"
+ },
+ {
+ "point_index": "39",
+ "x": "74",
+ "y": "102",
+ "u_raw": "2.3257531472741926",
+ "v_raw": "-1.1843353386572688",
+ "error_u_px": "0.025753147274192756",
+ "error_v_px": "0.015664661342731145",
+ "valid": "1"
+ },
+ {
+ "point_index": "40",
+ "x": "82",
+ "y": "102",
+ "u_raw": "2.3219383358955383",
+ "v_raw": "-1.1925569921731949",
+ "error_u_px": "0.021938335895538508",
+ "error_v_px": "0.0074430078268050703",
+ "valid": "1"
+ },
+ {
+ "point_index": "41",
+ "x": "90",
+ "y": "102",
+ "u_raw": "2.3040871151962645",
+ "v_raw": "-1.190339487828153",
+ "error_u_px": "0.0040871151962647012",
+ "error_v_px": "0.0096605121718469356",
+ "valid": "1"
+ },
+ {
+ "point_index": "42",
+ "x": "98",
+ "y": "102",
+ "u_raw": "2.3199366705613578",
+ "v_raw": "-1.1868900191131218",
+ "error_u_px": "0.01993667056135795",
+ "error_v_px": "0.013109980886878203",
+ "valid": "1"
+ },
+ {
+ "point_index": "43",
+ "x": "106",
+ "y": "102",
+ "u_raw": "2.319624030765485",
+ "v_raw": "-1.1904409959485878",
+ "error_u_px": "0.019624030765485134",
+ "error_v_px": "0.0095590040514121899",
+ "valid": "1"
+ },
+ {
+ "point_index": "44",
+ "x": "114",
+ "y": "102",
+ "u_raw": "2.3039276159276936",
+ "v_raw": "-1.1912529979690634",
+ "error_u_px": "0.0039276159276937506",
+ "error_v_px": "0.0087470020309365903",
+ "valid": "1"
+ },
+ {
+ "point_index": "45",
+ "x": "50",
+ "y": "110",
+ "u_raw": "2.3163291982013701",
+ "v_raw": "-1.1925941832907119",
+ "error_u_px": "0.016329198201370243",
+ "error_v_px": "0.0074058167092880822",
+ "valid": "1"
+ },
+ {
+ "point_index": "46",
+ "x": "58",
+ "y": "110",
+ "u_raw": "2.3043807906115101",
+ "v_raw": "-1.176779573049417",
+ "error_u_px": "0.0043807906115103101",
+ "error_v_px": "0.023220426950582906",
+ "valid": "1"
+ },
+ {
+ "point_index": "47",
+ "x": "66",
+ "y": "110",
+ "u_raw": "2.3120682841218692",
+ "v_raw": "-1.1769815171169555",
+ "error_u_px": "0.012068284121869333",
+ "error_v_px": "0.023018482883044422",
+ "valid": "1"
+ },
+ {
+ "point_index": "48",
+ "x": "74",
+ "y": "110",
+ "u_raw": "2.3127675950527191",
+ "v_raw": "-1.1919718235731125",
+ "error_u_px": "0.012767595052719294",
+ "error_v_px": "0.0080281764268874678",
+ "valid": "1"
+ },
+ {
+ "point_index": "49",
+ "x": "82",
+ "y": "110",
+ "u_raw": "2.3066532114054854",
+ "v_raw": "-1.1812648737786711",
+ "error_u_px": "0.0066532114054855818",
+ "error_v_px": "0.01873512622132889",
+ "valid": "1"
+ },
+ {
+ "point_index": "50",
+ "x": "90",
+ "y": "110",
+ "u_raw": "2.3022853181190208",
+ "v_raw": "-1.1878596270878559",
+ "error_u_px": "0.0022853181190209959",
+ "error_v_px": "0.012140372912144093",
+ "valid": "1"
+ },
+ {
+ "point_index": "51",
+ "x": "98",
+ "y": "110",
+ "u_raw": "2.3111352870710395",
+ "v_raw": "-1.1946516033327788",
+ "error_u_px": "0.011135287071039723",
+ "error_v_px": "0.0053483966672212002",
+ "valid": "1"
+ },
+ {
+ "point_index": "52",
+ "x": "106",
+ "y": "110",
+ "u_raw": "2.307172566652298",
+ "v_raw": "-1.1937390267848969",
+ "error_u_px": "0.0071725666522981513",
+ "error_v_px": "0.006260973215103105",
+ "valid": "1"
+ },
+ {
+ "point_index": "53",
+ "x": "114",
+ "y": "110",
+ "u_raw": "2.3088588984895391",
+ "v_raw": "-1.1894921728351153",
+ "error_u_px": "0.0088588984895392464",
+ "error_v_px": "0.010507827164884631",
+ "valid": "1"
+ },
+ {
+ "point_index": "54",
+ "x": "50",
+ "y": "118",
+ "u_raw": "2.3177550212158575",
+ "v_raw": "-1.1807460634155793",
+ "error_u_px": "0.017755021215857703",
+ "error_v_px": "0.01925393658442065",
+ "valid": "1"
+ },
+ {
+ "point_index": "55",
+ "x": "58",
+ "y": "118",
+ "u_raw": "2.320700134553368",
+ "v_raw": "-1.1938949143835089",
+ "error_u_px": "0.020700134553368166",
+ "error_v_px": "0.006105085616491035",
+ "valid": "1"
+ },
+ {
+ "point_index": "56",
+ "x": "66",
+ "y": "118",
+ "u_raw": "2.3128510713160138",
+ "v_raw": "-1.1788106070289186",
+ "error_u_px": "0.012851071316013929",
+ "error_v_px": "0.021189392971081356",
+ "valid": "1"
+ },
+ {
+ "point_index": "57",
+ "x": "74",
+ "y": "118",
+ "u_raw": "2.3138567332509226",
+ "v_raw": "-1.1824614034575187",
+ "error_u_px": "0.013856733250922737",
+ "error_v_px": "0.017538596542481244",
+ "valid": "1"
+ },
+ {
+ "point_index": "58",
+ "x": "82",
+ "y": "118",
+ "u_raw": "2.3093684613704681",
+ "v_raw": "-1.1727586537599564",
+ "error_u_px": "0.0093684613704683173",
+ "error_v_px": "0.027241346240043596",
+ "valid": "1"
+ },
+ {
+ "point_index": "59",
+ "x": "90",
+ "y": "118",
+ "u_raw": "2.318942501006684",
+ "v_raw": "-1.1787172467520959",
+ "error_u_px": "0.018942501006684154",
+ "error_v_px": "0.021282753247904074",
+ "valid": "1"
+ },
+ {
+ "point_index": "60",
+ "x": "98",
+ "y": "118",
+ "u_raw": "2.3064028024673462",
+ "v_raw": "-1.1882984191179276",
+ "error_u_px": "0.006402802467346369",
+ "error_v_px": "0.011701580882072404",
+ "valid": "1"
+ },
+ {
+ "point_index": "61",
+ "x": "106",
+ "y": "118",
+ "u_raw": "2.3087711883406072",
+ "v_raw": "-1.1972811660412901",
+ "error_u_px": "0.0087711883406074165",
+ "error_v_px": "0.0027188339587098653",
+ "valid": "1"
+ },
+ {
+ "point_index": "62",
+ "x": "114",
+ "y": "118",
+ "u_raw": "2.2954088786737823",
+ "v_raw": "-1.2030696992407768",
+ "error_u_px": "-0.0045911213262175288",
+ "error_v_px": "-0.0030696992407768242",
+ "valid": "1"
+ },
+ {
+ "point_index": "63",
+ "x": "50",
+ "y": "126",
+ "u_raw": "2.3179540372421301",
+ "v_raw": "-1.1885400208170456",
+ "error_u_px": "0.017954037242130294",
+ "error_v_px": "0.011459979182954383",
+ "valid": "1"
+ },
+ {
+ "point_index": "64",
+ "x": "58",
+ "y": "126",
+ "u_raw": "2.2973829209804535",
+ "v_raw": "-1.1694875359535217",
+ "error_u_px": "-0.0026170790195463312",
+ "error_v_px": "0.030512464046478227",
+ "valid": "1"
+ },
+ {
+ "point_index": "65",
+ "x": "66",
+ "y": "126",
+ "u_raw": "2.324372965383589",
+ "v_raw": "-1.1783759456171834",
+ "error_u_px": "0.024372965383589129",
+ "error_v_px": "0.02162405438281656",
+ "valid": "1"
+ },
+ {
+ "point_index": "66",
+ "x": "74",
+ "y": "126",
+ "u_raw": "2.3093464551497229",
+ "v_raw": "-1.1958378324077885",
+ "error_u_px": "0.0093464551497230985",
+ "error_v_px": "0.0041621675922114409",
+ "valid": "1"
+ },
+ {
+ "point_index": "67",
+ "x": "82",
+ "y": "126",
+ "u_raw": "2.3146425187587738",
+ "v_raw": "-1.1982965767383575",
+ "error_u_px": "0.014642518758773981",
+ "error_v_px": "0.0017034232616424116",
+ "valid": "1"
+ },
+ {
+ "point_index": "68",
+ "x": "90",
+ "y": "126",
+ "u_raw": "2.3091040253639221",
+ "v_raw": "-1.1891994774341583",
+ "error_u_px": "0.0091040253639222968",
+ "error_v_px": "0.01080052256584163",
+ "valid": "1"
+ },
+ {
+ "point_index": "69",
+ "x": "98",
+ "y": "126",
+ "u_raw": "2.3105098322323596",
+ "v_raw": "-1.1783632621558973",
+ "error_u_px": "0.010509832232359795",
+ "error_v_px": "0.021636737844102694",
+ "valid": "1"
+ },
+ {
+ "point_index": "70",
+ "x": "106",
+ "y": "126",
+ "u_raw": "2.3197659462598721",
+ "v_raw": "-1.1783988975415414",
+ "error_u_px": "0.019765946259872269",
+ "error_v_px": "0.021601102458458588",
+ "valid": "1"
+ },
+ {
+ "point_index": "71",
+ "x": "114",
+ "y": "126",
+ "u_raw": "2.3065233826637268",
+ "v_raw": "-1.1714743673801422",
+ "error_u_px": "0.0065233826637269843",
+ "error_v_px": "0.028525632619857744",
+ "valid": "1"
+ },
+ {
+ "point_index": "72",
+ "x": "50",
+ "y": "134",
+ "u_raw": "2.3113817451913516",
+ "v_raw": "-1.1772515163573163",
+ "error_u_px": "0.011381745191351822",
+ "error_v_px": "0.02274848364268367",
+ "valid": "1"
+ },
+ {
+ "point_index": "73",
+ "x": "58",
+ "y": "134",
+ "u_raw": "2.3206130197857124",
+ "v_raw": "-1.2010033230885411",
+ "error_u_px": "0.020613019785712616",
+ "error_v_px": "-0.0010033230885411193",
+ "valid": "1"
+ },
+ {
+ "point_index": "74",
+ "x": "66",
+ "y": "134",
+ "u_raw": "2.3136986479604027",
+ "v_raw": "-1.1866971340356638",
+ "error_u_px": "0.013698647960402877",
+ "error_v_px": "0.013302865964336164",
+ "valid": "1"
+ },
+ {
+ "point_index": "75",
+ "x": "74",
+ "y": "134",
+ "u_raw": "2.3097212927212287",
+ "v_raw": "-1.1820927272744051",
+ "error_u_px": "0.0097212927212289024",
+ "error_v_px": "0.017907272725594892",
+ "valid": "1"
+ },
+ {
+ "point_index": "76",
+ "x": "82",
+ "y": "134",
+ "u_raw": "2.31141395771987",
+ "v_raw": "-1.1921679674695309",
+ "error_u_px": "0.011413957719870194",
+ "error_v_px": "0.0078320325304690463",
+ "valid": "1"
+ },
+ {
+ "point_index": "77",
+ "x": "90",
+ "y": "134",
+ "u_raw": "2.3079878461910517",
+ "v_raw": "-1.1917375732338078",
+ "error_u_px": "0.0079878461910518617",
+ "error_v_px": "0.0082624267661921547",
+ "valid": "1"
+ },
+ {
+ "point_index": "78",
+ "x": "98",
+ "y": "134",
+ "u_raw": "2.3068626944602819",
+ "v_raw": "-1.1928030675741508",
+ "error_u_px": "0.0068626944602820927",
+ "error_v_px": "0.0071969324258491785",
+ "valid": "1"
+ },
+ {
+ "point_index": "79",
+ "x": "106",
+ "y": "134",
+ "u_raw": "2.3172183864139098",
+ "v_raw": "-1.202758641784843",
+ "error_u_px": "0.017218386413909936",
+ "error_v_px": "-0.0027586417848430855",
+ "valid": "1"
+ },
+ {
+ "point_index": "80",
+ "x": "114",
+ "y": "134",
+ "u_raw": "2.2945192158222198",
+ "v_raw": "-1.2025341987609863",
+ "error_u_px": "-0.0054807841777799737",
+ "error_v_px": "-0.0025341987609863725",
+ "valid": "1"
+ }
+ ],
+ "scope": "Clean small-translation benchmark only; full report retained separately"
+}
diff --git a/paper/figures/synthetic_verification.pdf b/paper/figures/synthetic_verification.pdf
new file mode 100644
index 0000000..30f6374
Binary files /dev/null and b/paper/figures/synthetic_verification.pdf differ
diff --git a/paper/figures/synthetic_verification.png b/paper/figures/synthetic_verification.png
new file mode 100644
index 0000000..17865c6
Binary files /dev/null and b/paper/figures/synthetic_verification.png differ
diff --git a/paper/figures/synthetic_verification.svg b/paper/figures/synthetic_verification.svg
new file mode 100644
index 0000000..e5d3bb5
--- /dev/null
+++ b/paper/figures/synthetic_verification.svg
@@ -0,0 +1,1034 @@
+
+
+
diff --git a/paper/figures/workflow.pdf b/paper/figures/workflow.pdf
new file mode 100644
index 0000000..8e6495e
Binary files /dev/null and b/paper/figures/workflow.pdf differ
diff --git a/paper/figures/workflow.png b/paper/figures/workflow.png
new file mode 100644
index 0000000..82651b1
Binary files /dev/null and b/paper/figures/workflow.png differ
diff --git a/paper/figures/workflow.svg b/paper/figures/workflow.svg
new file mode 100644
index 0000000..458d96c
--- /dev/null
+++ b/paper/figures/workflow.svg
@@ -0,0 +1,175 @@
+
+
+
diff --git a/paper/make_figures.py b/paper/make_figures.py
new file mode 100644
index 0000000..016a088
--- /dev/null
+++ b/paper/make_figures.py
@@ -0,0 +1,313 @@
+"""Reproducible publication graphics; no experimental measurements are synthesized."""
+
+import json
+import tempfile
+import numpy as np
+import csv
+from pathlib import Path
+import sys
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
+
+INK = "#142C3D"
+MUTED = "#516570"
+RULE = "#CCD7DC"
+ACCENT = "#087F8C"
+LIGHT = "#EFF7F8"
+WARM = "#B86B20"
+
+
+def style():
+ matplotlib.rcParams.update(
+ {
+ "font.family": "sans-serif",
+ "font.sans-serif": ["Arial", "DejaVu Sans"],
+ "font.size": 10,
+ "axes.labelsize": 10,
+ "axes.titlesize": 11,
+ "xtick.labelsize": 9,
+ "ytick.labelsize": 9,
+ "text.color": INK,
+ "axes.labelcolor": INK,
+ "axes.edgecolor": RULE,
+ "axes.linewidth": 0.7,
+ "lines.linewidth": 1.5,
+ "svg.fonttype": "none",
+ "svg.hashsalt": "publication-20260927",
+ "pdf.fonttype": 42,
+ "savefig.facecolor": "white",
+ }
+ )
+
+
+def canvas(height=4.6):
+ fig = plt.figure(figsize=(7.2, height), facecolor="white")
+ ax = fig.add_axes([0, 0, 1, 1], xlim=(0, 1), ylim=(0, 1))
+ ax.axis("off")
+ return fig, ax
+
+
+def label(ax, x, y, text, size=10, weight="normal", color=INK, ha="left", va="center"):
+ return ax.text(
+ x,
+ y,
+ text,
+ fontsize=size,
+ fontweight=weight,
+ color=color,
+ ha=ha,
+ va=va,
+ linespacing=1.45,
+ )
+
+
+def panel(ax, x, y, letter, title):
+ label(ax, x, y, letter, 12, "bold", ACCENT)
+ label(ax, x + 0.038, y, title, 11, "bold")
+
+
+def box(ax, x, y, w, h, title, body="", accent=None, face=None, size=9.5):
+ accent = ACCENT if accent is None else accent
+ face = LIGHT if face is None else face
+ ax.add_patch(
+ FancyBboxPatch(
+ (x, y),
+ w,
+ h,
+ boxstyle="round,pad=0,rounding_size=0.012",
+ linewidth=0.7,
+ edgecolor=RULE,
+ facecolor=face,
+ )
+ )
+ ax.plot(
+ [x + 0.015, x + 0.015],
+ [y + 0.02, y + h - 0.02],
+ color=accent,
+ lw=2.1,
+ solid_capstyle="round",
+ )
+ label(ax, x + 0.034, y + h - 0.037, title, 10, "bold", accent, va="top")
+ if body:
+ label(ax, x + 0.034, y + h - 0.099, body, size, va="top")
+
+
+def arrow(ax, a, b, color=MUTED, style="-"):
+ ax.add_patch(
+ FancyArrowPatch(
+ a,
+ b,
+ arrowstyle="-|>",
+ mutation_scale=10,
+ linewidth=1,
+ color=color,
+ linestyle=style,
+ shrinkA=2,
+ shrinkB=2,
+ )
+ )
+
+
+def save(fig, folder, stem, formats=("png", "svg", "pdf")):
+ folder.mkdir(parents=True, exist_ok=True)
+ for ext in formats:
+ meta = (
+ {"Date": None}
+ if ext == "svg"
+ else ({"CreationDate": None, "ModDate": None} if ext == "pdf" else {})
+ )
+ fig.savefig(folder / f"{stem}.{ext}", dpi=450, metadata=meta)
+ plt.close(fig)
+
+
+def clean_axes(ax):
+ ax.spines[["top", "right"]].set_visible(False)
+ ax.tick_params(length=3, width=0.6, color=RULE)
+ ax.grid(axis="y", color=RULE, linewidth=0.5, alpha=0.6)
+ ax.set_axisbelow(True)
+
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+PAPER = ROOT / "paper/figures"
+ACCENT = "#137E68"
+LIGHT = "#EFF8F4"
+
+
+def workflow():
+ fig, ax = canvas(4.8)
+ panel(ax, 0.035, 0.95, "a", "Two measurements from image sequences")
+ box(
+ ax,
+ 0.035,
+ 0.685,
+ 0.27,
+ 0.195,
+ "Inputs",
+ "Reference + image series\nRegions and configuration",
+ size=9,
+ )
+ box(
+ ax,
+ 0.385,
+ 0.695,
+ 0.58,
+ 0.185,
+ "Virtual extensometer",
+ "Track a region pair → gauge-length change\nEngineering strain and true strain",
+ size=10,
+ )
+ box(
+ ax,
+ 0.385,
+ 0.385,
+ 0.58,
+ 0.235,
+ "Fixed-reference 2D DIC",
+ "Local subsets → displacement → strain\nDisplacement and strain validity kept separate\nInfinitesimal / Green–Lagrange tensor strain",
+ size=9.5,
+ )
+ arrow(ax, (0.305, 0.79), (0.385, 0.79), ACCENT)
+ ax.plot([0.325, 0.325], [0.79, 0.505], color=MUTED, lw=1)
+ arrow(ax, (0.325, 0.505), (0.385, 0.505), ACCENT)
+ label(ax, 0.035, 0.535, "In-plane images", 10, "bold", ACCENT)
+ label(
+ ax, 0.035, 0.475, "Texture and geometry\nlimit interpretation", 9, color=MUTED
+ )
+ ax.plot([0.035, 0.965], [0.325, 0.325], color=RULE, lw=0.8)
+ panel(ax, 0.035, 0.265, "b", "Outputs retain the analysis context")
+ label(ax, 0.035, 0.19, "Tables and maps", 11, "bold", ACCENT)
+ label(ax, 0.40, 0.19, "Diagnostics and validity", 11, "bold", ACCENT)
+ label(
+ ax,
+ 0.035,
+ 0.13,
+ "Configuration · input identities · source fingerprints · file manifest",
+ 9.6,
+ )
+ label(
+ ax,
+ 0.035,
+ 0.055,
+ "Failed measurements remain explicit; synthetic verification does not establish camera uncertainty.",
+ 8.5,
+ color=MUTED,
+ )
+ save(fig, PAPER, "workflow")
+
+
+def benchmark_figure(destination):
+ from benchmarks.run_benchmark import run_benchmark
+ from benchmarks.synthetic_cases import make_case, locked_case_document
+
+ report = run_benchmark(output_dir=destination)
+ assert report["overall_pass"], "Locked benchmark failed"
+ records = list(
+ csv.DictReader((destination / "benchmark_report.csv").open(encoding="utf-8"))
+ )
+ rows = [
+ r
+ for r in records
+ if r["case_id"] == "small_translation" and r["run_id"] == "clean"
+ ]
+ case = next(
+ c
+ for c in locked_case_document()["cases"]
+ if c["case_id"] == "small_translation"
+ )
+ fixture = make_case(case)
+ fig = plt.figure(figsize=(7.2, 4.65))
+ a = fig.add_axes([0.08, 0.25, 0.35, 0.57])
+ b = fig.add_axes([0.59, 0.25, 0.35, 0.57])
+ a.imshow(
+ fixture["reference"],
+ cmap="gray",
+ vmin=0,
+ vmax=255,
+ origin="upper",
+ interpolation="nearest",
+ )
+ xy = fixture["coordinates"]
+ a.scatter(xy[:, 0], xy[:, 1], facecolors="none", edgecolors="#E69F00", s=9, lw=0.65)
+ a.set_title("a Locked synthetic reference", loc="left", fontweight="bold", pad=13)
+ a.set(xlabel="x (pixels)", ylabel="y (pixels)")
+ indices = np.array([int(r["point_index"]) for r in rows])
+ for key, marker, color, title in [
+ ("error_u_px", "o", ACCENT, "u error"),
+ ("error_v_px", "s", WARM, "v error"),
+ ]:
+ vals = np.array([float(r[key]) for r in rows])
+ b.plot(indices, vals, marker=marker, ms=3, lw=0, color=color, label=title)
+ b.axhline(0, color=MUTED, lw=0.7, ls="--")
+ b.set_title("b Displacement residuals", loc="left", fontweight="bold", pad=13)
+ b.set(xlabel="Point index", ylabel="Estimated − prescribed (pixels)")
+ b.legend(
+ frameon=False,
+ fontsize=9,
+ ncol=2,
+ loc="upper center",
+ bbox_to_anchor=(0.5, 1.02),
+ )
+ b.set_ylim(-0.007, 0.041)
+ clean_axes(b)
+ fig.text(
+ 0.08,
+ 0.91,
+ "Prescribed translation: u = 2.3 px, v = −1.2 px · 81 evaluation points",
+ fontsize=10,
+ color=MUTED,
+ )
+ fig.text(
+ 0.08,
+ 0.075,
+ "Clean small-translation case from the locked benchmark; no experimental images.",
+ fontsize=9,
+ color=MUTED,
+ )
+ save(fig, PAPER, "synthetic_verification")
+ # Retain exact plotted observations and the complete benchmark report for audit.
+ (PAPER / "synthetic_verification.data.json").write_text(
+ json.dumps(
+ {
+ "case": case,
+ "input_sha256": fixture["input_sha256"],
+ "points": [
+ {
+ k: r[k]
+ for k in (
+ "point_index",
+ "x",
+ "y",
+ "u_raw",
+ "v_raw",
+ "error_u_px",
+ "error_v_px",
+ "valid",
+ )
+ }
+ for r in rows
+ ],
+ "scope": "Clean small-translation benchmark only; full report retained separately",
+ },
+ indent=2,
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+ (PAPER / "benchmark_report.json").write_text(
+ json.dumps(report, indent=2) + "\n", encoding="utf-8"
+ )
+
+
+def main():
+ style()
+ workflow()
+ with tempfile.TemporaryDirectory(prefix="straintrace_figures_") as temp:
+ benchmark_figure(Path(temp))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/paper/paper.md b/paper/paper.md
index 3050cdb..055971b 100644
--- a/paper/paper.md
+++ b/paper/paper.md
@@ -77,6 +77,8 @@ established package rather than interpret this scope as a substitute.
# Software design
+{#fig:workflow width="100%"}
+
StrainTrace separates its numerical and export functions from the Tk desktop
interface. The command-line interface validates a versioned JSON configuration,
normalizes defaults and rejects unknown fields or non-finite values. The same
@@ -115,6 +117,8 @@ The benchmark also distinguishes quality ranking from calibration of a quality
threshold. Passing it does not establish uncertainty for an experimental camera,
specimen or texture, and the paper makes no such claim.
+{#fig:verification width="100%"}
+
# Research impact statement
On 27 September 2026, the author confirmed using StrainTrace in the research
@@ -136,6 +140,8 @@ assistance has not been confirmed for this software. The sole author must review
edit and validate the assisted outputs and confirm the complete disclosure and
human responsibility for scientific and design decisions before submission.
+The README cover is AI-generated conceptual artwork. Manuscript diagrams and numerical plots are produced by repository scripts; scientific data are not retouched by an image-generation model.
+
# Acknowledgements
No external funding was received for this software. There was no sponsor