Working with results#

For all example code on this page, ospgrillage is imported as og

import ospgrillage as og

Extracting results#

After analysis, results are obtained using get_results().

all_result   = example_bridge.get_results()
patch_result = example_bridge.get_results(load_case="patch load case")

The first call returns results for every load case; the second filters to one.

What is an xarray Dataset?#

The returned object is an xarray Dataset — think of it as a multi-dimensional, labelled table. Rather than accessing data by integer index (row 3, column 7), you access it by name (Loadcase="Barrier", Component="Mz_i"). This makes result queries self-describing and much less error-prone.

The Dataset contains five named data variables:

Variable

Axes (dimensions)

Contents

displacements

Loadcase x Node x Component

Translations (x, y, z) and rotations (theta_x, theta_y, theta_z) at each node

velocity

Loadcase x Node x Component

Velocities (dx, dy, dz) and angular velocities (dtheta_x/y/z)

acceleration

Loadcase x Node x Component

Accelerations (ddx, ddy, ddz) and angular accelerations

forces

Loadcase x Element x Component

Internal forces (Vx, Vy, Vz, Mx, My, Mz) at each element end (_i, _j)

ele_nodes

Element x Nodes

Which node tags (i, j) belong to each element

For a Shell & Beam Elements — shell_beam, forces are split into forces_beam / forces_shell and element connectivity into ele_nodes_beam / ele_nodes_shell. Additionally, stresses_shell (Loadcase x Element x Stress) contains shell section stress resultants at 4 Gauss points — 8 components per point (N11, N22, N12, M11, M22, M12, Q13, Q23) giving 32 values per element. See Shell contour plots (plot_srf) for visualisation.

Printing all_result shows the structure:

<xarray.Dataset>
Dimensions:        (Component: 18, Element: 142, Loadcase: 5, Node: 77, Nodes: 2)
Coordinates:
  * Component      (Component) <U7 'Mx_i' 'Mx_j' 'My_i' ... 'theta_y' 'theta_z'
  * Loadcase       (Loadcase) <U55 'Barrier' ... 'single_moving_point at glob...'
  * Node           (Node) int32 1 2 3 4 5 6 7 8 9 ... 69 70 71 72 73 74 75 76 77
  * Element        (Element) int32 1 2 3 4 5 6 7 ... 136 137 138 139 140 141 142
  * Nodes          (Nodes) <U1 'i' 'j'
Data variables:
    displacements  (Loadcase, Node, Component) float64 nan nan ... -4.996e-10
    forces         (Loadcase, Element, Component) float64 36.18 -156.9 ... nan
    ele_nodes      (Element, Nodes) int32 2 3 1 2 1 3 4 ... 32 75 33 76 34 77 35

Each line of Coordinates lists the labels along one dimension. Loadcase lists every load case name; Component lists every result quantity; Node and Element list the integer tags from the OpenSees model.

Figure 1 illustrates the overall dataset structure.

Figure 1: Structure of DataSet.

Extracting the data variables#

disp_array = all_result.displacements  # nodal displacements & rotations
force_array = all_result.forces        # element end forces
ele_array   = all_result.ele_nodes     # element->node connectivity

Available force and displacement components#

Each DataArray has its own Component coordinate. To see the labels:

disp_array.coords['Component'].values
# array(['x', 'y', 'z', 'theta_x', 'theta_y', 'theta_z'])

force_array.coords['Component'].values
# array(['Vx_i', 'Vy_i', 'Vz_i', 'Mx_i', 'My_i', 'Mz_i',
#        'Vx_j', 'Vy_j', 'Vz_j', 'Mx_j', 'My_j', 'Mz_j'])

Suffix _i / _j denotes the start / end node of the element respectively.

Selecting results by label#

Use xarray’s .sel() to pick results by name, and .isel() to pick by integer position:

# All nodes, one component — vertical displacement
disp_array.sel(Component='y')

# One load case, one node
disp_array.sel(Loadcase="patch load case", Node=20)

# One load case, several elements
force_array.sel(Loadcase="Barrier", Element=[2, 3, 4])

# One component across all load cases
force_array.sel(Component='Mz_i')

For results from a Moving load, each increment is stored as a separate load case named automatically as "<load name> at global position [x,y,z]". You can select these by full name or by position:

