Compare commits

..

18 Commits

Author SHA1 Message Date
8017159c5b patch export to add also detector details 2026-04-16 13:40:47 +02:00
eadf14fd49 Merge pull request #66 from pim-n/exporting-data
Exporting data
2026-04-15 09:31:32 +02:00
73f630bd47 fix if logic so warning not raised incorrectly 2026-04-15 09:02:03 +02:00
086b4b4b55 fix csv filename output 2026-04-15 08:58:56 +02:00
f02daa35dd add parameter JSON export 2026-04-15 08:44:18 +02:00
09609b4429 add landscape size as simulation output 2026-04-15 08:43:11 +02:00
0a0073ccce hotfix for export filename 2026-04-14 15:08:49 +02:00
237b301061 update docs. update example configs to latest spec. 2026-04-02 09:11:52 +02:00
3d4cea337b remove print statement 2026-04-02 08:34:58 +02:00
bd4f9af6ba add warning when user does not specify any outputs 2026-04-02 08:34:43 +02:00
06422b208b remove requirements.txt 2026-04-02 08:28:56 +02:00
2bd70634dc recursively include all datafiles in pg_rad.data module 2026-03-31 13:30:02 +02:00
f75a80f792 Merge pull request #60 from pim-n/feature-background-activity
background CPS for 3x3 inch NaI detector based on FWHM lookup method
export functionality (to CSV)
2026-03-31 11:16:20 +02:00
07741f1392 include landscape edges 2026-03-31 11:10:15 +02:00
3ec2a7601c PEP8 2026-03-31 10:59:26 +02:00
99028c916f PEP8 2026-03-31 10:51:16 +02:00
5bcf1778ea update plotting. add export functionality. update main to work with new plotting and saving/export functionality. 2026-03-31 10:48:54 +02:00
7ed12989f4 add waypoints (XY/East North) to CountRateOutput 2026-03-31 10:47:51 +02:00
12 changed files with 366 additions and 111 deletions

View File

