midas-nf-preprocess 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- midas_nf_preprocess/__init__.py +45 -0
- midas_nf_preprocess/cli.py +62 -0
- midas_nf_preprocess/device.py +69 -0
- midas_nf_preprocess/diffr_spots/__init__.py +83 -0
- midas_nf_preprocess/diffr_spots/binary_io.py +118 -0
- midas_nf_preprocess/diffr_spots/cli.py +70 -0
- midas_nf_preprocess/diffr_spots/geometry.py +193 -0
- midas_nf_preprocess/diffr_spots/hkls.py +118 -0
- midas_nf_preprocess/diffr_spots/orientations.py +176 -0
- midas_nf_preprocess/diffr_spots/params.py +115 -0
- midas_nf_preprocess/diffr_spots/pipeline.py +301 -0
- midas_nf_preprocess/hex_grid/__init__.py +30 -0
- midas_nf_preprocess/hex_grid/cli.py +58 -0
- midas_nf_preprocess/hex_grid/grid.py +237 -0
- midas_nf_preprocess/hex_grid/io.py +64 -0
- midas_nf_preprocess/hex_grid/params.py +62 -0
- midas_nf_preprocess/process_images/__init__.py +40 -0
- midas_nf_preprocess/process_images/cli.py +79 -0
- midas_nf_preprocess/process_images/io.py +92 -0
- midas_nf_preprocess/process_images/log_filter.py +130 -0
- midas_nf_preprocess/process_images/median.py +85 -0
- midas_nf_preprocess/process_images/params.py +133 -0
- midas_nf_preprocess/process_images/peaks.py +355 -0
- midas_nf_preprocess/process_images/pipeline.py +272 -0
- midas_nf_preprocess/process_images/spots_io.py +276 -0
- midas_nf_preprocess/seed_orientations/__init__.py +86 -0
- midas_nf_preprocess/seed_orientations/cli.py +150 -0
- midas_nf_preprocess/seed_orientations/crystal.py +136 -0
- midas_nf_preprocess/seed_orientations/dispatch.py +99 -0
- midas_nf_preprocess/seed_orientations/from_cache.py +141 -0
- midas_nf_preprocess/seed_orientations/from_grains.py +100 -0
- midas_nf_preprocess/seed_orientations/from_scratch.py +207 -0
- midas_nf_preprocess/seed_orientations/io.py +77 -0
- midas_nf_preprocess/tomo_filter/__init__.py +36 -0
- midas_nf_preprocess/tomo_filter/cli.py +66 -0
- midas_nf_preprocess/tomo_filter/filter.py +180 -0
- midas_nf_preprocess-0.1.0.dist-info/METADATA +96 -0
- midas_nf_preprocess-0.1.0.dist-info/RECORD +41 -0
- midas_nf_preprocess-0.1.0.dist-info/WHEEL +5 -0
- midas_nf_preprocess-0.1.0.dist-info/entry_points.txt +2 -0
- midas_nf_preprocess-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""midas-nf-preprocess: Differentiable PyTorch port of NF-HEDM preprocessing.
|
|
2
|
+
|
|
3
|
+
Bundles the preprocessing stages that sit between raw experimental inputs
|
|
4
|
+
and the orientation-fitting step (``FitOrientationOMP``):
|
|
5
|
+
|
|
6
|
+
- ``hex_grid`` : voxel grid generation (port of MakeHexGrid)
|
|
7
|
+
- ``tomo_filter`` : grid masking from a tomography image
|
|
8
|
+
(port of filterGridfromTomo)
|
|
9
|
+
- ``seed_orientations``: NF seed-orientation generation (cache, from-scratch,
|
|
10
|
+
or from an FF Grains.csv)
|
|
11
|
+
- ``diffr_spots`` : per-orientation diffraction-spot prediction
|
|
12
|
+
(differentiable port of MakeDiffrSpots; wraps
|
|
13
|
+
``midas_diffract.HEDMForwardModel.calc_bragg_geometry``)
|
|
14
|
+
- ``process_images`` : raw TIFF -> SpotsInfo.bin
|
|
15
|
+
(differentiable port of ProcessImagesCombined)
|
|
16
|
+
|
|
17
|
+
The shared ``device`` module provides device/dtype resolution
|
|
18
|
+
(env vars ``MIDAS_NF_PREPROCESS_DEVICE`` / ``MIDAS_NF_PREPROCESS_DTYPE``)
|
|
19
|
+
matching the convention used by ``midas-transforms``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
from . import (
|
|
25
|
+
device,
|
|
26
|
+
diffr_spots,
|
|
27
|
+
hex_grid,
|
|
28
|
+
process_images,
|
|
29
|
+
seed_orientations,
|
|
30
|
+
tomo_filter,
|
|
31
|
+
)
|
|
32
|
+
from .device import resolve_device, resolve_dtype, apply_cpu_threads
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"__version__",
|
|
36
|
+
"device",
|
|
37
|
+
"diffr_spots",
|
|
38
|
+
"hex_grid",
|
|
39
|
+
"process_images",
|
|
40
|
+
"seed_orientations",
|
|
41
|
+
"tomo_filter",
|
|
42
|
+
"resolve_device",
|
|
43
|
+
"resolve_dtype",
|
|
44
|
+
"apply_cpu_threads",
|
|
45
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Umbrella CLI: ``midas-nf-preprocess <subcommand> ...``.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
- ``hex-grid`` : generate the voxel grid (port of MakeHexGrid)
|
|
6
|
+
- ``tomo-filter`` : mask the grid by a tomography image / bbox
|
|
7
|
+
- ``diffr-spots`` : forward-simulate diffraction spots (port of MakeDiffrSpots)
|
|
8
|
+
- ``process-images`` : raw TIFF -> SpotsInfo.bin (port of ProcessImagesCombined)
|
|
9
|
+
|
|
10
|
+
Each subcommand mirrors its standalone ``python -m`` invocation:
|
|
11
|
+
|
|
12
|
+
python -m midas_nf_preprocess.hex_grid <args> ==
|
|
13
|
+
midas-nf-preprocess hex-grid <args>
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
from . import __version__
|
|
22
|
+
from .diffr_spots import cli as diffr_spots_cli
|
|
23
|
+
from .hex_grid import cli as hex_grid_cli
|
|
24
|
+
from .process_images import cli as process_images_cli
|
|
25
|
+
from .seed_orientations import cli as seed_orientations_cli
|
|
26
|
+
from .tomo_filter import cli as tomo_filter_cli
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_SUBCOMMANDS = {
|
|
30
|
+
"hex-grid": hex_grid_cli,
|
|
31
|
+
"tomo-filter": tomo_filter_cli,
|
|
32
|
+
"diffr-spots": diffr_spots_cli,
|
|
33
|
+
"process-images": process_images_cli,
|
|
34
|
+
"seed-orientations": seed_orientations_cli,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
prog="midas-nf-preprocess",
|
|
41
|
+
description="Differentiable PyTorch port of NF-HEDM preprocessing.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--version", action="version", version=f"midas-nf-preprocess {__version__}"
|
|
45
|
+
)
|
|
46
|
+
subparsers = parser.add_subparsers(
|
|
47
|
+
dest="subcommand", required=True, metavar="<subcommand>"
|
|
48
|
+
)
|
|
49
|
+
for name, mod in _SUBCOMMANDS.items():
|
|
50
|
+
sp = subparsers.add_parser(name, help=mod.__doc__.splitlines()[0] if mod.__doc__ else None)
|
|
51
|
+
mod.add_arguments(sp)
|
|
52
|
+
sp.set_defaults(_run=mod.run)
|
|
53
|
+
return parser
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main(argv: list[str] | None = None) -> int:
|
|
57
|
+
args = _build_parser().parse_args(argv)
|
|
58
|
+
return args._run(args)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
sys.exit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Device / dtype resolution.
|
|
2
|
+
|
|
3
|
+
Same contract as ``midas-transforms``:
|
|
4
|
+
- device default: cuda > mps > cpu (auto-detect)
|
|
5
|
+
- dtype default: float64 on cpu (matches the C binaries),
|
|
6
|
+
float32 on cuda/mps (matches the GPU codepath idioms).
|
|
7
|
+
- Both float32 and float64 are supported on every device.
|
|
8
|
+
- Env vars MIDAS_NF_PREPROCESS_DEVICE / MIDAS_NF_PREPROCESS_DTYPE
|
|
9
|
+
override defaults.
|
|
10
|
+
- Library ``device=``/``dtype=`` kwargs override env vars.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Optional, Union
|
|
17
|
+
|
|
18
|
+
import torch
|
|
19
|
+
|
|
20
|
+
_DTYPE_ALIAS = {
|
|
21
|
+
"float32": torch.float32,
|
|
22
|
+
"fp32": torch.float32,
|
|
23
|
+
"float64": torch.float64,
|
|
24
|
+
"fp64": torch.float64,
|
|
25
|
+
"double": torch.float64,
|
|
26
|
+
"single": torch.float32,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ENV_DEVICE = "MIDAS_NF_PREPROCESS_DEVICE"
|
|
30
|
+
ENV_DTYPE = "MIDAS_NF_PREPROCESS_DTYPE"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_device(user: Optional[Union[str, torch.device]]) -> torch.device:
|
|
34
|
+
"""Pick a torch.device. Precedence: arg > env var > auto-detect."""
|
|
35
|
+
if isinstance(user, torch.device):
|
|
36
|
+
return user
|
|
37
|
+
if user is None:
|
|
38
|
+
user = os.environ.get(ENV_DEVICE)
|
|
39
|
+
if user is None:
|
|
40
|
+
if torch.cuda.is_available():
|
|
41
|
+
return torch.device("cuda")
|
|
42
|
+
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
|
43
|
+
return torch.device("mps")
|
|
44
|
+
return torch.device("cpu")
|
|
45
|
+
return torch.device(user)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_dtype(
|
|
49
|
+
device: torch.device, user: Optional[Union[str, torch.dtype]]
|
|
50
|
+
) -> torch.dtype:
|
|
51
|
+
"""Pick a torch.dtype. Precedence: arg > env var > per-device default."""
|
|
52
|
+
if isinstance(user, torch.dtype):
|
|
53
|
+
return user
|
|
54
|
+
if user is None:
|
|
55
|
+
user = os.environ.get(ENV_DTYPE)
|
|
56
|
+
if user is not None:
|
|
57
|
+
try:
|
|
58
|
+
return _DTYPE_ALIAS[user.lower()]
|
|
59
|
+
except KeyError as e:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Unsupported dtype '{user}'. Use one of {sorted(_DTYPE_ALIAS)}."
|
|
62
|
+
) from e
|
|
63
|
+
return torch.float64 if device.type == "cpu" else torch.float32
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def apply_cpu_threads(num_procs: int, device: torch.device) -> None:
|
|
67
|
+
"""Honor a legacy ``numProcs`` argv on CPU only (no-op on GPU)."""
|
|
68
|
+
if device.type == "cpu" and num_procs > 0:
|
|
69
|
+
torch.set_num_threads(num_procs)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""diffr_spots: per-orientation diffraction-spot prediction (port of MakeDiffrSpots).
|
|
2
|
+
|
|
3
|
+
Differentiable PyTorch port of ``NF_HEDM/src/MakeDiffrSpots.c``. For each seed
|
|
4
|
+
orientation and each HKL, solve the Bragg condition:
|
|
5
|
+
|
|
6
|
+
- ``omega(s)`` (1 or 2 solutions): rotation angles where the reciprocal-space
|
|
7
|
+
G-vector intersects the Ewald sphere
|
|
8
|
+
- ``eta``: azimuthal angle on the detector
|
|
9
|
+
- ``(yl, zl)``: lab-frame detector position at the primary distance
|
|
10
|
+
|
|
11
|
+
then filter by ``OmegaRange``, ``BoxSize``, and ``ExcludePoleAngle``. Output is
|
|
12
|
+
either a structured tensor bundle or the same three binary files the C
|
|
13
|
+
executable produces:
|
|
14
|
+
|
|
15
|
+
- ``DiffractionSpots.bin`` : ``[TotalSpots, 3]`` float64 ``(yl, zl, omega)``
|
|
16
|
+
- ``Key.bin`` : ``[N_orient, 2]`` int32 ``(count_i, offset_i)``
|
|
17
|
+
- ``OrientMat.bin`` : ``[N_orient, 3, 3]`` float64 row-major
|
|
18
|
+
|
|
19
|
+
Differentiability:
|
|
20
|
+
|
|
21
|
+
- ``omega``, ``eta``, ``yl``, ``zl`` are differentiable in the input
|
|
22
|
+
quaternions / Euler angles / orientation matrices and in the lattice
|
|
23
|
+
parameters that produced ``hkls``.
|
|
24
|
+
- The hard ``OmegaRange`` / ``BoxSize`` / ``ExcludePoleAngle`` filters return
|
|
25
|
+
a boolean ``valid`` mask (no gradient), matching the convention used by
|
|
26
|
+
``midas_diffract.SpotDescriptors``.
|
|
27
|
+
|
|
28
|
+
The geometry primitives (``calc_omega``, ``calc_eta``, ``calc_spot_position``)
|
|
29
|
+
match the C implementations in ``MakeDiffrSpots.c`` L99-L204 line-by-line and
|
|
30
|
+
extend them to batched, vectorized PyTorch operations.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .params import DiffrSpotsParams
|
|
34
|
+
from .orientations import (
|
|
35
|
+
quat_to_orient_matrix,
|
|
36
|
+
orient_matrix_to_quat,
|
|
37
|
+
euler_to_orient_matrix,
|
|
38
|
+
)
|
|
39
|
+
from .geometry import (
|
|
40
|
+
bragg_omega_eta,
|
|
41
|
+
calc_eta_deg,
|
|
42
|
+
calc_spot_position,
|
|
43
|
+
rotate_around_z,
|
|
44
|
+
)
|
|
45
|
+
from .pipeline import (
|
|
46
|
+
DiffrSpotsPipeline,
|
|
47
|
+
DiffrSpotsResult,
|
|
48
|
+
predict_spots,
|
|
49
|
+
)
|
|
50
|
+
from .binary_io import (
|
|
51
|
+
write_diffr_spots_bin,
|
|
52
|
+
write_key_bin,
|
|
53
|
+
write_orient_mat_bin,
|
|
54
|
+
read_diffr_spots_bin,
|
|
55
|
+
read_key_bin,
|
|
56
|
+
read_orient_mat_bin,
|
|
57
|
+
write_all,
|
|
58
|
+
)
|
|
59
|
+
from .hkls import HKLEntry, read_hkls_csv, read_seed_orientations
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"DiffrSpotsParams",
|
|
63
|
+
"quat_to_orient_matrix",
|
|
64
|
+
"orient_matrix_to_quat",
|
|
65
|
+
"euler_to_orient_matrix",
|
|
66
|
+
"bragg_omega_eta",
|
|
67
|
+
"calc_eta_deg",
|
|
68
|
+
"calc_spot_position",
|
|
69
|
+
"rotate_around_z",
|
|
70
|
+
"DiffrSpotsPipeline",
|
|
71
|
+
"DiffrSpotsResult",
|
|
72
|
+
"predict_spots",
|
|
73
|
+
"write_diffr_spots_bin",
|
|
74
|
+
"write_key_bin",
|
|
75
|
+
"write_orient_mat_bin",
|
|
76
|
+
"read_diffr_spots_bin",
|
|
77
|
+
"read_key_bin",
|
|
78
|
+
"read_orient_mat_bin",
|
|
79
|
+
"write_all",
|
|
80
|
+
"HKLEntry",
|
|
81
|
+
"read_hkls_csv",
|
|
82
|
+
"read_seed_orientations",
|
|
83
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Binary I/O for the three MakeDiffrSpots output files.
|
|
2
|
+
|
|
3
|
+
File formats (from MakeDiffrSpots.c L595-L626):
|
|
4
|
+
|
|
5
|
+
- ``Key.bin`` : ``int32 [N_orient * 2]`` little-endian, with
|
|
6
|
+
entries ``(count_i, offset_i)`` for each orientation.
|
|
7
|
+
- ``DiffractionSpots.bin`` : ``float64 [TotalSpots * 3]`` little-endian, with
|
|
8
|
+
row layout ``(yl, zl, omega)``.
|
|
9
|
+
- ``OrientMat.bin`` : ``float64 [N_orient * 9]`` little-endian, with
|
|
10
|
+
row-major flattened 3x3 matrices.
|
|
11
|
+
|
|
12
|
+
Total spots = sum of per-orientation counts; ``offset_i`` is the cumulative
|
|
13
|
+
prefix sum (excluding ``count_i`` itself) so that orientation i's spots span
|
|
14
|
+
``DiffractionSpots[offset_i : offset_i + count_i]``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Union
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# -----------------------------------------------------------------------------
|
|
27
|
+
# Writers
|
|
28
|
+
# -----------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def write_key_bin(
|
|
32
|
+
counts: torch.Tensor, path: Union[str, Path]
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Write ``Key.bin`` from a ``(N,)`` int tensor of per-orientation spot counts."""
|
|
35
|
+
if counts.ndim != 1:
|
|
36
|
+
raise ValueError(f"Expected 1D counts tensor, got shape {tuple(counts.shape)}")
|
|
37
|
+
counts_np = counts.detach().cpu().numpy().astype(np.int32)
|
|
38
|
+
offsets = np.zeros_like(counts_np)
|
|
39
|
+
if counts_np.size > 0:
|
|
40
|
+
offsets[1:] = np.cumsum(counts_np[:-1])
|
|
41
|
+
key = np.stack([counts_np, offsets], axis=1) # (N, 2)
|
|
42
|
+
path = Path(path)
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
key.tofile(path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_diffr_spots_bin(
|
|
48
|
+
spots: torch.Tensor, path: Union[str, Path]
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Write ``DiffractionSpots.bin`` from a ``(TotalSpots, 3)`` float tensor."""
|
|
51
|
+
if spots.ndim != 2 or spots.shape[1] != 3:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Expected (TotalSpots, 3) tensor, got shape {tuple(spots.shape)}"
|
|
54
|
+
)
|
|
55
|
+
arr = spots.detach().cpu().numpy().astype(np.float64)
|
|
56
|
+
path = Path(path)
|
|
57
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
arr.tofile(path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_orient_mat_bin(
|
|
62
|
+
orient_mats: torch.Tensor, path: Union[str, Path]
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Write ``OrientMat.bin`` from a ``(N, 3, 3)`` float tensor."""
|
|
65
|
+
if orient_mats.ndim != 3 or orient_mats.shape[1:] != (3, 3):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Expected (N, 3, 3) tensor, got shape {tuple(orient_mats.shape)}"
|
|
68
|
+
)
|
|
69
|
+
arr = orient_mats.detach().cpu().numpy().astype(np.float64)
|
|
70
|
+
path = Path(path)
|
|
71
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
arr.tofile(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write_all(
|
|
76
|
+
output_dir: Union[str, Path],
|
|
77
|
+
counts: torch.Tensor,
|
|
78
|
+
spots: torch.Tensor,
|
|
79
|
+
orient_mats: torch.Tensor,
|
|
80
|
+
) -> dict[str, Path]:
|
|
81
|
+
"""Convenience wrapper that writes all three files into ``output_dir``."""
|
|
82
|
+
output_dir = Path(output_dir)
|
|
83
|
+
paths = {
|
|
84
|
+
"Key.bin": output_dir / "Key.bin",
|
|
85
|
+
"DiffractionSpots.bin": output_dir / "DiffractionSpots.bin",
|
|
86
|
+
"OrientMat.bin": output_dir / "OrientMat.bin",
|
|
87
|
+
}
|
|
88
|
+
write_key_bin(counts, paths["Key.bin"])
|
|
89
|
+
write_diffr_spots_bin(spots, paths["DiffractionSpots.bin"])
|
|
90
|
+
write_orient_mat_bin(orient_mats, paths["OrientMat.bin"])
|
|
91
|
+
return paths
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# -----------------------------------------------------------------------------
|
|
95
|
+
# Readers (for parity tests / round-trip)
|
|
96
|
+
# -----------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read_key_bin(path: Union[str, Path], n_orient: int) -> torch.Tensor:
|
|
100
|
+
"""Read ``Key.bin`` and return a ``(N, 2)`` int64 tensor ``(count_i, offset_i)``."""
|
|
101
|
+
arr = np.fromfile(path, dtype=np.int32, count=n_orient * 2).reshape(n_orient, 2)
|
|
102
|
+
return torch.from_numpy(arr.astype(np.int64))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def read_diffr_spots_bin(
|
|
106
|
+
path: Union[str, Path], n_spots: int
|
|
107
|
+
) -> torch.Tensor:
|
|
108
|
+
"""Read ``DiffractionSpots.bin`` and return a ``(N_spots, 3)`` float64 tensor."""
|
|
109
|
+
arr = np.fromfile(path, dtype=np.float64, count=n_spots * 3).reshape(n_spots, 3)
|
|
110
|
+
return torch.from_numpy(arr)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def read_orient_mat_bin(
|
|
114
|
+
path: Union[str, Path], n_orient: int
|
|
115
|
+
) -> torch.Tensor:
|
|
116
|
+
"""Read ``OrientMat.bin`` and return a ``(N, 3, 3)`` float64 tensor."""
|
|
117
|
+
arr = np.fromfile(path, dtype=np.float64, count=n_orient * 9).reshape(n_orient, 3, 3)
|
|
118
|
+
return torch.from_numpy(arr)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""``midas-nf-preprocess diffr-spots`` subcommand."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .params import DiffrSpotsParams
|
|
10
|
+
from .pipeline import DiffrSpotsPipeline
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"parameter_file", help="MIDAS parameter file (Lsd, SeedOrientations, etc.)"
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--device", default=None, help="cpu | cuda | mps (default: auto-detect)."
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--dtype", default=None, help="fp32 | fp64 (default: fp64 on cpu)."
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--hkls-csv",
|
|
25
|
+
default=None,
|
|
26
|
+
help="Override hkls.csv path (default: <DataDirectory>/hkls.csv).",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--seeds",
|
|
30
|
+
default=None,
|
|
31
|
+
help="Override SeedOrientations path (default: from parameter file).",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--output-dir",
|
|
35
|
+
default=None,
|
|
36
|
+
help="Override output directory for Key.bin / DiffractionSpots.bin / OrientMat.bin.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(args: argparse.Namespace) -> int:
|
|
41
|
+
params = DiffrSpotsParams.from_paramfile(args.parameter_file)
|
|
42
|
+
pipe = DiffrSpotsPipeline(
|
|
43
|
+
params,
|
|
44
|
+
device=args.device,
|
|
45
|
+
dtype=args.dtype,
|
|
46
|
+
hkls_csv=args.hkls_csv,
|
|
47
|
+
seed_orientations_csv=args.seeds,
|
|
48
|
+
)
|
|
49
|
+
result, paths = pipe.run(output_dir=args.output_dir)
|
|
50
|
+
total = int(result.counts.sum().item())
|
|
51
|
+
print(
|
|
52
|
+
f"Wrote {len(paths)} files to {Path(paths['Key.bin']).parent} "
|
|
53
|
+
f"({pipe.n_orientations} orientations, {total} total spots)"
|
|
54
|
+
)
|
|
55
|
+
for name, p in paths.items():
|
|
56
|
+
print(f" {name}: {p}")
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(argv: list[str] | None = None) -> int:
|
|
61
|
+
parser = argparse.ArgumentParser(
|
|
62
|
+
prog="midas-nf-preprocess diffr-spots",
|
|
63
|
+
description="Differentiable diffraction-spot prediction (port of MakeDiffrSpots).",
|
|
64
|
+
)
|
|
65
|
+
add_arguments(parser)
|
|
66
|
+
return run(parser.parse_args(argv))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Bragg geometry for the diffr_spots pipeline.
|
|
2
|
+
|
|
3
|
+
We **delegate** the omega/eta calculation to
|
|
4
|
+
``midas_diffract.HEDMForwardModel.calc_bragg_geometry`` (forward.py:680-857)
|
|
5
|
+
to avoid maintaining two copies of the same quadratic-Bragg solver. That
|
|
6
|
+
function ports ``CalcDiffractionSpots.c::CalcOmega`` (NF) and
|
|
7
|
+
``ForwardSimulationCompressed.c`` (FF), and adds a wedge correction.
|
|
8
|
+
|
|
9
|
+
Only the helpers that are MakeDiffrSpots-specific live here:
|
|
10
|
+
|
|
11
|
+
- ``calc_spot_position`` : ``(eta, ring_radius) -> (yl, zl)`` lab-frame
|
|
12
|
+
projection at a given detector distance (matches MakeDiffrSpots.c L115-L120).
|
|
13
|
+
- ``rotate_around_z`` : convenience helper used by tests.
|
|
14
|
+
|
|
15
|
+
Returned omega/eta from calc_bragg_geometry are in radians; the rest of this
|
|
16
|
+
package converts to degrees at the boundary so the binary output matches the
|
|
17
|
+
C convention.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
import torch
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_DEG2RAD = math.pi / 180.0
|
|
29
|
+
_RAD2DEG = 180.0 / math.pi
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rotate_around_z(v: torch.Tensor, alpha_deg: torch.Tensor) -> torch.Tensor:
|
|
33
|
+
"""Apply a rotation about the +Z axis (active, right-handed).
|
|
34
|
+
|
|
35
|
+
Direct port of ``RotateAroundZ`` (MakeDiffrSpots.c L99-L107). Used in tests
|
|
36
|
+
to validate the geometry; the production path goes through midas_diffract.
|
|
37
|
+
"""
|
|
38
|
+
if v.shape[-1] != 3:
|
|
39
|
+
raise ValueError(f"Expected last dim = 3, got shape {tuple(v.shape)}")
|
|
40
|
+
a = alpha_deg * _DEG2RAD
|
|
41
|
+
cosa = torch.cos(a)
|
|
42
|
+
sina = torch.sin(a)
|
|
43
|
+
x = v[..., 0]
|
|
44
|
+
y = v[..., 1]
|
|
45
|
+
z = v[..., 2]
|
|
46
|
+
return torch.stack(
|
|
47
|
+
[cosa * x - sina * y, sina * x + cosa * y, z], dim=-1
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def calc_eta_deg(y: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
52
|
+
"""Convert lab-frame (y, z) into eta in degrees.
|
|
53
|
+
|
|
54
|
+
Port of ``MakeDiffrSpots.c::CalcEtaAngle`` (L109-L113), used as a
|
|
55
|
+
cross-check against the radians version returned by midas_diffract.
|
|
56
|
+
"""
|
|
57
|
+
r = torch.sqrt(y * y + z * z)
|
|
58
|
+
eps = torch.finfo(r.dtype).eps
|
|
59
|
+
cos_eta = torch.where(r > 0, z / torch.clamp(r, min=eps), torch.zeros_like(z))
|
|
60
|
+
cos_eta = torch.clamp(cos_eta, -1.0, 1.0)
|
|
61
|
+
alpha = torch.acos(cos_eta) * _RAD2DEG
|
|
62
|
+
return torch.where(y > 0, -alpha, alpha)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def calc_spot_position(
|
|
66
|
+
ring_radius: torch.Tensor, eta_deg: torch.Tensor
|
|
67
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
68
|
+
"""Lab-frame ``(yl, zl)`` from ring radius and eta (degrees).
|
|
69
|
+
|
|
70
|
+
Direct port of ``MakeDiffrSpots.c::CalcSpotPosition`` (L115-L120):
|
|
71
|
+
|
|
72
|
+
yl = -RingRadius * sin(eta)
|
|
73
|
+
zl = RingRadius * cos(eta)
|
|
74
|
+
|
|
75
|
+
This is the bit MakeDiffrSpots needs but ``midas_diffract`` does not
|
|
76
|
+
expose -- the latter goes straight from eta to detector pixel coordinates,
|
|
77
|
+
skipping the lab-frame intermediate.
|
|
78
|
+
"""
|
|
79
|
+
eta_rad = eta_deg * _DEG2RAD
|
|
80
|
+
yl = -ring_radius * torch.sin(eta_rad)
|
|
81
|
+
zl = ring_radius * torch.cos(eta_rad)
|
|
82
|
+
return yl, zl
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# -----------------------------------------------------------------------------
|
|
86
|
+
# Lazy adapter to midas_diffract.HEDMForwardModel for the Bragg geometry call.
|
|
87
|
+
# -----------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _make_minimal_geometry(
|
|
91
|
+
distance_um: float, *, n_pixels: int = 2048, wedge_deg: float = 0.0
|
|
92
|
+
):
|
|
93
|
+
"""Build an HEDMGeometry whose pixel/beam fields are placeholders.
|
|
94
|
+
|
|
95
|
+
``calc_bragg_geometry`` only reads ``self.wedge`` and ``self.epsilon``
|
|
96
|
+
(the Bragg quadratic itself doesn't touch pixels or beam centers), so any
|
|
97
|
+
consistent values for the unused fields work.
|
|
98
|
+
"""
|
|
99
|
+
from midas_diffract.forward import HEDMGeometry
|
|
100
|
+
|
|
101
|
+
return HEDMGeometry(
|
|
102
|
+
Lsd=float(distance_um),
|
|
103
|
+
y_BC=0.0,
|
|
104
|
+
z_BC=0.0,
|
|
105
|
+
px=1.0,
|
|
106
|
+
omega_start=0.0,
|
|
107
|
+
omega_step=0.1,
|
|
108
|
+
n_frames=1,
|
|
109
|
+
n_pixels_y=n_pixels,
|
|
110
|
+
n_pixels_z=n_pixels,
|
|
111
|
+
min_eta=0.0,
|
|
112
|
+
wavelength=0.0,
|
|
113
|
+
wedge=float(wedge_deg),
|
|
114
|
+
flip_y=False,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def bragg_omega_eta(
|
|
119
|
+
orient_mats: torch.Tensor,
|
|
120
|
+
hkls: torch.Tensor,
|
|
121
|
+
thetas_deg: torch.Tensor,
|
|
122
|
+
*,
|
|
123
|
+
distance_um: float,
|
|
124
|
+
wedge_deg: float = 0.0,
|
|
125
|
+
device: Optional[torch.device] = None,
|
|
126
|
+
):
|
|
127
|
+
"""Compute (omega_deg, eta_deg, valid) by delegating to midas_diffract.
|
|
128
|
+
|
|
129
|
+
Wraps ``HEDMForwardModel.calc_bragg_geometry``: the work happens there.
|
|
130
|
+
Output is converted from radians (midas_diffract) to degrees (the
|
|
131
|
+
convention used by MakeDiffrSpots and the rest of this package).
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
orient_mats : Tensor of shape ``(N, 3, 3)``.
|
|
136
|
+
hkls : Tensor of shape ``(M, 3)``.
|
|
137
|
+
thetas_deg : Tensor of shape ``(M,)`` -- Bragg angles in degrees.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
omega_deg : Tensor of shape ``(N, 2, M)`` -- two solutions per HKL.
|
|
142
|
+
eta_deg : Tensor of shape ``(N, 2, M)``.
|
|
143
|
+
valid : Bool Tensor of shape ``(N, 2, M)``.
|
|
144
|
+
"""
|
|
145
|
+
from midas_diffract.forward import HEDMForwardModel
|
|
146
|
+
|
|
147
|
+
if orient_mats.ndim != 3 or orient_mats.shape[-2:] != (3, 3):
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Expected (N, 3, 3), got shape {tuple(orient_mats.shape)}"
|
|
150
|
+
)
|
|
151
|
+
if hkls.shape[-1] != 3:
|
|
152
|
+
raise ValueError(f"Expected hkls last dim = 3, got {tuple(hkls.shape)}")
|
|
153
|
+
|
|
154
|
+
device = device or orient_mats.device
|
|
155
|
+
|
|
156
|
+
# Construct a one-shot model. The constructor stores hkls/thetas as
|
|
157
|
+
# buffers, but calc_bragg_geometry will use the explicit args we pass.
|
|
158
|
+
geom = _make_minimal_geometry(distance_um, wedge_deg=wedge_deg)
|
|
159
|
+
model = HEDMForwardModel(
|
|
160
|
+
hkls=hkls.to(device),
|
|
161
|
+
thetas=thetas_deg.to(device) * _DEG2RAD, # midas_diffract expects radians
|
|
162
|
+
geometry=geom,
|
|
163
|
+
device=device,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
omega, eta, _two_theta, valid = model.calc_bragg_geometry(
|
|
167
|
+
orientation_matrices=orient_mats,
|
|
168
|
+
hkls_cart=hkls.to(device),
|
|
169
|
+
thetas=thetas_deg.to(device) * _DEG2RAD,
|
|
170
|
+
)
|
|
171
|
+
# midas_diffract layout: (..., 2N, M) -- the leading 2N is a *concatenation*
|
|
172
|
+
# of the two omega solutions across the N axis. For our (N, 1, 3, 3) input
|
|
173
|
+
# we get omega shape (1, 2*N, M); with N=N orientations this becomes
|
|
174
|
+
# (2*N, M) which we reshape into (N, 2, M).
|
|
175
|
+
# In our use we always pass orient_mats of shape (N, 3, 3) which makes
|
|
176
|
+
# midas_diffract interpret it as N "voxels" in a 1D batch. The concat
|
|
177
|
+
# along the K=2N axis stacks "omega_p (N entries)" then "omega_n (N
|
|
178
|
+
# entries)", so reshape with stride pattern needs care.
|
|
179
|
+
# Output of calc_bragg_geometry for our input shape:
|
|
180
|
+
# omega: (2*N, M) [no leading batch dim]
|
|
181
|
+
# The first N rows are omega_p (one per orientation); the next N rows
|
|
182
|
+
# are omega_n (same orientations, the other quadratic root). Reshape to
|
|
183
|
+
# (2, N, M) then permute to (N, 2, M).
|
|
184
|
+
n = orient_mats.shape[0]
|
|
185
|
+
omega_2nm = omega.reshape(2, n, -1).permute(1, 0, 2).contiguous()
|
|
186
|
+
eta_2nm = eta.reshape(2, n, -1).permute(1, 0, 2).contiguous()
|
|
187
|
+
valid_2nm = valid.reshape(2, n, -1).permute(1, 0, 2).bool().contiguous()
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
omega_2nm * _RAD2DEG,
|
|
191
|
+
eta_2nm * _RAD2DEG,
|
|
192
|
+
valid_2nm,
|
|
193
|
+
)
|