Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pathsim_chem/tritium/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
from .splitter import *
from .bubbler import *
from .glc import *
from .ionisation_chamber import *
# from .tcap import *
91 changes: 91 additions & 0 deletions src/pathsim_chem/tritium/ionisation_chamber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#########################################################################################
##
## Ionisation Chamber Block
##
#########################################################################################

# IMPORTS ===============================================================================

from pathsim.blocks.function import Function

# BLOCKS ================================================================================

class IonisationChamber(Function):
"""Ionisation chamber for tritium detection.

Algebraic block that models a flow-through ionisation chamber. The sample
passes through unchanged while the chamber produces a signal proportional
to the tritium concentration, scaled by a detection efficiency.

Mathematical Formulation
-------------------------
The chamber receives a tritium flux and flow rate, computes the
concentration, and applies the detection efficiency:

.. math::

c = \\frac{\\Phi_{in}}{\\dot{V}}

.. math::

\\text{signal} = \\varepsilon(c) \\cdot c

.. math::

\\Phi_{out} = \\Phi_{in}

where :math:`\\varepsilon` is the detection efficiency (constant or
concentration-dependent).

Parameters
----------
detection_efficiency : float or callable, optional
Constant efficiency factor or a function ``f(c) -> float`` that
returns the efficiency for a given concentration. Mutually
exclusive with *detection_threshold*.
detection_threshold : float, optional
If provided, the efficiency is a step function: 1 above the
threshold, 0 below. Mutually exclusive with *detection_efficiency*.
"""

input_port_labels = {
"flux_in": 0,
"flow_rate": 1,
}

output_port_labels = {
"flux_out": 0,
"signal": 1,
}

def __init__(self, detection_efficiency=None, detection_threshold=None):

# input validation
if detection_efficiency is not None and detection_threshold is not None:
raise ValueError(
"Specify either 'detection_efficiency' or 'detection_threshold', not both"
)
if detection_efficiency is None and detection_threshold is None:
raise ValueError(
"One of 'detection_efficiency' or 'detection_threshold' must be provided"
)

if detection_threshold is not None:
self.detection_efficiency = lambda c: 1.0 if c >= detection_threshold else 0.0
else:
self.detection_efficiency = detection_efficiency

self.detection_threshold = detection_threshold

super().__init__(func=self._eval)

def _eval(self, flux_in, flow_rate):
concentration = flux_in / flow_rate if flow_rate > 0 else 0.0

eff = self.detection_efficiency
epsilon = eff(concentration) if callable(eff) else eff

signal = epsilon * concentration
flux_out = flux_in

return (flux_out, signal)
117 changes: 117 additions & 0 deletions tests/tritium/test_ionisation_chamber.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
########################################################################################
##
## TESTS FOR
## 'tritium.ionisation_chamber.py'
##
########################################################################################

# IMPORTS ==============================================================================

import unittest

from pathsim_chem.tritium import IonisationChamber


# TESTS ================================================================================

class TestIonisationChamber(unittest.TestCase):
"""Test the IonisationChamber block."""

def test_init_constant_efficiency(self):
"""Test initialization with constant detection efficiency."""
ic = IonisationChamber(detection_efficiency=0.8)
self.assertEqual(ic.detection_efficiency, 0.8)
self.assertIsNone(ic.detection_threshold)

def test_init_threshold(self):
"""Test initialization with detection threshold."""
ic = IonisationChamber(detection_threshold=10.0)
self.assertEqual(ic.detection_threshold, 10.0)
self.assertTrue(callable(ic.detection_efficiency))

def test_init_callable_efficiency(self):
"""Test initialization with callable detection efficiency."""
eff = lambda c: min(c / 100.0, 1.0)
ic = IonisationChamber(detection_efficiency=eff)
self.assertIs(ic.detection_efficiency, eff)

def test_init_validation_both(self):
"""Providing both parameters should raise ValueError."""
with self.assertRaises(ValueError):
IonisationChamber(detection_efficiency=0.5, detection_threshold=10.0)

def test_init_validation_neither(self):
"""Providing neither parameter should raise ValueError."""
with self.assertRaises(ValueError):
IonisationChamber()

def test_port_labels(self):
"""Test port label definitions."""
self.assertEqual(IonisationChamber.input_port_labels["flux_in"], 0)
self.assertEqual(IonisationChamber.input_port_labels["flow_rate"], 1)
self.assertEqual(IonisationChamber.output_port_labels["flux_out"], 0)
self.assertEqual(IonisationChamber.output_port_labels["signal"], 1)

def test_passthrough(self):
"""Sample flux passes through unchanged."""
ic = IonisationChamber(detection_efficiency=0.5)
ic.inputs[0] = 100.0 # flux_in
ic.inputs[1] = 10.0 # flow_rate
ic.update(None)

self.assertAlmostEqual(ic.outputs[0], 100.0)

def test_signal_constant_efficiency(self):
"""Signal = efficiency * concentration."""
ic = IonisationChamber(detection_efficiency=0.8)
ic.inputs[0] = 200.0 # flux_in
ic.inputs[1] = 10.0 # flow_rate -> concentration = 20
ic.update(None)

self.assertAlmostEqual(ic.outputs[1], 0.8 * 20.0)

def test_signal_threshold_above(self):
"""Above threshold, signal = concentration."""
ic = IonisationChamber(detection_threshold=5.0)
ic.inputs[0] = 100.0 # flux
ic.inputs[1] = 10.0 # flow -> concentration = 10 > 5
ic.update(None)

self.assertAlmostEqual(ic.outputs[1], 10.0)

def test_signal_threshold_below(self):
"""Below threshold, signal = 0."""
ic = IonisationChamber(detection_threshold=50.0)
ic.inputs[0] = 100.0 # flux
ic.inputs[1] = 10.0 # flow -> concentration = 10 < 50
ic.update(None)

self.assertAlmostEqual(ic.outputs[1], 0.0)

def test_signal_callable_efficiency(self):
"""Callable efficiency applied to concentration."""
# Linear ramp: efficiency = c / 100, capped at 1
eff = lambda c: min(c / 100.0, 1.0)
ic = IonisationChamber(detection_efficiency=eff)
ic.inputs[0] = 500.0 # flux
ic.inputs[1] = 10.0 # flow -> concentration = 50
ic.update(None)

# efficiency(50) = 0.5, signal = 0.5 * 50 = 25
self.assertAlmostEqual(ic.outputs[1], 25.0)

def test_zero_flow_rate(self):
"""Zero flow rate should not crash, signal = 0."""
ic = IonisationChamber(detection_efficiency=1.0)
ic.inputs[0] = 100.0
ic.inputs[1] = 0.0
ic.update(None)

self.assertAlmostEqual(ic.outputs[0], 100.0)
self.assertAlmostEqual(ic.outputs[1], 0.0)


# RUN TESTS LOCALLY ====================================================================

if __name__ == '__main__':
unittest.main(verbosity=2)