@ -2,10 +2,7 @@
To get started quickly, you may copy and modify one of the example configs found [here](quickstart.md#example-configs). 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 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. The remainder of this chapter will explain the different required and optionals keys, what they represent, and allowed values.
The remainder of this chapter will explain the different required and optionals keys, what they represent, and allowed values.
## Required keys ## Required keys
@ -124,11 +121,11 @@ Like with the lengths, if a turn segment has no angle specified, a random one (w
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. 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 !!! info
For more information about how procedural roads are generated, including the random sampling of lengths and angles, see X For more information about how procedural roads are generated, including the random sampling of lengths and angles, see [this](explainers/prefab_roads.ipynb) explainer.
### Sources ### 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: Currently, the only type of source supported is an isotropic point source. However, an arbitrary number of point sources can be added to the landscape. Point sources can be added under the `sources` key, where the **subkey is the name** of the source:
```yaml ```yaml
sources: sources:
@ -190,21 +187,10 @@ Note that side is relative to the direction of travel. The path will by default
### Detector ### 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`: The final required key is the `detector`. Currently, custom detectors are not yet supported and you must choose from a list of existing detectors:
```yaml ```yaml
detector: detector: LU_HPGe_90
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 ## Optional keys

View File

@ -0,0 +1,121 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a8d303ad",
"metadata": {},
"source": [
"# Gamma detectors along a path"
]
},
{
"cell_type": "markdown",
"id": "08dda386",
"metadata": {},
"source": [
"## Fluence rate at $\\vec{r}$\n",
"\n",
"Let $\\vec{r}_{p} = (x_{p},y_{p},z_{p})$ denote the location of a point source $p$. Let $\\vec{r}_{i} = (x_{i},y_{i},z_{i})$ denote an arbitrary point in space. The primary photon fluence rate at $\\vec{r}$ is then given by\n",
"\n",
"$$\n",
"\\dot{\\phi}(r) = \\frac{A n_\\gamma \\exp(-\\mu_{air} r)}{4\\pi r^2}\n",
"$$\n",
"\n",
"where $r = ||\\vec{r}_p - \\vec{r}_i ||$. The units are $\\dot{\\phi} \\sim \\frac{\\text{photons}}{s \\cdot m^2}$\n",
"\n",
"## Count rate\n",
"\n",
"Gamma detectors are not perfectly efficient and efficiency is dependent on both photon energy $E_\\gamma$ and incident angle $\\theta$ [1].\n",
"\n",
"- the field efficiency $\\varepsilon_D (E_\\gamma) \\in [0, 1]$, in units of area $\\text{m}^2$,\n",
"- the relative angular efficiency $\\varepsilon_\\theta (E_\\gamma, \\theta) \\in [0, 1]$, dimensionless.\n",
"\n",
"The total efficiency of the detector is then defined as\n",
"\n",
"$$\n",
"\\varepsilon(E_\\gamma, \\theta) = \\varepsilon_D (E_\\gamma) \\varepsilon_\\theta (E_\\gamma, \\theta) \\; .\n",
"$$\n",
"\n",
"Where $\\varepsilon(E_\\gamma, \\theta) \\sim \\text{m}^2$.\n",
"\n",
"If the detector $D$ is positioned at $\\vec{r}_i$, the **count rate** becomes\n",
"\n",
"$$\n",
"\\dot{N}(r, E_\\gamma, \\theta) = \\varepsilon(E_\\gamma, \\theta) \\phi(r)\n",
"$$\n",
"\n",
"where $\\dot{N} \\sim \\frac{\\text{counts}}{s}$.\n",
"\n",
"## Acquisiton time \n",
"\n",
"The acquisition time window $t_{w}$ is the time during which counts are accumulated in the detector until readout into the digital system. A typical $t_{w}$ in mobile gamma spectrometry is 1 to 10 seconds [2]. \n",
"\n",
"## Integration of counts\n",
"\n",
"Suppose an acquisition time of $t_{w}$ seconds and a fixed velocity $v$ in meters per seconds. Let $R(u)$ describe a road of $L$ meters long in the xy-plane, described as a function of arc length $u$ in meters (distance traveled along the road), where $u \\in [0, L]$. The euclidian norm between the point $R(u)$ and point source $\\vec{r}_p$ is then\n",
"\n",
"$$\n",
"r(u) = || \\vec{r}_p - R(u) ||\n",
"$$\n",
"\n",
"Assuming a fixed velocity $v$, the distance traveled during one acquisition window $t_{w}$ is $\\Delta_s \\equiv vt_{w}$ meters. The path is divided into $K = L/\\Delta s$ segments, where the $k$-th segment represents the interval\n",
"\n",
"$$\n",
"u \\in [(k-1) \\Delta_s, k\\Delta_s] \\; , \\; k = 1, 2, \\dots, K\n",
"$$\n",
"\n",
"The total count rate acquired during segment $k$-th is then\n",
"\n",
"$$\n",
"N_{w}(k) = \\frac{1}{v} \\int_{(k-1)\\Delta_s}^{k\\Delta_s} \\underbrace{\\dot{N}(r(u), E_\\gamma, \\theta(u))}_{\\text{CPS}} du\n",
"$$\n",
"\n",
"## Numerical approximation\n",
"\n",
"Let us divide each segment into $N$ equally spaced points with step size $\\Delta u = \\Delta s / N$. Applying the trapezoidal rule then gives\n",
"\n",
"$$\n",
"N_w(k) \\approx \\frac{\\Delta u}{v}\n",
"\\left[\n",
"\\frac{\\dot{N}_0 + \\dot{N}_N}{2}\n",
"+ \\sum_{n=1}^{N-1} \\dot{N}_n\n",
"\\right],\n",
"$$\n",
"\n",
"where\n",
"\n",
"$$\n",
"\\dot{N}_n = \\dot{N}\\big(r(u_n), E_\\gamma, \\theta(u_n)\\big), \\quad\n",
"u_n = (k-1)\\Delta s + n \\Delta u.\n",
"$$\n",
"\n",
"## References\n",
"\n",
"[1] A. Bukartas, Assessment of mobile radiometry data in radiological emergencies using Bayesian statistical methods, thesis/doccomp, Lund University, 2021. Accessed: Jan. 19, 2026. [Online]. Available: http://lup.lub.lu.se/record/4c298e71-3278-42a7-818a-6f17a5121d56\n",
"\n",
"[2] R. Finck, A. Bukartas, M. Jönsson, and C. Rääf, Maximum detection distances for gamma emitting point sources in mobile gamma spectrometry, Applied Radiation and Isotopes, vol. 184, p. 110195, Jun. 2022, doi: 10.1016/j.apradiso.2022.110195.\n"
]
}
],
"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
}

