Files
pg-rad/src/pg_rad/physics/fluence.py

193 lines
5.6 KiB
Python

from typing import Tuple, TYPE_CHECKING
import numpy as np
from pg_rad.background.background import generate_background
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,
t_acq: float,
points_per_segment: int = 10,
bkg_cps_input: int | None = None,
seed: int | None = None
) -> 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.
bkg_cps_input (int | None, optional): Optional background CPS.
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
)
if bkg_cps_input is None:
bkg = generate_background(
cps, detector, landscape.point_sources[0].isotope.E,
seed=seed
)
elif bkg_cps_input == 0:
bkg = bkg_cps_input
else:
bkg = generate_background(
cps, detector, landscape.point_sources[0].isotope.E,
lam_inp=bkg_cps_input,
seed=seed
)
cps_with_bg = cps + bkg
# Integrate along time dimension. see e.g. Bukartas (2021 doccomp.)
t = s / abs(velocity)
# acquisition bins
t_bins = np.arange(0, t[-1] + t_acq, t_acq)
int_counts = np.zeros(len(t_bins) - 1)
acq_points = np.zeros(len(t_bins) - 1)
for i in range(len(t_bins) - 1):
mask = (t >= t_bins[i]) & (t < t_bins[i + 1])
int_counts[i] = np.trapezoid(cps_with_bg[mask], t[mask])
acq_points[i] = np.mean(s[mask])
return (
acq_points[:-1],
s[:-1],
cps_with_bg[:-1],
int_counts[:-1],
np.mean(bkg)
)