Seismic-wave analysis

This tutorial solves the Christoffel equation for a single crystal of hydroxylapatite. It calculates phase and group velocities, polarization axes, shear-wave splitting, power-flow quantities, acoustic enhancement, and sampling diagnostics. The calculation is performed first from the command line and then through the public Python API.

The underlying theory is presented in Seismic-wave propagation; the workflow organization is described in Seismic-wave analysis: implementation and workflow.

Scientific questions

The tutorial examines:

  • how the three acoustic phase velocities vary with propagation direction;

  • where the two shear branches become degenerate or strongly split;

  • how the energy-flow direction differs from the wave normal;

  • how tracked polarization axes should be interpreted;

  • what enhancement and caustic-candidate diagnostics mean;

  • how spherical maps and physical three-dimensional surfaces represent the same acoustic field.

Dataset

The input is a hexagonal hydroxylapatite stiffness tensor with density \(3178\ \mathrm{kg\,m^{-3}}\). It is distributed at examples/elasticity/hydroxylapatite.dat because the same physical tensor is also suitable for the Elasticity workflow.

Download hydroxylapatite.dat

Hydroxylapatite
    187.208   65.193   84.703    0.000    0.000    0.000
              187.208   84.703    0.000    0.000    0.000
                        222.658    0.000    0.000    0.000
                                   39.687    0.000    0.000
                                             39.687    0.000
                                                       61.007
3178

The first six numeric rows define the stiffness matrix in GPa. The final value is the density in kg m⁻³. Density is essential because Christoffel eigenvalues are converted to squared velocities through \(C/\rho\).

Choosing the sampling calculation

The tutorial samples the upper hemisphere with:

  • 61 polar values;

  • 121 azimuthal values;

  • no duplicated azimuthal seam;

  • the enhancement calculation level;

  • tracked polarization axes.

The selected level is hierarchical:

phase

Phase velocity, eigenvalues, polarization, and degeneracy diagnostics.

group

All phase quantities plus group velocity, ray direction, and power-flow angle.

enhancement

All phase and group quantities plus curvature, area factor, enhancement, and caustic-candidate diagnostics.

A coarse grid is adequate for learning and keeps the tutorial fast. Extrema reported from SEISMIC are sampled-grid extrema, so production work should test angular convergence.

Running SEISMIC from the command line

quantas seismic run examples/elasticity/hydroxylapatite.dat \
    --hemisphere upper \
    --level enhancement \
    --ntheta 61 \
    --nphi 121 \
    --track-polarizations \
    --output hydroxylapatite_seismic.hdf5 \
    --report hydroxylapatite_seismic.log \
    --verbosity standard \
    --no-progress \
    --force

The command produces:

hydroxylapatite_seismic.hdf5

Native result containing the elastic input, spherical grid, phase field, group field, enhancement field, tracking diagnostics, options, and provenance.

hydroxylapatite_seismic.log

Deterministic scientific report.

Reading the report

Sampling summary

Seismic calculation summary
---------------------------

           Property         |      Value
  --------------------------+----------------
  Job name                  | Hydroxylapatite
  Elastic symmetry          | hexagonal
  Density / kg m^-3         | 3178.000000
  Mechanical stability      | stable
  Sampling level            | enhancement
  Hemisphere                | upper
  Polar samples             | 61
  Azimuthal samples         | 121
  Sampled directions        | 7381
  Azimuthal seam duplicated | no
  Polarization tracking     | yes

The spherical field stores modes in the stable order \(V_{S2},V_{S1},V_P\). The labels \(S1\) and \(S2\) order the fast and slow shear branches away from degeneracy; exactly at a shear degeneracy, individual transverse eigenvectors are not unique.

Isotropic reference velocities

The Hill-average elastic moduli provide a useful isotropic reference:

Hill-average isotropic velocity reference
-----------------------------------------

   Quantity |   Value
  ----------+-----------
  V_S       | 4.00911178
  V_P       | 7.64303712
  V_P / V_S | 1.90641657

These values are not angular averages of the sampled single-crystal speeds. They are calculated from the Hill bulk and shear moduli and the supplied density. They provide a compact aggregate reference against which the single-crystal directional fields can be compared.

Phase velocities

The sampled extrema are:

Mode

Minimum / km s⁻¹

Maximum / km s⁻¹

Range anisotropy / %

\(V_P\)

7.539892

8.370323

10.43897

\(V_{S1}\)

3.533842

4.381418

21.41623

\(V_{S2}\)

3.533842

4.167169

16.44787