View File

@ -22,13 +22,18 @@ Primary Gamma RADiation landscape tool
If you get something like `pgrad: command not found`, please consult the [installation guide](installation.md). 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: You can run a quick test by running the example landscape as follows:
``` ```
pgrad --test pgrad --example
``` ```
This should produce a plot of a scenario containing a single point source and a path. This should produce an output like
```
INFO: Landscape built successfully: Example landscape
WARNING: No output produced. Use --save flag to save outputs and/or --showplots to display interactive plots.
```
## Running PG-RAD ## Running PG-RAD
@ -38,11 +43,11 @@ In order to use the CLI for your own simulations, you need to provide a *config
pgrad --config path/to/my_config.yml pgrad --config path/to/my_config.yml
``` ```
where `path/to/my_config.yml` points to your config file. where `path/to/my_config.yml` points to your config file. To check the results live, add the `--showplots` flag. If you want to save the results directly, then add the `--save` flag (you can use them at the same time as well).
## Example configs ## 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). The easiest way to get started 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" === "Example 1"
@ -61,15 +66,14 @@ The easiest way is to take one of these example configs, and adjust them as need
sources: sources:
source1: source1:
activity_MBq: 1000 activity_MBq: 1000
isotope: CS137 isotope: Cs137
gamma_energy_keV: 662
position: position:
along_path: 100 along_path: 100
dist_from_path: 50 dist_from_path: 50
side: left side: left
detector: detector: dummy
name: dummy
is_isotropic: True
``` ```
=== "Example 2" === "Example 2"
@ -89,21 +93,21 @@ The easiest way is to take one of these example configs, and adjust them as need
sources: sources:
source1: source1:
activity_MBq: 1000 activity_MBq: 1000
isotope: CS137 isotope: Cs137
gamma_energy_keV: 662
position: [104.3, 32.5, 0] position: [104.3, 32.5, 0]
source2: source2:
activity_MBq: 100 activity_MBq: 100
isotope: CS137 isotope: Cs137
gamma_energy_keV: 662
position: [0, 0, 0] position: [0, 0, 0]
detector: detector: dummy
name: dummy
is_isotropic: True
``` ```
=== "Example 3" === "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). 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](explainers/prefab_roads.ipynb).
``` yaml ``` yaml
name: Example 3 name: Example 3
@ -121,12 +125,11 @@ The easiest way is to take one of these example configs, and adjust them as need
sources: sources:
source1: source1:
activity_MBq: 1000 activity_MBq: 1000
isotope: CS137 isotope: Cs137
gamma_energy_keV: 662
position: [0, 0, 0] position: [0, 0, 0]
detector: detector: dummy
name: dummy
is_isotropic: True
``` ```
=== "Example 4" === "Example 4"
@ -148,12 +151,11 @@ The easiest way is to take one of these example configs, and adjust them as need
sources: sources:
source1: source1:
activity_MBq: 1000 activity_MBq: 1000
isotope: CS137 isotope: Cs137
gamma_energy_keV: 662
position: [0, 0, 0] position: [0, 0, 0]
detector: detector: dummy
name: dummy
is_isotropic: True
``` ```
=== "Example 5" === "Example 5"
@ -178,10 +180,9 @@ The easiest way is to take one of these example configs, and adjust them as need
sources: sources:
source1: source1:
activity_MBq: 1000 activity_MBq: 1000
isotope: CS137 isotope: Cs137
gamma_energy_keV: 662
position: [0, 0, 0] position: [0, 0, 0]
detector: detector: dummy
name: dummy
is_isotropic: True
``` ```

View File

