Version:
This tutorial is presented as Python code running inside a Jupyter Notebook, the recommended way to use Salvus. To run it yourself you can copy/type each individual cell or directly download the full notebook, including all required files.

My First Salvus Simulation

This micro-tutorial demonstrates how to set up a first Salvus simulation and teaches some of the fundamental concepts of Salvus.
The simulations are very light and do not require any serious resources. Some familiarity with Python is assumed.
For an in-depth tutorial of running simulations with Salvus, please have a look at one of our other tutorials.
Copy
# Three standard Python imports
import os

import numpy as np
import xarray as xr

# The Salvus package.
import salvus.namespace as sn

SALVUS_FLOW_SITE_NAME = os.environ.get("SITE_NAME", "local")

SalvusProject

SalvusProject is a file structure on disk that organizes and stores simulations and inversions for a given domain.
Salvus supports domains in two and three dimensions for spherical and cartesian coordinates systems. For this micro-tutorial we choose a two-dimensional cartesian box domain.
domain = sn.domain.dim2.BoxDomain(x0=0.0, x1=2000.0, y0=0.0, y1=1000.0)
domain.plot()
# Once we have a domain we can initialize a project with a path on disk.
p = sn.Project.from_domain(
    path="my_project", domain=domain, load_if_exists=True
)
Sources are necessary to inject energy into waveform simulations and most applications require the wavefield sampled at point receivers. In Salvus, both are specified as small Python objects.
# Source and receivers exist for multiple dimensions and coordinate systems.
source = sn.simple_config.source.cartesian.VectorPoint2D(
    x=600.0, y=700.0, fx=0.0, fy=-1e10
)

receivers = [
    sn.simple_config.receiver.cartesian.Point2D(
        y=800.0,
        x=x,
        station_code=f"REC{i + 1}",
        # Note that one can specify the desired recording field here.
        fields=["displacement"],
    )
    for i, x in enumerate(np.linspace(1000.0, 1400.0, 5))
]

# The source and receivers object are quite similar to Python dictionaries.
source, receivers
({'location': [600.0, 700.0],
  'spatial_type': 'vector',
  'spatial_weights': [0.0, -10000000000.0]},
 [{'fields': ['displacement'],
   'location': [1000.0, 800.0],
   'location_code': '',
   'network_code': 'XX',
   'station_code': 'REC1'},
  {'fields': ['displacement'],
   'location': [1100.0, 800.0],
   'location_code': '',
   'network_code': 'XX',
   'station_code': 'REC2'},
  {'fields': ['displacement'],
   'location': [1200.0, 800.0],
   'location_code': '',
   'network_code': 'XX',
   'station_code': 'REC3'},
  {'fields': ['displacement'],
   'location': [1300.0, 800.0],
   'location_code': '',
   'network_code': 'XX',
   'station_code': 'REC4'},
  {'fields': ['displacement'],
   'location': [1400.0, 800.0],
   'location_code': '',
   'network_code': 'XX',
   'station_code': 'REC5'}])
