Salvus 2026.5.0
This is a major release that brings a broad set of new features, performance improvements, and quality-of-life enhancements across the Salvus ecosystem.
The Salvus API remains fully compatible with 2025.X, and projects created
with a previous Salvus version can be continued with this new release.
Note, however, that this version of Salvus requires numpy>=2.2.0.
Please see https://docs.mondaic.com/updates/2026/update_guide/ for upgrading your Python environment.
On the meshing side, layered models now support interactive Plotly visualizations and experimental HDF5 serialization. Additional mesh utilities include Exodus export, improved mesh duality with dual graph support for quad and hex meshes, and significantly faster surface detection. This release also introduces experimental support for chunked mesh building and smoothing — enabling parallel construction and handling of meshes that exceed available memory.
SalvusCompute now offers an experimental memory estimation tool, parallel Jacobian computation via per-rank threading, and more detailed timing output including JSON-formatted statistics in the simulation’s meta.json file.
For inversions, the L-BFGS scheme now uses less memory and disk space by
computing model and gradient differences on the fly. Custom event batch
selection can now return empty control groups, and custom mapping classes can
be serialized into an InverseProblemConfiguration. Corrupted gradient files
are now automatically quarantined and the failing events resubmitted, allowing
inversions to continue without manual intervention.
Other notable additions include type hints in distributed wheel builds for improved IDE support, variable substitution in Slurm srun arguments, experimental support for the Flux job scheduler, a new attenuation model visualization function, and automatic mesh quality control when adding meshes to a project.
For the full list of changes, please refer to the detailed changelog below.
Changes
Added an experimental memory estimation tool that can be used to estimate the required memory usage of a simulation before running it.
Speed up the time step computation in both the solver and the mesher by binding the minimum distance computation to a C++ implementation.
Add more detailed timing statistics to adjoint runs.
Add JSON timing output to a simulation’s meta.json file for simplified downstream reporting.
Add a new experimental group "num_threads_per_rank" to the SalvusCompute,
which controls the maximum number of threads that may be spawned from each MPI
rank. If set, threading will be used to parallelize the computation of the
Jacobian during simulation startup, which can result in significant speedups.
Add experimental support for more detailed timing of simulations in SalvusCompute.
Improve class and function serialization logic.
Include type hints in the distributed wheel builds. This improves the type checking and usage experience of the Salvus Python library.
A good example is tab completion and docstring display in VSCode.
If an interactive SSH login is requested, Salvus will now ensure it happens, even if the initial connection attempt succeeded.
Enables using the rotation_on_output argument on normal Point2D and
Point3D objects.
New salvus.material.attenuation.plot_q_model() function to visualize attenuation models.
Add interactive Plotly visualizations for realized LayeredModel instances. The two new functions produce 2-D cross-section figures for 1-D interface models and 3-D surface figures for 2-D interface models:
from salvus.mesh.layered_meshing.layered_model import plot, plot_interfaces
# Plot from a realized LayeredModel directly:
fig = plot(model)
fig.show()
# Or from a sequence of realized interfaces:
fig = plot_interfaces(model.interfaces)
fig.show()
Note: visualization of spherical models is not yet supported.
Add support for the stacking of degrees of freedom (DOFs) along a chord of
elements. Stacking in this context refers to re-indexing degrees of freedom
within each element into tangent planes or lines. This functionality is
supported by stack_along_chord method in the
salvus.mesh.mesh.algorithms.dual module. This function returns two indexing
arrays, which contain local and global re-indexing arrays respectively. See the
function’s docstring for more information.
Add support for computing dual graphs of quad and hex meshes as sparse
adjacency matrices, along with new chord-analysis utilities. See
salvus.mesh.algorithms.unstructured_mesh.dual for more details.
Implement mesh writer for exporting Salvus meshes in the Exodus file format
(.e).
Speed up the find_surface algorithm in salvus.mesh.algorithms.unstructured_mesh.side_sets by using a new algorithm based on a mesh’s facet dual graph.
Enable empty control groups in an inversion when using a custom event batch selection. Note that this will not enfore a monotonically decreasing misfit function, and it will neither modify the trust region radius after the first iteration, because an actual reduction is not available in this case.
Example:
import typing
import salvus.namespace as sn
def my_custom_event_selection(
inverse_problem_configuration: sn.InverseProblemConfiguration,
current_iteration_id: int,
) -> typing.Sequence[str] | dict[str, typing.Sequence[str]]:
# Always use all events, but disable the control group.
return {
"events": inverse_problem_configuration.events,
"control_group": []
}
p.add_to_project(
sn.InverseProblemConfiguration(
...,
event_batch_selection=my_custom_event_selection,
)
)Reduce the memory consumption of the L-BFGS scheme and reduce the required disk space. Only models and gradients that are needed to compute the approximate Hessian are retained in memory. Model and gradient differences are now computed on the fly.
Add the ability to serialize custom mapping classes into an InverseProblemConfiguration.
This allows users to define their own mapping classes to transform between physical and inversion spaces:
from salvus.opt.mappings import Mapping
class MyCustomMapping(Mapping):
def __init__(self, *args, **kwargs):
print("In init")
super().__init__(*args, **kwargs)
p += sn.InverseProblemConfiguration(
mapping=MyCustomMapping(
scaling="absolute",
inversion_parameters=["VP", "RHO"]
),
...
)Improve the detection logic of a project directory. It will now raise a useful error message when passing the wrong path as a project directory instead of suggesting to migrate to another project revision.
Add the ability to run an automated quality control on UnstructuredMesh
objects when they are added to a project via an
UnstructuredMeshSimulationConfiguration. This should allow users to catch
potential issues with the mesh early on before running potentially expensive
simulations that might fail at a later stage. The same checks are now performed
when a mesh is first created using the internal Salvus mesher using the
function mesh_from_domain(). Use the skip_validation=False flag to enable
the mesh QC step.
Changes to Experimental Features
Allow using variables in the the additional_srun_arguments Slurm
configuration. Available variables are {NODES}, {RANKS}, and
{TASKS_PER_NODE}, which will be filled automatically.
Forward settings for multi-threading per rank to the solver in task chains.
When using a TaskChainSiteConfig each MPI rank will now use
max_threads_per_worker / ranks_per_salvus_simulation threads.
Assuming a machine with 4 GPUs, the following configuration will run one parallel simulation on each GPU, each using one MPI rank and 10 threads.
tcsc = TaskChainSiteConfig(
...
number_of_parallel_workers=4,
ranks_per_salvus_simulation=1,
max_threads_per_worker=10,
...
)Add a new executor to run Salvus on the Flux framework: https://flux-framework.readthedocs.io/en/latest/.
Add experimental de/serialization routines to meshing protocols, layered models, and materials.
import salvus.namespace as sn
from salvus.mesh.layered_meshing.serialization import (
read_from_h5,
write_to_h5,
)
lm = sn.layered_meshing.LayeredModel(
[
sn.material.from_params(rho=2000.0, vp=2000.0),
sn.layered_meshing.interface.Hyperplane.at(
sn.layered_meshing.interface.Depth(500.0)
),
sn.material.from_params(rho=2500.0, vp=3000.0, vs=2200.0),
sn.layered_meshing.interface.Hyperplane.at(
sn.layered_meshing.interface.Depth(1000.0)
),
sn.material.from_params(rho=3000.0, vp=4000.0, vs=3300.0),
]
)
write_to_h5(filename="layered_model.h5", model=lm)
lm_2 = read_from_h5(filename="layered_model.h5")Add a new experimental chunked representation of a mesh on disk.
This allows building a mesh in parallel and building meshes that are larger than the available memory.
import pathlib
import xarray as xr
import salvus.namespace as sn
from salvus.project.tools.task_chains.prebuilt_workflows.mesh_from_domain_distributed.distributed_mesh_building import ( # NOQA
mesh_from_domain_distributed,
)
domain = sn.domain.dim3.BoxDomain.unit()
model = sn.layered_meshing.LayeredModel(
[
sn.material.from_params(
rho=xr.DataArray([0.0, 1.0], [("x", [0.0, 1.0])]), vp=1.0
),
sn.layered_meshing.interface.Curve(
xr.DataArray(
[-0.1, +0.1],
[("x", [0.0, 1.0])],
attrs={"reference_elevation": 0.8},
)
),
sn.material.from_params(
rho=xr.DataArray([0.0, 1.0], [("x", [1.0, 0.0])]), vp=2.0
),
]
)
mesh_resolution = sn.MeshResolution(
reference_frequency=25.0, elements_per_wavelength=1.1, model_order=4
)
chunked_mesh = mesh_from_domain_distributed(
output_folder=pathlib.Path("chunked_mesh"),
domain=domain,
model=model,
mesh_resolution=mesh_resolution,
sites=None,
min_number_of_chunks=16
)
print(chunked_mesh)
ChunkedMesh
File size: 57.3 MB
Folder: chunked_mesh
Number of mesh chunks: 16
Number of elements: 10976
Model order: 4
An optional mesh_postprocessing_callback_function can be passed to
mesh_from_domain_distributed to modify each chunk and the embedding mesh
after meshing for custom workflows.
The chunked meshing uses shared memory to pass the embedding between multiple runners on a single machine.
Distributed smoothing for chunked meshes using a halo region.
# Smoothing configuration.
sc = sn.ConstantSmoothing(
smoothing_lengths_in_meters={"VP": (20, 20, 20), "RHO": (30, 30, 20)}
)
smoothed_chunked_mesh = smooth_chunked_mesh(
output_folder=tmp_path / "smoothed_chunked_mesh",
chunked_mesh=chunked_mesh,
smoothing_config=sc,
# Halo width around each chunk in meters.
halo_width_in_meters=150.0,
sites={"my_site": task_chain_site_config},
)Add a mitigation for corrupted gradient files in the HDF5 output.
The new measure will detect corrupted gradient files and move them to a quarantine folder inside the project attachments. The failing events are resubmitted during the inversion workflow by default, so the inversion can continue without interruption.
Only if the same event/model combination fails again, the user is required to manually investigate or remove the files from quarantine.