import os
import build123d
import matplotlib.pyplot as plt
import numpy as np
import salvus.namespace as sn
import wscan# Salvus site on which simulations are to be run
SALVUS_FLOW_SITE_NAME = os.environ.get("SITE_NAME", "local")
# Path where the project should be saved
PATH_TO_PROJECT = "project_weld_tutorial"# Maximum frequency of the source in Hz; also controls how finely the mesh is
# to be discretized
MAX_FREQUENCY = 2.5e6
# Due to the number of wavelengths that will be propagated, the
# spectral-element order is chosen as 7
SPECTRAL_ELEMENT_ORDER = 7
# Number of elements per wavelength; one of the key parameters that controls
# how fine the mesh is. Refer to
# https://docs.mondaic.com/knowledge_base/spectral_element_modelling/hp_refinement
# for more details.
ELEMENTS_PER_WAVELENGTH = 1.0
# Simulation end time in seconds
SIMULATION_END_TIME = 25.0e-6sn.material.Material objects. These can either be defined manually or with
W-Scan's material library.# The material properties of the wedge and the plate are taken from the
# material library
WEDGE_MATERIAL = wscan.library.materials.materials["epoxy resin"].material
PLATE_MATERIAL = wscan.library.materials.materials["steel"].material
# The properties of the weld are manually derived from the `PLATE_MATERIAL`
WELD_MATERIAL = sn.material.from_params(
VP=PLATE_MATERIAL.VP.p * 1.05,
VS=PLATE_MATERIAL.VS.p * 1.05,
RHO=PLATE_MATERIAL.RHO.p * 1.05,
)# Plate dimensions
PLATE_WIDTH = 0.15
PLATE_THICKNESS = 0.025
# V-type weld geometry
ROOT_GAP = 0.01
GROOVE_ANGLE_DEGREES = 45.0
FACE_REINFORCEMENT = 0.001
ROOT_REINFORCEMENT = 0.0005
# Probe geometry; PROBE_POSITION places the wedge front face to the left of the
# weld centerline, and TRANSDUCER_POSITION is the center of the transducer
# array on the wedge's inclined face (in probe-local coordinates)
WEDGE_ANGLE_DEGREES = 36.10
PROBE_POSITION = np.array([-1.25 * PLATE_THICKNESS, 0.0])
TRANSDUCER_POSITION = np.array([0.00387835, 0.00982814])
# A very dense collection of transducer elements is used in order to excite a
# smooth plane wave
N_TRANSDUCER_ELEMENTS = 256
TRANSDUCER_ELEMENT_WIDTH = 37.5e-6build123d. Each
component (plate, weld, crack, and probe) is defined separately before being
assembled into a single compound.PLATE_WIDTH and thickness
PLATE_THICKNESS. The Align.MAX setting places the top surface at y = 0,
so the plate extends downward to y = −PLATE_THICKNESS.plate = build123d.Rectangle(
width=PLATE_WIDTH,
height=PLATE_THICKNESS,
align=(build123d.Align.CENTER, build123d.Align.MAX),
).face()plate.label = "plate"
wscan.geometry.dim2.utils.plot_build123d_entity(plate)<Axes: xlabel='x [m]', ylabel='y [m]'>
build123d, W-Scan includes a utility class
wscan.geometry.dim2.welds.VTypeStandard, which parametrizes a standard
single V-shaped weld. The groove angle, root gap, and reinforcement heights
are controlled by the constants defined previously.weld = wscan.geometry.dim2.welds.VTypeStandard(
plate_thickness_in_meters=PLATE_THICKNESS,
root_gap_in_meters=ROOT_GAP,
groove_angle_degrees=GROOVE_ANGLE_DEGREES,
face_reinforcement_in_meters=FACE_REINFORCEMENT,
root_reinforcement_in_meters=ROOT_REINFORCEMENT,
).geometry
wscan.geometry.dim2.utils.plot_build123d_entity(weld)<Axes: xlabel='x [m]', ylabel='y [m]'>
InternalCrack class. A
small dislocation_distance is used to separate the two crack faces. The
origin and direction orient the crack near the weld root, and
lateral_variability controls the roughness of the crack path.random_seed ensures the result is reproducible. However, one
could alternatively vary this within a parameteric study in order to simulate
a collection of different crack geometries.crack = wscan.geometry.dim2.defects.InternalCrack(
extent=PLATE_THICKNESS / 4,
origin=np.array([-ROOT_GAP / 4, -PLATE_THICKNESS / 2]),
direction=np.array([-0.1, -1.0]),
dislocation_distance=PLATE_THICKNESS / 200,
lateral_variability=PLATE_THICKNESS,
n_samples=9,
random_seed=42,
).geometry
wscan.geometry.dim2.utils.plot_build123d_entity(crack)<Axes: xlabel='x [m]', ylabel='y [m]'>
WedgeProbe2D object that encapsulates both the CAD geometry
of the plastic wedge and the transducer element layout:front_face_distance and back_face_distance the lateral extents of the
wedge relative to the origin point.height is the maximum height of the wedge.damping_depth controls the thickness of the absorbing damping layer
attached to the back face of the wedge, which helps to suppress spurious
reflections from the probe body.PROBE_POSITION shifts the entire probe assembly so that the wedge front
face sits slightly to the left of the weld face reinforcement.probe = wscan.instruments.probe.WedgeProbe2D(
material=WEDGE_MATERIAL,
origin=PROBE_POSITION,
transducer_position=TRANSDUCER_POSITION,
front_face_distance=16.9e-3,
back_face_distance=2.7e-3,
height=14.2e-3,
wedge_angle_in_degrees=WEDGE_ANGLE_DEGREES,
element_width=TRANSDUCER_ELEMENT_WIDTH,
number_of_elements=N_TRANSDUCER_ELEMENTS,
maximum_frequency=MAX_FREQUENCY,
wedge_face_side_set_name="wedge",
damping_depth=2.0e-3,
sampling_factor=10.0,
)
probe.plot()<Axes: xlabel='x [m]', ylabel='y [m]'>
build123d.Compound object. Boolean operations are used to carve the weld
region out of the plate and to introduce the crack void into the weld:*(plate - weld): subtracts the weld footprint from the plate so that the
plate and weld bodies do not overlap. Since this boolean subtraction
separates the plate into two halves, the * operator is used to unpack the
resulting compound into its two face components.weld - crack: subtracts the crack from the weld body, creating the
defect void.*probe.geometry.children: unpacks the probe's wedge and damping
sub-bodies.welded_plate = build123d.Compound(
children=(*probe.geometry.children, *(plate - weld), weld - crack),
label="welded_plate",
)
ax = wscan.geometry.dim2.utils.plot_build123d_entity(welded_plate, legend=True)
plt.show()wscan.mesh.mesh_from_geometry. Material properties are assigned to each
labeled face using the material dictionaries defined above. The mesh
resolution is controlled by reference_frequency and
elements_per_wavelength, which together determine the element size needed
to accurately resolve the wavefield at MAX_FREQUENCY.wedge,
damping, plate, and weld. These labels can be inspected using
build123d's show_topology method, which lists the faces and their
attributes.print(welded_plate.show_topology())welded_plate Compound at 0x7426b211c710, Location(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00)) ├── wedge Face at 0x7426a9f3da10, Location(p=(-0.03, 0.00, 0.00), o=(-0.00, 0.00, -0.00)) ├── damping Face at 0x7426b20e8e10, Location(p=(-0.03, 0.00, 0.00), o=(-0.00, 0.00, -0.00)) ├── plate Face at 0x7426a9f36850, Location(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00)) ├── plate Face at 0x7426a9f37450, Location(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00)) └── weld Face at 0x7426a9f375d0, Location(p=(0.00, 0.00, 0.00), o=(-0.00, 0.00, -0.00))
mesh_from_geometry function must contain one material entry for each of
these distinct region names. Due to the geometric intricacies of this
example, the wscan.mesh.algorithms.cubit.pave algorithm is used. However,
since this additionally requires a Coreform Cubit installation, the mesh for
this example has been provided in the accompanying data directory.m = wscan.mesh.mesh_from_geometry( geometry=welded_plate, model={ "wedge": WEDGE_MATERIAL, "damping": WEDGE_MATERIAL, "plate": PLATE_MATERIAL, "weld": WELD_MATERIAL, }, mesh_resolution=sn.MeshResolution( reference_frequency=MAX_FREQUENCY, elements_per_wavelength=ELEMENTS_PER_WAVELENGTH, ), algorithm=wscan.mesh.algorithms.cubit.pave.Mesher( options=wscan.mesh.algorithms.cubit.pave.Options( site=SALVUS_FLOW_SITE_NAME, ) ), )
UnstructuredMesh.from_h5 constructor.m = sn.UnstructuredMesh.from_h5("data/weld_mesh.h5")m<unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x7426a9f317d0>
build123d.Compound:| Block index | Region |
|---|---|
| 1 | Wedge |
| 2 | Damping |
| 3 | Plate |
| 4 | Weld |
apply_element_mask
method.m_weld = m.apply_element_mask(m.elemental_fields["element_block_index"] == 4)
m_plate = m.apply_element_mask(m.elemental_fields["element_block_index"] == 3)
m_probe = m.apply_element_mask(
(m.elemental_fields["element_block_index"] == 1)
| (m.elemental_fields["element_block_index"] == 2)
)find_side_sets automatically detects the exterior
boundary and labels it with cardinal names (x0, x1, y0, y1). The
side sets x0 and x1 (left and right plate edges) will later be assigned
absorbing boundary conditions to prevent edge reflections. While y0 and
y1 are also present, they are not used subsequently and can be ignored.m_plate.find_side_sets()
m_plate<unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x7426a84a9250>
damping: the outer surface of the damping layer, used to apply an
absorbing boundary condition that mimics the physical backing material of
the probe.wedge: the inclined face of the wedge where the transducer elements are
located; this is the interface through which sources and receivers are
defined.# Get the exterior surrounding the entire probe
m_probe.find_surface()
# Select only the damping region and rename the side set to "damping"
m_damping = m_probe.apply_element_mask(
m_probe.elemental_fields["element_block_index"] == 2
)
m_damping.side_sets["damping"] = m_damping.side_sets["surface"]
m_damping.side_sets.pop("surface")
# Select only the wedge region and rename the side set to the wedge face
m_wedge = m_probe.apply_element_mask(
m_probe.elemental_fields["element_block_index"] == 1
)
m_wedge.side_sets[probe.wedge_face_side_set_name] = m_wedge.side_sets[
"surface"
]
m_wedge.side_sets.pop("surface")
# Recombine the probe
m_probe = m_damping + m_wedge
m_probe<unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x7426a8317e10>
m = m_probe + m_plate + m_weld
m<unstructured_mesh.unstructured_mesh.UnstructuredMesh object at 0x7426a8388f10>
# !rm -rf $PATH_TO_PROJECTp = sn.Project.from_mesh(PATH_TO_PROJECT, m)get_event method instantiates the required sources for the
transducer on the wedge probe. Theexcitation_mode="longitudinal" specifies
that the sources emit longitudinal (compressional) waves into the wedge.p.add_to_project(
probe.get_event(
excitation_mode="longitudinal",
source_type=sn.simple_config.source.cartesian.SideSetVectorPoint2D,
receiver_type=sn.simple_config.receiver.cartesian.SideSetPoint2D,
)
)ToneBurst source time function is used to produce a narrowband excitation
centred at approximately three-quarters of the maximum frequency.stf = sn.simple_config.stf.ToneBurst(
center_frequency=3 / 4 * MAX_FREQUENCY,
num_cycles=4,
)
stf.plot()x0) and right (x1) plate edges, in addition to the probe damping
layer (damping), in order to suppress spurious reflections from those
boundaries. For the sake of simplicity, sponge
layers
are disabled by setting width_in_meters=0.0, meaning that only
first-order
absorbing boundary conditions are applied directly on the boundary faces.p.add_to_project(
sn.UnstructuredMeshSimulationConfiguration(
name="forward_simulation",
unstructured_mesh=m,
event_configuration=sn.EventConfiguration(
wavelet=stf,
waveform_simulation_configuration=sn.WaveformSimulationConfiguration(
end_time_in_seconds=SIMULATION_END_TIME,
spectral_element_order=SPECTRAL_ELEMENT_ORDER,
boundary_conditions=sn.simple_config.boundary.Absorbing(
taper_amplitude=MAX_FREQUENCY,
side_sets=["x0", "x1", "damping"],
width_in_meters=0.0,
),
),
),
)
)ax = wscan.geometry.dim2.utils.plot_build123d_entity(
welded_plate, legend=True, figsize=(12, 12)
)
for source in p.events.get(p.events.list()[0]).sources:
plt.plot(source.point[0], source.point[1], "k.")
plt.show()sampling_interval_in_time_steps.p.simulations.launch(
simulation_configuration="forward_simulation",
events=p.events.list(),
site_name=SALVUS_FLOW_SITE_NAME,
ranks_per_job=sn.api.get_site(SALVUS_FLOW_SITE_NAME).config[
"default_ranks"
],
extra_output_configuration={
"volume_data": {
"sampling_interval_in_time_steps": 250,
"fields": ["displacement"],
},
},
)[2026-05-26 12:35:47,325] INFO: Submitting job ... Uploading 2 files... 🚀 Submitted job_2605261235575168_4993c8d48f@local
1
query call blocks until all jobs have completed.p.simulations.query(block=True)True
# Get the event data from which the wavefield can be extracted
event_data = p.waveforms.get("forward_simulation", p.events.list()[0])[0]
# Interpolate the wavefield to a regular grid
wavefield = sn.wavefield_output_to_xarray(
wo=event_data.get_wavefield_output(
output_type="volume",
field="displacement",
),
points=[
np.linspace(-0.013, 0.0125, 201),
np.linspace(-0.026, 0.0025, 201),
],
)[2026-05-26 12:37:14,237] WARNING - salvus.mesh.algorithms.unstructured_mesh.utils: 4285 points were not claimed by enclosing elements. Depending on your use case, this may not be an issue.
c=0) is shown,
which highlights the shear wave mode and the diffraction tips at the crack
ends.# Approximate times at which the waves arrive at the crack
times_to_plot = np.array([10.0e-6, 12.3e-6, 14.3e-6])
for t_plot in times_to_plot:
# Plot the outline of the geometry
ax = wscan.geometry.dim2.utils.plot_build123d_entity(
welded_plate,
face_alpha=0.0,
)
# Get the time index that is closest to `t_plot`
t_ind = np.argmin(np.abs(wavefield.t.to_numpy() - t_plot))
# Plot the wavefield; `c=0` is the x-component of displacement,
# `c=1` is the y-component
wavefield.sel(c=0, t=wavefield.t[t_ind]).T.plot(
shading="gouraud",
infer_intervals=False,
ax=ax,
cmap="coolwarm",
zorder=-1,
)
ax.axes.set_aspect("equal")
plt.show()p.simulations.get_simulation_output_directory(
simulation_configuration="forward_simulation", event=p.events.list()[0]
)PosixPath('project_weld_tutorial/EVENTS/wedge_probe_ef1b4407_0.0_degrees_longitudinal/WAVEFORM_DATA/INTERNAL/fa/78/a59e989f84a2')