Working with measured data
This page is for reading what GRAND detectors record: GP13, GP80 and GRANDProto300 files. It covers what the files hold, how to read the traces, times and positions, how to convert counts to volts and how to tell the kind of trigger that recorded an event. The code runs when this page is built, on a GP13 sample that ships with the repository.
Note
A few points, marked (to confirm), come from the code and the examples rather than from a specification of the data; the last section lists them.
What a measured file holds
The DAQ writes binary files. The collaboration’s converter, gtot,
turns them into ROOT files in GRANDlib’s format; GRANDlib reads those but
does not run gtot itself (Running at scale). A converted file holds:
Tree |
Holds |
|---|---|
|
The traces in ADC counts, four channels per unit, with the trigger, GPS and housekeeping data of each unit |
|
The same traces converted to µV by |
|
What the run covers: site, units, sampling |
|
The firmware settings: thresholds, gains, channel mapping |
File names follow SITE_DATE_TIME_RUNn_MODE_...root, for example
GP80_20250711_191026_RUN10127_CD_20dB-GP65-Y2float-60DUs-CD-100000-3619.root.
Files whose name contains -10s- hold the 10-second triggers; the
production pipeline monitors the detector with them. The older GP13 files
are named GRAND.TEST-RAW.20230307174423.001.root.
Getting the data
The converted files are kept at CC-IN2P3, the computing center of CNRS/IN2P3
in Lyon, by site, year and month: for example
/sps/grand/data/gp80/GrandRoot/2025/07/. Reading them needs a CC-IN2P3
account with access to GRAND’s storage; your GRAND group leader can tell you
how to request one.
With an account, work on the CC-IN2P3 machines, or copy the files you need
with scp or rsync from cca.in2p3.fr. granddb, GRAND’s data
catalog, can find files by name and copy them for you; notebook 10
(Tutorial notebooks) shows how.
Without an account, use the GP13 sample in the repository, as this page does.
Opening a file
DataFile opens a file, a list of files or a wildcard;
each tree becomes an attribute. The sample holds 638 background traces
recorded by seven GP13 units at night in February 2024, cut to three
channels:
from pathlib import Path
import numpy as np
import grand
from grand.dataio import DataFile
SAMPLE = (Path(grand.__file__).parents[1] / "sim2root/Common/LongNoiseTraces"
/ "noice_traces_merged_gp13_2024_02_night_datafiles_410-415.root")
f = DataFile(str(SAMPLE))
tadc = f.tadc
print(tadc.get_number_of_entries(), "entries")
tadc.get_entry(0)
counts = np.asarray(tadc.trace_ch) # (units, channels, samples), ADC counts
print(counts.shape, "from unit", tadc.du_id)
638 entries
(1, 3, 2048) from unit [1010]
Each entry is one event; du_id lists the units that recorded it and the
first axis of trace_ch follows the same order.
Channels
A GP80 or GP300 unit records four channels. The analysis examples read
channels 1, 2 and 3 as the X (south-north), Y (east-west) and Z (vertical)
arms; channel 0 is the floating arm (to confirm for each site and
period). GP80 file names state which arm floats, as Y2float above.
The run’s TRunRawVoltage.adc_input_channels_ch records how the ADC
inputs were wired. The sample above was cut to three channels, X, Y and Z.
From counts to volts
The ADC maps ±0.9 V to ±8192 counts, so one count is about 110 µV:
microvolt = counts * 0.9e6 / 8192
print("noise RMS per arm [µV]:", microvolt[0].std(axis=-1).round())
noise RMS per arm [µV]: [1082. 1421. 2812.]
For files converted by gtot, TRawVoltage.trace_ch already holds the
µV and TRunRawVoltage.adc_conversion records the factor it used. The
reverse, µV to counts, is
convert_voltage_to_ADC().
Times
du_seconds is the GPS time of each unit’s trigger, converted to Unix
seconds; du_nanoseconds is the fraction of it. trigger_position is
the sample at which the unit triggered; at 500 MHz a sample lasts 2 ns:
from datetime import datetime, timezone
print(datetime.fromtimestamp(tadc.du_seconds[0], timezone.utc),
"+ %d ns" % tadc.du_nanoseconds[0])
2024-02-19 21:26:02+00:00 + 123 ns
To compare units within an event, subtract the smallest du_seconds
before adding the nanoseconds, as in step 2 of the Tutorial: one shower, start to finish:
floating-point seconds since 1970 lose the nanoseconds.
Positions
Each unit reports its GPS position with every event. In TADC the
latitude, longitude and altitude are 64-bit floating-point numbers stored in
integer fields: reinterpret the bits, do not convert the value. Latitude
and longitude are then in radians and altitude in meters (to confirm):
lat, lon, alt = np.array([tadc.gps_lat, tadc.gps_long, tadc.gps_alt],
dtype=np.uint64).view(np.float64)[:, 0]
print("latitude %.5f deg, longitude %.5f deg, altitude %.0f m"
% (np.degrees(lat), np.degrees(lon), alt))
latitude 40.97690 deg, longitude 93.95788 deg, altitude 1220 m
A single GPS fix is accurate to meters. For timing analyses use surveyed
positions: examples/analysis/ uses RTK positions of the GP80 units. To
put positions in the array frame, see Coordinates and conventions and Sites and detector layouts.
Which trigger recorded the event
Field |
Meaning |
|---|---|
|
|
|
Per unit: recorded by the 10-second trigger |
|
Per unit: recorded by the 20 Hz random trigger |
|
Per unit: the channel combination that fired the self-trigger |
|
Per unit: calibration and test pulses |
|
Use the per-unit patterns when they disagree with event_type: every
entry of the sample has event_type 0 but was recorded by the 10-second
trigger:
ten_second = 0
for entry in range(tadc.get_number_of_entries()):
tadc.get_entry(entry)
ten_second += all(tadc.trigger_pattern_10s)
print(ten_second, "of", tadc.get_number_of_entries(), "entries are 10-second triggers")
f.close()
638 of 638 entries are 10-second triggers
10-second and random triggers record the background: use them for noise
studies, not as shower candidates. Which event_type values each
firmware version writes is (to confirm).
From events to a reconstruction
EventList reads a whole file event by event,
with the units’ positions and traces together:
from grand.aoi import EventList
events = EventList("GP80_..._CD_...root", use_trawvoltage=True,
trawvoltage_channels=[1, 2, 3]) # X, Y, Z
event = events.get_event(event_number=12, run_number=10127)
for voltage in event.voltages:
print(voltage.t0, voltage.trace.x.max()) # trigger time, X arm in µV
Pass trawvoltage_channels: the default, [0, 1, 2], matches
simulated files, not the four-channel measured ones. EventList reads
voltages, so it does not open the GP13 sample above, which holds ADC counts
only.
examples/analysis/main_AOI.py continues from there to peak times,
amplitudes and the fits of grand.analysis, on the GP80 events listed
in examples/analysis/Flagged_events_July_October.txt. It needs the GP80
data from CC-IN2P3.
To confirm
These need an answer from the people who run the detectors. If you know one, please say so in a GitHub issue.
The channel-to-arm mapping for each site and period.
The units and storage of
gps_lat,gps_longandgps_altinTADCandTRawVoltage.The
event_typevalues each firmware version writes.The meaning of the
MODEfield of the file names (CD,MD,TR).
Where next
Data format reference for every field of
TADCandTRawVoltage.Producing simulations to add this kind of noise to simulations.
Notebooks 11 and 12 (Tutorial notebooks) for reconstruction and the event viewer on GP13 data.