Compare commits

53 Commits

Author SHA1 Message Date
910469612f Merge pull request #74 from pim-n/dev
BUGFIX: incorrect shape comparison causing all saves to fail
2026-05-19 07:06:24 +02:00
4dfb893e93 BUGFIX: incorrect shape comparison causing all saves to fail 2026-05-19 07:04:00 +02:00
2810f14ab8 Merge pull request #73 from pim-n/dev
update README.md
2026-05-17 10:04:40 +02:00
9c79bc2d8e Update README.md 2026-05-17 10:04:16 +02:00
5340c0ca57 Update README.md 2026-05-17 09:58:25 +02:00
4d7d7ac819 Update README.md 2026-05-17 09:58:13 +02:00
f1658ac6e0 remove mkinit requirement 2026-05-17 08:30:44 +02:00
10868dae6d Merge pull request #72 from pim-n/release-0.1.0
prepare for release 0.1.0
2026-05-17 07:36:38 +02:00
80f1266183 exclude exp. coordinates due to data mismatch. warn users 2026-05-17 07:32:04 +02:00
e70b939c81 suppress matplotlib and PIL debug logging 2026-05-17 07:31:42 +02:00
3a23414810 fix interpolation issue at -170 to -180 degree 2026-05-17 07:31:14 +02:00
e39574eb8b Merge pull request #70 from pim-n/feature-unc-plotting
Feature unc plotting
2026-05-12 08:53:00 +02:00
fcbde3547c remove print line 2026-05-12 08:50:42 +02:00
adceafbec7 update NaIR efficiencies 2026-05-12 08:48:42 +02:00
a0bca78d5d add uncertainty bands in plot for sqrt(N) counting statistics 2026-05-06 00:38:17 +02:00
bc538ff5dd hotfix: fix incorrect integration scheme, move to time integration as Bukartas 2021 doccomp. 2026-05-06 00:37:26 +02:00
2a15ae12f6 Merge branch 'dev' of github.com:pim-n/pg-rad into dev 2026-05-03 17:46:50 +02:00
d48f42e8af update metadata 2026-05-03 17:46:10 +02:00
3afd217366 Merge pull request #69 from pim-n/feature-new-detector
Feature new detector
2026-05-03 17:40:27 +02:00
54ae0c4c0d fix #61 2026-05-03 17:38:09 +02:00
40b09971ff pass global seed onto fluence and background 2026-05-03 17:29:53 +02:00
372082f19b add bounds_check optional flag (default false) 2026-05-03 15:58:38 +02:00
2dd6ab6beb add angular dependence NaIR 2026-04-23 16:05:20 +02:00
ec4bf6cbe6 add NaIR detector (isotropic) 2026-04-23 16:02:29 +02:00
b63fa68f21 hotfix: dataframe export fail due to inconsistent dimensions 2026-04-21 12:19:25 +02:00
be6ef0e084 Merge pull request #68 from pim-n/feature-opt-bkg
optional background and fix cps bug
2026-04-21 11:46:40 +02:00
936e283705 bugfix: first cps is 0 2026-04-21 11:43:11 +02:00
591e2fb41a add bkg count option in input 2026-04-21 10:53:53 +02:00
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
591d81b8a3 rename test to example landscape. Fixes #51 2026-03-30 08:52:08 +02:00
78d877c9bc PEP8 2026-03-30 08:49:18 +02:00
60edbd1976 fix typos / PEP8 2026-03-30 08:48:37 +02:00
db12d573b2 improve out of bounds handling of source placement 2026-03-30 08:48:15 +02:00
c635c7f594 Add background functionality 2026-03-30 08:24:07 +02:00
0a60bb09e9 improve plotting functionality 2026-03-30 08:23:02 +02:00
8098ab9329 Add background counts for 3x3 inch NaI. Add auto ROI lookup function from the FWHM. Add ROI test. 2026-03-20 10:48:44 +01:00
26 changed files with 991 additions and 201 deletions

26
CITATION.cff Normal file
View File

@ -0,0 +1,26 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: PG-RAD - Primary Gamma RADiation Simulator
message: >-
If you use this software, please cite it using the
metadata from this file.
type: software
authors:
- given-names: Pim
family-names: Nelissen
email: pi0274ne-s@student.lu.se
affiliation: Lund University
repository-code: 'https://github.com/pim-n/pg-rad'
abstract: >-
Primary Gamma RADiation (PG-RAD) is a deterministic
landscape simulator to simulate vehicle-based gamma source
localization scenarios.
keywords:
- gamma spectrometry
- mobile gamma spectrometry
- emergency preparedness
- environmental radiation
license: MIT

View File

