Working with Salvus’ wavefield output files
Salvus contains a powerful set of tools that enable users to output simulated
dynamic quantities, such as velocity, displacement, or strain, within volumes
and along surfaces. These outputs are, in general, saved on unstructured
meshes, making them somewhat awkward to work with using standard tools such as
numpy or xarray. To remedy this, Salvus contains a lightweight
WavefieldOutput class that enables the simple extraction of stored
quantities to regular grids. Let’s dive in and take a look.
import os
import numpy as np
import pathlib
import salvus.namespace as sn
from salvus.mesh.unstructured_mesh_utils import read_model_from_h5
from salvus.toolbox.helpers import wavefield_output
SALVUS_FLOW_SITE_NAME = os.environ.get("SITE_NAME", "local")Setting up a simple simulation
We’ll demonstrate the basic concepts with a 2-D isotropic elastic mesh
created using Salvus’ layered_meshing API. The use of this API, however, is
not required to use the functionality presented herein, and any volumetric
simulation output from a Salvus simulation will suffice.
domain_2d = sn.domain.dim2.BoxDomain(x0=0, x1=1, y0=0, y1=1)
material = sn.layered_meshing.material.elastic.Velocity.from_params(
rho=1.0, vp=1.0, vs=np.sqrt(1 / 3)
)
mesh_2d = sn.layered_meshing.mesh_from_domain(
domain=domain_2d,
model=material,
mesh_resolution=sn.MeshResolution(
reference_frequency=10.0, elements_per_wavelength=2.0
),
)
mesh_2d<salvus.mesh.data_structures.unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x763dde336310>
We’ll then create a project …
p_2d = sn.Project.from_domain(
path="project_2d", domain=domain_2d, load_if_exists=True
)… with source at the top of the domain …
src_2d = sn.simple_config.source.cartesian.VectorPoint2D(
x=0.5,
y=1.0,
fx=0.0,
fy=-1.0,
)
p_2d.add_to_project(sn.Event(event_name="event", sources=[src_2d]))… and set up a simple simulation.
p_2d.add_to_project(
sn.UnstructuredMeshSimulationConfiguration(
name="sim_2d",
unstructured_mesh=mesh_2d,
event_configuration=sn.EventConfiguration(
wavelet=sn.simple_config.stf.Ricker(center_frequency=2.5),
waveform_simulation_configuration=sn.WaveformSimulationConfiguration(
end_time_in_seconds=2.0
),
),
),
)
p_2d.viz.nb.simulation_setup("sim_2d", events=["event"])<salvus.flow.simple_config.simulation.waveform.Waveform object at 0x763e02d56a10>
Finally, let’s run the simulation with additional outputs. Let’s save velocity, and sample the output every 10 time steps.
p_2d.simulations.launch(
simulation_configuration="sim_2d",
events=p_2d.events.list(),
site_name=SALVUS_FLOW_SITE_NAME,
ranks_per_job=1,
extra_output_configuration={
"volume_data": {
"sampling_interval_in_time_steps": 10,
"fields": ["velocity"],
},
},
)
p_2d.simulations.query(block=True)[2025-04-15 20:00:09,068] INFO: Submitting job ... Uploading 1 files... 🚀 Submitted job_2504152000211809_0ba7b3c514@local
True
Analyzing the wavefield output
We can use Salvus’ wavefield_output module to work with the volumetric data
that we just output.
out_2d = p_2d.simulations.get_simulation_output_directory("sim_2d", "event")
vel_wo = wavefield_output.WavefieldOutput.from_file(
pathlib.Path(
out_2d,
"volume_data_output.h5",
),
"velocity",
"volume",
)The simplest thing we might want to do is resample the wavefield to a regular
grid in order to visualize the wavefield directly in the notebook. To do this
we can use the wavefield_output_to_xarray function which, as one might
expect, returns the wavefield interpolated onto an xarray.DataArray. Below
we pass a list of length 2 containing points we want to extract to in each
dimension. Passing a list in this manner will result in an extraction to a
rectilinear grid spanned by the cartesian product of the input arrays, similar
to the behavior of np.meshgrid.
vel_2d = wavefield_output.wavefield_output_to_xarray(
vel_wo,
points=[np.linspace(0, 1, 101), np.linspace(0, 1, 101)],
)That’s all there is to it. We can now slice vel along arbitrary axes in
space and time using xarray’s powerful indexing tools, and plot the result
directly in the notebook. Below we plot the y- (second) component of velocity
at the 35th output slice.
vel_2d.isel(c=1, t=35).T.plot(shading="gouraud", infer_intervals=False)<matplotlib.collections.QuadMesh at 0x763ddbc42490>
In addition to outputting values on rectilinear grids, we can also pass a n-dimensional array of points to get “shotgathers” with complex geometries. For instance, below well get a shotgather that emulates receiver locations following a figure eight.
a, b = 0.5, 0.5
u = np.linspace(0, 2 * np.pi, 1001)
x = a * np.sin(u) / (1 + np.cos(u) ** 2) + 0.5
y = b * np.sin(u) * np.cos(u) / (1 + np.cos(u) ** 2) + 0.5
vel_sg8 = wavefield_output.wavefield_output_to_xarray(
vel_wo, points=np.column_stack((x, y))
)
vel_sg8.isel(c=1).plot(
shading="gouraud", infer_intervals=False, yincrease=False
)<matplotlib.collections.QuadMesh at 0x763ddb5ffb50>
Output data at final time
Saving the last snapshot of a simulation can be interesting for several use cases such as checkpointing or restarting a simulation with inhomogeneous initial values.
Salvus supports outputting the final time values on an unstructured mesh. The output format is independent of the partition and ranks used in the simulation.
To demonstrate its use, we add another simulation and reduce the end time yo 0.5 seconds.
p_2d.add_to_project(
sn.UnstructuredMeshSimulationConfiguration(
name="sim_2d_short",
unstructured_mesh=mesh_2d,
event_configuration=sn.EventConfiguration(
wavelet=sn.simple_config.stf.Ricker(center_frequency=2.5),
waveform_simulation_configuration=sn.WaveformSimulationConfiguration(
end_time_in_seconds=0.5
),
),
),
)Similar to volumetric output, final time data can be selected through the extra_output_configuration.
p_2d.simulations.launch(
simulation_configuration="sim_2d_short",
events=p_2d.events.list(),
site_name=SALVUS_FLOW_SITE_NAME,
ranks_per_job=1,
extra_output_configuration={
"final_time_data": {"fields": ["velocity"]},
},
)
p_2d.simulations.query(block=True)[2025-04-15 20:00:12,201] INFO: Submitting job ... Uploading 1 files... 🚀 Submitted job_2504152000206109_f53dce651e@local
True
To visualize the results, we can simply load the data and attach them to the mesh. Note that the output uses the polynomial degree of the wavefield (4) and we thus have to make sure the mesh has the same tensor order if we want to visualize the fields.
mesh_out = mesh_2d.copy()
mesh_out.change_tensor_order(4)
out_2d = p_2d.simulations.get_simulation_output_directory(
"sim_2d_short", "event"
)
final_output = read_model_from_h5(out_2d / "final_time_data_output.h5")
mesh_out.elemental_fields.clear()
mesh_out.attach_field("velocity_Y", final_output["velocity_Y"])
mesh_out<salvus.mesh.data_structures.unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x763ddb5ede90>
3-D simulations
In 3-D, the concepts are the same. Let’s again create a mesh using the layered_meshing API.
domain_3d = sn.domain.dim3.BoxDomain(x0=0, x1=1, y0=0, y1=1, z0=0, z1=1)
mesh_3d = sn.layered_meshing.mesh_from_domain(
domain=domain_3d,
model=material,
mesh_resolution=sn.MeshResolution(
reference_frequency=5.0, elements_per_wavelength=2.0
),
)
mesh_3d<salvus.mesh.data_structures.unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x763ddc7dfc10>
We’ll again create a porject with a source at the top of the domain …
p_3d = sn.Project.from_domain(
path="project_3d", domain=domain_3d, load_if_exists=True
)
src_3d = sn.simple_config.source.cartesian.VectorPoint3D(
x=0.5,
y=0.5,
z=1.0,
fx=0.0,
fy=0.0,
fz=-1.0,
)
p_3d.add_to_project(sn.Event(event_name="event", sources=[src_3d]))… and set up a simple simulation.
p_3d.add_to_project(
sn.UnstructuredMeshSimulationConfiguration(
name="sim_3d",
unstructured_mesh=mesh_3d,
event_configuration=sn.EventConfiguration(
wavelet=sn.simple_config.stf.Ricker(center_frequency=2.5),
waveform_simulation_configuration=sn.WaveformSimulationConfiguration(
end_time_in_seconds=1.0
),
),
),
)
p_3d.viz.nb.simulation_setup("sim_3d", events=["event"])<salvus.flow.simple_config.simulation.waveform.Waveform object at 0x763ddc2233d0>
Let’s again add some volumetric output, but this time also save the wavefield along the top mesh surface.
p_3d.simulations.launch(
simulation_configuration="sim_3d",
events=p_3d.events.list(),
site_name=SALVUS_FLOW_SITE_NAME,
ranks_per_job=1,
extra_output_configuration={
"volume_data": {
"sampling_interval_in_time_steps": 10,
"fields": ["velocity"],
},
"surface_data": {
"sampling_interval_in_time_steps": 10,
"fields": ["velocity"],
"side_sets": ["z1"],
},
},
)
p_3d.simulations.query(block=True)[2025-04-15 20:00:13,341] INFO: Submitting job ... Uploading 1 files... 🚀 Submitted job_2504152000343951_5b7cfdbf46@local
True
Analyzing wavefield output
As before we can use the WavefieldOutput class to help us manipulate the
data we just created. Here we create 2 objects: one for volume and one for
surface data.
output_dir = p_3d.simulations.get_simulation_output_directory(
"sim_3d", "event"
)
vel_3d_vol_wo = wavefield_output.WavefieldOutput.from_file(
pathlib.Path(output_dir, "volume_data_output.h5"), "velocity", "volume"
)
vel_3d_srf_wo = wavefield_output.WavefieldOutput.from_file(
pathlib.Path(output_dir, "surface_data_output.h5"), "velocity", "surface"
)Let’s take a look at the surface output first. Since the surface is a true
3-D surface it contains 3 spatial coordinate values, rather than 2 as you
might expect. This is important as the surface we are analyzing may not be
axis aligned, and in fact may not be planar at all. To emulate a truly 2-d
surface we can use the drop_coordinate method to drop a coordinate from the
output. Since we know we stored the surface output along the "z1", or top,
side set, we can drop the z, or last, dimension to pull out a surface.
vel_3d_srf_wo_xy = vel_3d_srf_wo.drop_dimension(2)We can then extract the data values to a rectilinear grid and plot using
xarray.
vel_3d_srf = wavefield_output.wavefield_output_to_xarray(
vel_3d_srf_wo_xy, [np.linspace(0, 1, 101)] * 2
)
vel_3d_srf.isel(c=2, t=25).plot(shading="gouraud", infer_intervals=False)<matplotlib.collections.QuadMesh at 0x763dd7b94550>
We can of alternatively the same result by just extracting the 3-D output and along the top surface of the mesh.
vel_3d_vol = wavefield_output.wavefield_output_to_xarray(
vel_3d_vol_wo, [np.linspace(0, 1, 101)] * 2 + [1.0]
)As we expect, the values are the same.
vel_3d_vol.isel(c=2, t=25).sel(z=1.0).plot(
shading="gouraud", infer_intervals=False
)<matplotlib.collections.QuadMesh at 0x763dd9936a90>
As show in the 2-D example, shotgathers or other point measurements can also be extracted at arbitrary locations and curves.