mirror of
https://github.com/pim-n/pg-rad
synced 2026-03-23 21:58:12 +01:00
Improve error handling. Add alignment feature for point sources
This commit is contained in:
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user