quantized-lab 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. quantized/__init__.py +11 -0
  2. quantized/__main__.py +14 -0
  3. quantized/api.py +220 -0
  4. quantized/app.py +206 -0
  5. quantized/calc/__init__.py +8 -0
  6. quantized/calc/_clipfit.py +76 -0
  7. quantized/calc/_natural_neighbor.py +219 -0
  8. quantized/calc/aggregate.py +159 -0
  9. quantized/calc/backgrounds.py +353 -0
  10. quantized/calc/baseline.py +349 -0
  11. quantized/calc/batch_fit.py +148 -0
  12. quantized/calc/constants.py +27 -0
  13. quantized/calc/corrections.py +192 -0
  14. quantized/calc/crystallography.py +400 -0
  15. quantized/calc/diffusion.py +120 -0
  16. quantized/calc/electrical.py +248 -0
  17. quantized/calc/electrochemistry.py +176 -0
  18. quantized/calc/element_data.json +1 -0
  19. quantized/calc/element_data.py +59 -0
  20. quantized/calc/errors.py +246 -0
  21. quantized/calc/figure.py +417 -0
  22. quantized/calc/figure_break.py +152 -0
  23. quantized/calc/figure_categorical.py +156 -0
  24. quantized/calc/figure_corner.py +229 -0
  25. quantized/calc/figure_facets.py +127 -0
  26. quantized/calc/figure_field.py +137 -0
  27. quantized/calc/figure_hitmap.py +116 -0
  28. quantized/calc/figure_labels.py +62 -0
  29. quantized/calc/figure_map.py +287 -0
  30. quantized/calc/figure_overrides.py +159 -0
  31. quantized/calc/figure_page.py +266 -0
  32. quantized/calc/figure_scale.py +125 -0
  33. quantized/calc/figure_statplots.py +167 -0
  34. quantized/calc/figure_styles.py +131 -0
  35. quantized/calc/figure_ternary.py +239 -0
  36. quantized/calc/figure_ticks.py +217 -0
  37. quantized/calc/fit_autoguess.py +156 -0
  38. quantized/calc/fit_bootstrap.py +163 -0
  39. quantized/calc/fit_bumps.py +258 -0
  40. quantized/calc/fit_constraints.py +114 -0
  41. quantized/calc/fit_equation.py +264 -0
  42. quantized/calc/fit_findxy.py +80 -0
  43. quantized/calc/fit_models.py +195 -0
  44. quantized/calc/fit_models_special.py +189 -0
  45. quantized/calc/fit_odr.py +99 -0
  46. quantized/calc/fit_scan.py +243 -0
  47. quantized/calc/fit_stats.py +215 -0
  48. quantized/calc/fitting.py +199 -0
  49. quantized/calc/formula.py +90 -0
  50. quantized/calc/global_curve_fit.py +305 -0
  51. quantized/calc/global_fit.py +181 -0
  52. quantized/calc/interp2d.py +260 -0
  53. quantized/calc/linecut.py +269 -0
  54. quantized/calc/magnetic.py +414 -0
  55. quantized/calc/magnetometry.py +464 -0
  56. quantized/calc/map.py +228 -0
  57. quantized/calc/mcmc.py +177 -0
  58. quantized/calc/optics.py +228 -0
  59. quantized/calc/pawley.py +251 -0
  60. quantized/calc/peak_batch.py +128 -0
  61. quantized/calc/peak_fit.py +259 -0
  62. quantized/calc/peak_integrate.py +104 -0
  63. quantized/calc/peak_multifit.py +260 -0
  64. quantized/calc/peak_track.py +134 -0
  65. quantized/calc/peaks.py +298 -0
  66. quantized/calc/peakshapes.py +85 -0
  67. quantized/calc/plotting.py +147 -0
  68. quantized/calc/processing.py +232 -0
  69. quantized/calc/qspace.py +48 -0
  70. quantized/calc/reductions.py +155 -0
  71. quantized/calc/reductions_fft.py +383 -0
  72. quantized/calc/refl_sld_presets.json +1 -0
  73. quantized/calc/reflectivity.py +80 -0
  74. quantized/calc/registry.py +303 -0
  75. quantized/calc/relaxation.py +119 -0
  76. quantized/calc/report.py +253 -0
  77. quantized/calc/report_emit.py +227 -0
  78. quantized/calc/resample.py +142 -0
  79. quantized/calc/rsm.py +91 -0
  80. quantized/calc/rsm_analyze.py +245 -0
  81. quantized/calc/semiconductor.py +488 -0
  82. quantized/calc/sld.py +131 -0
  83. quantized/calc/sld_formula.py +138 -0
  84. quantized/calc/spectral.py +357 -0
  85. quantized/calc/statplots.py +214 -0
  86. quantized/calc/stats.py +399 -0
  87. quantized/calc/stats_anova2.py +202 -0
  88. quantized/calc/stats_anova_ext.py +338 -0
  89. quantized/calc/stats_dist.py +196 -0
  90. quantized/calc/stats_glm.py +245 -0
  91. quantized/calc/stats_multivar.py +289 -0
  92. quantized/calc/stats_roc.py +157 -0
  93. quantized/calc/stats_survival.py +261 -0
  94. quantized/calc/stats_tests.py +380 -0
  95. quantized/calc/substrates.py +181 -0
  96. quantized/calc/superconductor.py +359 -0
  97. quantized/calc/surface_fit.py +290 -0
  98. quantized/calc/surface_models.py +156 -0
  99. quantized/calc/thermal.py +119 -0
  100. quantized/calc/thin_film.py +425 -0
  101. quantized/calc/unit_convert.py +259 -0
  102. quantized/calc/units.py +80 -0
  103. quantized/calc/vacuum.py +290 -0
  104. quantized/calc/xray.py +169 -0
  105. quantized/cli.py +214 -0
  106. quantized/datastruct.py +153 -0
  107. quantized/io/__init__.py +11 -0
  108. quantized/io/_hdf5_layout.py +308 -0
  109. quantized/io/_jcamp_asdf.py +135 -0
  110. quantized/io/_xrdml_scan.py +291 -0
  111. quantized/io/base.py +82 -0
  112. quantized/io/bruker_brml.py +177 -0
  113. quantized/io/bruker_raw.py +158 -0
  114. quantized/io/cif.py +266 -0
  115. quantized/io/consolidated.py +122 -0
  116. quantized/io/delimited.py +222 -0
  117. quantized/io/excel.py +135 -0
  118. quantized/io/hdf5.py +192 -0
  119. quantized/io/import_filters.py +178 -0
  120. quantized/io/import_preview.py +262 -0
  121. quantized/io/jcamp.py +179 -0
  122. quantized/io/lakeshore.py +163 -0
  123. quantized/io/ncnr.py +278 -0
  124. quantized/io/netcdf.py +195 -0
  125. quantized/io/opus.py +231 -0
  126. quantized/io/origin.py +346 -0
  127. quantized/io/origin_com.py +194 -0
  128. quantized/io/origin_project/__init__.py +221 -0
  129. quantized/io/origin_project/annotation_marks.py +288 -0
  130. quantized/io/origin_project/container.py +262 -0
  131. quantized/io/origin_project/curve_style_color.py +359 -0
  132. quantized/io/origin_project/figure_geometry.py +108 -0
  133. quantized/io/origin_project/figure_layers.py +333 -0
  134. quantized/io/origin_project/figure_text.py +258 -0
  135. quantized/io/origin_project/figures.py +210 -0
  136. quantized/io/origin_project/figures_opju.py +440 -0
  137. quantized/io/origin_project/notes.py +302 -0
  138. quantized/io/origin_project/opj.py +459 -0
  139. quantized/io/origin_project/opj_curves.py +297 -0
  140. quantized/io/origin_project/opj_shapes.py +148 -0
  141. quantized/io/origin_project/opju.py +146 -0
  142. quantized/io/origin_project/opju_axis_real_form.py +418 -0
  143. quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
  144. quantized/io/origin_project/opju_codec.py +370 -0
  145. quantized/io/origin_project/opju_curves.py +497 -0
  146. quantized/io/origin_project/opju_curves_allcols.py +258 -0
  147. quantized/io/origin_project/opju_figure_curves.py +302 -0
  148. quantized/io/origin_project/opju_figure_text.py +245 -0
  149. quantized/io/origin_project/opju_reports.py +129 -0
  150. quantized/io/origin_project/origin_richtext.py +145 -0
  151. quantized/io/origin_project/preview.py +132 -0
  152. quantized/io/origin_project/templates.py +314 -0
  153. quantized/io/origin_project/tree.py +379 -0
  154. quantized/io/origin_project/tree_opju.py +228 -0
  155. quantized/io/origin_project/windows.py +238 -0
  156. quantized/io/origin_project/windows_opju.py +393 -0
  157. quantized/io/origin_project/writer.py +156 -0
  158. quantized/io/origin_project/writer_blocks.py +282 -0
  159. quantized/io/qd.py +380 -0
  160. quantized/io/refl1d.py +132 -0
  161. quantized/io/registry.py +210 -0
  162. quantized/io/report_export.py +347 -0
  163. quantized/io/rigaku.py +100 -0
  164. quantized/io/sims.py +398 -0
  165. quantized/io/spc.py +311 -0
  166. quantized/io/xrd_csv.py +308 -0
  167. quantized/io/xrdml.py +394 -0
  168. quantized/jobs.py +173 -0
  169. quantized/plugins/__init__.py +50 -0
  170. quantized/plugins/contract.py +111 -0
  171. quantized/plugins/loader.py +394 -0
  172. quantized/plugins/steps.py +90 -0
  173. quantized/routes/__init__.py +7 -0
  174. quantized/routes/_bookcache.py +62 -0
  175. quantized/routes/_export_common.py +27 -0
  176. quantized/routes/_payload.py +58 -0
  177. quantized/routes/_uploadcache.py +59 -0
  178. quantized/routes/aggregate.py +46 -0
  179. quantized/routes/baseline.py +210 -0
  180. quantized/routes/books.py +117 -0
  181. quantized/routes/calc.py +56 -0
  182. quantized/routes/corrections.py +78 -0
  183. quantized/routes/crystallography.py +80 -0
  184. quantized/routes/diffusion.py +58 -0
  185. quantized/routes/electrical.py +101 -0
  186. quantized/routes/electrochemistry.py +83 -0
  187. quantized/routes/export.py +280 -0
  188. quantized/routes/export_facets.py +83 -0
  189. quantized/routes/export_figures.py +471 -0
  190. quantized/routes/export_page.py +125 -0
  191. quantized/routes/fitting.py +379 -0
  192. quantized/routes/fitting_bumps.py +97 -0
  193. quantized/routes/import_template.py +97 -0
  194. quantized/routes/import_wizard.py +150 -0
  195. quantized/routes/jobs_api.py +59 -0
  196. quantized/routes/magnetic.py +135 -0
  197. quantized/routes/magnetometry.py +133 -0
  198. quantized/routes/optics.py +98 -0
  199. quantized/routes/parsers.py +281 -0
  200. quantized/routes/peaks.py +184 -0
  201. quantized/routes/plot.py +103 -0
  202. quantized/routes/reductions.py +121 -0
  203. quantized/routes/reference.py +58 -0
  204. quantized/routes/reflectivity.py +91 -0
  205. quantized/routes/report_export.py +119 -0
  206. quantized/routes/rsm.py +136 -0
  207. quantized/routes/samples.py +32 -0
  208. quantized/routes/semiconductor.py +207 -0
  209. quantized/routes/sld.py +42 -0
  210. quantized/routes/spectral.py +54 -0
  211. quantized/routes/statplots.py +99 -0
  212. quantized/routes/stats.py +418 -0
  213. quantized/routes/stats_design.py +321 -0
  214. quantized/routes/substrates.py +46 -0
  215. quantized/routes/superconductor.py +139 -0
  216. quantized/routes/thermal.py +57 -0
  217. quantized/routes/thin_film.py +153 -0
  218. quantized/routes/vacuum.py +113 -0
  219. quantized/routes/xray.py +32 -0
  220. quantized/samples/demo_vsm.csv +42 -0
  221. quantized/server_launch.py +251 -0
  222. quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  223. quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  224. quantized/web/assets/index-BHmmCL-x.js +27 -0
  225. quantized/web/assets/index-BiZzN7J6.css +1 -0
  226. quantized/web/index.html +13 -0
  227. quantized/web/loading.html +69 -0
  228. quantized_lab-0.8.0.dist-info/METADATA +122 -0
  229. quantized_lab-0.8.0.dist-info/RECORD +233 -0
  230. quantized_lab-0.8.0.dist-info/WHEEL +4 -0
  231. quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
  232. quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
  233. quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
