Note
Go to the end to download the full example code.
From a CIF to a dynamical master pattern and detector images#
Load silicon, inspect the crystal and diffraction objects, simulate electron transport, compute both master-pattern hemispheres, and project them for known orix orientations. Finish with a compact phase overview and an HDF5 round trip.
The bundled CIF contains an asymmetric unit in angstroms. This example uses
5,000 Monte Carlo electrons, a 201-by-201 grid per hemisphere, and dmin=0.05
nm, which admits ~340 reflections – enough bands to read the pattern as a real
one. It is still not a converged simulation: the electron count is low and the
grid is a quarter of a production one. Increase the electron count, the
integration-depth window and the grid resolution in a convergence study before
interpreting fine experimental contrast.
from pathlib import Path
from tempfile import TemporaryDirectory
import anyplotlib as apl
import numpy as np
import torch
from orix.quaternion import Rotation
import de_ebsd as de
from de_ebsd.diffraction.kvectors import sample_incident_directions
Load the crystal#
Resolve the CIF beside this script, or beside a downloaded notebook. The last fallback also supports running the notebook from the repository root.
cif_path = Path(__file__).with_name("Si.cif") if "__file__" in globals() else Path("Si.cif")
if not cif_path.is_file():
cif_path = Path("examples") / "Si.cif"
crystal = de.Crystal.from_cif(cif_path)
print(crystal)
print(f"Unit-cell volume: {crystal.volume:.5f} nm^3; density: {crystal.density:.3f} g/cm^3")
crystal.show()
Crystal(a=0.5431 nm, b=0.5431, c=0.5431, alpha=90, beta=90, gamma=90, space_group=227, atoms=[Si(8x)])
Unit-cell volume: 0.16019 nm^3; density: 2.329 g/cm^3
Configure the experiment and simulate electron transport#
energy is in eV; the transport API takes keV. Explicitly pass the same
sample tilt to transport and detector geometry. A fixed seed makes repeat
runs on the same device reproducible. Set device = "cuda" for a CUDA GPU.
device = "cpu"
microscope = de.Microscope(energy=20e3, sample_tilt=70.0)
detector = de.EBSDDetector(
shape=(96, 128),
pattern_center=(0.45, 0.55, 0.60),
tilt=5.0,
sample_tilt=microscope.sample_tilt,
px_size=50.0,
)
depth = de.DepthDistribution.from_simulation(
crystal,
microscope.energy_kev,
n_electrons=5_000,
tilt_deg=microscope.sample_tilt,
e_min_kev=15.0,
e_bin_kev=2.5,
depth_max_nm=60.0,
depth_step_nm=2.0,
seed=7,
device=device,
)
print(depth)
print("Bruker PC:", detector.pattern_center, "gnomonic bounds:", detector.gnomonic_bounds)
DepthDistribution(3 energy bins [15..20 keV], 31 depth bins x 2 nm, 5e+03 electrons)
Bruker PC: (0.45, 0.55, 0.6) gnomonic bounds: [-1. 1.22222222 -0.75 0.91666667]
Inspect the simulation before computing#
Reusing the supplied depth distribution avoids repeating Monte Carlo when computing or refining this simulator.
sim = de.MasterPatternSimulator(
crystal,
microscope,
npx=100,
dmin=0.05,
monte_carlo=depth,
backend="bloch_prism",
)
sim.structure_factors().show(n=12)
Classify strong and weak reflections for a particular incident direction.
sim.reflections(k=(1, 1, 3)).show(n=16)
Inspect the corresponding dynamical matrix, including Bethe corrections.
sim.dynamical_matrix(k=(1, 1, 3)).show()
The sampling points cover both hemispheres, including their shared equator.
Drawn at a deliberately coarse npx so the individual directions stay
visible: the grid this simulation actually solves, npx=100, is 80,802
points and renders as a solid ball.
sample_incident_directions(16).show()
Compute the full master pattern#
Symmetry folding reduces the number of evaluated directions. The result still contains both complete hemispheres in square-Lambert storage. This single-energy calculation uses the 20 keV depth bin.
master = sim.compute(device=device, dtype=torch.float64, chunk_size=512)
assert master.data.shape == (1, 2, 2 * sim.npx + 1, 2 * sim.npx + 1)
assert torch.isfinite(master.data).all() and master.data.std() > 0
print(master)
master.show(projection="lambert")
MasterPattern(npx=100, 1 energy bin(s) [20..20 keV], device=cpu, backend=bloch_prism)
Rotate and zoom the real anyplotlib 3-D master sphere. texture_height and
n_grid control display resolution only; they do not refine the simulation.
master.show(projection="sphere", texture_height=128, n_grid=64)
Project for known orientations#
Orix Bunge Euler angles are given explicitly in degrees. Detector row zero
is at the top, unlike the Lambert display. rescale=False preserves
simulation intensities for comparison across orientations.
eulers = np.array([[0.0, 0.0, 0.0], [35.0, 45.0, 10.0], [90.0, 25.0, 60.0]])
orientations = Rotation.from_euler(eulers, degrees=True)
patterns = master.project(detector, orientations, rescale=False)
assert patterns.shape == (3, 96, 128) and torch.isfinite(patterns).all()
figure, axes = apl.subplots(1, 3, figsize=(900, 300))
for ax, pattern, angles in zip(np.ravel(axes), patterns, eulers, strict=True):
panel = ax.imshow(pattern.detach().cpu().numpy(), cmap="gray", origin="upper")
panel.set_title(f"Euler {tuple(angles.astype(int))} deg")
figure
Compose a phase overview for Jupyter or SpyDE#
The overview exposes metadata and a single native figure that an application
can mount through its existing anyplotlib bridge. In Jupyter, phase also
displays the metadata card; use phase.figure here for Gallery HTML capture.
phase = crystal.overview(master_pattern=master, name="Silicon", n_grid=64)
print(phase.metadata)
phase.figure
{'name': 'Silicon', 'space_group': 227, 'space_group_symbol': 'Fd-3m', 'a_nm': 0.5431, 'b_nm': 0.5431, 'c_nm': 0.5431, 'alpha_deg': 90.0, 'beta_deg': 90.0, 'gamma_deg': 90.0, 'volume_nm3': 0.16019147799100006, 'density_g_cm3': 2.3290247539484032, 'atom_count': 8, 'simulation_backend': 'bloch_prism'}
Save and reload without leaving generated files in the source tree#
Replace the temporary path with a persistent output path in your own work.
A standalone HTML export retains local camera interaction. A notebook setup
editor can additionally be opened with sim.show_setup(detector=detector);
read edits back using to_microscope() and to_detector().
with TemporaryDirectory(prefix="de_ebsd-example-") as output_dir:
master_path = Path(output_dir) / "silicon_master.h5"
master.save(str(master_path))
restored = de.MasterPattern.load(str(master_path), device=device)
torch.testing.assert_close(restored.data, master.data)
np.testing.assert_array_equal(restored.energies_kev, master.energies_kev)
print("HDF5 round trip preserved the master-pattern data and energy bins.")
HDF5 round trip preserved the master-pattern data and energy bins.
Total running time of the script: (0 minutes 8.587 seconds)