Mondaic

Salvus 2026.5.0

Released: 2026-05-18

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

SalvusCompute

Added an experimental memory estimation tool that can be used to estimate the required memory usage of a simulation before running it.

SalvusCompute

Speed up the time step computation in both the solver and the mesher by binding the minimum distance computation to a C++ implementation.

SalvusCompute

Add more detailed timing statistics to adjoint runs.

SalvusCompute

Add JSON timing output to a simulation’s meta.json file for simplified downstream reporting.

SalvusCompute

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.

SalvusCompute

Add experimental support for more detailed timing of simulations in SalvusCompute.

SalvusCore

Improve class and function serialization logic.

SalvusFlow

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.

SalvusFlow

If an interactive SSH login is requested, Salvus will now ensure it happens, even if the initial connection attempt succeeded.

SalvusFlow

Enables using the rotation_on_output argument on normal Point2D and Point3D objects.

SalvusMaterial

New salvus.material.attenuation.plot_q_model() function to visualize attenuation models.

SalvusMesh

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.

SalvusMesh

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.

SalvusMesh

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.

SalvusMesh

Implement mesh writer for exporting Salvus meshes in the Exodus file format (.e).

SalvusMesh

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.

SalvusOpt

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,
    )
)
SalvusOpt

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.

SalvusProject

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"]
    ),
    ...
)
SalvusProject

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.

SalvusProject

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

SalvusFlow

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.

SalvusFlow

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,
    ...
)
SalvusFlow

Add a new executor to run Salvus on the Flux framework: https://flux-framework.readthedocs.io/en/latest/.

SalvusMesh

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")
SalvusMesh

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.

SalvusProject

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},
)
SalvusProject

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.