From 8e429fe636ef2f8071033484520006c4914a599a Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Mon, 2 Mar 2026 12:58:12 +0100 Subject: [PATCH 01/20] fix interpolator to properly interpolate in 2D space --- src/pg_rad/physics/fluence.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/pg_rad/physics/fluence.py b/src/pg_rad/physics/fluence.py index e6eca91..8a448da 100644 --- a/src/pg_rad/physics/fluence.py +++ b/src/pg_rad/physics/fluence.py @@ -77,23 +77,27 @@ def calculate_fluence_along_path( points_per_segment: int = 10 ) -> Tuple[np.ndarray, np.ndarray]: path = landscape.path - num_segments = len(path.segments) + num_points = len(path.x_list) - xnew = np.linspace( - path.x_list[0], - path.x_list[-1], - num=num_segments*points_per_segment) + dx = np.diff(path.x_list) + dy = np.diff(path.y_list) + segment_lengths = np.sqrt(dx**2 + dy**2) - ynew = np.interp(xnew, path.x_list, path.y_list) + original_distances = np.zeros(num_points) + original_distances[1:] = np.cumsum(segment_lengths) + # arc lengths at which to evaluate the path + s = np.linspace( + 0, + original_distances[-1], + num=num_points * points_per_segment) + + # 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(xnew.shape, path.z) full_positions = np.c_[xnew, ynew, z] + phi_result = calculate_fluence_at(landscape, full_positions) - dist_travelled = np.linspace( - full_positions[0, 0], - path.length, - len(phi_result) - ) - - return dist_travelled, phi_result + return s, phi_result From bb781ed082f217e164c585e918475bafe4f7d231 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Mon, 2 Mar 2026 13:00:14 +0100 Subject: [PATCH 02/20] let segmented road generator take specific angles and lengths for segments --- src/pg_rad/inputparser/parser.py | 47 +++++-------------- src/pg_rad/inputparser/specs.py | 14 ++---- src/pg_rad/landscape/builder.py | 15 +++--- .../generators/base_road_generator.py | 6 --- .../generators/segmented_road_generator.py | 45 +++++++++--------- 5 files changed, 48 insertions(+), 79 deletions(-) diff --git a/src/pg_rad/inputparser/parser.py b/src/pg_rad/inputparser/parser.py index a566764..6680123 100644 --- a/src/pg_rad/inputparser/parser.py +++ b/src/pg_rad/inputparser/parser.py @@ -11,7 +11,6 @@ from .specs import ( MetadataSpec, RuntimeSpec, SimulationOptionsSpec, - SegmentSpec, PathSpec, ProceduralPathSpec, CSVPathSpec, @@ -122,7 +121,7 @@ class ConfigParser: def _parse_path(self) -> PathSpec: allowed_csv = {"file", "east_col_name", "north_col_name", "z"} - allowed_proc = {"segments", "length", "z"} + allowed_proc = {"segments", "length", "z", "alpha"} path = self.config.get("path") if path is None: @@ -172,13 +171,15 @@ class ConfigParser: if isinstance(raw_length, int | float): raw_length = [float(raw_length)] - segments = self._process_segment_angles(raw_segments) + segments, angles = self._process_segment_angles(raw_segments) lengths = self._process_segment_lengths(raw_length, len(segments)) - resolved_segments = self._combine_segments_lengths(segments, lengths) return ProceduralPathSpec( - segments=resolved_segments, + segments=segments, + angles=angles, + lengths=lengths, z=path.get("z", defaults.DEFAULT_PATH_HEIGHT), + alpha=path.get("alpha", defaults.DEFAULT_ALPHA) ) def _process_segment_angles( @@ -186,23 +187,25 @@ class ConfigParser: raw_segments: List[Union[str, dict]] ) -> List[Dict[str, Any]]: - normalized = [] + segments, angles = [], [] for segment in raw_segments: if isinstance(segment, str): - normalized.append({"type": segment, "angle": None}) + segments.append(segment) + angles.append(None) elif isinstance(segment, dict): if len(segment) != 1: raise ValueError("Invalid segment definition.") seg_type, angle = list(segment.items())[0] - normalized.append({"type": seg_type, "angle": angle}) + segments.append(seg_type) + angles.append(angle) else: raise ValueError("Invalid segment entry format.") - return normalized + return segments, angles def _process_segment_lengths( self, @@ -219,32 +222,6 @@ class ConfigParser: "number of elements equal to the number of segments." ) - def _combine_segments_lengths( - self, - segments: List[Dict[str, Any]], - lengths: List[float], - ) -> List[SegmentSpec]: - - resolved = [] - - for seg, length in zip(segments, lengths): - angle = seg["angle"] - - if angle is not None and not self._is_turn(seg["type"]): - raise ValueError( - f"A {seg['type']} segment does not support an angle." - ) - - resolved.append( - SegmentSpec( - type=seg["type"], - length=length, - angle=angle, - ) - ) - - return resolved - @staticmethod def _is_turn(segment_type: str) -> bool: return segment_type in {"turn_left", "turn_right"} diff --git a/src/pg_rad/inputparser/specs.py b/src/pg_rad/inputparser/specs.py index f483b6b..cf6b133 100644 --- a/src/pg_rad/inputparser/specs.py +++ b/src/pg_rad/inputparser/specs.py @@ -15,17 +15,10 @@ class RuntimeSpec: @dataclass class SimulationOptionsSpec: - air_density: float = 1.243 + air_density: float seed: int | None = None -@dataclass -class SegmentSpec: - type: str - length: float - angle: float | None - - @dataclass class PathSpec(ABC): pass @@ -33,8 +26,11 @@ class PathSpec(ABC): @dataclass class ProceduralPathSpec(PathSpec): - segments: list[SegmentSpec] + segments: list[str] + angles: list[float] + lengths: list[int | None] z: int | float + alpha: float @dataclass diff --git a/src/pg_rad/landscape/builder.py b/src/pg_rad/landscape/builder.py index 6912abc..0c6e694 100644 --- a/src/pg_rad/landscape/builder.py +++ b/src/pg_rad/landscape/builder.py @@ -56,23 +56,24 @@ class LandscapeBuilder: self, sim_spec: SimulationSpec ): - segments = sim_spec.path.segments - types = [s.type for s in segments] - lengths = [s.length for s in segments] - angles = [s.angle for s in segments] + lengths = sim_spec.path.lengths + angles = sim_spec.path.angles + alpha = sim_spec.path.alpha + + print(segments, lengths, angles) sg = SegmentedRoadGenerator( - length=lengths, ds=sim_spec.runtime.speed * sim_spec.runtime.acquisition_time, velocity=sim_spec.runtime.speed, seed=sim_spec.options.seed ) x, y = sg.generate( - segments=types, + segments=segments, lengths=lengths, - angles=angles + angles=angles, + alpha=alpha ) self._path = Path(list(zip(x, y))) diff --git a/src/road_gen/generators/base_road_generator.py b/src/road_gen/generators/base_road_generator.py index ac40fc2..9333852 100644 --- a/src/road_gen/generators/base_road_generator.py +++ b/src/road_gen/generators/base_road_generator.py @@ -7,7 +7,6 @@ class BaseRoadGenerator: """A base generator object for generating a road of a specified length.""" def __init__( self, - length: int | float, ds: int | float, velocity: int | float, mu: float = 0.7, @@ -17,7 +16,6 @@ class BaseRoadGenerator: """Initialize a BaseGenerator with a given or random seed. Args: - length (int | float): The total length of the road in meters. ds (int | float): The step size in meters. velocity (int | float): Velocity in meters per second. mu (float): Coefficient of friction. Defaults to 0.7 (dry asphalt). @@ -31,9 +29,6 @@ class BaseRoadGenerator: if not isinstance(seed, int): raise TypeError("seed must be an integer or None.") - if not isinstance(length, int | float): - raise TypeError("Length must be an integer or float in meters.") - if not isinstance(ds, int | float): raise TypeError("Step size must be integer or float in meters.") @@ -42,7 +37,6 @@ class BaseRoadGenerator: "Velocity must be integer or float in meters per second." ) - self.length = length self.ds = ds self.velocity = velocity diff --git a/src/road_gen/generators/segmented_road_generator.py b/src/road_gen/generators/segmented_road_generator.py index 0ba6f90..4e201e3 100644 --- a/src/road_gen/generators/segmented_road_generator.py +++ b/src/road_gen/generators/segmented_road_generator.py @@ -16,7 +16,6 @@ logger = logging.getLogger(__name__) class SegmentedRoadGenerator(BaseRoadGenerator): def __init__( self, - length: int | float | list[int | float], ds: int | float, velocity: int | float, mu: float = 0.7, @@ -26,7 +25,6 @@ class SegmentedRoadGenerator(BaseRoadGenerator): """Initialize a SegmentedRoadGenerator with given or random seed. Args: - length (int | float): The total length of the road in meters. ds (int | float): The step size in meters. velocity (int | float): Velocity in meters per second. mu (float): Coefficient of friction. Defaults to 0.7 (dry asphalt). @@ -34,18 +32,13 @@ class SegmentedRoadGenerator(BaseRoadGenerator): seed (int | None, optional): Set a seed for the generator. Defaults to random seed. """ - - if isinstance(length, list): - length = sum( - [seg_len for seg_len in length if seg_len is not None] - ) - super().__init__(length, ds, velocity, mu, g, seed) + super().__init__(ds, velocity, mu, g, seed) def generate( self, segments: list[str], - lengths: list[int | float] | None = None, - angles: list[int | float] | None = None, + lengths: list[int | float], + angles: list[float | None], alpha: float = defaults.DEFAULT_ALPHA, min_turn_angle: float = defaults.DEFAULT_MIN_TURN_ANGLE, max_turn_angle: float = defaults.DEFAULT_MAX_TURN_ANGLE @@ -54,6 +47,8 @@ class SegmentedRoadGenerator(BaseRoadGenerator): Args: segments (list[str]): List of segments. + lengths (list[int | float]): List of segment lengths. + angles (list[float | None]): List of angles. alpha (float, optional): Dirichlet concentration parameter. A higher value leads to more uniform apportionment of the length amongst the segments, while a lower value allows more @@ -88,26 +83,29 @@ class SegmentedRoadGenerator(BaseRoadGenerator): self.segments = segments self.alpha = alpha - num_points = np.ceil(self.length / self.ds).astype(int) + + total_length = sum(lengths) + num_points = np.ceil(total_length / self.ds).astype(int) # divide num_points into len(segments) randomly sized parts. - if isinstance(self.length, list): - parts = self.length + if len(lengths) == len(segments): + parts = np.array([seg_len / total_length for seg_len in lengths]) else: parts = self._rng.dirichlet( np.full(len(segments), alpha), size=1)[0] - parts = parts * num_points - parts = np.round(parts).astype(int) - # correct round off so the sum of parts is still total length L. - if sum(parts) != num_points: - parts[0] += num_points - sum(parts) + parts = parts * num_points + parts = np.round(parts).astype(int) + + # correct round off so the sum of parts is still total length L. + if sum(parts) != num_points: + parts[0] += num_points - sum(parts) curvature = np.zeros(num_points) current_index = 0 - for seg_name, seg_length in zip(segments, parts): + for seg_name, seg_length, seg_angle in zip(segments, parts, angles): seg_function = prefabs.PREFABS[seg_name] if seg_name == 'straight': @@ -128,12 +126,15 @@ class SegmentedRoadGenerator(BaseRoadGenerator): f"({R_min}, {R_max_angle})" ) - rand_radius = self._rng.uniform(R_min, R_max_angle) + if seg_angle: + radius = seg_length / np.deg2rad(seg_angle) + else: + radius = self._rng.uniform(R_min, R_max_angle) if seg_name.startswith("u_turn"): - curvature_s = seg_function(rand_radius) + curvature_s = seg_function(radius) else: - curvature_s = seg_function(seg_length, rand_radius) + curvature_s = seg_function(seg_length, radius) curvature[current_index:(current_index + seg_length)] = curvature_s current_index += seg_length From 41a8ca95b374f98efccf45a11f2fbb21c906b65f Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 09:32:45 +0100 Subject: [PATCH 03/20] remove print statement --- src/pg_rad/landscape/builder.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pg_rad/landscape/builder.py b/src/pg_rad/landscape/builder.py index 0c6e694..7fca241 100644 --- a/src/pg_rad/landscape/builder.py +++ b/src/pg_rad/landscape/builder.py @@ -61,8 +61,6 @@ class LandscapeBuilder: angles = sim_spec.path.angles alpha = sim_spec.path.alpha - print(segments, lengths, angles) - sg = SegmentedRoadGenerator( ds=sim_spec.runtime.speed * sim_spec.runtime.acquisition_time, velocity=sim_spec.runtime.speed, From c98000dfd89d31d82968de496bc57dfdfffdd6c2 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 09:48:20 +0100 Subject: [PATCH 04/20] Add detector architecture + isotropic detectors --- src/pg_rad/configs/defaults.py | 7 +++++ src/pg_rad/detector/__init__.py | 0 src/pg_rad/detector/builder.py | 20 +++++++++++++ src/pg_rad/detector/detectors.py | 38 +++++++++++++++++++++++++ src/pg_rad/inputparser/parser.py | 36 ++++++++++++++++++++++-- src/pg_rad/inputparser/specs.py | 8 ++++++ src/pg_rad/main.py | 4 ++- src/pg_rad/physics/fluence.py | 48 +++++++++++++++++++++++++++++--- src/pg_rad/simulator/engine.py | 12 ++++++-- tests/test_fluence_rate.py | 19 +++++++++++-- 10 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 src/pg_rad/detector/__init__.py create mode 100644 src/pg_rad/detector/builder.py create mode 100644 src/pg_rad/detector/detectors.py diff --git a/src/pg_rad/configs/defaults.py b/src/pg_rad/configs/defaults.py index 2d45681..9730f70 100644 --- a/src/pg_rad/configs/defaults.py +++ b/src/pg_rad/configs/defaults.py @@ -16,3 +16,10 @@ DEFAULT_MAX_TURN_ANGLE = 90. DEFAULT_FRICTION_COEFF = 0.7 # dry asphalt DEFAULT_GRAVITATIONAL_ACC = 9.81 # m/s^2 DEFAULT_ALPHA = 100. + +# --- Detector efficiencies --- +DETECTOR_EFFICIENCIES = { + "dummy": 1.0, + "NaIR": 0.0216, + "NaIF": 0.0254 +} diff --git a/src/pg_rad/detector/__init__.py b/src/pg_rad/detector/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pg_rad/detector/builder.py b/src/pg_rad/detector/builder.py new file mode 100644 index 0000000..cf5a6e8 --- /dev/null +++ b/src/pg_rad/detector/builder.py @@ -0,0 +1,20 @@ +from pg_rad.inputparser.specs import DetectorSpec + +from .detectors import IsotropicDetector, AngularDetector + + +class DetectorBuilder: + def __init__( + self, + detector_spec: DetectorSpec, + ): + self.detector_spec = detector_spec + + def build(self) -> IsotropicDetector | AngularDetector: + if self.detector_spec.is_isotropic: + return IsotropicDetector( + self.detector_spec.name, + self.detector_spec.eff + ) + else: + raise NotImplementedError("Angular detector not supported yet.") diff --git a/src/pg_rad/detector/detectors.py b/src/pg_rad/detector/detectors.py new file mode 100644 index 0000000..cb4ec1f --- /dev/null +++ b/src/pg_rad/detector/detectors.py @@ -0,0 +1,38 @@ +from abc import ABC + + +class BaseDetector(ABC): + def __init__( + self, + name: str, + eff: float + ): + self.name = name + self.eff = eff + + def get_efficiency(self): + pass + + +class IsotropicDetector(BaseDetector): + def __init__( + self, + name: str, + eff: float | None = None + ): + super().__init__(name, eff) + + def get_efficiency(self, energy): + return self.eff + + +class AngularDetector(BaseDetector): + def __init__( + self, + name: str, + eff: float | None = None + ): + super().__init__(name, eff) + + def get_efficiency(self, angle, energy): + pass diff --git a/src/pg_rad/inputparser/parser.py b/src/pg_rad/inputparser/parser.py index 6680123..d95c603 100644 --- a/src/pg_rad/inputparser/parser.py +++ b/src/pg_rad/inputparser/parser.py @@ -17,7 +17,8 @@ from .specs import ( SourceSpec, AbsolutePointSourceSpec, RelativePointSourceSpec, - SimulationSpec, + DetectorSpec, + SimulationSpec ) @@ -31,7 +32,8 @@ class ConfigParser: "acquisition_time", "path", "sources", - "options", + "detector", + "options" } def __init__(self, config_source: str): @@ -49,6 +51,7 @@ class ConfigParser: options = self._parse_options() path = self._parse_path() sources = self._parse_point_sources() + detector = self._parse_detector() return SimulationSpec( metadata=metadata, @@ -56,6 +59,7 @@ class ConfigParser: options=options, path=path, point_sources=sources, + detector=detector ) def _load_yaml(self, config_source: str) -> Dict[str, Any]: @@ -285,6 +289,34 @@ class ConfigParser: return specs + def _parse_detector(self) -> DetectorSpec: + det_dict = self.config.get("detector", {}) + required = {"name", "is_isotropic"} + + missing = required - det_dict.keys() + if missing: + raise MissingConfigKeyError(missing) + + name = det_dict.get("name") + is_isotropic = det_dict.get("is_isotropic") + eff = det_dict.get("efficiency") + + default_detectors = defaults.DETECTOR_EFFICIENCIES + + if eff is None and name in default_detectors.keys(): + eff = default_detectors[name] + elif eff is not None: + pass + else: + raise ValueError( + f"The detector {name} is not in the library, and no " + "efficiency was defined. Either specify detector efficiency " + "or choose one from the following list: " + f"{default_detectors.keys()}" + ) + + return DetectorSpec(name=name, eff=eff, is_isotropic=is_isotropic) + def _warn_unknown_keys(self, section: str, provided: set, allowed: set): unknown = provided - allowed if unknown: diff --git a/src/pg_rad/inputparser/specs.py b/src/pg_rad/inputparser/specs.py index cf6b133..7bb3631 100644 --- a/src/pg_rad/inputparser/specs.py +++ b/src/pg_rad/inputparser/specs.py @@ -63,6 +63,13 @@ class RelativePointSourceSpec(SourceSpec): z: float +@dataclass +class DetectorSpec: + name: str + eff: float | None + is_isotropic: bool + + @dataclass class SimulationSpec: metadata: MetadataSpec @@ -70,3 +77,4 @@ class SimulationSpec: options: SimulationOptionsSpec path: PathSpec point_sources: list[SourceSpec] + detector: DetectorSpec diff --git a/src/pg_rad/main.py b/src/pg_rad/main.py index 1c50784..4214f89 100644 --- a/src/pg_rad/main.py +++ b/src/pg_rad/main.py @@ -5,6 +5,7 @@ import sys from pandas.errors import ParserError from yaml import YAMLError +from pg_rad.detector.builder import DetectorBuilder from pg_rad.exceptions.exceptions import ( MissingConfigKeyError, OutOfBoundsError, @@ -81,9 +82,10 @@ def main(): try: cp = ConfigParser(args.config).parse() landscape = LandscapeDirector.build_from_config(cp) - + detector = DetectorBuilder(cp.detector).build() output = SimulationEngine( landscape=landscape, + detector=detector, runtime_spec=cp.runtime, sim_spec=cp.options ).simulate() diff --git a/src/pg_rad/physics/fluence.py b/src/pg_rad/physics/fluence.py index 8a448da..16d4f62 100644 --- a/src/pg_rad/physics/fluence.py +++ b/src/pg_rad/physics/fluence.py @@ -2,6 +2,9 @@ from typing import Tuple, TYPE_CHECKING import numpy as np +from pg_rad.detector.detectors import IsotropicDetector, AngularDetector + + if TYPE_CHECKING: from pg_rad.landscape.landscape import Landscape @@ -12,6 +15,7 @@ def phi( 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). @@ -34,6 +38,7 @@ def phi( phi_r = ( activity + * eff * branching_ratio * np.exp(-mu_air * r) / (4 * np.pi * r**2) @@ -42,12 +47,20 @@ def phi( return phi_r -def calculate_fluence_at(landscape: "Landscape", pos: np.ndarray, scaling=1E6): +def calculate_fluence_at( + landscape: "Landscape", + pos: np.ndarray, + detector: IsotropicDetector | AngularDetector, + tangent_vectors: np.ndarray, + scaling=1E6 +): """Compute fluence at an arbitrary position in the landscape. Args: landscape (Landscape): The landscape to compute. pos (np.ndarray): (N, 3) array of positions. + detector (IsotropicDetector | AngularDetector): + Detector object, needed to compute correct efficiency. Returns: total_phi (np.ndarray): (N,) array of fluences. @@ -56,15 +69,30 @@ def calculate_fluence_at(landscape: "Landscape", pos: np.ndarray, scaling=1E6): total_phi = np.zeros(pos.shape[0]) for source in landscape.point_sources: - r = np.linalg.norm(pos - np.array(source.pos), axis=1) + 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 isinstance(detector, AngularDetector): + cos_theta = ( + np.sum(tangent_vectors * source_to_detector, axis=1) / ( + np.linalg.norm(source_to_detector, axis=1) * + np.linalg.norm(tangent_vectors, axis=1) + ) + ) + cos_theta = np.clip(cos_theta, -1, 1) + theta = np.arccos(cos_theta) + eff = detector.get_efficiency(theta, energy=source.isotope.E) + else: + eff = detector.get_efficiency(energy=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 + air_density=landscape.air_density, + eff=eff ) total_phi += phi_source @@ -74,6 +102,7 @@ def calculate_fluence_at(landscape: "Landscape", pos: np.ndarray, scaling=1E6): def calculate_fluence_along_path( landscape: "Landscape", + detector: "IsotropicDetector | AngularDetector", points_per_segment: int = 10 ) -> Tuple[np.ndarray, np.ndarray]: path = landscape.path @@ -98,6 +127,17 @@ def calculate_fluence_along_path( z = np.full(xnew.shape, path.z) full_positions = np.c_[xnew, ynew, z] - phi_result = calculate_fluence_at(landscape, full_positions) + # to compute the angle between sources and the direction of travel, we + # compute tangent vectors along the path. + dx_ds = np.gradient(xnew, s) + dy_ds = np.gradient(ynew, s) + tangent_vectors = np.c_[dx_ds, dy_ds, np.zeros_like(dx_ds)] + tangent_vectors /= np.linalg.norm(tangent_vectors, axis=1, keepdims=True) + + phi_result = calculate_fluence_at( + landscape, + full_positions, + detector, + tangent_vectors) return s, phi_result diff --git a/src/pg_rad/simulator/engine.py b/src/pg_rad/simulator/engine.py index 8de7e98..c5aa754 100644 --- a/src/pg_rad/simulator/engine.py +++ b/src/pg_rad/simulator/engine.py @@ -6,6 +6,7 @@ from pg_rad.simulator.outputs import ( SimulationOutput, SourceOutput ) +from pg_rad.detector.detectors import IsotropicDetector, AngularDetector from pg_rad.physics.fluence import calculate_fluence_along_path from pg_rad.utils.projection import minimal_distance_to_path from pg_rad.inputparser.specs import RuntimeSpec, SimulationOptionsSpec @@ -16,11 +17,13 @@ class SimulationEngine: def __init__( self, landscape: Landscape, - runtime_spec=RuntimeSpec, - sim_spec=SimulationOptionsSpec + detector: IsotropicDetector | AngularDetector, + runtime_spec: RuntimeSpec, + sim_spec: SimulationOptionsSpec, ): self.landscape = landscape + self.detector = detector self.runtime_spec = runtime_spec self.sim_spec = sim_spec @@ -37,7 +40,10 @@ class SimulationEngine: ) def _calculate_count_rate_along_path(self) -> CountRateOutput: - arc_length, phi = calculate_fluence_along_path(self.landscape) + arc_length, phi = calculate_fluence_along_path( + self.landscape, + self.detector + ) return CountRateOutput(arc_length, phi) def _calculate_point_source_distance_to_path(self) -> List[SourceOutput]: diff --git a/tests/test_fluence_rate.py b/tests/test_fluence_rate.py index 20cdaae..7220612 100644 --- a/tests/test_fluence_rate.py +++ b/tests/test_fluence_rate.py @@ -7,7 +7,13 @@ from pg_rad.physics import calculate_fluence_at @pytest.fixture -def phi_ref(test_landscape): +def isotropic_detector(): + from pg_rad.detector.detectors import IsotropicDetector + return IsotropicDetector(name="test_detector", eff=1.0) + + +@pytest.fixture +def phi_ref(test_landscape, isotropic_detector): source = test_landscape.point_sources[0] r = np.linalg.norm(np.array([10, 10, 0]) - np.array(source.pos)) @@ -17,7 +23,8 @@ def phi_ref(test_landscape): mu_air = source.isotope.mu_mass_air * test_landscape.air_density mu_air *= 0.1 - return A * b * np.exp(-mu_air * r) / (4 * np.pi * r**2) + eff = isotropic_detector.get_efficiency(source.isotope.E) + return A * eff * b * np.exp(-mu_air * r) / (4 * np.pi * r**2) @pytest.fixture @@ -38,6 +45,10 @@ def test_landscape(): activity_MBq: 100 position: [0, 0, 0] isotope: CS137 + + detector: + name: dummy + is_isotropic: True """ cp = ConfigParser(test_yaml).parse() @@ -45,9 +56,11 @@ def test_landscape(): return landscape -def test_single_source_fluence(phi_ref, test_landscape): +def test_single_source_fluence(phi_ref, test_landscape, isotropic_detector): phi = calculate_fluence_at( test_landscape, np.array([10, 10, 0]), + isotropic_detector, + tangent_vectors=None, ) assert pytest.approx(phi[0], rel=1E-3) == phi_ref From b69b7455f1d5641bfe1c9315cc516b90be9bd348 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 20:58:34 +0100 Subject: [PATCH 05/20] fix isotope typo --- src/pg_rad/isotopes/isotope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pg_rad/isotopes/isotope.py b/src/pg_rad/isotopes/isotope.py index 9e7aa77..ec501af 100644 --- a/src/pg_rad/isotopes/isotope.py +++ b/src/pg_rad/isotopes/isotope.py @@ -40,7 +40,7 @@ class CS137(Isotope): preset_isotopes: Dict[str, Type[Isotope]] = { - "CS137": CS137 + "Cs137": CS137 } From 7612f74bcbe1f0f2639a06a12f69fba8144efcf0 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 20:59:15 +0100 Subject: [PATCH 06/20] rename eff to efficiency --- src/pg_rad/detector/builder.py | 2 +- src/pg_rad/detector/detectors.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pg_rad/detector/builder.py b/src/pg_rad/detector/builder.py index cf5a6e8..940a2f9 100644 --- a/src/pg_rad/detector/builder.py +++ b/src/pg_rad/detector/builder.py @@ -14,7 +14,7 @@ class DetectorBuilder: if self.detector_spec.is_isotropic: return IsotropicDetector( self.detector_spec.name, - self.detector_spec.eff + self.detector_spec.efficiency ) else: raise NotImplementedError("Angular detector not supported yet.") diff --git a/src/pg_rad/detector/detectors.py b/src/pg_rad/detector/detectors.py index cb4ec1f..439ee56 100644 --- a/src/pg_rad/detector/detectors.py +++ b/src/pg_rad/detector/detectors.py @@ -5,10 +5,10 @@ class BaseDetector(ABC): def __init__( self, name: str, - eff: float + efficiency: float ): self.name = name - self.eff = eff + self.efficiency = efficiency def get_efficiency(self): pass @@ -18,21 +18,21 @@ class IsotropicDetector(BaseDetector): def __init__( self, name: str, - eff: float | None = None + efficiency: float, ): - super().__init__(name, eff) + super().__init__(name, efficiency) def get_efficiency(self, energy): - return self.eff + return self.efficiency class AngularDetector(BaseDetector): def __init__( self, name: str, - eff: float | None = None + efficiency: float ): - super().__init__(name, eff) + super().__init__(name, efficiency) def get_efficiency(self, angle, energy): pass From 1c8cc41e3c45d5868bf4c1350a2ace8c6e95e097 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 21:01:51 +0100 Subject: [PATCH 07/20] Improve error handling. Add alignment feature for point sources --- src/pg_rad/exceptions/exceptions.py | 4 ++ src/pg_rad/inputparser/parser.py | 59 ++++++++++++++++++--------- src/pg_rad/inputparser/specs.py | 12 +++--- src/pg_rad/landscape/builder.py | 62 ++++++++++++++++++++++++++++- src/pg_rad/landscape/director.py | 2 +- 5 files changed, 113 insertions(+), 26 deletions(-) diff --git a/src/pg_rad/exceptions/exceptions.py b/src/pg_rad/exceptions/exceptions.py index 0b5a474..aac816d 100644 --- a/src/pg_rad/exceptions/exceptions.py +++ b/src/pg_rad/exceptions/exceptions.py @@ -31,3 +31,7 @@ class DimensionError(ValueError): class InvalidIsotopeError(ValueError): """Raised if attempting to load an isotope that is not valid.""" + + +class InvalidConfigValueError(ValueError): + """Raised if a config key has an incorrect type or value.""" diff --git a/src/pg_rad/inputparser/parser.py b/src/pg_rad/inputparser/parser.py index d95c603..5b5a111 100644 --- a/src/pg_rad/inputparser/parser.py +++ b/src/pg_rad/inputparser/parser.py @@ -4,7 +4,11 @@ from typing import Any, Dict, List, Union import yaml -from pg_rad.exceptions.exceptions import MissingConfigKeyError, DimensionError +from pg_rad.exceptions.exceptions import ( + MissingConfigKeyError, + DimensionError, + InvalidConfigValueError +) from pg_rad.configs import defaults from .specs import ( @@ -14,7 +18,7 @@ from .specs import ( PathSpec, ProceduralPathSpec, CSVPathSpec, - SourceSpec, + PointSourceSpec, AbsolutePointSourceSpec, RelativePointSourceSpec, DetectorSpec, @@ -98,19 +102,23 @@ class ConfigParser: def _parse_options(self) -> SimulationOptionsSpec: options = self.config.get("options", {}) - allowed = {"air_density", "seed"} + allowed = {"air_density_kg_per_m3", "seed"} self._warn_unknown_keys( section="options", provided=set(options.keys()), allowed=allowed, ) - air_density = options.get("air_density", defaults.DEFAULT_AIR_DENSITY) + air_density = options.get( + "air_density_kg_per_m3", + defaults.DEFAULT_AIR_DENSITY + ) seed = options.get("seed") if not isinstance(air_density, float) or air_density <= 0: raise ValueError( - "options.air_density must be a positive float in kg/m^3." + "options.air_density_kg_per_m3 must be a positive float " + "in kg/m^3." ) if ( seed is not None or @@ -230,9 +238,9 @@ class ConfigParser: def _is_turn(segment_type: str) -> bool: return segment_type in {"turn_left", "turn_right"} - def _parse_point_sources(self) -> List[SourceSpec]: + def _parse_point_sources(self) -> List[PointSourceSpec]: source_dict = self.config.get("sources", {}) - specs: List[SourceSpec] = [] + specs: List[PointSourceSpec] = [] for name, params in source_dict.items(): @@ -245,7 +253,7 @@ class ConfigParser: isotope = params.get("isotope") if not isinstance(activity, int | float) or activity <= 0: - raise ValueError( + raise InvalidConfigValueError( f"sources.{name}.activity_MBq must be positive value " "in MegaBequerels." ) @@ -270,6 +278,16 @@ class ConfigParser: ) elif isinstance(position, dict): + alignment = position.get("acquisition_alignment") + if alignment not in {'best', 'worst', None}: + raise InvalidConfigValueError( + f"sources.{name}.acquisition_alignment must be " + "'best' or 'worst', with 'best' aligning source " + f"{name} in the middle of the two nearest acquisition " + "points, and 'worst' aligning exactly perpendicular " + "to the nearest acquisition point." + ) + specs.append( RelativePointSourceSpec( name=name, @@ -278,12 +296,13 @@ class ConfigParser: along_path=float(position["along_path"]), dist_from_path=float(position["dist_from_path"]), side=position["side"], - z=position.get("z", defaults.DEFAULT_SOURCE_HEIGHT) + z=position.get("z", defaults.DEFAULT_SOURCE_HEIGHT), + alignment=alignment ) ) else: - raise ValueError( + raise InvalidConfigValueError( f"Invalid position format for source '{name}'." ) @@ -295,7 +314,7 @@ class ConfigParser: missing = required - det_dict.keys() if missing: - raise MissingConfigKeyError(missing) + raise MissingConfigKeyError("detector", missing) name = det_dict.get("name") is_isotropic = det_dict.get("is_isotropic") @@ -303,19 +322,23 @@ class ConfigParser: default_detectors = defaults.DETECTOR_EFFICIENCIES - if eff is None and name in default_detectors.keys(): + if name in default_detectors.keys() and not eff: eff = default_detectors[name] - elif eff is not None: + elif eff: pass else: raise ValueError( - f"The detector {name} is not in the library, and no " - "efficiency was defined. Either specify detector efficiency " - "or choose one from the following list: " - f"{default_detectors.keys()}" + f"The detector {name} not found in library. Either " + f"specify {name}.efficiency or " + "choose a detector from the following list: " + f"{default_detectors.keys()}." ) - return DetectorSpec(name=name, eff=eff, is_isotropic=is_isotropic) + return DetectorSpec( + name=name, + efficiency=eff, + is_isotropic=is_isotropic + ) def _warn_unknown_keys(self, section: str, provided: set, allowed: set): unknown = provided - allowed diff --git a/src/pg_rad/inputparser/specs.py b/src/pg_rad/inputparser/specs.py index 7bb3631..6f74f14 100644 --- a/src/pg_rad/inputparser/specs.py +++ b/src/pg_rad/inputparser/specs.py @@ -1,5 +1,6 @@ from abc import ABC from dataclasses import dataclass +from typing import Literal @dataclass @@ -42,31 +43,32 @@ class CSVPathSpec(PathSpec): @dataclass -class SourceSpec(ABC): +class PointSourceSpec(ABC): activity_MBq: float isotope: str name: str @dataclass -class AbsolutePointSourceSpec(SourceSpec): +class AbsolutePointSourceSpec(PointSourceSpec): x: float y: float z: float @dataclass -class RelativePointSourceSpec(SourceSpec): +class RelativePointSourceSpec(PointSourceSpec): along_path: float dist_from_path: float side: str z: float + alignment: Literal["best", "worst"] | None @dataclass class DetectorSpec: name: str - eff: float | None + efficiency: float is_isotropic: bool @@ -76,5 +78,5 @@ class SimulationSpec: runtime: RuntimeSpec options: SimulationOptionsSpec path: PathSpec - point_sources: list[SourceSpec] + point_sources: list[PointSourceSpec] detector: DetectorSpec diff --git a/src/pg_rad/landscape/builder.py b/src/pg_rad/landscape/builder.py index 7fca241..2ab78ef 100644 --- a/src/pg_rad/landscape/builder.py +++ b/src/pg_rad/landscape/builder.py @@ -1,5 +1,7 @@ import logging -from typing import Self +from typing import Literal, Self + +import numpy as np from .landscape import Landscape from pg_rad.dataloader.dataloader import load_data @@ -113,11 +115,25 @@ class LandscapeBuilder: pos = (s.x, s.y, s.z) elif isinstance(s, RelativePointSourceSpec): path = self.get_path() + + if s.alignment: + along_path = self._align_relative_source( + s.along_path, + path, + s.alignment + ) + logger.info( + f"Because source {s.name} was set to align with path " + f"({s.alignment} alignment), it was moved to be at " + f"{along_path} m along the path from {s.along_path} m." + ) + else: + along_path = s.along_path pos = rel_to_abs_source_position( x_list=path.x_list, y_list=path.y_list, path_z=path.z, - along_path=s.along_path, + along_path=along_path, side=s.side, dist_from_path=s.dist_from_path) if any( @@ -158,6 +174,48 @@ class LandscapeBuilder: max_size = max(self._path.size) self.set_landscape_size((max_size, max_size)) + def _align_relative_source( + self, + along_path: float, + path: "Path", + mode: Literal["best", "worst"], + ) -> tuple[float, float, float]: + """Given the arc length at which the point source is placed, + align the source relative to the waypoints of the path. Here, + 'best' means the point source is moved such that it is + perpendicular to the midpoint between two acuisition points. + 'worst' means the point source is moved such that it is + perpendicular to the nearest acquisition point. + + The distance to the path is not affected by this algorithm. + + For more details on alignment, see + Fig. 4 and page 24 in Bukartas (2021). + + Args: + along_path (float): Current arc length position of the source. + path (Path): The path to align to. + mode (Literal["best", "worst"]): Alignment mode. + + Returns: + along_new (float): The updated arc length position. + """ + ds = np.hypot( + path.x_list[1] - path.x_list[0], + path.y_list[1] - path.y_list[0], + ) + + if mode == "worst": + along_new = round(along_path / ds) * ds + + elif mode == "best": + along_new = (round(along_path / ds - 0.5) + 0.5) * ds + + else: + raise ValueError(f"Unknown alignment mode: {mode}") + + return along_new + def build(self): landscape = Landscape( name=self.name, diff --git a/src/pg_rad/landscape/director.py b/src/pg_rad/landscape/director.py index cd163b4..f8d7e68 100644 --- a/src/pg_rad/landscape/director.py +++ b/src/pg_rad/landscape/director.py @@ -22,7 +22,7 @@ class LandscapeDirector: def build_test_landscape(): fp = files('pg_rad.data').joinpath(TEST_EXP_DATA) source = PointSource( - activity_MBq=100E9, + activity_MBq=100E6, isotope="CS137", position=(0, 0, 0) ) From cdd6d3a8b45d6d6b29a39b48f61948a70f477ee0 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 21:03:26 +0100 Subject: [PATCH 08/20] improve logging. update test case for detector. --- src/pg_rad/main.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pg_rad/main.py b/src/pg_rad/main.py index 4214f89..e944fb5 100644 --- a/src/pg_rad/main.py +++ b/src/pg_rad/main.py @@ -10,6 +10,7 @@ from pg_rad.exceptions.exceptions import ( MissingConfigKeyError, OutOfBoundsError, DimensionError, + InvalidConfigValueError, InvalidIsotopeError ) from pg_rad.logger.logger import setup_logger @@ -64,15 +65,20 @@ def main(): activity_MBq: 1000 position: [500, 100, 0] isotope: CS137 + + detector: + name: dummy + is_isotropic: True """ cp = ConfigParser(test_yaml).parse() landscape = LandscapeDirector.build_from_config(cp) - + detector = DetectorBuilder(cp.detector).build() output = SimulationEngine( landscape=landscape, runtime_spec=cp.runtime, - sim_spec=cp.options + sim_spec=cp.options, + detector=detector ).simulate() plotter = ResultPlotter(landscape, output) @@ -96,18 +102,19 @@ def main(): MissingConfigKeyError, KeyError, YAMLError, - ): + ) as e: + logger.critical(e) logger.critical( - "The provided config file is invalid. " - "Check the log above. You can consult the documentation for " - "an explanation of how to define a config file." + "The config file is missing required keys or may be an " + "invalid YAML file. Check the log above. Consult the " + "documentation for examples of how to write a config file." ) sys.exit(1) except ( OutOfBoundsError, DimensionError, InvalidIsotopeError, - ValueError + InvalidConfigValueError ) as e: logger.critical(e) logger.critical( From 7e2d6076fdffb6624b87fc5d7a0f2339d92dd6d2 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 21:04:46 +0100 Subject: [PATCH 09/20] update docs --- .../landscape/create_landscape_from_path.md | 4 - docs/API/landscape/landscape.md | 4 - docs/API/objects/object.md | 5 - docs/API/path/path.md | 4 - docs/API/path/path_from_RT90.md | 5 - docs/API/path/simplify_path.md | 4 - docs/API/sources/point_source.md | 5 - docs/config-spec.md | 218 ++++++++++++++++ docs/explainers/planar_curve.ipynb | 203 +++++++++++++++ docs/explainers/prefab_roads.ipynb | 118 +++++++++ docs/index.md | 24 +- docs/installation.md | 47 ++++ docs/pg-rad-in-cli.md | 4 - docs/pg-rad-in-python.ipynb | 241 +----------------- docs/quickstart.md | 187 ++++++++++++++ mkdocs.yml | 16 +- 16 files changed, 796 insertions(+), 293 deletions(-) delete mode 100644 docs/API/landscape/create_landscape_from_path.md delete mode 100644 docs/API/landscape/landscape.md delete mode 100644 docs/API/objects/object.md delete mode 100644 docs/API/path/path.md delete mode 100644 docs/API/path/path_from_RT90.md delete mode 100644 docs/API/path/simplify_path.md delete mode 100644 docs/API/sources/point_source.md create mode 100644 docs/config-spec.md create mode 100644 docs/explainers/planar_curve.ipynb create mode 100644 docs/explainers/prefab_roads.ipynb create mode 100644 docs/installation.md delete mode 100644 docs/pg-rad-in-cli.md create mode 100644 docs/quickstart.md diff --git a/docs/API/landscape/create_landscape_from_path.md b/docs/API/landscape/create_landscape_from_path.md deleted file mode 100644 index 75c3b28..0000000 --- a/docs/API/landscape/create_landscape_from_path.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: pg_rad.landscape.create_landscape_from_path ---- -::: pg_rad.landscape.create_landscape_from_path \ No newline at end of file diff --git a/docs/API/landscape/landscape.md b/docs/API/landscape/landscape.md deleted file mode 100644 index dc4c9a6..0000000 --- a/docs/API/landscape/landscape.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: pg_rad.landscape.Landscape ---- -::: pg_rad.landscape.Landscape \ No newline at end of file diff --git a/docs/API/objects/object.md b/docs/API/objects/object.md deleted file mode 100644 index 188c8aa..0000000 --- a/docs/API/objects/object.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: pg_rad.objects.Object ---- - -::: pg_rad.objects.Object \ No newline at end of file diff --git a/docs/API/path/path.md b/docs/API/path/path.md deleted file mode 100644 index 302b51b..0000000 --- a/docs/API/path/path.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: pg_rad.path.Path ---- -::: pg_rad.path.Path \ No newline at end of file diff --git a/docs/API/path/path_from_RT90.md b/docs/API/path/path_from_RT90.md deleted file mode 100644 index 76f34ea..0000000 --- a/docs/API/path/path_from_RT90.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: pg_rad.path.path_from_RT90 ---- -::: pg_rad.path.path_from_RT90 - diff --git a/docs/API/path/simplify_path.md b/docs/API/path/simplify_path.md deleted file mode 100644 index 23294fb..0000000 --- a/docs/API/path/simplify_path.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: pg_rad.path.simplify_path ---- -::: pg_rad.path.simplify_path \ No newline at end of file diff --git a/docs/API/sources/point_source.md b/docs/API/sources/point_source.md deleted file mode 100644 index 5d7732e..0000000 --- a/docs/API/sources/point_source.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: pg_rad.sources.PointSource ---- - -::: pg_rad.sources.PointSource \ No newline at end of file diff --git a/docs/config-spec.md b/docs/config-spec.md new file mode 100644 index 0000000..315f27b --- /dev/null +++ b/docs/config-spec.md @@ -0,0 +1,218 @@ +!!! note + To get started quickly, you may copy and modify one of the example configs found [here](quickstart.md#example-configs). + + +The config file must be a [YAML](https://yaml.org/) file. YAML is a serialization language that works with key-value pairs, but in a syntax more readable than some other alternatives. In YAML, the indentation matters. I + + +The remainder of this chapter will explain the different required and optionals keys, what they represent, and allowed values. + +## Required keys + +### Simulation options + +The first step is to name the simulation, and define the speed of the vehicle (assumed constant) and acquisition time. + +#### Landscape name + +The name is a string, which may include spaces, numbers and special characters. + +Examples: + +```yaml +name: test_landscape +``` +```yaml +name: Test Landscape 1 +``` + +#### Acquisition time + +The acquisition time of the detector in seconds. + +Example: + +```yaml +acquisition_time: 1 +``` + +!!! note + All units in the config file must be specified in SI units, e.g. meters and seconds, unless the key contains a unit itself (e.g. `activity_MBq` means activity in MegaBequerels). + +#### Vehicle speed + +The speed of the vehicle in m/s. Currently, the vehicle speed must be assumed constant. An example could be + +```yaml +speed: 13.89 # this is approximately 50 km/h +``` + +!!! note + The text after the `#` signifies a comment. PG-RAD will ignore this, but it can be helpful for yourself to write notes. + + +### Path + +The `path` keyword is used to create a path for the detector to travel along. There are two ways to specify a path; from experimental data or by specifying a procedural path. + +#### Path - Experimental data + +Currently the only supported coordinate format is the RT90 (East, North) coordinate system. If you have experimental data in CSV format with columns for these coordinates, then you can load that path into PG-RAD as follows: + +```yaml +path: + file: path/to/experimental_data.csv + east_col_name: East + north_col_name: North +``` + +#### Path - Procedural path + +Alternatively, you can let PG-RAD generate a path for you. A procedural path can be specified with at least two subkeys: `length` and `segments`. + +Currently supported segments are: `straight`, `turn_left` and `turn_right`, and are provided in a list under the `segments` subkey as follows: + +```yaml +path: + segments: + - straight + - turn_left + - straight +``` + +The length must also be specified, using the `length` subkey. `length` can be specified in two ways: a list with the same length as the `segments` list + +```yaml +path: + segments: + - straight + - turn_left + - straight + length: + - 500 + - 250 + - 500 +``` + +which will assign that length (meters) to each segment. Alternatively, a single number can be passed: + +```yaml +path: + segments: + - straight + - turn_left + - straight + length: 1250 +``` + +Setting the length for the total path will cause PG-RAD to *randomly assign* portions of the total length to each segment. + +Finally, there is also an option to specify the turn angle in degrees: + +```yaml +path: + segments: + - straight + - turn_left: 90 + - straight + length: 1250 +``` + +Like with the lengths, if a turn segment has no angle specified, a random one (within pre-defined limits) will be taken. + +!!! warning + Letting PG-RAD randomly assign lengths and angles can cause (expected) issues. That is because of physics restrictions. If the combination of length, angle (radius) and velocity of the vehicle is such that the centrifugal force makes it impossible to take this turn, PG-RAD will raise an error. To fix it, you can 1) reduce the speed; 2) define a smaller angle for the turn; or 3) assign more length to the turn segment. + +!!! info + For more information about how procedural roads are generated, including the random sampling of lengths and angles, see X + +### Sources + +Currently, the only type of source supported is a point source. Point sources can be added under the `sources` key, where the **subkey is the name** of the source: + +```yaml +sources: + my_source: ... +``` + +the source name should not contain spaces or special characters other than `_` or `-`. There are three required subkeys under `sources.my_source`, which are: `activity_MBq`, `isotope` and `position`. + +#### Source activity + +The source activity is in MegaBequerels and must be a strictly positive number: + +```yaml +sources: + my_source: + activity_MBq: 100 +``` + +#### Source isotope + +The isotope for the point source. This must be a string, following the naming convention of the symbol followed by the number of nucleons, e.g. `Cs137`: + +```yaml +sources: + my_source: + activity_MBq: 100 + isotope: Cs137 +``` + +!!! info + Currently the following isotopes are supported: `Cs137` + +#### Source position + +There are two ways to specify the source position. Either with absolute (x,y,z) coordinates + +```yaml +sources: + my_source: + activity_MBq: 100 + isotope: Cs137 + position: [0, 0, 0] +``` + +or relative to the path, using the subkeys `along_path`, `dist_from_path` and `side` + +```yaml +sources: + my_source: + activity_MBq: 100 + isotope: Cs137 + position: + along_path: 100 + dist_from_path: 50 + side: left +``` + +Note that side is relative to the direction of travel. The path will by default start at (x,y) = (0,0) and initial heading is parallel to the x-axis. + +### Detector + +The final required key is the `detector`. Currently, only isotropic detectors are supported. Nonetheless, you must specify it with `name`, `is_isotropic` and `efficiency`: + +```yaml +detector: + name: test + is_isotropic: True + efficiency: 0.02 +``` + +Note there are some existing detectors available, where efficiency is not required and will be looked up by PG-RAD itself: + +```yaml +detector: + name: NaIR + is_isotropic: True +``` + +## Optional keys + +The following subkeys are optional and should be put under the `options` key. + +```yaml +options: + air_density_kg_per_m3: 1.243 + seed: 1234 +``` \ No newline at end of file diff --git a/docs/explainers/planar_curve.ipynb b/docs/explainers/planar_curve.ipynb new file mode 100644 index 0000000..9b2f576 --- /dev/null +++ b/docs/explainers/planar_curve.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "59981fce", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from road_gen.integrator.integrator import integrate_road\n", + "from road_gen.plotting.plot_road import plot_road" + ] + }, + { + "cell_type": "markdown", + "id": "a8d303ad", + "metadata": {}, + "source": [ + "# Describing a road as a planar curve" + ] + }, + { + "cell_type": "markdown", + "id": "08dda386", + "metadata": {}, + "source": [ + "Let $r(s)$ be a [planar curve](https://en.wikipedia.org/wiki/Plane_curve) describing a road in the xy-plane as a function of *arc length* $s$ (distance traveled along the road), where $s \\in [0, L]$.\n", + "Suppose a road of total length $L$ in the xy-plane. Let the distance traveled along the road be denoted $s$ where $s \\in [0, L]$.\n", + "\n", + "Let $\\kappa(s)$ be the curvature of $r(s)$\n", + "\n", + "$$\n", + "\\kappa(s) = \\frac{d\\theta}{ds}\n", + "$$\n", + "\n", + "where $\\theta$ is the heading (direction) of the road. Basically, $\\kappa(s)$ tells us at arc length $s$ whether the path is about to turn left or right, with larger magnitude of $\\kappa(s)$ indicating a sharper change, and $\\kappa(s) = 0$ indicating no change. A path in the xy-plane can now be fully defined by $\\kappa(s)$ and $L$, as the heading angle is simply\n", + "\n", + "$$\n", + "\\theta(s) = \\int_0^s \\kappa(u)~du\n", + "$$\n", + "\n", + "and\n", + "\n", + "$$\n", + "x(s) = \\int_0^s \\cos(\\theta(u))~du \\; , \\; y(s) = \\int_0^s \\sin(\\theta(u))~du\n", + "$$\n", + "\n", + "#### Discrete form\n", + "\n", + "In practice, if we are going to generate a road in computer code we need to discretize this above formulation. Suppose a step size $\\Delta s$, then at the $i$-th waypoint we have traveled a distance $s_i = i \\Delta s$ and we have $N = L/\\Delta s$ waypoints in total. At waypoint $i$, with known curvature $\\kappa_i$, the next heading is\n", + "\n", + "$$\n", + "\\theta_{i+1} = \\theta_i + \\kappa_i \\Delta s\n", + "$$\n", + "\n", + "which by recursion means that\n", + "\n", + "$$\n", + "\\theta_{i+1} = \\sum_{j=0}^i \\theta_j + \\kappa_j \\Delta s\n", + "$$\n", + "\n", + "and likewise\n", + "\n", + "$$\n", + "x_{i+1} = \\sum_{j=0}^i x_j \\cos(\\theta_j) \\Delta s \\; , \\; y_{i+1} = \\sum_{j=0}^i y_j \\sin(\\theta_j) \\Delta s.\n", + "$$\n", + "\n", + "This shows that with starting conditions $(x_0, y_0, \\theta_0)$ and discrete curvature field $\\{\\kappa_0, \\kappa_1 ... \\kappa_{N-1}\\}$ we can construct a path of any arbitrary length $L$ with its shape entirely determined by the curvature field." + ] + }, + { + "cell_type": "markdown", + "id": "7a22a5ef", + "metadata": {}, + "source": [ + "# Visualizing curvature effect on final road composition" + ] + }, + { + "cell_type": "markdown", + "id": "26135d05", + "metadata": {}, + "source": [ + "#### Straight road\n", + "A straight road can be defined by using curvature $\\kappa_i = 0$ at every step $i$; this would produce a straight road of length $L$ in the direction of the intitial heading $\\theta_0$." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "20f6ccb4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAL01JREFUeJzt3Xtc1XWex/H3AeQiysELggheoosSJiaBaK2VTKTWyoilriU6blclTbOwvHQnK8u8pGtbmqUPzUrHHLNFNNOVFCUrvGWTF9IBNBVUEgl++0fj2U4iHhI48p3X8/E4j12+5/s75/OTx2PmNb9zwWZZliUAAADUex7uHgAAAAA1g7ADAAAwBGEHAABgCMIOAADAEIQdAACAIQg7AAAAQxB2AAAAhiDsAAAADEHYAQAAGIKwA4AqzJ8/XzabTfv373f3KABwUYQdgMvGuYg6d/Py8lKrVq00dOhQHTp0yN3jwQUlJSV6+umn9fnnn7t7FOBfkpe7BwCA33v22WfVrl07nTlzRl9++aXmz5+vjRs3Kjc3V76+vu4eD1UoKSnRM888I0m6+eab3TsM8C+IsANw2enVq5diYmIkSf/5n/+p5s2ba8qUKVqxYoXuvvtuN08HAJcvXooFcNm76aabJEl///vfndbXrl2rm266Sf7+/goMDFTfvn21a9cupz0HDhzQww8/rGuuuUZ+fn5q1qyZ7rrrrkrfM7djxw7deuut8vPzU1hYmJ5//nlVVFS4NGN+fr6GDRumsLAw+fj4qGXLlurbt+95z/Ppp586Zm7cuLH69OmjHTt2nPd4S5cuVWRkpHx9fRUVFaVly5Zp6NChatu2rWPP/v37ZbPZ9Oqrr2rWrFm64oor1LBhQ912223Ky8uTZVl67rnnFBYWJj8/P/Xt21fHjh0777lcmWno0KFq1KiRDh06pKSkJDVq1EhBQUF67LHHVF5e7pgnKChIkvTMM884XlJ/+umnXfo3BHDpuGIH4LJ3Lo6aNGniWFuzZo169eqlK664Qk8//bR+/vlnzZgxQ927d1dOTo4jgLKzs7Vp0yYNHDhQYWFh2r9/v2bPnq2bb75ZO3fuVMOGDSX9Gma33HKLfvnlF6Wlpcnf319z586Vn5+fSzMmJydrx44dSk1NVdu2bVVYWKiMjAwdPHjQMct7772nlJQUJSYmasqUKSopKdHs2bN144036quvvnLs+9vf/qYBAwaoY8eOSk9P1/HjxzV8+HC1atWq0udeuHChzp49q9TUVB07dkwvv/yy7r77bt166636/PPP9cQTT+j777/XjBkz9Nhjj+mdd95xHOvqTJJUXl6uxMRExcXF6dVXX9WaNWs0depURURE6KGHHlJQUJBmz56thx56SH/+85/Vr18/SdJ1113n0r8hgBpgAcBlYt68eZYka82aNdaRI0esvLw868MPP7SCgoIsHx8fKy8vz7E3OjraatGihfXTTz851r7++mvLw8PDGjJkiGOtpKTkvOfJysqyJFkLFixwrI0ePdqSZG3evNmxVlhYaNntdkuStW/fvgvOffz4cUuS9corr1xwz8mTJ63AwEDrvvvuc1rPz8+37Ha703rHjh2tsLAw6+TJk461zz//3JJktWnTxrG2b98+S5IVFBRknThxwrE+fvx4S5LVqVMnq6yszLE+aNAgy9vb2zpz5ky1Z0pJSbEkWc8++6zT3s6dO1tdunRx/HzkyBFLkjV58uQL/lsAqD28FAvgspOQkKCgoCCFh4erf//+8vf314oVKxQWFiZJ+sc//qHt27dr6NChatq0qeO46667Tn/605+0atUqx9pvr7iVlZXpp59+0pVXXqnAwEDl5OQ47lu1apW6du2q2NhYx1pQUJAGDx580Xn9/Pzk7e2tzz//XMePH690T0ZGhk6cOKFBgwbp6NGjjpunp6fi4uK0bt06SdLhw4f17bffasiQIWrUqJHj+B49eqhjx46VPvZdd90lu93u+DkuLk6SdM8998jLy8tp/ezZs45PGLs60289+OCDTj/fdNNN+uGHHy76bwSgbvBSLIDLzqxZs3T11VerqKhI77zzjr744gv5+Pg47j9w4IAk6Zprrjnv2A4dOuizzz7T6dOn5e/vr59//lnp6emaN2+eDh06JMuyHHuLioqcHvNcEP1WZc/xez4+PpoyZYrGjh2r4OBgde3aVXfccYeGDBmikJAQSdLevXslSbfeemuljxEQEOB0bldeeeV5e6688kqnGD2ndevWTj+fi7zw8PBK18/Fp6sznePr6+t4D905TZo0uWDMAqh7hB2Ay05sbKzjU7FJSUm68cYb9R//8R/as2eP01UsV6SmpmrevHkaPXq04uPjZbfbZbPZNHDgQJc/GOGK0aNH684779Ty5cv12WefaeLEiUpPT9fatWvVuXNnx3O99957jtj7rd9eWasuT0/Paq2fi9vqznShxwNw+SDsAFzWPD09lZ6erltuuUUzZ85UWlqa2rRpI0nas2fPeft3796t5s2by9/fX5L04YcfKiUlRVOnTnXsOXPmjE6cOOF0XJs2bRxXsH6rsue4kIiICI0dO1Zjx47V3r17FR0dralTp+r9999XRESEJKlFixZKSEi44GOcO7fvv//+vPsqW7sUrs5UHTabrUYeB8Afw3vsAFz2br75ZsXGxmratGk6c+aMWrZsqejoaL377rtOgZabm6v/+Z//Ue/evR1rnp6eTi+/StKMGTMcX9FxTu/evfXll19qy5YtjrUjR45o4cKFF52vpKREZ86ccVqLiIhQ48aNVVpaKklKTExUQECAXnzxRZWVlZ33GEeOHJEkhYaGKioqSgsWLNCpU6cc969fv17ffvvtRWepDldnqo5znzL+fTgDqBtcsQNQL4wbN0533XWX5s+frwcffFCvvPKKevXqpfj4eA0fPtzxdSd2u93pe9PuuOMOvffee7Lb7YqMjFRWVpbWrFmjZs2aOT3+448/rvfee0+33367Ro0a5fi6kzZt2uibb76pcrbvvvtOPXv21N13363IyEh5eXlp2bJlKigo0MCBAyX9+n612bNn695779X111+vgQMHKigoSAcPHtTf/vY3de/eXTNnzpQkvfjii+rbt6+6d++uYcOG6fjx45o5c6aioqKcYu9SVWcmV/n5+SkyMlJLlizR1VdfraZNmyoqKkpRUVE1NjeAKrj5U7kA4HDu606ys7PPu6+8vNyKiIiwIiIirF9++cWyLMtas2aN1b17d8vPz88KCAiw7rzzTmvnzp1Oxx0/ftwaNmyY1bx5c6tRo0ZWYmKitXv3bqtNmzZWSkqK095vvvnG6tGjh+Xr62u1atXKeu6556y33377ol93cvToUWvEiBFW+/btLX9/f8tut1txcXHWBx98cN7edevWWYmJiZbdbrd8fX2tiIgIa+jQodbWrVud9i1evNhq37695ePjY0VFRVkrVqywkpOTrfbt2zv2nPu6k99/zcq6dessSdbSpUtd+vd1ZaaUlBTL39//vPOZPHmy9fv/Ktm0aZPVpUsXy9vbm68+AeqYzbJ+9xoFAOCyFB0draCgIGVkZLh7FACXKd5jBwCXmbKyMv3y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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "curvature = np.zeros(10)\n", + "x, y = integrate_road(curvature)\n", + "plot_road(x, y)" + ] + }, + { + "cell_type": "markdown", + "id": "ad3897f0", + "metadata": {}, + "source": [ + "#### Instant turn\n", + "\n", + "We could add a turn by setting $\\kappa \\neq 0$ somewhere along the curvature." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7a1aceb1", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARJRJREFUeJzt3Xt8jvXjx/H3DnYwNqeZsMzhWxKZDKGSWq1SX+dTNJZUQrQUOlAqI4cUi/g6lEPk3OmrH4tKFJFEpZRTtDlvGBu7r98f19dqGTbb7s+9+349H4/70ce167rv96x4d13X53N5WZZlCQAAAMWet+kAAAAAKBwUOwAAADdBsQMAAHATFDsAAAA3QbEDAABwExQ7AAAAN0GxAwAAcBMUOwAAADdBsQMAAHATFDsAHmnWrFny8vLS7t27TUcBgEJDsQNQ5M6XqPMvX19fValSRT179tT+/ftNx0MepKen68UXX9SaNWtMRwFwCb6mAwDwHCNGjFD16tV15swZff3115o1a5bWrl2rbdu2KSAgwHQ8XEJ6erpeeuklSdJtt91mNgyAi6LYAXCae+65R1FRUZKkhx9+WBUqVNDo0aP1wQcfqFOnTobTAUDxx6VYAMbccsstkqTffvstx/bPPvtMt9xyi4KCglSmTBm1bt1aP/30U4599uzZo8cff1zXXnutAgMDVb58eXXs2DHXe+a2b9+u22+/XYGBgapatapeeeUVORyOPGVMTk5WXFycqlatKn9/f1111VVq3br1BZ/z3//+Nztz6dKl1apVK23fvv2C91u4cKHq1KmjgIAA1a1bV0uXLlXPnj0VERGRvc/u3bvl5eWlsWPHKjExUTVq1FDJkiV11113ad++fbIsSy+//LKqVq2qwMBAtW7dWkePHr3gs/KSqWfPnipVqpT279+vNm3aqFSpUgoNDdWgQYOUlZWVnSc0NFSS9NJLL2VfUn/xxRfz9HsIwHk4YwfAmPPlqGzZstnbVq1apXvuuUc1atTQiy++qNOnT2vixIlq3ry5Nm/enF2ANm7cqHXr1qlLly6qWrWqdu/ercmTJ+u2227Tjz/+qJIlS0qyi1nLli117tw5DRkyREFBQZo6daoCAwPzlLF9+/bavn27+vfvr4iICB08eFArV67U3r17s7PMnj1bPXr0UExMjEaPHq309HRNnjxZN998s7777rvs/T7++GN17txZ9erVU0JCgo4dO6ZevXqpSpUquX723LlzlZmZqf79++vo0aN67bXX1KlTJ91+++1as2aNBg8erJ07d2rixIkaNGiQZsyYkX1sXjNJUlZWlmJiYtSkSRONHTtWq1at0rhx41SzZk316dNHoaGhmjx5svr06aO2bduqXbt2kqQbbrghT7+HAJzIAoAiNnPmTEuStWrVKuvQoUPWvn37rEWLFlmhoaGWv7+/tW/fvux9IyMjrYoVK1pHjhzJ3vb9999b3t7eVmxsbPa29PT0Cz5n/fr1liTr3Xffzd42cOBAS5L1zTffZG87ePCgFRISYkmydu3addHcx44dsyRZY8aMueg+J06csMqUKWP17t07x/bk5GQrJCQkx/Z69epZVatWtU6cOJG9bc2aNZYkq1q1atnbdu3aZUmyQkNDrePHj2dvHzp0qCXJql+/vnX27Nns7V27drX8/PysM2fO5DtTjx49LEnWiBEjcuzboEEDq2HDhtm/PnTokCXJGj58+EV/LwCYx6VYAE4THR2t0NBQhYeHq0OHDgoKCtIHH3ygqlWrSpL+/PNPbdmyRT179lS5cuWyj7vhhht055136pNPPsne9vczbmfPntWRI0dUq1YtlSlTRps3b87+2ieffKKbbrpJjRs3zt4WGhqqbt26XTZvYGCg/Pz8tGbNGh07dizXfVauXKnjx4+ra9euOnz4cPbLx8dHTZo00erVqyVJBw4c0A8//KDY2FiVKlUq+/gWLVqoXr16ub53x44dFRISkv3rJk2aSJK6d+8uX1/fHNszMzOzZxjnNdPfPfbYYzl+fcstt+j333+/7O8RANfCpVgATpOYmKhrrrlGqampmjFjhr744gv5+/tnf33Pnj2SpGuvvfaCY6+77jp9+umnOnXqlIKCgnT69GklJCRo5syZ2r9/vyzLyt43NTU1x3ueL0R/l9tn/JO/v79Gjx6tp556SmFhYbrpppt03333KTY2VpUqVZIk/frrr5Kk22+/Pdf3CA4OzvG91apV64J9atWqlaOMnnf11Vfn+PX5khceHp7r9vPlM6+ZzgsICMi+h+68smXLXrTMAnBdFDsATtO4cePsWbFt2rTRzTffrAceeEA7duzIcRYrL/r376+ZM2dq4MCBatq0qUJCQuTl5aUuXbrkeWJEXgwcOFD333+/li1bpk8//VQv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+vrK19dXn3/+ud588035+voqKyvLdERcxlVXXaU6derk2Hbddddp7969hhIhv55++mkNGTJEXbp0Ub169fTggw/qySefVEJCgulouEKVKlWSJKWkpOTYnpKSkv01Z6DYIV8sy1K/fv20dOlSffbZZ6pevbrpSMinO+64Qz/88IO2bNmS/YqKilK3bt20ZcsW+fj4mI6Iy2jevPkFywz98ssvqlatmqFEyK/09HR5e+f8K9jHx0cOh8NQIhRU9erVValSJSUlJWVvS0tL0zfffKOmTZs6LQeXYpEvffv21bx587R8+XKVLl06+76BkJAQBQYGGk6HvChduvQF90QGBQWpfPny3CtZTDz55JNq1qyZRo4cqU6dOmnDhg2aOnWqpk6dajoa8uj+++/Xq6++qquvvlrXX3+9vvvuO40fP14PPfSQ6Wi4hJMnT2rnzp3Zv961a5e2bNmicuXK6eqrr9bAgQP1yiuv6F//+peqV6+uF154QZUrV86eOesUFpAPknJ9zZw503Q0FECLFi2sAQMGmI6BfPjwww+tunXrWv7+/lbt2rWtqVOnmo6EfEhLS7MGDBhgXX311VZAQIBVo0YN67nnnrMyMjJMR8MlrF69Ote/A3v06GFZlmU5HA7rhRdesMLCwix/f3/rjjvusHbs2OHUjKxjBwAA4Ca4xw4AAMBNUOwAAADcBMUOAADATVDsAAAA3ATFDgAAwE1Q7AAAANwExQ4AAMBNUOwAAADcBMUOAApox44dqlSpkk6cOFGg97npppu0ePHiQkoFwBNR7AB4vKysLDVr1kzt2rXLsT01NVXh4eF67rnnLnn80KFD1b9/f5UuXbpAOZ5//nkNGTKEB8EDuGI8UgwAJP3yyy+KjIzUtGnT1K1bN0lSbGysvv/+e23cuFF+fn65Hrd3717VqlVLu3btUpUqVQqUISsrS1WqVNH06dPVqlWrAr0XAM/EGTsAkHTNNddo1KhR6t+/v/78808tX75c8+fP17vvvnvRUidJ77//vurXr5+j1M2aNUtlypTRRx99pGuvvVYlS5ZUhw4dlJ6ernfeeUcREREqW7asnnjiCWVlZWUf5+Pjo3vvvVfz588v0u8VgPvyNR0AAFxF//79tXTpUj344IP64YcfNGzYMNWvX/+Sx3z55ZeKioq6YHt6errefPNNzZ8/XydOnFC7du3Utm1blSlTRp988ol+//13tW/fXs2bN1fnzp2zj2vcuLFGjRpV6N8bAM9AsQOA//Hy8tLkyZN13XXXqV69ehoyZMhlj9mzZ0+uxe7s2bOaPHmyatasKUnq0KGDZs+erZSUFJUqVUp16tRRy5YttXr16hzFrnLlytq3b58cDoe8vbmoAiB/+FMDAP5mxowZKlmypHbt2qU//vjjsvufPn1aAQEBF2wvWbJkdqmTpLCwMEVERKhUqVI5th08eDDHcYGBgXI4HMrIyCjAdwHAU1HsAOB/1q1bp9dff10fffSRGjdurF69euly88sqVKigY8eOXbC9RIkSOX7t5eWV67Z/zoA9evSogoKCFBgYeIXfBQBPRrEDANn3xPXs2VN9+vRRy5YtNX36dG3YsEFTpky55HENGjTQjz/+WGg5tm3bpgYNGhTa+wHwLBQ7AJC9Fp1lWdkTFyIiIjR27Fg988wz2r1790WPi4mJ0fr163PMbi2IL7/8UnfddVehvBcAz0OxA+DxPv/8cyUmJmrmzJkqWbJk9vZHH31UzZo1u+Ql2XvuuUe+vr5atWpVgXPs379f69atU1xcXIHfC4BnYoFiACigxMREffDBB/r0008L9D6DBw/WsWPHNHXq1EJKBsDTsNwJABTQo48+quPHj+vEiRMFeqxYxYoVFR8fX4jJAHgaztgBAAC4Ce6xAwAAcBMUOwAAADdBsQMAAHATFDsAAAA3QbEDAABwExQ7AAAAN0GxAwAAcBMUOwAAADdBsQMAAHATFDsAAAA38f+O36fitjF57AAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "\n", + "curvature[4] = 0.1\n", + "x, y = integrate_road(curvature)\n", + "plot_road(x, y)" + ] + }, + { + "cell_type": "markdown", + "id": "3c04dad9", + "metadata": {}, + "source": [ + "#### Smooth turn\n", + "\n", + "A smooth turn would look like curvature being constant for a few steps." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "80696363", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAnYAAAHWCAYAAAD6oMSKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAASlJJREFUeJzt3X18T3Xjx/H3Nmxzs6GxGcvclJvcTG6WdG8ZqchNuLoau7qVZI3YFEvKEF1iu0hXIlcuSnGVq/Rj4Uott0kUqYhocxMbG8N2fn+cfLVsbGz7fL/fvZ6Px/fhs7PzPd/32Yr345zzOcfDsixLAAAAcHmepgMAAACgZFDsAAAA3ATFDgAAwE1Q7AAAANwExQ4AAMBNUOwAAADcBMUOAADATVDsAAAA3ATFDgAAwE1Q7ABA0ty5c+Xh4aE9e/aYjgIAl41iB6DMnStR514VKlRQ3bp1NWjQIO3fv990PBRBdna2nn/+ea1evdp0FAB/UMF0AADl1wsvvKAGDRro1KlT+vLLLzV37lytXbtW27Ztk4+Pj+l4uIjs7GyNGzdOknTbbbeZDQPAgWIHwJhu3bqpXbt2kqSHH35YAQEBmjRpkj744APdf//9htMBgOvhVCwAp3HzzTdLkn788cd8yz/99FPdfPPNqlKliqpXr64ePXrou+++y7fOzz//rCeeeEJNmjSRr6+vrrrqKvXt27fAa+a2b9+uO+64Q76+vqpXr55efPFF5eXlFSljWlqaoqOjVa9ePXl7e6tOnTrq0aPHBZ/z8ccfOzJXq1ZN3bt31/bt2y/Y3rvvvqvmzZvLx8dHLVq00JIlSzRo0CCFhoY61tmzZ488PDw0ZcoUJScnq2HDhqpcubK6dOmiffv2ybIsjR8/XvXq1ZOvr6969Oih33777YLPKkqmQYMGqWrVqtq/f7969uypqlWrqlatWhoxYoRyc3MdeWrVqiVJGjdunOOU+vPPP1+knyGA0sMROwBO41w5qlGjhmPZypUr1a1bNzVs2FDPP/+8Tp48qRkzZqhTp07avHmzowBt2LBBX3zxhfr376969eppz549mjlzpm677TZ9++23qly5siS7mN1+++06e/as4uLiVKVKFc2ePVu+vr5Fyti7d29t375dQ4cOVWhoqA4ePKgVK1Zo7969jizz58/XwIEDFRkZqUmTJik7O1szZ87UTTfdpK+++sqx3n//+1/169dPLVu2VGJioo4ePaqHHnpIdevWLfCz3377bZ0+fVpDhw7Vb7/9psmTJ+v+++/XHXfcodWrV2vUqFH64YcfNGPGDI0YMUJz5sxxvLeomSQpNzdXkZGRCg8P15QpU7Ry5UpNnTpVjRo10uDBg1WrVi3NnDlTgwcP1n333adevXpJklq1alWknyGAUmQBQBl78803LUnWypUrrUOHDln79u2zFi9ebNWqVcvy9va29u3b51g3LCzMql27tnXkyBHHsq+//try9PS0oqKiHMuys7Mv+JzU1FRLkvXWW285lsXExFiSrHXr1jmWHTx40PL397ckWbt37y4099GjRy1J1ssvv1zoOsePH7eqV69uPfLII/mWp6WlWf7+/vmWt2zZ0qpXr551/Phxx7LVq1dbkqz69es7lu3evduSZNWqVcs6duyYY3l8fLwlyWrdurV15swZx/IBAwZYlSpVsk6dOlXsTAMHDrQkWS+88EK+ddu0aWO1bdvW8fWhQ4csSVZCQkKhPwsAZY9TsQCMiYiIUK1atRQSEqI+ffqoSpUq+uCDD1SvXj1J0q+//qotW7Zo0KBBqlmzpuN9rVq10p133qmPPvrIseyPR9zOnDmjI0eOqHHjxqpevbo2b97s+N5HH32kG264QR06dHAsq1Wrlh544IFL5vX19VWlSpW0evVqHT16tMB1VqxYoWPHjmnAgAE6fPiw4+Xl5aXw8HCtWrVKknTgwAF98803ioqKUtWqVR3vv/XWW9WyZcsCt923b1/5+/s7vg4PD5ck/fWvf1WFChXyLT99+rRjhnFRM/3R448/nu/rm2++WT/99NMlf0YAzOJULABjkpOTde211yojI0Nz5szR//73P3l7ezu+//PPP0uSmjRpcsF7mzVrpk8++URZWVmqUqWKTp48qcTERL355pvav3+/LMtyrJuRkZFvm+cK0R8V9Bl/5u3trUmTJmn48OEKDAzUDTfcoLvvvltRUVEKCgqSJO3atUuSdMcddxS4DT8/v3z71rhx4wvWady4cb4yes7VV1+d7+tzJS8kJKTA5efKZ1EznePj4+O4hu6cGjVqFFpmATgPih0AYzp06OCYFduzZ0/ddNNN+stf/qKdO3fmO4pVFEOHDtWbb76pmJgYdezYUf7+/vLw8FD//v2LPDGiKGJiYnTPPfdo6dKl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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "curvature = np.zeros(10)\n", + "curvature[4:] = 0.02\n", + "x, y = integrate_road(curvature)\n", + "plot_road(x, y)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/explainers/prefab_roads.ipynb b/docs/explainers/prefab_roads.ipynb new file mode 100644 index 0000000..ffdb162 --- /dev/null +++ b/docs/explainers/prefab_roads.ipynb @@ -0,0 +1,118 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1a063d05", + "metadata": {}, + "source": [ + "# Pseudo-ramdom procedural roads\n", + "\n", + "Suppose one wishes to describe a road between A and B in terms of segments\n", + "\n", + "$$\n", + "\\text{straight, turn left, straight, turn right, straight}\n", + "$$\n", + "\n", + "Let's see how we can create a random road of length $L$ from a pre-determined set of prefabs.\n", + "\n", + "#### Random apportionment of total length\n", + "\n", + "Suppose we want to build a road of length $L$ out of $K$ segments. The total number of waypoints $N$ depends on the step size $\\Delta s$:\n", + "\n", + "$$\n", + "N = \\frac{L}{\\Delta s}.\n", + "$$\n", + "\n", + "Let $\\left( p_1, p_2, \\dots, p_K \\right)$ represent the proportion of $N$ that each prefab will be assigned, where $\\sum p_i = 1$. One useful distribution here is the [Dirichlet distribution](https://en.wikipedia.org/wiki/Dirichlet_distribution), which is parametrized by a vector $\\mathbf{\\alpha} = \\left(\\alpha_1, \\alpha_2, \\dots, \\alpha_K \\right)$. The special case where all $\\alpha_i$, the scalar parameter $\\alpha$ is called a *concentration parameter*. Setting the same $\\alpha$ across the entire parameter space makes the distribution symmetric, meaning no prior assumptions are made regarding the proportion of $N$ that will be assigned to each segment. $\\alpha = 1$ leads to what is known as a flat Dirichlet distribution, whereas higher values lead to more dense and evenly distributed $\\left( p_1, p_2, \\dots, p_K \\right)$. On the other hand, keeping $\\alpha \\leq 1$ gives a sparser distribution which can lead to larger variance in apportioned number of waypoints to $\\left( p_1, p_2, \\dots, p_K \\right)$.\n", + "\n", + "#### Expectation value and variance of Dirichlet distribution\n", + "\n", + "Suppose we draw our samples for proportion of length from the Dirichlet distribution\n", + "\n", + "$$\n", + "(p_1, p_2, \\ldots, p_K) \\sim \\text{Dirichlet}(\\alpha, \\alpha, \\ldots, \\alpha)\n", + "$$\n", + "\n", + "with $\\alpha _{0}=\\sum _{i=1}^{K}\\alpha _{i}$, the mean and variance are then\n", + "\n", + "$$\n", + "\\operatorname {E} [p_{i}]={\\frac {\\alpha _{i}}{\\alpha _{0}}}, \\; \\operatorname {Var} [p_{i}]={\\frac {\\alpha _{i}(\\alpha _{0}-\\alpha _{i})}{\\alpha _{0}^{2}(\\alpha _{0}+1)}}.\n", + "$$\n", + "\n", + "If $\\alpha$ is a scalar, then $\\alpha _{0}= K \\alpha$ and the above simplifies to\n", + "\n", + "$$\n", + "\\operatorname {E} [p_{i}]={\\frac {\\alpha}{K \\alpha}}={\\frac {1}{K}}, \\; \\operatorname {Var} [p_{i}]={\\frac {\\alpha(K \\alpha -\\alpha)}{(K \\alpha)^{2}(K \\alpha +1)}}.\n", + "$$\n", + "\n", + "We see that $\\operatorname {Var} [p_{i}] \\propto \\frac{1}{\\alpha}$ meaning that the variance reduces with increasing $\\alpha$. We can simply scale the proportions\n", + "\n", + "$$\n", + "(N \\cdot p_1, N \\cdot p_2, \\ldots, N \\cdot p_K)\n", + "$$\n", + "\n", + "to get the randomly assigned number of waypoints for each prefab. We now have a distribution which can give randomly assigned lengths to a given list of prefabs, with a parameter to control the degree of randomness. With a large concentration parameter $\\alpha$, the distribution of lengths will be more uniform, with each prefab getting $N \\cdot \\operatorname {E} [p_{i}]={\\frac {N}{K}}$ waypoints assigned to it. Likewise, keeping $\\alpha$ low increases variance and allows for a more random assignment of proportions of waypoints to each prefab segment.\n", + "\n", + "#### Random angles\n", + "\n", + "Suppose a turn of a pre-defined arc length $l$ made of $N/K$ waypoints. If one wants to create a random angle, one has to keep in mind that the minimum radius $R_{min}$ depends on the speed of the vehicle and the weather conditions:\n", + "\n", + "$$\n", + "R_{\\text{min,vehicle}} = \\frac{v^2}{g\\mu},\n", + "$$\n", + "\n", + "where\n", + "- $v$ is the velocity of the vehicle in $\\text{m/s}$,\n", + "- $g$ is the gravitational acceleration (about $9.8$ $\\text{m/s}^{2}$), and\n", + "- $\\mu$ is the friction coefficient (about $0.7$ for dry asphalt).\n", + "\n", + "A regular turn (not a U-turn or roundabout) should also have an lower and upper limit on the angle, say, 30 degrees to 90 degrees for a conservative estimate. In terms of radii, it becomes\n", + "\n", + "$$\n", + "R_{\\text{min}} = \\max\\left(R_{\\text{min,vehicle}}, \\frac{l}{\\pi/2}\\right)\n", + "$$\n", + "\n", + "and\n", + "\n", + "$$\n", + "R_{\\text{max}} = \\frac{l}{\\pi/6}.\n", + "$$\n", + "\n", + "We then sample\n", + "\n", + "$$\n", + "R \\sim \\text{Uniform}\\left(R_{\\text{min}}, R_{\\text{max\\_angle}}\\right)\n", + "$$\n", + "\n", + "and obtain a random radius for a turn of arc length $l$ with limits to ensure the radius is large enough given the velocity of the vehicle. Finally, the curvature profile is related to the radius by\n", + "\n", + "$$\n", + "\\kappa = \\frac{1}{R}\n", + "$$\n", + "\n", + "which means that the curvature profile of a turn is simply a vector $\\mathbf{\\kappa} = (1/R, \\dots, 1/R)$ with a length of $N/K$ waypoints." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/index.md b/docs/index.md index ca4d696..bac1eae 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,29 +2,17 @@ Primary Gamma RADiation Landscapes (PG-RAD) is a Python package for research in source localization. It can simulate mobile gamma spectrometry data acquired from vehicle-borne detectors along a predefined path (e.g. a road). -## Requirements +## About -PG-RAD requires Python `3.12`. The guides below assume a unix-like system. +This software has been developed as part of dissertation work for the degree of master of Computational Science and Physics at Lund University, Sweden. The work has been done at the department of Medical Radiation Physics (MSF), Faculty of Medicine. The radiological emergency preparedness research group of MSF is assigned by the Swedish Radiation Safety Authority (SSM) to aid in preparation for effective mitigation of radiological or nuclear disasters on Swedish soil. -## Installation (CLI) +## Value proposition - +PG-RAD is a toolbox that allows for simulation of detector response for a wide variety of source localization scenarios. The strength of the software lies in its simple and minimal configuration and user input, while its flexibility allows for reconstruction of specific scenarios with relative ease. PG-RAD is also general enough that novel methods such as UAV-borne detectors can be simulated and evaluated. -Lorem ipsum +User input takes the form of an input file (YAML), describing the path, detector and source(s), and optional parameters. The output of the program is visualizations of the world (the path and sources), as well as the detector count rate as a function of distance travelled along the path. -## Installation (Python module) - -If you are interested in using PG-RAD in another Python project, create a virtual environment first: - -``` -python3 -m venv .venv -``` - -Then install PG-RAD in it: - -``` -source .venv/bin/activate -(.venv) pip install git+https://github.com/pim-n/pg-rad +Users can provide experimental / geographical coordinates representing real roads. Alternatively, users can let PG-RAD generate a procedural road, where the user can easily control what that road should look like. The user can specify a single point source, several point sources, as well as a field of radioactive material covering a large area. ``` See how to get started with PG-RAD with your own Python code [here](pg-rad-in-python). diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..89818c5 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,47 @@ +## Requirements + +PG-RAD requires Python `>=3.12.4` and `<3.13`. It has been tested on `3.12.9`. The guides below assume a unix-like system. You can check the Python version you have installed as follows: + +``` +python --version +``` + +If you don't have the right version installed there are various ways to get a compatible version, such as [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation). + +## Installation (CLI) + + + +Lorem ipsum + +## Installation (Python module) + +If you are interested in using PG-RAD in another Python project, create a virtual environment first: + +``` +python -m venv .venv +``` + +Then install PG-RAD in it: + +``` +source .venv/bin/activate +(.venv) pip install git+https://github.com/pim-n/pg-rad +``` + +See how to get started with PG-RAD with your own Python code [here](pg-rad-in-python). + +## For developers +``` +git clone https://github.com/pim-n/pg-rad +cd pg-rad +git checkout dev +``` + +or + +``` +git@github.com:pim-n/pg-rad.git +cd pg-rad +git checkout dev +``` \ No newline at end of file diff --git a/docs/pg-rad-in-cli.md b/docs/pg-rad-in-cli.md deleted file mode 100644 index fedb76c..0000000 --- a/docs/pg-rad-in-cli.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Using PG-RAD in CLI ---- -Lorem ipsum. \ No newline at end of file diff --git a/docs/pg-rad-in-python.ipynb b/docs/pg-rad-in-python.ipynb index 560ba4f..cefc069 100644 --- a/docs/pg-rad-in-python.ipynb +++ b/docs/pg-rad-in-python.ipynb @@ -5,247 +5,10 @@ "id": "5e30f59a", "metadata": {}, "source": [ - "# Using PG-RAD as a module\n", + "# The design of PG-RAD\n", "\n", - "This discusses the overall design of the code to understand how different parts interact, doing so by an interactive notebook demo. For specific usage of functions and classes, consult the API documentation in the side bar (relevant API documentation sections will also be hyperlinked in the below explanation)." + "This discusses the overall design of the code by using an interactive notebook demo." ] - }, - { - "cell_type": "markdown", - "id": "f18912e5", - "metadata": {}, - "source": [ - "## Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "415fdd25", - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt\n", - "\n", - "from pg_rad.dataloader import load_data\n", - "from pg_rad.path import path_from_RT90\n", - "from pg_rad.landscape import Landscape\n", - "from pg_rad.sources import PointSource\n", - "\n", - "from pg_rad.isotope import Isotope\n", - "from pg_rad.logger import setup_logger\n", - "\n", - "setup_logger(log_level = \"INFO\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "5a0e470a", - "metadata": {}, - "outputs": [], - "source": [ - "FILENAME = \"B10_NaIR_MGS_ROI_CPS_IPL.CSV\"\n", - "df = load_data(FILENAME)" - ] - }, - { - "cell_type": "markdown", - "id": "baa7aba8", - "metadata": {}, - "source": [ - "## Demo: Path regression" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "2ec97553", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2026-01-28 15:53:22,182 - INFO: Piecewise regression reduced path from 105 to 4 segments.\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "p1 = path_from_RT90(df, east_col = \"East\", north_col = \"North\", path_simplify = False)\n", - "p2 = path_from_RT90(df, east_col = \"East\", north_col = \"North\", path_simplify = True)\n", - "\n", - "p1.plot(color='r', linestyle='-', linewidth = 10, label = \"Full path\")\n", - "p2.plot(color='b', linestyle='-', marker = 'o', label = \"Reduced path\")\n", - "\n", - "plt.xlabel(\"X [m]\")\n", - "plt.ylabel(\"Y [m]\")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "da8620fa", - "metadata": {}, - "source": [ - "## Landscape\n", - "\n", - "The [Landscape](/API/landscape/landscape) is the starting point for the model. It is essentially an empty data structure which represents a cuboid space\n", - "\n", - "$$\n", - "C = [0, x_{\\max}] \\times [0, y_{\\max}] \\times [0, z_{\\max}] \\subset \\mathbb{R}^3\n", - "$$\n", - "\n", - "where $x_{\\max}, y_{\\max}, z_{\\max}$ are defined by the user upon creating the landscape object." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "24f1159d", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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LKs9A++44w7t2bNHzzzzjKZPnx6QYr744gslJSWpuLhYf/d3fyev16vExEStW7dO99xzjyTp8OHDGj58uEpKSjR27Fht3rxZd911l06cOCGn0ylJWrNmjRYtWqQvvvhCUVFR33rc+vp6ORwOeb1e2e32gMwNAAB0rkD+/W73R2wtLS06cOBAwMKRJHm9XklSQkKCJKm0tFTNzc3KyMjwjRk2bJhSUlJUUlIiSSopKdGIESN84UiSMjMzVV9fr4MHD170OI2Njaqvr/d7AQAAtGl3QCoqKlL//v0DVkhra6vmzZuncePG6ZprrpEkeTweRUVFKT4+3m+s0+mUx+PxjfnrcNTW39Z3MQUFBXI4HL7XgAEDOnk2AACgO+syXzWSm5urjz76SOvXrw/4sfLz8+X1en2vqqqqgB8TAAB0Hx26SDtQ5syZo02bNmnnzp1+Z6lcLpeamppUV1fndxappqZGLpfLN2bPnj1++2u7y61tjCk6OlrR0dGdPAsAANBThPQMkmVZmjNnjjZu3Kjt27dr0KBBfv1jxoxRZGSktm3b5ms7cuSIKisr5Xa7JUlut1vl5eWqra31jSkqKpLdbldaWlpwJgIAAHqUkJ5Bys3N1bp16/Tmm2+qT58+vmuGHA6HevfuLYfDoZkzZyovL08JCQmy2+2aO3eu3G63xo4dK0maMGGC0tLSdN9992nlypXyeDxavHixcnNzOUsEAAC+k3bf5h+Qg9tsF21fu3atZsyYIenrB0UuWLBAr776qhobG5WZmannnnvO7+OziooKzZ49Wzt27FBsbKxycnK0YsWKC57y/U24zR8AgO4nkH+/QxqQugoCEgAA3U+XeA4SAADApYKABAAAYCAgAQAAGAhIAAAABgISAACAgYAEAABgICABAAAYCEgAAAAGAhIAAICBgAQAAGAgIAEAABgISAAAAAYCEgAAgIGABAAAYCAgAQAAGAhIAAAABgISAACAgYAEAABgICABAAAYCEgAAAAGAhIAAICBgAQAAGAgIAEAABgISAAAAAYCEgAAgIGABAAAYCAgAQAAGAhIAAAABgISAACAgYAEAABgICABAAAYCEgAAAAGAhIAAICBgAQAAGAgIAEAABgISAAAAAYCEgAAgIGABAAAYCAgAQAAGAhIAAAABgISAACAgYAEAABgICABAAAYCEgAAAAGAhIAAICB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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "my_landscape = Landscape(size = (500, 500, 500), scale = 'meters')\n", - "fig, ax = my_landscape.plot()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "a285a2b6", - "metadata": {}, - "source": [ - "Not much to see here, but we created the backbone of our simulation now." - ] - }, - { - "cell_type": "markdown", - "id": "035b4f42", - "metadata": {}, - "source": [ - "## Point sources\n", - "\n", - "A [PointSource](/API/sources/point_source) can be added to `my_landscape` in the following way:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "91019da5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[PointSource(name=Source 2, pos=(100, 100, 0), isotope=Cs137, A=100 MBq)]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "cs137 = Isotope(name = \"Cs137\", E = 662.66, b = 0.851)\n", - "my_source = PointSource(x = 100, y = 100, z = 0, activity = 100, isotope = cs137)\n", - "my_landscape.add_sources(my_source)\n", - "\n", - "my_landscape.sources" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "7913fe1e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(
,\n", - " )" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAG2CAYAAACEbnlbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMEJJREFUeJzt3Xt8FPW9//H35gq57MZgLiAEqMolCoioYaWWKpGAoUKJLfXwkEs5ijRBuUhpLIJwqqH0qEiL4qlHgo+KWHpEKxYkBYkVIpdgSgRBsJigZBOFXzZcJAnJ9/fHNqs7oCaazSbh9Xw85iE7852Zz+Qr5u13vjNrM8YYAQAAwCso0AUAAAC0NgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAACLgAakhx9+WDabzWfp06ePd/vZs2eVmZmpTp06KSoqShkZGSovL/c5RmlpqdLT0xUREaH4+HjNmTNH586da+lLAQAA7UhIoAu46qqr9Pe//937OSTki5Jmzpyp119/XWvXrpXD4VBWVpbGjh2rbdu2SZLq6uqUnp6uxMREbd++XWVlZZowYYJCQ0P16KOPtvi1AACA9sEWyC+rffjhh/XKK6+oqKjovG1ut1txcXFavXq17rjjDknSgQMH1LdvXxUUFGjw4MHasGGDRo0apWPHjikhIUGStGLFCs2dO1effvqpwsLCWvJyAABAOxHwEaRDhw6pS5cu6tChg5xOp3JycpSUlKTCwkLV1tYqNTXV27ZPnz5KSkryBqSCggL169fPG44kKS0tTdOmTdO+ffs0cODAC56zurpa1dXV3s/19fU6ceKEOnXqJJvN5r+LBQAAzcYYo5MnT6pLly4KCmreWUMBDUgpKSnKzc1V7969VVZWpoULF+qmm27Se++9J5fLpbCwMMXExPjsk5CQIJfLJUlyuVw+4ahhe8O2r5KTk6OFCxc278UAAICAOHr0qLp27dqsxwxoQBo5cqT3z/3791dKSoq6d++uP//5z+rYsaPfzpudna1Zs2Z5P7vdbiUlJeno0aOy2+1+Oy8AAGg+VVVV6tatm6Kjo5v92AG/xfZlMTEx6tWrlw4fPqxbb71VNTU1qqys9BlFKi8vV2JioiQpMTFRO3fu9DlGw1NuDW0uJDw8XOHh4eett9vtBCQAANoYf0yPaVXvQTp16pQ+/PBDde7cWYMGDVJoaKg2b97s3X7w4EGVlpbK6XRKkpxOp4qLi1VRUeFtk5eXJ7vdruTk5BavHwAAtA8BHUF64IEH9KMf/Ujdu3fXsWPHtGDBAgUHB+vOO++Uw+HQlClTNGvWLMXGxsput2v69OlyOp0aPHiwJGn48OFKTk7WXXfdpSVLlsjlcmnevHnKzMy84AgRAABAYwQ0IH388ce68847dfz4ccXFxen73/++3nnnHcXFxUmSnnjiCQUFBSkjI0PV1dVKS0vTU0895d0/ODhY69ev17Rp0+R0OhUZGamJEydq0aJFgbokAADQDgT0PUitRVVVlRwOh9xuN3OQAABoI/z5+7tVzUECAABoDQhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFgQkAAAACwISAAAABYEJAAAAAsCEgAAgAUBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFgQkAAAACwISAAAABYEJAAAAAsCEgAAgAUBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALBoNQFp8eLFstlsmjFjhnfd2bNnlZmZqU6dOikqKkoZGRkqLy/32a+0tFTp6emKiIhQfHy85syZo3PnzrVw9QAAoD1pFQFp165deuaZZ9S/f3+f9TNnztRrr72mtWvXKj8/X8eOHdPYsWO92+vq6pSenq6amhpt375dq1atUm5urubPn9/SlwAAANqRgAekU6dOafz48frjH/+oSy65xLve7Xbrf//3f/X444/rlltu0aBBg7Ry5Upt375d77zzjiRp06ZN2r9/v/70pz/pmmuu0ciRI/Vf//VfWr58uWpqagJ1SQAAoI0LeEDKzMxUenq6UlNTfdYXFhaqtrbW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X40YMUJ33323VqxYodraWmVlZelnP/tZo59gQ4DZbNK4cZ7lxAnp4489o0aXXy6Fhwe6OgDARSqgAWn37t26+eabvZ9n/fstxBMnTlRubq5++ctf6vTp07rnnntUWVmp73//+9q4caP3HUiS9MILLygrK0vDhg1TUFCQMjIytGzZsha/FjSD2FhupQEAWoVW8x6kQOI9SAAAtD0X5XuQAAAAAoWABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFgQkAAAACwISAAAABYEJAAAAAsCEgAAgAUBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMCCgAQAAGBBQAIAALAgIAEAAFgQkAAAACwISAAAABYEJAAAAAsCEgAAgAUBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISAACABQEJAADAgoAEAABgQUACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAACLdhOQli9frh49eqhDhw5KSUnRzp07A10SAABoo9pFQHrppZc0a9YsLViwQHv27NGAAQOUlpamioqKQJcGAADaoHYRkB5//HHdfffdmjx5spKTk7VixQpFREToueeeC3RpAACgDQoJdAHfVU1NjQoLC5Wdne1dFxQUpNTUVBUUFFxwn+rqalVXV3s/u91uSVJVVZV/iwUAAM2m4fe2MabZj93mA9Jnn32muro6JSQk+KxPSEjQgQMHLrhPTk6OFi5ceN76bt26+aVGAADgP8ePH5fD4WjWY7b5gPRtZGdna9asWd7PlZWV6t69u0pLS5v9B4ymqaqqUrdu3XT06FHZ7fZAl3NRoy9aD/qi9aAvWhe3262kpCTFxsY2+7HbfEC69NJLFRwcrPLycp/15eXlSkxMvOA+4eHhCg8PP2+9w+HgX/hWwm630xetBH3RetAXrQd90boEBTX/lOo2P0k7LCxMgwYN0ubNm73r6uvrtXnzZjmdzgBWBgAA2qo2P4IkSbNmzdLEiRN13XXX6YYbbtDSpUt1+vRpTZ48OdClAQCANqhdBKRx48bp008/1fz58+VyuXTNNddo48aN503c/irh4eFasGDBBW+7oWXRF60HfdF60BetB33RuvizP2zGH8/GAQAAtGFtfg4SAABAcyMgAQAAWBCQAAAALAhIAAAAFhd9QFq+fLl69OihDh06KCUlRTt37gx0Se3OW2+9pR/96Efq0qWLbDabXnnlFZ/txhjNnz9fnTt3VseOHZWamqpDhw75tDlx4oTGjx8vu92umJgYTZkyRadOnWrBq2gfcnJydP311ys6Olrx8fEaM2aMDh486NPm7NmzyszMVKdOnRQVFaWMjIzzXsRaWlqq9PR0RUREKD4+XnPmzNG5c+da8lLavKefflr9+/f3vnDQ6XRqw4YN3u30Q+AsXrxYNptNM2bM8K6jP1rOww8/LJvN5rP06dPHu72l+uKiDkgvvfSSZs2apQULFmjPnj0aMGCA0tLSVFFREejS2pXTp09rwIABWr58+QW3L1myRMuWLdOKFSu0Y8cORUZGKi0tTWfPnvW2GT9+vPbt26e8vDytX79eb731lu65556WuoR2Iz8/X5mZmXrnnXeUl5en2tpaDR8+XKdPn/a2mTlzpl577TWtXbtW+fn5OnbsmMaOHevdXldXp/T0dNXU1Gj79u1atWqVcnNzNX/+/EBcUpvVtWtXLV68WIWFhdq9e7duueUWjR49Wvv27ZNEPwTKrl279Mwzz6h///4+6+mPlnXVVVeprKzMu7z99tvebS3WF+YidsMNN5jMzEzv57q6OtOlSxeTk5MTwKraN0lm3bp13s/19fUmMTHR/O53v/Ouq6ysNOHh4ebFF180xhizf/9+I8ns2rXL22bDhg3GZrOZTz75pMVqb48qKiqMJJOfn2+M8fzsQ0NDzdq1a71t3n//fSPJFBQUGGOM+dvf/maCgoKMy+Xytnn66aeN3W431dXVLXsB7cwll1xinn32WfohQE6ePGmuvPJKk5eXZ4YOHWruv/9+Ywx/L1raggULzIABAy64rSX74qIdQaqpqVFhYaFSU1O964KCgpSamqqCgoIAVnZxOXLkiFwul08/OBwOpaSkePuhoKBAMTExuu6667xtUlNTFRQUpB07drR4ze2J2+2WJO8XPRYWFqq2ttanP/r06aOkpCSf/ujXr5/Pi1jT0tJUVVXlHf1A09TV1WnNmjU6ffq0nE4n/RAgmZmZSk9P9/m5S/y9CIRDhw6pS5cu+t73vqfx48ertLRUUsv2Rbt4k/a38dlnn6muru68t20nJCTowIEDAarq4uNyuSTpgv3QsM3lcik+Pt5ne0hIiGJjY71t0HT19fWaMWOGhgwZoquvvlqS52cdFhammJgYn7bW/rhQfzVsQ+MVFxfL6XTq7NmzioqK0rp165ScnKyioiL6oYWtWbNGe/bs0a5du87bxt+LlpWSkqLc3Fz17t1bZWVlWrhwoW666Sa99957LdoXF21AAi52mZmZeu+993zu7aNl9e7dW0VFRXK73frLX/6iiRMnKj8/P9BlXXSOHj2q+++/X3l5eerQoUOgy7nojRw50vvn/v37KyUlRd27d9ef//xndezYscXquGhvsV166aUKDg4+b+Z7eXm5EhMTA1TVxafhZ/11/ZCYmHjexPlz587pxIkT9NW3lJWVpfXr1+vNN99U165dvesTExNVU1OjyspKn/bW/rhQfzVsQ+OFhYXpiiuu0KBBg5STk6MBAwboySefpB9aWGFhoSoqKnTttdcqJCREISEhys/P17JlyxQSEqKEhAT6I4BiYmLUq1cvHT58uEX/bly0ASksLEyDBg3S5s2bvevq6+u1efNmOZ3OAFZ2cenZs6cSExN9+qGqqko7duzw9oPT6VRlZaUKCwu9bbZs2aL6+nqlpKS0eM1tmTFGWVlZWrdunbZs2aKePXv6bB80aJBCQ0N9+uPgwYMqLS316Y/i4mKf0JqXlye73a7k5OSWuZB2qr6+XtXV1fRDCxs2bJiKi4tVVFTkXa677jqNHz/e+2f6I3BOnTqlDz/8UJ07d27Zvxvfaop5O7FmzRoTHh5ucnNzzf79+80999xjYmJifGa+47s7efKkeffdd827775rJJnHH3/cvPvuu6akpMQYY8zixYtNTEyMefXVV83evXvN6NGjTc+ePc3nn3/uPcaIESPMwIEDzY4dO8zbb79trrzySnPnnXcG6pLarGnTphmHw2G2bt1qysrKvMuZM2e8be69916TlJRktmzZYnbv3m2cTqdxOp3e7efOnTNXX321GT58uCkqKjIbN240cXFxJjs7OxCX1Gb96le/Mvn5+ebIkSNm79695le/+pWx2Wxm06ZNxhj6IdC+/BSbMfRHS5o9e7bZunWrOXLkiNm2bZtJTU01l156qamoqDDGtFxfXNQByRhjfv/735ukpCQTFhZmbrjhBvPOO+8EuqR258033zSSzlsmTpxojPE86v/QQw+ZhIQEEx4eboYNG2YOHjzoc4zjx4+bO++800RFRRm73W4mT55sTp48GYCradsu1A+SzMqVK71tPv/8c/OLX/zCXHLJJSYiIsL8+Mc/NmVlZT7H+eijj8zIkSNNx44dzaWXXmpmz55tamtrW/hq2raf//znpnv37iYsLMzExcWZYcOGecORMfRDoFkDEv3RcsaNG2c6d+5swsLCzGWXXWbGjRtnDh8+7N3eUn1hM8aY7zT2BQAA0M5ctHOQAAAAvgoBCQAAwIKABAAAYEFAAgAAsCAgAQAAWBCQAAAALAhIAAAAFgQkAAAACwISgFajR48estlsstls530ZZVuWm5vrva4ZM2YEuhwAjUBAAtCs6urqdOONN2rs2LE+691ut7p166Zf//rXX7v/okWLVFZWJofD4c8ylZubq5iYGL+eo8G4ceNUVlbGF2EDbQgBCUCzCg4OVm5urjZu3KgXXnjBu3769OmKjY3VggULvnb/6OhoJSYmymaz+bvUZlFXV6f6+vqvbdOxY0clJiYqLCyshaoC8F0RkAA0u169emnx4sWaPn26ysrK9Oqrr2rNmjV6/vnnmxwSGkZ61q9fr969eysiIkJ33HGHzpw5o1WrVqlHjx665JJLdN9996murs67X3V1tR544AFddtllioyMVEpKirZu3SpJ2rp1qyZPniy32+299fXwww9/435fruevf/2rkpOTFR4ertLSUm3dulU33HCDIiMjFRMToyFDhqikpOS7/igBBEhIoAsA0D5Nnz5d69at01133aXi4mLNnz9fAwYM+FbHOnPmjJYtW6Y1a9bo5MmTGjt2rH784x8rJiZGf/vb3/Svf/1LGRkZGjJkiMaNGydJysrK0v79+7VmzRp16dJF69at04gRI1RcXKwbb7xRS5cu1fz583Xw4EFJUlRU1Dfud+WVV3rr+e1vf6tnn31WnTp1UmxsrK655hrdfffdevHFF1VTU6OdO3e2mVEwABdgAMBP3n//fSPJ9OvXz9TW1n5j++7du5snnnjCZ93KlSuNJHP48GHvuqlTp5qIiAhz8uRJ77q0tDQzdepUY4wxJSUlJjg42HzyySc+xxo2bJjJzs72HtfhcPhsb+x+kkxRUZF3+/Hjx40ks3Xr1q+9vqFDh5r777//a9sAaB0YQQLgN88995wiIiJ05MgRffzxx+rRo8e3Ok5ERIQuv/xy7+eEhAT16NHDO+rTsK6iokKSVFxcrLq6OvXq1cvnONXV1erUqdNXnqex+4WFhal///7ez7GxsZo0aZLS0tJ06623KjU1VT/96U/VuXPnb3W9AAKPgATAL7Zv364nnnhCmzZt0m9+8xtNmTJFf//737/VbafQ0FCfzzab7YLrGiZLnzp1SsHBwSosLFRwcLBPuy+HKqvG7texY8fzrmPlypW67777tHHjRr300kuaN2+e8vLyNHjw4MZfKIBWg4AEoNmdOXNGkyZN0rRp03TzzTerZ8+e6tevn1asWKFp06b5/fwDBw5UXV2dKioqdNNNN12wTVhYmM+k7sbu903nHThwoLKzs+V0OrV69WoCEtBG8RQbgGaXnZ0tY4wWL14syfMCyP/+7//WL3/5S3300Ud+P3+vXr00fvx4TZgwQS+//LKOHDminTt3KicnR6+//rq3plOnTmnz5s367LPPdObMmUbtdyFHjhxRdna2CgoKVFJSok2bNunQoUPq27ev368VgH8QkAA0q/z8fC1fvlwrV65URESEd/3UqVN14403asqUKTLG+L2OlStXasKECZo9e7Z69+6tMWPGaNeuXUpKSpIk3Xjjjbr33ns1btw4xcXFacmSJY3a70IiIiJ04MABZWRkqFevXrrnnnuUmZmpqVOn+v06AfiHzbTEf6kAoBF69OihGTNmtNuv4/jhD3+oa665RkuXLg10KQC+ASNIAFqVuXPnKioqSm63O9ClNJsXXnhBUVFR+sc//hHoUgA0EiNIAFqNkpIS1dbWSpK+973vKSioffw/3MmTJ1VeXi5JiomJ0aWXXhrgigB8EwISAACARfv43zMAAIBmREACAACwICABAABYEJAAAAAsCEgAAAAWBCQAAAALAhIAAIAFAQkAAMDi/wOQdC9eXamVegAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "my_landscape.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "df4715c1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(
,\n", - " )" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkgAAAG2CAYAAACEbnlbAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAARcJJREFUeJzt3XlclOX+//H3AIIgW5iCGy6VGuWWmo1mmxR2tDSxY2VpZmWGlkudokyrU2HWKeuk2fLNpTLLjkuaWmZqi+SaxzXNMjEVcfkBLgkI9++P6zDKiAoKcw/D6/l43A9n7vuemc94V/Puuq/FYVmWJQAAALj42V0AAACAtyEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuLE1ID377LNyOBxFtqZNm7qOHzt2TElJSapevbpCQ0OVmJiovXv3FnmPtLQ0denSRSEhIapZs6Yef/xxHT9+3NNfBQAA+JAAuwu47LLL9M0337ieBwScKGno0KH68ssvNX36dEVERGjQoEHq0aOHfvzxR0lSfn6+unTpopiYGC1btkx79uxRnz59VKVKFb300kse/y4AAMA3OOxcrPbZZ5/VrFmztHbt2lOOZWVlqUaNGpo6dap69uwpSfrll1906aWXKjU1VVdddZXmz5+vrl27avfu3YqOjpYkTZgwQU888YT27dunwMBAT34dAADgI2xvQfr1119Vu3ZtVa1aVU6nUykpKYqNjdXq1auVl5en+Ph417lNmzZVbGysKyClpqaqWbNmrnAkSQkJCRo4cKA2btyoVq1aFfuZOTk5ysnJcT0vKCjQwYMHVb16dTkcjvL7sgAAoMxYlqVDhw6pdu3a8vMr215Dtgakdu3aadKkSWrSpIn27Nmj5557Th07dtSGDRuUnp6uwMBARUZGFnlNdHS00tPTJUnp6elFwlHh8cJjp5OSkqLnnnuubL8MAACwxc6dO1W3bt0yfU9bA9LNN9/sety8eXO1a9dO9evX12effabg4OBy+9zk5GQNGzbM9TwrK0uxsbHauXOnwsPDy+1zAQBA2cnOzla9evUUFhZW5u9t+y22k0VGRqpx48batm2bbrzxRuXm5iozM7NIK9LevXsVExMjSYqJidGKFSuKvEfhKLfCc4oTFBSkoKCgU/aHh4cTkAAAqGDKo3uMV82DdPjwYf3222+qVauWWrdurSpVqmjRokWu41u2bFFaWpqcTqckyel0av369crIyHCds3DhQoWHhysuLs7j9QMAAN9gawvSY489pltuuUX169fX7t27NWrUKPn7++vOO+9URESE+vfvr2HDhikqKkrh4eEaPHiwnE6nrrrqKknSTTfdpLi4ON1zzz0aM2aM0tPTNWLECCUlJRXbQgQAAFAStgakP//8U3feeacOHDigGjVq6Oqrr9ZPP/2kGjVqSJJef/11+fn5KTExUTk5OUpISND48eNdr/f399fcuXM1cOBAOZ1OVatWTX379tXzzz9v11cCAAA+wNZ5kLxFdna2IiIilJWVRR8kAAAqiPL8/faqPkgAAADegIAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADghoAEAADgxmsC0ujRo+VwODRkyBDXvmPHjikpKUnVq1dXaGioEhMTtXfv3iKvS0tLU5cuXRQSEqKaNWvq8ccf1/Hjxz1cPQAA8CVeEZBWrlypd955R82bNy+yf+jQoZozZ46mT5+upUuXavfu3erRo4freH5+vrp06aLc3FwtW7ZMkydP1qRJkzRy5EhPfwUAAOBDbA9Ihw8fVu/evfXee+/pggsucO3PysrS//3f/+m1117TDTfcoNatW2vixIlatmyZfvrpJ0nS119/rU2bNumjjz5Sy5YtdfPNN+uf//ynxo0bp9zcXLu+EgAAqOBsD0hJSUnq0qWL4uPji+xfvXq18vLyiuxv2rSpYmNjlZqaKklKTU1Vs2bNFB0d7TonISFB2dnZ2rhx42k/MycnR9nZ2UU2AACAQgF2fvi0adO0Zs0arVy58pRj6enpCgwMVGRkZJH90dHRSk9Pd51zcjgqPF547HRSUlL03HPPnWf1AADAV9nWgrRz5049+uij+vjjj1W1alWPfnZycrKysrJc286dOz36+QAAwLvZFpBWr16tjIwMXXHFFQoICFBAQICWLl2qN998UwEBAYqOjlZubq4yMzOLvG7v3r2KiYmRJMXExJwyqq3weeE5xQkKClJ4eHiRDQAAoJBtAalTp05av3691q5d69ratGmj3r17ux5XqVJFixYtcr1my5YtSktLk9PplCQ5nU6tX79eGRkZrnMWLlyo8PBwxcXFefw7AQAA32BbH6SwsDBdfvnlRfZVq1ZN1atXd+3v37+/hg0bpqioKIWHh2vw4MFyOp266qqrJEk33XST4uLidM8992jMmDFKT0/XiBEjlJSUpKCgII9/JwAA4Bts7aR9Nq+//rr8/PyUmJionJwcJSQkaPz48a7j/v7+mjt3rgYOHCin06lq1aqpb9++ev75522sGgAAVHQOy7Isu4uwW3Z2tiIiIpSVlUV/JAAAKojy/P22fR4kAAAAb0NAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAAgAAcENAOsn330v5+XZXAQAA7EZAOknXrlKDBtKMGXZXAgAA7ERAcrNrl9SzJyEJAIDKjIDkxrLMn0OGcLsNAIDKioBUDMuSdu40fZIAAEDlQ0A6gz177K4AAADYgYB0BrVq2V0BAACwAwHpNOrVkzp2tLsKAABgBwLSaQwcKPn7210FAACwAwHJTXCw+XPsWGnHDltLAQAANiEgnWTuXNMxu2VLKSPDTByZnW13VQAAwNMISCfp2FGKiJDmzDEdtDdskO68k/mQAACobAhIxahbV5o9W6paVZo3T3rsMbsrAgAAnkRAOo22baUpU8zjsWOld96xtRwAAOBBBKQzuP126YUXzOOkJOmbb+ytBwAAeAYB6Syeekq6+27TD+n226UtW+yuCAAAlDcC0lk4HNJ770nt20uZmWZk24EDdlcFAADKEwGpBKpWlWbOlBo0kLZtkxITpdxcu6sCAADlhYBUQjVrmnmSwsKkpUvNTNuWZXdVAACgPBCQSuGyy6TPPpP8/KQPPpBefdXuigAAQHkgIJVS585m2L8kPfGEmS8JAAD4FgLSORg0SHr4YXOL7a67pJ9/trsiAABQlghI58DhkN54Q7rxRunoUemWW6Tdu+2uCgAAlBUC0jkKCDD9kZo2lXbtkrp1M2EJAABUfASk8xAZaUa2Va8urVol9e0rFRTYXRUAADhfBKTzdNFFZo6kKlWkzz+XRo2yuyIAAHC+CEhloGNHM9u2ZNZu++gje+sBAADnh4BURvr2lZ580jzu31/68Ud76wEAAOeOgFSGXnxRuu02swzJbbdJ27fbXREAADgXBKQy5OcnffihdMUV0r59ZmHbrCy7qwIAAKVFQCpj1apJX3wh1a4tbdok3XGHdPy43VUBAIDSICCVgzp1TEgKDpYWLJCGD7e7IgAAUBoEpHLSuvWJ0WxvvimNH29vPQAAoOQISOWoRw/ppZfM40cekb7+2t56AABAyRCQytmTT0p9+kj5+dLtt0ubN9tdEQAAOBtbA9Lbb7+t5s2bKzw8XOHh4XI6nZo/f77r+LFjx5SUlKTq1asrNDRUiYmJ2rt3b5H3SEtLU5cuXRQSEqKaNWvq8ccf13Ev6hXtcEjvvitdfbWUnW1Gtu3fb3dVAADgTGwNSHXr1tXo0aO1evVqrVq1SjfccIO6deumjRs3SpKGDh2qOXPmaPr06Vq6dKl2796tHj16uF6fn5+vLl26KDc3V8uWLdPkyZM1adIkjRw50q6vVKygILMcSaNG0u+/m1tvOTl2VwUAAE7HYVmWZXcRJ4uKitIrr7yinj17qkaNGpo6dap69uwpSfrll1906aWXKjU1VVdddZXmz5+vrl27avfu3YqOjpYkTZgwQU888YT27dunwMDAEn1mdna2IiIilJWVpfDw8HL7bps2SU6naUnq21eaONG0MAEAgNIrz99vr+mDlJ+fr2nTpunIkSNyOp1avXq18vLyFB8f7zqnadOmio2NVWpqqiQpNTVVzZo1c4UjSUpISFB2drarFao4OTk5ys7OLrJ5Qlyc9Nlnkr+/NHmy9PLLHvlYAABQSrYHpPXr1ys0NFRBQUF66KGHNHPmTMXFxSk9PV2BgYGKjIwscn50dLTS09MlSenp6UXCUeHxwmOnk5KSooiICNdWr169sv1SZ5CQYIb9S1JysjRjhsc+GgAAlJDtAalJkyZau3atli9froEDB6pv377atGlTuX5mcnKysrKyXNvOnTvL9fPcPfywNGiQeXz33dLq1R79eAAAcBa2B6TAwEBdfPHFat26tVJSUtSiRQu98cYbiomJUW5urjIzM4ucv3fvXsXExEiSYmJiThnVVvi88JziBAUFuUbOFW6e9vrrpjXpr7+kW2+Vdu3yeAkAAOA0Akpy0puF94RKoV+/fgoLCyv16woKCpSTk6PWrVurSpUqWrRokRITEyVJW7ZsUVpampxOpyTJ6XTqxRdfVEZGhmrWrClJWrhwocLDwxUXF1fqz/akgADp00+l9u1N5+1bb5W++86s5QYAAOxVolFsfn5+qlu3rvz9/Uv0pjt37tTWrVvVqFGjM56XnJysm2++WbGxsTp06JCmTp2ql19+WV999ZVuvPFGDRw4UPPmzdOkSZMUHh6uwYMHS5KWLVsmyXTsbtmypWrXrq0xY8YoPT1d99xzj+6//369VDiFdQl4ahRbcbZvl6680syN1KOHNH265Gd7ux4AAN6vPH+/S9SCJEmrVq1ytdKcTUlbjjIyMtSnTx/t2bNHERERat68uSscSdLrr78uPz8/JSYmKicnRwkJCRp/0qJm/v7+mjt3rgYOHCin06lq1aqpb9++ev7550v6tWzXsKE0a5Z0ww2mw/aIESeWJwEAAPYoUQvSc889p8cff1whISEletOUlBQNHDjwlBFo3srOFqRCH35oliSRpEmTzDxJAADg9Mrz99vrJoq0gzcEJMm0Hr34olSlirRokdSxo22lAADg9bxqosi//vpLR48edT3fsWOHxo4dq69Zqv68Pf+81LOnlJcn3Xab9NtvdlcEAEDlVOqA1K1bN02ZMkWSlJmZqXbt2ulf//qXunXrprfffrvMC6xM/PzMDNutW0sHDki33CK5zXIAAAA8oNQBac2aNer4v3s/n3/+uaKjo7Vjxw5NmTLlnKYDQFEhIdIXX0h16kibN0u9eknHj9tdFQAAlUupA9LRo0ddo9S+/vpr9ejRQ35+frrqqqu0Y8eOMi+wMqpdW5ozx4Slr7+WhgyxuyIAACqXUgekiy++WLNmzdLOnTv11Vdf6aabbpJkhuzb2cHZ17RqJX38seRwSOPGSW+9ZXdFAABUHqUOSCNHjtRjjz2mBg0aqF27dq5Zrb/++mu1atWqzAuszLp3l0aPNo8ffVRasMDWcgAAqDTOaZh/enq69uzZoxYtWsjvf9M+r1ixQuHh4WratGmZF1nevGWYf3EsS+rfX5o4UQoLk1JTpcsus7sqAADs5zXD/PPy8hQQEKD9+/erVatWrnAkSVdeeWWFDEfezuGQJkyQrrlGOnRI6tpV2rfP7qoAAPBtpQpIVapUUWxsrPLz88urHhQjMNAsQ3LRRdIff5hbb8eO2V0VAAC+q9R9kJ5++mk99dRTOnjwYHnUg9OoXl2aO1eKiJCWLZMeeMDcfgMAAGWv1H2QWrVqpW3btikvL0/169dXtWrVihxfs2ZNmRboCd7cB8ndN99InTtL+fnSCy9ITz9td0UAANijPH+/A0r7gu7du5dpASid+Hgz5H/gQLN2W5MmZnkSAABQdlisVhWrBanQkCHSG29IwcHS0qVS27Z2VwQAgGd5zSi2QpmZmXr//feVnJzs6ou0Zs0a7dq1q0yLw+n961/S3/4m/fWXdOut0s6ddlcEAIDvKHVAWrdunRo3bqyXX35Zr776qjL/t5rqjBkzlJycXNb14TT8/aVPPpEuv1xKTzch6fBhu6sCAMA3lDogDRs2TPfee69+/fVXVa1a1bX/b3/7m7777rsyLQ5nFh5u1myrUUNau1a6+26poMDuqgAAqPhKHZBWrlypAQMGnLK/Tp06Sk9PL5OiUHINGkizZklBQdLs2RKNeAAAnL9SB6SgoCBlZ2efsn/r1q2qUaNGmRSF0mnfXvrgA/N4zBizLAkAADh3pQ5It956q55//nnl5eVJkhwOh9LS0vTEE08oMTGxzAtEydx1l/TMM+bxgAFmZFt+vrRkiemrtGSJeQ4AAM6u1MP8s7Ky1LNnT61atUqHDh1S7dq1lZ6eLqfTqXnz5p0ycWRFUBGH+RenoEC6807ps8+k0FCznXzXs25dMzVAjx721QgAQFkpz9/vc54H6ccff9R///tfHT58WFdccYXi4+PLtDBP8pWAJJlh/82bS9u2nXrM4TB/fv45IQkAUPF5VUCaMmWKevXqpaCgoCL7c3NzNW3aNPXp06dMC/QEXwpI+flSvXrSnj3FH3c4TEvS9u1mqgAAACoqr5oosl+/fsrKyjpl/6FDh9SvX78yKQrn7vvvTx+OJLPA7c6dpjP3r79K/+tKBgAATlLqtdgsy5Kj8F7NSf78809FRESUSVE4d2cKRyd76imzBQRIjRpJjRubrUmTE49r1TpxWw4AgMqkxAGpVatWcjgccjgc6tSpkwICTrw0Pz9f27dvV+fOnculSJRcrVolO++ii6Tdu02fpa1bzeauWrVTQ1PhRhYGAPiyEgek7t27S5LWrl2rhIQEhYaGuo4FBgaqQYMGDPP3Ah07mj5Gu3aZ22nuCvsgbdliHu/adSIgbd1q9m/davooHTki/fyz2dxFRxcNTIUhqlEjM2klAAAVWak7aU+ePFm9evUqssxIRedLnbQlacYMqWdP8/jkq1uaUWy5udLvvxcNTYXbmSZM9/Mzs3sX1/JUt645DgBAWfCqUWySlJmZqc8//1y//fabHn/8cUVFRWnNmjWKjo5WnTp1yrRAT/C1gCSZkPToo9Kff57YV6+eNHbs+Q/xz84uGphODlFnWjA3OFi65JJTb9c1aSJFRZ1fTQCAyserAtK6desUHx+viIgI/fHHH9qyZYsaNWqkESNGKC0tTVOmTCnTAj3BFwOSZIb8F45qq1XL3H4rz6H9lmVal9xD09at0m+/ScePn/611asXf8vu4otNsAIAwJ1XBaROnTqpdevWGjNmjMLCwvTf//5XjRo10rJly3TXXXfpjz/+KNMCPcFXA5I3OX7c9GsqruVp167Tv87hMC1fxXUUr1+/bAOfpwMlAOD8eFVAioiI0Jo1a3TRRRcVCUg7duxQkyZNdOzYsTIt0BMISPY6fNjM/O3e6rRli1TMlFsugYGmham4lqcaNUo3RUFxtyRZmgUAvFt5/n6Xeh6koKAgZWdnn7J/69atqlGjRpkUhcolNFRq2dJsJ7Msaf/+UzuJb91qJrnMzZU2bTKbu4iI4juKX3KJ+byTFXZqd/9fhV27zH6WZgGAyqfULUj333+/Dhw4oM8++0xRUVFat26d/P391b17d11zzTUaO3ZsOZVafmhBqnjy86W0tOL7O6WlFT/FQaE6dU4EposvlkaPlg4cKP5clmYBAO/lVbfYsrKy1LNnT61atUqHDh1S7dq1lZ6eLqfTqXnz5qlatWplWqAnEJB8y19/mU7hxbU87d9/bu+5eLF03XVlWiYA4Dx51S22iIgILVy4UD/88IPWrVunw4cP64orrlB8fHyZFgacq+Bg6fLLzebu4MGigWnhQmnFirO/Z0mXcAEA+IZzmgfJ19CCVHktWSJdf/3Zz6MFCQC8j1e1IEnSypUrtXjxYmVkZKigoKDIsddee61MCgM84WxLsxT6+GPTiTwy0lOVAQDsVOqA9NJLL2nEiBFq0qSJoqOj5ThpLLWDpd9Rwfj7m6H8PXuaDtnuS7MUPn//fenLL6W33mJEGwBUBqW+xRYdHa2XX35Z9957bzmV5HncYsOZlmaJipIefNBMLSBJ3buboFQBV9UBAJ9Snr/fpV461M/PTx06dCjTIgC79egh/fGH6Ws0dar5c/t2s/+666R166SnnpICAqRZs6S4OOnttyW3O8wAAB9R6hakMWPGaPfu3RVyvqPToQUJJbVunfTAAydGvnXoIL37rglMAADP8qp5kAoKCtSlSxdt3bpVcXFxqlKlSpHjM2bMKNMCPYGAhNLIz5fGjTMtSkeOSFWqSE8/LT35pBQUZHd1AFB5eNUttkceeUSLFy9W48aNVb16dUVERBTZAF/n7y898ohZ4uRvf5Py8qRnn5VatZJ+/NHu6gAAZaHULUhhYWGaNm2aunTpUl41eRwtSDhXliV9+qnp4J2RYfYNHCilpJj14AAA5cerWpCioqJ00UUXlWkRQEXlcEh33CFt3iz162f2vf226ZM0a5atpQEAzkOpA9Kzzz6rUaNG6ejRo+VRD1AhRUVJH3wgLVokXXSRtHu3dNttUmKieQwAqFhKfYutVatW+u2332RZlho0aHBKJ+01a9aUaYGewC02lKW//pKef1565RXToTsiQhozRrr/fsmv1P9LAgA4Ha9aaqR79+5lWgDga4KDTR+kXr3MlACrVkkDBkgffWSmBGja1O4KAQBnw2K1ogUJ5Sc/X3rzTWnECOnoUSkw0Dx+4gnzGABw7ryqkzaAkvP3l4YOlTZulDp3lnJzpZEjpSuukFJT7a4OAHA6JQpIUVFR2r9/f4nfNDY2Vjt27DjnogBf06CBNG+e9PHH0oUXmsDUoYM0eLB06JDd1QEA3JWoD1JmZqbmz59f4okgDxw4oPz8/PMqDPA1Dod0111SQoI0fLg0ebJZ9HbWLGn8eOmWW+yuEABQqER9kPzOYejNtm3b1KhRo3MqytPogwQ7LFwoPfSQ9Pvv5vntt5v+SjEx9tYFABWF7X2QCgoKSr1VlHAE2OXGG6X166XHHzd9laZPly69VHr/fTNDNwDAPnTSBmwUEmLmSFq50nTczsw0UwNcf720davd1QFA5UVAArxAq1bS8uXSq6+aeZSWLpWaN5deeskshgsA8CwCEuAlAgJM5+0NG8ztt5wc6emnpdatTXgCAHhOiQPSbhaUAjyiUSPpq6+kKVOk6tVNPyWnU3r0UaYEAABPKXFAuuyyyzR16tTyrAXA/zgc0j33SJs3S3ffbTptv/mmdNll0pdf2l0dAPi+EgekF198UQMGDNDtt9+ugwcPlmdNAP6nRg3pww+lBQvMZJM7d0pdu0p33int3Wt3dQDgu0ockB5++GGtW7dOBw4cUFxcnObMmVOedQE4SUKC6Zs0fLjk5ydNm2amBJg4kSkBAKA8nNNitW+99ZaGDh2qSy+9VAEBRSfjXrNmTZkV5ylMFImKZNUqMxXA2rXm+Q03SO+8I118sa1lAYDH2T5R5Ml27NihGTNm6IILLlC3bt1O2UojJSVFbdu2VVhYmGrWrKnu3btry5YtRc45duyYkpKSVL16dYWGhioxMVF73e4tpKWlqUuXLgoJCVHNmjX1+OOP6/jx46X9akCF0KaNtGKF9PLLUtWq0rffSs2aSaNHMyUAAJSVUrUgvffeexo+fLji4+P1zjvvqEaNGuf14Z07d9Ydd9yhtm3b6vjx43rqqae0YcMGbdq0SdWqVZMkDRw4UF9++aUmTZqkiIgIDRo0SH5+fvrxxx8lSfn5+WrZsqViYmL0yiuvaM+ePerTp48eeOABvfTSSyWqgxYkVFS//SYNGCAtWmSet2hhZuJu08beugDAE8r199sqoYSEBOuCCy6wJk+eXNKXlFpGRoYlyVq6dKllWZaVmZlpValSxZo+fbrrnM2bN1uSrNTUVMuyLGvevHmWn5+flZ6e7jrn7bfftsLDw62cnJwSfW5WVpYlycrKyirDbwN4RkGBZU2caFlRUZYlWZafn2UNHWpZhw7ZXRkAlK/y/P0u8S22/Px8rVu3Tn369CnbhHaSrKwsSVJUVJQkafXq1crLy1N8fLzrnKZNmyo2NlapqamSpNTUVDVr1kzR0dGucxISEpSdna2NGzcW+zk5OTnKzs4usgEVlcMh3XuvmRLgzjulggLp9delyy83o98AAKVX4oC0cOFC1a1bt9wKKSgo0JAhQ9ShQwddfvnlkqT09HQFBgYqMjKyyLnR0dFKT093nXNyOCo8XnisOCkpKYqIiHBt9erVK+NvA3hezZrS1KnSvHlSbKy0Y4d0881S797Svn12VwcAFYvXLDWSlJSkDRs2aNq0aeX+WcnJycrKynJtO3fuLPfPBDzl5puljRulIUPMlABTp0pNm0qTJzMlAACUlFcEpEGDBmnu3LlavHhxkVaqmJgY5ebmKjMzs8j5e/fuVUxMjOsc91Fthc8Lz3EXFBSk8PDwIhvgS0JDzW22n34yi94ePGhuw910k+nYDQA4M1sDkmVZGjRokGbOnKlvv/1WDRs2LHK8devWqlKlihYVDtGRtGXLFqWlpcnpdEqSnE6n1q9fr4yMDNc5CxcuVHh4uOLi4jzzRQAv1batmTcpJcVMCfDNN2ZKgFdekZgJAwBO75wmiiwrDz/8sKZOnarZs2erSZMmrv0REREKDg6WZIb5z5s3T5MmTVJ4eLgGDx4sSVq2bJmkE8P8a9eurTFjxig9PV333HOP7r//fob5Ayf59VczJcDixeZ5q1ZmSoArrrC3LgA4V+X5+21rQHI4HMXunzhxou69915JZqLI4cOH65NPPlFOTo4SEhI0fvz4IrfPduzYoYEDB2rJkiWqVq2a+vbtq9GjR58yy/fpEJBQWViWWZ7kscek//f/TB+loUOl556T/jf1GABUGD4bkLwFAQmVzd690qOPSp9+ap43bChNmGD6KAFAReFVS40AqPiio82Ct3PnSvXqSdu3mwVx+/SR9u+3uzoAsB8BCajEunQxUwI88oiZcPLDD6VLL5U++ogpAQBUbgQkoJILC5PeeENatszMvr1/v3TPPWY+pe3b7a4OAOxBQAIgSbrqKmn1aumFF6SgIOmrr0xgeu01pgQAUPkQkAC4BAZKTz8trVsnXXutdPSoNHy4CU9r19pdHQB4DgEJwCkaN5a+/VZ67z0pIsK0LLVpIz3xhAlNAODrCEgAiuXnJ91/v7R5s9Szp5SfL40ZY2biPmlyewDwSQQkAGdUq5Y0fbo0e7ZUp470++9SfLxZ2+3AAburA4DyQUACUCK33ipt2iQlJZkpASZPNlMCTJ3KlAAAfA8BCUCJhYdLb70l/fCDFBcn7dsn9e5t5lPascPu6gCg7BCQAJRa+/bSzz9Lzz9vRr7Nny9ddpk0dqzpqwQAFR0BCcA5CQyUnnlG+u9/pY4dpSNHzMK3TqfZBwAVGQEJwHlp2lRassQsdhseLq1caaYESE6W/vrL7uoA4NwQkACcNz8/acAAMyVAjx5m5u3Ro6XmzaXFi+2uDgBKj4AEoMzUri395z/SzJnm8bZt0g03SP37SwcP2l0dAJQcAQlAmeve3UwJMHCgef7BB2ZKgE8/ZUoAABUDAQlAuYiIkMaPl77/3vRTysiQ7rhDuuUWKS3N7uoA4MwISADK1dVXm4VuR42SqlSRvvzSTAnw738zJQAA70VAAlDugoKkZ581Qal9e+nwYemRR6QOHaT16+2uDgBORUAC4DFxceaW2/jxUliYtHy5dMUV0ogR0rFjdlcHACcQkAB4lJ+f6by9aZPUrZuZEuDFF6UWLaSlS+2uDgAMAhIAW9Sta6YD+PxzKSZG2rpVuu466cEHpcxMu6sDUNkRkADYxuGQEhPNBJMPPmj2vfeemRLg88+ZEgCAfQhIAGwXGSm98465xda4sZSeLt1+u5lP6c8/7a4OQGVEQALgNa65xix0O2KEFBAgffGF6dg9bpxUUGB3dQAqEwISAK9Star0z39KP/8sXXWVdOiQNGiQmU9p40a7qwNQWRCQAHilyy+XfvjBTCgZGiqlpkqtWkkjR0o5OXZXB8DXEZAAeC1/f9N6tGmTWaIkL8+0LrVsacITAJQXAhIAr1evnjR7tvTZZ1J0tPTLL1LHjtJDD0lZWXZXB8AXEZAAVAgOhxnZtnmz1L+/2ffOO2ZKgBkz7K0NgO8hIAGoUC64QHr/fWnxYumSS6Q9e8xcSj16SLt3210dAF9BQAJQIV13nZkS4KmnzJQAM2ea1qQJE5gSAMD5IyABqLCCg806bqtXS1deKWVnm3Xerr3W3IoDgHNFQAJQ4TVvLi1bJr3xhlStmhnh1rKl9NxzTAkA4NwQkAD4BH9/6ZFHzGSSf/ublJsrPfusmTtp2TK7qwNQ0RCQAPiU+vWluXOlTz6RatY0t9quvlpKSjK34ACgJAhIAHyOwyHdcYcJR/36SZYljR9v1nWbPdvu6gBUBAQkAD4rKkr64APpm2+kiy6Sdu2SuneXevY00wMAwOkQkAD4vE6dpPXrpSeeMH2V/vMfMyXAu+8yJQCA4hGQAFQKwcHS6NHSqlVSmzZmiZIBA6Trr5e2bDlxXn6+tGSJ6cO0ZIl5DqDyISABqFRatpR++kl67TUpJET67jszTcA//2nWemvQwISmu+4yfzZowFImQGXksCzLsrsIu2VnZysiIkJZWVkKDw+3uxwAHvLHH2ZiyQULTn+Ow2H+/Pxzs5wJAO9Rnr/ftCABqLQaNJDmzZM+/FDyO81/DQv/F3LIEG63AZUJAQlApeZwSHXrnrmztmVJO3dK33/vuboA2IuABKDSK+mQf6YGACoPAhKASq9WrbI9D0DFR0ACUOl17GhusxV2yC5OvXrmPACVAwEJQKXn7y+98YZ5fLqQ5HSa8wBUDgQkAJAZwv/551KdOkX3X3CB+fOzz6SPP/Z8XQDsQUACgP/p0cPMjbR4sTR1qvlz3z7pH/8wx++7j5FsQGXBRJFiokgAZ1ZQIN1+u5lRu3p1MxP3xRfbXRUAJooEABv5+ZnJJNu2lQ4ckLp0kQ4etLsqAOWJgAQAJRASIn3xhRQbK23dKiUmSrm5dlcFoLwQkACghGJipLlzpbAwackS6cEHTyxFAsC3EJDgnfbtM6uIxsZKQUHmlykhQfrxR7srOzcpKeb+TFiYVLOm1L27tGWL3VXhHDRrZka0+ftLkyebSwvA9xCQ4J0SE6Wffza/QFu3mnsb111nOoCUp/K6Z7J0qZSUZHr3Llwo5eVJN90kHTlSPp+HctW5s/Tvf5vHTz9tAhMA30JAgvfJzDRjqV9+Wbr+eql+fenKK6XkZOnWW0+cl5YmdesmhYZK4eHS3/8u7d174vi995qWmpMNGWKCVqHrrpMGDTL7L7zQtFJJ0saNUteu5n3DwswUyr/9duJ1778vXXqpVLWq1LSpNH78mb/TggWmnssuk1q0kCZNMvWvXl2qvxp4j4EDzT82ktSnj8m+AHwHAQneJzTUbLNmSTk5xZ9TUGDC0cGDpnVm4ULp99+lXr1K/3mTJ0uBgeb23YQJ0q5d0jXXmFt7335rQsx990nHj5vzP/5YGjlSevFFafNm6aWXpGeeMe9TUllZ5s+oqNLXC6/x6qvSLbeYf0xvvVXavt3uigCUlQC7CwBOERBgWlgeeMAEliuukK69VrrjDql5c3POokXS+vXmF6lePbNvyhTTQrNypenvU1KXXCKNGXPi+VNPSRER0rRpUpUqZl/jxieOjxol/etfZlZBSWrYUNq0SXrnHalv37N/XkGBaXro0EG6/PKS1wmv4+9vJpS85hpzR7hrV5OzIyPtrgzA+aIFCd4pMVHavdv0Perc2QwZuuIKE5wk03JTr96JcCRJcXHml2nz5tJ9VuvWRZ+vXWtuqRWGo5MdOWJutfXvf6KlKzRUeuGForfgziQpSdqwwQQwVHihodKcOWaJkk2bzISSeXl2VwXgfBGQ4L2qVpVuvNHcvlq2zPThGTWq5K/38zt1DHZxv1zVqhV9Hhx8+vc8fNj8+d57JkgVbhs2lKwTyqBBZpz44sVm+Xj4hDp1TEiqVk365huTgRn+D1RsBCRUHHFxJ0Z9XXqptHOn2Qpt2mQ6eMfFmec1akh79hR9j7Vrz/45zZubTuLFhanoaKl2bdPf6eKLi24NG57+PS3LhKOZM02/pjOdiwqpVSvpk09MLn/vPXMXFkDFRUCCd9iyxfR4HTxYGjDAhJy335bWrTP9jKZPN/2EunUz58fHmwlpeveW1qyRVqwwQ4muvVZq08acc8MN0qpVpm/Sr7+a1qcNG85ey6BBUna26fO0apV57Ycfnpi36LnnzOQ3b75ppiBYv16aOFF67bXTv2dSkvTRR6bDSliYlJ5utr/+Or+/N3iVW2458Y/BP/5h8jCACsqy0dKlS62uXbtatWrVsiRZM2fOLHK8oKDAeuaZZ6yYmBiratWqVqdOnaytW7cWOefAgQPWXXfdZYWFhVkRERHWfffdZx06dKhUdWRlZVmSrKysrPP9SiitnTst69prLUuyLD8/y6pSxbICAsxzybKCgiwrJMSymjSxrBEjLOvo0ROv3bHDsm691bKqVbOssDDLuv12y0pPL/r+I0daVnS0ZUVEWNbQoZY1aJD5vELXXmtZjz56al3//a9l3XST+eywMMvq2NGyfvvtxPGPP7asli0tKzDQsi64wLKuucayZsw4/fcs/D7u28SJpf0bg5crKLCshx82lzc42LJWrrS7IsB3lefvt8Oy7LtTPn/+fP34449q3bq1evTooZkzZ6r7SfPWvPzyy0pJSdHkyZPVsGFDPfPMM1q/fr02bdqkqlWrSpJuvvlm7dmzR++8847y8vLUr18/tW3bVlOnTi1xHeW5GjDOIDtbatJE2r//xBB6dw6HackpxfUE7Hb8uGlNWrDATAK/fLmZFB5A2SrX3+8yj1znSG4tSAUFBVZMTIz1yiuvuPZlZmZaQUFB1ieffGJZlmVt2rTJkmStPOl/0ebPn285HA5r165dJf5sWpBs8vTTptXodK0rJ2/LltldLVAqWVmW1ayZ+ce3WTPzHEDZKs/fb6/tg7R9+3alp6crPj7etS8iIkLt2rVTamqqJCk1NVWRkZFqU9jnRFJ8fLz8/Py0fPny0753Tk6OsrOzi2ywwVdfmTmBziYgwHRsBiqQ8HAzYDEmxnRTu+OO0zeUAvA+XhuQ0tPTJUnR0dFF9kdHR7uOpaenq2bNmkWOBwQEKCoqynVOcVJSUhQREeHa6p08lw48JzzcDPk5m4ICM9kMUMHExpqpvIKDpfnzpaFD7a4IQEl5bUAqT8nJycrKynJtO08eKg7PGTKkZC1I1apJ99xT7uUA5aFtWzOA0eGQ3nrLDH4E4P28NiDFxMRIkvaevPjo/54XHouJiVFGRkaR48ePH9fBgwdd5xQnKChI4eHhRTbYoGtXs5yIZH493Pn7m9trH3zAmmWo0Hr0MGsvS6YVae5ce+sBcHZeG5AaNmyomJgYLVq0yLUvOztby5cvl9PplCQ5nU5lZmZq9Ukron/77bcqKChQu3btPF4zSsnhMOuXTZsmtW9fNCSFhJhflTVrpJ497asRKCOPPSbdf79pNL3jjpLNWQrAPrYuVnv48GFt27bN9Xz79u1au3atoqKiFBsbqyFDhuiFF17QJZdc4hrmX7t2bddUAJdeeqk6d+6sBx54QBMmTFBeXp4GDRqkO+64Q7Vr17bpW6FUHA6pVy+zHTwo/fmnaTW66CIpKMju6oAy43BI48ebeU8XLTINqMuXm2VKAHgfW+dBWrJkia6//vpT9vft21eTJk2SZVkaNWqU3n33XWVmZurqq6/W+PHj1fikldUPHjyoQYMGac6cOfLz81NiYqLefPNNhZaiUy/zIAHwlMxM02C6ebNZnuS77xiDAJyr8vz9tjUgeQsCEgBP2r5datdO2rdPuvVWacYM0+UOQOmU5++31/ZBAgBf1bChNHu2uYv8xRdm3TYA3oWABAA2cDqlyZPN49dekyZMsLceAEURkADAJr16SS+8YB4PGmQmlwfgHQhIAGCjp56S+vaV8vOl22+XNmywuyIAEgEJAGzlcEjvvitde6106JDUpYt0hpWSAHgIAQkAbBYYKP3nP9Ill0hpaVK3btLRo3ZXBVRuBCQA8ALVq0tffmlW1VmxQurTp2RLFQIoHwQkAPASl1wizZwpValiWpSeftruioDKi4AEAF7kmmvM+sySNHr0iccAPIuABABe5u67pZEjzeMBA8zabQA8i4AEAF7o2WelO++Ujh+XEhPN2m0APIeABABeyOEwt9fat5eysszw/3377K4KqDwISADgpapWlWbNkho1Mgvcdu8uHTtmd1VA5UBAAgAvVqOGGf4fESEtWybdd59kWXZXBfg+AhIAeLmmTaUZM6SAAOmTT0z/JADli4AEABXADTdI77xjHj//vPThh/bWA/g6AhIAVBD33Sc9+aR53L+/9N139tYD+DICEgBUIC++KPXsKeXlSbfdJv36q90VAb6JgAQAFYifnzRlinTlldLBg2b4/4EDdlcF+B4CEgBUMMHB0uzZUmysaUHq0UPKzbW7KsC3EJAAoAKKiTHD/8PCTF+kBx5g+D9QlghIAFBBXX65NH265O9vbru99JLdFQG+g4AEABVYQoL01lvm8YgR0qef2lsP4CsISABQwT30kDRsmHnct6+ZcRvA+SEgAYAPGDNGuvVWKSfHrNn2++92VwRUbAQkAPAB/v7S1KlSq1bSvn1m+H9mpt1VARUXAQkAfES1atKcOVKdOtIvv5yYUBJA6RGQAMCH1KkjzZ1rwtKiRdLDDzP8HzgXBCQA8DEtW0rTpplZt99/X3rlFbsrAioeAhIA+KCuXaXXXzePn3hCmjHD3nqAioaABAA+6pFHpEGDzOO775ZWrrS3HqAiISABgA97/XXp5pulv/6SbrlFSkuzuyKgYiAgAYAPCwgws2s3aybt3WuG/2dn210V4P0ISADg48LCzMi2mBhpwwapVy/p+HG7qwK8GwEJACqB2FgzR1JwsLRggfToowz/B86EgAQAlUSbNtLHH0sOhzR+vPTGG3ZXBHgvAhIAVCK33WbWbZPMArdz5thbD+CtCEgAUMkMHy498IC5xXbnndLPP9tdEeB9CEgAUMk4HNK4cdKNN0pHjphJJXftsrsqwLsQkACgEqpSRfrsMykuTtq928yRdPiw3VUB3oOABACVVGSkGf5fo4a5zXbXXVJ+vt1VAd6BgAQAlVjDhtIXX0hBQabD9mOP2V0R4B0ISABQyV11lTRlink8dqyZAgCo7AhIAAD9/e/Siy+ax4MHm8kkgcqMgAQAkCQlJ0t9+0oFBSYwrV9vd0WAfQhIAABJZvj/u+9K110nHTpkFrZNT7e7KsAeBCQAgEtgoPSf/0iNG0s7d0q33iodPWp3VYDnEZAAAEVERUlffilVry6tXCndc4+57QZUJgQkAMApLr5YmjnTtCjNmGH6JwGVCQEJAFCsjh2l//s/83jMGOm99+ytB/AkAhIA4LTuvlsaNco8fvhh6Ztv7K0H8BQCEgDgjEaNMsuQHD8u9ewpbdpkd0VA+SMgAQDOyOEwt9o6dJCysqSuXaWMDLurAsoXAQkAcFZVq0qzZkmNGknbt0vdu0vHjtldFVB+CEgAgBK58EIz/D8yUkpNle69l+H/8F0EJABAiTVtaob9BwRIn356ogM34GsISACAUrn+erMkiSS98II0ebK99QDlgYAEACi1fv1OTB75wAPS0qX21gOUNQISAOCcvPCCdPvtUl6edNtt0tatdlcElB0CEgDgnPj5mdtr7dpJ/+//SV26SAcO2F0VUDYISACAcxYcLM2eLdWvL23bZlqScnLsrgo4fwQkAMB5iY42w//Dw6Xvv5fuv1+yLLurAs4PAQkAcN4uu0yaPl3y95c++sj0TwIqMp8JSOPGjVODBg1UtWpVtWvXTitWrLC7JACoVG66SRo3zjweOVL65BMpP19assQ8XrLEPAcqAp8ISJ9++qmGDRumUaNGac2aNWrRooUSEhKUwWJBAOBRAwZIw4ebx336SLVqmXmT7rrL/NmggZloEvB2Dsuq+HeK27Vrp7Zt2+qtt96SJBUUFKhevXoaPHiwnnzyybO+Pjs7WxEREcrKylJ4eHh5lwsAPi0/X3I6pZUrTz3mcJg/P/9c6tHDs3XB95Tn73dAmb6bDXJzc7V69WolF85YJsnPz0/x8fFKTU0t9jU5OTnKOWmYRVZWliTzFw0AOD/5+dKffxZ/rPB/yR95xLQo+ft7ri74nsLf7fJo66nwAWn//v3Kz89XdHR0kf3R0dH65Zdfin1NSkqKnnvuuVP216tXr1xqBAAUtWuXFBVldxXwFQcOHFBERESZvmeFD0jnIjk5WcOGDXM9z8zMVP369ZWWllbmf8EonezsbNWrV087d+7kdqfNuBbeg2vhPbgW3iUrK0uxsbGKKoe0XeED0oUXXih/f3/t3bu3yP69e/cqJiam2NcEBQUpKCjolP0RERH8A+8lwsPDuRZegmvhPbgW3oNr4V38/Mp+zFmFH8UWGBio1q1ba9GiRa59BQUFWrRokZxOp42VAQCAiqrCtyBJ0rBhw9S3b1+1adNGV155pcaOHasjR46oX79+dpcGAAAqIJ8ISL169dK+ffs0cuRIpaenq2XLllqwYMEpHbdPJygoSKNGjSr2ths8i2vhPbgW3oNr4T24Ft6lPK+HT8yDBAAAUJYqfB8kAACAskZAAgAAcENAAgAAcENAAgAAcFPpA9K4cePUoEEDVa1aVe3atdOKFSvsLsnnfPfdd7rllltUu3ZtORwOzZo1q8hxy7I0cuRI1apVS8HBwYqPj9evv/5a5JyDBw+qd+/eCg8PV2RkpPr376/Dhw978Fv4hpSUFLVt21ZhYWGqWbOmunfvri1bthQ559ixY0pKSlL16tUVGhqqxMTEUyZiTUtLU5cuXRQSEqKaNWvq8ccf1/Hjxz35VSq8t99+W82bN3dNOOh0OjV//nzXca6DfUaPHi2Hw6EhQ4a49nE9POfZZ5+Vw+EosjVt2tR13FPXolIHpE8//VTDhg3TqFGjtGbNGrVo0UIJCQnKyMiwuzSfcuTIEbVo0ULjxo0r9viYMWP05ptvasKECVq+fLmqVaumhIQEHTt2zHVO7969tXHjRi1cuFBz587Vd999pwcffNBTX8FnLF26VElJSfrpp5+0cOFC5eXl6aabbtKRI0dc5wwdOlRz5szR9OnTtXTpUu3evVs9Tlp2PT8/X126dFFubq6WLVumyZMna9KkSRo5cqQdX6nCqlu3rkaPHq3Vq1dr1apVuuGGG9StWzdt3LhREtfBLitXrtQ777yj5s2bF9nP9fCsyy67THv27HFtP/zwg+uYx66FVYldeeWVVlJSkut5fn6+Vbt2bSslJcXGqnybJGvmzJmu5wUFBVZMTIz1yiuvuPZlZmZaQUFB1ieffGJZlmVt2rTJkmStXLnSdc78+fMth8Nh7dq1y2O1+6KMjAxLkrV06VLLsszffZUqVazp06e7ztm8ebMlyUpNTbUsy7LmzZtn+fn5Wenp6a5z3n77bSs8PNzKycnx7BfwMRdccIH1/vvvcx1scujQIeuSSy6xFi5caF177bXWo48+alkW/1542qhRo6wWLVoUe8yT16LStiDl5uZq9erVio+Pd+3z8/NTfHy8UlNTbaysctm+fbvS09OLXIeIiAi1a9fOdR1SU1MVGRmpNm3auM6Jj4+Xn5+fli9f7vGafUlWVpYkuRZ6XL16tfLy8opcj6ZNmyo2NrbI9WjWrFmRiVgTEhKUnZ3tav1A6eTn52vatGk6cuSInE4n18EmSUlJ6tKlS5G/d4l/L+zw66+/qnbt2mrUqJF69+6ttLQ0SZ69Fj4xk/a52L9/v/Lz80+ZbTs6Olq//PKLTVVVPunp6ZJU7HUoPJaenq6aNWsWOR4QEKCoqCjXOSi9goICDRkyRB06dNDll18uyfxdBwYGKjIyssi57tejuOtVeAwlt379ejmdTh07dkyhoaGaOXOm4uLitHbtWq6Dh02bNk1r1qzRypUrTznGvxee1a5dO02aNElNmjTRnj179Nxzz6ljx47asGGDR69FpQ1IQGWXlJSkDRs2FLm3D89q0qSJ1q5dq6ysLH3++efq27evli5dandZlc7OnTv16KOPauHChapatard5VR6N998s+tx8+bN1a5dO9WvX1+fffaZgoODPVZHpb3FduGFF8rf3/+Unu979+5VTEyMTVVVPoV/12e6DjExMad0nD9+/LgOHjzItTpHgwYN0ty5c7V48WLVrVvXtT8mJka5ubnKzMwscr779SjuehUeQ8kFBgbq4osvVuvWrZWSkqIWLVrojTfe4Dp42OrVq5WRkaErrrhCAQEBCggI0NKlS/Xmm28qICBA0dHRXA8bRUZGqnHjxtq2bZtH/92otAEpMDBQrVu31qJFi1z7CgoKtGjRIjmdThsrq1waNmyomJiYItchOztby5cvd10Hp9OpzMxMrV692nXOt99+q4KCArVr187jNVdklmVp0KBBmjlzpr799ls1bNiwyPHWrVurSpUqRa7Hli1blJaWVuR6rF+/vkhoXbhwocLDwxUXF+eZL+KjCgoKlJOTw3XwsE6dOmn9+vVau3ata2vTpo169+7tesz1sM/hw4f122+/qVatWp79d+Ocupj7iGnTpllBQUHWpEmTrE2bNlkPPvigFRkZWaTnO87foUOHrJ9//tn6+eefLUnWa6+9Zv3888/Wjh07LMuyrNGjR1uRkZHW7NmzrXXr1lndunWzGjZsaP3111+u9+jcubPVqlUra/ny5dYPP/xgXXLJJdadd95p11eqsAYOHGhFRERYS5Yssfbs2ePajh496jrnoYcesmJjY61vv/3WWrVqleV0Oi2n0+k6fvz4cevyyy+3brrpJmvt2rXWggULrBo1aljJycl2fKUK68knn7SWLl1qbd++3Vq3bp315JNPWg6Hw/r6668ty+I62O3kUWyWxfXwpOHDh1tLliyxtm/fbv34449WfHy8deGFF1oZGRmWZXnuWlTqgGRZlvXvf//bio2NtQIDA60rr7zS+umnn+wuyecsXrzYknTK1rdvX8uyzFD/Z555xoqOjraCgoKsTp06WVu2bCnyHgcOHLDuvPNOKzQ01AoPD7f69etnHTp0yIZvU7EVdx0kWRMnTnSd89dff1kPP/ywdcEFF1ghISHWbbfdZu3Zs6fI+/zxxx/WzTffbAUHB1sXXnihNXz4cCsvL8/D36Ziu++++6z69etbgYGBVo0aNaxOnTq5wpFlcR3s5h6QuB6e06tXL6tWrVpWYGCgVadOHatXr17Wtm3bXMc9dS0clmVZ59X2BQAA4GMqbR8kAACA0yEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAQAAuCEgAfAaDRo0kMPhkMPhOGUxyops0qRJru81ZMgQu8sBUAIEJABlKj8/X+3bt1ePHj2K7M/KylK9evX09NNPn/H1zz//vPbs2aOIiIjyLFOTJk1SZGRkuX5GoV69emnPnj0shA1UIAQkAGXK399fkyZN0oIFC/Txxx+79g8ePFhRUVEaNWrUGV8fFhammJgYORyO8i61TOTn56ugoOCM5wQHBysmJkaBgYEeqgrA+SIgAShzjRs31ujRozV48GDt2bNHs2fP1rRp0zRlypRSh4TClp65c+eqSZMmCgkJUc+ePXX06FFNnjxZDRo00AUXXKBHHnlE+fn5rtfl5OToscceU506dVStWjW1a9dOS5YskSQtWbJE/fr1U1ZWluvW17PPPnvW151czxdffKG4uDgFBQUpLS1NS5Ys0ZVXXqlq1aopMjJSHTp00I4dO873rxKATQLsLgCAbxo8eLBmzpype+65R+vXr9fIkSPVokWLc3qvo0eP6s0339S0adN06NAh9ejRQ7fddpsiIyM1b948/f7770pMTFSHDh3Uq1cvSdKgQYO0adMmTZs2TbVr19bMmTPVuXNnrV+/Xu3bt9fYsWM1cuRIbdmyRZIUGhp61tddcsklrnpefvllvf/++6pevbqioqLUsmVLPfDAA/rkk0+Um5urFStWVJhWMADFsACgnGzevNmSZDVr1szKy8s76/n169e3Xn/99SL7Jk6caEmytm3b5to3YMAAKyQkxDp06JBrX0JCgjVgwADLsixrx44dlr+/v7Vr164i79WpUycrOTnZ9b4RERFFjpf0dZKstWvXuo4fOHDAkmQtWbLkjN/v2muvtR599NEzngPAO9CCBKDcfPDBBwoJCdH27dv1559/qkGDBuf0PiEhIbroootcz6Ojo9WgQQNXq0/hvoyMDEnS+vXrlZ+fr8aNGxd5n5ycHFWvXv20n1PS1wUGBqp58+au51FRUbr33nuVkJCgG2+8UfHx8fr73/+uWrVqndP3BWA/AhKAcrFs2TK9/vrr+vrrr/XCCy+of//++uabb87ptlOVKlWKPHc4HMXuK+wsffjwYfn7+2v16tXy9/cvct7JocpdSV8XHBx8yveYOHGiHnnkES1YsECffvqpRowYoYULF+qqq64q+RcF4DUISADK3NGjR3Xvvfdq4MCBuv7669WwYUM1a9ZMEyZM0MCBA8v981u1aqX8/HxlZGSoY8eOxZ4TGBhYpFN3SV93ts9t1aqVkpOT5XQ6NXXqVAISUEExig1AmUtOTpZlWRo9erQkMwHkq6++qn/84x/6448/yv3zGzdurN69e6tPnz6aMWOGtm/frhUrViglJUVffvmlq6bDhw9r0aJF2r9/v44ePVqi1xVn+/btSk5OVmpqqnbs2KGvv/5av/76qy699NJy/64AygcBCUCZWrp0qcaNG6eJEycqJCTEtX/AgAFq3769+vfvL8uyyr2OiRMnqk+fPho+fLiaNGmi7t27a+XKlYqNjZUktW/fXg899JB69eqlGjVqaMyYMSV6XXFCQkL0yy+/KDExUY0bN9aDDz6opKQkDRgwoNy/J4Dy4bA88V8qACiBBg0aaMiQIT67HMd1112nli1bauzYsXaXAuAsaEEC4FWeeOIJhYaGKisry+5SyszHH3+s0NBQff/993aXAqCEaEEC4DV27NihvLw8SVKjRo3k5+cb/w936NAh7d27V5IUGRmpCy+80OaKAJwNAQkAAMCNb/zvGQAAQBkiIAEAALghIAEAALghIAEAALghIAEAALghIAEAALghIAEAALghIAEAALj5/7EELcc+nXSFAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "my_landscape.set_path(p2)\n", - "my_landscape.plot()" - ] - }, - { - "cell_type": "markdown", - "id": "17fec32f", - "metadata": {}, - "source": [] } ], "metadata": { diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..a5dc469 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,187 @@ +## Installation + +See the [installation guide](installation.md). + +## Test your installation + +First, see if PG-RAD is available on your system by typing + +``` +pgrad --help +``` + +You should get output along the lines of + +``` +usage: pg-rad [-h] ... + +Primary Gamma RADiation landscape tool + +... +``` + +If you get something like `pgrad: command not found`, please consult the [installation guide](installation.md). + +You can run a quick test scenario as follows: + +``` +pgrad --test +``` + +This should produce a plot of a scenario containing a single point source and a path. + +## Running PG-RAD + +In order to use the CLI for your own simulations, you need to provide a *config file*. To run with your config, run + +``` +pgrad --config path/to/my_config.yml +``` + +where `path/to/my_config.yml` points to your config file. + +## Example configs + +The easiest way is to take one of these example configs, and adjust them as needed. Alternatively, there is a detailed guide on how to write your own config file [here](config-spec.md). + +=== "Example 1" + + The position can be defined relative to the path. `along_path` means at what distance traveled along the path the source is found. If the path is 200 meters long and `along_path` is `100` then the source is halfway along the path. `dist_from_path` is the distance in meters from the path. `side` is the side of the path the source is located. This is relative to the direction the path is traveled. + + ``` yaml + name: Example 1 + speed: 13.89 + acquisition_time: 1 + + path: + file: path/to/exp_coords.csv + east_col_name: East + north_col_name: North + + sources: + source1: + activity_MBq: 1000 + isotope: CS137 + position: + along_path: 100 + dist_from_path: 50 + side: left + + detector: + name: dummy + is_isotropic: True + ``` + +=== "Example 2" + + The position can also just be defined with (x,y,z) coordinates. + + ``` yaml + name: Example 2 + speed: 13.89 + acquisition_time: 1 + + path: + file: path/to/exp_coords.csv + east_col_name: East + north_col_name: North + + sources: + source1: + activity_MBq: 1000 + isotope: CS137 + position: [104.3, 32.5, 0] + source2: + activity_MBq: 100 + isotope: CS137 + position: [0, 0, 0] + + detector: + name: dummy + is_isotropic: True + ``` + +=== "Example 3" + + This is an example of a procedural path with random apportionment of total length and random angles being assigned to turns. The parameter `alpha` is optional, and is related to randomness. A higher value leads to more uniform apportionment of lengths and a lower value to more random apportionment. More information about `alpha` can be found [here](pg-rad-config-spec.md). + + ``` yaml + name: Example 3 + speed: 8.33 + acquisition_time: 1 + + path: + length: 1000 + segments: + - straight + - turn_left + - straight + alpha: 100 + + sources: + source1: + activity_MBq: 1000 + isotope: CS137 + position: [0, 0, 0] + + detector: + name: dummy + is_isotropic: True + ``` + +=== "Example 4" + + This is an example of a procedural path that is partially specified. Note that turn_left now is a key for the corresponding angle of 45 degrees. The length is still divided randomly + + ``` yaml + name: Example 4 + speed: 8.33 + acquisition_time: 1 + + path: + length: 1000 + segments: + - straight + - turn_left: 45 + - straight + + sources: + source1: + activity_MBq: 1000 + isotope: CS137 + position: [0, 0, 0] + + detector: + name: dummy + is_isotropic: True + ``` + +=== "Example 5" + + This is an example of a procedural path that is fully specified. See how length is now a list matching the length of the segments. + + ``` yaml + name: Example 5 + speed: 8.33 + acquisition_time: 1 + + path: + length: + - 400 + - 200 + - 400 + segments: + - straight + - turn_left: 45 + - straight + + sources: + source1: + activity_MBq: 1000 + isotope: CS137 + position: [0, 0, 0] + + detector: + name: dummy + is_isotropic: True + ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 5f88fb0..734111c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,6 +30,11 @@ markdown_extensions: - pymdownx.superfences - pymdownx.arithmatex: generic: true + - admonition + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + combine_header_slug: true extra_javascript: - javascripts/mathjax.js @@ -46,4 +51,13 @@ plugins: python: options: show_source: false - show_root_heading: false \ No newline at end of file + show_root_heading: false + +nav: + - Home: index.md + - Installation Guide: installation.md + - Quickstart Guide: quickstart.md + - 'Tutorial: Writing a Config File': config-spec.md + - Explainers: + - explainers/planar_curve.ipynb + - explainers/prefab_roads.ipynb \ No newline at end of file From b882f20358c6bec7a203eabc0c0c20607cb3a13e Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 3 Mar 2026 21:42:19 +0100 Subject: [PATCH 10/20] add basic colouring --- src/pg_rad/configs/logging.yml | 5 ++++- src/pg_rad/logger/logger.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/pg_rad/configs/logging.yml b/src/pg_rad/configs/logging.yml index 5a8f1a5..d53a8c6 100644 --- a/src/pg_rad/configs/logging.yml +++ b/src/pg_rad/configs/logging.yml @@ -3,10 +3,13 @@ disable_existing_loggers: false formatters: simple: format: '%(asctime)s - %(levelname)s: %(message)s' + colored: + '()': pg_rad.logger.logger.ColorFormatter + format: '%(asctime)s - %(levelname)s: %(message)s' handlers: stdout: class: logging.StreamHandler - formatter: simple + formatter: colored stream: ext://sys.stdout loggers: root: diff --git a/src/pg_rad/logger/logger.py b/src/pg_rad/logger/logger.py index 370f35f..2307840 100644 --- a/src/pg_rad/logger/logger.py +++ b/src/pg_rad/logger/logger.py @@ -20,3 +20,21 @@ def setup_logger(log_level: str = "WARNING"): config["loggers"]["root"]["level"] = log_level logging.config.dictConfig(config) + + +class ColorFormatter(logging.Formatter): + # ANSI escape codes + COLORS = { + logging.DEBUG: "\033[36m", # Cyan + logging.INFO: "\033[32m", # Green + logging.WARNING: "\033[33m", # Yellow + logging.ERROR: "\033[31m", # Red + logging.CRITICAL: "\033[41m", # Red background + } + RESET = "\033[0m" + + def format(self, record): + color = self.COLORS.get(record.levelno, self.RESET) + record.levelname = f"{color}{record.levelname}{self.RESET}" + record.msg = f"{record.msg}" + return super().format(record) From b82196e431db4ea8df6f177c7a573988b5a1011a Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 10 Mar 2026 20:44:18 +0100 Subject: [PATCH 11/20] Add flip direction. Change mean to Trapezoidal rule for integration along path. Scale count rate properly with acquisition time --- src/pg_rad/inputparser/parser.py | 87 +++++++++++++++++++++++-------- src/pg_rad/inputparser/specs.py | 5 +- src/pg_rad/landscape/builder.py | 28 +++++++--- src/pg_rad/landscape/director.py | 1 + src/pg_rad/landscape/landscape.py | 3 ++ src/pg_rad/path/path.py | 3 ++ src/pg_rad/physics/fluence.py | 59 ++++++++++++++------- src/pg_rad/simulator/engine.py | 3 +- 8 files changed, 137 insertions(+), 52 deletions(-) diff --git a/src/pg_rad/inputparser/parser.py b/src/pg_rad/inputparser/parser.py index 5b5a111..67fe80e 100644 --- a/src/pg_rad/inputparser/parser.py +++ b/src/pg_rad/inputparser/parser.py @@ -7,7 +7,8 @@ import yaml from pg_rad.exceptions.exceptions import ( MissingConfigKeyError, DimensionError, - InvalidConfigValueError + InvalidConfigValueError, + InvalidYAMLError ) from pg_rad.configs import defaults @@ -45,7 +46,7 @@ class ConfigParser: def parse(self) -> SimulationSpec: self._warn_unknown_keys( - section="global", + section="root", provided=set(self.config.keys()), allowed=self._ALLOWED_ROOT_KEYS, ) @@ -74,7 +75,7 @@ class ConfigParser: data = yaml.safe_load(config_source) if not isinstance(data, dict): - raise ValueError( + raise InvalidYAMLError( "Provided path or string is not a valid YAML representation." ) @@ -86,13 +87,13 @@ class ConfigParser: name=self.config["name"] ) except KeyError as e: - raise MissingConfigKeyError("global", {"name"}) from e + raise MissingConfigKeyError("root", {"name"}) from e def _parse_runtime(self) -> RuntimeSpec: required = {"speed", "acquisition_time"} missing = required - self.config.keys() if missing: - raise MissingConfigKeyError("global", missing) + raise MissingConfigKeyError("root", missing) return RuntimeSpec( speed=float(self.config["speed"]), @@ -116,7 +117,7 @@ class ConfigParser: seed = options.get("seed") if not isinstance(air_density, float) or air_density <= 0: - raise ValueError( + raise InvalidConfigValueError( "options.air_density_kg_per_m3 must be a positive float " "in kg/m^3." ) @@ -124,7 +125,9 @@ class ConfigParser: seed is not None or (isinstance(seed, int) and seed <= 0) ): - raise ValueError("Seed must be a positive integer value.") + raise InvalidConfigValueError( + "Seed must be a positive integer value." + ) return SimulationOptionsSpec( air_density=air_density, @@ -133,14 +136,26 @@ class ConfigParser: def _parse_path(self) -> PathSpec: allowed_csv = {"file", "east_col_name", "north_col_name", "z"} - allowed_proc = {"segments", "length", "z", "alpha"} + allowed_proc = {"segments", "length", "z", "alpha", "direction"} path = self.config.get("path") + + direction = path.get("direction", 'positive') + + if direction == 'positive': + opposite_direction = False + elif direction == 'negative': + opposite_direction = True + else: + raise InvalidConfigValueError( + "Direction must be positive or negative." + ) + if path is None: - raise MissingConfigKeyError("global", {"path"}) + raise MissingConfigKeyError("path") if not isinstance(path, dict): - raise ValueError("Path must be a dictionary.") + raise InvalidConfigValueError("Path must be a dictionary.") if "file" in path: self._warn_unknown_keys( @@ -154,26 +169,31 @@ class ConfigParser: east_col_name=path["east_col_name"], north_col_name=path["north_col_name"], z=path.get("z", 0), + opposite_direction=opposite_direction ) - if "segments" in path: + elif "segments" in path: self._warn_unknown_keys( section="path (procedural)", provided=set(path.keys()), allowed=allowed_proc, ) - return self._parse_procedural_path(path) + return self._parse_procedural_path(path, opposite_direction) - raise ValueError("Invalid path configuration.") + else: + raise InvalidConfigValueError("Invalid path configuration.") def _parse_procedural_path( self, - path: Dict[str, Any] + path: Dict[str, Any], + opposite_direction: bool ) -> ProceduralPathSpec: raw_segments = path.get("segments") if not isinstance(raw_segments, List): - raise ValueError("path.segments must be a list of segments.") + raise InvalidConfigValueError( + "path.segments must be a list of segments." + ) raw_length = path.get("length") @@ -191,7 +211,8 @@ class ConfigParser: angles=angles, lengths=lengths, z=path.get("z", defaults.DEFAULT_PATH_HEIGHT), - alpha=path.get("alpha", defaults.DEFAULT_ALPHA) + alpha=path.get("alpha", defaults.DEFAULT_ALPHA), + opposite_direction=opposite_direction ) def _process_segment_angles( @@ -209,13 +230,17 @@ class ConfigParser: elif isinstance(segment, dict): if len(segment) != 1: - raise ValueError("Invalid segment definition.") + raise InvalidConfigValueError( + "Invalid segment definition." + ) seg_type, angle = list(segment.items())[0] segments.append(seg_type) angles.append(angle) else: - raise ValueError("Invalid segment entry format.") + raise InvalidConfigValueError( + "Invalid segment entry format." + ) return segments, angles @@ -239,12 +264,25 @@ class ConfigParser: return segment_type in {"turn_left", "turn_right"} def _parse_point_sources(self) -> List[PointSourceSpec]: - source_dict = self.config.get("sources", {}) + source_dict = self.config.get("sources") + if source_dict is None: + raise MissingConfigKeyError("sources") + + if not isinstance(source_dict, dict): + raise InvalidConfigValueError( + "sources must have subkeys representing point source names." + ) + specs: List[PointSourceSpec] = [] for name, params in source_dict.items(): - required = {"activity_MBq", "isotope", "position"} + if not isinstance(params, dict): + raise InvalidConfigValueError( + f"sources.{name} is not defined correctly." + f" Must have subkeys {required}" + ) + missing = required - params.keys() if missing: raise MissingConfigKeyError(name, missing) @@ -309,8 +347,13 @@ class ConfigParser: return specs def _parse_detector(self) -> DetectorSpec: - det_dict = self.config.get("detector", {}) + det_dict = self.config.get("detector") required = {"name", "is_isotropic"} + if not isinstance(det_dict, dict): + raise InvalidConfigValueError( + "detector is not specified correctly. Must contain at least" + f"the subkeys {required}" + ) missing = required - det_dict.keys() if missing: @@ -327,7 +370,7 @@ class ConfigParser: elif eff: pass else: - raise ValueError( + raise InvalidConfigValueError( f"The detector {name} not found in library. Either " f"specify {name}.efficiency or " "choose a detector from the following list: " diff --git a/src/pg_rad/inputparser/specs.py b/src/pg_rad/inputparser/specs.py index 6f74f14..db00bae 100644 --- a/src/pg_rad/inputparser/specs.py +++ b/src/pg_rad/inputparser/specs.py @@ -22,7 +22,8 @@ class SimulationOptionsSpec: @dataclass class PathSpec(ABC): - pass + z: int | float + opposite_direction: bool @dataclass @@ -30,7 +31,6 @@ class ProceduralPathSpec(PathSpec): segments: list[str] angles: list[float] lengths: list[int | None] - z: int | float alpha: float @@ -39,7 +39,6 @@ class CSVPathSpec(PathSpec): file: str east_col_name: str north_col_name: str - z: int | float @dataclass diff --git a/src/pg_rad/landscape/builder.py b/src/pg_rad/landscape/builder.py index 2ab78ef..b052734 100644 --- a/src/pg_rad/landscape/builder.py +++ b/src/pg_rad/landscape/builder.py @@ -12,7 +12,8 @@ from pg_rad.inputparser.specs import ( SimulationSpec, CSVPathSpec, AbsolutePointSourceSpec, - RelativePointSourceSpec + RelativePointSourceSpec, + DetectorSpec ) from pg_rad.path.path import Path, path_from_RT90 @@ -30,6 +31,7 @@ class LandscapeBuilder: self._point_sources = [] self._size = None self._air_density = 1.243 + self._detector = None logger.debug(f"LandscapeBuilder initialized: {self.name}") @@ -58,10 +60,12 @@ class LandscapeBuilder: self, sim_spec: SimulationSpec ): - segments = sim_spec.path.segments - lengths = sim_spec.path.lengths - angles = sim_spec.path.angles - alpha = sim_spec.path.alpha + path = sim_spec.path + segments = path.segments + lengths = path.lengths + angles = path.angles + alpha = path.alpha + z = path.z sg = SegmentedRoadGenerator( ds=sim_spec.runtime.speed * sim_spec.runtime.acquisition_time, @@ -76,7 +80,11 @@ class LandscapeBuilder: alpha=alpha ) - self._path = Path(list(zip(x, y))) + self._path = Path( + list(zip(x, y)), + z=z, + opposite_direction=path.opposite_direction + ) self._fit_landscape_to_path() return self @@ -89,7 +97,8 @@ class LandscapeBuilder: df=df, east_col=spec.east_col_name, north_col=spec.north_col_name, - z=spec.z + z=spec.z, + opposite_direction=spec.opposite_direction ) self._fit_landscape_to_path() @@ -151,6 +160,10 @@ class LandscapeBuilder: name=s.name )) + def set_detector(self, spec: DetectorSpec) -> Self: + self._detector = spec + return self + def _fit_landscape_to_path(self) -> None: """The size of the landscape will be updated if 1) _size is not set, or @@ -221,6 +234,7 @@ class LandscapeBuilder: name=self.name, path=self._path, point_sources=self._point_sources, + detector=self._detector, size=self._size, air_density=self._air_density ) diff --git a/src/pg_rad/landscape/director.py b/src/pg_rad/landscape/director.py index f8d7e68..d75c071 100644 --- a/src/pg_rad/landscape/director.py +++ b/src/pg_rad/landscape/director.py @@ -45,5 +45,6 @@ class LandscapeDirector: sim_spec=config, ) lb.set_point_sources(*config.point_sources) + lb.set_detector(config.detector) landscape = lb.build() return landscape diff --git a/src/pg_rad/landscape/landscape.py b/src/pg_rad/landscape/landscape.py index 057c761..a386815 100644 --- a/src/pg_rad/landscape/landscape.py +++ b/src/pg_rad/landscape/landscape.py @@ -1,6 +1,7 @@ import logging from pg_rad.path.path import Path +from pg_rad.detector.detectors import AngularDetector, IsotropicDetector from pg_rad.objects.sources import PointSource @@ -17,6 +18,7 @@ class Landscape: path: Path, air_density: float, point_sources: list[PointSource], + detector: IsotropicDetector | AngularDetector, size: tuple[int, int, int] ): """Initialize a landscape. @@ -34,5 +36,6 @@ class Landscape: self.point_sources = point_sources self.size = size self.air_density = air_density + self.detector = detector logger.debug(f"Landscape created: {self.name}") diff --git a/src/pg_rad/path/path.py b/src/pg_rad/path/path.py index 5c9ade3..cf68252 100644 --- a/src/pg_rad/path/path.py +++ b/src/pg_rad/path/path.py @@ -42,6 +42,7 @@ class Path: def __init__( self, coord_list: Sequence[tuple[float, float]], + opposite_direction: bool, z: float = 0., z_box: float = 50. ): @@ -74,6 +75,8 @@ class Path: ] self.z = z + self.opposite_direction = opposite_direction + self.size = ( np.ceil(max(self.x_list)), np.ceil(max(self.y_list)), diff --git a/src/pg_rad/physics/fluence.py b/src/pg_rad/physics/fluence.py index 16d4f62..874fb4e 100644 --- a/src/pg_rad/physics/fluence.py +++ b/src/pg_rad/physics/fluence.py @@ -47,14 +47,14 @@ def phi( return phi_r -def calculate_fluence_at( +def calculate_count_rate_per_second( landscape: "Landscape", pos: np.ndarray, detector: IsotropicDetector | AngularDetector, tangent_vectors: np.ndarray, scaling=1E6 ): - """Compute fluence at an arbitrary position in the landscape. + """Compute count rate in s^-1 m^-2 at a position in the landscape. Args: landscape (Landscape): The landscape to compute. @@ -63,7 +63,7 @@ def calculate_fluence_at( Detector object, needed to compute correct efficiency. Returns: - total_phi (np.ndarray): (N,) array of fluences. + total_phi (np.ndarray): (N,) array of count rates per second. """ pos = np.atleast_2d(pos) total_phi = np.zeros(pos.shape[0]) @@ -103,30 +103,42 @@ def calculate_fluence_at( def calculate_fluence_along_path( landscape: "Landscape", detector: "IsotropicDetector | AngularDetector", - points_per_segment: int = 10 + acquisition_time: int, + points_per_segment: int = 10, ) -> Tuple[np.ndarray, np.ndarray]: + """Compute the fluence along a full path in the landscape. + + Args: + landscape (Landscape): _description_ + detector (IsotropicDetector | AngularDetector): _description_ + points_per_segment (int, optional): _description_. Defaults to 100. + + Returns: + Tuple[np.ndarray, np.ndarray]: _description_ + """ + path = landscape.path num_points = len(path.x_list) + num_segments = len(path.segments) - dx = np.diff(path.x_list) - dy = np.diff(path.y_list) - segment_lengths = np.sqrt(dx**2 + dy**2) - + segment_lengths = np.array([seg.length for seg in path.segments]) + ds = segment_lengths[0] original_distances = np.zeros(num_points) original_distances[1:] = np.cumsum(segment_lengths) # arc lengths at which to evaluate the path - s = np.linspace( - 0, - original_distances[-1], - num=num_points * points_per_segment) + 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(xnew.shape, path.z) + 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) + # to compute the angle between sources and the direction of travel, we # compute tangent vectors along the path. dx_ds = np.gradient(xnew, s) @@ -134,10 +146,19 @@ def calculate_fluence_along_path( tangent_vectors = np.c_[dx_ds, dy_ds, np.zeros_like(dx_ds)] tangent_vectors /= np.linalg.norm(tangent_vectors, axis=1, keepdims=True) - phi_result = calculate_fluence_at( - landscape, - full_positions, - detector, - tangent_vectors) + count_rate = calculate_count_rate_per_second( + landscape, full_positions, detector, tangent_vectors + ) - return s, phi_result + count_rate *= (acquisition_time / points_per_segment) + + count_rate_segs = count_rate.reshape(num_segments, points_per_segment) + integrated = np.trapezoid( + count_rate_segs, + dx=ds/points_per_segment, + axis=1 + ) + + result = np.zeros(num_points) + result[1:] = integrated + return original_distances, result diff --git a/src/pg_rad/simulator/engine.py b/src/pg_rad/simulator/engine.py index c5aa754..4a05e5f 100644 --- a/src/pg_rad/simulator/engine.py +++ b/src/pg_rad/simulator/engine.py @@ -42,7 +42,8 @@ class SimulationEngine: def _calculate_count_rate_along_path(self) -> CountRateOutput: arc_length, phi = calculate_fluence_along_path( self.landscape, - self.detector + self.detector, + acquisition_time=self.runtime_spec.acquisition_time ) return CountRateOutput(arc_length, phi) From 938b3a7afcb548d5f7cf254662c927eaf72ca8cc Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 10 Mar 2026 20:44:45 +0100 Subject: [PATCH 12/20] update exceptions and main.py (including test case) --- src/pg_rad/exceptions/exceptions.py | 10 +++++++++- src/pg_rad/main.py | 21 ++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/pg_rad/exceptions/exceptions.py b/src/pg_rad/exceptions/exceptions.py index aac816d..ae7a72e 100644 --- a/src/pg_rad/exceptions/exceptions.py +++ b/src/pg_rad/exceptions/exceptions.py @@ -10,6 +10,10 @@ class InvalidCSVError(DataLoadError): """Raised when a file is not a valid CSV.""" +class InvalidYAMLError(DataLoadError): + """Raised when a file is not a valid YAML.""" + + class OutOfBoundsError(Exception): """Raised when an object is attempted to be placed out of bounds.""" @@ -18,7 +22,11 @@ class MissingConfigKeyError(KeyError): """Raised when a (nested) config key is missing in the config.""" def __init__(self, key, subkey=None): if subkey: - self.message = f"Missing key in {key}: {', '.join(list(subkey))}" + if isinstance(subkey, str): + pass + elif isinstance(subkey, set): + subkey = ', '.join(list(subkey)) + self.message = f"Missing key in {key}: {subkey}" else: self.message = f"Missing key: {key}" diff --git a/src/pg_rad/main.py b/src/pg_rad/main.py index e944fb5..bfc741c 100644 --- a/src/pg_rad/main.py +++ b/src/pg_rad/main.py @@ -3,7 +3,6 @@ import logging import sys from pandas.errors import ParserError -from yaml import YAMLError from pg_rad.detector.builder import DetectorBuilder from pg_rad.exceptions.exceptions import ( @@ -11,7 +10,8 @@ from pg_rad.exceptions.exceptions import ( OutOfBoundsError, DimensionError, InvalidConfigValueError, - InvalidIsotopeError + InvalidIsotopeError, + InvalidYAMLError ) from pg_rad.logger.logger import setup_logger from pg_rad.inputparser.parser import ConfigParser @@ -59,12 +59,14 @@ def main(): length: 1000 segments: - straight + - turn_left + direction: negative sources: test_source: - activity_MBq: 1000 - position: [500, 100, 0] - isotope: CS137 + activity_MBq: 100 + position: [250, 100, 0] + isotope: Cs137 detector: name: dummy @@ -100,8 +102,7 @@ def main(): plotter.plot() except ( MissingConfigKeyError, - KeyError, - YAMLError, + KeyError ) as e: logger.critical(e) logger.critical( @@ -125,8 +126,10 @@ def main(): except ( FileNotFoundError, - ParserError - ): + ParserError, + InvalidYAMLError + ) as e: + logger.critical(e) sys.exit(1) From 7dec54fa1c9d66c5d7b504fbcf4521dfc5dd3b8e Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Tue, 10 Mar 2026 20:45:35 +0100 Subject: [PATCH 13/20] update plotter to add direction of travel arrow and detector info --- src/pg_rad/plotting/landscape_plotter.py | 37 +++++++++++++++++++++++- src/pg_rad/plotting/result_plotter.py | 31 ++++++++++++++++---- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/pg_rad/plotting/landscape_plotter.py b/src/pg_rad/plotting/landscape_plotter.py index 4a990f3..aebfc36 100644 --- a/src/pg_rad/plotting/landscape_plotter.py +++ b/src/pg_rad/plotting/landscape_plotter.py @@ -69,7 +69,7 @@ class LandscapeSlicePlotter: ax.set_ylabel("Y [m]") ax.set_title(f"Landscape (top-down, z = {self.z})") - def _draw_path(self, ax, landscape): + def _draw_path(self, ax, landscape: Landscape): if landscape.path.z <= self.z: ax.plot( landscape.path.x_list, @@ -79,6 +79,9 @@ class LandscapeSlicePlotter: markersize=3, linewidth=1 ) + if len(landscape.path.x_list) >= 2: + ax = self._draw_path_direction_arrow(ax, landscape.path) + else: logger.warning( "Path is above the slice height z." @@ -113,3 +116,35 @@ class LandscapeSlicePlotter: f"Source {s.name} is above slice height z." "It will not show on the plot." ) + + def _draw_path_direction_arrow(self, ax, path) -> Axes: + inset_ax = ax.inset_axes([0.8, 0.1, 0.15, 0.15]) + + x_start, y_start = path.x_list[0], path.y_list[0] + x_end, y_end = path.x_list[1], path.y_list[1] + + dx = x_end - x_start + dy = y_end - y_start + + if path.opposite_direction: + dx = -dx + dy = -dy + + length = 10 + dx_norm = dx / (dx**2 + dy**2)**0.5 * length + dy_norm = dy / (dx**2 + dy**2)**0.5 * length + + inset_ax.arrow( + 0, 0, + dx_norm, dy_norm, + head_width=5, head_length=5, + fc='red', ec='red', + zorder=4, linewidth=1 + ) + + inset_ax.set_xlim(-2*length, 2*length) + inset_ax.set_ylim(-2*length, 2*length) + inset_ax.set_title("Direction", fontsize=8) + inset_ax.set_xticks([]) + inset_ax.set_yticks([]) + return ax diff --git a/src/pg_rad/plotting/result_plotter.py b/src/pg_rad/plotting/result_plotter.py index 51ba366..bcd4a65 100644 --- a/src/pg_rad/plotting/result_plotter.py +++ b/src/pg_rad/plotting/result_plotter.py @@ -16,10 +16,10 @@ class ResultPlotter: fig = plt.figure(figsize=(12, 10), constrained_layout=True) fig.suptitle(self.landscape.name) gs = GridSpec( - 3, + 4, 2, width_ratios=[0.5, 0.5], - height_ratios=[0.7, 0.15, 0.15], + height_ratios=[0.7, 0.1, 0.1, 0.1], hspace=0.2) ax1 = fig.add_subplot(gs[0, 0]) @@ -32,9 +32,11 @@ class ResultPlotter: self._draw_table(ax3) ax4 = fig.add_subplot(gs[2, :]) - self._draw_source_table(ax4) + self._draw_detector_table(ax4) + + ax5 = fig.add_subplot(gs[3, :]) + self._draw_source_table(ax5) - plt.tight_layout() plt.show() def _plot_landscape(self, ax, z): @@ -57,7 +59,26 @@ class ResultPlotter: cols = ('Parameter', 'Value') data = [ ["Air density (kg/m^3)", round(self.landscape.air_density, 3)], - ["Total path length (m)", round(self.landscape.path.length, 3)] + ["Total path length (m)", round(self.landscape.path.length, 3)], + ] + + ax.table( + cellText=data, + colLabels=cols, + loc='center' + ) + + return ax + + def _draw_detector_table(self, ax): + det = self.landscape.detector + print(det) + ax.set_axis_off() + ax.set_title('Detector') + cols = ('Parameter', 'Value') + data = [ + ["Name", det.name], + ["Type", det.efficiency], ] ax.table( From 09e74051f05fb5a7a465c350d3c7c87e902d67ce Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Wed, 11 Mar 2026 09:51:11 +0100 Subject: [PATCH 14/20] improve naming of integrated count calculation function --- src/pg_rad/physics/fluence.py | 7 ++++--- src/pg_rad/simulator/engine.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pg_rad/physics/fluence.py b/src/pg_rad/physics/fluence.py index 874fb4e..20a232d 100644 --- a/src/pg_rad/physics/fluence.py +++ b/src/pg_rad/physics/fluence.py @@ -100,13 +100,13 @@ def calculate_count_rate_per_second( return total_phi -def calculate_fluence_along_path( +def calculate_counts_along_path( landscape: "Landscape", detector: "IsotropicDetector | AngularDetector", acquisition_time: int, points_per_segment: int = 10, ) -> Tuple[np.ndarray, np.ndarray]: - """Compute the fluence along a full path in the landscape. + """Compute the counts recorded in each acquisition period in the landscape. Args: landscape (Landscape): _description_ @@ -114,7 +114,8 @@ def calculate_fluence_along_path( points_per_segment (int, optional): _description_. Defaults to 100. Returns: - Tuple[np.ndarray, np.ndarray]: _description_ + Tuple[np.ndarray, np.ndarray]: Array of acquisition points and + integrated count rates. """ path = landscape.path diff --git a/src/pg_rad/simulator/engine.py b/src/pg_rad/simulator/engine.py index 4a05e5f..3af7d9e 100644 --- a/src/pg_rad/simulator/engine.py +++ b/src/pg_rad/simulator/engine.py @@ -7,7 +7,7 @@ from pg_rad.simulator.outputs import ( SourceOutput ) from pg_rad.detector.detectors import IsotropicDetector, AngularDetector -from pg_rad.physics.fluence import calculate_fluence_along_path +from pg_rad.physics.fluence import calculate_counts_along_path from pg_rad.utils.projection import minimal_distance_to_path from pg_rad.inputparser.specs import RuntimeSpec, SimulationOptionsSpec @@ -40,7 +40,7 @@ class SimulationEngine: ) def _calculate_count_rate_along_path(self) -> CountRateOutput: - arc_length, phi = calculate_fluence_along_path( + arc_length, phi = calculate_counts_along_path( self.landscape, self.detector, acquisition_time=self.runtime_spec.acquisition_time From 1dc553d2e2bc2784f5680bdc35f9149a1b99ea5b Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Thu, 12 Mar 2026 14:32:21 +0100 Subject: [PATCH 15/20] update isotope lookup to tabular format. Add primary gamma energy to results plotting --- src/pg_rad/configs/filepaths.py | 1 + src/pg_rad/data/isotopes.csv | 6 ++++ src/pg_rad/inputparser/parser.py | 10 ++++-- src/pg_rad/isotopes/isotope.py | 52 +++++++++++++++++---------- src/pg_rad/main.py | 3 +- src/pg_rad/objects/sources.py | 4 +-- src/pg_rad/plotting/result_plotter.py | 2 +- src/pg_rad/simulator/engine.py | 1 + src/pg_rad/simulator/outputs.py | 1 + 9 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 src/pg_rad/data/isotopes.csv diff --git a/src/pg_rad/configs/filepaths.py b/src/pg_rad/configs/filepaths.py index e8103af..385a92b 100644 --- a/src/pg_rad/configs/filepaths.py +++ b/src/pg_rad/configs/filepaths.py @@ -1,3 +1,4 @@ ATTENUATION_TABLE = 'attenuation_table.csv' +ISOTOPE_TABLE = 'isotopes.csv' TEST_EXP_DATA = 'test_path_coords.csv' LOGGING_CONFIG = 'logging.yml' diff --git a/src/pg_rad/data/isotopes.csv b/src/pg_rad/data/isotopes.csv new file mode 100644 index 0000000..282a3b9 --- /dev/null +++ b/src/pg_rad/data/isotopes.csv @@ -0,0 +1,6 @@ +isotope,gamma_energy_keV,branching_ratio +Cs134,604.7,0.976 +Cs134,795.9,0.855 +Cs137,661.657,0.851 +Co60,1173.228,1.0 +Co60,1332.5,1.0 \ No newline at end of file diff --git a/src/pg_rad/inputparser/parser.py b/src/pg_rad/inputparser/parser.py index 67fe80e..c6dc8b2 100644 --- a/src/pg_rad/inputparser/parser.py +++ b/src/pg_rad/inputparser/parser.py @@ -11,6 +11,7 @@ from pg_rad.exceptions.exceptions import ( InvalidYAMLError ) from pg_rad.configs import defaults +from pg_rad.isotopes.isotope import get_isotope from .specs import ( MetadataSpec, @@ -276,7 +277,9 @@ class ConfigParser: specs: List[PointSourceSpec] = [] for name, params in source_dict.items(): - required = {"activity_MBq", "isotope", "position"} + required = { + "activity_MBq", "isotope", "position", "gamma_energy_keV" + } if not isinstance(params, dict): raise InvalidConfigValueError( f"sources.{name} is not defined correctly." @@ -288,7 +291,10 @@ class ConfigParser: raise MissingConfigKeyError(name, missing) activity = params.get("activity_MBq") - isotope = params.get("isotope") + isotope_name = params.get("isotope") + gamma_energy_keV = params.get("gamma_energy_keV") + + isotope = get_isotope(isotope_name, gamma_energy_keV) if not isinstance(activity, int | float) or activity <= 0: raise InvalidConfigValueError( diff --git a/src/pg_rad/isotopes/isotope.py b/src/pg_rad/isotopes/isotope.py index ec501af..c73fc26 100644 --- a/src/pg_rad/isotopes/isotope.py +++ b/src/pg_rad/isotopes/isotope.py @@ -1,5 +1,8 @@ -from typing import Dict, Type +from importlib.resources import files +from pandas import read_csv + +from pg_rad.configs.filepaths import ISOTOPE_TABLE from pg_rad.exceptions.exceptions import InvalidIsotopeError from pg_rad.physics.attenuation import get_mass_attenuation_coeff @@ -30,22 +33,35 @@ class Isotope: self.mu_mass_air = get_mass_attenuation_coeff(E / 1000) -class CS137(Isotope): - def __init__(self): - super().__init__( - name="Cs-137", - E=661.66, - b=0.851 +def get_isotope(isotope: str, energy_gamma_keV: float) -> Isotope: + """Lazy factory function to create isotope objects.""" + path = files('pg_rad.data').joinpath(ISOTOPE_TABLE) + df = read_csv(path) + + isotope_df = df[df['isotope'] == isotope] + + if isotope_df.empty: + raise InvalidIsotopeError(f"No data available for isotope {isotope}.") + + tol = 0.01 * energy_gamma_keV + closest_energy = isotope_df[ + (isotope_df['gamma_energy_keV'] >= energy_gamma_keV - tol) & + (isotope_df['gamma_energy_keV'] <= energy_gamma_keV + tol) + ] + + if closest_energy.empty: + available_gammas = ', '.join( + str(x)+' keV' for x in isotope_df['gamma_energy_keV'].to_list() + ) + raise InvalidIsotopeError( + f"No gamma of {energy_gamma_keV}±{tol} keV " + f"found for isotope {isotope}. " + f"Available gammas are: {available_gammas}" ) - -preset_isotopes: Dict[str, Type[Isotope]] = { - "Cs137": CS137 -} - - -def get_isotope(isotope_str: str) -> Isotope: - """Lazy factory function to create isotope objects.""" - if isotope_str not in preset_isotopes: - raise InvalidIsotopeError(f"Unknown isotope: {isotope_str}") - return preset_isotopes[isotope_str]() + matched_row = closest_energy.iloc[0] + return Isotope( + name=isotope, + E=matched_row['gamma_energy_keV'], + b=matched_row['branching_ratio'] + ) diff --git a/src/pg_rad/main.py b/src/pg_rad/main.py index bfc741c..b31db63 100644 --- a/src/pg_rad/main.py +++ b/src/pg_rad/main.py @@ -59,7 +59,7 @@ def main(): length: 1000 segments: - straight - - turn_left + - turn_left: 45 direction: negative sources: @@ -67,6 +67,7 @@ def main(): activity_MBq: 100 position: [250, 100, 0] isotope: Cs137 + gamma_energy_keV: 661 detector: name: dummy diff --git a/src/pg_rad/objects/sources.py b/src/pg_rad/objects/sources.py index 74a0ad3..08e4146 100644 --- a/src/pg_rad/objects/sources.py +++ b/src/pg_rad/objects/sources.py @@ -12,7 +12,7 @@ class PointSource(BaseObject): def __init__( self, activity_MBq: int, - isotope: str, + isotope: Isotope, position: tuple[float, float, float] = (0, 0, 0), name: str | None = None, color: str = 'red' @@ -40,7 +40,7 @@ class PointSource(BaseObject): super().__init__(position, name, color) self.activity = activity_MBq - self.isotope: Isotope = get_isotope(isotope) + self.isotope = isotope logger.debug(f"Source created: {self.name}") diff --git a/src/pg_rad/plotting/result_plotter.py b/src/pg_rad/plotting/result_plotter.py index bcd4a65..5163913 100644 --- a/src/pg_rad/plotting/result_plotter.py +++ b/src/pg_rad/plotting/result_plotter.py @@ -105,7 +105,7 @@ class ResultPlotter: data = [ [ s.name, - s.isotope, + s.isotope+f" ({s.primary_gamma} keV)", s.activity, "("+", ".join(f"{val:.2f}" for val in s.position)+")", round(s.dist_from_path, 2) diff --git a/src/pg_rad/simulator/engine.py b/src/pg_rad/simulator/engine.py index 3af7d9e..a7d4d9a 100644 --- a/src/pg_rad/simulator/engine.py +++ b/src/pg_rad/simulator/engine.py @@ -62,6 +62,7 @@ class SimulationEngine: SourceOutput( s.name, s.isotope.name, + s.isotope.E, s.activity, s.pos, dist_to_path) diff --git a/src/pg_rad/simulator/outputs.py b/src/pg_rad/simulator/outputs.py index 395317f..5186a5e 100644 --- a/src/pg_rad/simulator/outputs.py +++ b/src/pg_rad/simulator/outputs.py @@ -13,6 +13,7 @@ class CountRateOutput: class SourceOutput: name: str isotope: str + primary_gamma: float activity: float position: Tuple[float, float, float] dist_from_path: float From 1e81570cf45de1f4f575311b5c325f6ef32faed8 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Thu, 12 Mar 2026 14:34:40 +0100 Subject: [PATCH 16/20] clean up imports and remove hard-coded __init__ refs --- src/pg_rad/configs/__init__.py | 1 - src/pg_rad/data/__init__.py | 1 - src/pg_rad/dataloader/__init__.py | 8 -------- src/pg_rad/exceptions/__init__.py | 10 ---------- src/pg_rad/inputparser/__init__.py | 0 src/pg_rad/isotopes/__init__.py | 9 --------- src/pg_rad/logger/__init__.py | 5 ----- src/pg_rad/objects/__init__.py | 11 ----------- src/pg_rad/objects/sources.py | 2 +- src/pg_rad/path/__init__.py | 8 -------- src/pg_rad/physics/__init__.py | 12 ------------ src/pg_rad/plotting/__init__.py | 7 ------- 12 files changed, 1 insertion(+), 73 deletions(-) create mode 100644 src/pg_rad/inputparser/__init__.py diff --git a/src/pg_rad/configs/__init__.py b/src/pg_rad/configs/__init__.py index a9a2c5b..e69de29 100644 --- a/src/pg_rad/configs/__init__.py +++ b/src/pg_rad/configs/__init__.py @@ -1 +0,0 @@ -__all__ = [] diff --git a/src/pg_rad/data/__init__.py b/src/pg_rad/data/__init__.py index a9a2c5b..e69de29 100644 --- a/src/pg_rad/data/__init__.py +++ b/src/pg_rad/data/__init__.py @@ -1 +0,0 @@ -__all__ = [] diff --git a/src/pg_rad/dataloader/__init__.py b/src/pg_rad/dataloader/__init__.py index 0f01fd4..e69de29 100644 --- a/src/pg_rad/dataloader/__init__.py +++ b/src/pg_rad/dataloader/__init__.py @@ -1,8 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] - -from pg_rad.dataloader import dataloader - -from pg_rad.dataloader.dataloader import (load_data,) - -__all__ = ['dataloader', 'load_data'] diff --git a/src/pg_rad/exceptions/__init__.py b/src/pg_rad/exceptions/__init__.py index 99b24a1..e69de29 100644 --- a/src/pg_rad/exceptions/__init__.py +++ b/src/pg_rad/exceptions/__init__.py @@ -1,10 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] - -from pg_rad.exceptions import exceptions - -from pg_rad.exceptions.exceptions import (ConvergenceError, DataLoadError, - InvalidCSVError, OutOfBoundsError,) - -__all__ = ['ConvergenceError', 'DataLoadError', 'InvalidCSVError', - 'OutOfBoundsError', 'exceptions'] diff --git a/src/pg_rad/inputparser/__init__.py b/src/pg_rad/inputparser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pg_rad/isotopes/__init__.py b/src/pg_rad/isotopes/__init__.py index b926aa8..e69de29 100644 --- a/src/pg_rad/isotopes/__init__.py +++ b/src/pg_rad/isotopes/__init__.py @@ -1,9 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] - -from pg_rad.isotopes import isotope - -from pg_rad.isotopes.isotope import (CS137, Isotope, get_isotope, - preset_isotopes,) - -__all__ = ['CS137', 'Isotope', 'get_isotope', 'isotope', 'preset_isotopes'] diff --git a/src/pg_rad/logger/__init__.py b/src/pg_rad/logger/__init__.py index 57b4e3f..e69de29 100644 --- a/src/pg_rad/logger/__init__.py +++ b/src/pg_rad/logger/__init__.py @@ -1,5 +0,0 @@ -from pg_rad.logger import logger - -from pg_rad.logger.logger import (setup_logger,) - -__all__ = ['logger', 'setup_logger'] diff --git a/src/pg_rad/objects/__init__.py b/src/pg_rad/objects/__init__.py index 92edc21..e69de29 100644 --- a/src/pg_rad/objects/__init__.py +++ b/src/pg_rad/objects/__init__.py @@ -1,11 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] - -from pg_rad.objects import objects -from pg_rad.objects import sources - -from pg_rad.objects.objects import (BaseObject,) -from pg_rad.objects.sources import (PointSource,) - -__all__ = ['BaseObject', 'PointSource', 'objects', - 'sources'] diff --git a/src/pg_rad/objects/sources.py b/src/pg_rad/objects/sources.py index 08e4146..354357f 100644 --- a/src/pg_rad/objects/sources.py +++ b/src/pg_rad/objects/sources.py @@ -1,7 +1,7 @@ import logging from .objects import BaseObject -from pg_rad.isotopes.isotope import Isotope, get_isotope +from pg_rad.isotopes.isotope import Isotope logger = logging.getLogger(__name__) diff --git a/src/pg_rad/path/__init__.py b/src/pg_rad/path/__init__.py index 8874a3d..e69de29 100644 --- a/src/pg_rad/path/__init__.py +++ b/src/pg_rad/path/__init__.py @@ -1,8 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] - -from pg_rad.path import path - -from pg_rad.path.path import (Path, PathSegment, path_from_RT90,) - -__all__ = ['Path', 'PathSegment', 'path', 'path_from_RT90'] diff --git a/src/pg_rad/physics/__init__.py b/src/pg_rad/physics/__init__.py index dd98ebe..e69de29 100644 --- a/src/pg_rad/physics/__init__.py +++ b/src/pg_rad/physics/__init__.py @@ -1,12 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] -from pg_rad.physics import attenuation -from pg_rad.physics import fluence - -from pg_rad.physics.attenuation import (get_mass_attenuation_coeff,) -from pg_rad.physics.fluence import (calculate_fluence_along_path, - calculate_fluence_at, phi,) - -__all__ = ['attenuation', 'calculate_fluence_along_path', - 'calculate_fluence_at', 'fluence', 'get_mass_attenuation_coeff', - 'phi'] diff --git a/src/pg_rad/plotting/__init__.py b/src/pg_rad/plotting/__init__.py index 271d2c9..e69de29 100644 --- a/src/pg_rad/plotting/__init__.py +++ b/src/pg_rad/plotting/__init__.py @@ -1,7 +0,0 @@ -# do not expose internal logger when running mkinit -__ignore__ = ["logger"] -from pg_rad.plotting import landscape_plotter - -from pg_rad.plotting.landscape_plotter import (LandscapeSlicePlotter,) - -__all__ = ['LandscapeSlicePlotter', 'landscape_plotter'] From 890570e148bb1516a1768c08bdb760c83b6933b6 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 20 Mar 2026 09:21:40 +0100 Subject: [PATCH 17/20] simplify detector object. Add efficiency libraries and lookup interpolators --- .../data/angular_efficiencies/LU_HPGe_90.csv | 37 +++++++++++ src/pg_rad/data/detectors.csv | 4 ++ .../data/field_efficiencies/LU_HPGe_90.csv | 17 +++++ .../data/field_efficiencies/LU_NaI_3inch.csv | 64 +++++++++++++++++++ src/pg_rad/data/field_efficiencies/dummy.csv | 3 + src/pg_rad/detector/builder.py | 20 ------ src/pg_rad/detector/detector.py | 43 +++++++++++++ src/pg_rad/detector/detectors.py | 38 ----------- src/pg_rad/inputparser/parser.py | 38 ++--------- src/pg_rad/inputparser/specs.py | 2 - src/pg_rad/utils/interpolators.py | 56 ++++++++++++++++ 11 files changed, 228 insertions(+), 94 deletions(-) create mode 100644 src/pg_rad/data/angular_efficiencies/LU_HPGe_90.csv create mode 100644 src/pg_rad/data/detectors.csv create mode 100644 src/pg_rad/data/field_efficiencies/LU_HPGe_90.csv create mode 100644 src/pg_rad/data/field_efficiencies/LU_NaI_3inch.csv create mode 100644 src/pg_rad/data/field_efficiencies/dummy.csv delete mode 100644 src/pg_rad/detector/builder.py create mode 100644 src/pg_rad/detector/detector.py delete mode 100644 src/pg_rad/detector/detectors.py create mode 100644 src/pg_rad/utils/interpolators.py diff --git a/src/pg_rad/data/angular_efficiencies/LU_HPGe_90.csv b/src/pg_rad/data/angular_efficiencies/LU_HPGe_90.csv new file mode 100644 index 0000000..e71ec6d --- /dev/null +++ b/src/pg_rad/data/angular_efficiencies/LU_HPGe_90.csv @@ -0,0 +1,37 @@ +angle,662,1173,1332 +0,0.015,0.030,0.033 +10,0.011,0.021,0.024 +20,0.086,0.127,0.146 +30,0.294,0.356,0.397 +40,0.661,0.700,0.734 +50,1.054,1.057,1.057 +60,1.154,1.140,1.137 +70,1.186,1.152,1.138 +80,1.151,1.114,1.097 +90,1.000,1.000,1.000 +100,1.020,1.040,1.047 +110,1.074,1.093,1.103 +120,1.113,1.092,1.102 +130,1.139,1.122,1.113 +140,1.146,1.152,1.140 +150,1.113,1.118,1.104 +160,1.113,1.096,1.099 +170,1.091,1.076,1.083 +180,1.076,1.066,1.078 +-170,1.102,1.091,1.093 +-160,1.122,1.100,1.102 +-150,1.128,1.105,1.093 +-140,1.144,1.112,1.123 +-130,1.140,1.117,1.095 +-120,1.146,1.127,1.098 +-110,1.068,1.068,1.045 +-100,1.013,1.025,1.016 +-90,1.004,1.018,1.021 +-80,1.150,1.137,1.132 +-70,1.184,1.167,1.164 +-60,1.158,1.140,1.138 +-50,1.090,1.068,1.064 +-40,0.595,0.620,0.631 +-30,0.332,0.430,0.430 +-20,0.055,0.081,0.096 +-10,0.009,0.018,0.019 \ No newline at end of file diff --git a/src/pg_rad/data/detectors.csv b/src/pg_rad/data/detectors.csv new file mode 100644 index 0000000..f5ae726 --- /dev/null +++ b/src/pg_rad/data/detectors.csv @@ -0,0 +1,4 @@ +name,type,is_isotropic +dummy,NaI,true +LU_NaI_3inch,NaI,true +LU_HPGe_90,HPGe,false \ No newline at end of file diff --git a/src/pg_rad/data/field_efficiencies/LU_HPGe_90.csv b/src/pg_rad/data/field_efficiencies/LU_HPGe_90.csv new file mode 100644 index 0000000..c94f571 --- /dev/null +++ b/src/pg_rad/data/field_efficiencies/LU_HPGe_90.csv @@ -0,0 +1,17 @@ +energy_keV,field_efficiency_m2,uncertainty +59.5,0.00140,0.00005 +81.0,0.00310,0.00010 +122.1,0.00420,0.00013 +136.5,0.00428,0.00017 +160.6,0.00426,0.00030 +223.2,0.00418,0.00024 +276.4,0.00383,0.00012 +302.9,0.00370,0.00012 +356.0,0.00338,0.00010 +383.8,0.00323,0.00010 +511.0,0.00276,0.00008 +661.7,0.00241,0.00007 +834.8,0.00214,0.00007 +1173.2,0.00179,0.00005 +1274.5,0.00168,0.00005 +1332.5,0.00166,0.00005 \ No newline at end of file diff --git a/src/pg_rad/data/field_efficiencies/LU_NaI_3inch.csv b/src/pg_rad/data/field_efficiencies/LU_NaI_3inch.csv new file mode 100644 index 0000000..e3f4b1f --- /dev/null +++ b/src/pg_rad/data/field_efficiencies/LU_NaI_3inch.csv @@ -0,0 +1,64 @@ +energy_keV,field_efficiency_m2 +10,5.50129E-09 +11.22018454,2.88553E-07 +12.58925412,4.81878E-06 +14.12537545,3.55112E-05 +15.84893192,0.000146367 +17.7827941,0.000397029 +19.95262315,0.000803336 +22.38721139,0.00131657 +25.11886432,0.001862377 +28.18382931,0.002373449 +31.6227766,0.002811046 +33.164,0.00269554 +33.164,0.002698792 +35.48133892,0.002509993 +39.81071706,0.002801304 +44.66835922,0.003015877 +50.11872336,0.003227431 +56.23413252,0.00341077 +63.09573445,0.003562051 +70.79457844,0.00368852 +79.43282347,0.003788875 +89,0.003867423 +100,0.003925025 +112.2018454,0.003967222 +125.8925412,0.003991551 +141.2537545,0.004000729 +158.4893192,0.003993145 +177.827941,0.003969163 +199.5262315,0.003925289 +223.8721139,0.003856247 +251.1886432,0.00375596 +281.8382931,0.003619634 +316.227766,0.003446087 +354.8133892,0.003242691 +398.1071706,0.003021761 +446.6835922,0.002791816 +501.1872336,0.002568349 +562.3413252,0.002350052 +630.9573445,0.002147662 +707.9457844,0.001957893 +794.3282347,0.001785694 +891,0.001626634 +1000,0.001482571 +1122.018454,0.00135047 +1258.925412,0.001231358 +1412.537545,0.001116695 +1584.893192,0.001011833 +1778.27941,0.000917017 +1995.262315,0.000828435 +2238.721139,0.000746854 +2511.886432,0.000672573 +2818.382931,0.00060493 +3162.27766,0.000544458 +3548.133892,0.000488446 +3981.071706,0.000438438 +4466.835922,0.000392416 +5011.872336,0.00035092 +5623.413252,0.000313959 +6309.573445,0.000279409 +7079.457844,0.000247794 +7943.282347,0.000218768 +8913,0.000190209 +10000,0.000164309 \ No newline at end of file diff --git a/src/pg_rad/data/field_efficiencies/dummy.csv b/src/pg_rad/data/field_efficiencies/dummy.csv new file mode 100644 index 0000000..bd40665 --- /dev/null +++ b/src/pg_rad/data/field_efficiencies/dummy.csv @@ -0,0 +1,3 @@ +energy_keV,field_efficiency_m2 +0,1.0 +10000,1.0 \ No newline at end of file diff --git a/src/pg_rad/detector/builder.py b/src/pg_rad/detector/builder.py deleted file mode 100644 index 940a2f9..0000000 --- a/src/pg_rad/detector/builder.py +++ /dev/null @@ -1,20 +0,0 @@ -from pg_rad.inputparser.specs import DetectorSpec - -from .detectors import IsotropicDetector, AngularDetector - - -class DetectorBuilder: - def __init__( - self, - detector_spec: DetectorSpec, - ): - self.detector_spec = detector_spec - - def build(self) -> IsotropicDetector | AngularDetector: - if self.detector_spec.is_isotropic: - return IsotropicDetector( - self.detector_spec.name, - self.detector_spec.efficiency - ) - else: - raise NotImplementedError("Angular detector not supported yet.") diff --git a/src/pg_rad/detector/detector.py b/src/pg_rad/detector/detector.py new file mode 100644 index 0000000..89a6101 --- /dev/null +++ b/src/pg_rad/detector/detector.py @@ -0,0 +1,43 @@ +from importlib.resources import files + +from pandas import read_csv + +from pg_rad.utils.interpolators import ( + get_field_efficiency, get_angular_efficiency +) + + +class Detector: + def __init__( + self, + name: str, + type: str, + is_isotropic: bool + ): + self.name = name + self.type = type + self.is_isotropic = is_isotropic + + def get_efficiency(self, energy_keV, angle=None): + f_eff = get_field_efficiency(self.name, energy_keV) + + if self.is_isotropic or angle is None: + return f_eff + else: + f_eff = get_field_efficiency(self.name, energy_keV) + a_eff = get_angular_efficiency(self.name, energy_keV, *angle) + return f_eff * a_eff + + +def load_detector(name) -> Detector: + csv = files('pg_rad.data').joinpath('detectors.csv') + data = read_csv(csv) + dets = data['name'].values + if name in dets: + row = data[data['name'] == name].iloc[0] + return Detector(row['name'], row['type'], row['is_isotropic']) + else: + raise NotImplementedError( + f"Detector with name '{name}' not in detector library. Available:" + f"{', '.join(dets)}" + ) diff --git a/src/pg_rad/detector/detectors.py b/src/pg_rad/detector/detectors.py deleted file mode 100644 index 439ee56..0000000 --- a/src/pg_rad/detector/detectors.py +++ /dev/null @@ -1,38 +0,0 @@ -from abc import ABC - - -class BaseDetector(ABC): - def __init__( - self, - name: str, - efficiency: float - ): - self.name = name - self.efficiency = efficiency - - def get_efficiency(self): - pass - - -class IsotropicDetector(BaseDetector): - def __init__( - self, - name: str, - efficiency: float, - ): - super().__init__(name, efficiency) - - def get_efficiency(self, energy): - return self.efficiency - - -class AngularDetector(BaseDetector): - def __init__( - self, - name: str, - efficiency: float - ): - super().__init__(name, efficiency) - - def get_efficiency(self, angle, energy): - pass diff --git a/src/pg_rad/inputparser/parser.py b/src/pg_rad/inputparser/parser.py index c6dc8b2..9f4d981 100644 --- a/src/pg_rad/inputparser/parser.py +++ b/src/pg_rad/inputparser/parser.py @@ -353,41 +353,11 @@ class ConfigParser: return specs def _parse_detector(self) -> DetectorSpec: - det_dict = self.config.get("detector") - required = {"name", "is_isotropic"} - if not isinstance(det_dict, dict): - raise InvalidConfigValueError( - "detector is not specified correctly. Must contain at least" - f"the subkeys {required}" - ) + det_name = self.config.get("detector") + if not det_name: + raise MissingConfigKeyError("detector") - missing = required - det_dict.keys() - if missing: - raise MissingConfigKeyError("detector", missing) - - name = det_dict.get("name") - is_isotropic = det_dict.get("is_isotropic") - eff = det_dict.get("efficiency") - - default_detectors = defaults.DETECTOR_EFFICIENCIES - - if name in default_detectors.keys() and not eff: - eff = default_detectors[name] - elif eff: - pass - else: - raise InvalidConfigValueError( - f"The detector {name} not found in library. Either " - f"specify {name}.efficiency or " - "choose a detector from the following list: " - f"{default_detectors.keys()}." - ) - - return DetectorSpec( - name=name, - efficiency=eff, - is_isotropic=is_isotropic - ) + return DetectorSpec(name=det_name) def _warn_unknown_keys(self, section: str, provided: set, allowed: set): unknown = provided - allowed diff --git a/src/pg_rad/inputparser/specs.py b/src/pg_rad/inputparser/specs.py index db00bae..be3cd00 100644 --- a/src/pg_rad/inputparser/specs.py +++ b/src/pg_rad/inputparser/specs.py @@ -67,8 +67,6 @@ class RelativePointSourceSpec(PointSourceSpec): @dataclass class DetectorSpec: name: str - efficiency: float - is_isotropic: bool @dataclass diff --git a/src/pg_rad/utils/interpolators.py b/src/pg_rad/utils/interpolators.py new file mode 100644 index 0000000..d07bd9c --- /dev/null +++ b/src/pg_rad/utils/interpolators.py @@ -0,0 +1,56 @@ +from importlib.resources import files + +import numpy as np +from pandas import read_csv +from scipy.interpolate import interp1d, CubicSpline + +from pg_rad.configs.filepaths import ATTENUATION_TABLE + + +def get_mass_attenuation_coeff(*args) -> float: + csv = files('pg_rad.data').joinpath(ATTENUATION_TABLE) + data = read_csv(csv) + x = data["energy_mev"].to_numpy() + y = data["mu"].to_numpy() + f = interp1d(x, y) + return f(*args) + + +def get_field_efficiency(name: str, energy_keV: float) -> float: + csv = files('pg_rad.data.field_efficiencies').joinpath(name+'.csv') + data = read_csv(csv) + data = data.groupby("energy_keV", as_index=False).mean() + x = data["energy_keV"].to_numpy() + y = data["field_efficiency_m2"].to_numpy() + f = CubicSpline(x, y) + return f(energy_keV) + + +def get_angular_efficiency(name: str, energy_keV: float, *angle: float): + csv = files('pg_rad.data.angular_efficiencies').joinpath(name+'.csv') + data = read_csv(csv) + + # check all energies at which angular eff. is available for this detector. + # this is done within 1% tolerance + energy_cols = [col for col in data.columns if col != "angle"] + energies = np.array([float(col) for col in energy_cols]) + rel_diff = np.abs(energies - energy_keV) / energies + match_idx = np.where(rel_diff <= 0.01)[0] + + if len(match_idx) == 0: + raise NotImplementedError( + f"No angular efficiency defined for {energy_keV} keV " + f"in detector '{name}'. Available: {energies}" + ) + + best_idx = match_idx[np.argmin(rel_diff[match_idx])] + selected_energy_col = energy_cols[best_idx] + + x = data["angle"].to_numpy() + y = data[selected_energy_col].to_numpy() + idx = np.argsort(x) + x = x[idx] + y = y[idx] + f = interp1d(x, y) + + return f(angle) From a133b8b1c7b0c8b98ae1ca81d4ad0b37bd273d13 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 20 Mar 2026 09:23:02 +0100 Subject: [PATCH 18/20] update landscape to work with new detector --- src/pg_rad/landscape/builder.py | 4 ++-- src/pg_rad/landscape/landscape.py | 5 +++-- src/pg_rad/main.py | 13 ++++--------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/pg_rad/landscape/builder.py b/src/pg_rad/landscape/builder.py index b052734..56ca378 100644 --- a/src/pg_rad/landscape/builder.py +++ b/src/pg_rad/landscape/builder.py @@ -15,7 +15,7 @@ from pg_rad.inputparser.specs import ( RelativePointSourceSpec, DetectorSpec ) - +from pg_rad.detector.detector import load_detector from pg_rad.path.path import Path, path_from_RT90 from road_gen.generators.segmented_road_generator import SegmentedRoadGenerator @@ -161,7 +161,7 @@ class LandscapeBuilder: )) def set_detector(self, spec: DetectorSpec) -> Self: - self._detector = spec + self._detector = load_detector(spec.name) return self def _fit_landscape_to_path(self) -> None: diff --git a/src/pg_rad/landscape/landscape.py b/src/pg_rad/landscape/landscape.py index a386815..f5f5b86 100644 --- a/src/pg_rad/landscape/landscape.py +++ b/src/pg_rad/landscape/landscape.py @@ -1,7 +1,7 @@ import logging from pg_rad.path.path import Path -from pg_rad.detector.detectors import AngularDetector, IsotropicDetector +from pg_rad.detector.detector import Detector from pg_rad.objects.sources import PointSource @@ -18,7 +18,7 @@ class Landscape: path: Path, air_density: float, point_sources: list[PointSource], - detector: IsotropicDetector | AngularDetector, + detector: Detector, size: tuple[int, int, int] ): """Initialize a landscape. @@ -28,6 +28,7 @@ class Landscape: path (Path): The path of the detector. air_density (float): Air density in kg/m^3. point_sources (list[PointSource]): List of point sources. + detector (Detector): The detector object. size (tuple[int, int, int]): Size of the world. """ diff --git a/src/pg_rad/main.py b/src/pg_rad/main.py index b31db63..4a81386 100644 --- a/src/pg_rad/main.py +++ b/src/pg_rad/main.py @@ -4,7 +4,6 @@ import sys from pandas.errors import ParserError -from pg_rad.detector.builder import DetectorBuilder from pg_rad.exceptions.exceptions import ( MissingConfigKeyError, OutOfBoundsError, @@ -56,7 +55,9 @@ def main(): acquisition_time: 1 path: - length: 1000 + length: + - 500 + - 500 segments: - straight - turn_left: 45 @@ -69,19 +70,15 @@ def main(): isotope: Cs137 gamma_energy_keV: 661 - detector: - name: dummy - is_isotropic: True + detector: LU_NaI_3inch """ cp = ConfigParser(test_yaml).parse() landscape = LandscapeDirector.build_from_config(cp) - detector = DetectorBuilder(cp.detector).build() output = SimulationEngine( landscape=landscape, runtime_spec=cp.runtime, sim_spec=cp.options, - detector=detector ).simulate() plotter = ResultPlotter(landscape, output) @@ -91,10 +88,8 @@ def main(): try: cp = ConfigParser(args.config).parse() landscape = LandscapeDirector.build_from_config(cp) - detector = DetectorBuilder(cp.detector).build() output = SimulationEngine( landscape=landscape, - detector=detector, runtime_spec=cp.runtime, sim_spec=cp.options ).simulate() From b1d781714b3be5daff215ae731b6dcd0b4c3ac09 Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 20 Mar 2026 09:25:09 +0100 Subject: [PATCH 19/20] update fluence and simulation to work with detectors and correct count rates --- src/pg_rad/isotopes/isotope.py | 2 +- src/pg_rad/physics/attenuation.py | 17 ------- src/pg_rad/physics/fluence.py | 80 ++++++++++++++----------------- src/pg_rad/simulator/engine.py | 12 ++--- src/pg_rad/simulator/outputs.py | 6 ++- 5 files changed, 47 insertions(+), 70 deletions(-) delete mode 100644 src/pg_rad/physics/attenuation.py diff --git a/src/pg_rad/isotopes/isotope.py b/src/pg_rad/isotopes/isotope.py index c73fc26..ab3ce7c 100644 --- a/src/pg_rad/isotopes/isotope.py +++ b/src/pg_rad/isotopes/isotope.py @@ -4,7 +4,7 @@ from pandas import read_csv from pg_rad.configs.filepaths import ISOTOPE_TABLE from pg_rad.exceptions.exceptions import InvalidIsotopeError -from pg_rad.physics.attenuation import get_mass_attenuation_coeff +from pg_rad.utils.interpolators import get_mass_attenuation_coeff class Isotope: diff --git a/src/pg_rad/physics/attenuation.py b/src/pg_rad/physics/attenuation.py deleted file mode 100644 index 9af70e9..0000000 --- a/src/pg_rad/physics/attenuation.py +++ /dev/null @@ -1,17 +0,0 @@ -from importlib.resources import files - -from pandas import read_csv -from scipy.interpolate import interp1d - -from pg_rad.configs.filepaths import ATTENUATION_TABLE - - -def get_mass_attenuation_coeff( - *args - ) -> float: - csv = files('pg_rad.data').joinpath(ATTENUATION_TABLE) - data = read_csv(csv) - x = data["energy_mev"].to_numpy() - y = data["mu"].to_numpy() - f = interp1d(x, y) - return f(*args) diff --git a/src/pg_rad/physics/fluence.py b/src/pg_rad/physics/fluence.py index 20a232d..ff0a803 100644 --- a/src/pg_rad/physics/fluence.py +++ b/src/pg_rad/physics/fluence.py @@ -2,11 +2,9 @@ from typing import Tuple, TYPE_CHECKING import numpy as np -from pg_rad.detector.detectors import IsotropicDetector, AngularDetector - - if TYPE_CHECKING: from pg_rad.landscape.landscape import Landscape + from pg_rad.detector.detector import Detector def phi( @@ -33,16 +31,14 @@ def phi( """ # Linear photon attenuation coefficient in m^-1. - mu_mass_air *= 0.1 - mu_air = mu_mass_air * air_density + 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) - ) + ) / (4 * np.pi * r**2) return phi_r @@ -50,8 +46,7 @@ def phi( def calculate_count_rate_per_second( landscape: "Landscape", pos: np.ndarray, - detector: IsotropicDetector | AngularDetector, - tangent_vectors: np.ndarray, + detector: "Detector", scaling=1E6 ): """Compute count rate in s^-1 m^-2 at a position in the landscape. @@ -59,7 +54,7 @@ def calculate_count_rate_per_second( Args: landscape (Landscape): The landscape to compute. pos (np.ndarray): (N, 3) array of positions. - detector (IsotropicDetector | AngularDetector): + detector (Detector): Detector object, needed to compute correct efficiency. Returns: @@ -69,22 +64,29 @@ def calculate_count_rate_per_second( 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 isinstance(detector, AngularDetector): - cos_theta = ( - np.sum(tangent_vectors * source_to_detector, axis=1) / ( - np.linalg.norm(source_to_detector, axis=1) * - np.linalg.norm(tangent_vectors, axis=1) - ) - ) - cos_theta = np.clip(cos_theta, -1, 1) - theta = np.arccos(cos_theta) - eff = detector.get_efficiency(theta, energy=source.isotope.E) + 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(energy=source.isotope.E) + eff = detector.get_efficiency(source.isotope.E) phi_source = phi( r=r, @@ -102,15 +104,15 @@ def calculate_count_rate_per_second( def calculate_counts_along_path( landscape: "Landscape", - detector: "IsotropicDetector | AngularDetector", - acquisition_time: int, + 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 (IsotropicDetector | AngularDetector): _description_ + detector (Detector): _description_ points_per_segment (int, optional): _description_. Defaults to 100. Returns: @@ -123,7 +125,6 @@ def calculate_counts_along_path( num_segments = len(path.segments) segment_lengths = np.array([seg.length for seg in path.segments]) - ds = segment_lengths[0] original_distances = np.zeros(num_points) original_distances[1:] = np.cumsum(segment_lengths) @@ -140,26 +141,17 @@ def calculate_counts_along_path( if path.opposite_direction: full_positions = np.flip(full_positions, axis=0) - # to compute the angle between sources and the direction of travel, we - # compute tangent vectors along the path. - dx_ds = np.gradient(xnew, s) - dy_ds = np.gradient(ynew, s) - tangent_vectors = np.c_[dx_ds, dy_ds, np.zeros_like(dx_ds)] - tangent_vectors /= np.linalg.norm(tangent_vectors, axis=1, keepdims=True) - - count_rate = calculate_count_rate_per_second( - landscape, full_positions, detector, tangent_vectors + # [counts/s] + cps = calculate_count_rate_per_second( + landscape, full_positions, detector ) - count_rate *= (acquisition_time / points_per_segment) + # reshape so each segment is on a row + cps_per_seg = cps.reshape(num_segments, points_per_segment) - count_rate_segs = count_rate.reshape(num_segments, points_per_segment) - integrated = np.trapezoid( - count_rate_segs, - dx=ds/points_per_segment, - axis=1 - ) + 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 - result = np.zeros(num_points) - result[1:] = integrated - return original_distances, result + return original_distances, s, cps, int_counts_result diff --git a/src/pg_rad/simulator/engine.py b/src/pg_rad/simulator/engine.py index a7d4d9a..8e1a8d7 100644 --- a/src/pg_rad/simulator/engine.py +++ b/src/pg_rad/simulator/engine.py @@ -6,7 +6,7 @@ from pg_rad.simulator.outputs import ( SimulationOutput, SourceOutput ) -from pg_rad.detector.detectors import IsotropicDetector, AngularDetector + from pg_rad.physics.fluence import calculate_counts_along_path from pg_rad.utils.projection import minimal_distance_to_path from pg_rad.inputparser.specs import RuntimeSpec, SimulationOptionsSpec @@ -17,13 +17,12 @@ class SimulationEngine: def __init__( self, landscape: Landscape, - detector: IsotropicDetector | AngularDetector, runtime_spec: RuntimeSpec, sim_spec: SimulationOptionsSpec, ): self.landscape = landscape - self.detector = detector + self.detector = self.landscape.detector self.runtime_spec = runtime_spec self.sim_spec = sim_spec @@ -40,12 +39,13 @@ class SimulationEngine: ) def _calculate_count_rate_along_path(self) -> CountRateOutput: - arc_length, phi = calculate_counts_along_path( + acq_points, sub_points, cps, int_counts = calculate_counts_along_path( self.landscape, self.detector, - acquisition_time=self.runtime_spec.acquisition_time + velocity=self.runtime_spec.speed ) - return CountRateOutput(arc_length, phi) + + return CountRateOutput(acq_points, sub_points, cps, int_counts) def _calculate_point_source_distance_to_path(self) -> List[SourceOutput]: diff --git a/src/pg_rad/simulator/outputs.py b/src/pg_rad/simulator/outputs.py index 5186a5e..264553f 100644 --- a/src/pg_rad/simulator/outputs.py +++ b/src/pg_rad/simulator/outputs.py @@ -5,8 +5,10 @@ from dataclasses import dataclass @dataclass class CountRateOutput: - arc_length: List[float] - count_rate: List[float] + acquisition_points: List[float] + sub_points: List[float] + cps: List[float] + integrated_counts: List[float] @dataclass From a1a18c6a3577865597ff223d8ccfb6b5d6bc55ed Mon Sep 17 00:00:00 2001 From: Pim Nelissen Date: Fri, 20 Mar 2026 09:26:10 +0100 Subject: [PATCH 20/20] update tests and plotting --- src/pg_rad/plotting/result_plotter.py | 151 ++++++++++++++++++++------ tests/test_attenuation_functions.py | 4 +- tests/test_fluence_rate.py | 34 +++--- tests/test_sources.py | 2 +- 4 files changed, 140 insertions(+), 51 deletions(-) diff --git a/src/pg_rad/plotting/result_plotter.py b/src/pg_rad/plotting/result_plotter.py index 5163913..c7cd323 100644 --- a/src/pg_rad/plotting/result_plotter.py +++ b/src/pg_rad/plotting/result_plotter.py @@ -1,3 +1,7 @@ +from importlib.resources import files + +import numpy as np +import pandas as pd from matplotlib import pyplot as plt from matplotlib.gridspec import GridSpec @@ -13,45 +17,79 @@ class ResultPlotter: self.source_res = output.sources def plot(self, landscape_z: float = 0): - fig = plt.figure(figsize=(12, 10), constrained_layout=True) - fig.suptitle(self.landscape.name) - gs = GridSpec( - 4, - 2, - width_ratios=[0.5, 0.5], - height_ratios=[0.7, 0.1, 0.1, 0.1], - hspace=0.2) - - ax1 = fig.add_subplot(gs[0, 0]) - self._draw_count_rate(ax1) - - ax2 = fig.add_subplot(gs[0, 1]) - self._plot_landscape(ax2, landscape_z) - - ax3 = fig.add_subplot(gs[1, :]) - self._draw_table(ax3) - - ax4 = fig.add_subplot(gs[2, :]) - self._draw_detector_table(ax4) - - ax5 = fig.add_subplot(gs[3, :]) - self._draw_source_table(ax5) + self._plot_main(landscape_z) + self._plot_detector() + self._plot_metadata() plt.show() + def _plot_main(self, landscape_z): + fig = plt.figure(figsize=(12, 8)) + fig.suptitle(self.landscape.name) + fig.canvas.manager.set_window_title("Main results") + + gs = GridSpec( + 2, 2, figure=fig, + height_ratios=[1, 2], width_ratios=[1, 1], + hspace=0.3, wspace=0.3) + + ax_cps = fig.add_subplot(gs[0, 0]) + self._draw_cps(ax_cps) + + ax_counts = fig.add_subplot(gs[0, 1]) + self._draw_count_rate(ax_counts) + + ax_landscape = fig.add_subplot(gs[1, :]) + self._plot_landscape(ax_landscape, landscape_z) + + def _plot_detector(self): + det = self.landscape.detector + + fig = plt.figure(figsize=(10, 4)) + fig.canvas.manager.set_window_title("Detector") + + gs = GridSpec(1, 2, figure=fig, width_ratios=[0.5, 0.5]) + + ax_table = fig.add_subplot(gs[0, 0]) + self._draw_detector_table(ax_table) + + if not det.is_isotropic: + ax_polar = fig.add_subplot(gs[0, 1], projection='polar') + + energies = [ + source.primary_gamma for source in self.source_res + ] + + self._draw_angular_efficiency_polar(ax_polar, det, energies[0]) + + def _plot_metadata(self): + fig, axs = plt.subplots(2, 1, figsize=(10, 6)) + fig.canvas.manager.set_window_title("Simulation Metadata") + + self._draw_table(axs[0]) + self._draw_source_table(axs[1]) + def _plot_landscape(self, ax, z): lp = LandscapeSlicePlotter() ax = lp.plot(landscape=self.landscape, z=z, ax=ax, show=False) return ax - def _draw_count_rate(self, ax): - x = self.count_rate_res.arc_length - y = self.count_rate_res.count_rate - ax.plot(x, y, label='Count rate', color='r') - ax.set_title('Count rate') + def _draw_cps(self, ax): + x = self.count_rate_res.sub_points + y = self.count_rate_res.cps + ax.plot(x, y, color='b') + ax.set_title('Counts per second (CPS)') ax.set_xlabel('Arc length s [m]') - ax.set_ylabel('Counts') - ax.legend() + ax.set_ylabel('CPS [s$^{-1}$]') + + def _draw_count_rate(self, ax): + x = self.count_rate_res.acquisition_points + y = self.count_rate_res.integrated_counts + ax.plot(x, y, color='r', linestyle='--', alpha=0.2) + ax.scatter(x, y, color='r', marker='x') + ax.set_title('Integrated counts') + ax.set_xlabel('Arc length s [m]') + ax.set_ylabel('N') def _draw_table(self, ax): ax.set_axis_off() @@ -60,6 +98,7 @@ class ResultPlotter: data = [ ["Air density (kg/m^3)", round(self.landscape.air_density, 3)], ["Total path length (m)", round(self.landscape.path.length, 3)], + ["Readout points", len(self.count_rate_res.integrated_counts)], ] ax.table( @@ -72,13 +111,28 @@ class ResultPlotter: def _draw_detector_table(self, ax): det = self.landscape.detector - print(det) + + if det.is_isotropic: + det_type = "Isotropic" + else: + det_type = "Angular" + + source_energies = [ + source.primary_gamma for source in self.source_res + ] + + # list field efficiencies for each primary gamma in the landscape + effs = {e: det.get_efficiency(e) for e in source_energies} + formatted_effs = ", ".join( + f"{value:.3f} @ {key:.1f} keV" + for key, value in effs.items() + ) ax.set_axis_off() ax.set_title('Detector') cols = ('Parameter', 'Value') data = [ - ["Name", det.name], - ["Type", det.efficiency], + ["Detector", f"{det.name} ({det_type})"], + ["Field efficiency", formatted_effs], ] ax.table( @@ -119,3 +173,34 @@ class ResultPlotter: ) return ax + + def _draw_angular_efficiency_polar(self, ax, detector, energy_keV): + # find the energies available for this detector. + csv = files('pg_rad.data.angular_efficiencies').joinpath( + detector.name + '.csv' + ) + data = pd.read_csv(csv) + energy_cols = [col for col in data.columns if col != "angle"] + energies = np.array([float(col) for col in energy_cols]) + + # take energy column that is within 1% tolerance of energy_keV + rel_diff = np.abs(energies - energy_keV) / energies + match_idx = np.where(rel_diff <= 0.01)[0] + best_idx = match_idx[np.argmin(rel_diff[match_idx])] + col = energy_cols[best_idx] + + theta_deg = data["angle"].to_numpy() + eff = data[col].to_numpy() + + idx = np.argsort(theta_deg) + theta_deg = theta_deg[idx] + eff = eff[idx] + + theta_deg = np.append(theta_deg, theta_deg[0]) + eff = np.append(eff, eff[0]) + + theta_rad = np.radians(theta_deg) + + print(theta_rad) + ax.plot(theta_rad, eff) + ax.set_title(f"Rel. angular efficiency @ {energy_keV:.1f} keV") diff --git a/tests/test_attenuation_functions.py b/tests/test_attenuation_functions.py index 385ace5..75de49c 100644 --- a/tests/test_attenuation_functions.py +++ b/tests/test_attenuation_functions.py @@ -1,6 +1,6 @@ import pytest -from pg_rad.physics import get_mass_attenuation_coeff +from pg_rad.utils.interpolators import get_mass_attenuation_coeff @pytest.mark.parametrize("energy,mu", [ @@ -8,7 +8,7 @@ from pg_rad.physics import get_mass_attenuation_coeff (1.00000E-02, 5.120E+00), (1.00000E-01, 1.541E-01), (1.00000E+00, 6.358E-02), - (1.00000E+01, 2.045E-02) + (1.00000E+01, 2.045E-02) ]) def test_exact_attenuation_retrieval(energy, mu): """ diff --git a/tests/test_fluence_rate.py b/tests/test_fluence_rate.py index 7220612..bc1e6cb 100644 --- a/tests/test_fluence_rate.py +++ b/tests/test_fluence_rate.py @@ -3,20 +3,20 @@ import pytest from pg_rad.inputparser.parser import ConfigParser from pg_rad.landscape.director import LandscapeDirector -from pg_rad.physics import calculate_fluence_at +from pg_rad.physics.fluence import phi @pytest.fixture def isotropic_detector(): - from pg_rad.detector.detectors import IsotropicDetector - return IsotropicDetector(name="test_detector", eff=1.0) + from pg_rad.detector.detector import load_detector + return load_detector('dummy') @pytest.fixture def phi_ref(test_landscape, isotropic_detector): source = test_landscape.point_sources[0] - r = np.linalg.norm(np.array([10, 10, 0]) - np.array(source.pos)) + r = np.linalg.norm(np.array([0, 0, 0]) - np.array(source.pos)) A = source.activity * 1E6 b = source.isotope.b @@ -43,12 +43,11 @@ def test_landscape(): sources: test_source: activity_MBq: 100 - position: [0, 0, 0] - isotope: CS137 + position: [0, 100, 0] + isotope: Cs137 + gamma_energy_keV: 661 - detector: - name: dummy - is_isotropic: True + detector: dummy """ cp = ConfigParser(test_yaml).parse() @@ -57,10 +56,15 @@ def test_landscape(): def test_single_source_fluence(phi_ref, test_landscape, isotropic_detector): - phi = calculate_fluence_at( - test_landscape, - np.array([10, 10, 0]), - isotropic_detector, - tangent_vectors=None, + s = test_landscape.point_sources[0] + r = np.linalg.norm(np.array([0, 0, 0]) - np.array(s.pos)) + phi_calc = phi( + r, + s.activity*1E6, + s.isotope.b, + s.isotope.mu_mass_air, + test_landscape.air_density, + isotropic_detector.get_efficiency(s.isotope.E) ) - assert pytest.approx(phi[0], rel=1E-3) == phi_ref + + assert pytest.approx(phi_calc, rel=1E-6) == phi_ref diff --git a/tests/test_sources.py b/tests/test_sources.py index 5395269..b878da1 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from pg_rad.objects import PointSource +from pg_rad.objects.sources import PointSource @pytest.fixture