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