Metadata-Version: 2.4
Name: radionuclide-emission-db
Version: 0.4.5
Summary: Ideal photon emission line spectra from MIRDspecs CSV data, source mixtures and narrow-beam filtering
Author-email: Konstantinos Karafasoulis <ckaraf@gmail.com>
License-Expression: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: plot
Requires-Dist: matplotlib>=3.5; extra == "plot"
Provides-Extra: attenuation
Requires-Dist: xraylib>=4.0; extra == "attenuation"
Provides-Extra: all
Requires-Dist: xraylib>=4.0; extra == "all"
Requires-Dist: matplotlib>=3.5; extra == "all"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"

# radionuclide-emission-db

**Ideal radionuclide photon-emission spectra and energy-dependent material attenuation in Python.**

`radionuclide-emission-db` separates three tasks that should not be confused:

1. **Emission:** obtain gamma-ray, characteristic X-ray, and annihilation-photon emission lines from nuclear decay data.
2. **Filtering:** calculate the attenuation of those photons as they pass through one or more materials.
3. **Detection:** apply a detector response separately (not implemented in this package).

The package is useful for creating source spectra, studying photon transmission through absorbers, preparing labeled photon-energy samples, and supplying idealized spectra to downstream simulations. **It does not simulate a measured gamma-ray spectrum or perform full radiation transport.**

> **Data download required:** The normal code-only installation does **not** provide the emission-line CSVs or the 92 NIST attenuation tables. Download both datasets after installation (instructions below). A privately built data-inclusive wheel is possible, subject to the data providers' redistribution terms.

## Installation

Python 3.10 or later is recommended. Install the package and the optional plotting and material-composition dependencies:

```bash
python -m pip install 'radionuclide-emission-db[all]'
```

For development from the extracted source directory (the directory containing `pyproject.toml`):

```bash
python -m pip install -e '.[all]'
```

`matplotlib` is needed for `spectrum.plot()`. `xraylib` is used for chemical-formula parsing and may also support alternative attenuation calculations. In current releases, **NIST SRD 126 elemental tables are the default source of attenuation coefficients**; xraylib is *not* a bundled replacement for the NIST tables.

Check your installation:

```bash
python -m pip show radionuclide-emission-db
python -c "from radionuclide_emission_db import Radionuclide, MaterialFilter, AttenuationMaterial; print('Imports OK')"
```

### Required post-installation step 1 — download emission-line data

Emission data come from **MIRDspecs / ICRP Publication 107**: <https://mirdsoft.org/resources/mirdspecs>. Files are downloaded as radionuclide-specific summary-spectrum CSVs; they are not embedded in the standard code-only distribution.

```bash
# Download the radionuclides listed in the online MIRDspecs index:
radionuclide-emission-db cache-all --workers 4

# Confirm which files were saved:
radionuclide-emission-db list-cached
```

To download a few nuclides first:

```bash
radionuclide-emission-db cache-all --isotopes Co-60 Cs-137 Ba-137m Am-241
```

To refresh the local copy:

```bash
radionuclide-emission-db cache-all --refresh
```

**Storage:** In the project-local-data workflow, CSVs are placed in `src/radionuclide_emission_db/data/`. You can choose a writable directory explicitly:

```bash
radionuclide-emission-db cache-all --cache-dir "$HOME/NuclearData/MIRDspecs"
```

If installed from PyPI into a protected `site-packages` directory, **do not rely on writing into the installed package**. Specify a user-writable cache directory if needed, and pass the same directory when loading a radionuclide:

```python
from radionuclide_emission_db import Radionuclide

spectrum = Radionuclide(
    "Co-60", cache_dir="~/NuclearData/MIRDspecs"
).load(offline=True)
```

The `~` path is supported in the previously documented interface; if your local version does not expand it, use an absolute path. After download, `.load(offline=True)` prevents network access and raises an error if the required file is unavailable. The bulk index reflects the nuclides currently exposed by the MIRDspecs website; it is not necessarily every radionuclide included in ICRP 107. Some nuclides have no selected photon lines.

### Required post-installation step 2 — download NIST attenuation tables

The default attenuation backend uses the **NIST X-Ray Mass Attenuation Coefficients (SRD 126)** for elements **Z = 1–92**, nominally covering photon energies from **1 keV to 20 MeV**:
<https://physics.nist.gov/PhysRefData/XrayMassCoef/tab3.html>.