# Select by the auto-generated name
by_name  = force_array.sel(Loadcase="patch load case at global position [0,0,0]")
# Select by integer index (0 = first increment)
by_index = force_array.isel(Loadcase=0)

Influence lines and surfaces#

Influence studies are created through analyze_influence_lines() and analyze_influence_surfaces(), which return durable result objects. Those objects carry the compiled Dataset, can be plotted directly, and can be saved later without recomputing:

ils = bridge.analyze_influence_lines(
    paths={"Lane 1": lane_1_path, "Lane 2": lane_2_path}
)
ils.plot(array="forces", component="Mz_i", element=42)
ils.plot(array="forces", component="Mz_i", element=42, backend="plotly", view="path")
ils.save("lane_ils.nc")

iss = bridge.analyze_influence_surfaces(x=[2, 4], z=[2, 3], name="Deck IS")
iss.plot(array="forces", component="Mz_i", element=42, view="surface3d")

# Station-space reduction (recommended for skewed/curved decks)
iss.plot(
    array="forces",
    component="Mz_i",
    element=42,
    x_coord="longitudinal_station",
    y_coord="transverse_station",
)

CSV export is available directly from the result objects:

# IL: one long-form CSV containing all lanes/paths
ils.to_csv(
    "lane_ils.csv",
    array="forces",
    component="Mz_i",
    element=42,
    load_coord="x",
)

# IS: rectangular station-grid CSV of ordinates
# (+ optional companion point map with physical x/z)
iss.to_csv(
    "deck_is_grid.csv",
    array="forces",
    component="Mz_i",
    element=42,
    include_physical_coords=True,
)

By default, influence surfaces are generated from the mesh station grid (admissible deck points). For curved or skewed bridges, reduce in station space with x_coord="longitudinal_station" and y_coord="transverse_station" to avoid rectangular-grid assumptions.

When plotting a reduced surface, plot_is(..., coordinate_space="physical") uses mapped deck coordinates (x, z) and triangulates valid points so the surface remains contiguous on curved geometry in both Matplotlib and Plotly. Use coordinate_space="station" to keep station axes.

The lower-level helpers create_influence_line(), create_influence_surface(), plot_il(), and plot_is() remain available when direct xarray manipulation is preferred.

Influence lines support both the traditional view="ordinate" plot and a view="path" mode that draws the ordinates along the load path on top of the bridge model.

Technical notes (DKT and station mapping)#

  • shape_function="hermite" uses the higher-order consistent-load path: Hermite interpolation on quadrilateral regions, and a DKT-style condensed triangular distributor where skew geometry creates 3-node regions.

  • Influence-line load_coord="station" is cumulative distance along each axle path; this is typically preferred for skewed/curved paths because global x is not a uniform path-abscissa there.

  • Influence-surface default generation uses admissible mesh station points (not a global rectangular deck bounding box), then stores both station coordinates and mapped physical (x,z) coordinates.

  • Physical plotting (coordinate_space="physical") triangulates valid mapped points so curved/skewed surfaces remain contiguous without assuming a rectangular Cartesian deck.

For a complete workflow, see the Influence Lines and Surfaces tutorial notebook.

Note

For information on the full range of indexing and selection operations available on DataArrays, see the xarray indexing documentation.

Load combinations#

Load combinations are computed on the fly in get_results() by passing a combinations dictionary: keys are load case name strings and values are load factors. ospgrillage multiplies each load case by its factor and sums the results.

comb_dict   = {"patch_load_case": 2, "moving_truck": 1.6}
comb_result = example_bridge.get_results(combinations=comb_dict)
print(comb_result)
<xarray.Dataset>
Dimensions:        (Component: 18, Element: 142, Loadcase: 3, Node: 77, Nodes: 2)
Coordinates:
  * Component      (Component) <U7 'Mx_i' 'Mx_j' 'My_i' ... 'theta_y' 'theta_z'
  * Node           (Node) int32 1 2 3 4 5 6 7 8 9 ... 69 70 71 72 73 74 75 76 77
  * Element        (Element) int32 1 2 3 4 5 6 7 ... 136 137 138 139 140 141 142
  * Nodes          (Nodes) <U1 'i' 'j'
  * Loadcase       (Loadcase) <U55 'moving_truck at global position [2...'
Data variables:
    displacements  (Loadcase, Node, Component) float64 nan nan ... 0.0 7.688e-05
    forces         (Loadcase, Element, Component) float64 36.18 -156.9 ... nan
    ele_nodes      (Loadcase, Element, Nodes) int32 6 9 3 6 ... 228 102 231 105