The maximum \(V_P\) occurs along the input-frame \(z\) direction, which corresponds to the hexagonal \([001]\) axis. Its minimum is oblique to the principal axes. Both shear branches are degenerate along \([001]\) at approximately 3.53384 km s⁻¹, as expected for propagation along the high-symmetry axis of this tensor.

Shear splitting and velocity ratios

The maximum sampled shear splitting is:

V_S1 - V_S2 maximum = 0.84757530 km s^-1
Directional S-wave anisotropy maximum = 21.41623306 %

It occurs close to the basal plane, where the contrast between the two transverse branches is largest. Along the \(c\) axis the shear eigenvalues coincide, so the splitting is zero and the individual transverse polarization basis is not unique.

The directional \(V_P/V_S\) ratios are also reported. They are useful observables, but they should not be confused with the single isotropic Hill-reference ratio.

Group velocity and power flow

At the group and enhancement levels, the report gives both the wave normal and the ray direction at each extremum. In an anisotropic medium these directions need not coincide. The group speed is the magnitude of the energy- flow velocity; the power-flow angle measures the deflection between wave normal and ray direction.

For \(V_P\) in this example the difference is small near the speed extrema. For shear waves, the group-speed field and ray direction can differ more substantially from the phase-normal geometry, especially near rapidly changing polarizations or near-degenerate branches.

Enhancement and caustic candidates

The calculation reports extrema of \(\log_{10}(A)\) and masks for finite, resolved, and possible caustic points. For this grid:

Mode  Finite resolved points  Caustic candidates  Non-finite points
V_P                    7381                   0                  0
V_S1                   7260                   0                  0
V_S2                   7260                   0                  0

The lower count for the shear modes comes from unresolved degeneracy points, not from failed phase velocities. A zero candidate count means that no sampled point satisfies the selected curvature threshold. It does not prove that the continuous surface contains no caustic between grid points.

Polarization tracking diagnostics

The report records sign alignments, shear-branch exchanges, ambiguous permutations, and rotations within a degenerate shear subspace. These are not physical discontinuities added to the result: they document the deterministic choices required to represent axial eigenvectors continuously on an ordered grid.

A polarization axis has no intrinsic arrow direction, so \(\mathbf p\) and \(-\mathbf p\) describe the same physical axis. Near an exact degeneracy, the separate shear axes are not uniquely defined even though their two- dimensional transverse subspace remains well defined.

Exporting sampled fields

Export all sampled quantities to CSV:

quantas seismic export hydroxylapatite_seismic.hdf5 \
    --output hydroxylapatite_seismic.csv

The export contains one row per sampled direction and acoustic mode. The header identifies the stable mode ordering and includes phase, group, enhancement, degeneracy, and tracking fields:

# Quantas sampled seismic-wave export
# Job,Hydroxylapatite
# Density / kg m^-3,3178
# Mode order,"V_S2, V_S1, V_P"
point_index,mode,theta_rad,theta_degree,phi_rad,phi_degree,...

This is intentionally a detailed machine-readable table. Filtering by mode and selecting only the columns required for a downstream analysis is usually more practical than opening the full CSV in a text editor.

Selected plots

Six-panel seismic summary

quantas seismic plot hydroxylapatite_seismic.hdf5 \
    --summary \
    --polarization \
    --axis-labels crystal \
    --preset publication \
    --dpi 180 \
    --output hydroxylapatite_summary
Six-panel seismic summary for hydroxylapatite

Equal-area upper-hemisphere summary of \(V_P\), \(V_{S1}\), and \(V_{S2}\), directional shear anisotropy, and the two directional \(V_P/V_S\) ratios. Minimum and maximum markers are selected from the sampled grid. Decimated black segments show tracked polarization axes on maps where they are defined.

The summary makes the hexagonal pattern immediately visible. The central point is \([001]\); the outer boundary is the basal plane. The compressional wave is fastest along \([001]\), while the largest shear splitting occurs toward the edge of the projection.

A single shear map with polarization

quantas seismic plot hydroxylapatite_seismic.hdf5 \
    --2d \
    --property phase_v_s1 \
    --projection equal_area \
    --polarization \
    --polarization-stride 8 \
    --axis-labels crystal \
    --preset publication \
    --dpi 180 \
    --output hydroxylapatite_vs1
Fast shear-wave phase velocity and polarization axes

Fast shear-wave phase velocity \(V_{S1}\). The central minimum is the shear degeneracy on \([001]\); the fast branch reaches its maximum near the basal plane. Polarization segments are axial, not directed arrows, and should not be interpreted inside unresolved degenerate points.