@ -1,59 +1,32 @@
[![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-312/) [![Python 3.12](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/downloads/release/python-312/)
[![Tests](https://github.com/pim-n/pg-rad/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/pim-n/pg-rad/actions/workflows/ci-tests.yml) [![Tests](https://github.com/pim-n/pg-rad/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/pim-n/pg-rad/actions/workflows/ci-tests.yml)
# pg-rad # PG-RAD - Primary Gamma RADiation landscape simulator
Primary Gamma RADiation landscape - Development
## Clone PG-RAD is a command-line software package for simulating localisation of orphan sources using mobile gamma spectrometry. PG-RAD provides a framework for construction road geometries, arbitrary distributions of point sources, modelling the detector response to primary gammas. PG-RAD provides a configurable framework for constructing detector trajectories, defining radioactive source distributions, modelling detector response, and generating synthetic acquisition data. User input is specified through YAML configuration files, which makes simulations reproducible and easily shared.
```
git clone https://github.com/pim-n/pg-rad ## Installation
cd pg-rad
git checkout dev PG-RAD is a Python 3 package and is only tested on x86_64 Linux systems. The following installation instructions were testing for a fresh virtual machine running Ubuntu 26.04 LTS.
## Conda installation (Tested and recommended)
1. Install git by `sudo apt update && sudo apt install git`
2. Install miniforge by following [these](https://conda-forge.org/download/) instructions.
3. Create a file called `environment.yml`, and paste the following in there:
```yaml
name: my-pgrad-env
channels:
- conda-forge
dependencies:
- python=3.12
- pip:
- git+ssh://git@github.com/pim-n/pg-rad.git@v0.1.0
``` ```
4. Run `conda env create -f environment.yml` to create the environment
5. Run `conda activate pg-rad-analysis`. You should now be in the conda environment `my-pgrad-env`.
6. To test if installation was succesful, run `pgrad --example --show`. If this runs without errors and produces visual output, PG-RAD is correctly installed.
or ## Manual installation
``` If you prefer another virtual environment, you still need git installed. Ensure the Python version of the environment is `>3.12.4` and `<3.13`. Then, with your virtual environment activated, run `pip install git+ssh://git@github.com/pim-n/pg-rad.git@main`. Run `pgrad --example --show` to check if the installation was successful.
git@github.com:pim-n/pg-rad.git
cd pg-rad
git checkout dev
```
## Dependencies / venv
With Python verion `>=3.12.4` and `<3.13`, create a virtual environment and install pg-rad.
```
python3 -m venv .venv
source .venv/bin/activate
```
With the virtual environment activated, run:
```
pip install -e .[dev]
```
## Running example landscape
The example landscape can be generated using the command-line interface. Still in the virtual environment, run
```
pgrad --test --loglevel DEBUG
```
## Tests
Tests can be run with `pytest` from the root directory of the repository. With the virtual environment activated, run:
```
pytest
```
## Local viewing of documentation
PG-RAD uses [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) for generating documentation. It can be locally viewed by (in the venv) running:
```
mkdocs serve
```
where you can add the `--livereload` flag to automatically update the documentation as you write to the Markdown files.

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,16 +6,16 @@ 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]
name = "pg-rad" name = "pgrad"
version = "0.2.1" version = "0.1.0"
authors = [ authors = [
{ name="Pim Nelissen", email="pi0274ne-s@student.lu.se" }, { name="Pim Nelissen", email="pi0274ne-s@student.lu.se" },
] ]
description = "Primary Gamma RADiation Landscape" description = "PG-RAD - Primary Gamma RADiation Simulator"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12.4,<3.13" requires-python = ">=3.12.4,<3.13"
dependencies = [ dependencies = [
@ -37,4 +37,4 @@ Issues = "https://github.com/pim-n/pg-rad/issues"
[project.optional-dependencies] [project.optional-dependencies]
dev = ["pytest", "mkinit", "notebook", "mkdocs-material", "mkdocstrings-python", "mkdocs-jupyter", "flake8"] dev = ["pytest", "notebook", "mkdocs-material", "mkdocstrings-python", "mkdocs-jupyter", "flake8"]

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

@ -0,0 +1,68 @@
from importlib.resources import files
from typing import Tuple
import numpy as np
from pandas import read_csv
from pg_rad.configs.defaults import FWHM_PARAMS
from pg_rad.detector.detector import Detector
def generate_background(
cps_array: np.ndarray,
detector: Detector,
energy_keV: float,
lam_inp: int | None = None,
seed: int | None = None
) -> np.ndarray:
"""
Generate synthetic background cps for a given detector and energy.
"""
if not lam_inp:
ROI_lo, ROI_hi = get_roi_from_fwhm(detector, energy_keV)
lam = get_cps_from_roi(detector, ROI_lo, ROI_hi)
else:
lam = lam_inp
rng = np.random.default_rng(seed=seed)
return rng.poisson(lam=lam, size=cps_array.shape)
def fwhm(A: float, B: float, C: float, E: float) -> float:
return np.sqrt(A + B * E + C * E**2)
def get_roi_from_fwhm(
detector: Detector,
energy_keV: float
) -> Tuple[float, float]:
"""
Get the region of interest for given primary gamma energy, using
the 3*FWHM rule.
"""
A, B, C = FWHM_PARAMS.get(detector.type)
delta = 3*fwhm(A, B, C, energy_keV)
return (energy_keV-delta, energy_keV+delta)
def get_cps_from_roi(
detector: Detector,
roi_lo: float,
roi_hi: float
) -> float:
try:
csv = files('pg_rad.data.backgrounds').joinpath(detector.name+'.csv')
data = read_csv(csv)
except FileNotFoundError:
raise NotImplementedError(
f"Detector {detector.name} does not have backgrounds implemented."
)
# get indices of nearest bins
idx_min = (data["Energy"] - roi_lo).abs().idxmin()
idx_max = (data["Energy"] - roi_hi).abs().idxmin()
idx_start, idx_end = sorted([idx_min, idx_max])
cps_sum = data.loc[idx_start:idx_end, "cps"].sum()
return cps_sum

View File

@ -23,3 +23,10 @@ DETECTOR_EFFICIENCIES = {
"NaIR": 0.0216, "NaIR": 0.0216,
"NaIF": 0.0254 "NaIF": 0.0254
} }
# A, B, C parameters for different detector types
# for formula FWHM(E) = sqrt(A + B*E + C*E^2)
FWHM_PARAMS = {
"NaI": (-70.55, 2.678, 0.000602),
"HPGe": (1.778, 0.001843, 0.000001)
}

View File

@ -18,6 +18,7 @@ angle,662,1173,1332
160,1.113,1.096,1.099 160,1.113,1.096,1.099
170,1.091,1.076,1.083 170,1.091,1.076,1.083
180,1.076,1.066,1.078 180,1.076,1.066,1.078
-180,1.076,1.066,1.078
-170,1.102,1.091,1.093 -170,1.102,1.091,1.093
-160,1.122,1.100,1.102 -160,1.122,1.100,1.102
-150,1.128,1.105,1.093 -150,1.128,1.105,1.093

1 angle 662 1173 1332
18 160 1.113 1.096 1.099
19 170 1.091 1.076 1.083
20 180 1.076 1.066 1.078
21 -180 1.076 1.066 1.078
22 -170 1.102 1.091 1.093
23 -160 1.122 1.100 1.102
24 -150 1.128 1.105 1.093

View File

@ -0,0 +1,38 @@
angle,662
0,0.027
10,0.162
20,0.346
30,0.517
40,0.662
50,0.794
60,0.882
70,0.947
80,0.995
90,1.000
100,0.970
110,0.895
120,0.778
130,0.649
140,0.546
150,0.477
160,0.387
170,0.267
180,0.205
-180,0.205
-170,0.266
-160,0.385
-150,0.527
-140,0.671
-130,0.764
-120,0.838
-110,0.763
-100,0.838
-90,0.903
-80,0.904
-70,0.898
-60,0.862
-50,0.717
-40,0.337
-30,0.154
-20,0.253
-10,0.125
1 angle 662
2 0 0.027
3 10 0.162
4 20 0.346
5 30 0.517
6 40 0.662
7 50 0.794
8 60 0.882
9 70 0.947
10 80 0.995
11 90 1.000
12 100 0.970
13 110 0.895
14 120 0.778
15 130 0.649
16 140 0.546
17 150 0.477
18 160 0.387
19 170 0.267
20 180 0.205
21 -180 0.205
22 -170 0.266
23 -160 0.385
24 -150 0.527
25 -140 0.671
26 -130 0.764
27 -120 0.838
28 -110 0.763
29 -100 0.838
30 -90 0.903
31 -80 0.904
32 -70 0.898
33 -60 0.862
34 -50 0.717
35 -40 0.337
36 -30 0.154
37 -20 0.253
38 -10 0.125

View File

@ -0,0 +1,257 @@
Energy,Data,cps
0,0,0
11.6866,0,0
23.3732,1653,1.181558256
35.0598,3827,2.735525375
46.7464,5010,3.581129378
58.433,7880,5.632594711
70.1196,11068,7.911365261
81.8062,13414,9.588277341
93.4928,14536,10.39027877
105.179,14580,10.42172981
116.866,14138,10.10578985
128.553,13559,9.691922802
140.239,12725,9.095782702
151.926,11597,8.289492495
163.612,10696,7.645461044
175.299,9847,7.038598999
186.986,9063,6.478198713
198.672,8192,5.855611151
210.359,7627,5.451751251
222.045,6941,4.961401001
233.732,6450,4.610436026
245.419,6046,4.321658327
257.105,5560,3.974267334
268.792,4845,3.463187991
280.478,4267,3.05003574
292.165,4066,2.906361687
303.852,3711,2.652609006
315.538,3413,2.439599714
327.225,3080,2.201572552
338.911,3035,2.169406719
350.598,2789,1.993566833
362.285,2766,1.977126519
373.971,2432,1.73838456
385.658,2182,1.55968549
397.344,2010,1.436740529
409.031,1911,1.365975697
420.718,1871,1.337383846
432.404,1678,1.199428163
444.091,1601,1.144388849
455.777,1538,1.099356683
467.464,1473,1.052894925
479.151,1390,0.993566833
490.837,1369,0.978556112
502.524,1358,0.970693352
514.21,1356,0.96926376
525.897,1323,0.945675482
537.584,1256,0.897784132
549.27,1171,0.837026447
560.957,1072,0.766261615
572.643,1094,0.781987134
584.33,1157,0.827019299
596.017,1073,0.766976412
607.703,1083,0.774124375
619.39,1042,0.744817727
631.076,995,0.711222302
642.763,938,0.670478914
654.45,770,0.550393138
666.136,787,0.562544675
677.823,761,0.543959971
689.509,703,0.502501787
701.196,671,0.479628306
712.883,611,0.436740529
724.569,599,0.428162974
736.256,573,0.40957827
747.942,623,0.445318084
759.629,636,0.454610436
771.316,634,0.453180843
783.002,602,0.430307362
794.689,525,0.375268049
806.375,544,0.388849178
818.062,557,0.39814153
829.749,527,0.376697641
841.435,508,0.363116512
853.122,509,0.363831308
864.808,478,0.341672623
876.495,498,0.355968549
888.182,515,0.368120086
899.868,521,0.372408863
911.555,539,0.385275197
923.241,546,0.390278771
934.928,545,0.389563974
946.615,529,0.378127234
958.301,489,0.349535382
969.988,490,0.350250179
981.674,447,0.319513939
993.361,437,0.312365976
1005.05,406,0.290207291
1016.73,380,0.271622588
1028.42,357,0.255182273
1040.11,346,0.247319514
1051.79,348,0.248749107
1063.48,337,0.240886347
1075.17,376,0.268763402
1086.85,376,0.268763402
1098.54,363,0.259471051
1110.23,388,0.277340958
1121.91,330,0.235882773
1133.6,364,0.260185847
1145.29,323,0.230879199
1156.97,359,0.256611866
1168.66,330,0.235882773
1180.35,284,0.203002144
1192.03,256,0.182987848
1203.72,252,0.180128663
1215.41,243,0.173695497
1227.09,264,0.188706219
1238.78,226,0.16154396
1250.47,214,0.152966405
1262.15,184,0.131522516
1273.84,179,0.127948535
1285.53,168,0.120085776
1297.21,185,0.132237312
1308.9,177,0.126518942
1320.59,175,0.12508935
1332.27,171,0.122230164
1343.96,187,0.133666905
1355.65,206,0.147248034
1367.33,255,0.182273052
1379.02,331,0.23659757
1390.71,395,0.282344532
1402.39,540,0.385989993
1414.08,608,0.43459614
1425.77,612,0.437455325
1437.45,575,0.411007863
1449.14,516,0.368834882
1460.82,467,0.333809864
1472.51,369,0.263759828
1484.2,254,0.181558256
1495.88,220,0.157255182
1507.57,125,0.089349535
1519.26,136,0.097212294
1530.94,92,0.065761258
1542.63,74,0.052894925
1554.32,67,0.047891351
1566,68,0.048606147
1577.69,70,0.05003574
1589.38,58,0.041458184
1601.06,66,0.047176555
1612.75,59,0.042172981
1624.44,47,0.033595425
1636.12,60,0.042887777
1647.81,74,0.052894925
1659.5,74,0.052894925
1671.18,84,0.060042888
1682.87,88,0.062902073
1694.56,88,0.062902073
1706.24,65,0.046461758
1717.93,78,0.05575411
1729.62,84,0.060042888
1741.3,62,0.04431737
1752.99,71,0.050750536
1764.68,54,0.038598999
1776.36,53,0.037884203
1788.05,53,0.037884203
1799.74,34,0.024303074
1811.42,36,0.025732666
1823.11,46,0.032880629
1834.8,45,0.032165833
1846.48,41,0.029306648
1858.17,33,0.023588277
1869.86,33,0.023588277
1881.54,29,0.020729092
1893.23,39,0.027877055
1904.92,37,0.026447462
1916.6,28,0.020014296
1928.29,41,0.029306648
1939.98,39,0.027877055
1951.66,46,0.032880629
1963.35,46,0.032880629
1975.04,36,0.025732666
1986.72,39,0.027877055
1998.41,47,0.033595425
2010.1,59,0.042172981
2021.78,56,0.040028592
2033.47,44,0.031451036
2045.15,57,0.040743388
2056.84,50,0.035739814
2068.53,68,0.048606147
2080.21,56,0.040028592
2091.9,53,0.037884203
2103.59,51,0.03645461
2115.27,30,0.021443888
2126.96,40,0.028591851
2138.65,47,0.033595425
2150.33,42,0.030021444
2162.02,49,0.035025018
2173.71,31,0.022158685
2185.39,29,0.020729092
2197.08,49,0.035025018
2208.77,26,0.018584703
2220.45,32,0.022873481
2232.14,23,0.016440315
2243.83,19,0.013581129
2255.51,31,0.022158685
2267.2,16,0.011436741
2278.89,21,0.015010722
2290.57,19,0.013581129
2302.26,20,0.014295926
2313.95,14,0.010007148
2325.63,18,0.012866333
2337.32,18,0.012866333
2349.01,22,0.015725518
2360.69,27,0.0192995
2372.38,21,0.015010722
2384.07,21,0.015010722
2395.75,35,0.02501787
2407.44,49,0.035025018
2419.13,55,0.039313796
2430.81,47,0.033595425
2442.5,68,0.048606147
2454.19,56,0.040028592
2465.87,57,0.040743388
2477.56,63,0.045032166
2489.25,52,0.037169407
2500.93,61,0.043602573
2512.62,37,0.026447462
2524.31,34,0.024303074
2535.99,34,0.024303074
2547.68,26,0.018584703
2559.37,15,0.010721944
2571.05,16,0.011436741
2582.74,9,0.006433167
2594.43,12,0.008577555
2606.11,8,0.00571837
2617.8,11,0.007862759
2629.48,2,0.001429593
2641.17,7,0.005003574
2652.86,5,0.003573981
2664.54,2,0.001429593
2676.23,2,0.001429593
2687.92,0,0
2699.6,6,0.004288778
2711.29,2,0.001429593
2722.98,6,0.004288778
2734.66,5,0.003573981
2746.35,5,0.003573981
2758.04,2,0.001429593
2769.72,5,0.003573981
2781.41,6,0.004288778
2793.1,7,0.005003574
2804.78,2,0.001429593
2816.47,2,0.001429593
2828.16,6,0.004288778
2839.84,5,0.003573981
2851.53,4,0.002859185
2863.22,1,0.000714796
2874.9,4,0.002859185
2886.59,4,0.002859185
2898.28,5,0.003573981
2909.96,2,0.001429593
2921.65,3,0.002144389
2933.34,4,0.002859185
2945.02,5,0.003573981
2956.71,3,0.002144389
2968.4,0,0
2980.08,0,0
1 Energy Data cps
2 0 0 0
3 11.6866 0 0
4 23.3732 1653 1.181558256
5 35.0598 3827 2.735525375
6 46.7464 5010 3.581129378
7 58.433 7880 5.632594711
8 70.1196 11068 7.911365261
9 81.8062 13414 9.588277341
10 93.4928 14536 10.39027877
11 105.179 14580 10.42172981
12 116.866 14138 10.10578985
13 128.553 13559 9.691922802
14 140.239 12725 9.095782702
15 151.926 11597 8.289492495
16 163.612 10696 7.645461044
17 175.299 9847 7.038598999
18 186.986 9063 6.478198713
19 198.672 8192 5.855611151
20 210.359 7627 5.451751251
21 222.045 6941 4.961401001
22 233.732 6450 4.610436026
23 245.419 6046 4.321658327
24 257.105 5560 3.974267334
25 268.792 4845 3.463187991
26 280.478 4267 3.05003574
27 292.165 4066 2.906361687
28 303.852 3711 2.652609006
29 315.538 3413 2.439599714
30 327.225 3080 2.201572552
31 338.911 3035 2.169406719
32 350.598 2789 1.993566833
33 362.285 2766 1.977126519
34 373.971 2432 1.73838456
35 385.658 2182 1.55968549
36 397.344 2010 1.436740529
37 409.031 1911 1.365975697
38 420.718 1871 1.337383846
39 432.404 1678 1.199428163
40 444.091 1601 1.144388849
41 455.777 1538 1.099356683
42 467.464 1473 1.052894925
43 479.151 1390 0.993566833
44 490.837 1369 0.978556112
45 502.524 1358 0.970693352
46 514.21 1356 0.96926376
47 525.897 1323 0.945675482
48 537.584 1256 0.897784132
49 549.27 1171 0.837026447
50 560.957 1072 0.766261615
51 572.643 1094 0.781987134
52 584.33 1157 0.827019299
53 596.017 1073 0.766976412
54 607.703 1083 0.774124375
55 619.39 1042 0.744817727
56 631.076 995 0.711222302
57 642.763 938 0.670478914
58 654.45 770 0.550393138
59 666.136 787 0.562544675
60 677.823 761 0.543959971
61 689.509 703 0.502501787
62 701.196 671 0.479628306
63 712.883 611 0.436740529
64 724.569 599 0.428162974
65 736.256 573 0.40957827
66 747.942 623 0.445318084
67 759.629 636 0.454610436
68 771.316 634 0.453180843
69 783.002 602 0.430307362
70 794.689 525 0.375268049
71 806.375 544 0.388849178
72 818.062 557 0.39814153
73 829.749 527 0.376697641
74 841.435 508 0.363116512
75 853.122 509 0.363831308
76 864.808 478 0.341672623
77 876.495 498 0.355968549
78 888.182 515 0.368120086
79 899.868 521 0.372408863
80 911.555 539 0.385275197
81 923.241 546 0.390278771
82 934.928 545 0.389563974
83 946.615 529 0.378127234
84 958.301 489 0.349535382
85 969.988 490 0.350250179
86 981.674 447 0.319513939
87 993.361 437 0.312365976
88 1005.05 406 0.290207291
89 1016.73 380 0.271622588
90 1028.42 357 0.255182273
91 1040.11 346 0.247319514
92 1051.79 348 0.248749107
93 1063.48 337 0.240886347
94 1075.17 376 0.268763402
95 1086.85 376 0.268763402
96 1098.54 363 0.259471051
97 1110.23 388 0.277340958
98 1121.91 330 0.235882773
99 1133.6 364 0.260185847
100 1145.29 323 0.230879199
101 1156.97 359 0.256611866
102 1168.66 330 0.235882773
103 1180.35 284 0.203002144
104 1192.03 256 0.182987848
105 1203.72 252 0.180128663
106 1215.41 243 0.173695497
107 1227.09 264 0.188706219
108 1238.78 226 0.16154396
109 1250.47 214 0.152966405
110 1262.15 184 0.131522516
111 1273.84 179 0.127948535
112 1285.53 168 0.120085776
113 1297.21 185 0.132237312
114 1308.9 177 0.126518942
115 1320.59 175 0.12508935
116 1332.27 171 0.122230164
117 1343.96 187 0.133666905
118 1355.65 206 0.147248034
119 1367.33 255 0.182273052
120 1379.02 331 0.23659757
121 1390.71 395 0.282344532
122 1402.39 540 0.385989993
123 1414.08 608 0.43459614
124 1425.77 612 0.437455325
125 1437.45 575 0.411007863
126 1449.14 516 0.368834882
127 1460.82 467 0.333809864
128 1472.51 369 0.263759828
129 1484.2 254 0.181558256
130 1495.88 220 0.157255182
131 1507.57 125 0.089349535
132 1519.26 136 0.097212294
133 1530.94 92 0.065761258
134 1542.63 74 0.052894925
135 1554.32 67 0.047891351
136 1566 68 0.048606147
137 1577.69 70 0.05003574
138 1589.38 58 0.041458184
139 1601.06 66 0.047176555
140 1612.75 59 0.042172981
141 1624.44 47 0.033595425
142 1636.12 60 0.042887777
143 1647.81 74 0.052894925
144 1659.5 74 0.052894925
145 1671.18 84 0.060042888
146 1682.87 88 0.062902073
147 1694.56 88 0.062902073
148 1706.24 65 0.046461758
149 1717.93 78 0.05575411
150 1729.62 84 0.060042888
151 1741.3 62 0.04431737
152 1752.99 71 0.050750536
153 1764.68 54 0.038598999
154 1776.36 53 0.037884203
155 1788.05 53 0.037884203
156 1799.74 34 0.024303074
157 1811.42 36 0.025732666
158 1823.11 46 0.032880629
159 1834.8 45 0.032165833
160 1846.48 41 0.029306648
161 1858.17 33 0.023588277
162 1869.86 33 0.023588277
163 1881.54 29 0.020729092
164 1893.23 39 0.027877055
165 1904.92 37 0.026447462
166 1916.6 28 0.020014296
167 1928.29 41 0.029306648
168 1939.98 39 0.027877055
169 1951.66 46 0.032880629
170 1963.35 46 0.032880629
171 1975.04 36 0.025732666
172 1986.72 39 0.027877055
173 1998.41 47 0.033595425
174 2010.1 59 0.042172981
175 2021.78 56 0.040028592
176 2033.47 44 0.031451036
177 2045.15 57 0.040743388
178 2056.84 50 0.035739814
179 2068.53 68 0.048606147
180 2080.21 56 0.040028592
181 2091.9 53 0.037884203
182 2103.59 51 0.03645461
183 2115.27 30 0.021443888
184 2126.96 40 0.028591851
185 2138.65 47 0.033595425
186 2150.33 42 0.030021444
187 2162.02 49 0.035025018
188 2173.71 31 0.022158685
189 2185.39 29 0.020729092
190 2197.08 49 0.035025018
191 2208.77 26 0.018584703
192 2220.45 32 0.022873481
193 2232.14 23 0.016440315
194 2243.83 19 0.013581129
195 2255.51 31 0.022158685
196 2267.2 16 0.011436741
197 2278.89 21 0.015010722
198 2290.57 19 0.013581129
199 2302.26 20 0.014295926
200 2313.95 14 0.010007148
201 2325.63 18 0.012866333
202 2337.32 18 0.012866333
203 2349.01 22 0.015725518
204 2360.69 27 0.0192995
205 2372.38 21 0.015010722
206 2384.07 21 0.015010722
207 2395.75 35 0.02501787
208 2407.44 49 0.035025018
209 2419.13 55 0.039313796
210 2430.81 47 0.033595425
211 2442.5 68 0.048606147
212 2454.19 56 0.040028592
213 2465.87 57 0.040743388
214 2477.56 63 0.045032166
215 2489.25 52 0.037169407
216 2500.93 61 0.043602573
217 2512.62 37 0.026447462
218 2524.31 34 0.024303074
219 2535.99 34 0.024303074
220 2547.68 26 0.018584703
221 2559.37 15 0.010721944
222 2571.05 16 0.011436741
223 2582.74 9 0.006433167
224 2594.43 12 0.008577555
225 2606.11 8 0.00571837
226 2617.8 11 0.007862759
227 2629.48 2 0.001429593
228 2641.17 7 0.005003574
229 2652.86 5 0.003573981
230 2664.54 2 0.001429593
231 2676.23 2 0.001429593
232 2687.92 0 0
233 2699.6 6 0.004288778
234 2711.29 2 0.001429593
235 2722.98 6 0.004288778
236 2734.66 5 0.003573981
237 2746.35 5 0.003573981
238 2758.04 2 0.001429593
239 2769.72 5 0.003573981
240 2781.41 6 0.004288778
241 2793.1 7 0.005003574
242 2804.78 2 0.001429593
243 2816.47 2 0.001429593
244 2828.16 6 0.004288778
245 2839.84 5 0.003573981
246 2851.53 4 0.002859185
247 2863.22 1 0.000714796
248 2874.9 4 0.002859185
249 2886.59 4 0.002859185
250 2898.28 5 0.003573981
251 2909.96 2 0.001429593
252 2921.65 3 0.002144389
253 2933.34 4 0.002859185
254 2945.02 5 0.003573981
255 2956.71 3 0.002144389
256 2968.4 0 0
257 2980.08 0 0

View File

@ -0,0 +1,3 @@
Energy,Data,cps
0,0,0
10000,0,0
1 Energy Data cps
2 0 0 0
3 10000 0 0

View File

@ -1,4 +1,5 @@
name,type,is_isotropic name,type,is_isotropic
dummy,NaI,true dummy,NaI,true
LU_NaI_3inch,NaI,true LU_NaI_3inch,NaI,true
LU_HPGe_90,HPGe,false LU_HPGe_90,HPGe,false
LU_NaIR,NaI,false
1 name type is_isotropic
2 dummy NaI true
3 LU_NaI_3inch NaI true
4 LU_HPGe_90 HPGe false
5 LU_NaIR NaI false

View File

@ -0,0 +1,5 @@
energy_keV,field_efficiency_m2
0,0
661.657,0.0261
1173.228,0.0203
1332.492,0.0166
1 energy_keV field_efficiency_m2
2 0 0
3 661.657 0.0261
4 1173.228 0.0203
5 1332.492 0.0166

View File

@ -104,7 +104,7 @@ class ConfigParser:
def _parse_options(self) -> SimulationOptionsSpec: def _parse_options(self) -> SimulationOptionsSpec:
options = self.config.get("options", {}) options = self.config.get("options", {})
allowed = {"air_density_kg_per_m3", "seed"} allowed = {"air_density_kg_per_m3", "seed", "bkg_cps"}
self._warn_unknown_keys( self._warn_unknown_keys(
section="options", section="options",
provided=set(options.keys()), provided=set(options.keys()),
@ -116,23 +116,33 @@ class ConfigParser:
defaults.DEFAULT_AIR_DENSITY defaults.DEFAULT_AIR_DENSITY
) )
seed = options.get("seed") seed = options.get("seed")
bkg_cps = options.get("bkg_cps")
if not isinstance(air_density, float) or air_density <= 0: if not isinstance(air_density, float) or air_density <= 0:
raise InvalidConfigValueError( raise InvalidConfigValueError(
"options.air_density_kg_per_m3 must be a positive float " "options.air_density_kg_per_m3 must be a positive float "
"in kg/m^3." "in kg/m^3."
) )
if ( if (
seed is not None or seed is not None and
(isinstance(seed, int) and seed <= 0) (isinstance(seed, int) and seed <= 0)
): ):
raise InvalidConfigValueError( raise InvalidConfigValueError(
"Seed must be a positive integer value." "Seed must be a positive integer value."
) )
if bkg_cps is not None and (
not isinstance(bkg_cps, int) or bkg_cps < 0
):
raise InvalidConfigValueError(
"Background CPS must be an integer >= 0."
)
return SimulationOptionsSpec( return SimulationOptionsSpec(
air_density=air_density, air_density=air_density,
seed=seed, seed=seed,
bkg_cps=bkg_cps
) )
def _parse_path(self) -> PathSpec: def _parse_path(self) -> PathSpec:

View File

@ -18,6 +18,7 @@ class RuntimeSpec:
class SimulationOptionsSpec: class SimulationOptionsSpec:
air_density: float air_density: float
seed: int | None = None seed: int | None = None
bkg_cps: int | None = None
@dataclass @dataclass

View File

@ -107,7 +107,8 @@ class LandscapeBuilder:
def set_point_sources( def set_point_sources(
self, self,
*sources: AbsolutePointSourceSpec | RelativePointSourceSpec *sources: AbsolutePointSourceSpec | RelativePointSourceSpec,
bounds_check: bool = False
): ):
"""Add one or more point sources to the world. """Add one or more point sources to the world.
@ -145,8 +146,12 @@ class LandscapeBuilder:
along_path=along_path, along_path=along_path,
side=s.side, side=s.side,
dist_from_path=s.dist_from_path) dist_from_path=s.dist_from_path)
if any(
p < 0 or p >= s for p, s in zip(pos, self._size) # we dont support -x values, but negative y values are possible as
# the path is centered in the y direction.
if bounds_check and not (
(0 <= pos[0] <= self._size[0]) and
(-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

@ -2,6 +2,7 @@ import argparse
import logging import logging
import sys import sys
from numpy.random import SeedSequence
from pandas.errors import ParserError from pandas.errors import ParserError
from pg_rad.exceptions.exceptions import ( from pg_rad.exceptions.exceptions import (
@ -17,6 +18,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():
@ -29,9 +31,9 @@ def main():
help="Build from a config file." help="Build from a config file."
) )
parser.add_argument( parser.add_argument(
"--test", "--example",
action="store_true", action="store_true",
help="Load and run the test landscape." help="Load and run an example landscape."
) )
parser.add_argument( parser.add_argument(
"--loglevel", "--loglevel",
@ -39,18 +41,23 @@ 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()
setup_logger(args.loglevel) setup_logger(args.loglevel)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if args.test: if args.example:
test_yaml = """ input_config = """
name: Test landscape name: Example landscape
speed: 8.33 speed: 8.33
acquisition_time: 1 acquisition_time: 1
@ -66,67 +73,79 @@ def main():
sources: sources:
test_source: test_source:
activity_MBq: 100 activity_MBq: 100
position: [250, 100, 0] position: [250, 30, 0]
isotope: Cs137 isotope: Cs137
gamma_energy_keV: 661 gamma_energy_keV: 661
detector: LU_NaI_3inch detector: LU_NaI_3inch
"""
cp = ConfigParser(test_yaml).parse() options:
seed: 1234
"""
elif args.config:
input_config = args.config
else:
logger.warning(
"No input provided. Try --example or --config path/to/config.yml. "
)
sys.exit(1)
try:
cp = ConfigParser(input_config).parse()
if cp.options.seed is None:
entr = SeedSequence().entropy
cp.options.seed = int(str(entr)[:6])
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
) 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

@ -2,6 +2,8 @@ from typing import Tuple, TYPE_CHECKING
import numpy as np import numpy as np
from pg_rad.background.background import generate_background
if TYPE_CHECKING: if TYPE_CHECKING:
from pg_rad.landscape.landscape import Landscape from pg_rad.landscape.landscape import Landscape
from pg_rad.detector.detector import Detector from pg_rad.detector.detector import Detector
@ -106,7 +108,10 @@ def calculate_counts_along_path(
landscape: "Landscape", landscape: "Landscape",
detector: "Detector", detector: "Detector",
velocity: float, velocity: float,
t_acq: float,
points_per_segment: int = 10, points_per_segment: int = 10,
bkg_cps_input: int | None = None,
seed: int | None = None
) -> Tuple[np.ndarray, np.ndarray]: ) -> Tuple[np.ndarray, np.ndarray]:
"""Compute the counts recorded in each acquisition period in the landscape. """Compute the counts recorded in each acquisition period in the landscape.
@ -114,6 +119,7 @@ def calculate_counts_along_path(
landscape (Landscape): _description_ landscape (Landscape): _description_
detector (Detector): _description_ detector (Detector): _description_
points_per_segment (int, optional): _description_. Defaults to 100. points_per_segment (int, optional): _description_. Defaults to 100.
bkg_cps_input (int | None, optional): Optional background CPS.
Returns: Returns:
Tuple[np.ndarray, np.ndarray]: Array of acquisition points and Tuple[np.ndarray, np.ndarray]: Array of acquisition points and
@ -146,12 +152,41 @@ def calculate_counts_along_path(
landscape, full_positions, detector landscape, full_positions, detector
) )
# reshape so each segment is on a row if bkg_cps_input is None:
cps_per_seg = cps.reshape(num_segments, points_per_segment) bkg = generate_background(
cps, detector, landscape.point_sources[0].isotope.E,
seed=seed
)
elif bkg_cps_input == 0:
bkg = bkg_cps_input
else:
bkg = generate_background(
cps, detector, landscape.point_sources[0].isotope.E,
lam_inp=bkg_cps_input,
seed=seed
)
du = s[1] - s[0] cps_with_bg = cps + bkg
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
return original_distances, s, cps, int_counts_result # Integrate along time dimension. see e.g. Bukartas (2021 doccomp.)
t = s / abs(velocity)
# acquisition bins
t_bins = np.arange(0, t[-1] + t_acq, t_acq)
int_counts = np.zeros(len(t_bins) - 1)
acq_points = np.zeros(len(t_bins) - 1)
for i in range(len(t_bins) - 1):
mask = (t >= t_bins[i]) & (t < t_bins[i + 1])
int_counts[i] = np.trapezoid(cps_with_bg[mask], t[mask])
acq_points[i] = np.mean(s[mask])
return (
acq_points[:-1],
s[:-1],
cps_with_bg[:-1],
int_counts[:-1],
np.mean(bkg)
)

View File

@ -4,11 +4,10 @@ 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__)
plt.set_loglevel(level='warning')
class LandscapeSlicePlotter: class LandscapeSlicePlotter:
@ -58,12 +57,7 @@ class LandscapeSlicePlotter:
ax.set_xlim(right=max(width, .5*height)) ax.set_xlim(right=max(width, .5*height))
# if the road is very flat, we center it vertically (looks better) # if the road is very flat, we center it vertically (looks better)
if median(landscape.path.y_list) == 0: ax.set_ylim(bottom=-.5*width, top=.5*width)
h = max(height, .5*width)
ax.set_ylim(bottom=-h//2,
top=h//2)
else:
ax.set_ylim(top=max(height, .5*width))
ax.set_xlabel("X [m]") ax.set_xlabel("X [m]")
ax.set_ylabel("Y [m]") ax.set_ylabel("Y [m]")

View File

@ -10,6 +10,9 @@ from pg_rad.simulator.outputs import SimulationOutput
from pg_rad.landscape.landscape import Landscape from pg_rad.landscape.landscape import Landscape
plt.set_loglevel(level='warning')
class ResultPlotter: class ResultPlotter:
def __init__(self, landscape: Landscape, output: SimulationOutput): def __init__(self, landscape: Landscape, output: SimulationOutput):
self.landscape = landscape self.landscape = landscape
@ -23,6 +26,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)
@ -37,10 +49,11 @@ class ResultPlotter:
self._draw_cps(ax_cps) self._draw_cps(ax_cps)
ax_counts = fig.add_subplot(gs[0, 1]) ax_counts = fig.add_subplot(gs[0, 1])
self._draw_count_rate(ax_counts) self._draw_counts(ax_counts)
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 +74,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 +82,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()
@ -77,17 +92,35 @@ class ResultPlotter:
def _draw_cps(self, ax): def _draw_cps(self, ax):
x = self.count_rate_res.sub_points x = self.count_rate_res.sub_points
y = self.count_rate_res.cps y = self.count_rate_res.cps
ax.plot(x, y, color='b') ax.plot(x, y, color='b', label=f'max(CPS) = {y.max():.2f}')
ax.legend(handlelength=0, handletextpad=0, fancybox=True)
ax.set_title('Counts per second (CPS)') ax.set_title('Counts per second (CPS)')
ax.set_xlabel('Arc length s [m]') ax.set_xlabel('Arc length s [m]')
ax.set_ylabel('CPS [s$^{-1}$]') ax.set_ylabel('CPS [s$^{-1}$]')
def _draw_count_rate(self, ax): def _draw_counts(self, ax):
x = self.count_rate_res.acquisition_points x = self.count_rate_res.distance[1:]
y = self.count_rate_res.integrated_counts y = self.count_rate_res.integrated_counts[1:]
ax.plot(x, y, color='r', linestyle='--', alpha=0.2)
yerr = np.sqrt(y)
peak_idx = y.argmax()
ax.fill_between(
x,
y - yerr,
y + yerr,
color='red',
alpha=0.15,
)
ax.plot(
x, y,
color='r', linestyle='--', alpha=0.2,
label=rf'max(counts)={y[peak_idx]:.2f} $\pm$ {yerr[peak_idx]:.2f}'
)
ax.legend(handlelength=0, handletextpad=0, fancybox=True)
ax.scatter(x, y, color='r', marker='x') ax.scatter(x, y, color='r', marker='x')
ax.set_title('Integrated counts') ax.set_title('Counts')
ax.set_xlabel('Arc length s [m]') ax.set_xlabel('Arc length s [m]')
ax.set_ylabel('N') ax.set_ylabel('N')
@ -99,6 +132,8 @@ class ResultPlotter:
["Air density (kg/m^3)", round(self.landscape.air_density, 3)], ["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)],
["Readout points", len(self.count_rate_res.integrated_counts)], ["Readout points", len(self.count_rate_res.integrated_counts)],
["Seed", self.count_rate_res.seed],
["Mean background cps", round(self.count_rate_res.mean_bkg_cps, 3)]
] ]
ax.table( ax.table(
@ -124,7 +159,7 @@ class ResultPlotter:
# list field efficiencies for each primary gamma in the landscape # list field efficiencies for each primary gamma in the landscape
effs = {e: det.get_efficiency(e) for e in source_energies} effs = {e: det.get_efficiency(e) for e in source_energies}
formatted_effs = ", ".join( formatted_effs = ", ".join(
f"{value:.3f} @ {key:.1f} keV" f"{value:.5f} @ {key:.1f} keV"
for key, value in effs.items() for key, value in effs.items()
) )
ax.set_axis_off() ax.set_axis_off()
@ -201,6 +236,5 @@ class ResultPlotter:
theta_rad = np.radians(theta_deg) theta_rad = np.radians(theta_deg)
print(theta_rad)
ax.plot(theta_rad, eff) ax.plot(theta_rad, eff)
ax.set_title(f"Rel. angular efficiency @ {energy_keV:.1f} keV") ax.set_title(f"Rel. angular efficiency @ {energy_keV:.1f} keV")

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,21 +32,38 @@ 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
) )
def _calculate_count_rate_along_path(self) -> CountRateOutput: def _calculate_count_rate_along_path(self) -> CountRateOutput:
acq_points, sub_points, cps, int_counts = calculate_counts_along_path( acq_points, sub_points, cps, int_counts, mean_bkg_counts = (
self.landscape, calculate_counts_along_path(
self.detector, self.landscape,
velocity=self.runtime_spec.speed self.detector,
velocity=self.runtime_spec.speed,
bkg_cps_input=self.sim_spec.bkg_cps,
t_acq=self.runtime_spec.acquisition_time,
seed=self.sim_spec.seed
)
) )
return CountRateOutput(acq_points, sub_points, cps, int_counts) return CountRateOutput(
self.landscape.path.x_list,
self.landscape.path.y_list,
acq_points,
sub_points,
cps,
int_counts,
mean_bkg_counts,
self.sim_spec.seed
)
def _calculate_point_source_distance_to_path(self) -> List[SourceOutput]: def _calculate_point_source_distance_to_path(self) -> List[SourceOutput]:
@ -69,3 +87,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,10 +5,14 @@ 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]
mean_bkg_cps: List[float]
seed: int
@dataclass @dataclass
@ -21,8 +25,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]

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

@ -0,0 +1,127 @@
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__)
logging.getLogger("PIL").setLevel(logging.WARNING)
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
logger.debug(
f"Integrated counts: {list(sim.count_rate.integrated_counts)}"
)
logger.debug(
f"Distances: {list(sim.count_rate.distance)}"
)
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)
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
)
east_coords = sim.count_rate.x[1:]
north_coords = sim.count_rate.y[1:]
if len(east_coords) != sim.count_rate.integrated_counts.shape[0]:
east_coords = None
north_coords = None
logger.warning(
"PG-RAD currently does not support this experimental path."
" Only ROI_P, ROI_BR and Dist will be saved."
)
result_df = DataFrame(
{
"East": east_coords,
"North": north_coords,
"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

42
tests/test_roi.py Normal file
View File

@ -0,0 +1,42 @@
import pytest
from pg_rad.background.background import get_roi_from_fwhm, get_cps_from_roi
from pg_rad.detector.detector import load_detector
E_gamma = 661.7 # keV
@pytest.fixture
def detector():
return load_detector("LU_NaI_3inch")
@pytest.fixture
def roi_Cs137_NaI(detector):
return get_roi_from_fwhm(detector, E_gamma)
@pytest.mark.parametrize("ref_roi_lo, ref_roi_hi", [
(537.58, 792.69)
])
def test_roi_acquisition_Cs137_NaI(ref_roi_lo, ref_roi_hi, roi_Cs137_NaI):
"""Test against ROI used in GammaVision"""
est_roi_lo, est_roi_hi = roi_Cs137_NaI
assert pytest.approx(est_roi_lo, rel=0.05) == ref_roi_lo
assert pytest.approx(est_roi_hi, rel=0.05) == ref_roi_hi
@pytest.mark.parametrize("ref_cps", [
13.61
])
def test_cps_acquisition_Cs137_NaI(
ref_cps,
roi_Cs137_NaI,
detector
):
"""Test against cps from GammaVision"""
est_roi_lo, est_roi_hi = roi_Cs137_NaI
est_cps = get_cps_from_roi(detector, est_roi_lo, est_roi_hi)
assert pytest.approx(est_cps, rel=0.1) == ref_cps