objects round us will be barely radioactive. Americium in smoke detectors, radium in some classic watches, or uranium in classic glass; a full record will be lengthy. Largely, these objects are protected and can’t trigger a well being threat. Additionally it is attention-grabbing to establish them and examine the matter on the atomic degree. And we will do that utilizing a radiation detector. In the first part, I did an exploratory information evaluation of the gamma spectroscopy information. Within the second part, I created a machine studying mannequin for detecting radioactive isotopes. That is the final third half, and it’s time so as to add a created mannequin to the actual app!
On this story, I’ll take a look at two approaches:
- I’ll create a public Streamlit app that can be hosted at no cost on Streamlit Cloud (the app hyperlink is added to the top of the article).
- As a extra versatile and common resolution, I’ll create a Python HTMX-based app that may talk with actual {hardware} and make predictions in actual time.
In the identical means as within the earlier half, I’ll use a Radiacode scintillation detector to get the information (disclaimer: the system used on this take a look at was supplied by the producer; I don’t get any business revenue from their gross sales, and I didn’t get any editorial enter about all of the exams). Readers who don’t have a Radiacode {hardware} will have the ability to take a look at the app and the mannequin utilizing information accessible on Kaggle.
Let’s get began!
1. Isotopes Classification Mannequin
This mannequin was described within the previous part. It’s based mostly on XGBoost, and I educated the mannequin utilizing completely different radioactive samples. I used samples that may be legally bought, like classic uranium glass or outdated watches with radium dials made within the Fifties. As talked about earlier than, I additionally used a Radiacode scintillation detector, which permits me to get the gamma spectrum of the thing. Solely 10-20 years in the past, these kinds of detectors have been accessible solely in massive labs; in the present day, they are often bought for the value of a mid-range smartphone.
The mannequin comprises three parts:
- The XGBoost-based mannequin itself.
- An inventory of radioactive isotopes (like Lead-214 or Actinium-228), on which the mannequin was educated. The Radiacode scintillation detector returns 1024 spectrum values, and 23 of them have been used for the mannequin.
- A label encoder to transform record indexes into human-readable names.
Let’s wrap all this right into a single Python class:
from xgboost import XGBClassifier
from sklearn.preprocessing import LabelEncoder
class IsotopesClassificationModel:
""" Gamma Spectrum Classification Mannequin """
def __init__(self):
""" Load fashions """
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 """
options = 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 mannequin from file """
bst = XGBClassifier()
bst.load_model(filename)
return bst
@staticmethod
def _load_isotopes(filename: str) -> Record:
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 fashions. Mannequin information are saved in
'fashions/V1/' folder """
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
return parent_dir + f"/fashions/{IsotopesClassificationModel.VERSION}"
A Spectrum class comprises the spectrum information we get from a radiation detector:
@dataclass
class Spectrum:
""" Radiation spectrum information """
length: datetime.timedelta
a0: float
a1: float
a2: float
counts: record[int]
Right here, counts is a gamma spectrum, which is represented by a listing of 1024 channel values. Spectrum information will be exported utilizing the official Radiacode Android app or retrieved straight from a tool utilizing a radiacode Python library.
To load the spectrum into the mannequin, I created a SpectrumPreprocessing class:
class SpectrumPreprocessing:
""" Gamma Spectrum Preprocessing """
@staticmethod
def convert_to_features(spectrum: Spectrum, isotopes: Record) -> np.array:
""" Convert the spectrum to the record of options 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 """
Right here, I skip some code blocks that have been already printed within the previous part. Extracting options from the gamma spectrum was additionally defined there, and I extremely suggest studying that half first.
Now, let’s take a look at the mannequin! I took a Radiacode detector and picked up a gamma spectrum inside 10 minutes:
This Chinese language pendant was marketed as “ion-generated,” and it’s barely radioactive. Its gamma spectrum, collected within the official Radiacode Android app, appears like this:

After ready for ~10 minutes, I exported the spectrum into an XML file. Now, we will run the mannequin:
from spectrum import SpectrumPreprocessing
from ml_models import IsotopesClassificationModel
sp = SpectrumPreprocessing.load_from_file("spectrum.xml")
mannequin = IsotopesClassificationModel()
outcome = mannequin.predict(sp)
print(outcome)
#> Thorium
As we will see, the mannequin works effectively. We will examine the peaks with spectra of identified isotopes (for instance, here or here) and ensure that the spectrum belongs to thorium.
2. Streamlit
The mannequin works; nevertheless, we stay within the XXI century, and nearly no one will run the console app to get the outcomes. As a substitute, we will make the app accessible on-line, so all Radiacode customers will have the ability to run it.
There are a lot of Python frameworks for making browser-based apps, and Streamlit might be the most well-liked within the information science neighborhood. And what’s vital for us, a Streamlit Community Cloud platform permits everybody to publish their apps fully at no cost. To do that, let’s make the app first.
2.1 Streamlit App
A Streamlit framework is comparatively simple to make use of, no less than if a standard-looking app is sweet for us. Personally, I’m not a fan of this strategy. These frameworks disguise all low-level implementation particulars from customers. It’s easy to make a prototype, however the UI logic can be tightly coupled with a really area of interest framework and can’t be reused anyplace else. Doing every little thing non-standard, which isn’t supported by the framework, will be nearly unattainable or onerous to implement with out digging into tons of abstractions and pages of code. Nevertheless, in our case, the prototype is all we’d like.
Normally, a Streamlit code is easy, and we simply want to explain the logical hierarchy of our web page:
import streamlit as st
import logging
logger = logging.getLogger(__name__)
def is_xml_valid(xml_data: str) -> bool:
""" Test if the XML has legitimate measurement and information """
return len(xml_data) < 65535 and xml_data.startswith(" Non-compulsory[Spectrum]:
""" Load spectrum from the StringIO stream """
xml_data = stringio.learn()
if is_xml_valid(xml_data):
return SpectrumPreprocessing.load_from_xml(xml_data)
return None
def predominant():
""" Principal app """
st.set_page_config(page_title="Gamma Spectrum")
st.title("Radiacode Spectrum Detection")
st.textual content(
"Export the spectrum to XML utilizing the Radiacode app, and "
"add it to see the outcomes."
)
# File Add
uploaded_file = st.file_uploader(
"Select the XML file", sort="xml", key="uploader",
)
if uploaded_file isn't None:
stringio = StringIO(uploaded_file.getvalue().decode("utf-8"))
if sp := get_spectrum(stringio):
# Prediction
mannequin = IsotopesClassificationModel()
outcome = mannequin.predict(sp)
logger.data(f"Spectrum prediction: {outcome}")
# Present outcome
st.success(f"Prediction Outcome: {outcome}")
# Draw
fig = get_spectrum_barchart(sp)
st.pyplot(fig)
if __name__ == "__main__":
logger.setLevel(logging.INFO)
predominant()
As we will see, the complete app requires a minimal quantity of Python code. Streamlit will render all HTML for us, with title, file add, and outcomes. As a bonus, I can even show a spectrum utilizing Matplotlib:
def get_spectrum_barchart(sp: Spectrum) -> plt.Determine:
""" Get Matplotlib's barchart """
counts = SpectrumPreprocessing.get_counts(sp)
power = [
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(power, 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(power[0], power[-1])
ax.set_ylim(0, None)
ax.set_title("Gamma spectrum")
ax.set_xlabel("Vitality, keV")
ax.set_ylabel("Counts")
return fig
Now we will run the app domestically:
streamlit run st-app.py
After that, our app is absolutely operational and will be examined in a browser:

As talked about earlier than, I’m not a fan of very high-level frameworks and like to have a greater understanding of how issues work “below the hood.” Nevertheless, contemplating that I spent solely about 100 strains of code to make a totally useful internet app, I can’t complain – for prototyping, it really works effectively.
2.2 Streamlit Neighborhood Cloud
When the app is examined domestically, it’s time to make it public! A Streamlit Cloud is a free service, and clearly, it has a number of limitations:
- The app runs in a Docker-like container. Your GitHub account have to be linked to Streamlit. When the container begins, it pulls your code from GitHub and runs it.
- On the time of scripting this textual content, container sources are restricted to 2 cores and as much as 2,7 GB of RAM. It might be too constrained to run a 70B measurement LLM, however for a small XGBoost mannequin, it’s greater than sufficient.
- Streamlit doesn’t present any everlasting storage. After shutdown or restart, all logs and non permanent information can be misplaced (you should use API secrets and techniques and hook up with another cloud storage out of your Python code if wanted).
- After a interval of inactivity (about half-hour), the container can be stopped, and all non permanent information can even be misplaced. If somebody opens the app hyperlink, it should run once more.
As readers can guess, an inactive app prices Streamlit nearly nothing as a result of it shops solely a small configuration file. And it’s a good resolution for a free service – it permits us to publish the app with none prices and provides individuals a hyperlink to run it.
To publish the app in Streamlit, we have to carry out three easy steps.
First, we have to commit our Python app to GitHub. A necessities.txt file can be obligatory. Streamlit container makes use of it to put in required Python dependencies. In my case, it appears 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 will be modified utilizing a .streamlit/config.toml file. In my case, I restricted the uploaded file measurement to 1 MB as a result of all spectra information are smaller:
[server]
# Max measurement, in megabytes, for information uploaded with the file_uploader.
# Default: 200
maxUploadSize = 1
Second, we have to log in to share.streamlit.io utilizing a GitHub account and provides permission to entry the supply code.
Lastly, we will create a brand new Streamlit mission. Within the mission settings, we will additionally choose the specified URL and setting:

If every little thing was achieved accurately, we will see our app operating:

At this second, customers worldwide may entry our app! In my case, I chosen a gammaspectrumdetection title, and the app is offered utilizing this URL.
3. FastAPI + HTMX App
As readers can see, Streamlit is a pleasant resolution for a easy prototype. Nevertheless, within the case of the radiation detector, I want to see information coming from actual Radiacode {hardware}. This might be unattainable to do in Streamlit; this library simply was not designed for that. As a substitute, I’ll use a number of production-grade frameworks:
- An HTMX framework permits us to make a totally useful internet interface.
- FastAPI will run the server.
- The ML mannequin will course of the information retrieved in real-time from a radiation detector utilizing a Radiacode library.
As talked about earlier than, these readers who don’t have a Radiacode {hardware} will have the ability to replay the information utilizing uncooked log information, saved from an actual system. A hyperlink to the app and all information is offered on the finish of the article.
Let’s get into it!
3.1 HTML/HTMX
The app is linked to a Radiacode detector, and I made a decision to point out a connection standing, radiation degree, and a spectrum graph on the web page. On the backside, a spectrum assortment time and an ML mannequin prediction can be displayed.
An index.html file for this structure appears like this:
Gamma Spectrum & Monitoring
Assortment Time:
n/a
Prediction:
n/a

