simapy_results
Read SIMA results as a lazy signal hierarchy.
from simapy_results import open_results
res = open_results(r"C:\ws\TestWs\Simple_Flexible_Riser\Initial")
sig = res.run.signal("Static/RiserLine/Static forces/EFF. TENSION")
sig.unit, sig.xunit, len(sig) # ('N', 'm', 65) -- no numbers parsed yet
sig.y[:3] # array([183114.1, 184240.9, 185366. ])
A port, not a parallel design
The tree, the merge rules, the sibling order and select are ports of SIMA's
own code (ConditionResultImporter, SignalItemUtil, SignalItemQuery). Where
behaviour looks odd, matching SIMA is usually the reason:
- Signals merge without deduplication. Importing the same file twice doubles
its signals, as in SIMA. Repeats are renamed as SIMA's tree view renames them:
Tension,Tension_duplicate_1, … - Containers merge by attribute union. Two same-named containers merge unless both set one key to different values; such a pair stays separate and warns.
- Siblings sort with SIMA's comparator, which is not a total order: an
untyped node ties with everything. That is why
RiserLinestays ahead ofStatic iterations.
A condition, a run folder, or a file
Point open_results at a condition folder, at one working directory a solver
left its files in, or at one result file. Beside the results, a few files carry
what is not itself a result. All are optional:
| file | gives | without it |
|---|---|---|
names.map, riflexres.inp |
hashed model id to name -- Risea63d to RiserLine; riflexres.inp wins, as in SIMA's MPF import |
ids stay hashed |
riflexres.inp, for RIFLEX storage |
the units the values were written in, the element angles to derive, support vessel names | values scaled from kN, Mg, kW and kPa; no angles; vessels by number |
results.json |
the task and condition, the variables with their units, and each analysis's working directory | names from the path, variables from variables.inp, one folder at a time |
variables.inp |
the variable values alone, and the task name | no Variables container |
SIMA writes results.json from 5.4, and SIMA.run_condition reads the
condition through it, so it refuses a run that wrote none. A variable's value
and unit come from one place: results.json when it gives values, else
variables.inp, whose variables read unit -. The folder path names the run too --
<workspace>/<task>/<condition>, skipping timestamp folders such as
1-20260901063413 -- but only until the results are moved.
A condition is what results.json says it is. Static and dynamic can run
into separate timestamp folders, and with folder deletion off, stale ones
accumulate; nothing in the folders pairs them. The condition's results.json
maps each analysis to its working directory, so opening the condition folder
reads exactly those, and each folder gives only its own analysis -- SIMA copies
static results such as psc.lis into the dynamic folder too.
Without that map the caller says which run it means. The condition folder then
raises ResultsError naming its runs:
open_results(condition) # the whole condition
open_results(condition / "1-20260901083636") # one run folder
open_results(condition / "1-20260901083937" / "ConditionSet_1") # one set member's folder
Which files are results
SIMA decides mostly by hard-coded name: an allowlist. What is reproduced here is
a denylist of working files (simo.h5, env.h5, intermediates) plus "some
reader claims the extension". That is exact while the readers claim only mpf,
h5, hdf5, b5, bin, asc and one exact name, psc.lis, because nothing
else in a run folder (.inp, .dat, the other .lis, .ldat, …) uses those.
Unclaimed files are left out of sources rather than reported as gaps, with one
exception below.
RIFLEX storage is a .bin or .asc beside its key file --
sima_noddis.bin beside key_sima_noddis.txt. SIMA claims every .bin, but
turbulence boxes (wind_u.bin), FEM *_eledat.asc and fibre files share the
extensions, so without its key file a file is no result here. Every storage file
goes in Dynamic, as SIMA files it, whatever its name says. SIMA reads only the
binary files; the ASCII ones have the same key-file layouts and are read too.
Storage simapy does not read is reported, not skipped. SIMO's old result
structure (results.tda) is a result, so dropping it would leave a tree that
looks complete without its time series. It shows in sources as
unsupported, and an UnsupportedStorageWarning names the SIMA setting that
stores it as HDF5.
simo-static-results.h5 and simo-dynamic-results.h5 are results despite being
on SIMA's list of working files; SIMA adds them back explicitly, and so does this.
.json is never picked up by a scan, though a reader reads it: a workflow writes
its input and its output as the same signals model in one folder, and nothing in
either says which is which. Open the file by name.
psc.lis is copied between runs: SIMA puts the static run's into every
dynamic folder. With directories the copy is never read, since each folder
gives only its own analysis. Without them a folder offers psc.lis only beside
a static result of its own (simo-static-results.h5, a stamod file), so a
dynamic folder never claims it. A new reader needs the same look. Its names come
from the simores.inp SIMA writes beside it; without that file the source
fails, because SIMA's fallback is the model.
Laziness
| source | at open | values |
|---|---|---|
| HDF5 | shapes and attributes | the file is reopened and sliced on each access, uncached |
| MPF | headers, row counts and byte offsets | a matrix parsed whole on first touch, cached |
| JSON | the whole document | already in memory |
psc.lis |
the whole file, with simores.inp |
already in memory |
| RIFLEX storage | the key file and the first two time steps; an .asc is counted line by line |
the file read whole on first touch, cached |
name, type, attributes, unit, xunit, len() and navigation never
parse a number. The HDF5 reader reopens rather than holding datasets open,
because keeping every result file open for the life of the tree leaks
descriptors.
A SIMO time series has no stored x-axis: just start and delta, as in SIMA.
Signal.x is synthesised on each call and never kept, and Signal.same_axis
compares start, delta and size without reading anything. That lets a whole SIMO
body become one frame with a single time index:
res.run.container("Dynamic/Platform").to_dataframe(recursive=True) # (100, 26)
Selecting signals
select applies SIMA's signal requirements, so a requirement copied from SIMA
selects the same signals:
res.select("*.Dynamic.*.F-MAX") # path requirements
res.select(name=["XG translation", "YG translation"]) # one key: alternatives
res.select("*.RiserLine.*", name="F-MAX") # different keys: all must hold
- A path is SIMA's: names joined by
.from the root, the task first --Simple_Flexible_Riser.Initial.Dynamic.RiserLine.Force envelope curve.F-MAX. As in SIMA, a.inside a name is indistinguishable from the separator. *matches anything, across levels too;?at most one character.- A path without
*names a node exactly, and naming a container selects its subtree. Likewise, a container meeting every requirement brings all its signals.
The query-string mode (name==*Fx* && line!=Riser1) is not ported.
The result maps / paths, relative to the container called on, to signals, so
each key goes straight back into [].
numpy is the core; pandas is a view
Signal.x and Signal.y are read-only ndarrays. pandas and h5py are optional:
pip install simapy-results[pandas], simapy-results[hdf5], or simapy-results[all].
A container is the natural frame, since it is one source block -- one RIFLEX
MATRIX, one HDF5 group -- and its signals normally share an x-axis. Signals
whose axes differ are refused rather than aligned, because RIFLEX x-axes
repeat values: a curvature is sampled at both ends of every element
([0, 2, 2, 4, 4, …]), and aligning two such axes would invent data.
index=False gives positional semantics instead.
Condition sets
Opening a set's condition folder gives the tree SIMA's post processor shows:
task, then the set (type conditionContainer), then one condition per member,
each with its own Variables first and its analyses after. The set's own
Variables hold what it keeps constant. res.description.members describes
each member.
FORCE = "Dynamic/Platform/Positioning elements/MooringLine1/Total force"
res = open_results(condition)
for member in res.description.members:
print(member.variables["hs"].value, res.run.container(member.condition).signal(FORCE).y.max())
Best effort
Opening a folder never raises because one result file is unreadable. The failure
lands in res.sources and as an ErrorSignal where the data should have been,
as in SIMA. ResultsError is raised only when the source is neither a folder nor
a file, is a file no reader claims, or is a folder holding no result file.
for src in res.sources:
print(f"{src.file:26} {src.type:4} {src}")
# sima_stamod.mpf mpf read (15 signals)
# sima_elmfor.h5 h5 failed: SourceError: ...
One public module, and a read-only tree
simapy_results is the whole API; every module behind it is private, so readers,
registry, importer, merge rules and scanner can change. The tree is read-only:
nodes expose accessors only, an array handed out is either fresh or
non-writeable, and the functions that fill a tree live in _build. Adding a
format means adding a reader here, not registering one from outside.
Verified against SIMA
test_results_oracle.py checks container shape, names, units, lengths and end
values against riflex_run_oracle.json, a snapshot of the file SIMA's post
processor wrote to Riflex_Results.h5 from the same run. The HDF5 reader reads
all 44 of those signals back from that file under their true names, so the MPF
and HDF5 readers agree by independent routes.
test_results_psc.py checks every body, container, signal, unit and value read
from psc.lis against psc_oracle.json, the tree SIMA built from the same
static run.
HDF5 keys cannot hold ., / or non-ASCII, so the exporter writes
EFF_ TENSION and keeps EFF. TENSION in a name attribute (and suffixes
_duplicate_N on collisions). The reader looks nodes up by key while building,
then renames them to the true name.
The two HDF5 layouts
Dispatch is on the root format attribute. _readers/hdf5.py reads the
unformatted layout RIFLEX writes (ts_buffer.b5, Riflex_Results.h5);
_readers/simos_r0.py reads SIMO's. In the latter, metadata lives in child
datasets (value, unit, xunit, name), array members are keyed 0, 1, …
and named by a name dataset, and values are rescaled to base units as both
Java implementations do -- reading them untouched would be off by 1000 on most
forces.
SIMO's own result files, simo-static-results.h5 and simo-dynamic-results.h5,
get what SIMA's SIMOHDF5ResultsImporter does on top: the root results level
goes, and each body becomes a container of type body directly under its
analysis.
Units
_units.py ports SIMA's unit registry and label grammar rather than tabulating
answers. Resolution is per atom: kN/m^2 is {kN: 1, m: -2}, each atom mapped
to its base and recomposed, so kN*m/deg works unanticipated. The grammar is
SIMA's, quirks included -- / binds loosest, so a/b*c is a/(b*c).
test_results_units.py transcribes SIMA's own unit tests; the three deliberate
divergences (whitespace, colliding exponents, empty atoms) are documented in the
module and tested.
A label SIMA does not know keeps the file's numbers and label, is marked with
an unscaled attribute, and warns once per axis when read. Scaling the known
atoms only would turn kN/glorp into numbers labelled kN that are not.
Signal.to() has no SIMA counterpart: SIMA scales on the way in and never back.
It works per atom too, and to("kN").to("N") is the identity. Base units SIMA
keeps apart -- deg/rad, s/h, Hz/rpm/1/s -- never convert on the way
in; to() bridges them on request, but only where the factor is exact by
definition (no year, no C). Pa is not bridged to N/m^2, because SIMA
treats pressure as its own quantity.
Not done
ldatandres, never: SIMA registers them for its own use (3D view, listing output), and they hold nothing to compare.- Reading TDA, being phased out for HDF5. It is reported instead.
- The stress SIMA computed from RIFLEX element forces in run folders from before sima/issue#940; RIFLEX computes it since.
- Writing. The tree stays decoupled from the DMT signals model in
simapy.sima.signals; if results ever go back into SIMA, those classes already serialise to signals JSON and HDF5. - Eigenvalue and outmod post-transforms in the MPF reader.
- Post-processing: resampling between SIMO and RIFLEX, statistics, spectra.
- An
s2xreader, and replacing the srv integration tester's MPF reader -- both wait for the migration insrv.
Metadata
Release files for simapy-results 5.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| simapy_results-5.4.0.tar.gz | 116.9 kB | Without provenance | Details for simapy_results-5.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simapy_results-5.4.0-py3-none-any.whl | Python 3 | none | any | Details for simapy_results-5.4.0-py3-none-any.whl |
Total release size: 192.9 kB
Release files / simapy_results-5.4.0.tar.gz
| Download URL | simapy_results-5.4.0.tar.gz |
|---|---|
| Size | 116.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c8a2a4961f93762346ecbbfd276e0b1f7de1483d1bbf40082516f42477906032
|
|
BLAKE2b-256 checksum How to use checksums |
353c9d3ea02730e180a712afc79c5469487378ab3cb00c08c917a593acc559d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Release files / simapy_results-5.4.0-py3-none-any.whl
| Download URL | simapy_results-5.4.0-py3-none-any.whl |
|---|---|
| Size | 76.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e150491dcb6c94cadd4a438e732d2ba889a1a5dbdc25947f8a1c84d713d2fd31
|
|
BLAKE2b-256 checksum How to use checksums |
b3207a2c98efe23504d6d9182dc6456fd7f84244438bfc19fadb62784064a378
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Security for simapy-results 5.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.