Results session¶
The document behind post-solve presentation: ResultsSession —
docked mesh and plot panes, one time link, one selection. Results
answers what did the solver write?; the session answers what is on
screen, at which time, with which pictures, with which pick.
Since ADR 0098 this is what results.viewer() opens. The window
(Viewers) is one client that projects the document;
render() is another that draws a still with no Qt at all, and the
snapshot is the document written to disk. VTK actors, docks and
widgets are never truth — which is why a script can build, read and
render exactly what a human arranged in the window.
from apeGmsh import Results
from apeGmsh.results.session import Contour, Deform, Instant
results = Results.demo()
s = results.session() # the document — one empty mesh view
view = s.panes[0]
view.contour = Contour("displacement_x")
view.deform = Deform("displacement", scale=5.0)
s.time = Instant("stage_0", 5)
s.render("tip.png") # a still, no Qt
results.viewer() is sugar for results.session() + show(), and
returns the session once the window closes — still live, so you can
query it, render more stills off it, or snapshot it.
The session — results.session()¶
results.session() boots the default picture: one empty mesh view
(grey analysis mesh, mesh + outlines on, no slots, no legends,
unscoped, undeformed) bound to that broker. Section cuts persisted
under /opensees/cuts/ come back on that view as clips; a cut that
cannot be translated honestly into a plane is skipped with one
[session] line rather than silently widening what disappears.
A session with no results= is valid IR — the snapshot round-trips it
— but realize() and render() refuse loudly, because there is
nothing to read numbers from.
apeGmsh.results.session.ResultsSession ¶
The presentation library's root object.
Constructing one gives you presentation with no window (§1);
render() / realize() (S1) and show() (S2) are clients.
A session of zero panes is valid IR — the results.session()
factory owns the default-one-empty-mesh-view sugar (and the
results= binding the realize clients need).
Source code in src/apeGmsh/results/session/_session.py
results
property
¶
The data broker this session presents (§1: Results
answers "what did the solver write?"; the session answers what
is on screen). None for a bare IR-only session — realize
and render then refuse loudly.
time
property
writable
¶
The session instant. While the link is on this is THE instant of every pane (§7).
time_linked
property
writable
¶
Linked: one instant — scrubber, every mesh view, every plot cursor. Unlinked: each pane keeps its own. If the link is on and moving the plot does not move the meshes, the link is a lie (§7).
selection
property
¶
The one selection set — nodes XOR Gauss, last writer wins.
The owner-facing surface of the one SelectionState (0045
INV-5), never a second store.
pane ¶
The pane with this stable id. A miss is a KeyError — the
id is how render / MCP / snapshot address a pane, so a stale id
must fail loudly, never guess.
Source code in src/apeGmsh/results/session/_session.py
add_view ¶
A new mesh view, booted to the valid empty picture (§3).
Source code in src/apeGmsh/results/session/_session.py
add_plot ¶
add_plot(kind: str = 'history', series: Sequence[PlotSeries] = (), name: Optional[str] = None) -> PlotView
A new plot pane (§6).
Source code in src/apeGmsh/results/session/_session.py
add_plot_from_selection ¶
add_plot_from_selection(quantity: str, kind: str = 'history', name: Optional[str] = None) -> PlotView
The "select → New plot" law (§6/§8): the selection set IS the source. Membership is COPIED into concrete node/gauss sources at creation — mutating the selection afterwards does not touch the plot (a live alias is not v1). An empty selection has nothing to plot and refuses loudly.
Source code in src/apeGmsh/results/session/_session.py
effective_instant ¶
THE instant this pane is at — the §7 law in one place.
- A mode-posed mesh view has no instant, linked or not: the scrubber moves instants and a mode has none (§4/§7).
- Link on → the session instant, for every other pane; pane time is ignored (§9 — set it here; the link ignores it).
- Link off → the pane's own
time/cursor.
Source code in src/apeGmsh/results/session/_session.py
realize ¶
Realize one pane (S1).
A mesh pane is one-shot: it emits its complete layer set
into backend (an ADR 0042 RenderBackend) and returns a
RealizedPane whose layers carry stable keys — the S2
reconciler's diff surface. A plot pane resolves its series
to arrays and returns a RealizedPlot; it needs no backend
(§6: its client draws the numbers). With one pane no id is
needed; otherwise address by pane id.
The projection lives in apeGmsh.viewers.session (imported
lazily here) — the session package itself stays free of the
diagram/Qt/VTK machinery per the S0 purity guard.
Source code in src/apeGmsh/results/session/_session.py
render ¶
render(path: 'str | Path', pane: 'Pane | str | None' = None, *, camera: Optional[str] = None, window_size: tuple[int, int] = (1280, 720)) -> Optional[Path]
Write one offscreen still of a pane (§1). Path or None.
Same skip discipline as results.render (ADR 0094):
APEGMSH_SKIP_VIEWER=1 or no GL context prints the
[skip viewer] notice, writes no file and returns None.
camera= defaults to xy for a planar model, iso
otherwise.
Source code in src/apeGmsh/results/session/_session.py
show ¶
Open the Qt client on this session (ADR 0098 §1, S2).
The window is a projection: the outline lists the panes, the inspector edits the selected one through the §9 Add / change / clear loop, and the viewport reconciles the selected mesh view — every gesture writes THIS session, same as a script would. N tiled panes arrive with S3's pane host; until then one mesh view shows at a time.
blocking=False presents the window on an already-running
Qt loop and returns immediately. APEGMSH_SKIP_VIEWER=1
prints the standard skip notice and returns None. Returns
the SessionWindow handle otherwise.
The Qt client lives in apeGmsh.viewers.session (imported
lazily here) — the session package itself stays free of the
Qt/VTK machinery per the S0 purity guard.
Source code in src/apeGmsh/results/session/_session.py
snapshot ¶
This session as a JSON-safe dict (ADR 0098 §11 S5).
Panes, slots, pose, the time link AND every pane's own instant, the one selection set. Nothing derived (legends are a function of the slots, §5) and nothing about a window: the snapshot is the document, so an agent can draw a still of what a human arranged without Qt.
Source code in src/apeGmsh/results/session/_session.py
save_snapshot ¶
Write :meth:snapshot atomically; returns the path written.
path=None defaults to <results>.viewer-session.json
beside the results file — the old viewer's name, adopted at the
S6a flip now that nothing else writes it (plan decision 11).
A v13 file already sitting there is renamed aside, never
overwritten; :func:~apeGmsh.results.session.rename_legacy_aside
is the guarantee.
Source code in src/apeGmsh/results/session/_session.py
subscribe ¶
Register a change-tick subscriber (no payload — the v1 protocol; the S2 reconciler diffs realize() output instead of consuming granular events).
Source code in src/apeGmsh/results/session/_session.py
Panes¶
A pane is a MeshView or a PlotView. Both carry a stable id
(mesh-1, plot-2, …) — that id is how render(), the snapshot, the
outline and the MCP verb address a pane, so a stale one raises
KeyError rather than guessing.
A plot is a pane, not a dock hanging off a contour: several curves
on one chart are one PlotView, and its series are live queries
against Results evaluated at the cursor. plot.kind is fixed at
creation (history, path or xy); plot.series and plot.cursor
are assignable, and plot.name is a label.
session.remove_pane(pane_id) drops a pane and detaches it, so a
handle you still hold stops ticking the session.
MeshView¶
apeGmsh.results.session.MeshView ¶
A pane on the analysis mesh (§3). No BRep, no CAD.
Booted with the valid empty picture: grey mesh, mesh + outlines on, nodes / gauss off, no slots, no legends, unscoped, undeformed.
Source code in src/apeGmsh/results/session/_views.py
id
property
¶
Stable pane id — how render, the MCP verb, the outline and the snapshot address this pane (§1).
scope
property
writable
¶
One composition axis + checked names, or None = whole mesh. Scope chooses the ONE cell set every layer of this view is a function of (INV-MESH-1); it is a different knob from selection (§8).
deform
property
writable
¶
The pose — off (None) or on (field, scale, mode?). Never a picture, never a legend (§4/§5).
time
property
writable
¶
This pane's own instant. Ignored while the session link is on (§7/§9 — set it here; the link ignores it); irrelevant for a mode pose, which has no instant.
style
property
writable
¶
The four style buttons (INV-MESH-4) — independent toggles of cells that are ON. They never change the cell set or the selection set (§8).
overlay
property
writable
¶
Undeformed overlay — this mesh at scale 0 in the Outlines style, same cell set (§3). Never CAD, never a second Geometry.
pick_target
property
writable
¶
The Nodes | Gauss radio (§8). Aims clicks and windows in THIS view only; it neither owns nor clears the session's one selection set.
is_mode_posed
property
¶
Whether the pose is a mode shape — no instant, frozen under the session time link (§4/§7).
legends ¶
The colour scales this view carries — derived, never stored: one per distinct field over the occupied colour-mapped slots (INV-LEGEND-1/-2/-5). Same quantity on two slots → one scale. Deform on with every slot empty → zero legends.
Source code in src/apeGmsh/results/session/_views.py
set_legend_hidden ¶
Hide/show one scale — view chrome (INV-LEGEND-3). Does not touch the slot: the picture stays painted. Refuses a field no occupied colour-mapped slot causes (that legend does not exist — INV-LEGEND-2).
Source code in src/apeGmsh/results/session/_views.py
legend_placement ¶
Where this view's scale for field was dragged, or None
for the automatic stack (ADR 0098 A5.3). Realize reads this to
seed the controller.
Source code in src/apeGmsh/results/session/_views.py
legend_placements ¶
Every hand placement on this view, keyed by field. A copy — the caller must not mutate the view's record.
set_legend_placement ¶
set_legend_placement(field: str, anchor: 'tuple[float, float]', font_scale: Optional[float] = None) -> None
Record that one scale was placed by hand (ADR 0098 A5.3).
Refuses a field no occupied colour-mapped slot causes, the same
rule :meth:set_legend_hidden enforces — placement is state
ABOUT a legend, and a legend that does not exist cannot have
any.
This records; it does not repaint. The gesture that calls it has
already moved the live bar through the controller, and the
record is what makes the move survive the next realize, a theme
change and a snapshot. Placement is deliberately absent from the
reconciler's structure signature (see :class:LegendPlacement),
so a set here costs no realize — which also means a purely
programmatic call on a live window takes effect at the next
realize rather than immediately.
Source code in src/apeGmsh/results/session/_views.py
clear_legend_placement ¶
Return one scale to the automatic stack — what redock
records. Unknown or unplaced fields are a no-op, so a redock of
an already-docked legend costs nothing.
Source code in src/apeGmsh/results/session/_views.py
add_clip ¶
add_clip(normal: Sequence[float], *, offset: float = 0.0, name: Optional[str] = None, active: bool = True, flipped: bool = False, gizmo_visible: bool = True) -> ViewClip
Cut this view with a half-space plane; returns the record.
normal is normalised on the way in and points into the half
that SURVIVES; offset is the signed distance from the origin
along it. A clip belongs to the view, not to a slot — every
layer the pane draws is cut by it (ADR 0083 field shape, ADR
0098 ownership). Persisted section cuts boot as clips this way.
Source code in src/apeGmsh/results/session/_views.py
remove_clip ¶
Drop one clip by its plane_id. KeyError if absent —
a silent no-op would leave the caller believing the model is
uncut when it is not.
Source code in src/apeGmsh/results/session/_views.py
set_clip ¶
Replace fields of one clip (frozen record swap). plane_id
is identity and cannot be changed.
Source code in src/apeGmsh/results/session/_views.py
PlotView¶
apeGmsh.results.session.PlotView ¶
PlotView(pane_id: str, kind: str = 'history', series: Sequence[PlotSeries] = (), name: Optional[str] = None, on_changed: _Notify = None)
A plot is a pane, not a dock of a contour (§6).
kind is fixed at creation; series are live queries against
Results evaluated at the cursor. A history plot shows the whole
record with the cursor as time; a path plot is evaluated at the
current instant (§7).
Source code in src/apeGmsh/results/session/_views.py
cursor
property
writable
¶
This plot's own instant. Rides the session instant while the link is on (§7).
The slot catalog¶
A mesh view carries slots, from a catalog of exactly seven categories:
| Slot | Record | Carries | Colour-mapped |
|---|---|---|---|
contour |
Contour |
quantity + averaged / unaveraged |
yes |
vector |
Vector |
quantity (nodal vectors and Gauss principal families) | yes |
gauss |
Gauss |
quantity at integration points | yes |
line |
Line |
member-diagram component (N / V / M / torsion) | no |
sand |
Sand |
quantity | yes |
loads |
Loads |
pattern (or None for the default) |
no |
reactions |
Reactions |
— | no |
Each category name in that first column is an assignable property of
the mesh view — view.contour, view.vector, view.gauss,
view.line, view.sand, view.loads, view.reactions — reading the
occupant or None. (They are built by a factory, so they do not appear
in the generated member list below; the table is their reference.)
The catalog is closed. Different categories stack; one category
holds at most one occupant per view, and filling an occupied slot
replaces the occupant — there is no "add a second contour". Assign
None to clear, and a record of the wrong category refuses loudly.
view.contour = Contour("displacement_x")
view.contour = Contour("displacement_x", averaging="unaveraged")
view.slots
# {'contour': Contour(quantity='displacement_x', averaging='unaveraged')}
view.contour = None # clears
view.contour = Deform("displacement")
# TypeError: The 'contour' slot takes a Contour record or None; got Deform.
Fibers, shell layers and isochrones do not open an eighth slot: a new category is an ADR 0098 amendment, not a subclass.
apeGmsh.results.session._slots ¶
Result-slot records — the closed catalog of ADR 0098 §4.
Seven categories, at most one occupant each per mesh view; different categories stack; filling an occupied slot replaces the occupant. The records here carry only what §4 puts inside the slot (quantity / component / averaging / pattern) — render style (cmap, clim, scale …) is not slot identity and is a later, additive widening.
Tokens are opaque at this layer: quantity / component /
pattern are broker vocabulary strings resolved by S1's realize
mapping (contour averaging → ContourStyle.averaging, the vector
quantity → the vector_glyph / principal_glyph resolver, the line
component → the LineForceStyle axis machinery). S0 stores them;
it does not interpret them.
The catalog is CLOSED (amended ADR 0094 INV-10): a new category is an ADR 0098 amendment, not a subclass. Fibers, shell layers, isochrones and spring-as-its-own-kind do not open an eighth slot.
SLOT_CATALOG
module-attribute
¶
SLOT_CATALOG: dict[str, type] = {'contour': Contour, 'vector': Vector, 'gauss': Gauss, 'line': Line, 'sand': Sand, 'loads': Loads, 'reactions': Reactions}
COLOUR_MAPPED
module-attribute
¶
Slot
dataclass
¶
Base for the seven slot records. Empty — categories share no fields; the base exists so a mesh view can say "this is a slot record" without enumerating the catalog.
Contour
dataclass
¶
Bases: Slot
One heatmap: quantity + averaged | unaveraged (§4). Colour-mapped.
averaging uses the ADR vocabulary; S1 maps "unaveraged" to
the existing ContourStyle.averaging="discrete" token. It is
contour display state, not field identity — it never breaks the
one-scale match with a Gauss slot of the same quantity (§5).
Vector
dataclass
¶
Bases: Slot
Arrows: one quantity token spanning nodal vector fields AND
Gauss principal families (§4 — vector_glyph and
principal_glyph collapse to this one slot). Colour-mapped.
S1's resolver routes the token to the right emit path.
Gauss
dataclass
¶
Bases: Slot
Values at integration points (§4). Colour-mapped; shares the contour's scale when the field matches (§5 — same quantity token). Gauss values are unaveraged by nature, so there is no averaging field here.
Line
dataclass
¶
Bases: Slot
Member diagrams — amplitude fill, NOT colour-mapped (§4/§5).
One component in v1 (N / V / M / torsion); a later widening may put several inside this one slot — never a second category.
Loads
dataclass
¶
Bases: Slot
Applied loads — uniform glyphs, NOT colour-mapped (§4).
pattern selects the pattern / stage; None = the default the
realize layer resolves.
Reactions
dataclass
¶
slot_field ¶
The field identity a slot contributes to the legend law (§5).
"Field matches" = the same quantity token; averaging is display state and does not participate. Non-colour-mapped categories emit nothing and have no field identity.
Source code in src/apeGmsh/results/session/_slots.py
Deform is a pose, not a slot¶
view.deform warps the mesh. It is deliberately outside the
catalog, and the reason shows up in the legend: a pose is not a
picture of a quantity, so a warped mesh with every slot empty draws no
colour scale at all. Warping by displacement while contouring stress
gives one scale, and it says stress.
Deform(mode=…) makes it a mode pose: the view then has no
(stage, step) at all and is frozen under the session time link.
apeGmsh.results.session.Deform
dataclass
¶
The pose of a mesh view — off (view.deform = None) or
(field, scale, mode?). A pose, never a picture: it emits no
legend (§5 INV-LEGEND-1).
scale=None means auto-fit at realize (the existing attach-time
convention). mode set makes this a mode pose: the view has no
(stage, step) and is frozen under the session time link (§4/§7).
The mode index is an opaque token here; S1 resolves it against the
broker's modal stage.
The legend law¶
Legends are derived, never stored: one scale per distinct field over the view's occupied colour-mapped slots. Two slots showing the same quantity therefore share one scale, clearing a slot destroys its scale, and hiding a scale is chrome — it hides the scale, not the picture, and does not clear the slot.
from apeGmsh.results.session import Gauss
view.deform = Deform("displacement", scale=5.0)
view.legends() # () — a pose causes no scale
view.contour = Contour("stress_xx")
view.gauss = Gauss("stress_xx")
[lg.field for lg in view.legends()] # ['stress_xx'] — ONE scale
view.legends()[0].categories # ('contour', 'gauss')
view.set_legend_hidden("stress_xx") # chrome; the picture stays
view.set_legend_hidden("displacement_x")
# ValueError: No legend for field 'displacement_x' on this view — legends
# exist only for occupied colour-mapped slots (have: ['stress_xx']).
Nothing above reads a number: legends() is a function of the slots,
so it answers on any view, bound to results or not.
apeGmsh.results.session.Legend
dataclass
¶
One derived colour scale of one mesh view (§5). Never stored:
legends = f(occupied colour-mapped slots). field names the
slot quantity (INV-LEGEND-4); categories are the slots that
cause it — two slots of the same field share the one scale.
hidden is view chrome (INV-LEGEND-3): hiding the scale does not
hide the picture, and does not clear the slot.
Scope, style and clips¶
scope picks the ONE cell set every layer of the view is a function
of — one composition axis (physical_groups, materials or
element_types) plus checked names, never a boolean concrete AND
hex. style is the four independent buttons over cells that are
already on. Clips are the view's own section planes, added and edited
through the view (the plane id is identity and cannot be reassigned).
from dataclasses import replace
from apeGmsh.results.session import Scope
view.scope = Scope("physical_groups", ("Cols",))
view.style = replace(view.style, nodes=True)
clip = view.add_clip((0, 0, 1), offset=1.0, name="mid-height")
view.set_clip(clip.plane_id, offset=2.0, flipped=True)
view.remove_clip(clip.plane_id)
A scope name the model does not carry is a ValueError at realize
time, and it names what the model does have.
add_clip takes the plane normal (normalised for you) plus keyword
offset, name, active, flipped and gizmo_visible, and returns
the minted ViewClip. remove_clip(plane_id) drops one, raising
KeyError on an unknown id.
apeGmsh.results.session.Scope
dataclass
¶
What a mesh view draws — ONE composition axis plus one or more
checked names on it, or all names on that axis (§3/§9; never a
boolean concrete AND hex).
names=None = every name on the axis; view.scope = None = no
scope at all (the whole analysis mesh). Names are opaque tokens
here — resolution against the data (including the materials /
element-type indexes) is S1/S3 work.
apeGmsh.results.session.MeshStyle
dataclass
¶
The four style buttons (§3 INV-MESH-4) — view chrome, not slots.
gauss here draws integration-point locations (required to
click-pick Gauss); the gauss slot draws values. They must not
draw two clouds — the realize layer owns that merge.
Boot picture: mesh + outlines on, nodes / gauss off (§3).
apeGmsh.results.session.ViewClip
dataclass
¶
ViewClip(plane_id: str, name: str, normal: tuple[float, float, float] = (1.0, 0.0, 0.0), offset: float = 0.0, active: bool = True, flipped: bool = False, gizmo_visible: bool = True)
One section plane of one view — copies the ADR 0083 ClipPlane
field shape verbatim (the 0083 machinery is kept; ownership moved
viewer → view). Frozen here: edits go through
:meth:MeshView.set_clip, which replaces the record and ticks.
The six-active-planes GL cap is a render-time constraint enforced where planes meet a backend (S1/S3), not by the IR.
Time¶
An instant is (stage, step) — a name, so the step is a concrete
recorded index and negative aliases like -1 are rejected. While the
link is on, the session instant is the instant of every pane; turn it
off and each pane keeps its own. effective_instant() is that law in
one place, mode poses included.
s.time_linked # True
s.time = Instant("stage_0", 3)
s.effective_instant(view) # Instant(stage='stage_0', step=3)
view.deform = Deform("displacement", mode=1)
s.effective_instant(view) # None — a mode pose has no instant
apeGmsh.results.session.Instant
dataclass
¶
One recorded time sample: (stage, step).
stage is the stage id string (Results StageInfo.id);
step is the 0-based recorded step index within that stage.
Negative indices are rejected: an instant is a name, and letting
-1 alias the last step would give one instant two unequal names
(poison for the linked-time comparison and the S5 snapshot).
Selection and plots¶
The session holds one selection set, nodes XOR Gauss: a write of
the other kind replaces the whole set rather than mixing the two. A
mesh view's pick_target only aims clicks in that view; it neither
owns nor clears the set.
"Select → new plot" copies the membership into concrete sources at creation — the plot does not track later edits to the selection.
from apeGmsh.results.session import PlotSeries, PlotSource
plot = s.add_plot(
"history", series=[PlotSeries(PlotSource.node(9), "displacement_x")],
)
s.selection.set_nodes([5, 9])
s.selection.kind # 'nodes'
tip = s.add_plot_from_selection("displacement_x")
len(tip.series) # 2
realized = s.realize(pane=tip) # arrays, no backend needed
realized.series[0].label # 'node 5 · displacement_x'
apeGmsh.results.session.SessionSelection ¶
Nodes-XOR-Gauss surface over one real SelectionState.
Four writers (click, window, outline select-all, code) all land
here; S0 ships the code writer. The per-view Nodes|Gauss radio
(MeshView.pick_target) only AIMS clicks — it neither owns nor
clears this set.
Source code in src/apeGmsh/results/session/_selection.py
state
property
¶
The one underlying store (ADR 0045 / INV-5). The Qt client binds here; nothing may construct a second store.
kind
property
¶
"nodes" | "gauss" | None (empty set). Derived
from the set — the writers keep it homogeneous by law.
A heterogeneous or foreign-substrate set means some writer bypassed this surface (the raw store enforces no law); that is a programming error and reads FAIL LOUD rather than return a subset that silently misrepresents the store (probe H26).
gauss
property
¶
The selected (element_id, gp_index) pairs (empty unless
kind == "gauss").
set_nodes ¶
Replace the set with these nodes (one SET gesture).
set_gauss ¶
Replace the set with these Gauss points (one SET).
add_nodes ¶
Extend a node set — or, on a Gauss set, REPLACE it (the §8 last-writer law: a write of the other kind replaces).
Source code in src/apeGmsh/results/session/_selection.py
add_gauss ¶
Extend a Gauss set — or, on a node set, REPLACE it.
Source code in src/apeGmsh/results/session/_selection.py
toggle_node ¶
Ctrl+click on a node: add it, or drop it if already in.
On a Gauss set this is a write of the other kind, so the §8 last-writer law applies unchanged — the set is REPLACED by this one node rather than becoming heterogeneous.
Source code in src/apeGmsh/results/session/_selection.py
toggle_gauss ¶
Ctrl+click on a Gauss point — the mirror of
:meth:toggle_node, replacing a node set.
Source code in src/apeGmsh/results/session/_selection.py
apeGmsh.results.session.PlotSource
dataclass
¶
One series source (§6): a node, a Gauss point, a label, or a
physical group. The session selection is NOT a source kind — the
"select → New plot" flow COPIES the membership into concrete
node/gauss sources at creation (a live alias is not v1). "Live"
means live against Results at the cursor, not live membership.
apeGmsh.results.session.PlotSeries
dataclass
¶
One curve: a source + a quantity token. Several series on one chart are one plot view (§6).
Snapshot — the document on disk¶
session.snapshot() is the whole document as a JSON-safe dict: panes,
slots, pose, the time link and every pane's own instant, the one
selection set. Nothing derived (legends are a function of the slots)
and nothing about a window — which is the point: an agent can redraw
what a human arranged, with no Qt anywhere.
save_snapshot() writes it atomically to
<results>.viewer-session.json beside the results file — the same file
the window auto-saves on close.
from apeGmsh.results.session import load_snapshot
view.contour = Contour("displacement_x")
path = s.save_snapshot() # <results>.viewer-session.json
restored = load_snapshot(path, results=results)
restored.session.panes[0].contour # Contour(quantity='displacement_x', ...)
restored.notices # () — nothing degraded
load_snapshot returns a RestoredSession carrying notices:
degradation is not refusal, so a snapshot naming a stage these results
no longer have still restores its panes and slots, drops the instant,
and says so. A file from the retired v13 viewer is not restorable — it
is renamed aside to ….viewer-session.json.legacy and never
overwritten.
apeGmsh.results.session._snapshot ¶
Session snapshot — the JSON of what the human built (ADR 0098 §11 S5).
One file, one session: panes, their slots, the pose, the time link and
each pane's own instant, the one selection set. An agent can then draw
a still of what a human arranged (render of a snapshot, S5c) and a
pin can carry it (results_pin, S5b, under the record key
session_snapshot).
The schema is frozen at version 1 against exactly what S0 shipped.
S4 never widened the time surface — plan decision 9 landed on a
one-stage-at-a-time scrubber, which is a WIDGET choice: Instant is
(stage, step) under either traversal — so S5b's contract can
publish without paying a second version bump.
Amendment 5 adds one OPTIONAL pane key, legend_placement, and does
not bump the version either: a v1 file without it restores to the
automatic legend stack (the pre-amendment behaviour, and the default),
and a reader that does not know the key ignores it. Additive and
compatible in both directions is the bar for staying at 1; anything
that changed the meaning of an existing key would not clear it.
Two failure families, deliberately not alike (plan decision 15):
- Schema / ontology violations refuse loudly. An unknown slot category is the loudest of them: the §4 catalog is CLOSED (amended ADR 0094 INV-10), and this refusal is that amendment's enforcement point, not a nicety. Restore builds the real frozen records, so every law S0 wrote — the closed catalog, the scope axes, the deform fields, the plot kinds, no negative steps — is enforced on the way in by the same validator a script hits. There is no second, weaker copy of the laws here.
- Data mismatches degrade with a notice on
RestoredSession.notices. An instant naming a stage these results no longer have drops toNone(realize's documented "last stage, last step"); a stage rename must not cost the human every pane, slot and scope in the file. Silence is the only forbidden option.
What is NOT snapshot state: the SelectionLog op history (nothing
realizes from it, and a replayed gesture has no model left to hit — the
set restores as ONE honest SET write), the derived legends (§5:
legends = f(occupied colour-mapped slots); only the per-field
hidden chrome is state), and every widget geometry — the window is a
projection, never truth.
The legacy gate: the S6a flip ADOPTED <results>.viewer-session.json
(plan decision 11), so save-on-close now writes exactly where a v13
file from the retired window lives. A v13-shaped file therefore gets a
notice and a .legacy rename-aside — and never an overwrite, of
the original or of an existing .legacy. :func:legacy_shape is the
bare predicate so S5c's MCP verb can refuse an old file without
renaming anything (the rename belongs to the human flow), and
:mod:apeGmsh.results.session._boot is where the window's open policy
turns a refused rename into a notice plus a disarmed auto-save instead
of a window that will not open.
RestoredSession
dataclass
¶
A restored session plus every degradation it survived.
notices is empty for a clean restore. It is a RETURN value, not
a log line, because the caller decides how loud to be: the window
shows them, the MCP verb reports them, a test asserts them. What no
caller may do is not know.
SnapshotError ¶
Bases: ValueError
The file is not a session this ontology has — a schema or ontology violation (unknown slot category, unknown pane kind, missing marker, unknown version). Loud by design.
LegacySessionFile ¶
Bases: SnapshotError
The file is the OLD viewer's v13 session (viewers.diagrams).
Raised by :func:load_snapshot when rename_legacy=False — the
contract S5c's MCP verb needs: refuse an old-schema file, never
rename it. The human flow renames it aside instead.
Source code in src/apeGmsh/results/session/_snapshot.py
snapshot ¶
This session as a JSON-safe dict (schema
:data:SNAPSHOT_VERSION).
Panes in creation order; nothing derived is stored (legends are a function of the slots, §5) and nothing about a window is stored.
Source code in src/apeGmsh/results/session/_snapshot.py
save_snapshot ¶
Write this session's snapshot; returns the path written.
Atomically (ADR 0095 INV-16, via the shared
:func:apeGmsh._atomic_io.atomic_write_text): a reader may see the
file missing mid-replace, never a truncated JSON body. path=None
uses :func:default_snapshot_path, which needs a Results opened
from disk.
Source code in src/apeGmsh/results/session/_snapshot.py
load_snapshot ¶
load_snapshot(path: 'str | Path', results: 'Optional[Results]' = None, *, rename_legacy: bool = True) -> Optional[RestoredSession]
Read a snapshot from disk. None when the legacy gate fired.
An old v13-shaped viewer session is not restorable (ADR 0098
Consequences). In the human flow (rename_legacy=True) it earns
a notice and a .legacy rename-aside, and this returns None
— the caller boots a fresh session. With rename_legacy=False
(S5c's MCP verb) it raises :class:LegacySessionFile and touches
nothing on disk.
Source code in src/apeGmsh/results/session/_snapshot.py
restore_snapshot ¶
Build a session from a snapshot dict.
results= binds the broker the restored session presents; pass
None for an IR-only restore (nothing to validate instants
against, so they restore verbatim).
Source code in src/apeGmsh/results/session/_snapshot.py
default_snapshot_path ¶
<results>.viewer-session.json beside the results file.
The old viewer's name, adopted at the S6a flip (plan decision 11)
now that nothing else writes it. A file already at this path may
therefore be a v13 session from the retired window — which is what
:func:legacy_shape and :func:rename_legacy_aside below are for.
Source code in src/apeGmsh/results/session/_snapshot.py
legacy_shape ¶
Whether data is the OLD viewer's session (v13 and friends).
The bare predicate, so S5c's MCP verb can refuse an old-schema file
while renaming nothing. Told apart by keys, never by version
arithmetic: the old envelope carries an int schema_version plus
the retired ontology's diagrams / geometries blocks, and no
:data:SNAPSHOT_KIND marker.
Source code in src/apeGmsh/results/session/_snapshot.py
rename_legacy_aside ¶
Move an old viewer session to <path>.legacy. Never destroys.
The ADR keeps the old file so a one-shot importer would still have
its input, which is worth nothing if the second open overwrites the
first rename. So an existing .legacy is REFUSED, not replaced —
and the destination is reserved with an exclusive create before the
replace, making that guarantee atomic rather than advisory.
Source code in src/apeGmsh/results/session/_snapshot.py
See also¶
- Render a deformed shape or contour — the scripted path, end to end.
- Viewers — the Qt window that projects this document.
- Results — the broker the session reads from.