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,314 @@
|
|
|
1
|
+
"""Origin graph templates (``.otp``/``.otpu``) -> a quantized ``GraphTemplate``
|
|
2
|
+
(gap-ecosystem plan item 5, decode-plan #21). GRAPH templates only (owner
|
|
3
|
+
decision): a graph template's saved style (axis limits, legend, per-curve
|
|
4
|
+
color/line-vs-scatter/width/marker) maps onto the frontend's ``GraphTemplate``
|
|
5
|
+
shape (``frontend/src/lib/figuredoc.ts`` -- ``name``/``style``/``overrides``/
|
|
6
|
+
``seriesStyles``); workbook/analysis templates have no quantized counterpart
|
|
7
|
+
and are out of scope.
|
|
8
|
+
|
|
9
|
+
**Recon (2026-07-07).** Both template extensions are the SAME CPY container
|
|
10
|
+
family already documented in ``docs/origin_project_format.md`` sec 2 -- an
|
|
11
|
+
``.otp`` opens with the ``CPYA`` magic (verified: the corpus's
|
|
12
|
+
``SLD_DoubleY.otp`` is ``CPYA 4.3227``, an older sub-version of the same
|
|
13
|
+
family already handled), an ``.otpu`` with ``CPYUA`` (verified: all four
|
|
14
|
+
corpus ``.otpu`` files are ``CPYUA 4.3380``). Critically, the EXISTING figure
|
|
15
|
+
decoders already understand a template's bytes directly -- ``figures.
|
|
16
|
+
extract_figures``/``figures_opju.extract_figures_opju`` take raw file bytes,
|
|
17
|
+
not a parsed workbook, and a template's graph window(s) are laid out exactly
|
|
18
|
+
like a project's: axis ranges, log flags, titles, legend text/labels, frame
|
|
19
|
+
and page geometry all decoded verbatim, zero new code, against 4 of the 5
|
|
20
|
+
corpus templates (``SLD_DoubleY.otp``, ``PNR-SF.otpu``, ``SLDdouble.otpu``,
|
|
21
|
+
``UnpolFresnelNR.otpu``). The 5th, ``PNR.otpu``, carries exactly one axis
|
|
22
|
+
anchor (``03 00 00 1f``) whose record matches NONE of the three existing
|
|
23
|
+
``.opju`` axis-record forms (specimen / real / hybrid -- all return ``None``
|
|
24
|
+
at that offset): a genuinely new, undocumented 4th record shape. Per the
|
|
25
|
+
"conservative first decoder" scope, this is left undecoded and documented,
|
|
26
|
+
not chased -- ``read_origin_template`` degrades that one file to a
|
|
27
|
+
styles-only partial (see below) rather than guessing an axis record or
|
|
28
|
+
failing the whole file, since its curve-style tokens decode fine
|
|
29
|
+
independently.
|
|
30
|
+
|
|
31
|
+
**What does NOT decode via the existing path: curve style.** A template
|
|
32
|
+
carries no workbook columns, so ``opj_curves.extract_curves``/
|
|
33
|
+
``opju_figure_curves.extract_curves_by_id`` -- which bind a curve to
|
|
34
|
+
``(book, x, y)`` via a global column-id lookup built by scanning the
|
|
35
|
+
project's OWN column-storage blocks -- always come up with an empty id map
|
|
36
|
+
for a template file (there is no workbook to scan), so every curve is
|
|
37
|
+
silently dropped even though its raw style record is present on disk and
|
|
38
|
+
fully decodable. Confirmed by direct byte-level recon: every corpus template
|
|
39
|
+
carries real ``curve_style_color.style_fields``-decodable records (explicit
|
|
40
|
+
RGB colors, line/scatter, line width, symbol size) with NO book/column
|
|
41
|
+
resolution required -- the style lives entirely inside the curve's own
|
|
42
|
+
anchor record (``.otp``) / sparse id token (``.otpu``), the same record
|
|
43
|
+
``curve_style_color.py`` already decodes for real projects. This module reuses
|
|
44
|
+
ONLY the style half of those two decoders (:func:`_template_curve_styles_opj`
|
|
45
|
+
/ :func:`_template_curve_styles_opju`), never their book/column binding --
|
|
46
|
+
templates have no dataset to bind to, so book/x/y stay permanently absent by
|
|
47
|
+
design, not by gap.
|
|
48
|
+
|
|
49
|
+
**Mapping to ``GraphTemplate`` (honestly partial).** ``name`` is the file
|
|
50
|
+
stem; ``style`` stays the fixed string ``"default"`` (Origin templates carry
|
|
51
|
+
no quantized preset concept to recover). ``overrides`` comes from the
|
|
52
|
+
template's FIRST decoded graph layer only -- ``x_lim``/``y_lim`` (axis
|
|
53
|
+
range) and ``legend`` (``show`` + a nearest-quadrant ``loc``, mirroring
|
|
54
|
+
``frontend/src/lib/originFigures.ts``'s ``originLegendPos``); a template with
|
|
55
|
+
>1 layer (e.g. a double-Y style like ``SLD_DoubleY.otp``/``SLDdouble.otpu``)
|
|
56
|
+
has no way to carry its 2nd layer's own Y range in this shape (``GraphTemplate``
|
|
57
|
+
itself has no multi-layer/y2 concept -- see ``figuredoc.ts``), so that layer's
|
|
58
|
+
style is simply not represented; this is a target-SHAPE limitation, not a
|
|
59
|
+
decode failure. ``seriesStyles`` is built from EVERY decoded curve style
|
|
60
|
+
record found in the file, in on-disk order, mirroring
|
|
61
|
+
``originCurveSeriesStyle``'s color/line-vs-scatter/width/marker rule but
|
|
62
|
+
targeting the export-style shape (``frontend/src/lib/exportStyles.ts``'s
|
|
63
|
+
``ExportSeriesStyle`` -- ``color``/``width``/``line``/``marker``/
|
|
64
|
+
``marker_size``); since ``GraphTemplate.seriesStyles`` is ALREADY one flat
|
|
65
|
+
list with no per-layer grouping, collapsing a multi-layer template's curves
|
|
66
|
+
into one file-wide list matches the target shape's own limitation rather
|
|
67
|
+
than adding a new one. Undecodable per-curve properties (Origin's symbol
|
|
68
|
+
*shape* -- square/circle/triangle/... -- has no field in ``ExportSeriesStyle``,
|
|
69
|
+
only a boolean "draw a marker") and undecodable per-layer properties (grid
|
|
70
|
+
on/off, tick direction, font: no isolated on-disk field is known for any of
|
|
71
|
+
these -- see ``docs/origin_project_format.md`` sec 6.3's permanent-gaps list)
|
|
72
|
+
stay absent from the returned dict, never guessed.
|
|
73
|
+
|
|
74
|
+
Non-graph templates (a workbook/analysis template, or any file whose graph
|
|
75
|
+
layer(s) fail to decode at all) raise :class:`OriginProjectError` with what
|
|
76
|
+
was found, per the "an honest partial beats a guessed decoder" rule --
|
|
77
|
+
:func:`read_origin_template` never fabricates a template from nothing.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
from __future__ import annotations
|
|
81
|
+
|
|
82
|
+
import math
|
|
83
|
+
import re
|
|
84
|
+
import struct
|
|
85
|
+
from pathlib import Path
|
|
86
|
+
from typing import Any
|
|
87
|
+
|
|
88
|
+
from quantized.io.origin_project.container import OriginProjectError, walk_blocks
|
|
89
|
+
from quantized.io.origin_project.curve_style_color import (
|
|
90
|
+
apply_increment_colors,
|
|
91
|
+
opju_style_record,
|
|
92
|
+
style_fields,
|
|
93
|
+
)
|
|
94
|
+
from quantized.io.origin_project.figures import extract_figures
|
|
95
|
+
from quantized.io.origin_project.figures_opju import extract_figures_opju
|
|
96
|
+
from quantized.io.origin_project.opj_curves import _CURVE_PREFIX, _DATAPLOT_MAGIC
|
|
97
|
+
from quantized.io.origin_project.opju_codec import curve_plot_style
|
|
98
|
+
from quantized.io.origin_project.opju_figure_curves import _CURVE_TOKEN
|
|
99
|
+
|
|
100
|
+
__all__ = ["read_origin_template"]
|
|
101
|
+
|
|
102
|
+
_HEX_RE = re.compile(r"^#[0-9A-Fa-f]{6}$")
|
|
103
|
+
_StyleFields = dict[str, str | float]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _template_curve_styles_opj(b: bytes) -> list[_StyleFields]:
|
|
107
|
+
"""Every decodable curve-anchor style record in a CPYA template, file-wide,
|
|
108
|
+
in on-disk order -- the style half of ``opj_curves.extract_curves`` (item
|
|
109
|
+
11's ``01 00 00 00 <id>`` anchor immediately followed by the DataPlot
|
|
110
|
+
magic), deliberately WITHOUT its ``id_map``/``x_columns`` book/column
|
|
111
|
+
binding (a template has no workbook to build either from -- see module
|
|
112
|
+
docstring)."""
|
|
113
|
+
blocks = [(size, payload) for size, payload in walk_blocks(b) if size]
|
|
114
|
+
out: list[_StyleFields] = []
|
|
115
|
+
records: list[bytes | None] = []
|
|
116
|
+
for j in range(len(blocks) - 1):
|
|
117
|
+
_, payload = blocks[j]
|
|
118
|
+
if len(payload) < 6 or payload[:4] != _CURVE_PREFIX:
|
|
119
|
+
continue
|
|
120
|
+
_, next_payload = blocks[j + 1]
|
|
121
|
+
if next_payload[:8] != _DATAPLOT_MAGIC:
|
|
122
|
+
continue
|
|
123
|
+
out.append(style_fields(payload))
|
|
124
|
+
records.append(payload)
|
|
125
|
+
apply_increment_colors(out, records)
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _template_curve_styles_opju(b: bytes) -> list[_StyleFields]:
|
|
130
|
+
"""Same, for CPYUA templates: every unified id-token's sparse style record
|
|
131
|
+
(``opju_figure_curves``'s ``_CURVE_TOKEN``), reconstructed via
|
|
132
|
+
``curve_style_color.opju_style_record`` and read with ``style_fields`` --
|
|
133
|
+
independent of ``opju_figure_curves.column_id_table`` (empty for a
|
|
134
|
+
template: no workbook column ever assigns the token's id)."""
|
|
135
|
+
out: list[_StyleFields] = []
|
|
136
|
+
records: list[bytes | None] = []
|
|
137
|
+
for m in _CURVE_TOKEN.finditer(b):
|
|
138
|
+
record = opju_style_record(b, m.start() + 3)
|
|
139
|
+
curve: _StyleFields = dict(style_fields(record)) if record is not None else {}
|
|
140
|
+
style = curve_plot_style(b, m.start())
|
|
141
|
+
if style:
|
|
142
|
+
curve["style"] = style
|
|
143
|
+
out.append(curve)
|
|
144
|
+
records.append(record)
|
|
145
|
+
apply_increment_colors(out, records)
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _series_style(fields: _StyleFields) -> dict[str, Any] | None:
|
|
150
|
+
"""One decoded curve's style fields -> an ``ExportSeriesStyle``-shaped
|
|
151
|
+
dict (``frontend/src/lib/exportStyles.ts``), mirroring
|
|
152
|
+
``frontend/src/lib/originFigures.ts``'s ``originCurveSeriesStyle`` (the
|
|
153
|
+
already-shipped rule for applying a decoded Origin curve's style to a
|
|
154
|
+
plot) but targeting the *export* style shape a saved template carries:
|
|
155
|
+
"scatter" -> markers, no connecting line (``width: 0``); "line" -> a
|
|
156
|
+
solid line at the default 1.5pt width; a decoded ``lineWidth`` overrides
|
|
157
|
+
that default (never on a scatter curve -- Origin stores a latent line
|
|
158
|
+
width even on symbol-only plots); a decoded ``symbolSize`` sets
|
|
159
|
+
``marker_size`` only once a marker is already on. No ``markerShape`` key
|
|
160
|
+
exists in ``ExportSeriesStyle``, so a decoded ``symbol`` only turns
|
|
161
|
+
``marker`` on -- its glyph (square/circle/triangle/...) is not
|
|
162
|
+
representable in this target shape (documented gap, not a decode miss).
|
|
163
|
+
Returns ``None`` when nothing decoded, so the caller's list holds an
|
|
164
|
+
honest ``null`` slot rather than an empty object."""
|
|
165
|
+
out: dict[str, Any] = {}
|
|
166
|
+
style = fields.get("style")
|
|
167
|
+
if style == "scatter":
|
|
168
|
+
out["marker"] = True
|
|
169
|
+
out["width"] = 0
|
|
170
|
+
elif style == "line":
|
|
171
|
+
out["width"] = 1.5
|
|
172
|
+
color = fields.get("color")
|
|
173
|
+
if isinstance(color, str) and _HEX_RE.match(color):
|
|
174
|
+
out["color"] = color
|
|
175
|
+
if fields.get("symbol"):
|
|
176
|
+
out["marker"] = True
|
|
177
|
+
width = fields.get("lineWidth")
|
|
178
|
+
if isinstance(width, int | float) and width > 0 and style != "scatter":
|
|
179
|
+
out["width"] = width
|
|
180
|
+
size = fields.get("symbolSize")
|
|
181
|
+
if isinstance(size, int | float) and size > 0 and out.get("marker"):
|
|
182
|
+
out["marker_size"] = size
|
|
183
|
+
return out or None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _finite_range(lo: Any, hi: Any) -> bool:
|
|
187
|
+
return (
|
|
188
|
+
isinstance(lo, int | float)
|
|
189
|
+
and isinstance(hi, int | float)
|
|
190
|
+
and math.isfinite(lo)
|
|
191
|
+
and math.isfinite(hi)
|
|
192
|
+
and lo != hi
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _axis_fraction(v: float, lo: float, hi: float, log: bool) -> float | None:
|
|
197
|
+
"""Fraction of ``v`` along ``[lo, hi]``, log10-aware on a log axis --
|
|
198
|
+
mirrors ``frontend/src/lib/originFigures.ts``'s ``axisFraction`` (the
|
|
199
|
+
same model the backend used to decode the legend box position in the
|
|
200
|
+
first place, see ``annotation_marks.py``)."""
|
|
201
|
+
if log and lo > 0 and hi > 0 and v > 0:
|
|
202
|
+
a, b = math.log10(lo), math.log10(hi)
|
|
203
|
+
return (math.log10(v) - a) / (b - a) if b != a else None
|
|
204
|
+
return (v - lo) / (hi - lo) if hi != lo else None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _template_legend(fig: dict[str, Any]) -> dict[str, Any] | None:
|
|
208
|
+
"""``{"show": True, "loc": "<vert> <side>"}`` when the layer carries any
|
|
209
|
+
non-empty legend text or a decoded legend-box position, else ``None``.
|
|
210
|
+
``loc`` maps the decoded position (data coords) to the nearest quadrant
|
|
211
|
+
-- the same corner logic as ``originFigures.ts``'s ``originLegendPos``,
|
|
212
|
+
landing on one of ``frontend/src/lib/figureOverrides.ts``'s
|
|
213
|
+
``LEGEND_LOCS`` strings (``"upper right"``/``"upper left"``/
|
|
214
|
+
``"lower left"``/``"lower right"``) rather than a guessed pixel anchor."""
|
|
215
|
+
labels = [t for t in (fig.get("legend_labels") or []) if t]
|
|
216
|
+
pos = fig.get("legend_pos")
|
|
217
|
+
if not labels and pos is None:
|
|
218
|
+
return None
|
|
219
|
+
out: dict[str, Any] = {"show": True}
|
|
220
|
+
if pos is not None:
|
|
221
|
+
fx = _axis_fraction(pos["x"], fig["x_from"], fig["x_to"], fig["x_log"])
|
|
222
|
+
fy = _axis_fraction(pos["y"], fig["y_from"], fig["y_to"], fig["y_log"])
|
|
223
|
+
if fx is not None and fy is not None and math.isfinite(fx) and math.isfinite(fy):
|
|
224
|
+
vert = "upper" if fy >= 0.5 else "lower"
|
|
225
|
+
side = "right" if fx >= 0.5 else "left"
|
|
226
|
+
out["loc"] = f"{vert} {side}"
|
|
227
|
+
return out
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _template_overrides(fig: dict[str, Any]) -> dict[str, Any] | None:
|
|
231
|
+
"""``FigureOverrides``-shaped dict (``frontend/src/lib/figureOverrides.ts``)
|
|
232
|
+
from one figure-layer dict: ``x_lim``/``y_lim`` (only when both bounds are
|
|
233
|
+
finite and distinct) and ``legend``. Every other ``FigureOverrides`` key
|
|
234
|
+
(``grid``, ``ticks``, ``spines``, ``margins``, ``font_size``/``font_name``,
|
|
235
|
+
``annotations``) has no isolated on-disk field this codebase decodes (see
|
|
236
|
+
``docs/origin_project_format.md`` sec 6.3's permanent-gaps list) and stays
|
|
237
|
+
absent -- never defaulted."""
|
|
238
|
+
out: dict[str, Any] = {}
|
|
239
|
+
if _finite_range(fig.get("x_from"), fig.get("x_to")):
|
|
240
|
+
out["x_lim"] = [fig["x_from"], fig["x_to"]]
|
|
241
|
+
if _finite_range(fig.get("y_from"), fig.get("y_to")):
|
|
242
|
+
out["y_lim"] = [fig["y_from"], fig["y_to"]]
|
|
243
|
+
legend = _template_legend(fig)
|
|
244
|
+
if legend:
|
|
245
|
+
out["legend"] = legend
|
|
246
|
+
return out or None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
_DECODE_ERRORS = (IndexError, ValueError, KeyError, struct.error)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def read_origin_template(path: Path) -> dict[str, Any]:
|
|
253
|
+
"""Decode an Origin GRAPH template (``.otp``/``.otpu``) into a
|
|
254
|
+
``GraphTemplate``-shaped dict (``frontend/src/lib/figuredoc.ts``):
|
|
255
|
+
``name`` (the file stem), ``style`` (always ``"default"``), ``overrides``
|
|
256
|
+
(axis limits + legend from the first decoded layer, or ``None``), and
|
|
257
|
+
``seriesStyles`` (one entry per decoded curve, file-wide, in on-disk
|
|
258
|
+
order, or ``None`` when no curve style decoded). See the module docstring
|
|
259
|
+
for the full recon + mapping rationale.
|
|
260
|
+
|
|
261
|
+
Raises :class:`OriginProjectError` (mapped to a 422 by the import route)
|
|
262
|
+
when ``path`` isn't a recognized template container, or when its magic
|
|
263
|
+
header doesn't match the extension's expected CPY family, or when NO
|
|
264
|
+
graph layer decodes at all (e.g. a workbook/analysis template -- out of
|
|
265
|
+
scope for this graph-style mapping) -- an honest failure, never a
|
|
266
|
+
fabricated template.
|
|
267
|
+
"""
|
|
268
|
+
suffix = path.suffix.lower()
|
|
269
|
+
if suffix not in (".otp", ".otpu"):
|
|
270
|
+
raise OriginProjectError(
|
|
271
|
+
f"'{path.name}' is not an Origin graph template (expected .otp or .otpu)."
|
|
272
|
+
)
|
|
273
|
+
b = path.read_bytes()
|
|
274
|
+
# Magic-header checks stay OUTSIDE the decode try/except below: they raise
|
|
275
|
+
# OriginProjectError directly, which is itself a ValueError -- inside the
|
|
276
|
+
# try it would be caught by `_DECODE_ERRORS` and double-wrapped.
|
|
277
|
+
if suffix == ".otp":
|
|
278
|
+
if not b.startswith(b"CPYA"):
|
|
279
|
+
raise OriginProjectError(
|
|
280
|
+
f"'{path.name}' does not look like a CPYA .otp template (bad header)."
|
|
281
|
+
)
|
|
282
|
+
elif not b.startswith(b"CPYUA"):
|
|
283
|
+
raise OriginProjectError(
|
|
284
|
+
f"'{path.name}' does not look like a CPYUA .otpu template (bad header)."
|
|
285
|
+
)
|
|
286
|
+
try:
|
|
287
|
+
if suffix == ".otp":
|
|
288
|
+
figures = extract_figures(b)
|
|
289
|
+
curve_styles = _template_curve_styles_opj(b)
|
|
290
|
+
else:
|
|
291
|
+
figures = extract_figures_opju(b)
|
|
292
|
+
curve_styles = _template_curve_styles_opju(b)
|
|
293
|
+
except _DECODE_ERRORS as exc:
|
|
294
|
+
raise OriginProjectError(
|
|
295
|
+
f"'{path.name}' could not be decoded as an Origin graph template: {exc}"
|
|
296
|
+
) from exc
|
|
297
|
+
if not figures and not curve_styles:
|
|
298
|
+
raise OriginProjectError(
|
|
299
|
+
f"no graph layer or curve style could be decoded from '{path.name}' -- it "
|
|
300
|
+
"may be a workbook/analysis template (out of scope for graph-style import) "
|
|
301
|
+
"or an unrecognized template form."
|
|
302
|
+
)
|
|
303
|
+
# A layer can fail to decode (an axis-record shape this codebase doesn't
|
|
304
|
+
# recognize yet -- see module docstring's PNR.otpu note) while curve style
|
|
305
|
+
# STILL decodes (it lives in an independent record); overrides then stays
|
|
306
|
+
# None rather than the whole template failing -- an honest partial, not a
|
|
307
|
+
# guess (per-curve style is real either way; only axis/legend is missing).
|
|
308
|
+
series_styles = [_series_style(f) for f in curve_styles] or None
|
|
309
|
+
return {
|
|
310
|
+
"name": path.stem,
|
|
311
|
+
"style": "default",
|
|
312
|
+
"overrides": _template_overrides(figures[0]) if figures else None,
|
|
313
|
+
"seriesStyles": series_styles,
|
|
314
|
+
}
|