%matplotlib inline
# This notebook will use this variable to determine which
# remote site to run on.
import os
SALVUS_FLOW_SITE_NAME = os.environ.get("SITE_NAME", "local")
PROJECT_DIR = "project"
# Uncomment the following line to delete a
# potentially existing project for a fresh start
# !rm -rf project
import pathlib
import numpy as np
import salvus.namespace as sn
d = sn.domain.dim2.BoxDomain(x0=0.0, x1=2000.0, y0=0.0, y1=1000.0)
Project.from_domain()
constructor as shown below. This function takes a path and a domain object such as the one we just constructed. Calling this function several times will load the project state from disk instead. The project state is persistent and not tied to this notebook, so you can easily take a break and continue working on the project anytime.p = sn.Project.from_domain(path=PROJECT_DIR, domain=d, load_if_exists=True)
p.viz.nb.
followed by the name of the entity. Currently the project does not contain a lot of data yet, but at least we can visualize the domain extent.p.viz.nb.domain()
simple_config
objects from SalvusFlow above. You can use the tab
key to browse through the different categories and to see a list of all available options, or check the documentation here.src = sn.simple_config.source.cartesian.VectorPoint2D(
x=1000.0, y=500.0, fx=0.0, fy=-1e10
)
SimulationConfiguration
.y
position of 800 m. We need to specify a unique station code for each receiver, as well as a list of which fields we'd like the receivers to record (displacement in this case).recs = [
sn.simple_config.receiver.cartesian.Point2D(
y=800.0,
x=x,
network_code="XX",
station_code=f"REC{i + 1}",
fields=["displacement"],
)
for i, x in enumerate(np.linspace(1010.0, 1410.0, 5))
]
Event
to our project. Every event requires a unique name.p.add_to_project(sn.Event(event_name="event_0", sources=src, receivers=recs))
Event
, along with several other relevant objects, can be added to a project by calling add_to_project
or simply using the +=
operator. Once the object is successfully added to the project it is then "serialized", or saved, within the project directory structure. The power and usefulness of this concept will become apparent in a later tutorial. For now all you need to know is that the event is officially a part of our project!p.viz.nb.domain()
background_model
.mc = sn.ModelConfiguration(
background_model=sn.model.background.homogeneous.IsotropicElastic(
rho=2200.0, vp=3200.0, vs=1847.5
)
)
EventConfiguration
. Here, at a bare minimum, we need to specify what type of source wavelet we would like to model, and to provide some basic information about the temporal extent of our upcoming simulations. The reference data were computed with using a Ricker wavelet centered around time and with a center frequency of Hz. The time interval spans a bit more than 0.5 seconds. These parameters are now used to define our EventConfiguration
object. The start time is optional and only necessary here for comparing with the analytic solution in the next part. Salvus will auto-derive an appropriate start time if not given. Because the Ricker wavelet is centered around zero, we have non-zero contributions before and consequently need to start the simulation at a negative starting time.ec = sn.EventConfiguration(
wavelet=sn.simple_config.stf.Ricker(center_frequency=14.5),
waveform_simulation_configuration=sn.WaveformSimulationConfiguration(
start_time_in_seconds=-0.08,
end_time_in_seconds=0.6,
),
)
ec.wavelet.plot()
SimulationConfiguration
. A SimulationConfiguration
is a unique identifier that brings together the model, the source wavelet parameterization, and a proxy of the resolution of the simulation together.
Depending on the application, sufficient accuracy is typically achieved with 7 to 10 grid points per wavelength, sometimes even less. By default, we use a polynomial degree of 4 in the spectral-element mesh, which gives 5 grid points per element. Here, we need to specify elements_per_wavelength
and choose 1.0
.SimulationConfiguration
to our project as below.p.add_to_project(
sn.SimulationConfiguration(
name="my_first_simulation",
max_frequency_in_hertz=30.0,
elements_per_wavelength=1.0,
model_configuration=mc,
event_configuration=ec,
)
)
SimulationConfiguration
directly in the notebook, as below. This function takes a list of events as well, for the purpose of overplotting sources and receivers on the resultant domain. Let's have a look.p.viz.nb.simulation_setup(
simulation_configuration="my_first_simulation", events=p.events.list()
)
[2024-11-20 13:02:53,684] INFO: Creating mesh. Hang on.
<salvus.flow.simple_config.simulation.waveform.Waveform object at 0x79797975b1d0>
SimulationConfiguration
object. While the benefits of this approach are small for small domains and homogeneous models, they will become much clearer when we consider 3-D models and domains with topography.launch_simulations
command below takes a few arguments worth describing:site_name
: This is an identifier which tells SalvusFlow whether you're running on your local machine, some remote cluster, or perhaps the old chess computer in your grandfather's basement. As long as Salvus has been set up correctly on the specified site all data transfers to / from the local or remote machine will happen automatically. Additionally, if a job management system is present on the remote site Flow will monitor the job queue.ranks_per_job
: This is the number of MPI ranks the job will run on, and can range from 1 to whatever your license will allow.events
: A list of events for which to run simulations for.simulation_configuration
: The configuration for which to run simulations for.p.simulations.launch(
ranks_per_job=2,
site_name=SALVUS_FLOW_SITE_NAME,
events=p.events.list(),
simulation_configuration="my_first_simulation",
)
[2024-11-20 13:02:54,112] INFO: Submitting job ... Uploading 1 files... 🚀 Submitted job_2411201302347424_bfa0cfb9e4@local
1
query_simulations()
as below. The parameter block
specifies that we wait until all simulations are done.p.simulations.query(block=True)
True
p.viz.nb.waveforms("my_first_simulation", receiver_field="displacement")
wiggles
or a shotgather
.p.waveforms.get(data_name="my_first_simulation", events=["event_0"])[0].plot(
component="Y", receiver_field="displacement"
)
project
in the next part and compare the simulated seismograms to the semi-analytic benchmark.