mirror of
https://github.com/pim-n/pg-rad
synced 2026-05-14 03:58:10 +02:00
Compare commits
18 Commits
591d81b8a3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 8017159c5b | |||
| eadf14fd49 | |||
| 73f630bd47 | |||
| 086b4b4b55 | |||
| f02daa35dd | |||
| 09609b4429 | |||
| 0a0073ccce | |||
| 237b301061 | |||
| 3d4cea337b | |||
| bd4f9af6ba | |||
| 06422b208b | |||
| 2bd70634dc | |||
| f75a80f792 | |||
| 07741f1392 | |||
| 3ec2a7601c | |||
| 99028c916f | |||
| 5bcf1778ea | |||
| 7ed12989f4 |
@ -2,10 +2,7 @@
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
!!! 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
|
||||
|
||||
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
|
||||
sources:
|
||||
@ -190,21 +187,10 @@ Note that side is relative to the direction of travel. The path will by default
|
||||
|
||||
### 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
|
||||
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
|
||||
detector: LU_HPGe_90
|
||||
```
|
||||
|
||||
## Optional keys
|
||||
|
||||
121
docs/explainers/count_rate_along_path.ipynb
Normal file
121
docs/explainers/count_rate_along_path.ipynb
Normal 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
|
||||
}
|
||||
@ -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).
|
||||
|
||||
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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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"
|
||||
|
||||
@ -61,15 +66,14 @@ The easiest way is to take one of these example configs, and adjust them as need
|
||||
sources:
|
||||
source1:
|
||||
activity_MBq: 1000
|
||||
isotope: CS137
|
||||
isotope: Cs137
|
||||
gamma_energy_keV: 662
|
||||
position:
|
||||
along_path: 100
|
||||
dist_from_path: 50
|
||||
side: left
|
||||
|
||||
detector:
|
||||
name: dummy
|
||||
is_isotropic: True
|
||||
detector: dummy
|
||||
```
|
||||
|
||||
=== "Example 2"
|
||||
@ -89,21 +93,21 @@ The easiest way is to take one of these example configs, and adjust them as need
|
||||
sources:
|
||||
source1:
|
||||
activity_MBq: 1000
|
||||
isotope: CS137
|
||||
isotope: Cs137
|
||||
gamma_energy_keV: 662
|
||||
position: [104.3, 32.5, 0]
|
||||
source2:
|
||||
activity_MBq: 100
|
||||
isotope: CS137
|
||||
isotope: Cs137
|
||||
gamma_energy_keV: 662
|
||||
position: [0, 0, 0]
|
||||
|
||||
detector:
|
||||
name: dummy
|
||||
is_isotropic: True
|
||||
detector: dummy
|
||||
```
|
||||
|
||||
=== "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
|
||||
name: Example 3
|
||||
@ -121,12 +125,11 @@ The easiest way is to take one of these example configs, and adjust them as need
|
||||
sources:
|
||||
source1:
|
||||
activity_MBq: 1000
|
||||
isotope: CS137
|
||||
isotope: Cs137
|
||||
gamma_energy_keV: 662
|
||||
position: [0, 0, 0]
|
||||
|
||||
detector:
|
||||
name: dummy
|
||||
is_isotropic: True
|
||||
detector: dummy
|
||||
```
|
||||
|
||||
=== "Example 4"
|
||||
@ -148,12 +151,11 @@ The easiest way is to take one of these example configs, and adjust them as need
|
||||
sources:
|
||||
source1:
|
||||
activity_MBq: 1000
|
||||
isotope: CS137
|
||||
isotope: Cs137
|
||||
gamma_energy_keV: 662
|
||||
position: [0, 0, 0]
|
||||
|
||||
detector:
|
||||
name: dummy
|
||||
is_isotropic: True
|
||||
detector: dummy
|
||||
```
|
||||
|
||||
=== "Example 5"
|
||||
@ -178,10 +180,9 @@ The easiest way is to take one of these example configs, and adjust them as need
|
||||
sources:
|
||||
source1:
|
||||
activity_MBq: 1000
|
||||
isotope: CS137
|
||||
isotope: Cs137
|
||||
gamma_energy_keV: 662
|
||||
position: [0, 0, 0]
|
||||
|
||||
detector:
|
||||
name: dummy
|
||||
is_isotropic: True
|
||||
detector: dummy
|
||||
```
|
||||
@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"pg_rad.data" = ["*.csv"]
|
||||
"pg_rad.data" = ["**/*.csv"]
|
||||
"pg_rad.configs" = ["*.yml"]
|
||||
|
||||
[project]
|
||||
|
||||
@ -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
|
||||
@ -148,10 +148,9 @@ class LandscapeBuilder:
|
||||
|
||||
# we dont support -x values, but negative y values are possible as
|
||||
# the path is centered in the y direction.
|
||||
print(pos)
|
||||
if not (
|
||||
(0 < pos[0] < self._size[0]) and
|
||||
(-0.5 * self._size[1] < pos[1] < 0.5 * self._size[1])
|
||||
(0 <= pos[0] <= self._size[0]) and
|
||||
(-0.5 * self._size[1] <= pos[1] <= 0.5 * self._size[1])
|
||||
):
|
||||
raise OutOfBoundsError(
|
||||
"One or more sources attempted to "
|
||||
|
||||
@ -17,6 +17,7 @@ from pg_rad.inputparser.parser import ConfigParser
|
||||
from pg_rad.landscape.director import LandscapeDirector
|
||||
from pg_rad.plotting.result_plotter import ResultPlotter
|
||||
from pg_rad.simulator.engine import SimulationEngine
|
||||
from pg_rad.utils.export import generate_folder_name, save_results
|
||||
|
||||
|
||||
def main():
|
||||
@ -39,9 +40,14 @@ def main():
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
)
|
||||
parser.add_argument(
|
||||
"--saveplot",
|
||||
"--showplots",
|
||||
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()
|
||||
@ -49,7 +55,7 @@ def main():
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if args.example:
|
||||
example_yaml = """
|
||||
input_config = """
|
||||
name: Example landscape
|
||||
speed: 8.33
|
||||
acquisition_time: 1
|
||||
@ -72,62 +78,63 @@ def main():
|
||||
|
||||
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)
|
||||
output = SimulationEngine(
|
||||
landscape=landscape,
|
||||
runtime_spec=cp.runtime,
|
||||
sim_spec=cp.options,
|
||||
sim_spec=cp.options
|
||||
).simulate()
|
||||
|
||||
plotter = ResultPlotter(landscape, output)
|
||||
plotter.plot()
|
||||
|
||||
elif args.config:
|
||||
try:
|
||||
cp = ConfigParser(args.config).parse()
|
||||
landscape = LandscapeDirector.build_from_config(cp)
|
||||
output = SimulationEngine(
|
||||
landscape=landscape,
|
||||
runtime_spec=cp.runtime,
|
||||
sim_spec=cp.options
|
||||
).simulate()
|
||||
|
||||
plotter = ResultPlotter(landscape, output)
|
||||
if args.save:
|
||||
folder_name = generate_folder_name(output)
|
||||
save_results(output, folder_name)
|
||||
plotter.save(folder_name)
|
||||
if args.showplots:
|
||||
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 (
|
||||
FileNotFoundError,
|
||||
ParserError,
|
||||
InvalidYAMLError
|
||||
) as e:
|
||||
logger.critical(e)
|
||||
sys.exit(1)
|
||||
if not (args.save or args.showplots):
|
||||
logger.warning(
|
||||
"No output produced. Use --save flag to save outputs and/or "
|
||||
"--showplots to display interactive plots."
|
||||
)
|
||||
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 (
|
||||
FileNotFoundError,
|
||||
ParserError,
|
||||
InvalidYAMLError
|
||||
) as e:
|
||||
logger.critical(e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -4,8 +4,6 @@ from matplotlib import pyplot as plt
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.patches import Circle
|
||||
|
||||
from numpy import median
|
||||
|
||||
from pg_rad.landscape.landscape import Landscape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -23,6 +23,15 @@ class ResultPlotter:
|
||||
|
||||
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):
|
||||
fig = plt.figure(figsize=(12, 8))
|
||||
fig.suptitle(self.landscape.name)
|
||||
@ -41,6 +50,7 @@ class ResultPlotter:
|
||||
|
||||
ax_landscape = fig.add_subplot(gs[1, :])
|
||||
self._plot_landscape(ax_landscape, landscape_z)
|
||||
return fig
|
||||
|
||||
def _plot_detector(self):
|
||||
det = self.landscape.detector
|
||||
@ -61,6 +71,7 @@ class ResultPlotter:
|
||||
]
|
||||
|
||||
self._draw_angular_efficiency_polar(ax_polar, det, energies[0])
|
||||
return fig
|
||||
|
||||
def _plot_metadata(self):
|
||||
fig, axs = plt.subplots(2, 1, figsize=(10, 6))
|
||||
@ -68,6 +79,7 @@ class ResultPlotter:
|
||||
|
||||
self._draw_table(axs[0])
|
||||
self._draw_source_table(axs[1])
|
||||
return fig
|
||||
|
||||
def _plot_landscape(self, ax, z):
|
||||
lp = LandscapeSlicePlotter()
|
||||
@ -84,7 +96,7 @@ class ResultPlotter:
|
||||
ax.set_ylabel('CPS [s$^{-1}$]')
|
||||
|
||||
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:]
|
||||
ax.plot(
|
||||
x, y, color='r', linestyle='--',
|
||||
|
||||
@ -3,6 +3,7 @@ from typing import List
|
||||
from pg_rad.landscape.landscape import Landscape
|
||||
from pg_rad.simulator.outputs import (
|
||||
CountRateOutput,
|
||||
DetectorOutput,
|
||||
SimulationOutput,
|
||||
SourceOutput
|
||||
)
|
||||
@ -31,10 +32,13 @@ class SimulationEngine:
|
||||
|
||||
count_rate_results = self._calculate_count_rate_along_path()
|
||||
source_results = self._calculate_point_source_distance_to_path()
|
||||
detector_results = self._generate_detector_output()
|
||||
|
||||
return SimulationOutput(
|
||||
name=self.landscape.name,
|
||||
size=self.landscape.size,
|
||||
count_rate=count_rate_results,
|
||||
detector=detector_results,
|
||||
sources=source_results
|
||||
)
|
||||
|
||||
@ -48,6 +52,8 @@ class SimulationEngine:
|
||||
)
|
||||
|
||||
return CountRateOutput(
|
||||
self.landscape.path.x_list,
|
||||
self.landscape.path.y_list,
|
||||
acq_points,
|
||||
sub_points,
|
||||
cps,
|
||||
@ -77,3 +83,13 @@ class SimulationEngine:
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
@ -5,7 +5,9 @@ from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class CountRateOutput:
|
||||
acquisition_points: List[float]
|
||||
x: List[float]
|
||||
y: List[float]
|
||||
distance: List[float]
|
||||
sub_points: List[float]
|
||||
cps: List[float]
|
||||
integrated_counts: List[float]
|
||||
@ -22,8 +24,18 @@ class SourceOutput:
|
||||
dist_from_path: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class DetectorOutput:
|
||||
name: str
|
||||
type: str
|
||||
is_isotropic: bool
|
||||
field_eff: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimulationOutput:
|
||||
name: str
|
||||
size: tuple
|
||||
detector: DetectorOutput
|
||||
count_rate: CountRateOutput
|
||||
sources: List[SourceOutput]
|
||||
|
||||
109
src/pg_rad/utils/export.py
Normal file
109
src/pg_rad/utils/export.py
Normal 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
|
||||
Reference in New Issue
Block a user