```bash
# Download all 92 tables into your writable user-data area:
python -m radionuclide_emission_db.nist_cache --all

# Or download just Al (13), Fe (26), and Pb (82):
python -m radionuclide_emission_db.nist_cache --elements 13 26 82

# Check availability (if supported by your installed version):
python -m radionuclide_emission_db.nist_cache --status
```

The default user-data location documented for the attenuation tables on Linux is:

```text
~/.local/share/radionuclide-emission-db/nist-attenuation/
    z01.json
    z02.json
    ...
    z92.json
```

A first calculation can retrieve a missing table online, but **pre-downloading is strongly recommended** for reproducible offline work. To prevent network access:

```python
from radionuclide_emission_db import AttenuationMaterial
lead = AttenuationMaterial.from_name("Pb", offline=True)
```

If the relevant table is missing, offline mode fails rather than silently substituting a coefficient.

#### Optional: store the tables *inside the project*

From the editable source checkout:

```bash
python -m radionuclide_emission_db.nist_cache --all --project
```

This writes `z01.json` through `z92.json` to:

```text
src/radionuclide_emission_db/data/nist-attenuation/
```

The library checks bundled tables before the user cache. A wheel built **after** these files are populated can include them, but redistribution requires separate permission review (see [Data provenance and licenses](#data-provenance-and-licenses)). This `--project` command is intended for a writable source checkout, not for an installed read-only wheel.

## Quick start: ideal photon-emission spectrum

```python
from radionuclide_emission_db import Radionuclide

spectrum = Radionuclide("Co-60").load(offline=True)
print("Photons per decay:", spectrum.photons_per_decay)

for line in spectrum.lines:
    print(line.energy_keV, line.yield_per_decay, line.radiation_type)

spectrum.plot()                   # requires matplotlib
spectrum.to_csv("Co60_lines.csv")
```

For an emission CSV supplied separately, the API also supports:

```python
spectrum = Radionuclide("Co-60").load(csv_file="Co-60 Summary Spectrum.csv")
```

### What the numbers mean

Each discrete emission line is represented by energy `E` (keV), photon yield per nuclear transformation, and radiation type. MIRDspecs' tabulated yields are **per nuclear transformation**, not per 100 decays; CSV energies expressed in MeV are converted to keV. The default photon selection includes gamma rays, X-rays, and annihilation photons, rather than electrons or beta particles.

For a source of activity `A` in Bq observed for `t` seconds, the expected number of *emitted* photons in line `i` is:

\[
N_i = A\,t\,Y_i,
\]

where `Y_i` is the yield per transformation. This is **not the number of detected counts**: it includes neither solid-angle acceptance nor detection efficiency.

```python
expected_emitted = spectrum.as_counts(activity_Bq=1000, duration_s=60)
```

## Mixtures and radioactive equilibrium

`SourceMixture` combines several radionuclide emission spectra using **activities in Bq**, not percentages:

```python
from radionuclide_emission_db import SourceMixture

mix = SourceMixture({"Co-60": 1000, "Am-241": 500})
combined = mix.spectrum()  # line weights: emitted photons per second
combined.to_csv("mixture.csv")
combined.plot()
```

**Daughter activities and equilibrium are not calculated automatically.** For example, the main 661.7 keV gamma ray associated with a Cs-137 source is emitted by its daughter **Ba-137m**. For a Cs-137 source sufficiently old for daughter ingrowth equilibrium, a useful approximation is:

\[
A_{\mathrm{Ba\text{-}137m}} \simeq 0.946\, A_{\mathrm{Cs\text{-}137}}.
\]

Illustrative mixture:

```python
mix = SourceMixture({"Cs-137": 1000.0, "Ba-137m": 946.0})
spectrum = mix.spectrum()
```

**Check whether the source CSV includes daughter emissions** before explicitly combining parent and daughter, or those emissions may be counted twice. This example assumes the parent-only emission file and equilibrium branching approximation; the package has no general Bateman-equation solver.

## Material attenuation: physical model

For a monoenergetic photon line, the package calculates **narrow-beam, uncollided transmission** through a homogeneous absorber:

\[
T(E) = \frac{I(E)}{I_0(E)} = \exp[-\mu(E)x]
     = \exp\left[-\left(\frac{\mu}{\rho}\right)(E)\rho x\right].
\]

Here:

| Symbol | Meaning | Units |
|---|---|---|
| `E` | Photon energy | keV |
| `μ/ρ` | Total mass attenuation coefficient | cm²/g |
| `ρ` | Material density | g/cm³ |
| `μ` | Linear attenuation coefficient | cm⁻¹ |
| `x` | Absorber thickness | cm |
| `T` | Fraction of uncollided photons transmitted | dimensionless |

When a thickness is specified in millimeters, the program uses `x_cm = thickness_mm / 10`.

### Built-in material coefficients

For elemental absorbers, the current default uses tabulated **NIST SRD 126** mass attenuation coefficients. For compounds, `xraylib` parses the chemical formula to obtain elemental mass fractions and the package applies the mixture rule:

\[
\left(\frac{\mu}{\rho}\right)_{\!\mathrm{compound}}(E)
=\sum_j w_j\left(\frac{\mu}{\rho}\right)_j(E).
\]

The package has documented default material densities for Pb, Al, Fe, Cu, W, Si, water (`H2O`), NaI, and CZT. For other compounds, supply the formula and physical density explicitly.

```python
from radionuclide_emission_db import AttenuationMaterial

lead = AttenuationMaterial.from_name("Pb", offline=True)
print("mu/rho [cm2/g]:", lead.mass_attenuation_cm2_g(661.7))
print("mu [1/cm]:", lead.linear_attenuation_cm_inv(661.7))
print("Transmission:", lead.transmission(661.7, thickness_mm=5.0))

quartz = AttenuationMaterial.from_name("SiO2", density_g_cm3=2.65)
print(quartz.transmission(661.7, thickness_mm=10.0))
```

### One filter, then two filters

```python
from radionuclide_emission_db import Radionuclide, MaterialFilter

spectrum = Radionuclide("Co-60").load(offline=True)

pb = MaterialFilter.from_material("Pb", thickness_mm=2.0, offline=True)
al = MaterialFilter.from_material("Al", thickness_mm=5.0, offline=True)

after_pb = spectrum.apply_filter(pb, min_energy_keV=5.0)
after_both = after_pb.apply_filter(al, min_energy_keV=5.0)

after_pb.to_csv("Co60_after_Pb.csv")
after_both.to_csv("Co60_after_Pb_Al.csv")
```

For several uniform layers, the transmissions multiply:

\[
T_{\mathrm{total}}(E) = \prod_j \exp[-\mu_j(E)x_j].
\]

The implementation changes the *intensity* of each existing line. It does not create a Compton continuum, scattered photons, secondary characteristic X-rays, or a detector peak shape.

### User-supplied coefficient tables

You can also define your own **linear** attenuation coefficients in cm⁻¹:

```python
custom_filter = MaterialFilter.from_table(
    name="illustrative",
    thickness_mm=2.0,
    mu_table={100: 2.0, 2000: 0.2},  # illustrative values ONLY
)
custom_result = spectrum.apply_filter(custom_filter)
```

User-supplied tables require sufficient coverage of the requested photon energies. Values beyond the table's range are not extrapolated; use validated physical data rather than the illustrative coefficients above.

## Absorption edges, energy limits, and interpolation

These limits are scientifically important; **they are not arbitrary plotting limits**.

### 1. Coefficient energy coverage: 1 keV–20 MeV

The NIST SRD 126 tabulations used by the package cover **approximately 1–20,000 keV**. The package must not extrapolate the tabulated coefficients beyond this range. A requested photon line outside the coefficient range can raise an exception unless it is deliberately excluded by a spectrum-selection cutoff.

### 2. Lower-energy cutoff when filtering

MIRDspecs files can contain very low-energy photons. For example, an entry of `0.014503 keV` corresponds to **14.503 eV**, well below 1 keV. Attempting to obtain a NIST/X-ray coefficient at such an energy is not valid.

For automatically generated material filters, the documented default is to **omit lines below 1 keV** and issue a warning. You can request a higher threshold:

```python
filtered = spectrum.apply_filter(pb, min_energy_keV=5.0)
```

This excludes *all* lines with `E < 5 keV` from the filtered output. They are **not assumed to be fully absorbed**; rather, their transmission is **not calculated**. The original source spectrum remains available unchanged. Set `min_energy_keV=0` only if you intentionally want strict evaluation of every line, accepting that out-of-range energies may raise an error.

The cutoff should reflect your intended energy range of interest; it does **not** change the physical NIST data range or extend the model to lower energies.

### 3. Absorption-edge discontinuities

Photoelectric attenuation changes sharply at an element's absorption edges (K, L, etc.). Near an edge, attenuation immediately below and above the threshold can differ substantially. The NIST tables include edge-specific information; the package is designed to **preserve the discontinuity** rather than interpolate smoothly across it.

Away from edges, coefficients are obtained by **log–log interpolation** between valid tabulated energies. Near edges, accurate calculations require the tabulated pre-edge and post-edge values to be interpreted on the correct side of the discontinuity. Avoid collapsing duplicated edge energies into one row or smoothing across an edge. For a photon exactly at a tabulated edge, the choice of side depends on the interpolation convention; consult the implementation and source table when edge-level precision matters.

### 4. Upper limit and unsupported physics

High-energy emission lines up to 20 MeV are within the NIST tabulation domain, including the prominent Co-60 photons near 1173 and 1332 keV. Values above 20 MeV are not computed by extrapolation. Photon production via absorber interactions (including scattered and fluorescent photons) remains outside the narrow-beam model at *all* energies.

## Photon sampling and output

```python
sampled_energies_keV = spectrum.sample_energies(10000, seed=42)
```

These are energies sampled from the relative photon-line weights, **conditional on selecting an emitted photon**. They are not full decay histories: event time, photon direction, emission point, and coincidences are not simulated.

The package exports generic CSV line tables through `to_csv()`. These are **not OASYS-native ray files**. An OASYS interface would additionally have to define ray positions, directions, statistical weights, and any expected source format.

## Working offline

1. Download the necessary MIRDspecs emission CSVs.
2. Download the necessary NIST elemental attenuation tables (or all 92 tables).
3. Use `Radionuclide(...).load(offline=True)`.
4. Use `AttenuationMaterial.from_name(..., offline=True)` or `MaterialFilter.from_material(..., offline=True)`.

Keep both local datasets when moving between computers. If you install a **code-only PyPI wheel** on another machine, repeat both downloads there. An editable checkout with data saved under `src/.../data/` is different from an installed wheel with user cache files.

## Building a distribution

```bash
python -m pip install --upgrade build twine
rm -rf build dist src/*.egg-info
python -m build
python -m twine check dist/*
unzip -l dist/*.whl
```

**Before any public upload**, inspect the wheel *and* source distribution for MIRDspecs CSVs and NIST JSONs. If redistribution permissions have not been established, omit those files from the public artifacts even if they are present in your local checkout. Building a wheel from a populated `data/` directory may automatically bundle the files.

For a private offline wheel, after permission checks, populate the source-tree data directories and rebuild. Be aware that an installed wheel's package directory may be read-only; prefer a user-writable location for subsequent dataset updates.

## Data provenance and licenses

- **Emission lines:** MIRDspecs, based on ICRP Publication 107 — <https://mirdsoft.org/resources/mirdspecs>.
- **Attenuation coefficients:** NIST X-Ray Mass Attenuation Coefficients (SRD 126) — <https://physics.nist.gov/PhysRefData/XrayMassCoef/tab3.html>.
- **Composition parsing:** xraylib, an external Python dependency.

The package's software license does **not** automatically grant rights to redistribute third-party emission data or NIST tables. Review the corresponding notices and [DATA_LICENSE.md](DATA_LICENSE.md) before publishing data-inclusive wheels or archives. Respect the providers' terms for automated downloading.

## Known limitations and validation

- The package represents **ideal discrete photon lines**, not realistic detector spectra.
- No automatic daughter ingrowth/equilibrium calculation or general radioactive-chain solver.
- No photon transport, scattering, fluorescence, buildup, source self-attenuation, collimator geometry, or detector response.
- Compounds are treated with an elemental mass-fraction mixture rule; chemical and material-specific effects beyond that approximation are not modeled.
- The NIST backend relies on downloaded, correctly parsed reference tables; validate the actual files against selected NIST reference values before quantitative publication.
- CSV formats and online site markup may change. A complete bulk download can include individual failures; inspect the command output and confirm the saved file count.
- `xraylib` is an optional dependency, not a guarantee of broader energy coverage than the NIST tables.

For scientific results, report the nuclear-data source, data-download date, selected photon types, emission normalization, absorber composition and density, absorber thickness, energy cutoff, interpolation convention, and whether calculations included only uncollided photons.
