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,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))