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,194 @@
|
|
|
1
|
+
"""COM "Send to Origin" — push DataStructs into a RUNNING OriginPro instance.
|
|
2
|
+
|
|
3
|
+
Plan item 25 (Windows-only, optional, feature-flagged; architecture guard
|
|
4
|
+
#10): COM ("Send to Origin") is never a hard dependency and never a CI
|
|
5
|
+
requirement. Everywhere else (macOS/Linux, Origin absent, the flag off, or a
|
|
6
|
+
failed dispatch) callers fall back to the existing Origin-ASCII / ``.ogs``
|
|
7
|
+
export (``io/origin.py``, routes ``/api/export/origin`` and
|
|
8
|
+
``/api/export/origin-project``).
|
|
9
|
+
|
|
10
|
+
Gated behind the ``QZ_ORIGIN_COM=1`` environment variable so a stray pywin32
|
|
11
|
+
install on a Windows dev/CI box never silently starts dispatching COM.
|
|
12
|
+
``win32com`` is imported LAZILY inside the two public functions, never at
|
|
13
|
+
module scope, so this module imports cleanly on macOS/Linux and keeps the
|
|
14
|
+
``io/`` pure-layer + no-hard-dependency guarantees (enforced by
|
|
15
|
+
``tests/test_repo_integrity.py``).
|
|
16
|
+
|
|
17
|
+
Reuses the LabTalk/COM surface proven in ``tools/origin_trial/
|
|
18
|
+
generate_specimens.py`` (the only prior code in this repo to drive Origin's
|
|
19
|
+
COM API): a single ``Origin.ApplicationSI`` instance, ``newbook`` + LabTalk
|
|
20
|
+
``option:=lsname``, ``PutWorksheet(rows)`` for bulk numeric data (row-major,
|
|
21
|
+
time column first, then one column per value channel), and a single
|
|
22
|
+
``range ...; ....lname$=...; ....unit$=...;`` LabTalk ``Execute`` call per
|
|
23
|
+
column for long names/units — direct COM property assignment on worksheet
|
|
24
|
+
ranges is flaky in practice; LabTalk string commands are the reliable path
|
|
25
|
+
(see that script's docstring for the hard-won gotchas: only one live
|
|
26
|
+
``ApplicationSI`` instance at a time — a dead/killed server faults every
|
|
27
|
+
subsequent call).
|
|
28
|
+
|
|
29
|
+
Scope note: this pushes data + long names/units only (matches the plan-item
|
|
30
|
+
ask); it does not set column designations (X/Y/Y-error types) the way the
|
|
31
|
+
``.ogs`` exporter does — a deliberate smaller surface for the first cut,
|
|
32
|
+
worth revisiting once item 25 is live-verified against real Origin.
|
|
33
|
+
|
|
34
|
+
This repo's own tests never dispatch real COM — see ``tests/test_origin_com.py``
|
|
35
|
+
(mock-based only, a fake ``win32com`` module injected via ``sys.modules``).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
import re
|
|
42
|
+
import sys
|
|
43
|
+
from collections.abc import Sequence
|
|
44
|
+
from typing import Any
|
|
45
|
+
|
|
46
|
+
from quantized.datastruct import DataStruct
|
|
47
|
+
|
|
48
|
+
__all__ = ["com_available", "send_to_origin"]
|
|
49
|
+
|
|
50
|
+
_FLAG_ENV = "QZ_ORIGIN_COM"
|
|
51
|
+
_UNAVAILABLE_HINT = (
|
|
52
|
+
"Use the Origin-ASCII / .ogs export instead "
|
|
53
|
+
"(POST /api/export/origin or /api/export/origin-project)."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def com_available() -> bool:
|
|
58
|
+
"""True only on Windows, with ``QZ_ORIGIN_COM=1`` set, AND pywin32
|
|
59
|
+
importable. Never raises — every check is a soft probe, so callers
|
|
60
|
+
(routes, capability checks) can call this unconditionally on any platform.
|
|
61
|
+
"""
|
|
62
|
+
if sys.platform != "win32":
|
|
63
|
+
return False
|
|
64
|
+
if os.environ.get(_FLAG_ENV) != "1":
|
|
65
|
+
return False
|
|
66
|
+
try:
|
|
67
|
+
import win32com.client # noqa: F401 (lazy: optional Windows-only dep)
|
|
68
|
+
except ImportError:
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _escape_lt(text: str) -> str:
|
|
74
|
+
"""Make ``text`` safe inside a LabTalk string literal.
|
|
75
|
+
|
|
76
|
+
LabTalk has NO backslash escape — a ``\\"`` lands literally in Origin
|
|
77
|
+
(live-verified 2026-07-04, see docs/origin_re/validation_log.md), so
|
|
78
|
+
embedded double-quotes are downgraded to single quotes instead.
|
|
79
|
+
"""
|
|
80
|
+
return text.replace('"', "'")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _sanitize_book_name(name: str, index: int, used: set[str]) -> str:
|
|
84
|
+
"""A unique, LabTalk-legal workbook short name: non-word chars ->
|
|
85
|
+
underscore, never digit-leading, falls back to ``Book<N>`` when empty,
|
|
86
|
+
and de-duplicated against every name already handed out in this call."""
|
|
87
|
+
cleaned = re.sub(r"\W", "_", name).strip("_") or f"Book{index + 1}"
|
|
88
|
+
if cleaned[0].isdigit():
|
|
89
|
+
cleaned = f"B{cleaned}"
|
|
90
|
+
book, n = cleaned, 1
|
|
91
|
+
while book in used:
|
|
92
|
+
n += 1
|
|
93
|
+
book = f"{cleaned}{n}"
|
|
94
|
+
used.add(book)
|
|
95
|
+
return book
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _label_column(app: Any, book: str, col: int, lname: str, unit: str) -> None:
|
|
99
|
+
"""One LabTalk ``Execute`` assigning a column's long name + unit via a
|
|
100
|
+
``range`` reference — mirrors ``tools/origin_trial/generate_specimens.py``
|
|
101
|
+
(``range ra = [Book]1!col(1); ra.lname$="..."; ra.unit$="...";``)."""
|
|
102
|
+
var = f"__c{col}"
|
|
103
|
+
parts = [f"range {var} = [{book}]1!col({col});"]
|
|
104
|
+
if lname:
|
|
105
|
+
parts.append(f'{var}.lname$ = "{_escape_lt(lname)}";')
|
|
106
|
+
if unit:
|
|
107
|
+
parts.append(f'{var}.unit$ = "{_escape_lt(unit)}";')
|
|
108
|
+
if len(parts) == 1:
|
|
109
|
+
return # nothing to label
|
|
110
|
+
cmd = " ".join(parts)
|
|
111
|
+
if not app.Execute(cmd):
|
|
112
|
+
raise RuntimeError(f"Origin LabTalk command failed: {cmd}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def send_to_origin(
|
|
116
|
+
datasets: Sequence[DataStruct],
|
|
117
|
+
*,
|
|
118
|
+
book_names: Sequence[str] | None = None,
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
"""Push each ``DataStruct`` into a new workbook in a RUNNING Origin
|
|
121
|
+
instance via COM, with long names/units set from ``.labels``/``.units``.
|
|
122
|
+
|
|
123
|
+
``book_names[i]`` (if given and non-empty) names workbook ``i``;
|
|
124
|
+
otherwise falls back to ``metadata['origin_book']`` then ``Book<N>``.
|
|
125
|
+
Returns ``{"books": [<created workbook names>], "rows": [<row count per
|
|
126
|
+
book>]}``.
|
|
127
|
+
|
|
128
|
+
Raises ``RuntimeError`` (never a raw COM exception) when COM is
|
|
129
|
+
unavailable, Origin rejects a command, or dispatch fails — the caller
|
|
130
|
+
(the export route) maps that to an HTTP 409 pointing at the ``.ogs``
|
|
131
|
+
fallback.
|
|
132
|
+
"""
|
|
133
|
+
if not datasets:
|
|
134
|
+
raise ValueError("send_to_origin needs at least one dataset")
|
|
135
|
+
if not com_available():
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"Origin COM is unavailable on this machine (needs Windows, "
|
|
138
|
+
f"pywin32, {_FLAG_ENV}=1, and a running OriginPro instance). "
|
|
139
|
+
+ _UNAVAILABLE_HINT
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
import win32com.client as win32 # lazy: optional Windows-only dep
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
app = win32.gencache.EnsureDispatch("Origin.ApplicationSI")
|
|
146
|
+
except Exception as exc: # pragma: no cover - real COM only, never in CI
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
"Could not connect to a running Origin instance via COM. Start "
|
|
149
|
+
"OriginPro first, then retry. " + _UNAVAILABLE_HINT
|
|
150
|
+
) from exc
|
|
151
|
+
|
|
152
|
+
names = list(book_names) if book_names is not None else []
|
|
153
|
+
used: set[str] = set()
|
|
154
|
+
books: list[str] = []
|
|
155
|
+
rows_created: list[int] = []
|
|
156
|
+
try:
|
|
157
|
+
for i, ds in enumerate(datasets):
|
|
158
|
+
raw = names[i] if i < len(names) and names[i] else ""
|
|
159
|
+
if not raw:
|
|
160
|
+
raw = str(ds.metadata.get("origin_book", "")) or f"Book{i + 1}"
|
|
161
|
+
book = _sanitize_book_name(raw, i, used)
|
|
162
|
+
|
|
163
|
+
if not app.Execute(f"newbook name:={book} option:=lsname;"):
|
|
164
|
+
raise RuntimeError(f"Origin rejected 'newbook' for workbook {book!r}.")
|
|
165
|
+
|
|
166
|
+
cols = [ds.time.tolist()]
|
|
167
|
+
cols.extend(ds.values[:, c].tolist() for c in range(ds.n_channels))
|
|
168
|
+
rows = [list(r) for r in zip(*cols, strict=True)]
|
|
169
|
+
app.PutWorksheet(f"[{book}]1", rows, 0, 0)
|
|
170
|
+
|
|
171
|
+
x_name = str(
|
|
172
|
+
ds.metadata.get("x_column_name")
|
|
173
|
+
or ds.metadata.get("xColumnName")
|
|
174
|
+
or ds.metadata.get("x_column_long") # writer.py's key family
|
|
175
|
+
or "X"
|
|
176
|
+
)
|
|
177
|
+
x_unit = str(
|
|
178
|
+
ds.metadata.get("x_column_unit")
|
|
179
|
+
or ds.metadata.get("xColumnUnit")
|
|
180
|
+
or ds.metadata.get("x_unit")
|
|
181
|
+
or ""
|
|
182
|
+
)
|
|
183
|
+
_label_column(app, book, 1, x_name, x_unit)
|
|
184
|
+
for k, (label, unit) in enumerate(zip(ds.labels, ds.units, strict=True)):
|
|
185
|
+
_label_column(app, book, k + 2, label, unit)
|
|
186
|
+
|
|
187
|
+
books.append(book)
|
|
188
|
+
rows_created.append(ds.n_points)
|
|
189
|
+
except RuntimeError:
|
|
190
|
+
raise
|
|
191
|
+
except Exception as exc: # pragma: no cover - real COM only, never in CI
|
|
192
|
+
raise RuntimeError(f"Origin COM call failed: {exc}") from exc
|
|
193
|
+
|
|
194
|
+
return {"books": books, "rows": rows_created}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Origin project readers (``.opj`` / ``.opju``) — clean-room, no GPL liborigin.
|
|
2
|
+
|
|
3
|
+
Package layout (split per plan item 15 to stay under the module ceiling as
|
|
4
|
+
decoders grow): ``container`` (CPY block primitives), ``opj`` (worksheet data +
|
|
5
|
+
names/units), ``opju`` (FPC column codec, see ``opju_codec``), ``windows``
|
|
6
|
+
(windows-section metadata). Format notes: ``docs/origin_project_format.md`` +
|
|
7
|
+
``docs/origin_re/``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import dataclasses
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from quantized.datastruct import DataStruct
|
|
18
|
+
from quantized.io.origin_project.container import OriginProjectError
|
|
19
|
+
from quantized.io.origin_project.notes import notes_windows, parse_results_log, results_log
|
|
20
|
+
from quantized.io.origin_project.opj import (
|
|
21
|
+
build_opj_books,
|
|
22
|
+
build_opj_primary,
|
|
23
|
+
parse_opj,
|
|
24
|
+
read_opj,
|
|
25
|
+
read_opj_books,
|
|
26
|
+
)
|
|
27
|
+
from quantized.io.origin_project.opju import (
|
|
28
|
+
build_opju_books,
|
|
29
|
+
build_opju_primary,
|
|
30
|
+
parse_opju,
|
|
31
|
+
read_opju,
|
|
32
|
+
read_opju_books,
|
|
33
|
+
)
|
|
34
|
+
from quantized.io.origin_project.tree import opj_folder_paths, opj_project_dates
|
|
35
|
+
from quantized.io.origin_project.tree_opju import opju_folder_paths
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"OriginProjectError",
|
|
39
|
+
"drop_empty_library_books",
|
|
40
|
+
"drop_nonactionable_figures",
|
|
41
|
+
"read_origin_books",
|
|
42
|
+
"read_origin_project",
|
|
43
|
+
"read_origin_project_all",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _with_provenance(ds: DataStruct, path: Path, *, raw: bytes | None = None) -> DataStruct:
|
|
48
|
+
"""Attach project-global provenance (results log + notes) to one DataStruct.
|
|
49
|
+
|
|
50
|
+
Both are project-global, so they ride only the primary dataset (and the
|
|
51
|
+
first book of a multi-book read) rather than being duplicated per book.
|
|
52
|
+
The file is read once and both scans run over the same bytes (``raw``
|
|
53
|
+
lets a caller that already has the bytes, e.g. :func:`read_origin_books`,
|
|
54
|
+
skip a second read of the file). ``path``'s suffix lets both scans
|
|
55
|
+
restrict themselves to the structurally-derived tail on ``.opj`` (see
|
|
56
|
+
``notes.py``'s ``_tail_scan_start``) instead of the whole file.
|
|
57
|
+
"""
|
|
58
|
+
raw = path.read_bytes() if raw is None else raw
|
|
59
|
+
suffix = path.suffix.lower()
|
|
60
|
+
extra: dict[str, object] = {}
|
|
61
|
+
log = results_log(raw, suffix=suffix)
|
|
62
|
+
if log:
|
|
63
|
+
extra["origin_results_log"] = log
|
|
64
|
+
records = parse_results_log(log)
|
|
65
|
+
if records:
|
|
66
|
+
extra["origin_results_log_records"] = records
|
|
67
|
+
notes = notes_windows(raw, suffix=suffix)
|
|
68
|
+
if notes:
|
|
69
|
+
extra["origin_notes"] = notes
|
|
70
|
+
# .opj project created/modified timestamps (tree.py::opj_project_dates,
|
|
71
|
+
# §13.2 #5); None on the 4.3227 variant / CPYUA bytes — key then absent.
|
|
72
|
+
dates = opj_project_dates(raw)
|
|
73
|
+
if dates:
|
|
74
|
+
extra["origin_project_dates"] = dates
|
|
75
|
+
if not extra:
|
|
76
|
+
return ds
|
|
77
|
+
return dataclasses.replace(ds, metadata={**ds.metadata, **extra})
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_origin_project(path: Path) -> DataStruct:
|
|
81
|
+
"""Dispatch by extension to the clean-room ``.opj`` / ``.opju`` decoder.
|
|
82
|
+
|
|
83
|
+
Origin projects are proprietary binary files; quantized decodes them itself
|
|
84
|
+
(it will not bundle the GPL liborigin). Both containers recover worksheet
|
|
85
|
+
data with real column names/units; the project's results log (analysis
|
|
86
|
+
provenance) lands in ``metadata['origin_results_log']`` (raw text) and
|
|
87
|
+
``metadata['origin_results_log_records']`` (parsed per-operation records,
|
|
88
|
+
when at least one parses) and any notes-window text in
|
|
89
|
+
``metadata['origin_notes']`` when present.
|
|
90
|
+
"""
|
|
91
|
+
reader = read_opju if path.suffix.lower() == ".opju" else read_opj
|
|
92
|
+
return _with_provenance(reader(path), path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _with_folder_path(ds: DataStruct, folder_paths: dict[str, list[str]]) -> DataStruct:
|
|
96
|
+
"""Attach ``metadata['origin_folder_path']`` (the Project Explorer folder
|
|
97
|
+
a book lives in, root-exclusive; ``[]`` when unknown or at the root) --
|
|
98
|
+
see ``tree.py``. A ``Book4@2``-style sheet pseudo-book (plan item 5)
|
|
99
|
+
resolves through its base book's name, since sheets aren't separate
|
|
100
|
+
Project Explorer windows."""
|
|
101
|
+
book = str(ds.metadata.get("origin_book", ""))
|
|
102
|
+
base_book, _, _sheet = book.partition("@")
|
|
103
|
+
folder_path = folder_paths.get(base_book, [])
|
|
104
|
+
return dataclasses.replace(ds, metadata={**ds.metadata, "origin_folder_path": folder_path})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def read_origin_books(path: Path) -> list[DataStruct]:
|
|
108
|
+
"""Every workbook in an Origin project as its own DataStruct (plan item 3).
|
|
109
|
+
|
|
110
|
+
``read_origin_project`` keeps the registry's single-DataStruct contract
|
|
111
|
+
(largest book); this is the pure API a multi-dataset import flow (plan
|
|
112
|
+
item 16) builds on. The results log rides the first book only; every
|
|
113
|
+
book gets an ``origin_folder_path`` (plan item: Project Explorer folder
|
|
114
|
+
tree -- see ``tree.py``; decoded for ``.opj`` and both CPYUA ``.opju``
|
|
115
|
+
sub-versions (4.3811 + 4.3380), degrading to ``[]`` only on an unknown
|
|
116
|
+
container or a framing/consistency mismatch).
|
|
117
|
+
"""
|
|
118
|
+
is_opju = path.suffix.lower() == ".opju"
|
|
119
|
+
books = read_opju_books(path) if is_opju else read_opj_books(path)
|
|
120
|
+
if not books:
|
|
121
|
+
return books
|
|
122
|
+
raw = path.read_bytes()
|
|
123
|
+
books[0] = _with_provenance(books[0], path, raw=raw)
|
|
124
|
+
folder_paths = opju_folder_paths(raw) if is_opju else opj_folder_paths(raw)
|
|
125
|
+
return [_with_folder_path(b, folder_paths) for b in books]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def read_origin_project_all(
|
|
129
|
+
path: Path, *, raw: bytes | None = None
|
|
130
|
+
) -> tuple[DataStruct, list[DataStruct]]:
|
|
131
|
+
"""Parse an Origin project ONCE and return both ``read_origin_project``'s
|
|
132
|
+
primary DataStruct (with provenance) and ``read_origin_books``'s full
|
|
133
|
+
per-book list (with folder paths + book[0] provenance) — the combined
|
|
134
|
+
result of calling both entry points, without the redundant second
|
|
135
|
+
``parse_opj``/``parse_opju`` (and repeated file reads) each independently
|
|
136
|
+
performs today.
|
|
137
|
+
|
|
138
|
+
Used by the ``/api/parsers`` import route: profiling a 122-book /
|
|
139
|
+
8.5M-cell project (``PNR.opj``, 121.56 MB) showed the duplicated
|
|
140
|
+
project-wide parse dominating the ~4s round-trip (2026-07-09) — calling
|
|
141
|
+
``import_auto`` (-> ``read_origin_project`` -> a full parse) and then
|
|
142
|
+
``read_origin_books`` (-> another full parse) back to back reads the file
|
|
143
|
+
from disk repeatedly and decodes every column twice.
|
|
144
|
+
|
|
145
|
+
Primary-book construction (``build_opj*_primary``) and all-books
|
|
146
|
+
construction (``build_opj*_books``) stay independent calls sharing only
|
|
147
|
+
the parsed intermediate — mirroring ``read_opj``/``read_opj_books``'s
|
|
148
|
+
separation — so a decode issue confined to one non-primary book still
|
|
149
|
+
degrades the book list to ``[]`` without taking down the primary import,
|
|
150
|
+
exactly as ``routes.parsers._import_with_books`` behaved before (its
|
|
151
|
+
``except OriginProjectError`` around ``read_origin_books`` only ever
|
|
152
|
+
guarded against that isolated-book-build failure, since a shared-parse
|
|
153
|
+
failure would already have surfaced on the first, unguarded call).
|
|
154
|
+
"""
|
|
155
|
+
is_opju = path.suffix.lower() == ".opju"
|
|
156
|
+
raw = path.read_bytes() if raw is None else raw
|
|
157
|
+
|
|
158
|
+
books: list[DataStruct]
|
|
159
|
+
if is_opju:
|
|
160
|
+
opju_parsed = parse_opju(path, raw=raw)
|
|
161
|
+
primary = _with_provenance(build_opju_primary(opju_parsed), path, raw=raw)
|
|
162
|
+
try:
|
|
163
|
+
books = build_opju_books(opju_parsed)
|
|
164
|
+
except OriginProjectError:
|
|
165
|
+
books = []
|
|
166
|
+
else:
|
|
167
|
+
opj_parsed = parse_opj(path, raw=raw)
|
|
168
|
+
primary = _with_provenance(build_opj_primary(opj_parsed), path, raw=raw)
|
|
169
|
+
try:
|
|
170
|
+
books = build_opj_books(opj_parsed)
|
|
171
|
+
except OriginProjectError:
|
|
172
|
+
books = []
|
|
173
|
+
|
|
174
|
+
if books:
|
|
175
|
+
books[0] = _with_provenance(books[0], path, raw=raw)
|
|
176
|
+
folder_paths = opju_folder_paths(raw) if is_opju else opj_folder_paths(raw)
|
|
177
|
+
books = [_with_folder_path(b, folder_paths) for b in books]
|
|
178
|
+
return primary, books
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def drop_empty_library_books(books: list[DataStruct]) -> list[DataStruct]:
|
|
182
|
+
"""Presentation gate for the multi-book Library listing: hide books with no
|
|
183
|
+
plottable numeric data and no text content.
|
|
184
|
+
|
|
185
|
+
Origin fit/report sheets (plan item 4) surface as pseudo-books whose numeric
|
|
186
|
+
``.values`` are empty -- their content is unresolved ``cell://`` reference
|
|
187
|
+
stubs in ``metadata['origin_report_sheets']``, not real data -- so listing
|
|
188
|
+
them as top-level Library entries floods it with empty rows (the Hc2 project
|
|
189
|
+
alone produced 48 ``Book2@N`` fit-report shells). Mirrors
|
|
190
|
+
``drop_nonactionable_figures``: a gate applied at the import boundary, not in
|
|
191
|
+
the decoder, so ``read_origin_books`` stays a complete reader (the report
|
|
192
|
+
metadata still rides the returned DataStruct for a future fit-report surface).
|
|
193
|
+
Text-only books (real content in ``origin_text_columns``) are kept. Never
|
|
194
|
+
returns empty: a degenerate all-empty project passes through unchanged so the
|
|
195
|
+
Library still shows something.
|
|
196
|
+
"""
|
|
197
|
+
kept = [
|
|
198
|
+
b
|
|
199
|
+
for b in books
|
|
200
|
+
if (b.values.size and int(np.count_nonzero(np.isfinite(b.values))) > 0)
|
|
201
|
+
or b.metadata.get("origin_text_columns")
|
|
202
|
+
]
|
|
203
|
+
return kept or books
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def drop_nonactionable_figures(figs: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
207
|
+
"""Keep only figures a user can act on: those with bound curves OR a resolvable
|
|
208
|
+
source hint.
|
|
209
|
+
|
|
210
|
+
The layer-anchor scan also locates internal storage/thumbnail blocks that carry
|
|
211
|
+
a decodable axis record but no bound curves and no source reference -- the
|
|
212
|
+
decoder can find them, but they are not user graphs and cannot be restored onto
|
|
213
|
+
any imported dataset. Surfacing them just floods the Library's Figures section
|
|
214
|
+
with dead "SYSTEM" rows (the Hc2 project produced 32 of them). This is a
|
|
215
|
+
presentation gate, applied at the import boundary, not in the decoder -- so the
|
|
216
|
+
decoder stays a complete axis-record reader for the synthetic tests. Verified
|
|
217
|
+
against the corpus: keeps all 14 real figures (each has curves), drops all 32
|
|
218
|
+
non-actionable Hc2 anchors. (Moved here from ``figures_opju`` 2026-07-06 --
|
|
219
|
+
it pairs with ``drop_empty_library_books``, the sibling gate above.)
|
|
220
|
+
"""
|
|
221
|
+
return [f for f in figs if f.get("curves") or str(f.get("source_hint") or "").strip()]
|