Skip to content

Interfluo/exovista

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExoVista

An extension of the Sandia exodusii library for exporting PyVista meshes to Exodus format with user-defined element blocks and side sets. This project focuses on a simpler interface for PyVista users (e.g., meshes created via meshio or PyVista's geometric utilities).

Note: This code was initially a fork of: https://github.com/sandialabs/exodusii

Features

  • Element Blocks: Automatically split meshes by cell type and user-defined regions
  • Side Sets: Define boundary conditions via surface meshes with region arrays
  • 2D & 3D Support: Works with Quads, Triangles, Hexahedra, Tetrahedra, Wedges, Pyramids
  • VTK Compatibility: Handles VOXEL and PIXEL cell types (auto-permuted to Exodus ordering)
  • Node Arrays: Save point data arrays to the Exodus file
  • Time-Varying Fields: Write a time history of node and element results (transient data)
  • Compressed Output: Array data is zlib/deflate compressed by default (netCDF4), dramatically shrinking file size
  • Named Blocks/Sets: Optionally provide custom names for element blocks and side sets
  • Read Back to PyVista: Load an Exodus file straight into a PyVista UnstructuredGrid with read_exo (inverse of write_exo)

Installation

From PyPI

pip install exovista

Editable Install (Development)

git clone https://github.com/Interfluo/exovista.git
cd exovista
pip install -e .

Quick Start

import exovista
import numpy as np
import pyvista as pv

# Load a tetrahedral mesh
volume = pv.examples.download_letter_a()
volume.points -= volume.center

# Assign regions for element blocks
volume["region"] = 1 * (volume.cell_centers().points[:, 0] > 0)

# Extract surface and assign regions for side sets
surface = volume.extract_surface()
surface["region"] = 1 * (surface.cell_centers().points[:, 2] > 0)

# Write to Exodus file
exovista.write_exo("output.exo", volume, surface, region_key="region")

API Reference

exovista.write_exo

write_exo(
    filename: str,
    volume: pv.UnstructuredGrid,
    surface: pv.PolyData = None,
    region_key: str = "region",
    block_names: list[str] = None,
    side_set_names: list[str] = None,
    save_node_arrays: bool = True,
    times: np.ndarray | list = None,
    node_fields: dict = None,
    element_fields: dict = None,
)

Parameters:

Parameter Type Description
filename str Output Exodus file path
volume pv.UnstructuredGrid Volume mesh containing cells
surface pv.PolyData Surface mesh for side sets (optional)
region_key str Cell data array name used to split blocks/sets
block_names list[str] Custom names for element blocks
side_set_names list[str] Custom names for side sets
save_node_arrays bool Whether to save point data arrays
times array_like Time values, one per time step (enables a time history)
node_fields dict name -> array (n_steps, n_nodes) time-varying node results
element_fields dict name -> array (n_steps, n_cells) time-varying element results

Time-Varying Fields

Supply times together with node_fields and/or element_fields to write a transient result history. Each field array carries one row per time value; element fields are given in the original volume cell order and are automatically distributed to the correct element blocks. Static node arrays (from save_node_arrays) are broadcast across every time step.

import numpy as np

times = np.linspace(0.0, 1.0, 11)                 # 11 time steps

# (n_steps, n_nodes) node field and (n_steps, n_cells) element field
temperature = np.array([t * volume.points[:, 0] for t in times])
energy = np.outer(times, np.arange(volume.n_cells))

exovista.write_exo(
    "transient.exo",
    volume,
    times=times,
    node_fields={"temperature": temperature},
    element_fields={"energy": energy},
)

For a single time step a 1D array (length n_nodes or n_cells) is also accepted. The resulting file animates over time in ParaView.

exovista.read_exo

The inverse of write_exo: load an ExodusII database into a PyVista UnstructuredGrid.

read_exo(
    filename: str,
    *files: str,
    time_step: int = -1,
    read_node_variables: bool = True,
    read_element_variables: bool = True,
    read_side_sets: bool = False,
) -> pv.UnstructuredGrid | tuple[pv.UnstructuredGrid, pv.MultiBlock]

Parameters:

Parameter Type Description
filename str Input Exodus file path
*files str Additional file parts of a parallel decomposition
time_step int 1-based time step to sample variables at; negative indexes from the end (-1 = last, -2 = second-to-last). 0 is invalid and raises IndexError
read_node_variables bool | str | list[str] Node variables to load into point_data: True (all), False (none), or a name / list of names
read_element_variables bool | str | list[str] Element variables to load into cell_data; same semantics as read_node_variables
read_side_sets bool Also reconstruct side sets and return them alongside the grid

The returned grid reconstructs node coordinates (2D meshes are embedded in 3D with z = 0) and all element blocks, concatenated in block order. Each cell records its originating Exodus block via cell_data["exo_block_id"] and cell_data["exo_block_name"]. Node and element variables are sampled at the requested time_step, and the full time history is available in grid.field_data["times"] (grid.field_data["num_dim"] holds the original spatial dimension).

When read_side_sets=True, a second value is returned: a pv.MultiBlock with one pv.PolyData per side set (named after the side set), whose faces are rebuilt over the shared volume nodes. Each face carries orig_elem_id (1-based Exodus element id) and face_id (1-based local side id) cell data. 3D element faces become polygons; 2D element edges become line cells.

import exovista

mesh = exovista.read_exo("output.exo")          # last time step
mesh_t0 = exovista.read_exo("output.exo", time_step=1)  # first time step

# Load only the variables you need (skips reading the rest)
mesh = exovista.read_exo("output.exo", read_node_variables="temperature",
                         read_element_variables=False)

# Also reconstruct side sets
mesh, side_sets = exovista.read_exo("output.exo", read_side_sets=True)
top_faces = side_sets["side_0"]

# Parallel decomposition (pass each part, or use exovista.File for globbing)
joined = exovista.read_exo("run.exo.4.0", "run.exo.4.1",
                           "run.exo.4.2", "run.exo.4.3")

exovista.read_node_fields / exovista.read_element_fields

The inverses of write_exo's node_fields / element_fields arguments. They return the full time history of the requested variables as a dict, without rebuilding the mesh — the lightweight way to pull a transient field's history out of a file.

read_node_fields(filename: str, *files: str,
                 names: str | list[str] | None = None) -> dict[str, np.ndarray]
read_element_fields(filename: str, *files: str,
                    names: str | list[str] | None = None) -> dict[str, np.ndarray]

names=None (default) reads every variable; pass a name or list to read a subset. Unknown names raise KeyError. Each value has shape (num_times, num_nodes) (or (num_times, num_elems)) — the same layout write_exo accepts. Element variables are concatenated in block-id order, NaN-padding any block on which a variable is not defined.

import exovista

# Whole history of every node field, no mesh reconstruction
node_fields = exovista.read_node_fields("output.exo")
temperature = node_fields["temperature"]   # shape (num_times, num_nodes)

# Just one element field
pressure = exovista.read_element_fields("output.exo", names="pressure")["pressure"]

Output Compression

When writing netCDF4 files (the default), array variables are stored with zlib/deflate compression, which typically shrinks output files by an order of magnitude with negligible effect on read performance. The behavior is controlled by an environment variable:

Variable Default Description
EXODUSII_COMPRESSION_LEVEL 1 Deflate level. 1 is fast and captures most of the savings; 9 is maximum compression; 0 disables it (legacy uncompressed output).

Compression only applies to the netCDF4 backend; it is skipped automatically for the netCDF3 fallback. Files remain standard ExodusII and are readable by ParaView, Sierra, and other Exodus tools without any changes.

Examples

Example scripts are located in the examples/ directory:

Script Description
example_2d_quads.py 2D Quad mesh with regions
example_multi_block.py Multi-block Hex mesh split by regions
example_side_sets.py 3D Tet mesh with side sets
example_mixed_elements.py Mixed Hex and Tet elements
example_time_fields.py Time-varying node and element fields (transient data)
example_read_exo.py Round-trip: write a mesh then read it back with read_exo
example_read_field_history.py Read full field time histories with read_node_fields / read_element_fields, select variables, and time_step indexing
save_exo.py Comprehensive example with multiple mesh types

Run an example:

python examples/example_2d_quads.py

Development

Running Tests

The test suite uses pytest:

pip install -e . pytest
pytest test/

Tests that depend on upstream ExodusII reference files not redistributed with this fork (e.g. noh.exo, edges.base.exo) are skipped automatically when the files are absent. Continuous integration runs flake8 and the full test suite across Python 3.10–3.12 (see .github/workflows/ci.yml).

Cleaning Generated Files

python clean.py

This removes generated .exo files, __pycache__ directories, and temporary test outputs.

Copyright

This repo is a fork of https://github.com/sandialabs/exodusii and therefore inherits the below copyright:

Copyright 2022 National Technology & Engineering Solutions of Sandia, LLC
(NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
Government retains certain rights in this software.

SCR# 2748

About

improved exodus output for PyVista

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages