dynamic-systems-testing-framework 1.0.1__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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""A DSresult ``.mat`` read WHILE its run writes it.
|
|
2
|
+
|
|
3
|
+
:class:`LiveMatReader` is built for one result-file path and polled about
|
|
4
|
+
once a second; every :meth:`~LiveMatReader.poll` returns a
|
|
5
|
+
:class:`LiveSnapshot` — how far the file has got and, for the names asked
|
|
6
|
+
for, the samples that are NEW since those names were last served. It reads
|
|
7
|
+
the file the way :mod:`.mat_reader` does (the row count from the file's
|
|
8
|
+
size, a variable resolved by the same code: an alias's sign, a parameter as
|
|
9
|
+
a constant), so what it returns, concatenated, is what the finished file
|
|
10
|
+
reads as.
|
|
11
|
+
|
|
12
|
+
What it is built around, each measured on Dymola 2026x / omc 1.27 under WSL:
|
|
13
|
+
|
|
14
|
+
* **Nothing is held between polls.** A poll opens the file, reads, and
|
|
15
|
+
closes it. A persistent worker wipes a test's directory before each run;
|
|
16
|
+
on Windows a handle held open makes that removal fail.
|
|
17
|
+
* **No memory map.** A map over a file that is then truncated in place
|
|
18
|
+
kills the process (SIGBUS).
|
|
19
|
+
* **Only the new rows are read.** Reading a whole 300 MB result costs over a
|
|
20
|
+
second on a Windows mount; its last rows cost milliseconds. In the steady
|
|
21
|
+
state a poll is one read: the last row already served (to see that the
|
|
22
|
+
file is still the same run) and the rows after it.
|
|
23
|
+
* **It never raises** on a file that is short, vanishing or being recreated.
|
|
24
|
+
A poll that finds no file says ``"gone"``; one that finds a file whose
|
|
25
|
+
header matrices are not whole yet says ``"starting"``.
|
|
26
|
+
|
|
27
|
+
A NEW RUN is a different file under the same path. It is recognised by any
|
|
28
|
+
of: the file was gone at a poll, its identity (device, inode) changed, it
|
|
29
|
+
got smaller, the last row served no longer reads back as it did — or, while
|
|
30
|
+
no row was served yet, its header names another structure than the one
|
|
31
|
+
read before (other names, other columns, another row shape).
|
|
32
|
+
The last two because a file removed and recreated takes the same inode on
|
|
33
|
+
ext4, and can have outgrown the old one before the next poll. The reader
|
|
34
|
+
then starts over, and :attr:`LiveSnapshot.run` counts up so its caller
|
|
35
|
+
drops what it had accumulated.
|
|
36
|
+
|
|
37
|
+
Not recognised: a file put in place of one whose rows were served, no
|
|
38
|
+
smaller than it, whose row at the last index served is the same bytes. Its
|
|
39
|
+
earlier rows are then the old file's on the caller's trace until the run
|
|
40
|
+
ends and the result is read whole.
|
|
41
|
+
|
|
42
|
+
A poll returns at most ``max_rows`` samples per name — fewer for a file
|
|
43
|
+
with very wide rows, so that no read of rows exceeds 16 MB (one row, where a
|
|
44
|
+
single row is larger). When more are new — the
|
|
45
|
+
first poll of a run already long under way, a name first asked for late —
|
|
46
|
+
they are thinned to that many, evenly spaced and always ending on the newest
|
|
47
|
+
row. Thinned rows are not served later: the live trace is a preview, and the
|
|
48
|
+
finished file is read by :func:`.mat_reader.read_result_mat`.
|
|
49
|
+
|
|
50
|
+
A reader belongs to one poller; it is not safe to poll from two threads.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import os
|
|
56
|
+
from collections.abc import Collection, Mapping
|
|
57
|
+
from dataclasses import dataclass, field
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
from types import MappingProxyType
|
|
60
|
+
from typing import Literal, NamedTuple
|
|
61
|
+
|
|
62
|
+
import numpy as np
|
|
63
|
+
|
|
64
|
+
from .mat_reader import (
|
|
65
|
+
_TRAJECTORY,
|
|
66
|
+
_read_tables,
|
|
67
|
+
_scan_mat4_headers,
|
|
68
|
+
_variable_sources,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
LiveState = Literal["starting", "running", "gone"]
|
|
72
|
+
|
|
73
|
+
#: A file is there and its header matrices are not whole yet: nothing to read.
|
|
74
|
+
STARTING: LiveState = "starting"
|
|
75
|
+
#: The header matrices are read; rows are served as they arrive. Also the
|
|
76
|
+
#: state of a file whose run has ended — the reader does not judge that.
|
|
77
|
+
RUNNING: LiveState = "running"
|
|
78
|
+
#: No file at the path: not created yet, or removed.
|
|
79
|
+
GONE: LiveState = "gone"
|
|
80
|
+
|
|
81
|
+
#: Samples one poll returns per name at most (see the module docstring).
|
|
82
|
+
MAX_ROWS = 2000
|
|
83
|
+
|
|
84
|
+
#: No read of rows exceeds this many bytes (one row, where a single row is
|
|
85
|
+
#: larger). The rows a poll returns are capped to fit; a thinned backlog
|
|
86
|
+
#: spanning less is read in one piece and thinned in memory, a longer one a
|
|
87
|
+
#: row at a time.
|
|
88
|
+
_READ_BYTES = 16 << 20
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class LiveSeries:
|
|
93
|
+
"""One variable's new samples, in the file's own precision. Read-only."""
|
|
94
|
+
|
|
95
|
+
time: np.ndarray
|
|
96
|
+
values: np.ndarray
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class LiveSnapshot:
|
|
101
|
+
"""What one poll found. Immutable."""
|
|
102
|
+
|
|
103
|
+
#: ``"starting"`` / ``"running"`` / ``"gone"``.
|
|
104
|
+
state: LiveState
|
|
105
|
+
#: Which file under the path this is, counted from 0: it goes up each
|
|
106
|
+
#: time the reader starts over on a new run. A caller that accumulates
|
|
107
|
+
#: series drops them when this differs from the snapshot before.
|
|
108
|
+
run: int = 0
|
|
109
|
+
#: Whole rows (time points) the file holds.
|
|
110
|
+
rows: int = 0
|
|
111
|
+
#: The time of the last whole row; ``None`` before the first.
|
|
112
|
+
time: float | None = None
|
|
113
|
+
#: The new samples of each requested name the file has (possibly none:
|
|
114
|
+
#: empty arrays). A name the file lacks is left out, as is every name
|
|
115
|
+
#: until the state is ``"running"``.
|
|
116
|
+
series: Mapping[str, LiveSeries] = field(
|
|
117
|
+
default_factory=lambda: MappingProxyType({})
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class _Layout(NamedTuple):
|
|
122
|
+
"""The header matrices of one run's file, parsed once."""
|
|
123
|
+
|
|
124
|
+
offset: int # where the trajectory's rows start
|
|
125
|
+
dtype: np.dtype
|
|
126
|
+
n_values: int # values in a row; the first is the time
|
|
127
|
+
#: name → (index in a row, negate, constant) — :func:`_variable_sources`
|
|
128
|
+
sources: dict[str, tuple[int | None, bool, float | None]]
|
|
129
|
+
#: Every variable the file names, in its order, the abscissa left out:
|
|
130
|
+
#: what `result_files.mat_names` lists of the same file.
|
|
131
|
+
names: tuple[str, ...]
|
|
132
|
+
#: The file's STRUCTURE, hashed: where the rows start, their shape,
|
|
133
|
+
#: every name and where it is stored. Two headers with one key are one
|
|
134
|
+
#: file to a reader that has served no row. The parameters' VALUES
|
|
135
|
+
#: (``data_1``) are not in it: a tool may write them again under the
|
|
136
|
+
#: same structure, and nothing of them was served.
|
|
137
|
+
key: int
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def row_bytes(self) -> int:
|
|
141
|
+
return self.n_values * self.dtype.itemsize
|
|
142
|
+
|
|
143
|
+
def cap(self, max_rows: int) -> int:
|
|
144
|
+
"""Rows one poll returns per name: ``max_rows``, or as many as
|
|
145
|
+
``_READ_BYTES`` holds when that is fewer."""
|
|
146
|
+
return max(1, min(max_rows, _READ_BYTES // self.row_bytes))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _read(f, offset: int, n: int) -> bytes:
|
|
150
|
+
f.seek(offset)
|
|
151
|
+
return f.read(n)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class LiveMatReader:
|
|
155
|
+
"""Polls one result file for the rows its run has written so far."""
|
|
156
|
+
|
|
157
|
+
def __init__(self, path: Path, *, max_rows: int = MAX_ROWS) -> None:
|
|
158
|
+
if max_rows < 1:
|
|
159
|
+
raise ValueError(f"max_rows must be at least 1, got {max_rows}")
|
|
160
|
+
self.path = Path(path)
|
|
161
|
+
self.max_rows = max_rows
|
|
162
|
+
self._run = 0
|
|
163
|
+
self._forget()
|
|
164
|
+
|
|
165
|
+
def _forget(self) -> None:
|
|
166
|
+
self._identity: tuple[int, int] | None = None
|
|
167
|
+
self._size = 0
|
|
168
|
+
self._layout: _Layout | None = None
|
|
169
|
+
self._rows = 0
|
|
170
|
+
self._time: float | None = None
|
|
171
|
+
self._last_row = b""
|
|
172
|
+
self._served: dict[str, int] = {}
|
|
173
|
+
|
|
174
|
+
def _start_over(self) -> None:
|
|
175
|
+
self._run += 1
|
|
176
|
+
self._forget()
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def names(self) -> tuple[str, ...] | None:
|
|
180
|
+
"""Every variable the file names, from the header the last poll
|
|
181
|
+
read rows through — no second parse of it; None until a poll found
|
|
182
|
+
the header whole."""
|
|
183
|
+
return None if self._layout is None else self._layout.names
|
|
184
|
+
|
|
185
|
+
def poll(self, names: Collection[str] = ()) -> LiveSnapshot:
|
|
186
|
+
"""The file's state now, and each of ``names``' samples that are new
|
|
187
|
+
since that name was last served (all of them the first time, within
|
|
188
|
+
``max_rows``). Opens, reads and closes the file; never raises for
|
|
189
|
+
what the file is or is not."""
|
|
190
|
+
try:
|
|
191
|
+
with open(self.path, "rb") as f:
|
|
192
|
+
return self._poll(f, list(dict.fromkeys(names)))
|
|
193
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
194
|
+
if self._identity is not None:
|
|
195
|
+
self._start_over() # whatever comes next is another run
|
|
196
|
+
return LiveSnapshot(GONE, self._run)
|
|
197
|
+
except OSError:
|
|
198
|
+
# Unreadable right now (a sharing violation, a mount hiccup):
|
|
199
|
+
# no news, and no reason to think the run changed.
|
|
200
|
+
return self._snapshot({})
|
|
201
|
+
|
|
202
|
+
def _snapshot(self, series: dict[str, LiveSeries]) -> LiveSnapshot:
|
|
203
|
+
return LiveSnapshot(
|
|
204
|
+
state=STARTING if self._layout is None else RUNNING,
|
|
205
|
+
run=self._run,
|
|
206
|
+
rows=self._rows,
|
|
207
|
+
time=self._time,
|
|
208
|
+
series=MappingProxyType(series),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def _poll(self, f, names: list[str]) -> LiveSnapshot:
|
|
212
|
+
st = os.fstat(f.fileno())
|
|
213
|
+
identity = (st.st_dev, st.st_ino)
|
|
214
|
+
if self._identity is not None and (
|
|
215
|
+
identity != self._identity or st.st_size < self._size
|
|
216
|
+
):
|
|
217
|
+
self._start_over()
|
|
218
|
+
self._identity, self._size = identity, st.st_size
|
|
219
|
+
|
|
220
|
+
if self._layout is None or self._rows == 0:
|
|
221
|
+
# Until a row was served nothing but the header itself says the
|
|
222
|
+
# file is still the one it was read from: a file put in its
|
|
223
|
+
# place — in place, and no smaller — would be read through the
|
|
224
|
+
# other's offsets and columns. So it is read again each poll
|
|
225
|
+
# (the parameters' values with it), and another STRUCTURE is
|
|
226
|
+
# another run.
|
|
227
|
+
layout = self._read_layout(f)
|
|
228
|
+
if self._layout is not None and (
|
|
229
|
+
layout is None or layout.key != self._layout.key
|
|
230
|
+
):
|
|
231
|
+
self._start_over()
|
|
232
|
+
self._identity, self._size = identity, st.st_size
|
|
233
|
+
self._layout = layout
|
|
234
|
+
if layout is None:
|
|
235
|
+
return self._snapshot({})
|
|
236
|
+
layout = self._layout
|
|
237
|
+
present = max(0, st.st_size - layout.offset) // layout.row_bytes
|
|
238
|
+
cap = layout.cap(self.max_rows)
|
|
239
|
+
|
|
240
|
+
# Each name's unserved rows, the names grouped by where theirs
|
|
241
|
+
# start: one group in the steady state, a second when a name is
|
|
242
|
+
# asked for the first time.
|
|
243
|
+
groups: dict[int, list[str]] = {}
|
|
244
|
+
for name in names:
|
|
245
|
+
if name in layout.sources:
|
|
246
|
+
groups.setdefault(self._served.get(name, 0), []).append(name)
|
|
247
|
+
|
|
248
|
+
series: dict[str, LiveSeries] = {}
|
|
249
|
+
last_row: bytes | None = None
|
|
250
|
+
checked = self._rows == 0 # nothing served yet: nothing to check
|
|
251
|
+
for first, group in sorted(groups.items()):
|
|
252
|
+
# The steady state's one read: from the last row served, which
|
|
253
|
+
# must still read back as it did.
|
|
254
|
+
overlap = not checked and first == self._rows and present - first < cap
|
|
255
|
+
start = first - 1 if overlap else first
|
|
256
|
+
table, got = self._rows_between(f, layout, start, present, cap)
|
|
257
|
+
if overlap:
|
|
258
|
+
if len(table) == 0 or table[0].tobytes() != self._last_row:
|
|
259
|
+
return self._another_run(f, names)
|
|
260
|
+
checked = True
|
|
261
|
+
table = table[1:]
|
|
262
|
+
if got == present and len(table):
|
|
263
|
+
last_row = table[-1].tobytes()
|
|
264
|
+
for name in group:
|
|
265
|
+
series[name] = self._series(layout, name, table)
|
|
266
|
+
self._served[name] = max(got, first)
|
|
267
|
+
|
|
268
|
+
if (
|
|
269
|
+
not checked
|
|
270
|
+
and _read(
|
|
271
|
+
f, layout.offset + (self._rows - 1) * layout.row_bytes, layout.row_bytes
|
|
272
|
+
)
|
|
273
|
+
!= self._last_row
|
|
274
|
+
):
|
|
275
|
+
return self._another_run(f, names)
|
|
276
|
+
if present > self._rows:
|
|
277
|
+
if last_row is None:
|
|
278
|
+
last_row = _read(
|
|
279
|
+
f,
|
|
280
|
+
layout.offset + (present - 1) * layout.row_bytes,
|
|
281
|
+
layout.row_bytes,
|
|
282
|
+
)
|
|
283
|
+
if len(last_row) == layout.row_bytes:
|
|
284
|
+
self._rows, self._last_row = present, last_row
|
|
285
|
+
self._time = float(np.frombuffer(last_row, layout.dtype, 1)[0])
|
|
286
|
+
return self._snapshot(series)
|
|
287
|
+
|
|
288
|
+
def _another_run(self, f, names: list[str]) -> LiveSnapshot:
|
|
289
|
+
"""The file is not the one the last poll read: start over on it."""
|
|
290
|
+
self._start_over()
|
|
291
|
+
return self._poll(f, names)
|
|
292
|
+
|
|
293
|
+
def _read_layout(self, f) -> _Layout | None:
|
|
294
|
+
"""The header matrices, or ``None`` while the file does not hold
|
|
295
|
+
them whole (or holds something this reader cannot follow)."""
|
|
296
|
+
try:
|
|
297
|
+
f.seek(0) # a poll that started over has read rows already
|
|
298
|
+
blocks = _scan_mat4_headers(f).blocks
|
|
299
|
+
if not {"name", "dataInfo", _TRAJECTORY} <= blocks.keys():
|
|
300
|
+
return None
|
|
301
|
+
names, data_info, data_1 = _read_tables(f, blocks, self.path)
|
|
302
|
+
offset, dtype, n_values, _ = blocks[_TRAJECTORY]
|
|
303
|
+
if n_values == 0:
|
|
304
|
+
return None
|
|
305
|
+
sources = {
|
|
306
|
+
name: (index, negate, constant)
|
|
307
|
+
for name, index, negate, constant in _variable_sources(
|
|
308
|
+
names, data_info, data_1, n_values, self.path
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
except Exception:
|
|
312
|
+
# Cut between the scan and the read, or not a binTrans result:
|
|
313
|
+
# whatever a foreign file's bytes decode to, it is not a layout.
|
|
314
|
+
return None
|
|
315
|
+
listed = tuple(n for n in names if n.lower() != "time")
|
|
316
|
+
key = hash((offset, dtype.str, n_values, tuple(names), data_info.tobytes()))
|
|
317
|
+
return _Layout(offset, dtype, n_values, sources, listed, key)
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _rows_between(
|
|
321
|
+
f, layout: _Layout, first: int, present: int, cap: int
|
|
322
|
+
) -> tuple[np.ndarray, int]:
|
|
323
|
+
"""Rows ``[first, present)`` as a ``(rows, values)`` table — every
|
|
324
|
+
one, or ``cap`` of them evenly spaced and ending on the last — and
|
|
325
|
+
the row count the table reaches (short of ``present`` only when the
|
|
326
|
+
file was cut under the read)."""
|
|
327
|
+
row_bytes = layout.row_bytes
|
|
328
|
+
count = max(0, present - first)
|
|
329
|
+
if count <= cap:
|
|
330
|
+
picks = range(first, first + count)
|
|
331
|
+
else:
|
|
332
|
+
step = -(-count // cap)
|
|
333
|
+
picks = range(present - 1 - step * ((count - 1) // step), present, step)
|
|
334
|
+
if not picks:
|
|
335
|
+
return np.empty((0, layout.n_values), dtype=layout.dtype), first
|
|
336
|
+
span = (picks[-1] - picks[0] + 1) * row_bytes
|
|
337
|
+
if picks.step == 1 or span <= _READ_BYTES:
|
|
338
|
+
raw = _read(f, layout.offset + picks[0] * row_bytes, span)
|
|
339
|
+
whole = len(raw) // row_bytes # a short read: its whole rows
|
|
340
|
+
table = np.frombuffer(
|
|
341
|
+
raw, dtype=layout.dtype, count=whole * layout.n_values
|
|
342
|
+
).reshape(whole, layout.n_values)[:: picks.step]
|
|
343
|
+
else:
|
|
344
|
+
rows = []
|
|
345
|
+
for pick in picks:
|
|
346
|
+
raw = _read(f, layout.offset + pick * row_bytes, row_bytes)
|
|
347
|
+
if len(raw) < row_bytes:
|
|
348
|
+
break
|
|
349
|
+
rows.append(raw)
|
|
350
|
+
table = np.frombuffer(b"".join(rows), dtype=layout.dtype).reshape(
|
|
351
|
+
len(rows), layout.n_values
|
|
352
|
+
)
|
|
353
|
+
reached = picks[len(table) - 1] + 1 if len(table) else first
|
|
354
|
+
return table, reached
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _series(layout: _Layout, name: str, table: np.ndarray) -> LiveSeries:
|
|
358
|
+
index, negate, constant = layout.sources[name]
|
|
359
|
+
time = table[:, 0].copy()
|
|
360
|
+
if index is None:
|
|
361
|
+
# A parameter: its one value at every time point.
|
|
362
|
+
values = np.full_like(time, constant)
|
|
363
|
+
else:
|
|
364
|
+
values = table[:, index].copy()
|
|
365
|
+
if negate:
|
|
366
|
+
values = -values
|
|
367
|
+
time.flags.writeable = False
|
|
368
|
+
values.flags.writeable = False
|
|
369
|
+
return LiveSeries(time, values)
|