Architecture¶
Module overview¶
ops_post/
├── __main__.py CLI entry point (same-name convention)
├── tcl_parser.py TCL model parser + .postdata generator
├── model.py Data model (dataclasses)
├── mpco_reader.py File I/O (HDF5 + .postdata)
├── ops_elements.py Element type registry (61 types)
├── mesh_builder.py PyVista mesh construction
├── result_processor.py Result extraction + mapping
├── gui.py PyQt5 tabbed UI (Results + View), in-place
│ mesh updates, persistent HDF5, export
└── utils.py Arcball interactor, von Mises, utilities
Workflow¶
ops-post my_model
|
┌──────────┴──────────┐
v v
my_model.tcl my_model.mpco
| |
v v
tcl_parser.py mpco_reader.py
(parse nodes, (read HDF5:
elements, nodes, elements,
sections, sections, results)
geomTransf) |
| |
v |
my_model.mpco.postdata |
(local axes, beam |
profiles, elem info) |
| |
└────────┬───────────┘
v
ModelState
|
┌──────────────┼──────────────┐
v v v
MeshBuilder ResultProcessor MainWindow
(PyVista (scalar (Qt + PyVista
meshes) arrays) viewport)
Data flow¶
-
__main__.pyresolves the base name, checks if.postdataneeds regeneration, callstcl_parser.parse_tcl()+write_postdata()if needed. -
tcl_parser.parse_tcl(path)reads the TCL model, following allsourcecommands. Returns aTclModelwith nodes, elements, sections, materials, and geomTransf definitions. Element parsing is dispatched by theops_elementsregistry -- the element'sElemFamilydetermines how many nodes to read and what flags to extract. -
tcl_parser.write_postdata(model, path)computes local axes (rotation matrices -> quaternions), derives beam cross-section profiles from section properties, and writes the.mpco.postdatafile. -
mpco_reader.read_mpco(path)reads the HDF5 file and the.mpco.postdatacompanion, returning aModelState. -
MeshBuilder(model)converts the model into PyVista meshes. -
ResultProcessor(model, builder)extracts per-step scalar data from HDF5 and maps it onto the meshes. -
MainWindowties it together with a tabbed control panel (Results and View tabs), in-place mesh updates during animation, and export controls. The viewport has a fixed size (1200x800 default, adjustable via View tab spinners). All figure settings apply immediately -- no Apply button.
Key dataclasses (model.py)¶
| Class | Purpose |
|---|---|
NodeData |
Node IDs, coordinates, ID-to-index map |
ElementGroup |
One element type: IDs, connectivity, HDF5 key |
SectionAssignment |
Fiber data, thickness, element-to-section mapping |
BeamProfile |
2D cross-section vertices for beam visualization |
ResultDescriptor |
Describes one result (category, name, sub-groups) |
ResultSubGroup |
One HDF5 data group: elem IDs, GP/fiber/comp counts |
StageInfo |
Stage index and its step keys |
ModelState |
Top-level container for the entire model |
Element registry (ops_elements.py)¶
Maps 61 OpenSees element type strings to ElemInfo objects. This single
registry drives three systems:
- TCL parsing: determines how many nodes to read and which flags
to extract (
-local,-orient, geomTransf). - Mesh building: provides shape functions, GP coordinates, and node counts for surface/extrusion/GP cloud construction.
- Result processing: provides extrapolation matrices for GP-to-node mapping.
info = lookup("ASDShellQ4")
info.family # ElemFamily.SHELL_QUAD
info.num_nodes # 4
info.num_gp # 4
info.gp_coords # (4, 2) array at +/-1/sqrt(3)
info.shape_fn # shape_q4(xi, eta) -> (4,)
info.extrap_fn # extrapolation_matrix_q4() -> (4, 4)
info.is_surface # True
Adding a new element type to ELEMENT_REGISTRY automatically handles
TCL parsing, mesh building, and result display.
TCL parser (tcl_parser.py)¶
Parses OpenSees TCL commands:
| Command | What is extracted |
|---|---|
model basic -ndm -ndf |
Dimensionality |
node |
Node ID, coordinates |
element |
ID, type, node IDs, section tag, -local, -orient, geomTransf |
geomTransf |
ID, type, vecxz vector |
section |
ID, type, LayeredShell layers |
nDMaterial / uniaxialMaterial |
ID, type |
source |
Follows included files recursively |
Element parsing is registry-driven: lookup(elem_type) returns the
ElemFamily, which determines the parser:
| Family | Parser | Extracts |
|---|---|---|
SHELL_QUAD, SHELL_TRI, QUAD_2D, TRI_2D |
_parse_surface_element |
N nodes + section tag + -local |
BEAM |
_parse_beam |
2 nodes + geomTransf + inline A/Iy/Iz |
TRUSS, LINK |
_parse_two_node |
2 nodes |
ZERO_LENGTH |
_parse_zero_length |
2 nodes + -orient |
| Unknown | _parse_generic_element |
Consecutive integer node IDs |
Render update strategy¶
The GUI uses two update paths:
Full rebuild (_full_rebuild)¶
Triggered by: visibility toggles, colormap change, result/component change, grid spacing change.
- Clears the scene.
- Rebuilds all meshes from base geometry + current displacement.
- Stores mesh and actor references in
_meshes/_actorsdicts.
Fast update (_fast_update)¶
Triggered by: time step change, displacement scale change.
- Updates
.points[:]arrays in-place on existing meshes (no clear/add). - Updates scalar arrays for contour results.
- Only GP spheres are removed and re-added (glyph geometry depends on values).
HDF5 access¶
A single h5py.File handle is opened in MainWindow.__init__ and kept
for the session (persistent HDF5 handle). All read_step_data() calls
pass this handle to avoid repeated file open/close. The handle is closed
in closeEvent.
GP sphere zero mode¶
When displaying GP spheres, the zero mode controls sphere radius scaling:
- "Zero at 0": radius is zero when the value is zero (for signed quantities like stress).
- "Zero at min": radius is zero at the minimum value (for damage-like quantities that range from 0 upward).
This setting is in the View tab under Figure controls and applies immediately.
Displacement interpolation¶
GP cloud displacement uses vectorized numpy:
# Precomputed in build_gauss_point_cloud():
_gp_node_indices # (N_gp, max_nodes) int array
_gp_weights # (N_gp, max_nodes) float array
# At render time:
d = displacements[node_indices] # (N_gp, max_nodes, 3)
interp = np.einsum("ij,ijk->ik", wts, d) # (N_gp, 3)