Read, write, and analyse COMSOL Multiphysics simulation results in HDF5.
cmslh5 is a Python toolkit that bridges COMSOL Multiphysics and the scientific Python ecosystem. It converts COMSOL .mph model files into a structured, open HDF5 archive (.cmslh5), then provides a rich API to extract, interpolate, compare, and analyse the results — without needing a COMSOL license for post-processing.
COMSOL's native .mph format is proprietary and requires a running COMSOL server to read. This creates friction when you want to:
- Post-process results on a laptop without a COMSOL license
- Share simulation data with collaborators who don't have COMSOL
- Run automated analysis pipelines in Python
- Load results into ParaView, matplotlib, or Jupyter notebooks
- Compare results across mesh refinements or parameter sweeps
- Archive simulation data in a long-term, vendor-neutral format
cmslh5 solves all of these by extracting everything into HDF5 once, then providing fast, license-free access forever.
| Feature | Description |
|---|---|
| Convert | Extract mesh, elements, physical groups, and all physics fields from .mph to .cmslh5 |
| Parallel extraction | Split node range across multiple COMSOL worker processes for large models |
| Read | Query nodes, elements, domains, studies, timesteps, and field data |
| Interpolate | Temporal interpolation between stored timesteps; spatial IDW interpolation to arbitrary points |
| Line extraction | Sample any field along parametric curves defined by equations in x, y, z |
| Point probes | Evaluate any component at any spatial point and time |
| Node time histories | Extract full time series at specific mesh nodes by node ID |
| Time histories | IDW-interpolated time series at arbitrary spatial coordinates |
| Compare | Diff two .cmslh5 files field-by-field for regression testing |
| Statistics | Time-average, RMS, min/max, FFT at points |
| Export | Write to CSV, VTK (for ParaView via meshio) |
| ParaView plugin | Native ParaView reader with animation bar, study selection, domain colouring |
# Core (reader, compare, stats, export) — no COMSOL needed
pip install cmslh5
# With COMSOL conversion support (requires mph + COMSOL license)
pip install cmslh5[convert]
# With plotting
pip install cmslh5[plot]
# Everything
pip install cmslh5[all]
# Development
git clone https://github.com/smartgeomechanics/cmslh5.git
cd cmslh5
pip install -e ".[dev]"- Python ≥ 3.9
- Core:
h5py,numpy,scipy - Conversion:
mph(+ COMSOL Multiphysics installation) - Plotting:
matplotlib - VTK export:
meshio
On your COMSOL server:
# Mesh, solve, and extract
cmslh5-convert model.mph --cores 8
# Extract from an already-solved model
cmslh5-convert Results_Model.mph --extract-results
# Extract only specific fields
cmslh5-convert Results_Model.mph --extract-results --fields Temperature Pressure VelocityThis produces a .cmslh5 file that you can copy anywhere.
from cmslh5 import CmslH5Reader
reader = CmslH5Reader('Results_Model.cmslh5')
print(reader.summary())Output:
File : Results_Model.cmslh5
Size : 847.3 MB
Mesh:
Nodes : 1,245,678
BBox min : [0, -0.025, 0]
BBox max : [0.5, 0.025, 0.025]
Elements : 7,123,456 (tet)
Domains : 1
1 : Fluid
Studies: 1
[0] "Time Dependent" (time-dep, T=201)
t : [0 .. 2] s
Velocity [vx, vy, vz] unit=m/s shape=(1245678, 3, 201)
Pressure [p] unit=Pa shape=(1245678, 1, 201)
Temperature [T] unit=K shape=(1245678, 1, 201)
Available components:
Pressure : p
Temperature : T
Velocity : vx, vy, vz
# Temperature along the pipe centreline at t=1.0s
result = reader.extract_along_line(
component='T',
time=1.0,
x_expr='s * 0.5', # x sweeps from 0 to 0.5
y_expr='0.0', # constant y
z_expr='0.0', # constant z
n_points=500,
)
z = result['coords'][:, 0] # x-coordinates
T = result['values'] # temperature values
arc = result['arc_length'] # cumulative arc lengthThe s parameter goes from 0 to 1. Use any math expressions:
# Circular path
x_expr='0.01 * cos(2*pi*s)'
y_expr='0.01 * sin(2*pi*s)'
z_expr='0.25'
# Non-uniform spacing
z_expr='0.5 * s**2'result = reader.extract_components_along_line(
components=['vx', 'vy', 'vz', 'p', 'T'],
time=1.0,
x_expr='s * 0.5', y_expr='0.0', z_expr='0.0',
)
x = result['coords'][:, 0]
vx = result['data']['vx']['values']
p = result['data']['p']['values']
T = result['data']['T']['values']p = reader.probe_point('p', time=0.5, point=[0.25, 0.0, 0.0])
print(f'Pressure: {p["value"]:.2f} {p["unit"]}')
print(f'Nearest mesh node: {p["nearest_distance"]:.2e} m away')IDW-interpolated time series at an arbitrary spatial coordinate. Only the k
nearest mesh nodes are loaded — the full field is never read into memory.
th = reader.extract_time_history('vx', point=[0.25, 0.0, 0.0])
import matplotlib.pyplot as plt
plt.plot(th['times'], th['values'])
plt.xlabel('Time [s]')
plt.ylabel(f'vx [{th["unit"]}]')
plt.show()Use extract_node_time_history when you know the exact node IDs (e.g. from a
COMSOL probe, a monitoring set, or a previous nearest-node lookup). It reads
only the requested rows from HDF5 — no full-field allocation.
# Single node by 1-based label (default)
result = reader.extract_node_time_history(
nodes=42,
components='T',
)
times = result['times'] # (T,) float64
T = result['data']['T'] # (1, T) float64
coords = result['coords'] # (1, 3) node coordinates
print(f'Node {result["node_ids"][0]} at {coords[0]}')
import matplotlib.pyplot as plt
plt.plot(times, T[0])
plt.xlabel('Time [s]')
plt.ylabel(f'T [{result["units"]["T"]}]')
plt.show()# Multiple nodes, multiple components
result = reader.extract_node_time_history(
nodes=[101, 202, 303],
components=['vx', 'vy', 'p'],
)
times = result['times']
vx = result['data']['vx'] # (3, T)
p = result['data']['p'] # (3, T)# 0-based position indices instead of 1-based node labels
result = reader.extract_node_time_history(
nodes=[0, 99, 999],
components='T',
index_base=0,
)The returned dict contains:
| Key | Shape | Description |
|---|---|---|
times |
(T,) |
Timestep values in seconds |
node_ids |
(N,) |
1-based node labels |
node_indices |
(N,) |
0-based position indices |
coords |
(N, 3) |
Node coordinates |
data |
dict | component → (N, T) float64 arrays |
units |
dict | component → unit string |
study |
str | Study label |
To look up available node IDs:
ids = reader.node_ids() # (N,) int32, 1-based
print(f'Node labels: {ids[0]} .. {ids[-1]}')from cmslh5.stats import time_average, fft_at_point, field_statistics
# Time-averaged temperature field
avg_T, unit = time_average(reader, 'T')
# FFT of pressure at a monitoring point
fft = fft_at_point(reader, 'p', point=[0.25, 0.0, 0.0])
print(f'Dominant frequency: {fft["dominant_freq"]:.2f} Hz')
# Spatial statistics at a given time
stats = field_statistics(reader, 'T', time=1.0)
print(f'T range: [{stats["min"]:.1f}, {stats["max"]:.1f}] {stats["unit"]}')from cmslh5 import compare_files
diff = compare_files('coarse_mesh.cmslh5', 'fine_mesh.cmslh5')
print(diff['summary'])Output:
Comparing: coarse_mesh.cmslh5 vs fine_mesh.cmslh5
Time: 2.0 s
Mesh match: False (A=312,456 nodes, B=1,245,678 nodes)
T PASS max_abs=0.1234 max_rel=0.0004 rms=nan
p PASS max_abs=12.34 max_rel=0.0012 rms=nan
vx FAIL max_abs=0.0567 max_rel=0.0234 rms=nan
Overall: DIFFERENCES FOUND
from cmslh5.export import to_csv, to_vtk, line_to_csv
# Nodal fields to CSV
to_csv(reader, 'output.csv', components=['T', 'p'], time=1.0)
# Line extraction to CSV
result = reader.extract_along_line(component='T', time=1.0,
x_expr='s*0.5', y_expr='0', z_expr='0')
line_to_csv('centreline_T.csv', result)
# VTK for ParaView (requires meshio)
to_vtk(reader, 'output.vtk', time=1.0)1. pvpython -m pip install h5py
2. ParaView → Tools → Manage Plugins → Load New → select cmslh5/paraview/plugin.py
3. Tick "Auto Load"
4. File → Open → select .cmslh5 file
Features: study selection dropdown, animation bar with all timesteps, domain colouring via DomainId cell data.
/Model/
Nodes/
Id int32 [N_nodes]
Coordinates float32 [N_nodes, 3]
Elements/
Id, Type, NumNodes, Connectivity
DomainId, DomainName, DomainLegend/
Physical_Groups/
{dim}D_{label}/
Id, Type, NumNodes, Connectivity
/Analysis/
{idx}_{StudyLabel}/
Times float64 [T]
Velocity float32 [N, 3, T]
Pressure float32 [N, 1, T]
Temperature float32 [N, 1, T]
Displacement float32 [N, 3, T]
Stress float32 [N, 6, T]
Strain float32 [N, 6, T]
You can inspect any .cmslh5 file with standard HDF5 tools:
h5dump -H Results_Model.cmslh5 # structure
h5ls -r Results_Model.cmslh5 # recursive listingOr in Python:
import h5py
with h5py.File('Results_Model.cmslh5', 'r') as f:
f.visititems(lambda name, obj: print(name, obj.shape if hasattr(obj, 'shape') else ''))| Component | Field | Unit |
|---|---|---|
T |
Temperature | K |
p |
Pressure | Pa |
vx, vy, vz |
Velocity | m/s |
ux, uy, uz |
Displacement | m |
sx, sy, sz, sxy, sxz, syz |
Stress | Pa |
exx, eyy, ezz, exy, exz, eyz |
Strain | 1 |
Aliases: 'temperature' → T, 'pressure' → p, 'temp' → T.
cmslh5-convert MODEL_FILE [OPTIONS]
positional:
MODEL_FILE Path to COMSOL .mph file
options:
-o, --output OUTPUT_FILE Output .cmslh5 path
--extract-results Skip meshing/solving; extract from solved model
--cores N CPU cores for COMSOL solver
--studies TAG [TAG ...] Restrict to specific study tags
--fields FIELD [FIELD ...] Fields to extract (e.g. Temperature Pressure)
--chunk-size N Nodes per extraction chunk (default: 200000)
--workers N Parallel worker processes (default: 1)
--comp COMP_TAG Component tag (default: comp1)
--no-start-server Connect to external COMSOL server
--host HOST COMSOL server host (default: localhost)
--port PORT COMSOL server port (default: 2036)
| Nodes | Workers | Chunk size | JVM heap | Approx. speedup |
|---|---|---|---|---|
| < 200k | 1 | 200,000 | 16 GB | baseline |
| 200k–1M | 1–2 | 200,000 | 32 GB | 1–1.5× |
| 1M–5M | 2–4 | 250,000–500,000 | 24 GB per worker | 2–3× |
| > 5M | 4–8 | 500,000 | 16–24 GB per worker | 3–6× |
The --workers flag splits the node range across independent COMSOL processes. Each worker needs its own JVM heap and COMSOL license token. The master process handles mesh/element writing and merges the worker outputs.
# Typical large-model invocation
export _JAVA_OPTIONS='-Xmx24G'
cmslh5-convert Results_LargeModel.mph \
--extract-results \
--workers 4 \
--chunk-size 500000 \
--fields Velocity Pressure Temperaturecmslh5/
├── __init__.py # public API
├── reader.py # CmslH5Reader class
├── convert.py # .mph → .cmslh5 (requires mph); includes parallel worker mode
├── compare.py # diff two files
├── stats.py # time_average, fft_at_point, etc.
├── export.py # to_csv, to_vtk
├── _fields.py # field configs, component map (single source of truth)
├── _interp.py # IDW + temporal interpolation
├── _expr.py # parametric expression evaluator
├── cli/
│ └── convert_cli.py # cmslh5-convert entry point
└── paraview/
└── plugin.py # ParaView Python plugin
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
git clone https://github.com/smartgeomechanics/cmslh5.git
cd cmslh5
pip install -e ".[dev]"
pytestIf you use cmslh5 in your research, please cite:
@software{cmslh5,
author = {Sinha, S. K.},
title = {cmslh5: Read, write, and analyse COMSOL simulation results in HDF5},
url = {https://github.com/smartgeomechanics/cmslh5},
year = {2025},
}