When a combination mixes static and moving load cases, the factored static load case is added to each increment of the moving load.

Load envelopes#

A load envelope finds the maximum (or minimum) of a chosen result component across all load cases. Use create_envelope() to build an Envelope object, then call .get():

envelope = og.create_envelope(ds=comb_result, load_effect="y", array="displacements")
disp_env = envelope.get()
print(disp_env)

By default get() returns, for each node, the maximum value of vertical displacement y:

<xarray.DataArray 'Loadcase' (Node: 77, Component: 18)>
array([[nan, nan, nan, ...,
        'single_moving_point at global position [2.00,0.00,2.00]', ...],
       ...],
      dtype=object)
Coordinates:
  * Component  (Component) <U7 'Mx_i' 'Mx_j' 'My_i' ... 'theta_y' 'theta_z'
  * Node       (Node) int32 1 2 3 4 5 6 7 8 9 10 ... 69 70 71 72 73 74 75 76 77

For more options see create_envelope().

Saving and loading results#

Results can be saved to a NetCDF file — the standard binary format for labelled multi-dimensional data — by passing the save_filename keyword to get_results():

# Save while retrieving
results = example_bridge.get_results(save_filename="my_results.nc")

# Also works with load combinations
comb = example_bridge.get_results(
    combinations={"Dead load": 1.2, "SIDL": 1.5},
    save_filename="combination_results.nc",
)

If you omit the NetCDF extension and pass a stem (for example save_filename="my_results"), ospgrillage writes semantic names: *.res.nc for ordinary results, *.il.nc for influence-line exports, and *.is.nc for influence-surface exports.

This writes the full xarray Dataset — including node coordinates and member-element connectivity — to a .nc file in the current working directory.

Loading saved results#

To reload saved results later, use xarray directly:

import xarray as xr
reloaded = xr.open_dataset("my_results.nc")

The reloaded Dataset has the same structure (displacements, forces, ele_nodes, etc.) so all the selection and envelope operations described above work identically.

Plotting from a saved file#

The saved file is self-contained: it includes the node coordinates and member mappings needed by the plotting functions. Use model_proxy_from_results() to create a lightweight proxy that stands in for the original grillage model:

import xarray as xr
import ospgrillage as og

ds = xr.open_dataset("my_results.nc")
proxy = og.model_proxy_from_results(ds)

# All plotting functions work with the proxy
og.plot_bmd(proxy, ds, backend="plotly")
og.plot_sfd(proxy, ds, backend="plotly")
og.plot_def(proxy, ds, backend="plotly")
og.plot_tmd(proxy, ds, backend="plotly")

Note

The proxy supports force diagrams and deflected shapes. For the full model geometry visualisation (plot_model()) you still need the original :class:~ospgrillage.osp_grillage.OspGrillage object.

Viewing results in the GUI#

The ospgui application can open .nc files directly via File > Open Results (.nc) (Ctrl+O). This switches to a results viewer with interactive BMD, SFD, TMD, and deflection tabs, plus loadcase and member-filter controls.

To generate test .nc files for all three mesh types (Oblique, GMS, Ortho), run::

python tests/generate_test_results.py

Plotting results#

Model visualisation#

Use plot_model() to inspect the mesh geometry before or after analysis:

og.plot_model(bridge_28)                                     # matplotlib plan view
og.plot_model(bridge_28, backend="plotly")                   # interactive 3D
og.plot_model(bridge_28, show_node_labels=True)              # with node tags

Post-processing results#

ospgrillage includes a dedicated post-processing module for force diagrams, deflected shapes, and envelopes across multiple load cases.

Note

The post-processing module supports force diagrams and deflected shapes for beam-based model types (beam_only and beam_link).

For this section, we refer to an exemplar 28 m super-T bridge (Figure 2). The grillage object is named bridge_28.

Figure 2: 28 m super-T bridge model.

To plot deflection from the displacements DataArray use plot_def(), specifying a grillage member name:

og.plot_def(bridge_28, results, members="exterior_main_beam_2")

Figure 3: Deflected shape of exterior main beam 2.

To plot internal forces from the forces DataArray use plot_force():

og.plot_force(bridge_28, results, member="exterior_main_beam_2", component="Mz")

Figure 4: Bending moment about z axis of exterior main beam 2.

Convenience plotting functions#

For the most common diagrams, convenience wrappers are provided that default to plotting all member groups when no member is specified:

og.plot_bmd(bridge_28, results)          # bending moment diagram (Mz)
og.plot_sfd(bridge_28, results)          # shear force diagram (Fy)
og.plot_def(bridge_28, results)   # vertical deflection (y)

Each returns a list of figures (one per member group). Pass member="interior_main_beam" to plot a single member instead.

Selecting member groups#

The member parameter accepts a string for a single member, or a Members bitflag to plot any combination of groups:

# Plot only longitudinal members
og.plot_bmd(bridge_28, results, member=og.Members.LONGITUDINAL, backend="plotly")

# Combine individual members with |
og.plot_bmd(bridge_28, results,
            member=og.Members.EDGE_BEAM | og.Members.INTERIOR_MAIN_BEAM,
            backend="plotly")

# Plot everything (the default for plotly)
og.plot_bmd(bridge_28, results, member=og.Members.ALL, backend="plotly")

Available individual flags:

Flag

Member name string

EDGE_BEAM

"edge_beam"

EXTERIOR_MAIN_BEAM_1

"exterior_main_beam_1"

INTERIOR_MAIN_BEAM

"interior_main_beam"

EXTERIOR_MAIN_BEAM_2

"exterior_main_beam_2"

START_EDGE

"start_edge"

END_EDGE

"end_edge"

TRANSVERSE_SLAB

"transverse_slab"

Pre-defined composites: LONGITUDINAL (all four longitudinal types), TRANSVERSE (transverse slab + start/end edges), ALL (everything).

Customising plots#

All plotting functions accept keyword arguments for common customisations:

# Wide figure, values in kN*m, custom title, red with no fill
og.plot_bmd(bridge_28, results,
            member="interior_main_beam",
            figsize=(12, 4),
            scale=0.001,
            title="Bending Moment (kN*m)",
            color="r",
            fill=False)

To compose multi-panel figures, pass an existing matplotlib Axes:

import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 3, figsize=(18, 4))
og.plot_bmd(bridge_28, results, member="interior_main_beam", ax=axes[0])
og.plot_sfd(bridge_28, results, member="interior_main_beam", ax=axes[1])
og.plot_def(bridge_28, results, member="interior_main_beam", ax=axes[2], color="g")
fig.tight_layout()

The full set of keyword arguments is:

Kwarg

Default

Description

figsize

matplotlib default

Figure size in inches (width, height)

ax

None

Existing Axes (matplotlib) or Figure (Plotly) to draw on

scale

1.0

Multiply values by this factor (e.g. 0.001 for N to kN)

title

auto

Custom title string, or None to suppress

color

"k" / "b"

Line colour

fill

True

Shade the area under force diagrams

alpha

0.4

Fill / trace transparency

show

False

Call fig.show() before returning

Interactive 3D plots (Plotly)#

For interactive rotation — especially useful in Jupyter notebooks — pass backend="plotly":

pip install ospgrillage[gui]   # includes plotly
og.plot_bmd(bridge_28, results, backend="plotly")
og.plot_sfd(bridge_28, results, backend="plotly")
og.plot_def(bridge_28, results, backend="plotly")

Each returns a single Plotly Figure that renders interactively in Jupyter notebooks and in browser windows from the terminal. The figure can be further customised using the standard Plotly API. The GUI auto-detects plotly and uses it by default when available.

Shell contour plots (plot_srf)#

For shell_beam models, plot_srf() renders contour plots over the shell mesh. It supports three families of component:

Family

Components

Description

Shell forces

Vx, Vy, Vz, Mx, My, Mz

Element end forces averaged to nodes

Displacements

Dx, Dy, Dz

Nodal translations (x, y, z)

Stress resultants

N11, N22, N12, M11, M22, M12, Q13, Q23

Section stress resultants averaged from Gauss points

Note

plot_srf requires a shell_beam model. The results Dataset must contain ele_nodes_shell and node_coordinates. Stress resultant components additionally require stresses_shell (generated automatically by ospgrillage ≥ 0.6.0).

Basic usage#

plot_srf takes a results Dataset (not a grillage object) as its first argument:

results = bridge.get_results(save_filename="bridge_results.nc")

# Shell bending moment about the x-axis
og.plot_srf(results, component="Mx")