Three-dimensional compressional phase surface

quantas seismic plot hydroxylapatite_seismic.hdf5 \
    --3d \
    --surface phase \
    --mode v_p \
    --geometry physical \
    --axis-labels crystal \
    --no-polarization \
    --preset publication \
    --dpi 180 \
    --output hydroxylapatite_vp_surface
Physical three-dimensional P-wave phase-velocity surface

Physical \(V_P\) surface completed by antipodal symmetry from the sampled upper hemisphere. Radius and color both encode phase speed. The elongation along \([001]\) reflects the maximum compressional velocity in that direction.

The slowness and group surface types answer different questions. Slowness geometry is especially useful for wave-normal constructions, whereas the group surface represents energy-flow wavefronts.

Running the same workflow from Python

The complete public-API script is distributed with the examples:

Download the SEISMIC API script

  1"""Reproduce the Hydroxylapatite SEISMIC tutorial with the public API."""
  2
  3from __future__ import annotations
  4
  5import argparse
  6from pathlib import Path
  7
  8import numpy as np
  9
 10from quantas.api import common, rendering, seismic
 11
 12
 13def main() -> None:
 14    """Run SEISMIC, persist fields, export CSV, and render selected figures."""
 15    parser = argparse.ArgumentParser()
 16    parser.add_argument("input", type=Path)
 17    parser.add_argument(
 18        "--output-dir",
 19        type=Path,
 20        default=Path("seismic_tutorial"),
 21    )
 22    args = parser.parse_args()
 23
 24    args.output_dir.mkdir(parents=True, exist_ok=True)
 25
 26    options = seismic.Options(
 27        ntheta=61,
 28        nphi=121,
 29        hemisphere="upper",
 30        level="enhancement",
 31        track_polarization_axes=True,
 32    )
 33    result = seismic.run(args.input, options=options)
 34    payload = seismic.get_result(result)
 35
 36    report = rendering.render_tables(
 37        seismic.build_report(result, level="extended")
 38    )
 39    report_path = args.output_dir / "hydroxylapatite_seismic_api.log"
 40    report_path.write_text(report, encoding="utf-8", newline="\n")
 41    hdf5_path = seismic.write_result(
 42        result,
 43        args.output_dir / "hydroxylapatite_seismic_api.hdf5",
 44        report_text=report,
 45    )
 46    csv_path = seismic.write_csv(
 47        result,
 48        args.output_dir / "hydroxylapatite_seismic_api.csv",
 49    )
 50
 51    summary_options = seismic.PlotOptions(
 52        include_polarizations=True,
 53        polarization_stride=8,
 54    )
 55    summary = seismic.build_summary(result, options=summary_options)
 56    rendering.render_plots(
 57        common.PlotCollection(plots=[summary]),
 58        output_dir=args.output_dir,
 59        filename_prefix="hydroxylapatite_",
 60        preset="publication",
 61        dpi=180,
 62        close=True,
 63        axis_label_mode="crystal",
 64    )
 65
 66    vs1_map = seismic.build_plots(
 67        result,
 68        seismic.PlotOptions(
 69            properties=("phase_v_s1",),
 70            projection="equal_area",
 71            include_extrema_markers=True,
 72            include_polarizations=True,
 73            polarization_stride=8,
 74        ),
 75    )
 76    rendering.render_plots(
 77        vs1_map,
 78        output_dir=args.output_dir,
 79        filename_prefix="hydroxylapatite_",
 80        preset="publication",
 81        dpi=180,
 82        close=True,
 83        axis_label_mode="crystal",
 84    )
 85
 86    vp_surface = seismic.build_surfaces(
 87        result,
 88        seismic.SurfaceOptions(
 89            properties=(),
 90            modes=("v_p",),
 91            surface_types=("phase",),
 92            geometry="physical",
 93            include_polarizations=False,
 94        ),
 95    )
 96    rendering.render_plots(
 97        vp_surface,
 98        output_dir=args.output_dir,
 99        filename_prefix="hydroxylapatite_",
100        preset="publication",
101        dpi=180,
102        close=True,
103        axis_label_mode="crystal",
104    )
105
106    phase = payload.field.phase
107    vp = phase.for_mode("v_p")
108    vs1 = phase.for_mode("v_s1")
109    vs2 = phase.for_mode("v_s2")
110    print(f"Sampled directions: {phase.n_points}")
111    print(f"Isotropic V_S / km s^-1: {payload.isotropic_velocities.shear:.8f}")
112    print(
113        "Isotropic V_P / km s^-1: "
114        f"{payload.isotropic_velocities.compressional:.8f}"
115    )
116    print(f"V_P range / km s^-1: {np.nanmin(vp.phase_speeds):.8f} "
117          f"{np.nanmax(vp.phase_speeds):.8f}")
118    print(f"V_S1 range / km s^-1: {np.nanmin(vs1.phase_speeds):.8f} "
119          f"{np.nanmax(vs1.phase_speeds):.8f}")
120    print(f"V_S2 range / km s^-1: {np.nanmin(vs2.phase_speeds):.8f} "
121          f"{np.nanmax(vs2.phase_speeds):.8f}")
122    print(f"Written: {hdf5_path}")
123    print(f"Written: {report_path}")
124    print(f"Written: {csv_path}")
125
126
127if __name__ == "__main__":
128    main()