@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
where = ["src"] where = ["src"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
"pg_rad.data" = ["*.csv"] "pg_rad.data" = ["**/*.csv"]
"pg_rad.configs" = ["*.yml"] "pg_rad.configs" = ["*.yml"]
[project] [project]

View File

@ -1,6 +0,0 @@
matplotlib>=3.9.2
notebook>=7.2.1
numpy>=2
pandas>=2.3.1
piecewise_regression==1.5.0
pyyaml>=6.0.2

View File

@ -148,10 +148,9 @@ class LandscapeBuilder:
# we dont support -x values, but negative y values are possible as # we dont support -x values, but negative y values are possible as
# the path is centered in the y direction. # the path is centered in the y direction.
print(pos)
if not ( if not (
(0 < pos[0] < self._size[0]) and (0 <= pos[0] <= self._size[0]) and
(-0.5 * self._size[1] < pos[1] < 0.5 * self._size[1]) (-0.5 * self._size[1] <= pos[1] <= 0.5 * self._size[1])
): ):
raise OutOfBoundsError( raise OutOfBoundsError(
"One or more sources attempted to " "One or more sources attempted to "

View File

@ -17,6 +17,7 @@ from pg_rad.inputparser.parser import ConfigParser
from pg_rad.landscape.director import LandscapeDirector from pg_rad.landscape.director import LandscapeDirector
from pg_rad.plotting.result_plotter import ResultPlotter from pg_rad.plotting.result_plotter import ResultPlotter
from pg_rad.simulator.engine import SimulationEngine from pg_rad.simulator.engine import SimulationEngine
from pg_rad.utils.export import generate_folder_name, save_results
def main(): def main():
@ -39,9 +40,14 @@ def main():
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
) )
parser.add_argument( parser.add_argument(
"--saveplot", "--showplots",
action="store_true", action="store_true",
help="Save the plot or not." help="Show the plots immediately."
)
parser.add_argument(
"--save",
action="store_true",
help="Save the outputs"
) )
args = parser.parse_args() args = parser.parse_args()
@ -49,7 +55,7 @@ def main():
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if args.example: if args.example:
example_yaml = """ input_config = """
name: Example landscape name: Example landscape
speed: 8.33 speed: 8.33
acquisition_time: 1 acquisition_time: 1
@ -72,62 +78,63 @@ def main():
detector: LU_NaI_3inch detector: LU_NaI_3inch
""" """
elif args.config:
input_config = args.config
cp = ConfigParser(example_yaml).parse() try:
cp = ConfigParser(input_config).parse()
landscape = LandscapeDirector.build_from_config(cp) landscape = LandscapeDirector.build_from_config(cp)
output = SimulationEngine( output = SimulationEngine(
landscape=landscape, landscape=landscape,
runtime_spec=cp.runtime, runtime_spec=cp.runtime,
sim_spec=cp.options, sim_spec=cp.options
).simulate() ).simulate()
plotter = ResultPlotter(landscape, output) plotter = ResultPlotter(landscape, output)
plotter.plot() if args.save:
folder_name = generate_folder_name(output)
elif args.config: save_results(output, folder_name)
try: plotter.save(folder_name)
cp = ConfigParser(args.config).parse() if args.showplots:
landscape = LandscapeDirector.build_from_config(cp)
output = SimulationEngine(
landscape=landscape,
runtime_spec=cp.runtime,
sim_spec=cp.options
).simulate()
plotter = ResultPlotter(landscape, output)
plotter.plot() plotter.plot()
except (
MissingConfigKeyError,
KeyError
) as e:
logger.critical(e)
logger.critical(
"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,
InvalidConfigValueError,
NotImplementedError
) as e:
logger.critical(e)
logger.critical(
"One or more items in config are not specified correctly. "
"Please consult this log and fix the problem."
)
sys.exit(1)
except ( if not (args.save or args.showplots):
FileNotFoundError, logger.warning(
ParserError, "No output produced. Use --save flag to save outputs and/or "
InvalidYAMLError "--showplots to display interactive plots."
) as e: )
logger.critical(e) except (
sys.exit(1) MissingConfigKeyError,
KeyError
) as e:
logger.critical(e)
logger.critical(
"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,
InvalidConfigValueError,
NotImplementedError
) as e:
logger.critical(e)
logger.critical(
"One or more items in config are not specified correctly. "
"Please consult this log and fix the problem."
)
sys.exit(1)
except (
FileNotFoundError,
ParserError,
InvalidYAMLError
) as e:
logger.critical(e)
sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -4,8 +4,6 @@ from matplotlib import pyplot as plt
from matplotlib.axes import Axes from matplotlib.axes import Axes
from matplotlib.patches import Circle from matplotlib.patches import Circle
from numpy import median
from pg_rad.landscape.landscape import Landscape from pg_rad.landscape.landscape import Landscape
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -23,6 +23,15 @@ class ResultPlotter:
plt.show() plt.show()
def save(self, path: str, landscape_z: float = 0) -> None:
fig_1 = self._plot_main(landscape_z)
fig_2 = self._plot_detector()
fig_3 = self._plot_metadata()
fig_1.savefig(path+"/main.jpg")
fig_2.savefig(path+"/detector.jpg")
fig_3.savefig(path+"/metadata.jpg")
def _plot_main(self, landscape_z): def _plot_main(self, landscape_z):
fig = plt.figure(figsize=(12, 8)) fig = plt.figure(figsize=(12, 8))
fig.suptitle(self.landscape.name) fig.suptitle(self.landscape.name)
@ -41,6 +50,7 @@ class ResultPlotter:
ax_landscape = fig.add_subplot(gs[1, :]) ax_landscape = fig.add_subplot(gs[1, :])
self._plot_landscape(ax_landscape, landscape_z) self._plot_landscape(ax_landscape, landscape_z)
return fig
def _plot_detector(self): def _plot_detector(self):
det = self.landscape.detector det = self.landscape.detector
@ -61,6 +71,7 @@ class ResultPlotter:
] ]
self._draw_angular_efficiency_polar(ax_polar, det, energies[0]) self._draw_angular_efficiency_polar(ax_polar, det, energies[0])
return fig
def _plot_metadata(self): def _plot_metadata(self):
fig, axs = plt.subplots(2, 1, figsize=(10, 6)) fig, axs = plt.subplots(2, 1, figsize=(10, 6))
@ -68,6 +79,7 @@ class ResultPlotter:
self._draw_table(axs[0]) self._draw_table(axs[0])
self._draw_source_table(axs[1]) self._draw_source_table(axs[1])
return fig
def _plot_landscape(self, ax, z): def _plot_landscape(self, ax, z):
lp = LandscapeSlicePlotter() lp = LandscapeSlicePlotter()
@ -84,7 +96,7 @@ class ResultPlotter:
ax.set_ylabel('CPS [s$^{-1}$]') ax.set_ylabel('CPS [s$^{-1}$]')
def _draw_counts(self, ax): def _draw_counts(self, ax):
x = self.count_rate_res.acquisition_points[1:] x = self.count_rate_res.distance[1:]
y = self.count_rate_res.integrated_counts[1:] y = self.count_rate_res.integrated_counts[1:]
ax.plot( ax.plot(
x, y, color='r', linestyle='--', x, y, color='r', linestyle='--',

View File

@ -3,6 +3,7 @@ from typing import List
from pg_rad.landscape.landscape import Landscape from pg_rad.landscape.landscape import Landscape
from pg_rad.simulator.outputs import ( from pg_rad.simulator.outputs import (
CountRateOutput, CountRateOutput,
DetectorOutput,
SimulationOutput, SimulationOutput,
SourceOutput SourceOutput
) )
@ -31,10 +32,13 @@ class SimulationEngine:
count_rate_results = self._calculate_count_rate_along_path() count_rate_results = self._calculate_count_rate_along_path()
source_results = self._calculate_point_source_distance_to_path() source_results = self._calculate_point_source_distance_to_path()
detector_results = self._generate_detector_output()
return SimulationOutput( return SimulationOutput(
name=self.landscape.name, name=self.landscape.name,
size=self.landscape.size,
count_rate=count_rate_results, count_rate=count_rate_results,
detector=detector_results,
sources=source_results sources=source_results
) )
@ -48,6 +52,8 @@ class SimulationEngine:
) )
return CountRateOutput( return CountRateOutput(
self.landscape.path.x_list,
self.landscape.path.y_list,
acq_points, acq_points,
sub_points, sub_points,
cps, cps,
@ -77,3 +83,13 @@ class SimulationEngine:
) )
return source_output return source_output
def _generate_detector_output(self) -> DetectorOutput:
return DetectorOutput(
name=self.detector.name,
type=self.detector.type,
is_isotropic=self.detector.is_isotropic,
field_eff=self.detector.get_efficiency(
self.landscape.point_sources[0].isotope.E
)
)