# Vertical displacement contour
og.plot_srf(results, component="Dy")

# Membrane force N11 (stress resultant)
og.plot_srf(results, component="N11")

Choosing a loadcase#

By default the first loadcase is plotted. Pass loadcase= to select a specific one:

og.plot_srf(results, component="Mx", loadcase="Dead Load")

Shell force components#

Shell element end forces (VxMz) are extracted from forces_shell and averaged at shared nodes:

og.plot_srf(results, "Mx")   # bending moment about x
og.plot_srf(results, "Vy")   # shear force in y
og.plot_srf(results, "Mz")   # torsion

Displacement components#

Displacement contours (Dx, Dy, Dz) are read directly from the nodal displacements array:

og.plot_srf(results, "Dy")                              # vertical deflection
og.plot_srf(results, "Dy", colorscale="Viridis")        # sequential palette

Tip

Use a sequential colorscale like Viridis for displacements (single-sign data) and a diverging colorscale like RdBu_r (the default) for forces and stress resultants (signed data that crosses zero).

Stress resultant components#

Shell section stress resultants are extracted via OpenSeesPy’s eleResponse(tag, "stresses"), which returns 8 values at each of the 4 Gauss points for a 4-node shell element:

Notation

Meaning

N11

Membrane force per unit length in local 1-direction

N22

Membrane force per unit length in local 2-direction

N12

In-plane shear force per unit length

M11

Bending moment per unit length about local 2-axis

M22

Bending moment per unit length about local 1-axis

M12

Twisting moment per unit length

Q13

Transverse shear force per unit length in 1–3 plane

Q23

Transverse shear force per unit length in 2–3 plane

og.plot_srf(results, "M11")   # plate bending about local 2-axis
og.plot_srf(results, "N11")   # membrane force in local 1-direction
og.plot_srf(results, "Q13")   # transverse shear

Composing shell contours with beam diagrams#

A common workflow is to overlay the beam BMD/SFD on top of a shell contour. Pass the Plotly figure returned by plot_srf as the ax= argument to any beam plot function:

# 1. Create the shell contour (show=False to keep the figure object)
fig = og.plot_srf(results, "Mx", backend="plotly", show=False)

# 2. Create a model proxy for beam plotting
proxy = og.model_proxy_from_results(results)

# 3. Overlay the BMD onto the same figure
og.plot_bmd(proxy, results, backend="plotly", ax=fig)

This works with plot_bmd, plot_sfd, plot_tmd, and plot_def.

Colorscale guidance#

Data type

Recommended colorscale

Why

Forces / moments (VxMz)

RdBu_r (default)

Diverging — highlights sign change

Stress resultants (N11Q23)

RdBu_r (default)

Diverging — signed data

Displacements (Dx, Dy, Dz)

Viridis

Sequential — typically single-sign

og.plot_srf(results, "Mx", colorscale="RdBu_r")     # diverging (default)
og.plot_srf(results, "Dy", colorscale="Viridis")     # sequential
og.plot_srf(results, "N11", colorscale="Plasma")     # alternative sequential

Additional keyword arguments#

Kwarg

Default

Description

backend

"plotly"

"plotly" for interactive 3-D or "matplotlib" for 2-D

colorscale

"RdBu_r"

Plotly colorscale name (or matplotlib cmap name)

show_colorbar

True

Display the colour bar legend

opacity

1.0

Surface opacity (0–1)

show

True (plotly) / False (mpl)

Display the figure immediately

ax / fig

None

Existing Plotly Figure or matplotlib Axes to draw on

title

auto

Custom title string, or None to suppress

figsize

None

Figure size as (width, height) in inches

Using the GUI shell contour tab#

When you open a shell_beam results file in the GUI (File > Open Results (.nc)), a Shell Contour tab appears alongside the BMD, SFD, TMD, and Deflection tabs.

The results panel on the left provides three controls:

  1. Component — select any of the 17 available components (MxMz, DxDz, N11Q23)

  2. Colorscale — choose from RdBu_r, Viridis, Plasma, Cividis, or Turbo

  3. Overlay — optionally composite a beam diagram (BMD, SFD, TMD, or Deflection) on top of the shell contour

The contour controls are greyed out when a non-contour tab is selected and automatically enabled when you switch to the Shell Contour tab. For non-shell models, the tab and controls are hidden entirely.