@@ -0,0 +1,497 @@
1
+ """Per-curve dataset/column binding for ``.opju`` (CPYUA) graph figures (item 35).
2
+
3
+ Every curve (``DataPlot``) object carries its own copy of the generic CPYUA
4
+ "graph object" header (the same ``58 80 09 98 03 40 B3 <u32 bodyLen>`` shape
5
+ axis/legend/config objects also use — this header is NOT curve-exclusive, so
6
+ it cannot be located by the magic bytes alone). Immediately inside a curve's
7
+ own body, at a position anchored right after the object's name token
8
+ (auto-named ``_NNN``, mirroring ``.opj``'s convention — see ``figures.py``),
9
+ sits a small fixed-shape token that was diffed out of a purpose-built
10
+ oracle, ``fig_pairs.opju`` (one project, 4 graphs: A-B scatter, A-B scatter
11
+ log-Y, **A-C scatter** — the deliberate diff — and A-B line; see
12
+ ``tools/origin_trial/generate_specimens.py``'s ``fig_pairs`` section, whose
13
+ LabTalk ``plotxy iy:=`` calls are the ground truth since Origin's own GT
14
+ exporter did not capture this — see "Missing oracle" below):
15
+
16
+ ```
17
+ <flag:1> 01 <konst:1> 01 80 03 <y_ord:1> 00
18
+ ```
19
+
20
+ Byte-for-byte diffing all 4 fig_pairs curves against each other (2 pairs
21
+ share columns A,B; one pair differs only in log-Y; one differs only in plot
22
+ type line-vs-scatter; one — the deliberate diff — plots A,C instead of A,B)
23
+ isolated exactly 2 differing bytes total: ``flag`` (a per-curve
24
+ creation-order/style counter — increments monotonically across curves
25
+ regardless of column choice, confirmed unrelated) and ``y_ord`` (changes
26
+ from ``0x02`` to ``0x03`` in lockstep with the B->C column swap, and only
27
+ then). ``y_ord`` is a **1-based ordinal counted cumulatively across every
28
+ column of every workbook the project's FPC codec (``opju_codec.scan_columns``)
29
+ actually decoded, in book-appearance order** (a book with zero decodable
30
+ columns — e.g. an unused default "Book1" — does not participate in the
31
+ count; confirmed by reproducing the corpus's own book/column layout and
32
+ finding the decoded ordinal always lands exactly on the expected column).
33
+
34
+ **Validation.**
35
+
36
+ * ``fig_pairs`` (self-authored, by-construction ground truth — Origin's own
37
+ exporter has no oracle for this, see below): the byte pattern's regex
38
+ ``.\\x01.\\x01\\x80\\x03.\\x00`` finds *exactly* the 4 expected curve
39
+ tokens in the whole file (no false positives), and every ``y_ord`` decodes
40
+ to the exact column the generation script plotted (A,B / A,B / **A,C** /
41
+ A,B) — all 4 survive the designation gate below unchanged.
42
+ ``test_realdata_fig_pairs_curve_bindings`` asserts this exactly.
43
+ * Real corpus (RockingCurve, XAS, UnpolPlots, "Fixed Lambdas SI" — no GT
44
+ oracle available either, see below): a whole-file scan finds ~40 curve
45
+ tokens; scoped per decoded figure's window and passed through the
46
+ designation gate, 2-4 survive per file (12 total across the 4 files) —
47
+ every single one lands on a column ``windows_opju.py`` independently
48
+ designates ``"Y"`` (never ``"X"``/``"Y-error"``) with a physically-sensible
49
+ dependent-variable long-name (``Intensity``, ``Absorption``, ``R``,
50
+ "Theory SA"), never an independent-variable one (``Theta``, ``Energy``,
51
+ ``Q``, ``Z``). RockingCurve's ``NbAuRocking`` figure's surviving curve
52
+ count also matches the independent legend-derived ``n_curves`` signal
53
+ exactly (2 == 2). ``test_realdata_real_corpus_curves_are_plausible``
54
+ asserts the designation/long-name check for all four files.
55
+
56
+ **Missing oracle.** ``tools/origin_trial/export_ground_truth.py`` was meant
57
+ to dump Origin's own per-plot dataset references (``layer.nplots`` +
58
+ ``range __rp = {pi}; ... __rp.name$``) into each ``index.json``'s
59
+ ``graphs[].layers[].plots`` list. In every project exported in this corpus
60
+ that list comes back **empty** (a LabTalk/COM issue in that trial-window
61
+ script — the ``range __rp = {pi}`` assignment does not behave as
62
+ documented — not something fixable here: this module is pure byte-level RE,
63
+ no Origin). So no direct "Origin says the answer is X" string match was
64
+ possible for curve bindings, for either the specimens or the real corpus;
65
+ validation instead rests on the by-construction specimen (still a real
66
+ ground truth — the script wrote the ``iy:=`` argument, we did not invent
67
+ it) plus the designation gate and independent-signal cross-checks above.
68
+
69
+ **Known gap — per-figure attribution (significant recall loss).** Deciding
70
+ *which* of a graph's curve tokens belongs to *which* decoded axis-anchor
71
+ window is a best-effort byte-range heuristic: a curve counts toward a figure
72
+ only when its token falls inside ``[anchor, next_anchor)`` AND survives the
73
+ designation gate. This drops the *majority* of curve tokens for composite/
74
+ derived real-corpus graphs — confirmed: "Fixed Lambdas SI"'s ten cleanest
75
+ tokens (each landing exactly on a different PNR book's reflectivity column,
76
+ one per book) sit entirely *outside* all four of that file's decoded
77
+ figures' windows, so none of them ship; what each figure gets instead is
78
+ whatever handful of tokens happen to sit inside its own narrow window (often
79
+ just 1, and not necessarily the "main" curve a user would expect). XAS's
80
+ ``Co`` self-curve and two of UnpolPlots's curves are similarly never
81
+ attributed to any figure. This is an honest, *significant* recall gap, not a
82
+ soundness one: every curve that IS reported is independently confirmed
83
+ (designation + long-name), never fabricated or mis-typed. Closing it needs
84
+ either a real oracle (the missing ``plots`` export) or a further RE pass to
85
+ locate the object boundary that actually scopes a curve to its owning
86
+ layer (not yet found).
87
+
88
+ **Multi-curve-per-layer and multi-book layout — confirmed against two new
89
+ controlled specimens (item 35 recall push, 2026-07-04).** ``curves_multi.opju``
90
+ (one graph, one layer, three curves — MBook B/C/D vs A) and
91
+ ``curves_2books.opju`` (one graph, curves from two different books —
92
+ ``BookOne!B`` and ``BookTwo!C``) were purpose-built to answer two open
93
+ questions: how do *multiple* curve tokens sit in one layer, and does the
94
+ cumulative-ordinal base really carry over correctly across a book boundary?
95
+ Both answers turn out to be exactly what this module already implements,
96
+ with **zero code change required** — these specimens are validating
97
+ regression tests, not bug reports:
98
+
99
+ * *Multi-curve layout*: each curve is a fully self-contained, back-to-back
100
+ copy of the same ~750-900-byte "graph object" (the generic
101
+ ``58 80 09 98 03 40 B3`` header + its own style/pen sub-records + its own
102
+ copy of the 8-byte token). ``curves_multi``'s three curves' tokens sit at
103
+ offsets 789 bytes apart, each with a strictly increasing ``y_ord``
104
+ (``0x02``, ``0x03``, ``0x04`` — MBook's B, C, D) and no shared/wrapper
105
+ record binding them together. There is no "run" or "count" prefix to
106
+ decode; the whole-file regex scan already finds all three independently,
107
+ and every one survives the designation gate and resolves correctly.
108
+ * *Cross-book base*: ``curves_2books`` plots ``BookOne!B`` (``y_ord=2``,
109
+ since BookOne contributes columns 1-2) and ``BookTwo!C`` (``y_ord=5``,
110
+ since BookOne's 2 columns are counted before BookTwo's 3 start at ordinal
111
+ 3) — exactly what ``_global_column_map``'s cumulative, book-appearance-order
112
+ counting already produces. No change needed.
113
+
114
+ Both specimens decode with 100% precision *and* 100% recall via the
115
+ existing pipeline (regex → designation gate → BCO gate), confirmed by
116
+ ``test_realdata_curves_multi_bindings`` and
117
+ ``test_realdata_curves_2books_bindings``. This raises the aggregate
118
+ oracle-covered recall from 6/31 (19.4%) to 11/36 (30.6%) — see
119
+ ``docs/origin_project_format.md`` §6.2.1 for the updated per-stem table.
120
+
121
+ **A second near-miss shape, found and confirmed excluded — the per-book
122
+ "column candidate list."** Investigating why real-corpus recall stays far
123
+ below what these clean specimens suggest turned up a second decoy shape,
124
+ structurally distinct from ``__BCO`` but with the same danger profile: a
125
+ run of 7-byte records, one per column of a referenced book, in column
126
+ order —
127
+
128
+ ```
129
+ <flag:1> 01 <marker:1> 80 03 <ord:1> 00
130
+ ```
131
+
132
+ — found near *every* book reference in *every* file checked, both new
133
+ specimens included (e.g. ``curves_multi`` shows MBook's A/B/C/D enumerated
134
+ this way *in addition to* the three real per-curve tokens). It is one byte
135
+ *shorter* than the real curve token — a single ``0x01`` (position 1) then
136
+ straight to ``0x80 0x03``, never the real token's ``01 .. 01 80 03`` double
137
+ ``0x01`` — so it can **never** satisfy ``_CURVE_RE`` (which requires two
138
+ literal ``0x01`` bytes at positions 1 and 3); confirmed by direct
139
+ byte-window inspection, not just by construction. Whether this list's
140
+ entries carry *any* independently-decodable "this one is selected" marker
141
+ was checked directly (tail bytes compared item-by-item across several
142
+ real-corpus runs, e.g. "Fixed Lambdas SI"'s ``PNRNbAu100nm`` A-K run) — they
143
+ do not; the items are byte-identical in shape apart from the running
144
+ ordinal. In every run checked, the columns that happen to be *actually*
145
+ plotted are the run's *last* one to three entries, but that is a corpus
146
+ *convention* (derived "SA"/"dSA"/"Theory SA" analysis columns are
147
+ habitually appended last), not a decodable structural signal — using
148
+ "trust the tail of the list" would be exactly the kind of guess this
149
+ module's design forbids, and was rejected.
150
+ ``test_synthetic_column_enum_list_not_mistaken_for_curve_token`` is a
151
+ regression guard: a future regex relaxation must not start accepting this
152
+ shape.
153
+
154
+ **Remaining gap, sharpened by the above.** RockingCurve's ``Graph1``
155
+ (``Nb!B``) and ``Graph2`` (``NbAl!B``), and essentially all of XAS's and
156
+ UnpolPlots's oracle-required curves, have **neither** a real 8-byte token
157
+ **nor** a column-candidate-list tail match anywhere in the file — an
158
+ exhaustive whole-file regex scan (both shapes) confirms zero candidates
159
+ exist for these pairs at all. These are ordinary, single-curve, default-
160
+ dialog graphs (unlike ``NbAuRocking``'s custom-styled multi-curve layer,
161
+ or "Fixed Lambdas SI"'s "Theory SA" reference-overlay curves, both of which
162
+ DO carry the real token) — Origin evidently encodes their column choice a
163
+ third way, not yet located. This — not the multi-curve/multi-book layout,
164
+ which is solved — is why real-corpus recall stays low; closing it needs a
165
+ further RE pass specifically on simple/default single-curve graphs.
166
+
167
+ **X is not decoded.** ``konst`` (the position a naive by-symmetry read would
168
+ expect an X-column ordinal to occupy) was observed as exactly ``0x01`` in
169
+ *every one* of ~44 samples across the specimen and the full real corpus —
170
+ zero variation, including cases whose Y column belongs to a *different*
171
+ workbook than the layer's other curves. That is equally consistent with
172
+ "X is always column A" and with "this byte is an unrelated constant,
173
+ unconfirmed to be a column selector at all" — no specimen ever varied X, so
174
+ neither reading can be confirmed and it is not reported as a decoded value.
175
+ Instead, ``"x"`` in this module's output is a **structural inference**: the
176
+ Y column's own workbook's first column — Origin's near-universal per-sheet
177
+ X designation, independently confirmed via ``windows_opju.py``'s validated
178
+ designation markers for every corpus book checked here. This may be wrong
179
+ for a workbook whose designated X column is not the first one; no such case
180
+ was observed in this corpus.
181
+
182
+ **Designation gate (precision over recall).** The whole-file regex scan is
183
+ not curve-exclusive enough on its own: one real-corpus file ("Fixed Lambdas
184
+ SI") produced a ``y_ord`` landing on a ``Y-error`` column (``dQ``, a PNR
185
+ uncertainty column) inside a decoded figure's window — clearly not a real
186
+ plotted curve. Rather than report it, every resolved ``y`` column is
187
+ cross-checked against ``windows_opju.opju_window_metadata``'s independently
188
+ validated per-column designation and **dropped unless it is exactly
189
+ ``"Y"``** (never ``"X"``, ``"Y-error"``, or unresolved). This trades recall
190
+ for precision, matching the "never guess" rule: some real curves are lost
191
+ when a book's window-section metadata doesn't resolve (windows_opju.py's own
192
+ documented limitation), but nothing reported here is a mis-typed column.
193
+
194
+ **False positive found and fixed — the ``__BCO`` boilerplate (item 35
195
+ rework, 2026-07-04).** Once a real per-plot oracle existed (``plots.json``,
196
+ via ``tools/origin_trial/export_plot_refs.py``'s ``range -w`` LabTalk recipe
197
+ — the earlier ``layer.nplots``/``range __rp`` approach used by
198
+ ``export_ground_truth.py`` never worked, see below), it exposed 2 false
199
+ positives: ``UnpolPlots`` decoded ``(PrNiO3STOprof, C)`` and
200
+ ``(PrNiO3STOrefl, C)``, but the oracle plots neither book's column C at all
201
+ (the real bindings are ``B`` and ``G``/``H``/``I`` respectively). Root
202
+ cause: the whole-file regex also matches the *tail* of a completely
203
+ unrelated, fixed ~365-byte-long per-book boilerplate record that begins at a
204
+ length-prefixed ``__BCO2`` (occasionally ``__BCO3`` etc.) string — one per
205
+ book, byte-for-byte identical across every book in every file checked
206
+ (``XAS``, ``UnpolPlots``, ``"Fixed Lambdas SI"``) aside from a handful of
207
+ small varying counter/row-count fields. This record's last 8 bytes always
208
+ happen to fit the curve-token shape and always resolve to **local column 3
209
+ (index 2, i.e. always "C")** of its own book — not because it references
210
+ any column at all, but because that offset in the fixed template always
211
+ holds the literal value 3. It is *not* curve-exclusive: it exists whether or
212
+ not that book is plotted anywhere, and for a 3-column book whose real Y
213
+ column *happens* to be column C (every XAS book: ``Co``/``bl11YIGPy032``/
214
+ ``bl11YIGPy033``, all plotting ``Intensity`` at C) the artifact is
215
+ coincidentally "correct" — which is how it went undetected before the
216
+ plots.json oracle existed. ``UnpolPlots``' books use column C for a
217
+ *different* quantity than what's plotted (``Absorption``/``R``, not the
218
+ real ``Nuclear``/``R-Rsub`` curves), exposing the coincidence as a false
219
+ positive.
220
+
221
+ Fix: :func:`_is_bco_boilerplate` requires **both** confirmed structural
222
+ signals before excluding a match — (1) the match sits 340-380 bytes past a
223
+ preceding ``__BCO`` marker (the exact span measured across every confirmed
224
+ instance: 357-360 bytes) **and** (2) the resolved column is local index 2.
225
+ Neither signal alone is safe to use: distance alone is untested against
226
+ unseen templates; "local column 3" alone would wrongly exclude a real curve
227
+ that legitimately plots that position (``fig_pairs``' A-C diff curve also
228
+ resolves to local column 3, but at a completely different, ~1288-byte
229
+ distance from any ``__BCO`` marker, and is correctly kept). Applying this
230
+ filter removes exactly the ``UnpolPlots`` false positives and, incidentally,
231
+ the previously "correct-by-coincidence" ``XAS`` pair (``Co``/
232
+ ``bl11YIGPy032``/``bl11YIGPy033``, all local-column-3) — these were never
233
+ soundly decoded, only luckily right, and reporting them would contradict the
234
+ "replicate the method, not just the answer" porting principle. See
235
+ ``tests/test_io_origin_figures_opju.py``'s realdata precision/recall suite
236
+ and ``docs/origin_project_format.md`` §6.2.1 for the corrected validation
237
+ counts.
238
+
239
+ **The "third encoding" search — negative result (item 35, 2026-07-04).**
240
+ A further session went looking specifically for how ordinary, single-curve,
241
+ *default-dialog* graphs (``RockingCurve`` ``Graph1``/``Graph2``, all of
242
+ ``XAS``, all of ``UnpolPlots``, most of ``"Fixed Lambdas SI"``) encode their
243
+ Y column, since neither the real 8-byte token nor the column-candidate-list
244
+ shape exists anywhere in their files. Three leads were chased; none
245
+ validated, so **no code changed** — recall stays 30.6% / precision 100%.
246
+
247
+ 1. *Version-pair diff (refuted).* ``specimens/converted/*.opju`` are the
248
+ same corpus projects re-saved by the trial-writer's Origin build
249
+ (4.3811) from the corpus's native 4.3380 — a hoped-for Rosetta stone.
250
+ They are not one: re-scanning ``converted/XAS.opju`` and
251
+ ``converted/RockingCurve.opju`` with the existing regex finds matches
252
+ that *look* like new real tokens (e.g. an exact ``Co!C`` hit), but every
253
+ one is the **same ``__BCO`` boilerplate coincidence** described above,
254
+ just at a version-shifted distance — 383 bytes from the preceding
255
+ ``__BCO`` marker in the 4.3811 re-save vs. the pinned 357-360 in the
256
+ 4.3380 corpus (the boilerplate record itself grew a handful of bytes
257
+ between builds), which happens to fall just outside
258
+ ``_BCO_ARTIFACT_LO``/``_HI``'s ``[340, 380)`` window. Re-saving also
259
+ introduces a *second*, further false-positive-shaped cluster at a
260
+ ~1872-byte distance from a ``__BCO`` marker (seen resolving to XAS's
261
+ local column B in the converted file only) — the converted corpus is a
262
+ **noisier** source for this investigation, not a cleaner one. No
263
+ genuine new curve/DataPlot token appears inside any default-dialog
264
+ graph's own axis window in either version.
265
+ 2. *Window-local alternate encoding (found a candidate, confirmed it does
266
+ not validate).* Anchoring on a length-prefixed workbook short-name
267
+ string embedded directly in ``RockingCurve``'s curve-object body (e.g.
268
+ ``\x80\x03Nb\x00``, ``\x80\x06Nb/Au\x00`` — a real, confirmed landmark,
269
+ found 11 bytes before the real token in the *solved* ``NbAuRocking``
270
+ object) turned up a byte sequence at the identical relative offset in
271
+ the *unsolved* ``Graph1``/``Graph2`` objects that shares the real
272
+ token's first 5 bytes (``<flag> 01 01 01 80``) but substitutes ``0x01``
273
+ for the literal ``0x03`` sub-type byte, has **no fixed ``0x00``
274
+ terminator** (``Graph1``: ``0x83``; ``Graph2``: also non-zero), and
275
+ does not resync with the shared downstream template until ~30-40 bytes
276
+ later (unlike the real token's clean fixed 2-byte tail). The byte
277
+ immediately following the ``0x01`` sub-type was ``0x09`` for ``Nb``
278
+ (expected column B: local ordinal 2, global cumulative ordinal 8) and
279
+ ``0x0e`` for ``NbAl`` (expected column B: local ordinal 2, global
280
+ ordinal 12) — matching neither numbering scheme, and the two values'
281
+ own difference (``14 - 9 = 5``) doesn't match the true ordinals'
282
+ difference (``12 - 8 = 4``) either, ruling out a fixed per-value
283
+ correction. **Decisive cross-check:** ``specimens/converted/
284
+ RockingCurve.opju`` (the same project, re-saved by 4.3811) rewrites
285
+ this exact slot into the *canonical* ``0x03``/``0x00`` token shape
286
+ while preserving the *same* numeric value (``9`` for ``Nb``, ``14`` for
287
+ ``NbAl``) — proving the slot is a real Origin-native field, not a
288
+ misparse — yet decoding ``9`` through the already-validated
289
+ ``_global_column_map`` resolves to ``Nb!C`` (wrong; the oracle wants
290
+ ``B``) and ``14`` is out of range entirely (``RockingCurve`` has only
291
+ 12 FPC-decoded columns total). So this slot is governed by some *other*,
292
+ unidentified numbering rule that default-dialog plots use — not the
293
+ cumulative FPC-decoded ordinal explicit multi-curve/Select-Data tokens
294
+ use — and accepting it would have silently produced a **wrong** answer,
295
+ not merely a missing one. It also doesn't generalize as a locatable
296
+ *shape*: the raw 4-byte prefix (``01 01 80 01``) recurs ~90 times in
297
+ ``XAS.opju`` alone (a generic tag+small-int idiom used throughout this
298
+ format for style/color fields), so finding it at all required first
299
+ anchoring on the book-name string — and ``XAS``'s own default-dialog
300
+ curve objects don't embed a length-prefixed short name this way at all
301
+ (``\x80\x03Co\x00`` and friends: zero occurrences anywhere in the
302
+ file), so even the anchor technique is RockingCurve-specific, not
303
+ corpus-general.
304
+ 3. *Legend / ``__FRAMESRCDATAINFOS`` backrefs (dead end).* The
305
+ ``__FRAMESRCDATAINFOS`` marker exists exactly once each in
306
+ ``UnpolPlots`` and ``"Fixed Lambdas SI"`` (absent from ``XAS`` and
307
+ ``RockingCurve``) but its body decodes as a dense small-int/float64 run
308
+ consistent with multi-panel frame *layout geometry* (position/size),
309
+ not a per-curve dataset backref. The curve-object slots in that same
310
+ region carry Origin's generic auto-label macros (length-prefixed
311
+ ``%(?X)``/``%(?Y)`` strings — "substitute this axis's own column short
312
+ name at render time"), confirming these are ordinary default-titled
313
+ plots whose title template doesn't fix a literal dataset reference at
314
+ all, let alone one we could read back out.
315
+
316
+ Conclusion: the encoding default-dialog graphs use to select their Y column
317
+ remains unlocated. This is reported as a confirmed negative result (not
318
+ merely "not yet tried") so a future pass doesn't re-spend time on the same
319
+ three leads — see ``docs/origin_project_format.md`` §6.2.1 for the
320
+ corresponding writeup.
321
+
322
+ **The third encoding — FOUND (item 35, 2026-07-04 rework).** Lead #2 above
323
+ ("window-local alternate encoding") was the real thing after all: re-
324
+ anchored on the byte pattern itself rather than the book-name string that
325
+ made it look file-specific, ``<flag> 01 01 01 80 01 <val>`` (subtype
326
+ ``0x01`` vs. this module's ``0x03``) is found corpus-wide. The earlier
327
+ rejection was a counting-convention bug, not a wrong shape: ``val`` counts
328
+ cumulatively over **every allocated column of every workbook**, including
329
+ empty/undecoded ones — not ``_global_column_map``'s FPC-decoded-only
330
+ ordinal. Re-resolving through that all-columns map raises aggregate
331
+ oracle-covered recall from 30.6% (11/36) to 100% (36/36) at unchanged 100%
332
+ precision. Full trail, the map builder, and why its designation gate is
333
+ deliberately skipped live in ``opju_curves_allcols.py`` (split out to stay
334
+ under the 500-line ceiling); :func:`extract_curves` below merges both
335
+ families per figure, deduped on ``(book, y)``.
336
+ """
337
+
338
+ from __future__ import annotations
339
+
340
+ import re
341
+ from collections.abc import Mapping, Sequence
342
+
343
+ from quantized.io.origin_project.opj import _group
344
+ from quantized.io.origin_project.opju_codec import curve_plot_style, scan_columns
345
+ from quantized.io.origin_project.opju_curves_allcols import (
346
+ _allocated_column_map,
347
+ extract_curves_allcols,
348
+ )
349
+ from quantized.io.origin_project.windows import BookMeta
350
+ from quantized.io.origin_project.windows_opju import opju_window_metadata
351
+
352
+ __all__ = [
353
+ "allocated_columns_from_bytes",
354
+ "book_columns_from_bytes",
355
+ "book_metadata_from_bytes",
356
+ "extract_curves",
357
+ ]
358
+
359
+ # <flag:1> 01 <konst:1> 01 80 03 <y_ord:1> 00 -- see module docstring.
360
+ _CURVE_RE = re.compile(rb".\x01.\x01\x80\x03.\x00")
361
+
362
+ # The "__BCO" boilerplate false positive -- see the module docstring's
363
+ # "False positive found and fixed" section. The span from the marker to the
364
+ # start of the coincidentally-matching tail token measures 357-360 bytes
365
+ # across every confirmed instance; the range below gives margin either side.
366
+ _BCO_MARKER = b"__BCO"
367
+ _BCO_ARTIFACT_LO = 340
368
+ _BCO_ARTIFACT_HI = 380
369
+
370
+
371
+ def _is_bco_boilerplate(b: bytes, match_start: int, local_index: int) -> bool:
372
+ """True when a curve-token match is the tail of the fixed per-book
373
+ ``__BCO<n>`` worksheet-window record, not a real curve/DataPlot object.
374
+
375
+ Requires both confirmed signals at once (see module docstring): the
376
+ resolved column is local index 2 (always "C" in the boilerplate) AND a
377
+ ``__BCO`` marker sits 340-380 bytes before the match. Neither condition
378
+ alone is used -- a real curve can legitimately plot local column 3
379
+ (``fig_pairs``' A-C diff), and an unrelated ``__BCO`` marker could in
380
+ principle precede a real token at some other distance.
381
+ """
382
+ if local_index != 2:
383
+ return False
384
+ lo = max(0, match_start - _BCO_ARTIFACT_HI)
385
+ hi = max(0, match_start - _BCO_ARTIFACT_LO)
386
+ return b.find(_BCO_MARKER, lo, hi) >= 0
387
+
388
+
389
+ def book_columns_from_bytes(b: bytes) -> dict[str, list[str]]:
390
+ """``{book: [column letter, ...]}`` in book-appearance / sheet-column order,
391
+ restricted to columns the FPC codec (``opju_codec.scan_columns``) actually
392
+ decoded — the same universe ``y_ord`` below counts over."""
393
+ columns = scan_columns(b)
394
+ books = _group(columns)
395
+ return {book: [c for c, _ in cols] for book, cols in books.items()}
396
+
397
+
398
+ def book_metadata_from_bytes(
399
+ b: bytes, book_columns: Mapping[str, Sequence[str]]
400
+ ) -> dict[str, BookMeta]:
401
+ """Per-book column designation/label metadata (see the "Designation gate"
402
+ section of the module docstring) — a thin wrapper so ``figures_opju.py``
403
+ computes it once and threads it through, rather than every call re-scanning."""
404
+ return opju_window_metadata(b, book_columns)
405
+
406
+
407
+ def allocated_columns_from_bytes(b: bytes) -> dict[str, int]:
408
+ """``{book: total allocated column count}`` over EVERY allocated column
409
+ of EVERY workbook, including empty/undecoded books and columns — the
410
+ universe the 0x01-subtype token's ordinal counts over (see "The third
411
+ encoding — FOUND" section of the module docstring and
412
+ ``opju_curves_allcols.py``). A thin wrapper, mirroring
413
+ :func:`book_columns_from_bytes`, so ``figures_opju.py`` computes it once
414
+ per file and threads it through rather than every figure re-scanning."""
415
+ return dict(_allocated_column_map(b))
416
+
417
+
418
+ def _global_column_map(
419
+ book_columns: Mapping[str, Sequence[str]],
420
+ ) -> dict[int, tuple[str, str]]:
421
+ """1-based cumulative ordinal -> ``(book, column letter)``, book-appearance order."""
422
+ out: dict[int, tuple[str, str]] = {}
423
+ cum = 0
424
+ for book, cols in book_columns.items():
425
+ for col in cols:
426
+ cum += 1
427
+ out[cum] = (book, col)
428
+ return out
429
+
430
+
431
+ def _extract_curves_0x03(
432
+ b: bytes,
433
+ start: int,
434
+ end: int,
435
+ book_columns: Mapping[str, Sequence[str]],
436
+ books_meta: Mapping[str, BookMeta],
437
+ ) -> list[dict[str, str | float]]:
438
+ """The shipped 0x03-subtype curve token path — UNCHANGED (see module
439
+ docstring; do not modify this function, its map, or the ``__BCO`` gate).
440
+
441
+ ``y`` is decoded from the curve token and gated on the "Designation gate"
442
+ (must independently confirm as ``"Y"``); ``x`` is inferred as ``y``'s own
443
+ book's first column (not decoded from the byte record — see the module
444
+ docstring's "X is not decoded"). A token whose ``y_ord`` doesn't resolve
445
+ to a known, ``Y``-designated column is silently dropped — never guessed.
446
+ """
447
+ gmap = _global_column_map(book_columns)
448
+ out: list[dict[str, str | float]] = []
449
+ for m in _CURVE_RE.finditer(b, start, end):
450
+ y_ord = m.group()[6]
451
+ info = gmap.get(y_ord)
452
+ if info is None:
453
+ continue
454
+ book, y_col = info
455
+ cols = book_columns.get(book)
456
+ if not cols:
457
+ continue
458
+ if _is_bco_boilerplate(b, m.start(), cols.index(y_col)):
459
+ continue # the __BCO boilerplate, not a real curve -- see module docstring
460
+ bm = books_meta.get(book)
461
+ if bm is None:
462
+ continue # can't independently confirm the column type: drop, never guess
463
+ cm = bm.columns.get(y_col)
464
+ if cm is None or cm.designation != "Y":
465
+ continue
466
+ style = curve_plot_style(b, m.start())
467
+ out.append({"book": book, "x": cols[0], "y": y_col, **({"style": style} if style else {})})
468
+ return out
469
+
470
+
471
+ def extract_curves(
472
+ b: bytes,
473
+ start: int,
474
+ end: int,
475
+ book_columns: Mapping[str, Sequence[str]],
476
+ books_meta: Mapping[str, BookMeta],
477
+ book_counts_all: Mapping[str, int],
478
+ ) -> list[dict[str, str | float]]:
479
+ """Every curve's ``{book, x, y}`` binding in ``b[start:end)``, merging
480
+ BOTH curve-token families (see "The third encoding — FOUND" above): the
481
+ shipped 0x03-subtype token (:func:`_extract_curves_0x03`, unchanged,
482
+ ``"Y"``-designation gated) and the 0x01-subtype all-columns token
483
+ (``opju_curves_allcols.extract_curves_allcols``, deliberately not
484
+ designation-gated — see that module's docstring). Deduped on
485
+ ``(book, y)`` (composite/duplicate-referencing windows, or the 0x01
486
+ family's own doubled hits, commonly re-emit one binding twice), first-
487
+ seen order (0x03 before 0x01).
488
+ """
489
+ out = _extract_curves_0x03(b, start, end, book_columns, books_meta)
490
+ seen = {(c["book"], c["y"]) for c in out}
491
+ for c in extract_curves_allcols(b, start, end, book_counts_all):
492
+ key = (c["book"], c["y"])
493
+ if key in seen:
494
+ continue
495
+ seen.add(key)
496
+ out.append(c)
497
+ return out