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.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""The 0x01-subtype ``.opju`` curve token — the second, all-columns curve
|
|
2
|
+
encoding for ordinary, single-curve *default-dialog* graphs (item 35, the
|
|
3
|
+
"third encoding" search — this closes it, 2026-07-04).
|
|
4
|
+
|
|
5
|
+
The shipped ``opju_curves.py`` decodes one curve-token family (subtype
|
|
6
|
+
``0x03``), reverse-engineered from custom-styled/multi-curve/Select-Data
|
|
7
|
+
graphs. A prior session searched specifically for how *ordinary* single-
|
|
8
|
+
curve default-dialog graphs (``RockingCurve`` ``Graph1``/``Graph2``, all of
|
|
9
|
+
``XAS``, all of ``UnpolPlots``, most of ``"Fixed Lambdas SI"``) encode their
|
|
10
|
+
Y column and came up empty — three hypotheses chased, none validated (see
|
|
11
|
+
``opju_curves.py``'s docstring, "The third encoding search — negative
|
|
12
|
+
result"). This module is the found fourth hypothesis, now validated end to
|
|
13
|
+
end against the real ``plots.json`` oracle on every one of those files.
|
|
14
|
+
|
|
15
|
+
**The token.** A whole-file scan for ``[[byte]] 01 01 01 80 01 [[byte]]``
|
|
16
|
+
(the discovery regex ``rb"[\\x00-\\xff]\\x01\\x01\\x01\\x80([\\x01\\x03])(.)"``
|
|
17
|
+
with the subtype fixed to ``0x01``) finds exactly the shape the earlier
|
|
18
|
+
session already isolated in ``RockingCurve``'s ``Graph1``/``Graph2`` objects
|
|
19
|
+
and confirmed *was* a real Origin field (the 4.3811 re-save of the same
|
|
20
|
+
project rewrites the identical slot into the canonical ``0x03`` shape while
|
|
21
|
+
preserving the same numeric value) — but could not decode, because it was
|
|
22
|
+
being read through the ``0x03`` family's ordinal map:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
<flag:1> 01 01 01 80 01 <val:1>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Same family as the shipped token (``<flag> 01 01 01 80 03 <y_ord> 00``),
|
|
29
|
+
same fixed ``01 01`` "konst" pair, but subtype byte ``0x01`` instead of
|
|
30
|
+
``0x03``, and **no fixed ``0x00`` terminator** — confirmed by dumping every
|
|
31
|
+
hit's context across the full real corpus (below): the 7 bytes above are
|
|
32
|
+
followed by a short, subtype-specific tail (``83 01 <byte> 82 01 01 80 01``
|
|
33
|
+
for XAS; ``83 07 <2 bytes> 00 00 0b 00`` for RockingCurve; several other
|
|
34
|
+
shapes for UnpolPlots/"Fixed Lambdas SI") that never repeats the fixed
|
|
35
|
+
``0x00`` the ``0x03`` family always carries. No consistent terminator or
|
|
36
|
+
tail structure was found *across files* (each file's tail shape differs),
|
|
37
|
+
so none is folded into the matcher — the 7-byte prefix above is already
|
|
38
|
+
100% precise by itself (see "Validation" below), and adding an unproven
|
|
39
|
+
tail constraint would only risk *losing* matches, never gaining precision.
|
|
40
|
+
The ``<flag>`` byte was checked across every hit and, like the ``0x03``
|
|
41
|
+
family's own flag byte, shows no correlation with column choice — it
|
|
42
|
+
clusters loosely in ``0xae``-``0xc4`` (a per-curve creation-order/style
|
|
43
|
+
counter, consistent with the shipped family's documented flag semantics)
|
|
44
|
+
but is never used as a match constraint.
|
|
45
|
+
|
|
46
|
+
**The counting convention — the actual bug in every earlier refutation.**
|
|
47
|
+
`opju_curves.py`'s ``_global_column_map`` (the shipped ordinal map) counts
|
|
48
|
+
*only* columns ``opju_codec.scan_columns`` actually FPC-decoded, skipping
|
|
49
|
+
any book with zero decodable columns entirely (e.g. an unused default
|
|
50
|
+
"Book1"). Decoding the 0x01 token's ``<val>`` through that map is exactly
|
|
51
|
+
what the earlier session tried and rejected (``RockingCurve``'s ``Nb!B``
|
|
52
|
+
decoded to ``Nb!C`` — wrong). The real convention is different: ``<val>``
|
|
53
|
+
is a 1-based ordinal counted cumulatively across **every allocated column
|
|
54
|
+
of every workbook, including empty/undecoded books and empty/undecoded
|
|
55
|
+
columns**, in file book-appearance order. Re-decoding through this all-
|
|
56
|
+
columns map (below) resolves every hit exactly:
|
|
57
|
+
|
|
58
|
+
* ``XAS`` (``Book1``=2 cols, an EMPTY/undecoded default book, then ``Co``=3,
|
|
59
|
+
``bl11YIGPy032``=3, ``bl11YIGPy033``=3): ``<val>`` = 5, 8, 11 resolve to
|
|
60
|
+
exactly ``Co!C``, ``bl11YIGPy032!C``, ``bl11YIGPy033!C`` — the file's
|
|
61
|
+
full ``plots.json`` oracle, 3/3.
|
|
62
|
+
* ``RockingCurve`` (``NbAu``=7 cols incl. an undecoded ``G``, ``Nb``=5,
|
|
63
|
+
``NbAl``=3): ``<val>`` = 9, 14 (each found twice — the composite
|
|
64
|
+
``Graph3`` re-references) resolve to ``Nb!B``, ``NbAl!B`` — exactly the
|
|
65
|
+
2 oracle pairs the shipped ``0x03`` path could not reach (it already
|
|
66
|
+
recovers ``NbAu!D``/``NbAu!F`` from the multi-curve ``NbAuRocking``
|
|
67
|
+
layer), bringing this file to 4/4.
|
|
68
|
+
* ``UnpolPlots`` (``Book1``=2, ``J315NdNiO3STO``=3, ``J315NdNiO3ST1``=9,
|
|
69
|
+
``PrNiO3STOprof``=3, ``PrNiO3STOrefl``=9): 16 hits collapse to the 8
|
|
70
|
+
unique oracle pairs (each doubled by a composite-window re-reference),
|
|
71
|
+
8/8.
|
|
72
|
+
* ``"Fixed Lambdas SI"`` (``Book1``=2, then 10 PNR books x 11 cols each):
|
|
73
|
+
28 hits collapse to exactly the file's 14 unique oracle pairs (each
|
|
74
|
+
doubled), 14/14.
|
|
75
|
+
|
|
76
|
+
No book in this corpus needed anything beyond the FPC-decoded-only map's
|
|
77
|
+
*complement* — the all-columns map — to resolve every hit; every value
|
|
78
|
+
that decodes lands on a real, contiguous, allocated column.
|
|
79
|
+
|
|
80
|
+
**Building the all-columns map without ground truth.** ``opju_codec._NAME``
|
|
81
|
+
(``rb"[A-Za-z][\\w ]{0,40}_[A-Za-z0-9]{1,4}(?:@\\d{1,2})?"``) matches a
|
|
82
|
+
length-prefixed dataset name for *every* allocated column — including
|
|
83
|
+
empty ones (``XAS``'s ``Book1_A``/``Book1_B``, never decoded by
|
|
84
|
+
``scan_columns`` because ``Book1`` carries no data) — but also binary
|
|
85
|
+
noise (arbitrary byte runs that happen to look like ``t_R``, ``G_L``,
|
|
86
|
+
``U_5``). :func:`_allocated_column_map` filters this down to a clean,
|
|
87
|
+
GT-matching book/column inventory with three checks, all confirmed
|
|
88
|
+
necessary and sufficient across the four real-corpus files (see
|
|
89
|
+
``test_realdata_allocated_column_map_matches_index`` for the exact
|
|
90
|
+
book/count comparison against each file's independently-exported
|
|
91
|
+
``index.json``):
|
|
92
|
+
|
|
93
|
+
1. Reuse ``scan_columns``'s own length-prefix anchor (``b[m.start()-1] ==
|
|
94
|
+
len(match)``) — the same check that makes ``_NAME`` usable at all.
|
|
95
|
+
2. Keep only matches whose column suffix is **pure letters, 1-2 chars**
|
|
96
|
+
(``[A-Z]{1,2}``) and drop any ``@N`` sheet-suffixed match entirely (this
|
|
97
|
+
map does not track the extra-sheet pseudo-books the FPC-decoded map
|
|
98
|
+
does — no multi-sheet book in this corpus needed one).
|
|
99
|
+
3. Group by book (everything before the last ``_``) and require the
|
|
100
|
+
resulting column-letter *set* to be an exact contiguous run starting at
|
|
101
|
+
``A`` (``{A}``, ``{A,B}``, ``{A,B,C}``, …) — this is what rejects the
|
|
102
|
+
noise matches, which never land on a clean contiguous run.
|
|
103
|
+
|
|
104
|
+
Book order is first-appearance order of a *surviving* book's name records
|
|
105
|
+
in the byte stream (matches every stem's ``index.json`` book order
|
|
106
|
+
exactly). Column letters use the standard spreadsheet base-26 scheme
|
|
107
|
+
(``A``=1..``Z``=26, ``AA``=27, … — length-then-lexicographic).
|
|
108
|
+
|
|
109
|
+
**No designation gate — a deliberate, checked difference from the ``0x03``
|
|
110
|
+
path.** The shipped path drops any resolved column unless
|
|
111
|
+
``windows_opju``'s independently-validated designation is exactly ``"Y"``
|
|
112
|
+
(never ``"X"``/``"Y-error"``), because that path's whole-file regex alone
|
|
113
|
+
is not curve-exclusive enough (the ``__BCO`` boilerplate false positive).
|
|
114
|
+
Applying the same gate here was checked against every one of the 27 unique
|
|
115
|
+
oracle-confirmed bindings this token resolves and would **wrongly reject
|
|
116
|
+
four of them**: ``UnpolPlots``' ``J315NdNiO3ST1!H``/``PrNiO3STOrefl!H``
|
|
117
|
+
("dR Fresnel") and ``"Fixed Lambdas SI"``'s ``PNRNbAl80nm!J``/
|
|
118
|
+
``PNRNbAu100nm!J`` ("dSA") are genuinely plotted curves per ``plots.json``
|
|
119
|
+
whose column is independently designated ``"Y-error"``, not ``"Y"`` — the
|
|
120
|
+
project plots an uncertainty column as its own curve, which is a legitimate
|
|
121
|
+
Origin usage the designation gate cannot distinguish from the ``__BCO``
|
|
122
|
+
artifact. Since this token's raw 7-byte match is *already* 100% precise
|
|
123
|
+
file-wide with zero designation cross-check (confirmed by scanning every
|
|
124
|
+
``.opju`` in the corpus, both real-corpus files and every specimen —
|
|
125
|
+
zero hits anywhere except the four files that need them, see
|
|
126
|
+
``tools/origin_trial/score_curve_bindings.py``), adding the ``0x03`` path's
|
|
127
|
+
designation gate here would only lose true positives for no precision
|
|
128
|
+
gain. The only safety check applied is structural: an ordinal that exceeds
|
|
129
|
+
the map's total column count for its file, or that the cumulative map
|
|
130
|
+
simply has no entry for, is dropped — never guessed.
|
|
131
|
+
|
|
132
|
+
**Attribution.** Reuses the same ``[anchor, next_anchor)`` per-figure
|
|
133
|
+
window scoping the ``0x03`` path uses (see ``figures_opju.py`` /
|
|
134
|
+
``opju_curves.extract_curves``). Every hit in this corpus falls inside
|
|
135
|
+
some figure's window (none needed to be dropped for falling outside every
|
|
136
|
+
window), though — like the shipped path's own documented attribution gap
|
|
137
|
+
— a composite/last window can absorb tokens that structurally belong to
|
|
138
|
+
an earlier, already-closed window (``"Fixed Lambdas SI"``'s last anchor's
|
|
139
|
+
span runs to EOF and physically contains all 28 hits for both of its
|
|
140
|
+
book families). This is a known, pre-existing class of imprecision
|
|
141
|
+
(*which* figure a curve is attributed to), not a soundness one (the
|
|
142
|
+
``(book, column)`` pair itself is never wrong) — see
|
|
143
|
+
``opju_curves.py``'s "Known gap — per-figure attribution".
|
|
144
|
+
|
|
145
|
+
**Validation.** ``tools/origin_trial/score_curve_bindings.py`` re-run
|
|
146
|
+
after wiring this module in: precision 100% (0 wrong) on every stem,
|
|
147
|
+
aggregate recall 36/36 (100%), up from 11/36 (30.6%) before this change —
|
|
148
|
+
see ``docs/origin_project_format.md`` §6.2.1 for the full updated table.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
from __future__ import annotations
|
|
152
|
+
|
|
153
|
+
import re
|
|
154
|
+
from collections import OrderedDict
|
|
155
|
+
from collections.abc import Mapping
|
|
156
|
+
|
|
157
|
+
from quantized.io.origin_project.opju_codec import _NAME, curve_plot_style
|
|
158
|
+
|
|
159
|
+
__all__ = ["extract_curves_allcols"]
|
|
160
|
+
|
|
161
|
+
# <flag:1> 01 01 01 80 01 <val:1> -- see module docstring. Unlike the shipped
|
|
162
|
+
# 0x03 family, no fixed terminator byte was found, so none is required here.
|
|
163
|
+
_CURVE_RE = re.compile(rb"[\x00-\xff]\x01\x01\x01\x80\x01(.)")
|
|
164
|
+
|
|
165
|
+
# A column suffix eligible for the all-columns map: pure letters, any width,
|
|
166
|
+
# no sheet suffix (that case is dropped by the caller before this check runs).
|
|
167
|
+
# No length cap: the old {1,2} silently discarded AAA+ columns of a >702-col
|
|
168
|
+
# book while the survivors still formed a contiguous A..ZZ run, shifting every
|
|
169
|
+
# later book's cumulative ordinal base (2026-07-06 genericity audit) —
|
|
170
|
+
# ``_letter_index`` already handles arbitrary-width bijective base-26.
|
|
171
|
+
_PURE_COLUMN = re.compile(r"[A-Z]+")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _letter_index(letters: str) -> int:
|
|
175
|
+
"""Standard spreadsheet column lettering: A=1, .., Z=26, AA=27, .."""
|
|
176
|
+
idx = 0
|
|
177
|
+
for ch in letters:
|
|
178
|
+
idx = idx * 26 + (ord(ch) - ord("A") + 1)
|
|
179
|
+
return idx
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _index_to_letters(n: int) -> str:
|
|
183
|
+
"""Inverse of :func:`_letter_index`."""
|
|
184
|
+
out = ""
|
|
185
|
+
while n > 0:
|
|
186
|
+
n, rem = divmod(n - 1, 26)
|
|
187
|
+
out = chr(ord("A") + rem) + out
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _allocated_column_map(b: bytes) -> OrderedDict[str, int]:
|
|
192
|
+
"""``{book: total allocated column count}``, book-appearance order.
|
|
193
|
+
|
|
194
|
+
Counts EVERY allocated column of every workbook (including empty/
|
|
195
|
+
undecoded books and columns) -- the universe the 0x01 token's ``val``
|
|
196
|
+
counts over, unlike ``opju_curves._global_column_map``'s FPC-decoded-
|
|
197
|
+
only universe. See the module docstring's "Building the all-columns
|
|
198
|
+
map" section for the filter rule and its validation.
|
|
199
|
+
"""
|
|
200
|
+
names = [
|
|
201
|
+
m.group(0).decode("latin1")
|
|
202
|
+
for m in _NAME.finditer(b)
|
|
203
|
+
if m.start() > 0 and b[m.start() - 1] == len(m.group(0))
|
|
204
|
+
]
|
|
205
|
+
seen: OrderedDict[str, set[str]] = OrderedDict()
|
|
206
|
+
for name in names:
|
|
207
|
+
base, _, sheet = name.partition("@")
|
|
208
|
+
if sheet:
|
|
209
|
+
continue # drop sheet-suffixed matches -- see module docstring
|
|
210
|
+
book, _, col = base.rpartition("_")
|
|
211
|
+
if not book or not _PURE_COLUMN.fullmatch(col):
|
|
212
|
+
continue
|
|
213
|
+
seen.setdefault(book, set()).add(col)
|
|
214
|
+
out: OrderedDict[str, int] = OrderedDict()
|
|
215
|
+
for book, cols in seen.items():
|
|
216
|
+
n = len(cols)
|
|
217
|
+
if cols == {_index_to_letters(i) for i in range(1, n + 1)}:
|
|
218
|
+
out[book] = n
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _cumulative_ordinals(book_counts: Mapping[str, int]) -> dict[int, tuple[str, str]]:
|
|
223
|
+
"""1-based cumulative ordinal -> ``(book, column letter)`` over EVERY
|
|
224
|
+
allocated column of EVERY book (contrast ``opju_curves._global_column_map``,
|
|
225
|
+
which only counts FPC-decoded columns) -- see module docstring."""
|
|
226
|
+
out: dict[int, tuple[str, str]] = {}
|
|
227
|
+
cum = 0
|
|
228
|
+
for book, n in book_counts.items():
|
|
229
|
+
for i in range(1, n + 1):
|
|
230
|
+
cum += 1
|
|
231
|
+
out[cum] = (book, _index_to_letters(i))
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def extract_curves_allcols(
|
|
236
|
+
b: bytes, start: int, end: int, book_counts: Mapping[str, int]
|
|
237
|
+
) -> list[dict[str, str | float]]:
|
|
238
|
+
"""Every curve's ``{book, x, y}`` binding found via the 0x01-subtype
|
|
239
|
+
token in ``b[start:end)``.
|
|
240
|
+
|
|
241
|
+
No designation gate is applied (see module docstring — it would reject
|
|
242
|
+
genuine Y-error-plotted-as-curve bindings this token legitimately
|
|
243
|
+
finds); the only safety check is structural: a ``val`` with no entry in
|
|
244
|
+
the cumulative map (out of range, or the book/column doesn't exist) is
|
|
245
|
+
dropped, never guessed. ``x`` is the resolved book's first column
|
|
246
|
+
("A") -- always present by construction, since :func:`_allocated_column_map`
|
|
247
|
+
only keeps books whose columns form a contiguous run starting at A.
|
|
248
|
+
"""
|
|
249
|
+
ordmap = _cumulative_ordinals(book_counts)
|
|
250
|
+
out: list[dict[str, str | float]] = []
|
|
251
|
+
for m in _CURVE_RE.finditer(b, start, end):
|
|
252
|
+
info = ordmap.get(m.group(1)[0])
|
|
253
|
+
if info is None:
|
|
254
|
+
continue
|
|
255
|
+
book, y_col = info
|
|
256
|
+
style = curve_plot_style(b, m.start())
|
|
257
|
+
out.append({"book": book, "x": "A", "y": y_col, **({"style": style} if style else {})})
|
|
258
|
+
return out
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""``.opju`` (CPYUA) figure->curve binding via the global column-id table.
|
|
2
|
+
|
|
3
|
+
**The discovery (2026-07-05, the Hc2 per-graph rework).** The two curve-token
|
|
4
|
+
"families" ``opju_curves.py`` / ``opju_curves_allcols.py`` decode by *counting
|
|
5
|
+
columns* (``<flag> 01 01 01 80 03 <y_ord> 00`` and ``… 80 01 <val>``) are in
|
|
6
|
+
fact ONE encoding, and the value is not an ordinal to count at all: it is the
|
|
7
|
+
plotted column's own **global, project-wide, creation-order serial id** — the
|
|
8
|
+
exact CPYUA analogue of the ``.opj`` curve-anchor id ``opj_curves.py`` decodes.
|
|
9
|
+
The token's id field is a tagged variable-width little-endian integer:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
<flag:1> 01 01 01 80 <width:01|03> <payload>
|
|
13
|
+
width 0x01 -> payload = <id:u8>
|
|
14
|
+
width 0x03 -> payload = <id:u16 LE> <flag:1> (3rd byte varies: 01/09/0b/
|
|
15
|
+
0c/11/21 observed; not id)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The old ``0x03``-family regex required the byte after the id byte to be
|
|
19
|
+
``0x00`` — that byte is really the u16 id's HIGH byte, so the old reader
|
|
20
|
+
worked only on projects with < 256 columns and silently aliased ids mod 256
|
|
21
|
+
on larger ones (measured on ``Hc2 data.opju``: 1251+ allocated columns, 12 of
|
|
22
|
+
14 decoded bindings wrong before this rework). The counting conventions the
|
|
23
|
+
old modules validated were an artifact: in a never-edited project, creation
|
|
24
|
+
order equals current layout order, so the cumulative all-columns ordinal
|
|
25
|
+
happens to equal the stored id. ``Hc2 data`` (heavily edited: columns added
|
|
26
|
+
to early books after later books existed, e.g. the ``Derivative Y1`` column
|
|
27
|
+
``AH`` of the first Lockin book carrying id 132) breaks the count and proves
|
|
28
|
+
the id semantics.
|
|
29
|
+
|
|
30
|
+
**The id table.** Every worksheet column's own windows-section record stores
|
|
31
|
+
that id. Two record forms exist (both validated against the per-column
|
|
32
|
+
designation-marker runs ``windows_opju.py`` independently decodes):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
form A: 80 <serial> 01 10 80 03 <id:u16 LE> <pb> <fields…>
|
|
36
|
+
form B: 80 <serial> 07 10 01 00 00 <id:u16 LE> <pb> <fields…>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
``<pb>`` is one uninterpreted byte (``03/09/0b/0c`` observed). ``<fields…>``
|
|
40
|
+
is a run of tagged fields ``<tag:0x80-0x9f> <len:u8> <payload:len>``:
|
|
41
|
+
|
|
42
|
+
* the column's **short name**: payload = optional designation prefix byte
|
|
43
|
+
(``0x03`` on X columns, ``0x02`` on Y-error columns) + the ASCII name;
|
|
44
|
+
identified as the first alnum-payload field whose *next* field's payload
|
|
45
|
+
opens with ``0x09``;
|
|
46
|
+
* a fixed ``<tag> 01 09`` separator field;
|
|
47
|
+
* the field carrying the 2-byte **plot-designation marker** as its payload
|
|
48
|
+
tail — ``21 51`` X / ``21 61`` Y / ``30 61`` Y-error, the same markers
|
|
49
|
+
``windows_opju.py`` anchors label runs on; a Y column's marker field is
|
|
50
|
+
exactly 4 bytes — ``<x_partner_id:u16 LE> 21 61`` — giving the column's own
|
|
51
|
+
designated **X partner column id** (validated: Hc2's ``Derivative Y1``
|
|
52
|
+
column AH, id 132, carries partner id 131 = its sibling ``Derivative X1``
|
|
53
|
+
column AG, not the sheet's column A).
|
|
54
|
+
|
|
55
|
+
Records are attributed to their book by the containing **page span**: the
|
|
56
|
+
``0a``-framed page headers ``tree_opju._OPJU_WIN_RE`` already enumerates
|
|
57
|
+
(byte-exact vs live COM on 5 corpus files) mark every window's start; a
|
|
58
|
+
column record belongs to the page (workbook) whose span it falls in.
|
|
59
|
+
|
|
60
|
+
**Form B semantics are unknown** beyond carrying the same id+fields: it is
|
|
61
|
+
rare (RockingCurve ``NbAu!D``/``NbAl!B``, UnpolPlots ``PrNiO3STOprof!B``/
|
|
62
|
+
``PrNiO3STOrefl!I`` — exactly the four bindings the form-A-only table
|
|
63
|
+
missed) and never overlaps form A ids (checked corpus-wide: 0 overlaps in
|
|
64
|
+
28 files). Both forms parse identically after the id.
|
|
65
|
+
|
|
66
|
+
**Validation (2026-07-05).**
|
|
67
|
+
|
|
68
|
+
* File-level ``plots.json`` oracle (7 stems, 36 unique pairs): 36/36 decoded,
|
|
69
|
+
0 wrong — same aggregate as the shipped counting decoders, now via ids.
|
|
70
|
+
* Per-graph ``index.json`` oracle (``Hc2 data``, the first stem whose export
|
|
71
|
+
populated ``graphs[].layers[].plots``): decoded figures are matched to
|
|
72
|
+
oracle graphs by their *page name* (see ``figures_opju.py``); 7 of the 8
|
|
73
|
+
oracle graphs that exist as real graph pages bind exactly (Graph1/2/4/6/8/
|
|
74
|
+
10/11 — every ``(book, column)`` set identical to the oracle, including
|
|
75
|
+
u16 ids > 255 and the id-132 later-added column). 0 wrong bindings.
|
|
76
|
+
* ``Graph5`` (single-curve, [A6221LockinD3]!I) is a **documented negative**:
|
|
77
|
+
its page span contains no curve token at all — its two DataPlot-magic
|
|
78
|
+
objects carry no recognisable id field (a diffed/duplicate-window form,
|
|
79
|
+
byte-dumped in the RE log). It decodes with ``curves == []`` (missing,
|
|
80
|
+
never guessed). Do NOT be tempted by the ``90 00 80 <tag> 01 89`` bytes in
|
|
81
|
+
its ``_202``/``_232`` sub-objects: that shape occurs in EVERY graph page of
|
|
82
|
+
the corpus with the constant 0x89 (and 0x02) regardless of what is plotted
|
|
83
|
+
— a style-boilerplate coincidence that happens to equal Graph5's true
|
|
84
|
+
column id 137, chased and refuted during this rework.
|
|
85
|
+
* Embedded fit-report graphs (the oracle's ``FitLine*``/``Residual*`` pages,
|
|
86
|
+
which are NOT ``0a``-framed pages): their report-sheet spans (Book2/Book3)
|
|
87
|
+
carry one canonical token per embedded layer anchor, resolving to the
|
|
88
|
+
fitted source column (Book2!C/D/E) — in-oracle, but per-graph attribution
|
|
89
|
+
for them is unverifiable (no page name), so they ship on unnamed figures
|
|
90
|
+
only. The fit-CURVE overlays (``FitNLCurveN!B`` etc.) are not
|
|
91
|
+
token-encoded anywhere and are honestly missing.
|
|
92
|
+
|
|
93
|
+
The old counting decoders stay in place as the fallback for byte streams
|
|
94
|
+
with no id table at all (synthetic fixtures; degraded files): see
|
|
95
|
+
``figures_opju.extract_figures_opju``.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
from __future__ import annotations
|
|
99
|
+
|
|
100
|
+
import re
|
|
101
|
+
from typing import NamedTuple
|
|
102
|
+
|
|
103
|
+
from quantized.io.origin_project.curve_style_color import (
|
|
104
|
+
apply_increment_colors,
|
|
105
|
+
opju_style_record,
|
|
106
|
+
style_fields,
|
|
107
|
+
)
|
|
108
|
+
from quantized.io.origin_project.opju_codec import curve_plot_style
|
|
109
|
+
from quantized.io.origin_project.tree_opju import iter_opju_windows
|
|
110
|
+
|
|
111
|
+
__all__ = ["ColumnIdTable", "column_id_table", "extract_curves_by_id", "opju_pages"]
|
|
112
|
+
|
|
113
|
+
# Column-record id field, form A / form B (see module docstring).
|
|
114
|
+
_ID_FORM_A = re.compile(rb"\x01\x10\x80\x03(..)", re.DOTALL)
|
|
115
|
+
_ID_FORM_B = re.compile(rb"\x07\x10\x01\x00\x00(..)", re.DOTALL)
|
|
116
|
+
|
|
117
|
+
# The unified curve token: <flag> 01 01 01 80 <width> <id…> (module docstring).
|
|
118
|
+
_CURVE_TOKEN = re.compile(rb"\x01\x01\x01\x80([\x01\x03])", re.DOTALL)
|
|
119
|
+
|
|
120
|
+
# Per-column plot-designation markers (same bytes windows_opju.py validates).
|
|
121
|
+
_MARKS = {b"\x21\x51": "X", b"\x21\x61": "Y", b"\x30\x61": "Y-error"}
|
|
122
|
+
|
|
123
|
+
_MAX_FIELDS = 10 # field-walk runaway backstop; real records resolve in <= 6
|
|
124
|
+
# Column short-name gate: no 16-char ceiling (the old {0,15} was a corpus
|
|
125
|
+
# maximum — a longer user-renamed short name made the column's id unresolvable
|
|
126
|
+
# and silently dropped every curve plotting it; 2026-07-06 genericity audit).
|
|
127
|
+
# The structural guards (the 09-separator field walk + designation marker)
|
|
128
|
+
# carry the precision; digit-led short names are legal in Origin.
|
|
129
|
+
_NAME_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_]*\Z")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ColumnIdTable(NamedTuple):
|
|
133
|
+
"""The decoded global column-id table of one ``.opju`` byte stream.
|
|
134
|
+
|
|
135
|
+
``ids``: column id -> ``(book page name, column short name)``.
|
|
136
|
+
``x_partner``: a Y column's id -> its designated X partner column's id.
|
|
137
|
+
``book_x``: book -> its first X-designated column's short name.
|
|
138
|
+
``book_pages``: page names that own >= 1 column record (i.e. workbook /
|
|
139
|
+
report-table pages — used by ``figures_opju`` to tell a graph page from
|
|
140
|
+
a book page when attaching window names to figures).
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
ids: dict[int, tuple[str, str]]
|
|
144
|
+
x_partner: dict[int, int]
|
|
145
|
+
book_x: dict[str, str]
|
|
146
|
+
book_pages: frozenset[str]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def opju_pages(b: bytes) -> list[tuple[int, str]]:
|
|
150
|
+
"""Every ``0a``-framed page header as ``(offset, name)``, file order.
|
|
151
|
+
|
|
152
|
+
Reuses the exact enumeration ``tree_opju`` validated byte-exact against
|
|
153
|
+
live COM (first occurrence per name; the ``namelen+2`` length prefix
|
|
154
|
+
drives the name span, so arbitrary-length window names enumerate — a
|
|
155
|
+
missed header here would attribute every column in its span to the
|
|
156
|
+
previous page).
|
|
157
|
+
"""
|
|
158
|
+
return iter_opju_windows(b)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _walk_fields(b: bytes, p: int) -> list[bytes]:
|
|
162
|
+
"""The tagged-field run at ``p``: payload per ``<tag 0x80-0x9f> <len>
|
|
163
|
+
<payload>`` field, stopping at the first byte that isn't a field tag."""
|
|
164
|
+
out: list[bytes] = []
|
|
165
|
+
n = len(b)
|
|
166
|
+
for _ in range(_MAX_FIELDS):
|
|
167
|
+
if p + 2 > n or not 0x80 <= b[p] <= 0x9F:
|
|
168
|
+
break
|
|
169
|
+
ln = b[p + 1]
|
|
170
|
+
payload = b[p + 2 : p + 2 + ln]
|
|
171
|
+
if len(payload) < ln:
|
|
172
|
+
break
|
|
173
|
+
out.append(payload)
|
|
174
|
+
p += 2 + ln
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_column_record(b: bytes, after_id: int) -> tuple[str, str | None, int | None] | None:
|
|
179
|
+
"""Decode one column record's field run (starting just past the id).
|
|
180
|
+
|
|
181
|
+
Returns ``(short_name, designation, x_partner_id)`` or ``None`` when the
|
|
182
|
+
record doesn't resolve — dropped, never guessed. The name field must be
|
|
183
|
+
followed by the fixed ``09`` separator field AND a designation-marker
|
|
184
|
+
field within the next 3 fields (both checks together are what reject
|
|
185
|
+
coincidental byte runs; see module docstring).
|
|
186
|
+
"""
|
|
187
|
+
fields = _walk_fields(b, after_id + 1) # +1: skip the uninterpreted <pb> byte
|
|
188
|
+
for i in range(len(fields) - 1):
|
|
189
|
+
payload = fields[i]
|
|
190
|
+
body = payload[1:] if payload[:1] in (b"\x02", b"\x03") else payload
|
|
191
|
+
try:
|
|
192
|
+
name = body.decode("ascii")
|
|
193
|
+
except UnicodeDecodeError:
|
|
194
|
+
continue
|
|
195
|
+
if not _NAME_RE.match(name) or fields[i + 1][:1] != b"\x09":
|
|
196
|
+
continue
|
|
197
|
+
for later in fields[i + 1 : i + 4]:
|
|
198
|
+
if len(later) >= 2 and later[-2:] in _MARKS:
|
|
199
|
+
desig = _MARKS[later[-2:]]
|
|
200
|
+
partner: int | None = None
|
|
201
|
+
if desig == "Y" and len(later) == 4:
|
|
202
|
+
partner = int.from_bytes(later[:2], "little")
|
|
203
|
+
return name, desig, partner
|
|
204
|
+
return None # name shape found but no designation marker: not a column record
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def column_id_table(b: bytes, pages: list[tuple[int, str]]) -> ColumnIdTable:
|
|
209
|
+
"""Scan every page span for column records and build the global id table.
|
|
210
|
+
|
|
211
|
+
A record whose fields don't resolve is skipped; an id claimed twice with
|
|
212
|
+
*different* ``(book, column)`` is poisoned (removed entirely) — fail
|
|
213
|
+
closed, never guess. Records outside any page span cannot be attributed
|
|
214
|
+
to a book and are ignored.
|
|
215
|
+
"""
|
|
216
|
+
ids: dict[int, tuple[str, str]] = {}
|
|
217
|
+
poisoned: set[int] = set()
|
|
218
|
+
x_partner: dict[int, int] = {}
|
|
219
|
+
book_x: dict[str, str] = {}
|
|
220
|
+
book_pages: set[str] = set()
|
|
221
|
+
bounds = [*pages, (len(b), "")]
|
|
222
|
+
for (start, book), (end, _next) in zip(bounds, bounds[1:], strict=False):
|
|
223
|
+
for pattern in (_ID_FORM_A, _ID_FORM_B):
|
|
224
|
+
for m in pattern.finditer(b, start, end):
|
|
225
|
+
cid = int.from_bytes(m.group(1), "little")
|
|
226
|
+
parsed = _parse_column_record(b, m.end())
|
|
227
|
+
if parsed is None:
|
|
228
|
+
continue
|
|
229
|
+
name, desig, partner = parsed
|
|
230
|
+
if cid in ids and ids[cid] != (book, name):
|
|
231
|
+
poisoned.add(cid)
|
|
232
|
+
continue
|
|
233
|
+
ids[cid] = (book, name)
|
|
234
|
+
book_pages.add(book)
|
|
235
|
+
if partner is not None:
|
|
236
|
+
x_partner[cid] = partner
|
|
237
|
+
if desig == "X" and book not in book_x:
|
|
238
|
+
book_x[book] = name
|
|
239
|
+
for cid in poisoned:
|
|
240
|
+
ids.pop(cid, None)
|
|
241
|
+
x_partner.pop(cid, None)
|
|
242
|
+
return ColumnIdTable(ids, x_partner, book_x, frozenset(book_pages))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def extract_curves_by_id(
|
|
246
|
+
b: bytes, start: int, end: int, table: ColumnIdTable
|
|
247
|
+
) -> list[dict[str, str | float]]:
|
|
248
|
+
"""Every curve binding in ``b[start:end)`` via the unified id token.
|
|
249
|
+
|
|
250
|
+
``y`` resolves through ``table.ids`` (a token whose id is unknown is
|
|
251
|
+
dropped, never guessed). ``x`` is the Y column's own stored X-partner
|
|
252
|
+
when its record carried one, else the book's first X-designated column,
|
|
253
|
+
else ``"A"`` (the documented structural fallback). Deduped on
|
|
254
|
+
``(book, y)`` in first-seen (token) order; ``style`` is attached when
|
|
255
|
+
``opju_codec.curve_plot_style`` finds the ``8f 01 <style> 83`` tag
|
|
256
|
+
(falling back to the reconstructed record's own style byte), and
|
|
257
|
+
``color``/``symbol`` when the token's sparse style record decodes
|
|
258
|
+
(``curve_style_color.py`` -- oracle-verified on the ``Hc2 data``/
|
|
259
|
+
``RockingCurve``/``UnpolPlots`` ``curve_style.json`` oracle; auto/
|
|
260
|
+
undecodable fields stay absent, never defaulted).
|
|
261
|
+
"""
|
|
262
|
+
out: list[dict[str, str | float]] = []
|
|
263
|
+
records: list[bytes | None] = []
|
|
264
|
+
seen: set[tuple[str, str]] = set()
|
|
265
|
+
for m in _CURVE_TOKEN.finditer(b, start, end):
|
|
266
|
+
width = m.group(1)[0]
|
|
267
|
+
if m.end() + width > len(b):
|
|
268
|
+
continue
|
|
269
|
+
if width == 1:
|
|
270
|
+
cid = b[m.end()]
|
|
271
|
+
else:
|
|
272
|
+
cid = int.from_bytes(b[m.end() : m.end() + 2], "little")
|
|
273
|
+
info = table.ids.get(cid)
|
|
274
|
+
if info is None:
|
|
275
|
+
continue
|
|
276
|
+
book, y_col = info
|
|
277
|
+
key = (book, y_col)
|
|
278
|
+
if key in seen:
|
|
279
|
+
continue
|
|
280
|
+
seen.add(key)
|
|
281
|
+
partner = table.x_partner.get(cid)
|
|
282
|
+
partner_info = table.ids.get(partner) if partner is not None else None
|
|
283
|
+
if partner_info is not None and partner_info[0] == book:
|
|
284
|
+
x_col = partner_info[1]
|
|
285
|
+
else:
|
|
286
|
+
x_col = table.book_x.get(book, "A")
|
|
287
|
+
curve: dict[str, str | float] = {"book": book, "x": x_col, "y": y_col}
|
|
288
|
+
# the token IS the sparse form of the .opj curve-anchor record; the
|
|
289
|
+
# id chunk's 0x80 tag sits 3 bytes into the regex match
|
|
290
|
+
record = opju_style_record(b, m.start() + 3)
|
|
291
|
+
if record is not None:
|
|
292
|
+
curve.update(style_fields(record))
|
|
293
|
+
# the shipped 8f-tag reader stays authoritative for line/scatter
|
|
294
|
+
style = curve_plot_style(b, m.start())
|
|
295
|
+
if style:
|
|
296
|
+
curve["style"] = style
|
|
297
|
+
out.append(curve)
|
|
298
|
+
records.append(record)
|
|
299
|
+
# auto/increment placeholders resolve by group role + plot order
|
|
300
|
+
# (curve_style_color.apply_increment_colors, pixel-oracle-verified)
|
|
301
|
+
apply_increment_colors(out, records)
|
|
302
|
+
return out
|