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,297 @@
|
|
|
1
|
+
"""Curve->column binding for ``.opj`` (CPYA) graph figures (item 11, long
|
|
2
|
+
presumed permanently undecodable -- solved 2026-07-04).
|
|
3
|
+
|
|
4
|
+
``docs/origin_project_format.md`` sec 6.1 previously documented the DataPlot
|
|
5
|
+
("X-block") record as `` 58 00 00 00 98 03 40 b3 <u32 bodyLen> <enum> ...``
|
|
6
|
+
with "the column selector is inside the undecoded body -- no ASCII, no plain
|
|
7
|
+
indices found". That body is a fixed style/geometry record and genuinely
|
|
8
|
+
carries no selector. The selector lives one level up: immediately BEFORE each
|
|
9
|
+
DataPlot's style+body pair sits a small, previously-uninvestigated "curve
|
|
10
|
+
anchor" record --
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
01 00 00 00 <id:u16 LE> 00 01 00 00 00 00 a1 00 ...
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
-- whose first 6 bytes are the whole story: a fixed ``01 00 00 00`` marker,
|
|
17
|
+
then a little-endian ``u16`` that is the plotted column's own **global,
|
|
18
|
+
project-wide, monotonically-assigned serial id**. Detected structurally (no
|
|
19
|
+
fixed size -- ``519`` in ``Moke.opj``, ``515`` in ``XRD.opj``, evidently a
|
|
20
|
+
per-file/build constant, not part of the encoding) by requiring the anchor be
|
|
21
|
+
immediately followed by a block starting with the DataPlot magic itself
|
|
22
|
+
(``58 00 00 00 98 03 40 b3``, the same 8 bytes sec 6.1 already documents) --
|
|
23
|
+
this pairing is what a real curve looks like; nothing else in a graph window
|
|
24
|
+
matches both halves at once.
|
|
25
|
+
|
|
26
|
+
**How this was found.** The designed experiment from the RE brief: Moke's
|
|
27
|
+
``Graph8``/``Graph9`` both plot ``[Book4]Sheet1!B`` and have byte-identical
|
|
28
|
+
block-size sequences end to end -- diffing them block-by-block isolates
|
|
29
|
+
*noise* (every difference was a per-graph object/window serial counter,
|
|
30
|
+
always off by a small constant like +7, or the window's own creation index,
|
|
31
|
+
off by exactly 1 between adjacent windows -- never anything column-shaped).
|
|
32
|
+
Diffing ``Graph8`` against ``Graph2`` (``[Book4]Sheet1!O`` -- same book,
|
|
33
|
+
different column, same style family) with the SAME block alignment isolates
|
|
34
|
+
exactly one block that is identical between ``Graph8``/``Graph9`` (the noise
|
|
35
|
+
pair) but differs between ``Graph8``/``Graph2`` (the signal pair): the 519-
|
|
36
|
+
byte block immediately preceding the first DataPlot style+body pair. Its
|
|
37
|
+
first differing byte (offset 4, `` 1f`` vs ``5d`` = 31 vs 93) looked at first
|
|
38
|
+
like a per-book column ordinal (Book2's ``D``/``H``/``L`` curves read 12/16/
|
|
39
|
+
20 -- exactly ``letter_position + 8``; Book3's ``B``/``C``/``D``/``E`` read
|
|
40
|
+
26/27/28/29 -- exactly ``letter_position + 24``) but Book4 broke that model
|
|
41
|
+
outright (``B``=31, ``H``=94, ``O``=93, ``M``=53, ``N``=92 -- no additive
|
|
42
|
+
constant fits, and creation-order position fares no better). The values
|
|
43
|
+
*were* unique per (book, column) pair across 15 distinct pairs and 8
|
|
44
|
+
independently-authored graphs, though -- exactly the profile of a real
|
|
45
|
+
per-column identifier, just not one derivable from position within its own
|
|
46
|
+
book. Cross-checking the SAME 16-bit value against each column's own
|
|
47
|
+
storage block in the windows section (searched independently via each
|
|
48
|
+
column's ``"<Book>_<Col>\\0"`` dataset-name string, unrelated to any graph) found
|
|
49
|
+
it verbatim, at the identical relative offset (4, u16 LE) in that block's own
|
|
50
|
+
header -- 5-for-5 exact matches on the first pass, then every one of the 45
|
|
51
|
+
curves validated below. **Book and column are resolved together by this one
|
|
52
|
+
id** -- there is no separate "book selector" field to find; the column's own
|
|
53
|
+
id implicitly carries which book it belongs to, since ids are assigned
|
|
54
|
+
globally across the whole project, not restarted per book.
|
|
55
|
+
|
|
56
|
+
**Column-storage lookup (`column_id_map`).** Every ≥500-byte column-storage
|
|
57
|
+
block in the windows section (``00 00 <Book> 00 …`` window header, then one
|
|
58
|
+
block per column, per sheet -- see ``windows.py``) opens with the SAME 4-byte
|
|
59
|
+
tag family the curve anchor uses, then this same u16 id at offset 4, then
|
|
60
|
+
(among other things) the column's NUL-terminated short name at offset 0x12
|
|
61
|
+
and its plot designation byte at 0x11 (``windows.py``'s ``_DESIGNATION``
|
|
62
|
+
enum: 0=Y, 3=X, ...). ``windows.py``'s own ``_is_column_block`` additionally
|
|
63
|
+
requires ``payload[0x06] == 0x0B``, which is too strict for this corpus --
|
|
64
|
+
most of ``Moke.opj``'s ``Book4``/``Book1`` Sheet1 columns carry ``0x09``
|
|
65
|
+
there instead (only a handful, e.g. ``Book4``'s ``H``, carry ``0x0B``), so
|
|
66
|
+
reusing it silently drops most columns. This module's own detector
|
|
67
|
+
(``_column_short_name``) only requires the size floor + a clean printable
|
|
68
|
+
alpha-first short name, and is scanned across ALL sheets of a book (not just
|
|
69
|
+
the primary one, unlike ``windows.window_metadata``) so a curve referencing
|
|
70
|
+
a report-sheet column (e.g. Moke's ``FitLinearCurve1``) can still resolve --
|
|
71
|
+
none of the corpus's *locatable* graphs happen to need this, but nothing
|
|
72
|
+
about the id scheme restricts it to primary-sheet columns.
|
|
73
|
+
|
|
74
|
+
**Aside -- a ``windows.window_metadata`` bug surfaced by this investigation,
|
|
75
|
+
FIXED 2026-07-04.** Because most of ``Book4`` Sheet1's columns failed
|
|
76
|
+
``_is_column_block``'s old strict check (they carry header byte ``0x09``, not
|
|
77
|
+
``0x0B``), ``window_metadata``'s "sheet 2 restarted" guard never triggered for
|
|
78
|
+
Book4's real primary sheet -- so its ``FitLinear1`` report-sheet columns got
|
|
79
|
+
committed as if they were the *primary* sheet's data, mislabeling Book4's
|
|
80
|
+
``A``-``G`` designations/long-names. Fixed in ``windows.py`` by accepting
|
|
81
|
+
``0x06 in (0x09, 0x0B)`` and using the real sheet-boundary signal (a 365-byte
|
|
82
|
+
``Pd<Name>`` sub-header at offset 0xD0) to close collection at the 2nd marker
|
|
83
|
+
per window. This was orthogonal to the curve/column-id decode here (which uses
|
|
84
|
+
its own more permissive scan and never touches ``windows.py``).
|
|
85
|
+
|
|
86
|
+
**X is a structural inference, exactly as in ``.opju`` (`book_x_columns`).**
|
|
87
|
+
No oracle (Moke's or XRD's ``index.json``) records which column is plotted as
|
|
88
|
+
X -- only the axis *range*, which doesn't identify a column. Exactly
|
|
89
|
+
mirroring ``opju_curves.py``'s "X is not decoded" section: ``x`` is inferred
|
|
90
|
+
as the book's own designated-X column (primary sheet, designation byte ``3``)
|
|
91
|
+
falling back to the sheet's first column when no column is explicitly marked
|
|
92
|
+
X. This is unverified against any oracle, here or in the ``.opju`` sibling --
|
|
93
|
+
a documented structural assumption, not a decoded value.
|
|
94
|
+
|
|
95
|
+
**Validation (2026-07-04, both required oracle files).**
|
|
96
|
+
|
|
97
|
+
* ``Moke.opj``: 39/39 correct, 0 wrong, on every graph reachable via a
|
|
98
|
+
``00 00 <Name> 00`` window header (``Graph1``-``Graph12``, all single- and
|
|
99
|
+
multi-curve, multi-layer and cross-book cases -- ``Graph7``/``Graph10``
|
|
100
|
+
each mix ``Book4``/``Book5`` curves in one window). ``FitLine``/
|
|
101
|
+
``Residual`` (the FitLinear analysis's own auto-generated report graphs, 7
|
|
102
|
+
more oracle refs) have **no** ``00 00 <Name> 00`` header anywhere in the
|
|
103
|
+
block stream at all (confirmed by an exhaustive string search) -- these
|
|
104
|
+
live in the FitLinear analysis's own embedded storage, structurally
|
|
105
|
+
unreachable via ``container.walk_blocks``, not a decode failure.
|
|
106
|
+
* ``XRD.opj``: 6/6 correct on ``Graph1`` (all 6 curves, one per book,
|
|
107
|
+
``Book1``/``Book5``/``Book4``/``Book3``/``Book2``/``Book6`` in that
|
|
108
|
+
cross-book order -- also confirms the "book resolved via the same id, no
|
|
109
|
+
separate selector" claim across 6 different books in one layer). The 18
|
|
110
|
+
``sparkline*`` refs are a structurally different feature -- per-COLUMN
|
|
111
|
+
inline mini-plots embedded in the worksheet, not separate Graph windows at
|
|
112
|
+
all (no ``00 00 sparklineN 00`` header exists, and a whole-file scan for
|
|
113
|
+
the curve-anchor pattern -- ``01 00 00 00`` + DataPlot magic -- finds
|
|
114
|
+
exactly 6 hits total in the entire file, all inside ``Graph1``): out of
|
|
115
|
+
reach for this decoder by construction, not a missed match.
|
|
116
|
+
* **Aggregate: 45/45 correct (100% precision) on every curve this decoder can
|
|
117
|
+
see at all; 45/70 (64.3%) of the full task-level oracle**, the remaining 25
|
|
118
|
+
being two structurally distinct, out-of-scope window kinds (FitLinear
|
|
119
|
+
report graphs; per-column sparklines) rather than undecoded curves.
|
|
120
|
+
|
|
121
|
+
See ``tests/test_io_origin_figures_opj_curves.py`` for the synthetic + real-
|
|
122
|
+
corpus tests and ``tools/origin_trial/score_curve_bindings_opj.py`` for a
|
|
123
|
+
standalone rescorer.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
from __future__ import annotations
|
|
127
|
+
|
|
128
|
+
import struct
|
|
129
|
+
from collections.abc import Sequence
|
|
130
|
+
|
|
131
|
+
from quantized.io.origin_project.curve_style_color import apply_increment_colors, style_fields
|
|
132
|
+
from quantized.io.origin_project.windows import _cstring, _is_window_header
|
|
133
|
+
|
|
134
|
+
__all__ = ["book_x_columns", "column_id_map", "extract_curves"]
|
|
135
|
+
|
|
136
|
+
# The curve-anchor record's fixed 4-byte marker (see module docstring).
|
|
137
|
+
_CURVE_PREFIX = b"\x01\x00\x00\x00"
|
|
138
|
+
|
|
139
|
+
# The DataPlot ("X-block") magic already documented in
|
|
140
|
+
# docs/origin_project_format.md sec 6.1 ("0x58 marker byte, constant magic
|
|
141
|
+
# 0xB3400398"). A real curve anchor is always immediately followed by a block
|
|
142
|
+
# opening with this exact sequence -- the co-occurrence is what makes the
|
|
143
|
+
# anchor detector precise without needing a fixed block size.
|
|
144
|
+
_DATAPLOT_MAGIC = b"\x58\x00\x00\x00\x98\x03\x40\xb3"
|
|
145
|
+
|
|
146
|
+
# windows.py's plot-designation enum (0=Y, 3=X, ...) -- see its `_DESIGNATION`.
|
|
147
|
+
_DESIGNATION_X = 3
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _column_short_name(payload: bytes) -> str | None:
|
|
151
|
+
"""A column-storage block's short name (e.g. ``"B"``), or ``None``.
|
|
152
|
+
|
|
153
|
+
Printable, NUL-terminated, alpha-first, at payload offset ``0x12`` --
|
|
154
|
+
mirrors ``windows.py``'s column-property-block layout but WITHOUT
|
|
155
|
+
``windows._is_column_block``'s stricter type-byte check, which misses
|
|
156
|
+
most real columns in this corpus (see module docstring)."""
|
|
157
|
+
if len(payload) < 500:
|
|
158
|
+
return None
|
|
159
|
+
name = _cstring(payload, 0x12, 8)
|
|
160
|
+
return name if name and name[0].isalpha() else None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _is_graph_header(blocks: Sequence[tuple[int, bytes]], i: int) -> str | None:
|
|
164
|
+
"""``True`` (the window name) when block ``i`` opens a GRAPH window --
|
|
165
|
+
mirrors ``figures.py``'s own detector: a window header whose immediately
|
|
166
|
+
following block is a layer-continuation block (>=90 B, head
|
|
167
|
+
``00 00 1f 00``)."""
|
|
168
|
+
_, payload = blocks[i]
|
|
169
|
+
name = _is_window_header(payload)
|
|
170
|
+
if name is None:
|
|
171
|
+
return None
|
|
172
|
+
nxt = blocks[i + 1][1] if i + 1 < len(blocks) else b""
|
|
173
|
+
if len(nxt) >= 90 and nxt[:4] == b"\x00\x00\x1f\x00":
|
|
174
|
+
return name
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def column_id_map(blocks: Sequence[tuple[int, bytes]]) -> dict[int, tuple[str, str]]:
|
|
179
|
+
"""Every workbook column's global serial id -> ``(book, column)``.
|
|
180
|
+
|
|
181
|
+
Scanned across the whole windows section (ALL sheets of every book, not
|
|
182
|
+
just the primary one -- see module docstring), skipping graph windows
|
|
183
|
+
entirely (they hold no column-storage blocks of their own, but their
|
|
184
|
+
internal 519-ish-byte records could otherwise false-positive against the
|
|
185
|
+
permissive short-name check)."""
|
|
186
|
+
id_map: dict[int, tuple[str, str]] = {}
|
|
187
|
+
current_book: str | None = None
|
|
188
|
+
in_graph = False
|
|
189
|
+
i = 0
|
|
190
|
+
n = len(blocks)
|
|
191
|
+
while i < n:
|
|
192
|
+
if _is_graph_header(blocks, i) is not None:
|
|
193
|
+
in_graph, current_book = True, None
|
|
194
|
+
i += 2
|
|
195
|
+
continue
|
|
196
|
+
_, payload = blocks[i]
|
|
197
|
+
name = _is_window_header(payload)
|
|
198
|
+
if name is not None:
|
|
199
|
+
in_graph, current_book = False, name
|
|
200
|
+
i += 1
|
|
201
|
+
continue
|
|
202
|
+
if not in_graph and current_book is not None and len(payload) >= 6:
|
|
203
|
+
short = _column_short_name(payload)
|
|
204
|
+
if short is not None:
|
|
205
|
+
cid = struct.unpack_from("<H", payload, 4)[0]
|
|
206
|
+
id_map[cid] = (current_book, short)
|
|
207
|
+
i += 1
|
|
208
|
+
return id_map
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def book_x_columns(blocks: Sequence[tuple[int, bytes]]) -> dict[str, str]:
|
|
212
|
+
"""book -> its designated X column's short name (structural inference).
|
|
213
|
+
|
|
214
|
+
Primary sheet only -- stops collecting for a book at the first repeated
|
|
215
|
+
short name (the same "sheet 2 restarts at column A" signal
|
|
216
|
+
``windows.window_metadata`` uses, reimplemented here against this
|
|
217
|
+
module's more permissive column detector). Picks the column whose
|
|
218
|
+
designation byte is ``3`` (X); falls back to the sheet's first column
|
|
219
|
+
when none is explicitly marked. See module docstring: this is NOT
|
|
220
|
+
verified against any oracle, exactly like ``opju_curves.py``'s X.
|
|
221
|
+
"""
|
|
222
|
+
seen: dict[str, dict[str, int]] = {}
|
|
223
|
+
closed: set[str] = set()
|
|
224
|
+
current_book: str | None = None
|
|
225
|
+
in_graph = False
|
|
226
|
+
i = 0
|
|
227
|
+
n = len(blocks)
|
|
228
|
+
while i < n:
|
|
229
|
+
if _is_graph_header(blocks, i) is not None:
|
|
230
|
+
in_graph, current_book = True, None
|
|
231
|
+
i += 2
|
|
232
|
+
continue
|
|
233
|
+
_, payload = blocks[i]
|
|
234
|
+
name = _is_window_header(payload)
|
|
235
|
+
if name is not None:
|
|
236
|
+
in_graph, current_book = False, name
|
|
237
|
+
i += 1
|
|
238
|
+
continue
|
|
239
|
+
if not in_graph and current_book is not None and current_book not in closed:
|
|
240
|
+
short = _column_short_name(payload)
|
|
241
|
+
if short is not None and len(payload) > 0x11:
|
|
242
|
+
cols = seen.setdefault(current_book, {})
|
|
243
|
+
if short in cols:
|
|
244
|
+
closed.add(current_book) # sheet 2 restarting at the same letters
|
|
245
|
+
else:
|
|
246
|
+
cols[short] = payload[0x11]
|
|
247
|
+
i += 1
|
|
248
|
+
out: dict[str, str] = {}
|
|
249
|
+
for book, cols in seen.items():
|
|
250
|
+
if not cols:
|
|
251
|
+
continue
|
|
252
|
+
x_col = next((c for c, d in cols.items() if d == _DESIGNATION_X), None)
|
|
253
|
+
out[book] = x_col if x_col is not None else next(iter(cols))
|
|
254
|
+
return out
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def extract_curves(
|
|
258
|
+
blocks: Sequence[tuple[int, bytes]],
|
|
259
|
+
start: int,
|
|
260
|
+
end: int,
|
|
261
|
+
id_map: dict[int, tuple[str, str]],
|
|
262
|
+
x_columns: dict[str, str],
|
|
263
|
+
) -> list[dict[str, str | float]]:
|
|
264
|
+
"""Every curve's ``{book, x, y}`` binding found in ``blocks[start:end)``.
|
|
265
|
+
|
|
266
|
+
``y`` is decoded exactly via the curve anchor's own global column id (see
|
|
267
|
+
module docstring); a curve whose id doesn't resolve to a known column, or
|
|
268
|
+
whose book has no inferable X column, is silently dropped -- never
|
|
269
|
+
guessed. The anchor payload is also the curve's fixed style record
|
|
270
|
+
(``curve_style_color.py``: symbol color/kind, line color, line-vs-
|
|
271
|
+
scatter -- oracle-verified on ``hc2convert.opj``), so any decodable
|
|
272
|
+
``color``/``symbol``/``style`` keys ride along; undecodable fields
|
|
273
|
+
(auto color, unmapped bytes) are absent, never defaulted."""
|
|
274
|
+
out: list[dict[str, str | float]] = []
|
|
275
|
+
records: list[bytes | None] = []
|
|
276
|
+
last = min(end, len(blocks) - 1)
|
|
277
|
+
for j in range(start, last):
|
|
278
|
+
_, payload = blocks[j]
|
|
279
|
+
if len(payload) < 6 or payload[:4] != _CURVE_PREFIX:
|
|
280
|
+
continue
|
|
281
|
+
_, npayload = blocks[j + 1]
|
|
282
|
+
if npayload[:8] != _DATAPLOT_MAGIC:
|
|
283
|
+
continue
|
|
284
|
+
cid = struct.unpack_from("<H", payload, 4)[0]
|
|
285
|
+
info = id_map.get(cid)
|
|
286
|
+
if info is None:
|
|
287
|
+
continue
|
|
288
|
+
book, col = info
|
|
289
|
+
x = x_columns.get(book)
|
|
290
|
+
if x is None:
|
|
291
|
+
continue # unknown/columnless book: drop, never guess
|
|
292
|
+
out.append({"book": book, "x": x, "y": col, **style_fields(payload)})
|
|
293
|
+
records.append(payload)
|
|
294
|
+
# auto/increment placeholders resolve by group role + plot order
|
|
295
|
+
# (curve_style_color.apply_increment_colors, pixel-oracle-verified)
|
|
296
|
+
apply_increment_colors(out, records)
|
|
297
|
+
return out
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Region-shape graphic objects (``Rect*``) in ``.opj`` graph layers
|
|
2
|
+
(decode-plan item 41 — the Graph1 SLD-profile "layer bands").
|
|
3
|
+
|
|
4
|
+
Origin lets a user drop filled rectangles onto a graph layer — the corpus
|
|
5
|
+
uses them as vertical film-stack region bands (PNR.opj ``Graph1``:
|
|
6
|
+
SiO2/Pt/YIG/Py/Ru/Air, each a full-height pastel band). These are a
|
|
7
|
+
distinct graph-child object class the figure decoder previously routed to
|
|
8
|
+
"ignore":
|
|
9
|
+
|
|
10
|
+
* the universal **133-byte object header** (``figures.py``) with type tag
|
|
11
|
+
``0x31`` at payload offset 2 (a previously-undocumented value — the known
|
|
12
|
+
tags were 0x00 text / 0x07 curve / 0x22 line / 0x23 storage) and the
|
|
13
|
+
object's own name (``Rect``, ``Rect1``…) at offset 70;
|
|
14
|
+
* followed by one **130-byte body block** holding the geometry + fill:
|
|
15
|
+
|
|
16
|
+
======== =====================================================
|
|
17
|
+
offset field
|
|
18
|
+
======== =====================================================
|
|
19
|
+
7 fill colour low byte (mirrors the u32 at 66; equal for
|
|
20
|
+
every palette-type instance corpus-wide)
|
|
21
|
+
10 (f64) left edge, layer-frame fraction (== the header's own
|
|
22
|
+
anchor fraction at header offset 19)
|
|
23
|
+
18 (f64) top edge fraction (from the frame TOP — the same
|
|
24
|
+
convention as text-object anchors, ``frac_to_data``)
|
|
25
|
+
26 (f64) width fraction
|
|
26
|
+
34 (f64) height fraction
|
|
27
|
+
66 (u32) fill colour, on-disk **ocolor** encoding — high byte
|
|
28
|
+
0x00 = 0-BASED classic-palette index (the same disk
|
|
29
|
+
convention as curve colours, ``curve_style_color``),
|
|
30
|
+
0x01 = direct COLORREF ``0x01BBGGRR``
|
|
31
|
+
======== =====================================================
|
|
32
|
+
|
|
33
|
+
Corpus evidence (2026-07-11 sweep, all local ``.opj`` + ``.otp``): 329
|
|
34
|
+
instances across 4 files (PNR 156, SuperlatticeFits 151, MnN_Diffusion_PNR
|
|
35
|
+
16, SLD_DoubleY.otp 6) — every one named ``Rect*``, every body exactly 130
|
|
36
|
+
bytes, every fraction quad plausible. The width fraction reproduces the
|
|
37
|
+
header's page-unit box width / the layer frame width exactly (PNR Graph1
|
|
38
|
+
Rect5: 207/4913 = 0.04213 = the offset-26 double). Fill colours validated
|
|
39
|
+
against the live-Origin PNG oracle on PNR ``Graph1`` — 6/6 bands match
|
|
40
|
+
(Ru=1 red, Air/SiO2=0x12 light gray, Py=0x0b olive, YIG=3 blue, Pt=0x0e
|
|
41
|
+
orange, all 0-based palette); 29 corpus instances carry direct-RGB type-1
|
|
42
|
+
values (e.g. ``0x012DAFE6`` → #E6AF2D) — the same two-type ocolor model
|
|
43
|
+
``ocolor_to_rgb`` already decodes for curves. No ``Circle*``/other shape
|
|
44
|
+
name exists anywhere in the corpus, and the real ``.opju`` corpus has no
|
|
45
|
+
shape objects at all (only the ``SLDdouble.otpu`` template twin) — so this
|
|
46
|
+
module decodes exactly what is evidenced: the CPYA rectangle record.
|
|
47
|
+
|
|
48
|
+
**Known-not-decoded (honest gaps, see docs/origin_project_format.md):** a
|
|
49
|
+
fill *transparency* field could not be isolated (all instances within any
|
|
50
|
+
one graph share whatever it is; no body byte reads like an alpha across
|
|
51
|
+
files) — rendering opacity is a frontend presentation choice, documented
|
|
52
|
+
as such, not a decoded value. The rare non-zero bytes at body offsets
|
|
53
|
+
49-65 (6 instances) and the 3-value field at 114-115 are uncharacterized;
|
|
54
|
+
nothing here reads them.
|
|
55
|
+
|
|
56
|
+
Pure library: bytes in → dicts out. No fastapi/pydantic/routes imports.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
from struct import unpack_from
|
|
62
|
+
from typing import Any
|
|
63
|
+
|
|
64
|
+
from quantized.io.origin_project.annotation_marks import frac_to_data
|
|
65
|
+
from quantized.io.origin_project.curve_style_color import ORIGIN_PALETTE, ocolor_to_rgb
|
|
66
|
+
|
|
67
|
+
__all__ = ["SHAPE_TYPE", "build_region_shade", "rect_fill_color", "rect_shape_fractions"]
|
|
68
|
+
|
|
69
|
+
# The 133-byte object header's type tag for closed-shape graphic objects.
|
|
70
|
+
SHAPE_TYPE = 0x31
|
|
71
|
+
|
|
72
|
+
_FRAC_LEFT_OFF = 10
|
|
73
|
+
_FRAC_TOP_OFF = 18
|
|
74
|
+
_FRAC_WIDTH_OFF = 26
|
|
75
|
+
_FRAC_HEIGHT_OFF = 34
|
|
76
|
+
_FILL_COLOR_OFF = 66
|
|
77
|
+
_BODY_MIN_LEN = _FILL_COLOR_OFF + 4
|
|
78
|
+
|
|
79
|
+
# Same plausibility bound as annotation anchors (annotation_marks): a shape
|
|
80
|
+
# can legitimately hang outside its layer frame (MnN's cross-panel bands
|
|
81
|
+
# reach height fraction ~2.0), but a misread double is astronomical.
|
|
82
|
+
_FRAC_BOUND = 50.0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rect_shape_fractions(body: bytes) -> tuple[float, float, float, float] | None:
|
|
86
|
+
"""``(left, top, width, height)`` layer-frame fractions from a shape
|
|
87
|
+
object's body block, or ``None`` when the block is too short or any
|
|
88
|
+
double is implausible (dropped, never guessed)."""
|
|
89
|
+
if len(body) < _BODY_MIN_LEN:
|
|
90
|
+
return None
|
|
91
|
+
left = float(unpack_from("<d", body, _FRAC_LEFT_OFF)[0])
|
|
92
|
+
top = float(unpack_from("<d", body, _FRAC_TOP_OFF)[0])
|
|
93
|
+
width = float(unpack_from("<d", body, _FRAC_WIDTH_OFF)[0])
|
|
94
|
+
height = float(unpack_from("<d", body, _FRAC_HEIGHT_OFF)[0])
|
|
95
|
+
values = (left, top, width, height)
|
|
96
|
+
if not all(abs(v) <= _FRAC_BOUND for v in values):
|
|
97
|
+
return None
|
|
98
|
+
if width <= 0 or height <= 0:
|
|
99
|
+
return None
|
|
100
|
+
return values
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def rect_fill_color(body: bytes) -> str | None:
|
|
104
|
+
"""The shape's fill as ``"#RRGGBB"`` from the body's ocolor u32 (offset
|
|
105
|
+
66), or ``None`` for auto/unrecognized encodings (never guessed).
|
|
106
|
+
|
|
107
|
+
The on-disk field uses the SAME two-type ocolor model as curve colours
|
|
108
|
+
(``curve_style_color``): high byte 0x00 = 0-based classic-palette index
|
|
109
|
+
(converted to the 1-based form ``ocolor_to_rgb`` takes), 0x01 = direct
|
|
110
|
+
COLORREF."""
|
|
111
|
+
if len(body) < _BODY_MIN_LEN:
|
|
112
|
+
return None
|
|
113
|
+
field = int(unpack_from("<I", body, _FILL_COLOR_OFF)[0])
|
|
114
|
+
kind = field >> 24
|
|
115
|
+
if kind == 0:
|
|
116
|
+
return ocolor_to_rgb(field + 1) if field < len(ORIGIN_PALETTE) else None
|
|
117
|
+
return ocolor_to_rgb(field) if kind == 1 else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_region_shade(
|
|
121
|
+
body: bytes,
|
|
122
|
+
x_from: float,
|
|
123
|
+
x_to: float,
|
|
124
|
+
y_from: float,
|
|
125
|
+
y_to: float,
|
|
126
|
+
x_log: bool,
|
|
127
|
+
y_log: bool,
|
|
128
|
+
) -> dict[str, Any] | None:
|
|
129
|
+
"""One region-shade dict ``{"x1","x2","y1","y2","fill"}`` in DATA
|
|
130
|
+
coordinates (``x1 < x2``, ``y1 < y2``; log axes interpolate in log10
|
|
131
|
+
space via :func:`frac_to_data`, the confirmed position model), or
|
|
132
|
+
``None`` when the geometry never decoded. ``fill`` is ``"#RRGGBB"`` or
|
|
133
|
+
``None`` (colour undecoded — the shape still ships so the extent isn't
|
|
134
|
+
lost, but a renderer should skip a fill-less shade rather than guess a
|
|
135
|
+
colour)."""
|
|
136
|
+
fracs = rect_shape_fractions(body)
|
|
137
|
+
if fracs is None:
|
|
138
|
+
return None
|
|
139
|
+
left, top, width, height = fracs
|
|
140
|
+
xa, ya = frac_to_data(left, top, x_from, x_to, y_from, y_to, x_log, y_log)
|
|
141
|
+
xb, yb = frac_to_data(left + width, top + height, x_from, x_to, y_from, y_to, x_log, y_log)
|
|
142
|
+
return {
|
|
143
|
+
"x1": min(xa, xb),
|
|
144
|
+
"x2": max(xa, xb),
|
|
145
|
+
"y1": min(ya, yb),
|
|
146
|
+
"y2": max(ya, yb),
|
|
147
|
+
"fill": rect_fill_color(body),
|
|
148
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Read Origin ``.opju`` (CPYUA, 2018+) projects: worksheet data → DataStruct.
|
|
2
|
+
|
|
3
|
+
The worksheet-column codec is solved (``opju_codec.py``): each column is an
|
|
4
|
+
FPC-compressed float64 stream, located by its LEB128-varint record header and
|
|
5
|
+
labelled by the nearest preceding ``<Book>_<Col>[@sheet]`` dataset name. Books
|
|
6
|
+
are grouped and assembled exactly like the ``.opj`` reader (shared ``_group``
|
|
7
|
+
/ ``_build_book`` / ``_inventory``). Column long-names/units/comments come
|
|
8
|
+
from the CPYUA windows section (``windows_opju.py``, plan item 10) the same
|
|
9
|
+
way `.opj`'s windows-section metadata feeds ``_build_book`` — designation-X
|
|
10
|
+
becomes the x axis, real labels/units/comments attach where a book's window
|
|
11
|
+
section can be structurally confirmed, and book display titles recover from
|
|
12
|
+
the embedded import filename where available. Columns/books that can't be
|
|
13
|
+
confirmed keep the Origin short-designation fallback (A, B, C…) rather than
|
|
14
|
+
being guessed at.
|
|
15
|
+
|
|
16
|
+
Plan item 4's report-sheet residue (``opju_reports.py``) is folded in the
|
|
17
|
+
same way ``.opj``'s ``text_cols``/``report_cols`` are: a book made entirely
|
|
18
|
+
of report-sheet columns (e.g. a fit's "FitNL1" sheet, with zero
|
|
19
|
+
plausible-numeric columns of its own) still gets its own pseudo-book via
|
|
20
|
+
``_build_book``'s empty-``cols`` branch, rather than being silently dropped
|
|
21
|
+
for having nothing in ``books``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections import OrderedDict
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from quantized.datastruct import DataStruct
|
|
30
|
+
from quantized.io.origin_project.container import fallback
|
|
31
|
+
from quantized.io.origin_project.opj import (
|
|
32
|
+
Columns,
|
|
33
|
+
TextColumns,
|
|
34
|
+
_build_book,
|
|
35
|
+
_group,
|
|
36
|
+
_group_named,
|
|
37
|
+
_inventory,
|
|
38
|
+
)
|
|
39
|
+
from quantized.io.origin_project.opju_codec import scan_columns
|
|
40
|
+
from quantized.io.origin_project.opju_reports import scan_report_columns
|
|
41
|
+
from quantized.io.origin_project.windows import BookMeta
|
|
42
|
+
from quantized.io.origin_project.windows_opju import opju_window_metadata
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"build_opju_books",
|
|
46
|
+
"build_opju_primary",
|
|
47
|
+
"parse_opju",
|
|
48
|
+
"read_opju",
|
|
49
|
+
"read_opju_books",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
_ParseResult = tuple[
|
|
53
|
+
OrderedDict[str, Columns],
|
|
54
|
+
OrderedDict[str, TextColumns],
|
|
55
|
+
dict[str, BookMeta],
|
|
56
|
+
list[dict[str, object]],
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse(path: Path, *, raw: bytes | None = None) -> _ParseResult:
|
|
61
|
+
"""Decode the project once. ``raw`` lets a caller that already has the
|
|
62
|
+
file's bytes (e.g. :func:`parse_opju`'s callers in
|
|
63
|
+
``origin_project/__init__.py``) skip a second disk read; ``None`` (the
|
|
64
|
+
default) reads ``path`` itself, unchanged from before."""
|
|
65
|
+
b = path.read_bytes() if raw is None else raw
|
|
66
|
+
if not b.startswith(b"CPYUA"):
|
|
67
|
+
raise fallback(path, f"'{path.name}' does not look like a CPYUA .opju (bad header).")
|
|
68
|
+
columns = scan_columns(b)
|
|
69
|
+
if not columns:
|
|
70
|
+
raise fallback(path, f"no worksheet columns could be decoded from '{path.name}'.")
|
|
71
|
+
books = _group(columns)
|
|
72
|
+
report_books = _group_named(scan_report_columns(b))
|
|
73
|
+
books_meta = opju_window_metadata(b, {k: [c for c, _ in v] for k, v in books.items()})
|
|
74
|
+
return books, report_books, books_meta, _inventory(books, books_meta, report_books)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _primary_key(books: OrderedDict[str, Columns]) -> str:
|
|
78
|
+
"""The book :func:`read_opju`/:func:`build_opju_primary` treat as *the*
|
|
79
|
+
primary dataset — see ``opj._primary_key`` (identical selection rule,
|
|
80
|
+
duplicated here since the two containers' ``Columns`` groupings are
|
|
81
|
+
built independently)."""
|
|
82
|
+
primary_pool = [k for k in books if "@" not in k] or list(books)
|
|
83
|
+
return max(primary_pool, key=lambda k: sum(len(v) for _, v in books[k]))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_opju(path: Path, *, raw: bytes | None = None) -> _ParseResult:
|
|
87
|
+
"""Public single-parse entry point — see ``opj.parse_opj``. Lets a caller
|
|
88
|
+
that needs both the primary book (:func:`build_opju_primary`) and every
|
|
89
|
+
book (:func:`build_opju_books`) parse the project once."""
|
|
90
|
+
return _parse(path, raw=raw)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_opju_primary(parsed: _ParseResult) -> DataStruct:
|
|
94
|
+
"""Build the primary book's DataStruct from an already-:func:`parse_opju`'d
|
|
95
|
+
project — see ``opj.build_opj_primary``."""
|
|
96
|
+
books, report_books, books_meta, inventory = parsed
|
|
97
|
+
primary = _primary_key(books)
|
|
98
|
+
return _build_book(
|
|
99
|
+
primary,
|
|
100
|
+
books[primary],
|
|
101
|
+
books_meta,
|
|
102
|
+
inventory,
|
|
103
|
+
source_format="origin_opju",
|
|
104
|
+
report_cols=report_books.get(primary),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_opju_books(parsed: _ParseResult) -> list[DataStruct]:
|
|
109
|
+
"""Build every book's DataStruct from an already-:func:`parse_opju`'d
|
|
110
|
+
project — see ``opj.build_opj_books``.
|
|
111
|
+
|
|
112
|
+
A sheet made entirely of report-sheet columns (plan item 4) has no
|
|
113
|
+
plausible-numeric columns, so it never appears in ``books`` — the union
|
|
114
|
+
with ``report_books`` below still surfaces it as its own pseudo-book
|
|
115
|
+
(see ``_build_book``'s empty-``cols`` branch).
|
|
116
|
+
"""
|
|
117
|
+
books, report_books, books_meta, inventory = parsed
|
|
118
|
+
keys = list(books) + [k for k in report_books if k not in books]
|
|
119
|
+
return [
|
|
120
|
+
_build_book(
|
|
121
|
+
k,
|
|
122
|
+
books.get(k, []),
|
|
123
|
+
books_meta,
|
|
124
|
+
inventory,
|
|
125
|
+
source_format="origin_opju",
|
|
126
|
+
report_cols=report_books.get(k),
|
|
127
|
+
)
|
|
128
|
+
for k in keys
|
|
129
|
+
if books.get(k) or report_books.get(k)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def read_opju(path: Path) -> DataStruct:
|
|
134
|
+
"""The single-DataStruct contract: the largest workbook (inventory in metadata)."""
|
|
135
|
+
return build_opju_primary(_parse(path))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def read_opju_books(path: Path) -> list[DataStruct]:
|
|
139
|
+
"""Every workbook in the project as its own DataStruct (plan item 3/16).
|
|
140
|
+
|
|
141
|
+
A sheet made entirely of report-sheet columns (plan item 4) has no
|
|
142
|
+
plausible-numeric columns, so it never appears in ``books`` — the union
|
|
143
|
+
with ``report_books`` below still surfaces it as its own pseudo-book
|
|
144
|
+
(see ``_build_book``'s empty-``cols`` branch).
|
|
145
|
+
"""
|
|
146
|
+
return build_opju_books(_parse(path))
|