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,370 @@
|
|
|
1
|
+
"""The ``.opju`` (CPYUA) worksheet-column codec — canonical Burtscher FPC.
|
|
2
|
+
|
|
3
|
+
Format facts derived clean-room (no GPL liborigin) from known-content Rosetta
|
|
4
|
+
specimens generated with an Origin 2026b trial, then locked against Origin's
|
|
5
|
+
own exported ground truth (``tests/test_io_origin_ground_truth.py``): every
|
|
6
|
+
``XAS.opju`` column (243/243 values) and 193 ``Hc2 data`` columns decode
|
|
7
|
+
bit-exact, plus ``RockingCurve`` axes at 71 and 201 rows.
|
|
8
|
+
|
|
9
|
+
Each numeric column is an FPC-compressed float64 stream (Burtscher &
|
|
10
|
+
Ratanaworabhan, *FPC: A High-Speed Compressor for Double-Precision
|
|
11
|
+
Floating-Point Data*, IEEE TC 2009). Two predictors race for every value:
|
|
12
|
+
|
|
13
|
+
* **FCM** — a value predictor: ``pred = fcm[fh]``;
|
|
14
|
+
* **DFCM** — a stride predictor: ``pred = last + dfcm[dh]``.
|
|
15
|
+
|
|
16
|
+
The encoder XORs the true bits against the closer prediction and stores the
|
|
17
|
+
low ``k`` bytes little-endian (the dropped high bytes are the leading zeros).
|
|
18
|
+
Every value carries a 4-bit code; two codes pack into one control byte, low
|
|
19
|
+
nibble first:
|
|
20
|
+
|
|
21
|
+
* **bit 3** selects the predictor (0 = FCM, 1 = DFCM),
|
|
22
|
+
* **bits 0-2** give the residual byte-count: codes 0-3 store 0-3 bytes and
|
|
23
|
+
codes 4-7 store 5-8 (a count of exactly 4 is unsupported, per the paper),
|
|
24
|
+
so code 0 with either selector (``0x0``/``0x8``) means predictor-exact.
|
|
25
|
+
|
|
26
|
+
Both hash tables hold ``2**12`` entries and update the textbook FPC way::
|
|
27
|
+
|
|
28
|
+
fh = ((fh << 6) ^ (value >> 48)) & 0xFFF
|
|
29
|
+
dh = ((dh << 2) ^ (stride >> 40)) & 0xFFF
|
|
30
|
+
|
|
31
|
+
Origin frames each column record as ``0a 05 <varint> ff ff <nrows:varint> 00
|
|
32
|
+
<segments>``. Segments open with a ZigZag varint: −m = "m FPC rows", with
|
|
33
|
+
``0x0c`` + the stream *inline* and a FRESH predictor state per stream (a
|
|
34
|
+
plain column is the single segment ``zigzag(−nrows)``); +k = "k rows of one
|
|
35
|
+
repeated value" whose value-spec follows (``0x50``+float64, ``0x1a``/``0x11``
|
|
36
|
+
+ the double's top 2/1 bytes, or bare ``0x64`` = 0.0). Segments interleave
|
|
37
|
+
freely — Origin run-length-compresses constant runs (total-reflection
|
|
38
|
+
plateaus, logger hold-steps) *outside* the FPC streams, and a fully constant
|
|
39
|
+
column has no stream at all. Empty numeric cells carry the
|
|
40
|
+
``ORIGIN_MISSING`` sentinel and map to NaN.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
import re
|
|
46
|
+
import struct
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
from numpy.typing import NDArray
|
|
50
|
+
|
|
51
|
+
from quantized.io.origin_project.container import ORIGIN_MISSING, plausible_column
|
|
52
|
+
|
|
53
|
+
__all__ = ["CodecError", "curve_plot_style", "decode_stream", "scan_columns", "tail_start"]
|
|
54
|
+
|
|
55
|
+
_MASK = 0xFFFFFFFFFFFFFFFF
|
|
56
|
+
_TABLE_MASK = (1 << 12) - 1 # 2**12-entry FCM/DFCM hash tables
|
|
57
|
+
_FCM_SHIFT, _FCM_DROP = 6, 48 # fh = ((fh << 6) ^ (value >> 48)) & mask
|
|
58
|
+
_DFCM_SHIFT, _DFCM_DROP = 2, 40 # dh = ((dh << 2) ^ (stride >> 40)) & mask
|
|
59
|
+
|
|
60
|
+
# A length-prefixed dataset name "<Book>_<Col>[@sheet]" (CPYUA strings carry
|
|
61
|
+
# no NUL terminator, so the byte before the match must equal the name
|
|
62
|
+
# length). The optional "@<N>" suffix marks a column of sheet N>1 in a
|
|
63
|
+
# multi-sheet book (same convention `.opj`'s container.NAME_RE uses) — added
|
|
64
|
+
# alongside plan item 4's report-sheet decode, whose fitreport2.opju oracle
|
|
65
|
+
# was the first `.opju` specimen with more than one sheet in a book and
|
|
66
|
+
# exposed that every extra-sheet column was previously mis-anchored to the
|
|
67
|
+
# nearest SHEET-1 name instead (e.g. every FitNL1/FitNLCurve1 column
|
|
68
|
+
# collapsing onto "FitBook_B"): the un-suffixed pattern still matched an
|
|
69
|
+
# "@2"-suffixed name as a prefix, but the length-prefix byte then disagreed
|
|
70
|
+
# with the (shorter) matched length, so it should have failed the anchoring
|
|
71
|
+
# check below -- except sheet-1's own un-suffixed names sit right before the
|
|
72
|
+
# extra-sheet block and satisfied it instead, silently swallowing every
|
|
73
|
+
# later column into whichever sheet-1 name came last.
|
|
74
|
+
# Bounds deliberately wider than any corpus maximum (book 62 / column 16 /
|
|
75
|
+
# sheet 999 — the old 40/4/99 caps were corpus-shaped and silently dropped a
|
|
76
|
+
# column whose name exceeded them; 2026-07-06 genericity audit). The
|
|
77
|
+
# length-prefix byte check at the match site is the actual validity gate.
|
|
78
|
+
_NAME = re.compile(rb"[A-Za-z][\w ]{0,62}_[A-Za-z0-9]{1,16}(?:@\d{1,3})?")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CodecError(ValueError):
|
|
82
|
+
"""A column stream that doesn't parse under the FPC codec."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _width(nibble: int) -> int:
|
|
86
|
+
"""Residual byte-count for a 4-bit code — canonical FPC bcode semantics.
|
|
87
|
+
|
|
88
|
+
The low 3 bits encode the count of *stored* residual bytes, except that a
|
|
89
|
+
count of exactly 4 is unsupported (rare in practice, per the FPC paper) so
|
|
90
|
+
codes 4-7 mean 5-8 bytes. Code 0 is the predictor-exact case (0 bytes) —
|
|
91
|
+
for either predictor, so ``0x0`` (FCM exact) and ``0x8`` (DFCM exact) both
|
|
92
|
+
carry no payload. An earlier width table ``(c & 7) + 1`` coincided with
|
|
93
|
+
this one for c >= 4 — the only codes clean ramps ever exercise — which let
|
|
94
|
+
hundreds of columns validate bit-exact while ultra-smooth data (codes 0-3)
|
|
95
|
+
misparsed; that was the real cause of the "DFCM-collision" drop-outs.
|
|
96
|
+
"""
|
|
97
|
+
c = nibble & 7
|
|
98
|
+
return c if c < 4 else c + 1
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _decode(stream: bytes, nrows: int) -> tuple[NDArray[np.float64], int]:
|
|
102
|
+
"""Decode ``nrows`` float64s; return ``(values, bytes_consumed)``."""
|
|
103
|
+
fcm: dict[int, int] = {}
|
|
104
|
+
dfcm: dict[int, int] = {}
|
|
105
|
+
fh = dh = 0
|
|
106
|
+
last = 0
|
|
107
|
+
out: list[int] = []
|
|
108
|
+
pos = 0
|
|
109
|
+
n = len(stream)
|
|
110
|
+
while len(out) < nrows:
|
|
111
|
+
if pos >= n:
|
|
112
|
+
raise CodecError(f"stream exhausted at {len(out)}/{nrows} values")
|
|
113
|
+
ctrl = stream[pos]
|
|
114
|
+
pos += 1
|
|
115
|
+
for nibble in (ctrl & 0xF, ctrl >> 4):
|
|
116
|
+
width = _width(nibble)
|
|
117
|
+
if pos + width > n:
|
|
118
|
+
raise CodecError("residual overruns the buffer")
|
|
119
|
+
resid = int.from_bytes(stream[pos : pos + width] + b"\x00" * (8 - width), "little")
|
|
120
|
+
pos += width
|
|
121
|
+
pred = (last + dfcm.get(dh, 0)) & _MASK if nibble & 8 else fcm.get(fh, 0)
|
|
122
|
+
val = pred ^ resid
|
|
123
|
+
out.append(val)
|
|
124
|
+
stride = (val - last) & _MASK
|
|
125
|
+
fcm[fh] = val
|
|
126
|
+
dfcm[dh] = stride
|
|
127
|
+
fh = ((fh << _FCM_SHIFT) ^ (val >> _FCM_DROP)) & _TABLE_MASK
|
|
128
|
+
dh = ((dh << _DFCM_SHIFT) ^ (stride >> _DFCM_DROP)) & _TABLE_MASK
|
|
129
|
+
last = val
|
|
130
|
+
if len(out) >= nrows:
|
|
131
|
+
break
|
|
132
|
+
vals = np.frombuffer(np.asarray(out, dtype="<u8").tobytes(), dtype="<f8").copy()
|
|
133
|
+
vals[vals == ORIGIN_MISSING] = np.nan # empty cells → NaN
|
|
134
|
+
return vals, pos
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def decode_stream(stream: bytes, nrows: int) -> NDArray[np.float64]:
|
|
138
|
+
"""Decode ``nrows`` float64 values from one FPC control+residual stream."""
|
|
139
|
+
return _decode(stream, nrows)[0]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _read_varint(b: bytes, p: int) -> tuple[int, int]:
|
|
143
|
+
"""Read one LEB128 varint at ``p``; return ``(value, next_pos)``."""
|
|
144
|
+
val = shift = 0
|
|
145
|
+
while p < len(b):
|
|
146
|
+
byte = b[p]
|
|
147
|
+
p += 1
|
|
148
|
+
val |= (byte & 0x7F) << shift
|
|
149
|
+
shift += 7
|
|
150
|
+
if not byte & 0x80:
|
|
151
|
+
return val, p
|
|
152
|
+
raise CodecError("varint ran off the buffer")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _zigzag(v: int) -> int:
|
|
156
|
+
"""ZigZag-decode a varint (0,1,2,3,… → 0,−1,1,−2,…)."""
|
|
157
|
+
return (v >> 1) ^ -(v & 1)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Repeat-run value tags: tag byte → payload length. The payload holds the
|
|
161
|
+
# float64's TOP bytes (rest zero) — the compact forms cover round values.
|
|
162
|
+
_TAG_LEN = {0x50: 8, 0x1A: 2, 0x11: 1, 0x64: 0}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _decode_record(b: bytes, ff: int) -> tuple[NDArray[np.float64], int] | None:
|
|
166
|
+
"""Parse *and* decode the column record whose ``ff ff`` sits at ``ff``.
|
|
167
|
+
|
|
168
|
+
Grammar (validated against Origin's own CSV dumps): ``ff ff
|
|
169
|
+
<nrows:varint> 00`` then a segment list. Each segment opens with a ZigZag
|
|
170
|
+
varint:
|
|
171
|
+
|
|
172
|
+
* **−m** — m FPC-coded rows; ``0x0c`` and the stream follow *inline*, and
|
|
173
|
+
the predictor state starts FRESH for every stream (the plain column is
|
|
174
|
+
the one-segment case — the old "2·nrows−1 size-ish field" was really
|
|
175
|
+
``zigzag(−nrows)``);
|
|
176
|
+
* **+k** — k rows of one repeated value; a value-spec tag follows:
|
|
177
|
+
``0x50`` + float64, ``0x1a``/``0x11`` + the double's top 2/1 bytes
|
|
178
|
+
(rest zero — round values like 1.0, 2.0, 5.0), or bare ``0x64`` = 0.0.
|
|
179
|
+
|
|
180
|
+
Segments interleave freely (staircase logger columns alternate hold-runs
|
|
181
|
+
and FPC bursts); a fully constant column is one repeat segment with no
|
|
182
|
+
stream at all. Parsing and decoding fuse because an inline stream's byte
|
|
183
|
+
length is only known by decoding it. Returns ``(values, end)`` with
|
|
184
|
+
``end`` just past the record's last byte, or None for anything that
|
|
185
|
+
doesn't sum exactly to ``nrows`` — dropped, never guessed at.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
nrows, p = _read_varint(b, ff + 2)
|
|
189
|
+
except CodecError:
|
|
190
|
+
return None
|
|
191
|
+
if not 2 <= nrows <= 50_000_000:
|
|
192
|
+
return None
|
|
193
|
+
if p >= len(b) or b[p] != 0x00:
|
|
194
|
+
return None
|
|
195
|
+
p += 1
|
|
196
|
+
parts: list[NDArray[np.float64]] = []
|
|
197
|
+
total = 0
|
|
198
|
+
while total < nrows: # each iteration adds ≥1 row, so this terminates
|
|
199
|
+
try:
|
|
200
|
+
raw, p = _read_varint(b, p)
|
|
201
|
+
except CodecError:
|
|
202
|
+
return None
|
|
203
|
+
count = _zigzag(raw)
|
|
204
|
+
if count > 0:
|
|
205
|
+
if total + count > nrows or p >= len(b):
|
|
206
|
+
return None
|
|
207
|
+
plen = _TAG_LEN.get(b[p])
|
|
208
|
+
if plen is None or p + 1 + plen > len(b):
|
|
209
|
+
return None
|
|
210
|
+
value = struct.unpack("<d", b"\x00" * (8 - plen) + b[p + 1 : p + 1 + plen])[0]
|
|
211
|
+
p += 1 + plen
|
|
212
|
+
parts.append(np.full(count, value, dtype=float))
|
|
213
|
+
total += count
|
|
214
|
+
elif count < 0:
|
|
215
|
+
m = -count
|
|
216
|
+
if total + m > nrows or p >= len(b) or b[p] != 0x0C:
|
|
217
|
+
return None
|
|
218
|
+
try:
|
|
219
|
+
vals, consumed = _decode(b[p + 1 : p + 1 + m * 9 + 16], m)
|
|
220
|
+
except CodecError:
|
|
221
|
+
return None
|
|
222
|
+
parts.append(vals)
|
|
223
|
+
p += 1 + consumed
|
|
224
|
+
total += m
|
|
225
|
+
else:
|
|
226
|
+
return None
|
|
227
|
+
return np.concatenate(parts), p
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _records(b: bytes) -> list[int]:
|
|
231
|
+
"""Candidate record markers (``ff ff`` positions), in file order.
|
|
232
|
+
|
|
233
|
+
``0xff 0xff`` also occurs *inside* FPC residual data, so this over-reports;
|
|
234
|
+
:func:`scan_columns` walks the list with a cursor that jumps past each
|
|
235
|
+
decoded record, so the false in-stream markers are skipped.
|
|
236
|
+
"""
|
|
237
|
+
out: list[int] = []
|
|
238
|
+
ff = b.find(b"\xff\xff")
|
|
239
|
+
while ff >= 0:
|
|
240
|
+
# The header opens with `0a 05 <varint>` before ff ff. The varint is
|
|
241
|
+
# LEB128-shaped and GROWS with the record (a 120k-row column stores
|
|
242
|
+
# `c0 a9 07` = 120000, three bytes — the old fixed 1-2-byte gate
|
|
243
|
+
# silently rejected every large column's record; 2026-07-06 audit
|
|
244
|
+
# item #16, confirmed by the bigcolumn.opju specimen). Accept any
|
|
245
|
+
# 1..5-byte span that is a WELL-FORMED varint (continuation bytes
|
|
246
|
+
# have the high bit set, the final byte does not) — a structural
|
|
247
|
+
# check, not a size cap.
|
|
248
|
+
for k in (3, 4, 5, 6, 7):
|
|
249
|
+
if ff - k < 0 or b[ff - k] != 0x0A or b[ff - k + 1] != 0x05:
|
|
250
|
+
continue
|
|
251
|
+
varint = b[ff - k + 2 : ff]
|
|
252
|
+
if varint and all(v & 0x80 for v in varint[:-1]) and not varint[-1] & 0x80:
|
|
253
|
+
out.append(ff)
|
|
254
|
+
break
|
|
255
|
+
ff = b.find(b"\xff\xff", ff + 1)
|
|
256
|
+
return out
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _plausible(vals: NDArray[np.float64]) -> bool:
|
|
260
|
+
"""Reject any column that shows a decode desync.
|
|
261
|
+
|
|
262
|
+
A wrong predictor shatters the float exponent field the same way a
|
|
263
|
+
non-double payload does, so the shared gate applies; all-NaN additionally
|
|
264
|
+
rejects here because a mis-located ``.opju`` record never legitimately
|
|
265
|
+
decodes to nothing but sentinels.
|
|
266
|
+
"""
|
|
267
|
+
return plausible_column(vals, allow_all_nan=False)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def scan_columns(b: bytes) -> list[tuple[str, NDArray[np.float64]]]:
|
|
271
|
+
"""Decode every ``<Book>_<Col>`` numeric column in a CPYUA ``.opju`` file.
|
|
272
|
+
|
|
273
|
+
Records are walked in file order with a cursor: each decoded stream advances
|
|
274
|
+
the cursor past its own bytes, so the false ``ff ff`` markers buried in long
|
|
275
|
+
residual streams are skipped. Every real record is labelled by the nearest
|
|
276
|
+
preceding length-prefixed dataset name (validated on the local corpus: the
|
|
277
|
+
owning ``<Book>_<Col>`` name always leads its record). Streams that fail the
|
|
278
|
+
codec or decode to garbage are dropped — never emitted.
|
|
279
|
+
"""
|
|
280
|
+
names = [
|
|
281
|
+
(m.start(), m.group(0).decode("latin1"))
|
|
282
|
+
for m in _NAME.finditer(b)
|
|
283
|
+
if m.start() > 0 and b[m.start() - 1] == len(m.group(0))
|
|
284
|
+
]
|
|
285
|
+
out: list[tuple[str, NDArray[np.float64]]] = []
|
|
286
|
+
cursor = 0
|
|
287
|
+
for marker in _records(b):
|
|
288
|
+
if marker < cursor: # a false marker inside an already-decoded record
|
|
289
|
+
continue
|
|
290
|
+
got = _decode_record(b, marker)
|
|
291
|
+
if got is None:
|
|
292
|
+
continue
|
|
293
|
+
vals, end = got
|
|
294
|
+
if not _plausible(vals):
|
|
295
|
+
continue
|
|
296
|
+
prev = [name for pos, name in names if pos < marker]
|
|
297
|
+
if prev:
|
|
298
|
+
out.append((prev[-1], vals))
|
|
299
|
+
cursor = end
|
|
300
|
+
return out
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def tail_start(b: bytes) -> int:
|
|
304
|
+
"""Byte offset just past the last decoded worksheet-data record.
|
|
305
|
+
|
|
306
|
+
Everything before this point is the datasets section (FPC streams, whose
|
|
307
|
+
residual bytes routinely contain byte pairs that coincidentally match the
|
|
308
|
+
windows-section designation markers). ``windows_opju`` uses this to bound
|
|
309
|
+
its marker search to the tail region, avoiding those false positives.
|
|
310
|
+
"""
|
|
311
|
+
end = 0
|
|
312
|
+
cursor = 0
|
|
313
|
+
for marker in _records(b):
|
|
314
|
+
if marker < cursor: # a false marker inside an already-decoded record
|
|
315
|
+
continue
|
|
316
|
+
got = _decode_record(b, marker)
|
|
317
|
+
if got is None:
|
|
318
|
+
continue
|
|
319
|
+
cursor = got[1]
|
|
320
|
+
end = max(end, cursor, marker)
|
|
321
|
+
return end
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ── curve plot-style (item 35 follow-on: line vs scatter) ─────────────────────
|
|
325
|
+
#
|
|
326
|
+
# A compact-int property tag inside a CPYUA curve's own object body, found at
|
|
327
|
+
# a variable forward offset (22-44 bytes past the curve/column token's start
|
|
328
|
+
# -- never a fixed offset; see ``opju_curves.py``'s ``_CURVE_RE``/``_extract_
|
|
329
|
+
# curves_0x03`` and ``opju_curves_allcols.py``'s ``extract_curves_allcols``,
|
|
330
|
+
# both of which locate a curve token first and then call this on its start).
|
|
331
|
+
# Validated 135/147 real-corpus curve tokens, 0 disagreements, and 4/4 on the
|
|
332
|
+
# ``fig_pairs`` by-construction oracle (scatter/scatter/scatter/line, matching
|
|
333
|
+
# LabTalk's ``plot:=201``/``plot:=200``). Lives here, not in ``opju_curves.py``,
|
|
334
|
+
# because ``opju_curves_allcols.py`` needs it too and already sits *beneath*
|
|
335
|
+
# ``opju_curves.py`` in the import graph (the latter imports the former) --
|
|
336
|
+
# importing it the other way round would be circular. ``opju_codec`` is the
|
|
337
|
+
# lowest-level module both curve modules already import (``scan_columns``/
|
|
338
|
+
# ``_NAME``), so it carries no new dependency edge.
|
|
339
|
+
_STYLE_RE = re.compile(rb"\x8f\x01(.)\x83")
|
|
340
|
+
_STYLE_WINDOW = 400 # backstop only; the next-token bound below is the gate
|
|
341
|
+
_STYLE_BYTES = {0xC8: "line", 0xC9: "scatter"}
|
|
342
|
+
# The two curve-token shapes (opju_figure_curves' unified id token and
|
|
343
|
+
# opju_curves' 0x03-family token): the style search must STOP at the next
|
|
344
|
+
# curve token so a curve with no style tag of its own can never pick up its
|
|
345
|
+
# NEIGHBOR's tag. The old fixed 400-byte window was sized against the
|
|
346
|
+
# corpus-wide minimum inter-token gap (697 B) — corpus-shaped: a file packing
|
|
347
|
+
# curve objects closer than 400 B aliased the next curve's style (2026-07-06
|
|
348
|
+
# genericity audit). A false positive of this pattern only SHRINKS the search
|
|
349
|
+
# window (worst case an honest ``None``), never widens it.
|
|
350
|
+
_NEXT_TOKEN_RE = re.compile(rb"\x01\x01\x01\x80[\x01\x03]|\x01.\x01\x80\x03.\x00", re.DOTALL)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def curve_plot_style(b: bytes, token_start: int) -> str | None:
|
|
354
|
+
"""Plot style ("line"/"scatter") of the curve token starting at
|
|
355
|
+
``token_start``, or ``None`` when no ``8f 01 <style> 83`` tag is found
|
|
356
|
+
before the next curve token (or the 400-byte backstop), or the style
|
|
357
|
+
byte is unrecognized.
|
|
358
|
+
|
|
359
|
+
~8% of real-corpus curve tokens (composite-window duplicate references)
|
|
360
|
+
legitimately have no tag in range -- this returns ``None`` for those
|
|
361
|
+
rather than fabricating a default.
|
|
362
|
+
"""
|
|
363
|
+
hi = min(len(b), token_start + _STYLE_WINDOW)
|
|
364
|
+
nxt = _NEXT_TOKEN_RE.search(b, token_start + 8, hi)
|
|
365
|
+
if nxt is not None:
|
|
366
|
+
hi = nxt.start()
|
|
367
|
+
m = _STYLE_RE.search(b, token_start, hi)
|
|
368
|
+
if m is None:
|
|
369
|
+
return None
|
|
370
|
+
return _STYLE_BYTES.get(m.group(1)[0])
|