Run it with:

python examples/seismic/tutorial_api.py \
    examples/elasticity/hydroxylapatite.dat \
    --output-dir seismic_tutorial

The script uses only supported public namespaces:

from quantas.api import common, rendering, seismic

It demonstrates:

The printed checkpoints are:

Sampled directions: 7381
Isotropic V_S / km s^-1: 4.00911178
Isotropic V_P / km s^-1: 7.64303712
V_P range / km s^-1: 7.53989166 8.37032277
V_S1 range / km s^-1: 3.53384250 4.38141779
V_S2 range / km s^-1: 3.53384250 4.16716850

Inspecting structured fields

from quantas.api import seismic

result = seismic.read_result("hydroxylapatite_seismic.hdf5")
payload = seismic.get_result(result)

phase = payload.field.phase
vp = phase.for_mode("v_p")
vs1 = phase.for_mode("v_s1")

print(phase.n_points)
print(vp.phase_speeds.shape)
print(vs1.polarizations.shape)
print(phase.shear_axis_candidate_mask.sum())

if payload.field.group is not None:
    group_vp = payload.field.group.for_mode("v_p")
    print(group_vp.group_speeds.shape)
    print(group_vp.power_flow_angles.shape)

if payload.field.enhancement is not None:
    print(payload.field.enhancement.log10_enhancement.shape)
    print(payload.field.enhancement.caustic_candidate_mask.sum())

All arrays use the same ordered spherical grid. Masks must be respected when working with degenerate or unresolved quantities.

CLI/API equivalence

import numpy as np
from quantas.api import seismic

cli = seismic.get_result(
    seismic.read_result("hydroxylapatite_seismic.hdf5")
)
api = seismic.get_result(
    seismic.read_result(
        "seismic_tutorial/hydroxylapatite_seismic_api.hdf5"
    )
)

np.testing.assert_allclose(
    cli.field.phase.phase_speeds,
    api.field.phase.phase_speeds,
)
np.testing.assert_allclose(
    cli.field.group.group_speeds,
    api.field.group.group_speeds,
    equal_nan=True,
)
np.testing.assert_allclose(
    cli.field.enhancement.log10_enhancement,
    api.field.enhancement.log10_enhancement,
    equal_nan=True,
)

Reproducibility checkpoints

For the documented grid and default numerical tolerances:

Quantity

Expected value

Elastic symmetry

hexagonal

Sampled directions

7381

Isotropic \(V_S\) / km s⁻¹

4.00911178

Isotropic \(V_P\) / km s⁻¹

7.64303712

Minimum \(V_P\) / km s⁻¹

7.53989166

Maximum \(V_P\) / km s⁻¹

8.37032277

Maximum shear splitting / km s⁻¹

0.84757530

Maximum directional shear anisotropy / %

21.41623306

Caustic candidates on this grid

0

The phase speeds at a fixed sampled direction should be invariant to batch size. Sampled extrema, equivalent-point counts, and possible caustic detection can change when angular resolution or numerical tolerances are changed.

Grid-convergence exercise

Repeat the calculation with a denser grid:

quantas seismic run examples/elasticity/hydroxylapatite.dat \
    --hemisphere upper \
    --level enhancement \
    --ntheta 121 \
    --nphi 241 \
    --output hydroxylapatite_seismic_dense.hdf5 \
    --report hydroxylapatite_seismic_dense.log \
    --no-progress \
    --force

Compare:

  • phase-velocity minima and maxima;

  • maximum shear splitting;

  • number and location of shear-axis candidate points;

  • extrema of \(\log_{10}(A)\);

  • caustic-candidate count.

Phase fields generally converge more smoothly than enhancement fields because enhancement depends on curvature. A candidate that appears only on one coarse grid should be treated as a numerical hypothesis requiring refinement, not as a confirmed physical caustic.