Dynamic SENT: inspect a complete public PhAST example¶
This notebook follows the public B3_dynamic_sent example from mesh and named
regions to boundary conditions, solver selection, retained histories, and
crack-growth animation. It is a propagation-focused tutorial because the
example already includes a compact public evidence bundle.
The notebook does not rerun the full dynamic simulation by default.
Displayed fields, histories, and animations are retained evidence from a prior
run. The optional execution cell prints the exact current command and requires
an explicit RUN_FULL = True decision.
Learning objectives:
Locate the current YAML and retained mesh for one public example.
Connect named mesh regions to displacement boundary conditions.
Identify material, phase-field, loading, and explicit-solver choices.
Distinguish schema preflight from simulation execution.
Inspect retained propagation without claiming a fresh calculation.
Detect provenance differences between retained evidence and current input.
Claim boundary: B3 is a qualitative lightweight dynamic-fracture example, not convergence-quality benchmark validation.
1. Locate the existing public artifacts¶
Run this notebook from an installed PhAST repository checkout. For a new environment, follow Getting started first. This notebook performs no geometry construction: it inspects checked-in artifacts so that the physical and numerical decisions remain visible.
import json
import subprocess
import sys
from pathlib import Path
from IPython.display import Image, display
import matplotlib.pyplot as plt
import numpy as np
import yaml
here = Path.cwd().resolve()
repo_root = next(
(
candidate
for candidate in (here, *here.parents)
if (candidate / "pyproject.toml").is_file()
and (candidate / "src" / "phast").is_dir()
),
None,
)
if repo_root is None:
raise RuntimeError(
"Start Jupyter from a PhAST repository checkout. "
"See docs/getting-started.md for installation."
)
example_dir = repo_root / "examples" / "dynamic" / "B3_dynamic_sent"
required = [
example_dir / "config.yaml",
example_dir / "mesh.msh",
example_dir / "initial_conditions.png",
example_dir / "damage_final.png",
example_dir / "damage_evolution.gif",
example_dir / "energy.csv",
example_dir / "crack_tip.csv",
example_dir / "run_metadata.json",
]
missing = [str(path.relative_to(repo_root)) for path in required if not path.is_file()]
if missing:
raise FileNotFoundError(f"Missing public B3 artifacts: {missing}")
print("Repository:", repo_root)
print("Example:", example_dir.relative_to(repo_root))
print("Required public artifacts:", len(required))
Repository: /home/runner/work/PhAST/PhAST
Example: examples/dynamic/B3_dynamic_sent
Required public artifacts: 8
2. Read the problem before reading solver options¶
B3 represents a 40 mm by 40 mm single-edge-notched tension plate with a 20 mm wedge notch. The top and bottom edges receive equal and opposite prescribed vertical displacements. The left and right edges are constrained in the horizontal direction.
Before continuing, predict:
which named regions should appear in the mesh;
why opposite vertical displacements are used;
where damage should first localize;
which quantities require density and physical time.
config = yaml.safe_load((example_dir / "config.yaml").read_text(encoding="utf-8"))
print("Problem:", config["problem"]["name"])
print("Geometry units:", config["geometry"]["units"])
print("Named regions:", sorted(config["geometry"]["named_groups"]))
print("Material:")
for key in ("E", "nu", "Gc", "l0", "rho", "eta_residual", "energy_split", "pf_model"):
print(f" {key}: {config['material'][key]}")
print("Loading:", config["loading"])
print("Solver:", config["solver"])
Problem: Dynamic SENT
Geometry units: mm
Named regions: ['bottom', 'left', 'right', 'top']
Material:
E: 32000.0
nu: 0.2
Gc: 0.003
l0: 0.5
rho: 2.45e-09
eta_residual: 1e-07
energy_split: spectral
pf_model: AT2
Loading: {'protocol': 'simple', 't_total': 0.0001, 'ramp_type': 'smooth_step', 't_ramp': 2e-05}
Solver: {'solver_type': 'explicit', 'dt_safety': 0.8}
3. Inspect the retained mesh and named-region contract¶
The checked-in mesh is a retained input artifact. The important teaching contract is that region names identify the boundaries used by loading and constraints. The current YAML also contains a geometry recipe, but this notebook does not assume that remeshing under another PhAST or Gmsh revision reproduces the checked-in mesh exactly.
The separate problem-setup notebook explains geometry construction and meshing when you are ready to author a new specimen.
import phast
mesh_summary = phast.inspect_mesh(example_dir / "mesh.msh")
print("Points:", mesh_summary["n_points"])
print("Cell blocks:", mesh_summary["cells"])
print("Named groups:")
print(json.dumps(mesh_summary["named_groups"], indent=2))
Points: 1091
Cell blocks: [{'type': 'line', 'count': 240, 'nodes_per_cell': 2}, {'type': 'triangle', 'count': 1940, 'nodes_per_cell': 3}]
Named groups:
{
"bottom": {
"id": 1,
"dimension": 1,
"cell_counts": {
"line": 20
}
},
"left": {
"id": 4,
"dimension": 1,
"cell_counts": {
"line": 24
}
},
"notch_lower": {
"id": 6,
"dimension": 1,
"cell_counts": {
"line": 76
}
},
"notch_upper": {
"id": 5,
"dimension": 1,
"cell_counts": {
"line": 76
}
},
"plate": {
"id": 7,
"dimension": 2,
"cell_counts": {
"triangle": 1940
}
},
"right": {
"id": 2,
"dimension": 1,
"cell_counts": {
"line": 24
}
},
"top": {
"id": 3,
"dimension": 1,
"cell_counts": {
"line": 20
}
}
}
display(Image(filename=str(example_dir / "initial_conditions.png")))
4. Connect regions to boundary conditions¶
Each boundary-condition entry names a mesh region, a condition type, a component
(0 for horizontal or 1 for vertical displacement), and, where required,
a prescribed value. This is the shortest auditable route from the diagram to
the executable input.
for index, condition in enumerate(config["boundary_conditions"], start=1):
print(
f"{index}. region={condition['nodes']!r}, "
f"type={condition['type']!r}, "
f"component={condition.get('component')!r}, "
f"value={condition.get('value', 0.0)!r}"
)
expected_regions = {"left", "right", "top", "bottom"}
used_regions = {condition["nodes"] for condition in config["boundary_conditions"]}
if used_regions != expected_regions:
raise RuntimeError(
f"Boundary-condition regions {used_regions} do not match {expected_regions}"
)
1. region='left', type='fix', component=0, value=0.0
2. region='right', type='fix', component=0, value=0.0
3. region='top', type='prescribe', component=1, value=0.002
4. region='bottom', type='prescribe', component=1, value=-0.002
5. Identify the numerical route¶
The current YAML selects explicit mechanics with a stable-step safety factor. The phase-field damage update remains a separate subproblem within the coupled algorithm. Inspect solver logs and run metadata rather than inferring the actual route from hardware alone.
solver = config["solver"]
material = config["material"]
assert solver["solver_type"] == "explicit"
assert material["pf_model"] == "AT2"
assert material["energy_split"] == "spectral"
assert material["rho"] > 0.0
print("Mechanics route:", solver["solver_type"])
print("Stable-step safety factor:", solver["dt_safety"])
print("Phase-field model:", material["pf_model"])
print("Energy split:", material["energy_split"])
print("Density:", material["rho"])
Mechanics route: explicit
Stable-step safety factor: 0.8
Phase-field model: AT2
Energy split: spectral
Density: 2.45e-09
6. Run structural preflight¶
This command checks YAML structure and implemented-option constraints. It does not advance time, solve mechanics or damage, regenerate the animation, or validate the model against Borden et al.
validate_command = [
sys.executable,
"-m",
"phast",
"run",
str(example_dir / "config.yaml"),
"--validate-only",
]
print(" ".join(validate_command))
subprocess.run(validate_command, check=True, cwd=repo_root)
/opt/hostedtoolcache/Python/3.11.16/x64/bin/python -m phast run /home/runner/work/PhAST/PhAST/examples/dynamic/B3_dynamic_sent/config.yaml --validate-only
OK: /home/runner/work/PhAST/PhAST/examples/dynamic/B3_dynamic_sent/config.yaml passes schema validation.
CompletedProcess(args=['/opt/hostedtoolcache/Python/3.11.16/x64/bin/python', '-m', 'phast', 'run', '/home/runner/work/PhAST/PhAST/examples/dynamic/B3_dynamic_sent/config.yaml', '--validate-only'], returncode=0)
7. Keep full execution an explicit decision¶
The current full command is provided for deliberate reproduction work, but it is disabled here. Runtime depends on the regenerated mesh, device, output cadence, and installed environment. The retained A100 timing shown later is not a runtime promise for the current YAML or classroom hardware.
RUN_FULL = False
output_dir = repo_root / "runs" / "B3_dynamic_sent"
run_command = [
sys.executable,
"-m",
"phast",
"run",
str(example_dir / "config.yaml"),
"--output_dir",
str(output_dir),
]
if RUN_FULL:
subprocess.run(run_command, check=True, cwd=repo_root)
else:
print("Full solve disabled. To run deliberately, set RUN_FULL = True.")
print("Command:", " ".join(run_command))
Full solve disabled. To run deliberately, set RUN_FULL = True.
Command: /opt/hostedtoolcache/Python/3.11.16/x64/bin/python -m phast run /home/runner/work/PhAST/PhAST/examples/dynamic/B3_dynamic_sent/config.yaml --output_dir /home/runner/work/PhAST/PhAST/runs/B3_dynamic_sent
8. Inspect retained crack propagation¶
The following field and animation are loaded from
examples/dynamic/B3_dynamic_sent/. They are retained public evidence, not
outputs generated by this notebook session.
display(Image(filename=str(example_dir / "damage_final.png")))
display(Image(filename=str(example_dir / "damage_evolution.gif")))
9. Inspect retained energy and crack-tip histories¶
A dynamic result should not be interpreted from a final damage image alone. The retained files record energy and inferred crack-tip position against physical time. These plots describe the checked-in result.
energy = np.genfromtxt(
example_dir / "energy.csv",
delimiter=",",
names=True,
dtype=float,
)
crack_tip = np.genfromtxt(
example_dir / "crack_tip.csv",
delimiter=",",
names=True,
dtype=float,
)
fig, axes = plt.subplots(1, 2, figsize=(12, 4.2))
time_us = energy["t_s"] * 1.0e6
for name in ("elastic", "kinetic", "fracture", "total"):
axes[0].plot(time_us, energy[name], label=name)
axes[0].set_xlabel("Time [microseconds]")
axes[0].set_ylabel("Energy [consistent model units]")
axes[0].set_title("Retained energy history")
axes[0].grid(alpha=0.25)
axes[0].legend()
axes[1].plot(crack_tip["t_us"], crack_tip["crack_tip_x_mm"])
axes[1].set_xlabel("Time [microseconds]")
axes[1].set_ylabel("Crack-tip x position [mm]")
axes[1].set_title("Retained inferred crack-tip position")
axes[1].grid(alpha=0.25)
fig.tight_layout()
plt.show()
10. Compare retained provenance with the current YAML¶
The retained bundle records the run that produced the public images and histories. It is not automatically identical to the current configuration. This comparison prevents accidental claims that the notebook has reproduced the retained result.
metadata = json.loads(
(example_dir / "run_metadata.json").read_text(encoding="utf-8")
)
print("Retained source revision:", metadata.get("git_hash"))
print("Retained device:", metadata.get("device"))
print("Retained mesh:", metadata.get("mesh"))
print("Retained solver:", metadata.get("solver"))
print("Retained runtime [s]:", metadata.get("total_time_s"))
print("Retained crack time [microseconds]:", metadata.get("crack_time_us"))
print(
"Residual stiffness: retained =",
metadata["material"]["eta_residual"],
", current YAML =",
config["material"]["eta_residual"],
)
if metadata["material"]["eta_residual"] != config["material"]["eta_residual"]:
print(
"PROVENANCE NOTE: retained evidence and current YAML use different "
"residual-stiffness values; they must not be described as identical runs."
)
Retained source revision: adfab6b
Retained device: {'device': 'cuda:0', 'dtype_default': 'float64', 'gpu_name': 'NVIDIA A100 80GB PCIe', 'vram_gb': 79.3}
Retained mesh: {'n_nodes': 1091, 'n_elements': 1940, 'h_min': 0.07787059161639752}
Retained solver: {'solver_type': 'explicit', 'num_steps': 6116, 'dt': 1.635282423944348e-08, 'dt_s': '1.6353e-08', 'dt_us': '0.0164', 't_total_us': '100.01', 't_ramp_us': '20.0', 'disp_max_mm': 0.002, 'c_R_mm_s': 2119003.0}
Retained runtime [s]: 21.25
Retained crack time [microseconds]: 17.645
Residual stiffness: retained = 1e-06 , current YAML = 1e-07
PROVENANCE NOTE: retained evidence and current YAML use different residual-stiffness values; they must not be described as identical runs.
11. Interpretation and next steps¶
You should now be able to trace:
current config.yaml + retained mesh/evidence
-> named regions
-> boundary conditions and loading
-> explicit mechanics plus phase-field updates
-> retained metadata, histories, fields, and animation
Exit questions:
Which evidence in this notebook is generated now, and which is retained?
What does
--validate-onlyleave untested?Why must current YAML and retained evidence be distinguished?
Which mesh, time-step, material, and comparison studies would be required before making a quantitative fracture claim?
Continue with the problem-setup notebook to author geometry and conditions, the phase-field primer for the governing model, and From a tutorial to a first research study to plan a defensible extension.