objects around us can be slightly radioactive. Americium in smoke detectors, radium in some vintage watches, or uranium in vintage glass; a full list can be long. Mostly, these objects are safe and cannot cause a health risk. It is also interesting to identify them and study the matter on the atomic level. And we can do this using a radiation detector. In the first part, I did an exploratory data analysis of the gamma spectroscopy data. In the second part, I created a machine learning model for detecting radioactive isotopes. This is the last 3rd part, and it’s time to add a created model to the real app!
In this story, I will test two approaches:
- I will create a public Streamlit app that will be hosted for free on Streamlit Cloud (the app link is added to the end of the article).
- As a more flexible and universal solution, I will create a Python HTMX-based app that can communicate with real hardware and make predictions in real time.
In the same way as in the previous part, I will use a Radiacode scintillation detector to get the data (disclaimer: the device used in this test was provided by the manufacturer; I don’t get any commercial profit from their sales, and I did not get any editorial input about all the tests). Readers who don’t have a Radiacode hardware will be able to test the app and the model using files available on Kaggle.
Let’s get started!
1. Isotopes Classification Model
This model was described in the previous part. It is based on XGBoost, and I trained the model using different radioactive samples. I used samples that can be legally purchased, like vintage uranium glass or old watches with radium dials made in the 1950s. As mentioned before, I also used a Radiacode scintillation detector, which allows me to get the gamma spectrum of the object. Only 10-20 years ago, these types of detectors were available only in big labs; today, they can be purchased for the price of a mid-range smartphone.
The model contains three components:
- The XGBoost-based model itself.
- A list of radioactive isotopes (like Lead-214 or Actinium-228), on which the model was trained. The Radiacode scintillation detector returns 1024 spectrum values, and 23 of them were used for the model.
- A label encoder to convert list indexes into human-readable names.
Let’s wrap all this into a single Python class:
from xgboost import XGBClassifier
from sklearn.preprocessing import LabelEncoder
class IsotopesClassificationModel:
""" Gamma Spectrum Classification Model """
def __init__(self):
""" Load models """
path = self._get_models_path()
self._classifier = self._load_model(path + "/XGBClassifier.json")
self._isotopes = self._load_isotopes(path + "/isotopes.json")
self._labels_encoder = self._load_labels_encoder(path + "/LabelEncoder.npy")
def predict(self, spectrum: Spectrum) -> str:
""" Predict the isotope """
features = SpectrumPreprocessing.convert_to_features(
spectrum, self._isotopes
)
preds = self._classifier.predict([features])
preds = self._labels_encoder.inverse_transform(preds)
return preds[0]
@staticmethod
def _load_model(filename: str) -> XGBClassifier:
""" Load model from file """
bst = XGBClassifier()
bst.load_model(filename)
return bst
@staticmethod
def _load_isotopes(filename: str) -> List:
with open(filename, "r") as f_in:
return json.load(f_in)
@staticmethod
def _load_labels_encoder(filename: str) -> LabelEncoder:
le = LabelEncoder()
le.classes_ = np.load(filename)
return le
@staticmethod
def _get_models_path() -> str:
""" Get path to models. Model files are stored in
'models/V1/' folder """
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return parent_dir + f"/models/{IsotopesClassificationModel.VERSION}"
A Spectrum class contains the spectrum data we get from a radiation detector:
@dataclass
class Spectrum:
""" Radiation spectrum data """
duration: datetime.timedelta
a0: float
a1: float
a2: float
counts: list[int]
Here, counts is a gamma spectrum, which is represented by a list of 1024 channel values. Spectrum data can be exported using the official Radiacode Android app or retrieved directly from a device using a radiacode Python library.
To load the spectrum into the model, I created a SpectrumPreprocessing class:
class SpectrumPreprocessing:
""" Gamma Spectrum Preprocessing """
@staticmethod
def convert_to_features(spectrum: Spectrum, isotopes: List) -> np.array:
""" Convert the spectrum to the list of features for prediction """
sp_norm = SpectrumPreprocessing._normalize(spectrum)
energies = [energy for _, energy in isotopes]
channels = [SpectrumPreprocessing.energy_to_channel(spectrum, energy) for energy in energies]
return np.array([sp_norm.counts[ch] for ch in channels])
@staticmethod
def load_from_xml_file(file_path: str) -> Spectrum:
""" Load spectrum from a Radiacode Android app file """
Here, I skip some code blocks that were already published in the previous part. Extracting features from the gamma spectrum was also explained there, and I highly recommend reading that part first.
Now, let’s test the model! I took a Radiacode detector and collected a gamma spectrum within 10 minutes:
This Chinese pendant was advertised as “ion-generated,” and it is slightly radioactive. Its gamma spectrum, collected in the official Radiacode Android app, looks like this:
After waiting for ~10 minutes, I exported the spectrum into an XML file. Now, we can run the model:
from spectrum import SpectrumPreprocessing
from ml_models import IsotopesClassificationModel
sp = SpectrumPreprocessing.load_from_file("spectrum.xml")
model = IsotopesClassificationModel()
result = model.predict(sp)
print(result)
#> Thorium
As we can see, the model works well. We can compare the peaks with spectra of known isotopes (for example, here or here) and confirm that the spectrum belongs to thorium.
2. Streamlit
The model works; however, we live in the XXI century, and almost nobody will run the console app to get the results. Instead, we can make the app available online, so all Radiacode users will be able to run it.
There are many Python frameworks for making browser-based apps, and Streamlit is probably the most popular in the data science community. And what is important for us, a Streamlit Community Cloud platform allows everyone to publish their apps completely for free. To do this, let’s make the app first.
2.1 Streamlit App
A Streamlit framework is relatively easy to use, at least if a standard-looking app is good for us. Personally, I’m not a fan of this approach. These frameworks hide all low-level implementation details from users. It is straightforward to make a prototype, but the UI logic will be tightly coupled with a very niche framework and cannot be reused anywhere else. Doing everything non-standard, which is not supported by the framework, can be almost impossible or hard to implement without digging into tons of abstractions and pages of code. However, in our case, the prototype is all we need.
In general, a Streamlit code is simple, and we just need to describe the logical hierarchy of our page:
import streamlit as st
import logging
logger = logging.getLogger(__name__)
def is_xml_valid(xml_data: str) -> bool:
""" Check if the XML has valid size and data """
return len(xml_data) Optional[Spectrum]:
""" Load spectrum from the StringIO stream """
xml_data = stringio.read()
if is_xml_valid(xml_data):
return SpectrumPreprocessing.load_from_xml(xml_data)
return None
def main():
""" Main app """
st.set_page_config(page_title="Gamma Spectrum")
st.title("Radiacode Spectrum Detection")
st.text(
"Export the spectrum to XML using the Radiacode app, and "
"upload it to see the results."
)
# File Upload
uploaded_file = st.file_uploader(
"Choose the XML file", type="xml", key="uploader",
)
if uploaded_file is not None:
stringio = StringIO(uploaded_file.getvalue().decode("utf-8"))
if sp := get_spectrum(stringio):
# Prediction
model = IsotopesClassificationModel()
result = model.predict(sp)
logger.info(f"Spectrum prediction: {result}")
# Show result
st.success(f"Prediction Result: {result}")
# Draw
fig = get_spectrum_barchart(sp)
st.pyplot(fig)
if __name__ == "__main__":
logger.setLevel(logging.INFO)
main()
As we can see, the full app requires a minimal amount of Python code. Streamlit will render all HTML for us, with title, file upload, and results. As a bonus, I will also display a spectrum using Matplotlib:
def get_spectrum_barchart(sp: Spectrum) -> plt.Figure:
""" Get Matplotlib's barchart """
counts = SpectrumPreprocessing.get_counts(sp)
energy = [
SpectrumPreprocessing.channel_to_energy(sp, x) for x in range(len(counts))
]
fig, ax = plt.subplots(figsize=(9, 6))
ax.spines["top"].set_color("lightgray")
ax.spines["right"].set_color("lightgray")
# Bars
ax.bar(energy, counts, width=3.0, label="Counts")
# X values
ticks_x = [SpectrumPreprocessing.channel_to_energy(sp, ch) for ch in range(0, len(counts), len(counts) // 20)]
labels_x = [f"{int(ch)}" for ch in ticks_x]
ax.set_xticks(ticks_x, labels=labels_x, rotation=45)
ax.set_xlim(energy[0], energy[-1])
ax.set_ylim(0, None)
ax.set_title("Gamma spectrum")
ax.set_xlabel("Energy, keV")
ax.set_ylabel("Counts")
return fig
Now we can run the app locally:
streamlit run st-app.py
After that, our app is fully operational and can be tested in a browser:
As mentioned before, I am not a fan of very high-level frameworks and prefer to have a better understanding of how things work “under the hood.” However, considering that I spent only about 100 lines of code to make a fully functional web app, I cannot complain – for prototyping, it works well.
2.2 Streamlit Community Cloud
When the app is tested locally, it’s time to make it public! A Streamlit Cloud is a free service, and obviously, it has a lot of limitations:
- The app runs in a Docker-like container. Your GitHub account must be linked to Streamlit. When the container starts, it pulls your code from GitHub and runs it.
- At the time of writing this text, container resources are limited to 2 cores and up to 2,7 GB of RAM. It would be too constrained to run a 70B size LLM, but for a small XGBoost model, it is more than enough.
- Streamlit does not provide any permanent storage. After shutdown or restart, all logs and temporary files will be lost (you can use API secrets and connect to any other cloud storage from your Python code if needed).
- After a period of inactivity (about 30 minutes), the container will be stopped, and all temporary files will also be lost. If someone opens the app link, it will run again.
As readers can guess, an inactive app costs Streamlit almost nothing because it stores only a small configuration file. And it is a nice solution for a free service – it allows us to publish the app without any costs and give people a link to run it.
To publish the app in Streamlit, we need to perform three simple steps.
First, we need to commit our Python app to GitHub. A requirements.txt file is also mandatory. Streamlit container uses it to install required Python dependencies. In my case, it looks like this:
xgboost==3.0.2
scikit-learn==1.6.1
numpy==1.26.4
streamlit==1.47.0
pillow==11.1.0
matplotlib==3.10.3
xmltodict==0.14.2
Server settings can be changed using a .streamlit/config.toml file. In my case, I limited the uploaded file size to 1 MB because all spectra files are smaller:
[server]
# Max size, in megabytes, for files uploaded with the file_uploader.
# Default: 200
maxUploadSize = 1
Second, we need to log in to share.streamlit.io using a GitHub account and give permission to access the source code.
Finally, we can create a new Streamlit project. In the project settings, we can also select the desired URL and environment:
If everything was done correctly, we can see our app running:
At this moment, users worldwide can also access our app! In my case, I selected a gammaspectrumdetection name, and the app is available using this URL.
3. FastAPI + HTMX App
As readers can see, Streamlit is a nice solution for a simple prototype. However, in the case of the radiation detector, I would like to see data coming from real Radiacode hardware. This would be impossible to do in Streamlit; this library just was not designed for that. Instead, I will use several production-grade frameworks:
- An HTMX framework allows us to make a fully functional web interface.
- FastAPI will run the server.
- The ML model will process the data retrieved in real-time from a radiation detector using a Radiacode library.
As mentioned before, those readers who don’t have a Radiacode hardware will be able to replay the data using raw log files, saved from a real device. A link to the app and all files is available at the end of the article.
Let’s get into it!
3.1 HTML/HTMX
The app is connected to a Radiacode detector, and I decided to show a connection status, radiation level, and a spectrum graph on the page. At the bottom, a spectrum collection time and an ML model prediction will be displayed.
An index.html file for this layout looks like this: