Coverage for dynasor/core/frame_processors.py: 34%
83 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 19:46 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-03 19:46 +0000
1"""Factory function that builds the callable used to populate
2``rho_qs_dict`` (and, if requested, ``jz_qs_dict``/``jper_qs_dict``) on
3trajectory frames, dispatched on the configured compute backend.
5Used by :mod:`dynasor.correlation_functions` to keep the public API
6functions free of backend-selection details.
7"""
8import numpy as np
10from dynasor.compute_config import check_backend_is_available
11from dynasor.logging_tools import logger
12from dynasor.core.reciprocal import calc_rho_q, calc_rho_j_q
15def get_frame_processor(q_points, backend, precision, gpu_batch_size,
16 calculate_currents=False, q_directions=None):
17 r"""Build the callable used to compute the reciprocal-space density
18 (and, if requested, current) for trajectory frames.
20 Parameters
21 ----------
22 q_points
23 Array of q-points in units of rad/Å with shape ``(N_qpoints, 3)`` in
24 Cartesian coordinates.
25 backend
26 Compute backend, one of ``'numba'``, ``'torch'``, or ``'cupy'``.
27 precision
28 Floating point precision used by the GPU backends (``'float64'`` or
29 ``'float32'``).
30 gpu_batch_size
31 Number of frames processed per GPU kernel call. Only used here for
32 logging; the batching itself is done by
33 :func:`_prefetch_batched_iter <dynasor.trajectory.prefetch._prefetch_batched_iter>`.
34 calculate_currents
35 If True, also compute the longitudinal and transverse current
36 components. Requires velocities to be available on the frames.
37 q_directions
38 Unit vectors along :attr:`q_points`. Required if
39 :attr:`calculate_currents` is True.
41 Returns
42 -------
43 A tuple ``(is_batched, processor)``. If :attr:`is_batched` is True,
44 :attr:`processor` takes a list of frames and is meant for use with
45 :func:`_prefetch_batched_iter <dynasor.trajectory.prefetch._prefetch_batched_iter>`.
46 Otherwise, :attr:`processor` takes a single frame and is meant for use as
47 a :class:`WindowIterator <dynasor.trajectory.WindowIterator>`
48 ``element_processor`` or with :func:`map`.
49 """
50 check_backend_is_available(backend)
52 if backend == 'torch': 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 from dynasor.core.reciprocal_torch import (
54 make_q_tensor, calc_rho_q_torch_batched, calc_rho_j_q_torch_batched
55 )
56 q_d = make_q_tensor(q_points, device='cuda', precision=precision)
57 logger.info(f'Using PyTorch backend (device={q_d.device}, precision={precision}, '
58 f'batch_size={gpu_batch_size})')
60 def fn_batch(frames):
61 for f in frames:
62 f.rho_qs_dict = {}
63 if calculate_currents:
64 f.jz_qs_dict = {}
65 f.jper_qs_dict = {}
66 for atom_type in frames[0].positions_by_type:
67 x_batch = np.stack([f.positions_by_type[atom_type] for f in frames])
68 if calculate_currents:
69 v_batch = np.stack([f.velocities_by_type[atom_type] for f in frames])
70 rho_np, j_np = calc_rho_j_q_torch_batched(x_batch, v_batch, q_d)
71 for b, frame in enumerate(frames):
72 frame.rho_qs_dict[atom_type] = rho_np[b]
73 j_qs = j_np[b]
74 jz_qs = np.sum(j_qs * q_directions, axis=1)
75 frame.jz_qs_dict[atom_type] = jz_qs
76 frame.jper_qs_dict[atom_type] = j_qs - jz_qs[:, None] * q_directions
77 else:
78 rho_np = calc_rho_q_torch_batched(x_batch, q_d)
79 for b, frame in enumerate(frames):
80 frame.rho_qs_dict[atom_type] = rho_np[b]
81 return frames
83 return True, fn_batch
85 elif backend == 'cupy': 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 from dynasor.core.reciprocal_cupy import (
87 make_q_array, calc_rho_q_cupy_batched, calc_rho_j_q_cupy_batched
88 )
89 q_d = make_q_array(q_points, precision=precision)
90 logger.info(f'Using CuPy backend (precision={precision}, batch_size={gpu_batch_size})')
92 def fn_batch(frames):
93 for f in frames:
94 f.rho_qs_dict = {}
95 if calculate_currents:
96 f.jz_qs_dict = {}
97 f.jper_qs_dict = {}
98 for atom_type in frames[0].positions_by_type:
99 x_batch = np.stack([f.positions_by_type[atom_type] for f in frames])
100 if calculate_currents:
101 v_batch = np.stack([f.velocities_by_type[atom_type] for f in frames])
102 rho_np, j_np = calc_rho_j_q_cupy_batched(x_batch, v_batch, q_d)
103 for b, frame in enumerate(frames):
104 frame.rho_qs_dict[atom_type] = rho_np[b]
105 j_qs = j_np[b]
106 jz_qs = np.sum(j_qs * q_directions, axis=1)
107 frame.jz_qs_dict[atom_type] = jz_qs
108 frame.jper_qs_dict[atom_type] = j_qs - jz_qs[:, None] * q_directions
109 else:
110 rho_np = calc_rho_q_cupy_batched(x_batch, q_d)
111 for b, frame in enumerate(frames):
112 frame.rho_qs_dict[atom_type] = rho_np[b]
113 return frames
115 return True, fn_batch
117 else: # numba: original per-frame path, unchanged
118 def f2_rho(frame):
119 rho_qs_dict = dict()
120 for atom_type in frame.positions_by_type.keys():
121 x = frame.positions_by_type[atom_type]
122 rho_qs_dict[atom_type] = calc_rho_q(x, q_points)
123 frame.rho_qs_dict = rho_qs_dict
124 return frame
126 def f2_rho_and_j(frame):
127 rho_qs_dict = dict()
128 jz_qs_dict = dict()
129 jper_qs_dict = dict()
131 for atom_type in frame.positions_by_type.keys():
132 x = frame.positions_by_type[atom_type]
133 v = frame.velocities_by_type[atom_type]
134 rho_qs, j_qs = calc_rho_j_q(x, v, q_points)
135 jz_qs = np.sum(j_qs * q_directions, axis=1)
136 jper_qs = j_qs - (jz_qs[:, None] * q_directions)
138 rho_qs_dict[atom_type] = rho_qs
139 jz_qs_dict[atom_type] = jz_qs
140 jper_qs_dict[atom_type] = jper_qs
142 frame.rho_qs_dict = rho_qs_dict
143 frame.jz_qs_dict = jz_qs_dict
144 frame.jper_qs_dict = jper_qs_dict
145 return frame
147 return (False, f2_rho_and_j) if calculate_currents else (False, f2_rho)