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,302 @@
1
+ """Results-log + notes-window recovery from Origin project files (plan item 6).
2
+
3
+ Origin's *results log* — the running record of every analysis operation
4
+ (fits, subtractions, smoothing) with its parameters and outputs — is stored
5
+ as plain text in the windows section of both containers, as timestamped
6
+ records shaped like::
7
+
8
+ [5/6/2019 15:16:34 "" (2458609)]
9
+ subtract_line(subtract_line)
10
+ Input
11
+ iy(Input) = [Book4]Sheet1!(C"H",M)
12
+ ...
13
+
14
+ Because the records are plain text in both ``.opj`` (CPYA) and ``.opju``
15
+ (CPYUA), one byte-level scan serves both: collect printable runs that
16
+ contain at least one timestamp record header. This is fit *provenance*
17
+ worth surfacing, not data — it lands in ``metadata['origin_results_log']``.
18
+
19
+ That raw text is further parsed into structured per-operation records by
20
+ ``parse_results_log`` (plan item 22): each record is
21
+ ``{"timestamp": "M/D/YYYY H:MM:SS", "operation": str, "params": {...}}``
22
+ where ``params`` nests by the log's ``Input``/``Output``/etc. section
23
+ headers, e.g. ``params["Input"]["iy"] == '[Book4]Sheet1!(C"H",M)'``.
24
+ Parameter lines that precede any section header land under the empty-
25
+ string section ``params[""]``. Lines inside a record that don't parse as
26
+ a timestamp header, an operation line, a section header, or a
27
+ ``key(display) = value`` parameter are never dropped silently — they
28
+ collect in that record's ``"extra"`` list (omitted when empty). A record
29
+ with no operation line still yields its timestamp, with
30
+ ``operation == ""``. This lands in
31
+ ``metadata['origin_results_log_records']`` alongside the raw text.
32
+
33
+ *Notes windows* (free-form user text pages) sit in the ``.opju`` (CPYUA)
34
+ windows section as a tight, contiguous pair of length-prefixed records::
35
+
36
+ 93 <nl> <window-name> 00 0a <tl-varint> <note-text> 00
37
+
38
+ — a ``0x93`` window-name record (``nl`` counts name+NUL) whose NUL butts
39
+ directly against a ``0x0a`` text record. ``tl`` is a **LEB128 varint**
40
+ counting text+NUL (a 718-byte note stores ``ce 05``): the original
41
+ single-byte read silently dropped every note past ~127 chars, fixed
42
+ 2026-07-06 against ``notes_real.opju`` (a real 717-char note loaded via
43
+ ``open -n``, holding Windows paths and inequalities). Text decodes
44
+ UTF-8-first (latin-1 fallback), and the internal-junk filter keys on named
45
+ OriginStorage/CDATA tokens only — NOT bare ``\``/``<``/``>``, which real
46
+ notes legitimately carry. It still matches **zero** records across the whole
47
+ real corpus (none carry a notes window), so it attaches nothing
48
+ speculatively — the false-positive bar the earlier log-only scan set. Notes
49
+ land in ``metadata['origin_notes']`` as ``{window_name: text}``. The scan is
50
+ byte-level so it also runs over ``.opj`` (CPYA), where it is likewise
51
+ false-positive-clean on the corpus but has no known-content oracle; Origin
52
+ 2023+ cannot write ``.opj`` so no such specimen can be produced.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import re
58
+
59
+ from quantized.io.origin_project.tree import _find_tail_start, _TailParseError
60
+
61
+ __all__ = ["notes_windows", "parse_results_log", "results_log"]
62
+
63
+ # One timestamped operation-record header, e.g. `[5/6/2019 15:16:34 "" (2458609)]`.
64
+ _RECORD = re.compile(rb"\[\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2}:\d{2}[^\]\r\n]{0,80}\]")
65
+
66
+ # A printable text run long enough to hold at least one record.
67
+ _RUN = re.compile(rb"[\x20-\x7e\r\n\t]{40,}")
68
+
69
+ _MAX_LOG = 200_000 # metadata guard: never attach unbounded text
70
+
71
+ # Text containing any of these is an internal storage blob, not a user note.
72
+ # Bare `\`, `<`, `>` were REMOVED 2026-07-06: real notes legitimately hold
73
+ # Windows paths ("C:\lab\data") and inequalities ("T < 4 K", "H > 2 T"); the
74
+ # named storage tokens below still reject every OriginStorage/CDATA XML blob
75
+ # (corpus stays false-positive-clean). `</` catches XML close-tags without
76
+ # hitting a lone inequality.
77
+ _NOTE_JUNK = (b"OriginStorage", b"ColumnInfo", b"ImportFile", b"CDATA", b"</", b"<NEWBOOK")
78
+ _MAX_NOTES = 64 # sane cap on notes windows per project
79
+ _MAX_NOTE_LEN = 100_000 # a note is free-form prose; bounded, not a 250-byte cap
80
+
81
+ _WINDOW_MARK = re.compile(rb"\x93")
82
+
83
+ # Safety cushion (bytes) subtracted from the structurally-derived tail
84
+ # boundary (`tree._find_tail_start`) before scanning a `.opj` for a results
85
+ # log / notes window (see `_tail_scan_start`). Never a hardcoded byte offset
86
+ # or file-size percentage -- always relative to THIS file's own computed
87
+ # block-stream-end position. Sized against the real corpus
88
+ # (`../test-data/origin`): every project there with a results log has its
89
+ # first match within ~550 bytes of that boundary (XMCD.opj, the largest hit,
90
+ # is 521 bytes past it), so 2 MiB leaves a wide cushion while still skipping
91
+ # the ~90%+ of a large project (PNR.opj: 127 MB file, 116.5 MB boundary)
92
+ # that is bulk float data and structurally cannot contain a match.
93
+ _TAIL_SAFETY_MARGIN = 2 * 1024 * 1024
94
+
95
+
96
+ def _tail_scan_start(b: bytes, suffix: str) -> int:
97
+ """Where `results_log`/`notes_windows` should start scanning ``b``.
98
+
99
+ Restricted to ``.opj`` (CPYA): its block-stream framing lets
100
+ `tree._find_tail_start` locate exactly where the free-text tail begins
101
+ (results-log records and notes windows both live there -- see the
102
+ module docstring's ``_skip_notes`` reference), and the real corpus
103
+ confirms every match sits within a few hundred bytes of that boundary.
104
+
105
+ ``.opju`` (CPYUA) uses a different, FPC-compressed column codec with no
106
+ equivalent boundary function, and the corpus has no full-size `.opju`
107
+ results-log specimen to verify a tail-only property against (its two
108
+ notes-window specimens are ~4 KB, too small for a restriction to matter)
109
+ -- so it (and any other/unknown suffix) keeps the full-buffer scan.
110
+ ``suffix=""`` (the default on both public functions) always full-scans,
111
+ so direct callers that don't know/care about the container type --
112
+ including every synthetic-bytes test in ``test_io_origin_project.py`` --
113
+ are unaffected.
114
+
115
+ Never raises: a tail that fails to parse (older/corrupt/unexpected
116
+ containers) degrades to a full scan, same "degrade, never guess"
117
+ convention as `tree.opj_project_dates`/`tree.opj_folder_paths`.
118
+ """
119
+ if suffix != ".opj":
120
+ return 0
121
+ try:
122
+ tail = _find_tail_start(b)
123
+ except _TailParseError:
124
+ return 0
125
+ return max(0, tail - _TAIL_SAFETY_MARGIN)
126
+
127
+
128
+ def results_log(b: bytes, *, suffix: str = "") -> str:
129
+ """The project's results-log text, or ``""`` when none is present.
130
+
131
+ Only printable runs containing a timestamped record header qualify —
132
+ OriginStorage XML, LabTalk scripts, and other internal text never match
133
+ the record shape, so nothing is scraped speculatively. ``suffix`` (e.g.
134
+ ``".opj"``) restricts the scan to the structurally-derived tail where a
135
+ match can actually occur (see `_tail_scan_start`); omit it (or pass
136
+ ``""``) to scan the whole buffer.
137
+ """
138
+ start = _tail_scan_start(b, suffix)
139
+ parts: list[str] = []
140
+ total = 0
141
+ for m in _RUN.finditer(b, start):
142
+ run = m.group(0)
143
+ if not _RECORD.search(run):
144
+ continue
145
+ text = run.decode("latin1").strip()
146
+ parts.append(text)
147
+ total += len(text)
148
+ if total >= _MAX_LOG:
149
+ break
150
+ return "\n\n".join(parts)[:_MAX_LOG]
151
+
152
+
153
+ def _printable(data: bytes, *, allow_newlines: bool = False) -> bool:
154
+ lo = 0x09 if allow_newlines else 0x20
155
+ return bool(data) and all(lo <= c <= 0x0D or 0x20 <= c <= 0x7E for c in data)
156
+
157
+
158
+ def _read_varint(b: bytes, p: int) -> tuple[int | None, int]:
159
+ """LEB128 length varint at ``p`` -> ``(value, next_pos)``.
160
+
161
+ Returns ``(None, p)`` if it isn't a well-formed varint within 3 bytes. The
162
+ ``.opju`` text record's length prefix GROWS with the note (a 718-byte note
163
+ stores ``ce 05``), so a single-byte read silently dropped every note past
164
+ ~127 chars -- the same varint-width class as the big-column fix (audit
165
+ #16), here confirmed by ``notes_real.opju`` (2026-07-06)."""
166
+ val = shift = 0
167
+ for k in range(3):
168
+ if p + k >= len(b):
169
+ return None, p
170
+ c = b[p + k]
171
+ val |= (c & 0x7F) << shift
172
+ if not c & 0x80:
173
+ return val, p + k + 1
174
+ shift += 7
175
+ return None, p
176
+
177
+
178
+ def _decode_note(raw: bytes) -> str | None:
179
+ """UTF-8-first (latin-1 fallback) note text, ``\\r\\n`` normalized, or
180
+ ``None`` if it isn't printable text. The ``.opju`` container is UTF-8, so a
181
+ note with a degree sign / Greek survives."""
182
+ for enc in ("utf-8", "latin1"):
183
+ try:
184
+ s = raw.decode(enc)
185
+ except UnicodeDecodeError:
186
+ continue
187
+ if any(ord(c) < 0x20 and c not in "\t\r\n" for c in s):
188
+ return None
189
+ return s.replace("\r\n", "\n")
190
+ return None
191
+
192
+
193
+ def notes_windows(b: bytes, *, suffix: str = "") -> dict[str, str]:
194
+ """Map notes-window name -> its free text (``\\r\\n`` normalized to ``\\n``).
195
+
196
+ Recognizes only the exact contiguous ``93 <nl> <name> 00 0a <tl> <text>
197
+ 00`` framing (see module docstring). Every candidate must pass a
198
+ printable-character + internal-junk-token filter, so OriginStorage XML
199
+ and storage blobs never masquerade as user notes. Returns ``{}`` when no
200
+ notes window is present. ``suffix`` restricts the scan the same way
201
+ `results_log` does -- see `_tail_scan_start`.
202
+ """
203
+ out: dict[str, str] = {}
204
+ n = len(b)
205
+ start = _tail_scan_start(b, suffix)
206
+ for m in _WINDOW_MARK.finditer(b, start):
207
+ p = m.start()
208
+ if p + 2 >= n:
209
+ continue
210
+ nl = b[p + 1]
211
+ if not (2 <= nl <= 64) or p + 1 + nl >= n or b[p + 1 + nl] != 0:
212
+ continue
213
+ name = b[p + 2 : p + 1 + nl] # nl-1 bytes + the NUL just checked
214
+ if not _printable(name):
215
+ continue
216
+ q = p + 2 + nl # first byte after the name's NUL
217
+ if q + 1 >= n or b[q] != 0x0A:
218
+ continue
219
+ tl, text_start = _read_varint(b, q + 1) # tl counts text + trailing NUL
220
+ if tl is None or not (2 <= tl <= _MAX_NOTE_LEN):
221
+ continue
222
+ text_end = text_start + tl - 1 # index of the trailing NUL
223
+ if text_end >= n or b[text_end] != 0:
224
+ continue
225
+ text = b[text_start:text_end]
226
+ if any(j in text for j in _NOTE_JUNK):
227
+ continue
228
+ decoded = _decode_note(text)
229
+ if decoded is None:
230
+ continue
231
+ out[name.decode("latin1")] = decoded
232
+ if len(out) >= _MAX_NOTES:
233
+ break
234
+ return out
235
+
236
+
237
+ # ── structured results-log parsing (plan item 22) ────────────────────────────
238
+
239
+ # A results-log record header, on ``results_log()``'s decoded str text (see
240
+ # ``_RECORD`` above for the byte-level equivalent used to find the raw runs).
241
+ _LOG_HEADER = re.compile(r"\[(\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2}:\d{2})[^\[\]\r\n]{0,80}\]")
242
+
243
+ # A bare operation line, e.g. `subtract_line(subtract_line)`: no `=`, so it
244
+ # never collides with a `key(display) = value` parameter line.
245
+ _OPERATION_LINE = re.compile(r"^([A-Za-z_]\w*)\([^)]*\)\s*$")
246
+
247
+ # A `key(display) = value` (or plain `key = value`) parameter line.
248
+ _PARAM_LINE = re.compile(r"^([A-Za-z_]\w*)(?:\([^)]*\))?\s*=\s*(.*)$")
249
+
250
+ # A section header, e.g. `Input` / `Output`: a short word/phrase, no `=`.
251
+ _SECTION_LINE = re.compile(r"^[A-Za-z][A-Za-z0-9_ ]{0,40}$")
252
+
253
+
254
+ def parse_results_log(text: str) -> list[dict[str, object]]:
255
+ """Parse ``results_log()``'s raw text into structured per-operation records.
256
+
257
+ Each record is ``{"timestamp": str, "operation": str, "params": dict}``,
258
+ optionally with an ``"extra"`` list of lines inside the record that didn't
259
+ match any recognized shape (never dropped silently). ``params`` nests by
260
+ the log's section headers (``Input``/``Output``/etc.); parameter lines
261
+ that appear before any section header land under ``params[""]``. A
262
+ record with no operation line still yields its timestamp, with
263
+ ``operation == ""``. Returns ``[]`` when ``text`` holds no timestamp
264
+ headers at all (e.g. ``""``).
265
+ """
266
+ records: list[dict[str, object]] = []
267
+ headers = list(_LOG_HEADER.finditer(text))
268
+ for i, m in enumerate(headers):
269
+ body_end = headers[i + 1].start() if i + 1 < len(headers) else len(text)
270
+ operation = ""
271
+ params: dict[str, dict[str, str]] = {}
272
+ extra: list[str] = []
273
+ section = ""
274
+ is_first_line = True
275
+ for line in text[m.end() : body_end].splitlines():
276
+ stripped = line.strip()
277
+ if not stripped:
278
+ continue
279
+ if is_first_line:
280
+ is_first_line = False
281
+ op_m = _OPERATION_LINE.match(stripped)
282
+ if op_m:
283
+ operation = op_m.group(1)
284
+ continue
285
+ # no operation line present -- fall through, reprocess below
286
+ param_m = _PARAM_LINE.match(stripped)
287
+ if param_m:
288
+ params.setdefault(section, {})[param_m.group(1)] = param_m.group(2).strip()
289
+ elif _SECTION_LINE.match(stripped):
290
+ section = stripped
291
+ params.setdefault(section, {})
292
+ else:
293
+ extra.append(stripped)
294
+ record: dict[str, object] = {
295
+ "timestamp": m.group(1),
296
+ "operation": operation,
297
+ "params": params,
298
+ }
299
+ if extra:
300
+ record["extra"] = extra
301
+ records.append(record)
302
+ return records