View File

@ -5,7 +5,9 @@ from dataclasses import dataclass
@dataclass @dataclass
class CountRateOutput: class CountRateOutput:
acquisition_points: List[float] x: List[float]
y: List[float]
distance: List[float]
sub_points: List[float] sub_points: List[float]
cps: List[float] cps: List[float]
integrated_counts: List[float] integrated_counts: List[float]
@ -22,8 +24,18 @@ class SourceOutput:
dist_from_path: float dist_from_path: float
@dataclass
class DetectorOutput:
name: str
type: str
is_isotropic: bool
field_eff: float
@dataclass @dataclass
class SimulationOutput: class SimulationOutput:
name: str name: str
size: tuple
detector: DetectorOutput
count_rate: CountRateOutput count_rate: CountRateOutput
sources: List[SourceOutput] sources: List[SourceOutput]

109
src/pg_rad/utils/export.py Normal file
View File

@ -0,0 +1,109 @@
from dataclasses import asdict
from datetime import datetime as dt
import json
import os
import logging
import re
from numpy import array, full_like, ndarray, bool_
from pandas import DataFrame
from pg_rad.simulator.outputs import SimulationOutput
logger = logging.getLogger(__name__)
class NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, ndarray):
return obj.tolist()
elif isinstance(obj, bool_):
return bool(obj)
return super().default(obj)
def generate_folder_name(sim: SimulationOutput) -> str:
formatted_sim_name = re.sub(r"\s+", '_', sim.name.lower())
folder_name = (
formatted_sim_name +
'_result_' +
dt.today().strftime('%Y%m%d_%H%M')
)
return folder_name
def save_results(sim: SimulationOutput, folder_name: str) -> None:
"""Parse all simulation output and save to a folder."""
if not os.path.exists(folder_name):
os.makedirs(folder_name)
else:
logger.warning("Folder already exists. Overwrite?")
ans = input("[type 'n' to cancel overwrite] ")
if ans.lower() == 'n':
return
df = generate_df(sim)
csv_name = generate_csv_name(sim)
df.to_csv(f"{folder_name}/{csv_name}.csv", index=False)
param_dict = generate_sim_param_dict(sim)
print(type(param_dict['detector']['is_isotropic']))
with open(f"{folder_name}/parameters.json", 'w') as f:
json.dump(param_dict, f, cls=NumpyEncoder)
logger.info(f"Simulation output saved to {folder_name}!")
def generate_sim_param_dict(sim: SimulationOutput) -> dict:
"""Parse simulation parameters and hyperparameters to dictionary."""
d = asdict(sim)
d.pop('count_rate')
return d
def generate_df(sim: SimulationOutput) -> DataFrame:
"""Parse simulation output to CSV format and the name of CSV."""
br_array = full_like(
sim.count_rate.integrated_counts,
sim.count_rate.mean_bkg_cps
)
result_df = DataFrame(
{
"East": sim.count_rate.x,
"North": sim.count_rate.y,
"ROI_P": sim.count_rate.integrated_counts,
"ROI_BR": br_array,
"Dist": sim.count_rate.distance
}
)
return result_df
def generate_csv_name(sim: SimulationOutput) -> str:
"""Generate CSV name according to Alex' specification"""
num_src = len(sim.sources)
src_ids = [str(i+1) for i in range(num_src)]
bkg_cps = round(sim.count_rate.mean_bkg_cps)
source_param_strings = [
[
str(round(s.activity))+"MBq",
str(round(s.dist_from_path))+"m",
str(round(s.position[0]))+'_'+str(round(s.position[1]))
]
for s in sim.sources
]
if num_src == 1:
src_str = "_".join(source_param_strings[0])
else:
src_str_array = array(
[list(item) for item in zip(*source_param_strings)]
)
src_str = "_".join(src_str_array.flat)
src_ids_str = "_".join(src_ids)
csv_name = f"{src_ids_str}_src_{bkg_cps}_cps_bkg_{src_str}"
return csv_name