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,156 @@
|
|
|
1
|
+
"""Write Origin ``.opj`` (CPYA) projects from DataStructs (plan item 24/34).
|
|
2
|
+
|
|
3
|
+
Origin >= 2023 dropped *writing* ``.opj`` but still *reads* it, so one CPYA
|
|
4
|
+
writer reaches every Origin version — the "hand a file back to Origin
|
|
5
|
+
colleagues" path. The emitted container follows the loader model pinned by
|
|
6
|
+
the 2026-07-07 PN/PQ COM probe series (``docs/origin_re/validation_log.md``
|
|
7
|
+
+ ``writer_blocks.py``): stream = header line + fh block + per-column
|
|
8
|
+
``[NULL][147B header][data]`` triples + ``NULL NULL`` + per-book window
|
|
9
|
+
sections + ``NULL NULL NULL``; then the full tail (params, project record,
|
|
10
|
+
the loader-required ``ResultsLog`` note, folder tree, 8-record global
|
|
11
|
+
storage) and the file-size u32 patched into fh offset 115.
|
|
12
|
+
|
|
13
|
+
Round-trip through :func:`quantized.io.origin_project.read_origin_books` is
|
|
14
|
+
CI-tested; loading in REAL Origin (COM ``app.Load``) is verified live per
|
|
15
|
+
license window (validation log) — the probe series this layout ships from
|
|
16
|
+
loaded ``True`` on Origin 2026b.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import struct
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from quantized.datastruct import DataStruct
|
|
27
|
+
from quantized.io.origin_project import writer_blocks as wb
|
|
28
|
+
from quantized.io.origin_project.container import ORIGIN_MISSING
|
|
29
|
+
|
|
30
|
+
__all__ = ["opj_bytes", "write_opj"]
|
|
31
|
+
|
|
32
|
+
_HEADER_LINE = b"CPYA 4.3380 188 W64 #\n"
|
|
33
|
+
_DESIGNATION_CODE = {"Y": 0, "disregard": 1, "Y-error": 2, "X": 3, "label": 4, "Z": 5, "X-error": 6}
|
|
34
|
+
_NULL = struct.pack("<I", 0) + b"\n"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _col_short(i: int) -> str:
|
|
38
|
+
"""0-based column index -> Origin short name, bijective base-26
|
|
39
|
+
(A..Z, AA, AB, …) — the same lettering the readers decode. (The old
|
|
40
|
+
A..Z-only table made the writer reject >26-column books that the read
|
|
41
|
+
path handles fine; 2026-07-06 genericity audit.)"""
|
|
42
|
+
out = ""
|
|
43
|
+
i += 1
|
|
44
|
+
while i:
|
|
45
|
+
i, rem = divmod(i - 1, 26)
|
|
46
|
+
out = chr(ord("A") + rem) + out
|
|
47
|
+
return out
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _block(payload: bytes) -> bytes:
|
|
51
|
+
return struct.pack("<I", len(payload)) + b"\n" + payload + b"\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _data_block(values: np.ndarray) -> bytes:
|
|
55
|
+
vals = np.asarray(values, dtype="<f8").copy()
|
|
56
|
+
vals[np.isnan(vals)] = ORIGIN_MISSING
|
|
57
|
+
rows = np.zeros((len(vals), 10), dtype=np.uint8)
|
|
58
|
+
rows[:, 2:] = vals.view(np.uint8).reshape(len(vals), 8)
|
|
59
|
+
return _block(rows.tobytes())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _label_block(long_name: str, unit: str, comment: str) -> bytes:
|
|
63
|
+
text = "\r\n".join([long_name, unit, comment]).rstrip("\r\n") or " "
|
|
64
|
+
return _block(text.encode("latin1", errors="replace") + b"\x00")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _book_name(ds: DataStruct, index: int, used: set[str]) -> tuple[str, str]:
|
|
68
|
+
"""(short, long) book names: short must be latin-1 word-ish and unique."""
|
|
69
|
+
raw = str(ds.metadata.get("origin_book", "") or f"Book{index + 1}")
|
|
70
|
+
# LabTalk/dataset names must stay ASCII word characters (the reader's
|
|
71
|
+
# NAME_RE contract): strip everything else, including non-Latin scripts.
|
|
72
|
+
short = "".join(c for c in raw if c.isascii() and (c.isalnum() or c == " ")).strip()
|
|
73
|
+
short = short or f"Book{index + 1}"
|
|
74
|
+
base = short
|
|
75
|
+
n = 1
|
|
76
|
+
while short in used:
|
|
77
|
+
n += 1
|
|
78
|
+
short = f"{base}{n}"
|
|
79
|
+
used.add(short)
|
|
80
|
+
long_name = str(ds.metadata.get("origin_book_long", "") or short)
|
|
81
|
+
return short, long_name
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def opj_bytes(books: list[DataStruct]) -> bytes:
|
|
85
|
+
"""Serialize DataStructs as a CPYA ``.opj`` project (one workbook each).
|
|
86
|
+
|
|
87
|
+
Column 0 of every book is the DataStruct's ``time`` (designation X, named
|
|
88
|
+
by ``x_column_long``/``x_unit`` metadata when present); the value columns
|
|
89
|
+
follow with their labels/units. Short designations run A, B, C, …
|
|
90
|
+
"""
|
|
91
|
+
if not books:
|
|
92
|
+
raise ValueError("opj_bytes needs at least one DataStruct")
|
|
93
|
+
out = bytearray(_HEADER_LINE)
|
|
94
|
+
out += _block(wb.FH_123)
|
|
95
|
+
used: set[str] = set()
|
|
96
|
+
named: list[tuple[str, str, DataStruct, list[tuple[str, str, str, str]]]] = []
|
|
97
|
+
serial = 0 # global column counter: 147B header id + 519B property serial
|
|
98
|
+
|
|
99
|
+
for i, ds in enumerate(books):
|
|
100
|
+
short_book, long_book = _book_name(ds, i, used)
|
|
101
|
+
cols: list[tuple[str, str, str, str]] = [] # (short, long, unit, designation)
|
|
102
|
+
cols.append(
|
|
103
|
+
(
|
|
104
|
+
_col_short(0),
|
|
105
|
+
str(ds.metadata.get("x_column_long", "") or ""),
|
|
106
|
+
str(ds.metadata.get("x_unit", "") or ""),
|
|
107
|
+
"X",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
for j in range(ds.values.shape[1]):
|
|
111
|
+
label = ds.labels[j] if j < len(ds.labels) else ""
|
|
112
|
+
unit = ds.units[j] if j < len(ds.units) else ""
|
|
113
|
+
cols.append((_col_short(j + 1), str(label), str(unit), "Y"))
|
|
114
|
+
named.append((short_book, long_book, ds, cols))
|
|
115
|
+
|
|
116
|
+
# datasets section: [NULL][147B column header][data] per column
|
|
117
|
+
arrays = [np.asarray(ds.time, dtype=float)] + [
|
|
118
|
+
np.asarray(ds.values[:, j], dtype=float) for j in range(ds.values.shape[1])
|
|
119
|
+
]
|
|
120
|
+
for (short, _lng, _unit, _desig), arr in zip(cols, arrays, strict=True):
|
|
121
|
+
serial += 1
|
|
122
|
+
out += _NULL
|
|
123
|
+
out += _block(wb.col_header(f"{short_book}_{short}", len(arr), serial))
|
|
124
|
+
out += _data_block(arr)
|
|
125
|
+
|
|
126
|
+
out += _NULL * 2 # datasets / windows section separator
|
|
127
|
+
|
|
128
|
+
# windows section: per book a window section; a WORKSHEET section is
|
|
129
|
+
# followed by 6 NULLs before the next section (graph sections use 2 —
|
|
130
|
+
# measured across every Moke window; with 2 the loader folds the next
|
|
131
|
+
# book into the previous one: PW1 probe, books=1 -> 2)
|
|
132
|
+
serial = 0
|
|
133
|
+
for k, (short_book, long_book, ds, cols) in enumerate(named):
|
|
134
|
+
if k:
|
|
135
|
+
out += _NULL * 6
|
|
136
|
+
out += _block(wb.window_header(short_book, long_book))
|
|
137
|
+
out += _block(wb.sheet_subheader(len(ds.time)))
|
|
138
|
+
out += wb.sheet_storage_group()
|
|
139
|
+
out += _NULL # closes the record-group run (real files: group NULL + one)
|
|
140
|
+
x_serial = serial + 1 # the book's X column (column A) comes first
|
|
141
|
+
for short, long_name, unit, desig in cols:
|
|
142
|
+
serial += 1
|
|
143
|
+
out += _block(
|
|
144
|
+
wb.prop_block(serial, short, _DESIGNATION_CODE.get(desig, 0), x_serial)
|
|
145
|
+
)
|
|
146
|
+
out += _label_block(long_name, unit, "")
|
|
147
|
+
out += _NULL * 3 # closes the windows section (stream end)
|
|
148
|
+
|
|
149
|
+
out += wb.tail(len(named))
|
|
150
|
+
struct.pack_into("<I", out, wb.fh_size_offset(_HEADER_LINE), len(out))
|
|
151
|
+
return bytes(out)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def write_opj(books: list[DataStruct], path: Path) -> None:
|
|
155
|
+
"""Write ``books`` to ``path`` as an Origin ``.opj`` project."""
|
|
156
|
+
Path(path).write_bytes(opj_bytes(books))
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Template blocks + tail builders for the native ``.opj`` writer (item 34).
|
|
2
|
+
|
|
3
|
+
Origin's ``.opj`` loader is two-phase (``docs/origin_re/validation_log.md``):
|
|
4
|
+
phase 1 parses the block stream sequentially and builds pages; phase 2
|
|
5
|
+
requires the post-stream tail to parse to the very last byte. The 2026-07-07
|
|
6
|
+
PN probe series pinned the full requirement set — a file loads in real
|
|
7
|
+
Origin iff it has:
|
|
8
|
+
|
|
9
|
+
1. the PR3 **stream grammar**: header line, 123-byte fh block, per column
|
|
10
|
+
``[NULL][147B column header][data block]``, ``NULL NULL``, per book a
|
|
11
|
+
window section (window header + sheet sub-header + per-column property
|
|
12
|
+
+ label blocks), closed by ``NULL NULL NULL``;
|
|
13
|
+
2. a **tail**: params section, NULL + project record, a note list that
|
|
14
|
+
CONTAINS a ``ResultsLog`` note (presence required, content free — PN4),
|
|
15
|
+
NULL, the ``37 + len(tree)`` scalar, the constant 16-byte id blob, the
|
|
16
|
+
folder tree, and a global-storage section of exactly 8 indexed records
|
|
17
|
+
(content lax — empty slots + the three constant records suffice, PN1);
|
|
18
|
+
3. the **file-size u32 at fh offset 115**.
|
|
19
|
+
|
|
20
|
+
Everything else measured lax: storage-record content (even dialog XML
|
|
21
|
+
referencing windows the stream lacks — PN3), tree window ordinals, fh's
|
|
22
|
+
seven ``rand()``-like u32s.
|
|
23
|
+
|
|
24
|
+
The byte templates below were extracted once from the local ``Moke.opj``
|
|
25
|
+
corpus specimen and SANITIZED — every name / count / id field is zeroed
|
|
26
|
+
here and patched per write by the builders. Format facts are clean-room
|
|
27
|
+
(``docs/origin_project_format.md``); no GPL code consulted.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import struct
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"col_header",
|
|
36
|
+
"fh_size_offset",
|
|
37
|
+
"make_block",
|
|
38
|
+
"make_null",
|
|
39
|
+
"prop_block",
|
|
40
|
+
"sheet_subheader",
|
|
41
|
+
"tail",
|
|
42
|
+
"window_header",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def make_block(payload: bytes) -> bytes:
|
|
47
|
+
return struct.pack("<I", len(payload)) + b"\n" + payload + b"\n"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def make_null() -> bytes:
|
|
51
|
+
return struct.pack("<I", 0) + b"\n"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- sanitized templates (Moke.opj, CPYA 4.3380 / Origin 9.7) --------------
|
|
55
|
+
|
|
56
|
+
FH_123 = bytes.fromhex(
|
|
57
|
+
"0200350bf5ff8b1623060000000059000000000000000001000000711aa20a7f662340000020"
|
|
58
|
+
"0801000000ba6300003415000098200000e23f0000d47a000000280000254d00000000000000"
|
|
59
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000900000"
|
|
60
|
+
"000000000000000000"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_COL_147 = bytes.fromhex(
|
|
64
|
+
"00000100000000000000a00f010000000010000000002151030000000000000000a100000000"
|
|
65
|
+
"0000000000f03f000000000000f03f00000000500000000a0000000000000000000000000000"
|
|
66
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
67
|
+
"000003000000000000000000000000000000000000000000000000000000000000"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
_WIN_197 = bytes.fromhex(
|
|
71
|
+
"000000000000000000000000000000000000000000000000000000030018023e092d05800738"
|
|
72
|
+
"040000000000000013001300500000000020000000000000050004000211114f524947494e00"
|
|
73
|
+
"0000002c01460000640000580258020000000000000000000000000100130000000000c30000"
|
|
74
|
+
"006cc116e19ac14241efeeeef49ac1424100000000c000000058025802ffffffff0000000011"
|
|
75
|
+
"000000fcffffff00190000000000000000000000000000000000000000000000000000000000"
|
|
76
|
+
"00000000000000"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_SHEET_365 = bytes.fromhex(
|
|
80
|
+
"00005e00000000000000000000000000000000000000000000000000002840000000000000f0"
|
|
81
|
+
"3f080000000c0040010000000000000000010000000000000000000000000000000039400000"
|
|
82
|
+
"00000000f03f0000000006004001000000000000000001000010100001000000001600000081"
|
|
83
|
+
"0070000709bf02000000000000000020284c80000005010107000000000000f03f0000000000"
|
|
84
|
+
"0000000000000000000000001c00000312120000000080000000000000000000080000010000"
|
|
85
|
+
"00000ad7a33d0ad7a33d11001c00000000005064536865657431000000000000000000000000"
|
|
86
|
+
"00000000000000000000000000000000000000b80b00000200000000006003000012000000fc"
|
|
87
|
+
"ffffff000000000000000000000000fcffffff00000000fcffffff0000000006000000000000"
|
|
88
|
+
"000700000000000000000000007b14ae47e17ab43f0000000000000000000000000c00000000"
|
|
89
|
+
"00000000000000000000000000000000000000ffff0100"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_PROP_519 = bytes.fromhex(
|
|
93
|
+
"1000000000000b00000000a10000000000000000000000000000000000000000000000000021"
|
|
94
|
+
"5100000000000000000000000001000000000020000000000000000000000000000000006500"
|
|
95
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
96
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
97
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
98
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
99
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
100
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
101
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
102
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
103
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
104
|
+
"0000000000000000000000000000000000000000000000000000000000000000000000000000"
|
|
105
|
+
"00000000000000000000000000000000f03f000000000000f03f4b1100000000000000000000"
|
|
106
|
+
"00000000000000000000000000000000000000000000000000"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
_PARAMS = bytes.fromhex(
|
|
110
|
+
"494d474558500a00000000000000000a41584953545950450a00000000000000000a70616765"
|
|
111
|
+
"5f6e6f636c69636b0a00000000000000000a000a"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
_PROJREC_88 = bytes.fromhex(
|
|
115
|
+
"0000000000000000000000000000000000000000000000000000000000000000571e907ac3c2"
|
|
116
|
+
"4241571e907ac3c2424100000000000000000000000000000000000000000000000000000000"
|
|
117
|
+
"000000000000000000000000"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
_TREE_HDR32 = bytes.fromhex(
|
|
121
|
+
"000000000000000000000000000000001cd232de9ac14241571e907ac3c24241"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
_TREE_ATTRS = bytes.fromhex(
|
|
125
|
+
"471111110100000000000000000000000000000000000000000000000000000000000000"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
_TREE_FSTORAGE = bytes.fromhex(
|
|
129
|
+
"4d11111101000000000000004300000040247b5b307c347c466f6c6465724c61737455736564"
|
|
130
|
+
"7c33317c38343232313934395d7d3c4f726967696e53746f726167653e3c2f4f726967696e53"
|
|
131
|
+
"746f726167653e000000000000000000000000"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# The __LayerInfoStorage record group (133B '#' header + 72B descriptor +
|
|
135
|
+
# 58B owned-storage marker), byte-identical across every corpus workbook.
|
|
136
|
+
# Origin's loader requires a window section to carry AT LEAST ONE
|
|
137
|
+
# ``133B/72B/content`` record group (PJ1 with none -> load refused; PK1 with
|
|
138
|
+
# only this one -> loads); this is the smallest invariant one.
|
|
139
|
+
_GRP_133 = bytes.fromhex(
|
|
140
|
+
"0000230000000064006400000000000000000000000000000000000000000000000000480000"
|
|
141
|
+
"000001013e003e00c2ffc2ff00010000000000000000000000100000000000005f5f4c617965"
|
|
142
|
+
"72496e666f53746f726167650000000000000000000037000000000000000000000000000028"
|
|
143
|
+
"01000000000000000000000000000000000000"
|
|
144
|
+
)
|
|
145
|
+
_GRP_72 = bytes.fromhex(
|
|
146
|
+
"08d0053c073a00000064000000000000000000000000000000d00500003c0700000000480000"
|
|
147
|
+
"000001013e003e00c2ffc2ff00010000000000000000000000100000000000005f5f"
|
|
148
|
+
)
|
|
149
|
+
_GRP_58 = bytes.fromhex(
|
|
150
|
+
"40247b5b307c347c5f53746f726167655f4562646465645f70616765735f446174615f7c387c"
|
|
151
|
+
"3833343535363236335d7d5c305c305c305c3000"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def sheet_storage_group() -> bytes:
|
|
156
|
+
"""The framed ``__LayerInfoStorage`` record group + its closing NULL —
|
|
157
|
+
the loader-required minimum of the per-sheet record-group run."""
|
|
158
|
+
return (
|
|
159
|
+
make_block(_GRP_133)
|
|
160
|
+
+ make_block(_GRP_72)
|
|
161
|
+
+ make_block(_GRP_58)
|
|
162
|
+
+ make_null()
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Global-storage records byte-identical across every corpus project
|
|
167
|
+
# (map_opj_tail2 sweep: Moke / XRD / MnN / SuperlatticeFits):
|
|
168
|
+
_STORAGE_IDX5 = bytes.fromhex("3e111111" + "01000000") + bytes(16)
|
|
169
|
+
_STORAGE_IDX6 = bytes(4)
|
|
170
|
+
_STORAGE_IDX7 = bytes.fromhex("5d111111" + "01000100") + bytes(24)
|
|
171
|
+
_ID_BLOB = bytes.fromhex("de361003") + bytes(12)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def fh_size_offset(header_line: bytes) -> int:
|
|
175
|
+
"""Absolute file offset of the fh block's file-size u32 (payload @115)."""
|
|
176
|
+
return len(header_line) + 5 + 115
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def col_header(dataset: str, nrows: int, col_id: int) -> bytes:
|
|
180
|
+
"""147-byte dataset column header: name @88, filled rows u32 @25,
|
|
181
|
+
allocated rows u32 @6, per-column id u16 @113."""
|
|
182
|
+
p = bytearray(_COL_147)
|
|
183
|
+
struct.pack_into("<I", p, 6, nrows)
|
|
184
|
+
struct.pack_into("<I", p, 25, nrows)
|
|
185
|
+
enc = dataset.encode("latin1", errors="replace")[:23]
|
|
186
|
+
p[88 : 88 + len(enc) + 1] = enc + b"\x00"
|
|
187
|
+
struct.pack_into("<H", p, 113, col_id & 0xFFFF)
|
|
188
|
+
return bytes(p)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def window_header(book: str, long_name: str = "") -> bytes:
|
|
192
|
+
"""Window header: short name @2; a display long name (when it differs)
|
|
193
|
+
rides the real files' owned-storage slot at payload offset 195, anchored
|
|
194
|
+
by the ``@${`` marker the reader decodes (197-byte minimal variant
|
|
195
|
+
otherwise — both shapes exist in the corpus)."""
|
|
196
|
+
p = bytearray(_WIN_197[:195])
|
|
197
|
+
enc = book.encode("latin1", errors="replace")[:24]
|
|
198
|
+
p[2 : 2 + len(enc) + 1] = enc + b"\x00"
|
|
199
|
+
if long_name and long_name != book:
|
|
200
|
+
p += long_name.encode("latin1", errors="replace") + b"@${[0|]}\x00"
|
|
201
|
+
else:
|
|
202
|
+
p += b"\x00\x00"
|
|
203
|
+
return bytes(p)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def sheet_subheader(nrows: int) -> bytes:
|
|
207
|
+
"""365-byte sheet sub-header (``Pd`` block, sheet name fixed 'Sheet1'):
|
|
208
|
+
row count u16 @82."""
|
|
209
|
+
p = bytearray(_SHEET_365)
|
|
210
|
+
struct.pack_into("<H", p, 82, min(nrows, 0xFFFF))
|
|
211
|
+
return bytes(p)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def prop_block(serial: int, short: str, designation: int, x_serial: int) -> bytes:
|
|
215
|
+
"""519-byte column property block — the column-association model measured
|
|
216
|
+
across every corpus workbook (61 columns, 6 books):
|
|
217
|
+
|
|
218
|
+
* @4 u16: the column's global dataset serial — Origin binds column <->
|
|
219
|
+
dataset by matching this against the dataset's 1-based ordinal in the
|
|
220
|
+
file's stream order (the PN/PT probe series' central finding);
|
|
221
|
+
* @30 u16: the constant 9 in every corpus column (semantics unknown —
|
|
222
|
+
cloned, never varied);
|
|
223
|
+
* @35 u16: the associated X column's serial for Y/Y-error columns,
|
|
224
|
+
0 for X/disregard columns themselves;
|
|
225
|
+
* @38: designation flag — X 0x51, Y/Y-error 0x61, disregard 0x41;
|
|
226
|
+
* @51: X-group index (1-based; the writer emits one X group per book);
|
|
227
|
+
* @0x11 designation code, @0x12 short name.
|
|
228
|
+
|
|
229
|
+
Getting @35/@38 wrong doesn't refuse the load — it silently unbinds the
|
|
230
|
+
DATA (columns render empty), the PU5 probe's failure mode.
|
|
231
|
+
"""
|
|
232
|
+
p = bytearray(_PROP_519)
|
|
233
|
+
struct.pack_into("<H", p, 4, serial & 0xFFFF)
|
|
234
|
+
struct.pack_into("<H", p, 30, 9)
|
|
235
|
+
is_x = designation in (1, 3) # X and disregard carry no X-association
|
|
236
|
+
struct.pack_into("<H", p, 35, 0 if is_x else x_serial & 0xFFFF)
|
|
237
|
+
p[38] = {3: 0x51, 1: 0x41}.get(designation, 0x61)
|
|
238
|
+
p[51] = 1
|
|
239
|
+
p[0x11] = designation
|
|
240
|
+
enc = short.encode("latin1", errors="replace")[:11]
|
|
241
|
+
p[0x12 : 0x12 + len(enc) + 1] = enc + b"\x00"
|
|
242
|
+
return bytes(p)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def tail(n_windows: int, root_name: str = "Project") -> bytes:
|
|
246
|
+
"""The full post-stream tail: params through the global-storage section.
|
|
247
|
+
|
|
248
|
+
The note list carries the loader-required ``ResultsLog`` note (PN4:
|
|
249
|
+
absence -> load refused); the tree is one root folder holding every
|
|
250
|
+
window by stream ordinal; storage is the minimal 8-record set (PN1).
|
|
251
|
+
"""
|
|
252
|
+
tree = bytearray()
|
|
253
|
+
tree += make_block(_TREE_HDR32) + make_null()
|
|
254
|
+
tree += make_block(root_name.encode("latin1", errors="replace") + b"\x00")
|
|
255
|
+
tree += struct.pack("<I", 2) + b"\n" # bare marker fragment (not a block)
|
|
256
|
+
tree += make_block(_TREE_ATTRS) + make_block(_TREE_FSTORAGE)
|
|
257
|
+
tree += make_block(struct.pack("<I", n_windows))
|
|
258
|
+
for ordinal in range(n_windows):
|
|
259
|
+
tree += make_null() + make_block(struct.pack("<II", 0, ordinal)) + make_null()
|
|
260
|
+
tree += make_block(struct.pack("<I", 0)) # no subfolders
|
|
261
|
+
|
|
262
|
+
out = bytearray()
|
|
263
|
+
out += _PARAMS
|
|
264
|
+
out += make_null() + make_block(_PROJREC_88)
|
|
265
|
+
out += make_block(b"ResultsLog") + make_block(b" ")
|
|
266
|
+
out += make_null() # note-list terminator
|
|
267
|
+
out += make_block(struct.pack("<I", 37 + len(tree))) + make_block(_ID_BLOB)
|
|
268
|
+
out += tree
|
|
269
|
+
out += make_null()
|
|
270
|
+
out += make_block(b"\x00\x10\x00\x00" + struct.pack("<I", 8))
|
|
271
|
+
slot = {5: _STORAGE_IDX5, 6: _STORAGE_IDX6, 7: _STORAGE_IDX7}
|
|
272
|
+
for idx in range(8):
|
|
273
|
+
data = slot.get(idx, b"")
|
|
274
|
+
out += (
|
|
275
|
+
b"\x00\x10\x00\x00"
|
|
276
|
+
+ struct.pack("<II", idx, len(data))
|
|
277
|
+
+ bytes(16)
|
|
278
|
+
+ b"\n"
|
|
279
|
+
+ data
|
|
280
|
+
+ b"\n"
|
|
281
|
+
)
|
|
282
|
+
return bytes(out)
|