mirror of
https://github.com/pim-n/pg-rad
synced 2026-03-22 21:48:11 +01:00
158 lines
4.7 KiB
Python
158 lines
4.7 KiB
Python
from typing import Tuple, TYPE_CHECKING
|
|
|
|
import numpy as np
|
|
|
|
if TYPE_CHECKING:
|
|
from pg_rad.landscape.landscape import Landscape
|
|
from pg_rad.detector.detector import Detector
|
|
|
|
|
|
def phi(
|
|
r: float,
|
|
activity: float | int,
|
|
branching_ratio: float,
|
|
mu_mass_air: float,
|
|
air_density: float,
|
|
eff: float
|
|
) -> float:
|
|
"""Compute the contribution of a single point source to the
|
|
primary photon fluence rate phi at position (x,y,z).
|
|
|
|
Args:
|
|
r (float): [m] Distance to the point source.
|
|
activity (float | int): [Bq] Activity of the point source.
|
|
branching_ratio (float): Branching ratio for the photon energy E_gamma.
|
|
mu_mass_air (float): [cm^2/g] Mass attenuation coefficient for air.
|
|
air_density (float): [kg/m^3] Air density.
|
|
|
|
Returns:
|
|
phi (float): [s^-1 m^-2] Primary photon fluence rate at distance r from
|
|
a point source.
|
|
"""
|
|
|
|
# Linear photon attenuation coefficient in m^-1.
|
|
mu_air = 0.1 * mu_mass_air * air_density
|
|
|
|
phi_r = (
|
|
activity
|
|
* eff
|
|
* branching_ratio
|
|
* np.exp(-mu_air * r)
|
|
) / (4 * np.pi * r**2)
|
|
|
|
return phi_r
|
|
|
|
|
|
def calculate_count_rate_per_second(
|
|
landscape: "Landscape",
|
|
pos: np.ndarray,
|
|
detector: "Detector",
|
|
scaling=1E6
|
|
):
|
|
"""Compute count rate in s^-1 m^-2 at a position in the landscape.
|
|
|
|
Args:
|
|
landscape (Landscape): The landscape to compute.
|
|
pos (np.ndarray): (N, 3) array of positions.
|
|
detector (Detector):
|
|
Detector object, needed to compute correct efficiency.
|
|
|
|
Returns:
|
|
total_phi (np.ndarray): (N,) array of count rates per second.
|
|
"""
|
|
pos = np.atleast_2d(pos)
|
|
total_phi = np.zeros(pos.shape[0])
|
|
|
|
for source in landscape.point_sources:
|
|
# See Bukartas (2021) page 25 for incidence angle math
|
|
source_to_detector = pos - np.array(source.pos)
|
|
r = np.linalg.norm(source_to_detector, axis=1)
|
|
r = np.maximum(r, 1E-3) # enforce minimum distance of 1cm
|
|
|
|
if not detector.is_isotropic:
|
|
v = np.zeros_like(pos)
|
|
v[1:] = pos[1:] - pos[:-1]
|
|
v[0] = v[1] # handle first point
|
|
vx, vy = v[:, 0], v[:, 1]
|
|
|
|
r_vec = pos - np.array(source.pos)
|
|
rx, ry = r_vec[:, 0], r_vec[:, 1]
|
|
|
|
theta = np.arctan2(vy, vx) - np.arctan2(ry, rx)
|
|
|
|
# normalise to [-pi, pi] and convert to degrees
|
|
theta = (theta + np.pi) % (2 * np.pi) - np.pi
|
|
theta_deg = np.degrees(theta)
|
|
|
|
eff = detector.get_efficiency(source.isotope.E, theta_deg)
|
|
else:
|
|
eff = detector.get_efficiency(source.isotope.E)
|
|
|
|
phi_source = phi(
|
|
r=r,
|
|
activity=source.activity * scaling,
|
|
branching_ratio=source.isotope.b,
|
|
mu_mass_air=source.isotope.mu_mass_air,
|
|
air_density=landscape.air_density,
|
|
eff=eff
|
|
)
|
|
|
|
total_phi += phi_source
|
|
|
|
return total_phi
|
|
|
|
|
|
def calculate_counts_along_path(
|
|
landscape: "Landscape",
|
|
detector: "Detector",
|
|
velocity: float,
|
|
points_per_segment: int = 10,
|
|
) -> Tuple[np.ndarray, np.ndarray]:
|
|
"""Compute the counts recorded in each acquisition period in the landscape.
|
|
|
|
Args:
|
|
landscape (Landscape): _description_
|
|
detector (Detector): _description_
|
|
points_per_segment (int, optional): _description_. Defaults to 100.
|
|
|
|
Returns:
|
|
Tuple[np.ndarray, np.ndarray]: Array of acquisition points and
|
|
integrated count rates.
|
|
"""
|
|
|
|
path = landscape.path
|
|
num_points = len(path.x_list)
|
|
num_segments = len(path.segments)
|
|
|
|
segment_lengths = np.array([seg.length for seg in path.segments])
|
|
original_distances = np.zeros(num_points)
|
|
original_distances[1:] = np.cumsum(segment_lengths)
|
|
|
|
# arc lengths at which to evaluate the path
|
|
total_subpoints = num_segments * points_per_segment
|
|
s = np.linspace(0, original_distances[-1], total_subpoints)
|
|
|
|
# Interpolate x and y as functions of arc length
|
|
xnew = np.interp(s, original_distances, path.x_list)
|
|
ynew = np.interp(s, original_distances, path.y_list)
|
|
z = np.full_like(xnew, path.z)
|
|
full_positions = np.c_[xnew, ynew, z]
|
|
|
|
if path.opposite_direction:
|
|
full_positions = np.flip(full_positions, axis=0)
|
|
|
|
# [counts/s]
|
|
cps = calculate_count_rate_per_second(
|
|
landscape, full_positions, detector
|
|
)
|
|
|
|
# reshape so each segment is on a row
|
|
cps_per_seg = cps.reshape(num_segments, points_per_segment)
|
|
|
|
du = s[1] - s[0]
|
|
integrated_counts = np.trapezoid(cps_per_seg, dx=du, axis=1) / velocity
|
|
int_counts_result = np.zeros(num_points)
|
|
int_counts_result[1:] = integrated_counts
|
|
|
|
return original_distances, s, cps, int_counts_result
|