# Sources and receivers are tied together in an event. An event can contain an
# arbitrary number of sources and receivers and each event will be one simulation.
event = sn.Event(event_name="event_a", sources=source, receivers=receivers)
# Lastly add the event to the already existing project and visualize it.
p.add_to_project(event)
p.visualizations.notebook.domain()
Salvus is very flexible when it comes to specifying material models or numerical meshes and supports models ranging in complexity from homogeneous materials all the way to fully unstructured custom meshes with a variety of wave physics.
To keep it somewhat interesting, we'll specify a heterogeneous 2-D elastic model here. Salvus interfaces with the xarray library to achieve that. This is a widely used scientific Python library and readers/write to a large variety of file formats exists. Also note that nothing in the next cell is Salvus specific code.
# It helps to define the model larger than your domain to avoid
# edge effects during the model interpolation step.
nx = 500
x = np.linspace(-100, 2100, nx)
y = np.linspace(-100, 1100, nx // 2)
xx, yy = np.meshgrid(x, y, indexing="ij")

# Background model.
vp = 3000.0 * np.ones_like(xx)
vs = 1847.5 * np.ones_like(xx)
rho = 2200.0 * np.ones_like(xx)

# Sinc perturbations.
r = ((xx - 1000.0) / 200) ** 2 + ((yy - 500) / 200) ** 2
vp -= (np.sin(r) / r) * 1000

# Assemble into one dataset.
ds = xr.Dataset(
    data_vars={
        "VP": (["x", "y"], vp),
        "VS": (["x", "y"], vs),
        "RHO": (["x", "y"], rho),
    },
    coords={"x": x, "y": y},
)

# Plot it.
ax = ds.VP.T.plot(figsize=(10, 6))
ax.axes.set_aspect("equal")
# Once we have an xarray data set, we can add it to the project.
model = sn.model.volume.cartesian.GenericModel(name="my_volume_model", data=ds)
p.add_to_project(model, overwrite=True)
A simulation tied together the model with the domain and the event, in addition to a few more settings.
sc = sn.SimulationConfiguration(
    name="my_first_simulation",
    # Mesh resolution settings.
    max_frequency_in_hertz=30.0,
    elements_per_wavelength=2.0,
    tensor_order=2,
    # Using the volumetric model we specified before.
    model_configuration=sn.ModelConfiguration(
        volume_models=["my_volume_model"],
    ),
    # Event specific configuration.
    event_configuration=sn.EventConfiguration(
        # Source wavelet.
        wavelet=sn.simple_config.stf.Ricker(center_frequency=15.0),
        waveform_simulation_configuration=sn.WaveformSimulationConfiguration(
            end_time_in_seconds=0.6
        ),
    ),
    # Absorbing boundaries everywhere but the top.
    absorbing_boundaries=sn.AbsorbingBoundaryParameters(
        reference_frequency=15.0,
        number_of_wavelengths=0.0,
        reference_velocity=1900.0,
        free_surface=["y1"],
    ),
)
# As always, we'll add it to the project, and visualize the full setup.
p.add_to_project(
    sc,
    overwrite=True,
)
p.visualizations.notebook.simulation_setup("my_first_simulation")
[2024-07-02 13:44:19,705] INFO: Creating mesh. Hang on.
<salvus.mesh.data_structures.unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x7f6d23f23190>
# It is finally time to launch the simulation.
p.simulations.launch(
    simulation_configuration="my_first_simulation",
    events=p.events.list(),
    # Determines where and how to run. Depending on the site
    # it will either directly launch, use a job queuing system, use GPUs, ...
    # The simulations will be automatically load balanced.
    site_name=SALVUS_FLOW_SITE_NAME,
    ranks_per_job=2,
    # For many applications, volumetric output is not necessary, for
    # illustrative purposes we use it here.
    extra_output_configuration={
        "volume_data": {
            "sampling_interval_in_time_steps": 10,
            "fields": ["displacement"],
        },
    },
)
p.simulations.query(block=True)
[2024-07-02 13:44:22,442] INFO: Submitting job ...
Uploading 1 files...

🚀  Submitted job_2407021344675127_8200cddf6d@local
True
# Results can be queried per event from the project.
event_data = p.waveforms.get("my_first_simulation", p.events.list()[0])[0]
event_data
<salvus.flow.collections.event_data.EventData at 0x7f6d21778fd0>
# Plot receiver data.
event_data.plot(receiver_field="displacement", component="X")
# It is of course possible to visualize the volumetric wavefield in Paraview
# and we do this in other tutorials. Here we interpolate the wavefield
# to a regular gridded xarray which is oftentimes helpful for future analysis.
ds_wavefield = sn.wavefield_output_to_xarray(
    wo=event_data.get_wavefield_output(
        output_type="volume",
        field="displacement",
        # We could access all time steps here but for larger examples this would be
        # excessive.
        time_steps=slice(30, 31),
    ),
    points=[np.linspace(0, 2000, 200), np.linspace(0, 1000, 100)],
)

# Standard xarray functionality.
ax = ds_wavefield.sel(c=0, t=ds_wavefield.t[0]).T.plot(
    shading="gouraud", infer_intervals=False, figsize=(10, 6)
)
ax.axes.set_aspect("equal")
PAGE CONTENTS