CSV Input
You have already learned that fuse can read Geant4 root files and simulate the microphysics effects as well as the detector response. However, it is also possible to run fuse simulations from csv input files. This can be useful if you want to simulate a specific event topology or in cases where a full Geant4 simulation is not necessary. In this notebook, we will show you how to run fuse simulations from csv input files.
Imports and config preparation
[ ]:
import fuse
Microphysics Simulation
At the moment fuse has two options for csv input. The first option is to give a csv file as input to the microphysics simulation similar as you would give a root file as input. The second option is to give a csv file as input to the detector simulation. In this case, the microphysics simulation is skipped and the csv file is directly given to the detector simulation.
Building some instructions
First we need to generate some instructions. This notebook provides two examples: a mono-energetic gamma source and Kr83m.
[ ]:
import pandas as pd
import numpy as np
def monoenergetic_source(n, energy): # number of photons, energy of photons
df = pd.DataFrame()
# radius and depth are in mm for microphysics instructions
r = np.sqrt(np.random.uniform(0, 435600, n)) # 435600 = radius of TPC in mm^2
theta = np.random.uniform(-np.pi, np.pi, n)
df["xp"] = r * np.cos(theta)
df["yp"] = r * np.sin(theta)
df["zp"] = np.random.uniform(-1500, 0, n) # 0 is the position of the gate electrode
df["xp_pri"] = df["xp"]
df["yp_pri"] = df["yp"]
df["zp_pri"] = df["zp"]
df["ed"] = np.array([energy] * n)
# if the times are all zero they are distributed according to source_rate by fuse
# custom interaction times in nanoseconds can also be used e.g np.arange(n)*1e9
df["time"] = np.zeros(n)
df["eventid"] = np.arange(n)
df["type"] = np.repeat("gamma", n)
df["trackid"] = np.ones(n)
df["parentid"] = np.zeros(n, dtype=np.int32)
df["creaproc"] = np.repeat("None", n)
df["parenttype"] = np.repeat("None", n)
df["edproc"] = np.repeat("None", n)
return df
def Kr83m_example(n):
half_life = 156.94e-9 # Kr intermediate state half-life in ns
decay_energies = [32.2, 9.4] # Decay energies in kev
df = pd.DataFrame()
r = np.sqrt(np.random.uniform(0, 435600, n))
t = np.random.uniform(-np.pi, np.pi, n)
df["xp"] = np.repeat(r * np.cos(t), 2)
df["yp"] = np.repeat(r * np.sin(t), 2)
df["zp"] = np.repeat(np.random.uniform(-1500, 0, n), 2)
df["xp_pri"] = df["xp"]
df["yp_pri"] = df["yp"]
df["zp_pri"] = df["zp"]
df["ed"] = np.tile(decay_energies, n)
dt = np.random.exponential(half_life / np.log(2), n)
df["time"] = np.array(list(zip(np.zeros(n), dt))).flatten() * 1e9 # in ns
df["eventid"] = np.repeat(np.arange(n), 2)
df["parenttype"] = np.tile(["Kr83[41.557]", "Kr83[9.405]"], n)
# Not used:
# a) since Kr83m is classified using only the parenttype
# b) trackid, parentid are not used right now.
# Please keep in mind that other "simulations" may require properly set
# edproc, type and creaproc. Future epix updates using e.g. track reconstructions
# may also need proper track- and parent-ids.
df["trackid"] = np.tile([1, 2], n)
df["parentid"] = np.zeros(2 * n, dtype=np.int32)
df["creaproc"] = np.repeat("None", 2 * n)
df["edproc"] = np.repeat("None", 2 * n)
df["type"] = np.repeat("None", 2 * n)
return df
We will use the mono-energetic souce here, but you can give Kr83m a try if you like. To pass the instructions to fuse we need to save them to a csv file first. The following code will generate 1000 gamma events with with an energy of 200 keV randomly distributed in the TPC.
[ ]:
microphysics_instructions = monoenergetic_source(1000, 200)
microphysics_instructions.to_csv("monoenergetic_200keV.csv")
Running the simulation
To use the csv file as input we just need to specify the correct path and file_name in the config. The ChunkInput plugin can handle both csv and root files as input.
When dealing with csv input files we should reduce n_interactions_per_chunk so that our chunks will not get too large later on.
[ ]:
st = fuse.context.xenonnt_fuse_full_chain_simulation(
output_folder="./fuse_data",
simulation_config="sr0_dev",
)
st.set_config(
{
"path": ".",
"file_name": "monoenergetic_200keV.csv",
"n_interactions_per_chunk": 250,
}
)
run_number = "00000"
[ ]:
st.make(run_number, "microphysics_summary")
[ ]:
microphysics_summary = st.get_df(run_number, "microphysics_summary")
Now that we have the data loaded we can plot the number of photons per interaction against the number of electrons.
[ ]:
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde
fig = plt.figure()
ax = fig.gca()
ax.set_xlim(11000, 14000)
ax.set_ylim(1000, 4000)
xdata = microphysics_summary["photons"].values
ydata = microphysics_summary["electrons"].values
xy = np.vstack([xdata, ydata])
z = gaussian_kde(xy)(xy)
ax.scatter(xdata, ydata, c=z, s=7.5, cmap="viridis")
ax.set_xlabel("Number of photons]")
ax.set_ylabel("Number of electrons")
plt.show()
Detector Simulation
As already mentioned we can also run the detector physics simulation from a csv file. In this case, the microphysics simulation is skipped. To do so, we need to register a new plugin that will do the csv file handling and chunking for us but first lets create some simulation instructions.
Building the instructions
To run the detector simulation we need to specify the following information for each event: - x: the x position of the interaction - y: the y position of the interaction - z: the z position of the interaction - t: the time of the interaction - photons: the number of photons produced in the interaction - electrons: the number of electrons produced in the interaction - e_field: the electric field at the interaction position - nest_id: the nest id of the interaction
- eventid: the event id of the interaction - ed: the energy deposited in the interaction - cluster_id: The number of the specific cluster. Needs to start from 1 and increase by 1 for each new cluster.
Not all of the informations are necessary to run the detector simulation. I will set them to zero in the example below.
[ ]:
import pandas as pd
def build_random_instructions(n):
df = pd.DataFrame()
r = np.sqrt(np.random.uniform(0, 2500, n))
t = np.random.uniform(-np.pi, np.pi, n)
df["x"] = r * np.cos(t)
df["y"] = r * np.sin(t)
df["z"] = np.random.uniform(-150, -1, n)
df["photons"] = np.random.uniform(100, 5000, n)
df["electrons"] = np.random.uniform(100, 5000, n)
df["excitons"] = np.zeros(n)
df["e_field"] = np.array([23] * n)
df["nestid"] = np.array([7] * n)
df["ed"] = np.zeros(n)
# just set the time with respect to the start of the event
# The events will be distributed in time by fuse
df["t"] = np.zeros(n)
df["eventid"] = np.arange(n)
df["cluster_id"] = np.arange(n) + 1
return df
[ ]:
detectorphysics_instructions = build_random_instructions(100)
detectorphysics_instructions.to_csv("random_detectorphysics_instructions.csv", index=False)
Run the simulation
Now that the simulation instructions are prepared we can set up our simulation context. We will use the xenonnt_fuse_full_chain_simulation and register the ChunkCsvInput plugin.
[ ]:
st = fuse.context.xenonnt_fuse_full_chain_simulation(
output_folder="./fuse_data",
simulation_config="sr0_dev",
)
st.register(fuse.detector_physics.ChunkCsvInput)
st.set_config(
{
"input_file": "./random_detectorphysics_instructions.csv",
"n_interactions_per_chunk": 50,
}
)
run_number = "00042"
[ ]:
st.make(run_number, "raw_records", progress_bar=True)
After we finished simulating raw_records we can process the data up to event_info.
[ ]:
st.make(run_number, "event_info", progress_bar=True)
[ ]:
event_info_data = st.get_df(run_number, "event_info")
[ ]:
event_info_data.head()