Simulation

The instrument response, from electric field to ADC counts: grand.sim.

grand.sim.efield2voltage

Master module for the detector unit simulation GRAND

grand.sim.detector.antenna_model

grand.sim.detector.process_ant

Processing effective length of antenna

grand.sim.detector.rf_chain

grand.sim.noise.galaxy

Simulation of Galactic radio noise.

grand.sim.detector.adc

Master module for the ADC in GRAND

grand.sim.detector.trigger

Offline, DAQ-style first-level (T1) trigger on ADC traces.

grand.sim.shower.gen_shower

grand.sim.shower.pdg

grand.sim.efield2voltage

Master module for the detector unit simulation GRAND

grand.sim.efield2voltage.get_fastest_size_fft(sig_size, f_samp_mhz, padding_factor=1)[source]

Returns an FFT-friendly transform length and its frequency axis.

Real FFTs are fastest at lengths whose prime factorisation is small, so rather than transforming sig_size samples directly this rounds up to the next such length via scipy.fft.next_fast_len() and returns the matching one-sided frequency axis for scipy.fft.rfft(). Padding to a longer length also improves the frequency resolution of the result, which is what padding_factor is for.

Parameters:
  • sig_size (int) – Length of the time traces, in samples.

  • f_samp_mhz (ndarray) – Sampling frequency in MHz, e.g. 2000 MHz for a 0.5 ns bin. An array is expected and only its first element is used: the routine assumes every trace in the event shares one time binning.

  • padding_factor (float, optional) – Factor by which to stretch the traces with zeros before transforming. Must be at least 1; the default of 1 pads only as far as the next fast length.

Returns:

  • fast_size (int) – The transform length actually to use, >= padding_factor*sig_size.

  • freqs_mhz (ndarray) – Frequency axis in MHz, of length fast_size//2 + 1, matching the output of scipy.fft.rfft() at that length.

Raises:

ValueError – If padding_factor is less than 1.

Examples

import numpy as np
from grand.sim.efield2voltage import get_fastest_size_fft

n, freqs = get_fastest_size_fft(1000, np.array([2000.0]))
print("padded length:", n)                 # 1000 is already 2^3 x 5^3
print("frequency bins:", freqs.size, "| Nyquist:", freqs[-1], "MHz")
padded length: 1000
frequency bins: 501 | Nyquist: 1000.0 MHz

Doubling the padding halves the bin spacing:

n2, freqs2 = get_fastest_size_fft(1000, np.array([2000.0]),
                                  padding_factor=2)
print("padded length:", n2)
print("bin spacing: %.3f -> %.3f MHz" % (freqs[1], freqs2[1]))
padded length: 2000
bin spacing: 2.000 -> 1.000 MHz

Notes

That only f_samp_mhz[0] is read is a real limitation, not an oversight in this docstring: an event whose detection units record at different sampling rates would silently get the first unit’s frequency axis applied to all of them. The ToDo in the body marks the same point.

grand.sim.efield2voltage.PARAM_DEFAULTS = {'add_jitter_ns': 0, 'add_noise': True, 'add_rf_chain': True, 'add_rf_chain_gaa': False, 'add_rf_chain_nut': False, 'calibration_smearing_sigma': 0, 'extend_to_us': 0, 'lst': 18.0, 'resample_to_mhz': 0}

The processing switches of Efield2Voltage.params, with their defaults.

class grand.sim.efield2voltage.Efield2Voltage(d_input, f_output=None, output_directory=None, seed=None, padding_factor=1.0, du_type='GP300', efield_level=None)[source]

Bases: object

Computes the voltage at each detection unit from simulated electric fields.

Reads the folder sim2root.py wrote (efield, run and shower trees), computes the voltage of every event or of one, and writes a voltage file. See compute_voltage() for the steps.

params

The processing switches, with these keys and defaults:

add_noise (True)

Add Galactic noise.

lst (18.0)

Local sidereal time for the Galactic noise, in hours, 0 to 24.

add_rf_chain (True)

Apply the GP300 RF chain.

add_rf_chain_nut (False), add_rf_chain_gaa (False)

Apply the RF chain up to the LNA output, or the G@Auger chain.

resample_to_mhz (0)

Resample to this rate; 0 keeps the input rate. Other rates stay in memory: save_voltage() refuses them.

extend_to_us (0)

Extend the traces to this duration, in µs; 0 keeps their length.

calibration_smearing_sigma (0)

Relative Gaussian smearing of each unit’s amplitude calibration.

add_jitter_ns (0)

Gaussian jitter of the trigger times, in ns.

Unknown keys and invalid values are refused when the computation starts.

Type:

dict

See also

grand.sim.detector.adc.ADC

Digitizes the voltages this class writes.

grand.sim.detector.rf_chain.RFChain

The RF chain applied to the open-circuit voltage.

grand.sim.noise.galaxy.galactic_noise

The Galactic noise it adds.

grand.sim.detector.antenna_model.AntennaModel

The antenna response it uses.

Examples

Simulate the voltages of the shower that ships with the repository, with the Galactic noise of 6 h local sidereal time:

import tempfile
from pathlib import Path

import numpy as np
import grand
from grand import Efield2Voltage
from grand.dataio import TVoltage

sample = (Path(grand.__file__).parents[1]
          / "sim2root/Common/sim_Xiaodushan_20221026_000000_RUN1_CD_ZHAireS_0000")
out = Path(tempfile.mkdtemp())

sim = Efield2Voltage(str(sample), "voltage.root", output_directory=str(out),
                     seed=1, efield_level=0)
sim.params["lst"] = 6.0
sim.compute_voltage()

with TVoltage(str(out / "voltage.root")) as tvoltage:
    tvoltage.get_entry(0)
    traces = np.asarray(tvoltage.trace)
print("%d units, traces of shape %s, in µV" % (traces.shape[0], traces.shape[1:]))
44 units, traces of shape (3, 8192), in µV
get_event(event_idx=None, event_number=None, run_number=None)[source]

Loads the data of one event, selected by index or by number.

Call this for every new event: it replaces the traces, positions and shower parameters the other methods work from.

Parameters:
  • event_idx (int, optional) – Index of the event in events_list, from range(len(event_list)).

  • event_number (int, optional) – Event number. Must be given together with run_number; the pair is unique.

  • run_number (int, optional) – Run number. Must be given together with event_number.

Raises:

Exception – If neither event_idx nor the (event_number, run_number) pair identifies an event in the input.

Notes

Either event_idx, or both event_number and run_number, must be given.

get_leff(du_idx)[source]

Builds the antenna response for one detection unit.

The effective length depends on the direction of the incoming signal in the antenna frame, so it is constructed per unit from that unit’s position and the shower direction.

Parameters:

du_idx (int) – Index of the detection unit in the event arrays.

Returns:

The response object for that unit’s three arms.

Return type:

AntennaProcessing

get_time_samples()[source]

Define time sample in ns for the duration of the trace t_samples.shape = (nb_du, self.sig_size) t_start_ns.shape = (nb_du,)

Returns:

Time axis of each unit, in nanoseconds.

Return type:

ndarray, shape (n_du, n_samples)

add(addend)[source]

Adds addend to the output voltage spectrum, in place.

Provided so that a caller can inject their own noise instead of, or in addition to, the built-in Galactic model.

Parameters:

addend (ndarray) – A frequency-domain quantity of exactly the shape of vout_f, (n_du, 3, n_freqs) – it is not broadcast; another shape raises ValueError. It must already be evaluated on self.freqs_mhz: nothing here interpolates it.

multiply(multiplier)[source]

Multiplies the output voltage spectrum by multiplier, in place.

Provided so that a caller can apply their own transfer function instead of the built-in RF chain.

Parameters:

multiplier (ndarray) – A frequency-domain quantity that broadcasts against vout_f, whose shape is (n_du, 3, n_freqs), already evaluated on self.freqs_mhz.

final_resample()[source]

Brings the voltage back to the time domain, resampled and cut as requested.

Called last: resamples to params["resample_to_mhz"] if set (by Fourier interpolation), otherwise inverts the spectrum if noise or an RF chain changed it, then truncates the traces to the target length. The result is in self.vout.

compute_voc_du(du_idx)[source]

Computes the open-circuit voltage for one detection unit.

This is the base of every voltage computation: the others call it, directly or through compute_voc_event().

Parameters:

du_idx (int) – Index of the detection unit in the trace arrays.

Notes

Stores the result on the instance rather than returning it: voc in the time domain and voc_f in the frequency domain.

compute_voc_event(event_idx=None, event_number=None, run_number=None)[source]

Computes the open-circuit voltage for every unit in one event.

Fills voc with shape (n_du, 3, n_samples) and voc_f with shape (n_du, 3, n_freqs).

Parameters:
  • event_idx (int, optional) – Index of the event in events_list, from range(len(event_list)).

  • event_number (int, optional) – Event number. Must be given together with run_number; the pair is unique.

  • run_number (int, optional) – Run number. Must be given together with event_number.

Raises:

Exception – If neither event_idx nor the (event_number, run_number) pair identifies an event in the input.

Notes

Either event_idx, or both event_number and run_number, must be given.

compute_voltage_du(du_idx)[source]

Computes the output voltage for one detection unit.

Applies, in order:

  1. the open-circuit voltage from the antenna response,

  2. Galactic noise, if params["add_noise"],

  3. the GP300 RF chain, if params["add_rf_chain"],

  4. the RF chain up to the LNA output, if params["add_rf_chain_nut"],

  5. the G@Auger RF chain, if params["add_rf_chain_gaa"].

Parameters:

du_idx (int) – Index of the detection unit in the trace arrays.

Notes

Which stages run is taken from self.params, not from arguments.

compute_voltage_event(event_idx=None, event_number=None, run_number=None)[source]

Computes the output voltage for every unit in one event.

Equivalent to calling compute_voltage_du() for each unit in turn, but vectorised over units and therefore much faster.

Parameters:
  • event_idx (int, optional) – Index of the event in events_list, from range(len(event_list)).

  • event_number (int, optional) – Event number. Must be given together with run_number; the pair is unique.

  • run_number (int, optional) – Run number. Must be given together with event_number.

Raises:

Exception – If neither event_idx nor the (event_number, run_number) pair identifies an event in the input.

Notes

Either event_idx, or both event_number and run_number, must be given.

compute_voltage(event_idx=None, du_idx=None, event_number=None, run_number=None, append_file=True)[source]

Computes voltages for any or all events, and saves them.

The primary entry point. With no arguments it processes every event in the input file.

Parameters:
  • event_idx (int, list or ndarray, optional) – Index or indices of events to process. None processes all.

  • du_idx (int, list or ndarray, optional) – Detection units to process. None processes all of them. May be used for a single event only.

  • event_number (int, list or ndarray, optional) – Event number or numbers, given with run_number.

  • run_number (int, list or ndarray, optional) – Run number or numbers, given with event_number.

  • append_file (bool, optional) – Append to the output file rather than replacing it.

Notes

Give either event_idx, or both event_number and run_number, or none of the three. When lists are given, event_number and run_number must be the same length.

The result is written to self.f_output as a side effect; the method returns nothing.

save_voltage(append_file=True)[source]

Writes the computed voltages to the output file.

Parameters:

append_file (bool, optional) – Append to an existing file instead of replacing it.

Notes

The destination is self.f_output, fixed when the object was constructed.

grand.sim.detector.antenna_model

class grand.sim.detector.antenna_model.DataTable(frequency: Number | ndarray, theta: Number | ndarray, phi: Number | ndarray, leff_theta: Number | ndarray = None, phase_theta: Number | ndarray = None, leff_phi: Number | ndarray = None, phase_phi: Number | ndarray = None, leff_phi_reim: Number | ndarray = None, leff_theta_reim: Number | ndarray = None)[source]

Bases: object

The tabulated antenna response, on a grid of frequency, azimuth and zenith.

Parameters:
  • frequency (Union[Number, np.ndarray]) – Frequencies of the table, in Hz

  • theta (Union[Number, np.ndarray]) – Zenith angles of the table, in degrees, in the antenna frame

  • phi (Union[Number, np.ndarray]) – Azimuths of the table, in degrees, in the antenna frame

  • leff_theta (Union[Number, np.ndarray]) – Modulus of the effective length along e_theta, shape (n_freq, n_phi, n_theta), in m

  • phase_theta (Union[Number, np.ndarray]) – Phase of the effective length along e_theta, in degrees

  • leff_phi (Union[Number, np.ndarray]) – Modulus of the effective length along e_phi, shape (n_freq, n_phi, n_theta), in m

  • phase_phi (Union[Number, np.ndarray]) – Phase of the effective length along e_phi, in degrees

  • leff_phi_reim (Union[Number, np.ndarray]) – Complex effective length along e_phi (real and imaginary parts), in m

  • leff_theta_reim (Union[Number, np.ndarray]) – Complex effective length along e_theta (real and imaginary parts), in m

frequency: Number | ndarray

Frequencies of the table, in Hz

theta: Number | ndarray

Zenith angles of the table, in degrees, in the antenna frame

phi: Number | ndarray

Azimuths of the table, in degrees, in the antenna frame

leff_theta: Number | ndarray = None

Modulus of the effective length along e_theta, shape (n_freq, n_phi, n_theta), in m

phase_theta: Number | ndarray = None

Phase of the effective length along e_theta, in degrees

leff_phi: Number | ndarray = None

Modulus of the effective length along e_phi, shape (n_freq, n_phi, n_theta), in m

phase_phi: Number | ndarray = None

Phase of the effective length along e_phi, in degrees

leff_phi_reim: Number | ndarray = None

Complex effective length along e_phi (real and imaginary parts), in m

leff_theta_reim: Number | ndarray = None

Complex effective length along e_theta (real and imaginary parts), in m

grand.sim.detector.antenna_model.tabulated_antenna_model(filename)[source]

Returns the antenna response read from a tabulated file.

Parameters:

filename (str) – Path to the tabulated response.

Returns:

The response, on its native grid.

Return type:

DataTable

class grand.sim.detector.antenna_model.AntennaModel(du_type='GP300')[source]

Bases: object

The effective length of a GRAND antenna, for the three arms.

Loads the tabulated response and provides it on whatever frequency and direction grid the caller needs.

See also

grand.sim.detector.process_ant.AntennaProcessing

Interpolates this response for a direction.

grand.sim.efield2voltage.Efield2Voltage

Uses it to compute the open-circuit voltage.

Examples

The tables of one arm. The frequency axis is in Hz, unlike the rest of the simulation, which works in MHz:

import numpy as np
from grand import AntennaModel

model = AntennaModel()                       # 'GP300', the HFSS simulation
sn = model.leff_sn                           # the south-north arm
print("frequencies from %.0f to %.0f MHz" % (sn.frequency[0] / 1e6, sn.frequency[-1] / 1e6))
print("table shape (frequency, azimuth, zenith):", np.shape(sn.leff_theta_reim))
frequencies from 30 to 250 MHz
table shape (frequency, azimuth, zenith): (221, 361, 91)
plot_effective_length()[source]

Plots the effective length against frequency, for inspection.

grand.sim.detector.process_ant

Processing effective length of antenna

exception grand.sim.detector.process_ant.MissingFrameError[source]

Bases: ValueError

Error class

class grand.sim.detector.process_ant.PreComputeInterpol(idx_first: Any = None, idx_lastp1: Any = None, idx_itp: Any = None, c_inf: Any = None, c_sup: Any = None)[source]

Bases: object

Precompute linear interpolation of frequency of Leff

Parameters:
  • idx_first (Any) – index of freq in first in band 30-250MHz

  • idx_lastp1 (Any) – index of freq in last plus one in band 30-250MHz

  • idx_itp (Any) – array of index where f_out are in f_in for interpolation between idx_itp[i], idx_itp[i]+1 with c_inf and c_sup coefficient

  • c_inf (Any) – array of coefficient inf

  • c_sup (Any) – array of coefficient sup c_inf + c_sup = 1

idx_first: Any = None
idx_lastp1: Any = None
idx_itp: Any = None
c_inf: Any = None
c_sup: Any = None
init_linear_interpol(freq_in_mhz, freq_out_mhz)[source]

Precompute coefficient of linear interpolation for freq_out_mhz with reference defined at freq_in_mhz

Parameters:
  • freq_in_mhz (ndarray) – Frequency axis the response is tabulated on, in MHz.

  • freq_out_mhz (ndarray) – Frequency axis wanted, in MHz.

get_linear_interpol(a_val)[source]

Return f(freq_out_mhz) by linear interpolation of f defined by f(freq_in_mhz) = a_val

Parameters:

a_val (ndarray) – Values on the tabulated axis.

Returns:

The same values on the output axis.

Return type:

ndarray

class grand.sim.detector.process_ant.AntennaProcessing(model_leff: Any, pos: LTP | GRANDCS)[source]

Bases: object

Processing effective length of antenna

Parameters:
  • model_leff (Any) – The antenna response of one arm, as tabulated_antenna_model returns it

  • pos (Union[LTP, GRANDCS]) – Position of the antenna, in m

  • pre_cpt (ClassVar[PreComputeInterpol])

model_leff: Any

The antenna response of one arm, as tabulated_antenna_model returns it

pos: LTP | GRANDCS

Position of the antenna, in m

pre_cpt: ClassVar[PreComputeInterpol]
classmethod init_interpolation(freq_sampling_mhz, freq_out_mhz)[source]

Precomputes the interpolation from the tabulated grid onto an output axis.

Doing this once and reusing it is what keeps the per-unit voltage computation cheap.

Parameters:
  • freq_sampling_mhz (ndarray) – Frequency axis the response is tabulated on, in MHz.

  • freq_out_mhz (ndarray) – Frequency axis wanted, in MHz.

set_out_freq_mhz(a_freq)[source]

! typically the return of scipy.fft.rfftfreq/1e6

Parameters:

a_freq (ndarray) – Output frequency axis, in MHz.

effective_length(xmax: LTP, efield: ElectricField, frame: LTP | GRANDCS) → CartesianRepresentation[source]

Calculation of FFT length effective of antenna for a given Xmax position. Linear interpolation of template of effective length in Fourier space by tripler (theta, phi, frequency) in direction of the efield source at antenna position

Parameters:
Returns:

Effective length per antenna arm, in the frequency domain.

Return type:

ndarray, shape (3, n_freq)

compute_voltage(xmax: LTP, efield: ElectricField, frame: LTP | GRANDCS | None = None) → Voltage[source]
Parameters:
Returns:

Open-circuit voltage at the three arms. Qualified because grand.aoi.timetrace.Voltage exists too and a bare name is ambiguous.

Return type:

Voltage

Raises:

MissingFrameError – If the antenna position or the shower frame is absent or not finite.

grand.sim.detector.rf_chain

grand.sim.detector.rf_chain.read_config(xml_file)[source]

Reads the XML configuration file and returns component settings.

Parameters:

xml_file (str) – Configuration file listing the chain components.

Returns:

The components and their settings.

Return type:

dict

grand.sim.detector.rf_chain.xml_file = PosixPath('/home/runner/work/grand/grand/grand/sim/detector/rf_chain_config.xml')

it was parsed at import, so a broken file broke import grand.sim

Type:

The configuration file, read on first use (#255)

grand.sim.detector.rf_chain.get_axis_filename(component_name, axis)[source]

Returns the correct filename for a given component and axis.

Parameters:
  • component_name (str) – Chain component.

  • axis (int or str) – Antenna arm: 0, 1, 2 or “X”, “Y”, “Z” (also “0”, “1”, “2”).

Returns:

Path to that component measurements for that arm.

Return type:

str

Raises:
  • KeyError – If the component is not in the configuration file.

  • ValueError – If the component is disabled there, or the axis is not one of the above.

  • FileNotFoundError – If the configuration gives no file for the component.

grand.sim.detector.rf_chain.interp(x, y, z)[source]

Returns z interpolated onto x from samples at y.

A thin wrapper over numpy.interp(), kept so that the interpolation used across this module can be changed in one place.

Parameters:
  • x (ndarray) – Positions to interpolate onto.

  • y (ndarray)

  • positions. (Sample)

  • z (ndarray)

  • values. (Sample)

Returns:

  • ndarray

  • Interpolated values, of the shape of x.

grand.sim.detector.rf_chain.interpol_at_new_x(a_x, a_y, new_x)[source]

Returns a_y interpolated onto new_x, zero outside the input range.

Parameters:
  • a_x (ndarray, shape (n,)) – Sample positions, in increasing order.

  • a_y (ndarray, shape (n,)) – Values at a_x.

  • new_x (ndarray, shape (m,)) – Positions to interpolate onto.

Returns:

Interpolated values. Cubic within the range of a_x, and zero outside it rather than extrapolated – the S-parameter tables are measured over 30-250 MHz and have no meaning beyond it.

Return type:

ndarray, shape (m,)

grand.sim.detector.rf_chain.db2reim(dB, phase)[source]

Returns the real and imaginary parts of a quantity given in decibels.

A magnitude in decibels and a phase describe a complex number in polar form. This converts that pair to Cartesian form, using the voltage convention \(|z| = 10^{dB/20}\) rather than the power convention \(10^{dB/10}\), because the S-parameters this module reads are measured as voltage ratios by a vector network analyzer.

Parameters:
  • dB (array_like) – Magnitude in decibels.

  • phase (array_like) – Phase in radians. Note the unit: the tabulated S-parameter files store degrees, so callers convert with numpy.deg2rad() before calling this.

Returns:

  • re (ndarray) – Real part.

  • im (ndarray) – Imaginary part.

Examples

A 0 dB magnitude is unit modulus, so a quarter-turn of phase lands on the imaginary axis:

import numpy as np
from grand.sim.detector.rf_chain import db2reim

re, im = db2reim(np.array([0.0, -20.0]), np.array([0.0, np.pi/2]))
print("re =", np.round(re, 6))
print("im =", np.round(im, 6))
re = [1. 0.]
im = [0.  0.1]

The second entry shows the voltage convention: -20 dB is a factor of ten in amplitude, not a factor of a hundred.

grand.sim.detector.rf_chain.s2abcd(s11, s21, s12, s22)[source]

Returns the normalized ABCD matrix of a two-port from its S-parameters.

Scattering parameters are what a vector network analyzer measures, but they do not cascade: the S-matrix of two networks in series is not the product of their S-matrices. The ABCD (transmission) representation does cascade, which is the whole reason for this conversion — it is what lets RFChain obtain the response of the complete chain by multiplying the matrices of the LNA, the baluns, the cable and the VGA-plus-filter in order.

The normalized form returned here assumes equal reference impedances at both ports, which holds for the 50 \(\Omega\) measurements this module reads.

Parameters:
  • s11 (array_like) – The four complex scattering parameters, each of shape (n_freq,). Obtain them from tabulated magnitude and phase with db2reim().

  • s21 (array_like) – The four complex scattering parameters, each of shape (n_freq,). Obtain them from tabulated magnitude and phase with db2reim().

  • s12 (array_like) – The four complex scattering parameters, each of shape (n_freq,). Obtain them from tabulated magnitude and phase with db2reim().

  • s22 (array_like) – The four complex scattering parameters, each of shape (n_freq,). Obtain them from tabulated magnitude and phase with db2reim().

Returns:

The ABCD matrix, shape (2, 2, n_freq), whose entries are

\[ \begin{align}\begin{aligned}A = \frac{(1+S_{11})(1-S_{22}) + S_{12}S_{21}}{2S_{21}},\quad B = \frac{(1+S_{11})(1+S_{22}) - S_{12}S_{21}}{2S_{21}},\\C = \frac{(1-S_{11})(1-S_{22}) - S_{12}S_{21}}{2S_{21}},\quad D = \frac{(1-S_{11})(1+S_{22}) + S_{12}S_{21}}{2S_{21}}.\end{aligned}\end{align} \]

Return type:

ndarray

Notes

A matched, lossless through-line has \(S_{11}=S_{22}=0\) and \(S_{12}=S_{21}=1\), for which the ABCD matrix is the identity — the check in the example below, and a useful invariant for any test of this function.

Examples

import numpy as np
from grand.sim.detector.rf_chain import s2abcd

one  = np.ones(3, dtype=complex)
zero = np.zeros(3, dtype=complex)
abcd = s2abcd(zero, one, one, zero)          # ideal through-line

print("shape:", abcd.shape)
print("identity at every frequency:",
      np.allclose(abcd, np.eye(2)[:, :, None]))
shape: (2, 2, 3)
identity at every frequency: True

See also

matmul

cascades two ABCD matrices.

RFChain.compute_for_freqs

builds the full chain from its stages.

References

Section 8.3 of arXiv:2408.10926, and the Xidian University RF-chain note cited in the module docstring.

grand.sim.detector.rf_chain.matmul(A, B)[source]

Returns the product of two stacked 2x2 matrices.

Multiplies elementwise over the trailing axes, which is what cascades two stages of the RF chain: an ABCD matrix per port and per frequency.

Parameters:
  • A (ndarray, shape (2, 2, n_ports, n_freqs)) – The matrices to multiply.

  • B (ndarray, shape (2, 2, n_ports, n_freqs)) – The matrices to multiply.

Returns:

The product, computed as

\[\begin{split}AB = \begin{bmatrix} A_{11}B_{11} + A_{12}B_{21} & A_{11}B_{12} + A_{12}B_{22} \\ A_{21}B_{11} + A_{22}B_{21} & A_{21}B_{12} + A_{22}B_{22} \end{bmatrix}\end{split}\]

Return type:

ndarray, shape (2, 2, n_ports, n_freqs)

See also

s2abcd

produces the matrices this cascades.

Examples

Cascading a stage with the identity leaves it unchanged, which is the property that makes the ABCD representation worth converting to.

import numpy as np
from grand.sim.detector.rf_chain import matmul

n = 4
identity = np.zeros((2, 2, n), dtype=complex)
identity[0, 0] = identity[1, 1] = 1.0

rng = np.random.default_rng(0)
stage = rng.normal(size=(2, 2, n)) + 1j * rng.normal(size=(2, 2, n))

print("cascading with the identity is a no-op:",
      np.allclose(matmul(stage, identity), stage))
cascading with the identity is a no-op: True
class grand.sim.detector.rf_chain.GenericProcessingDU[source]

Bases: object

Define common attribut for frequencies for all DU effects processing

set_out_freq_mhz(freqs_mhz)[source]

Define frequencies

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Frequency axis the stage’s response is wanted on, in MHz.

release_arrays()[source]

Frees the arrays computed for the current frequencies.

Every array whose last axis runs over the frequencies is dropped, in this stage and in the stages it holds; the measured input tables are kept. A chain over a million-sample trace holds about 1 GB of them, while a caller such as grand.sim.efield2voltage.Efield2Voltage needs only the transfer function. Call compute_for_freqs() again before using the stage.

class grand.sim.detector.rf_chain.MatchingNetwork[source]

Bases: GenericProcessingDU

The impedance matching network between antenna and LNA.

compute_for_freqs(freqs_mhz)[source]

Computes this stage’s response on the given frequency axis.

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz. The tabulated S-parameters are interpolated onto it, and are zero outside the 30-250 MHz band they were measured over.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.gaa_frontend0db[source]

Bases: GenericProcessingDU

The GAA front end at 0 dB gain.

compute_for_freqs(freqs_mhz)[source]

Computes this stage’s response on the given frequency axis.

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz. The tabulated S-parameters are interpolated onto it, and are zero outside the 30-250 MHz band they were measured over.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.LowNoiseAmplifier[source]

Bases: GenericProcessingDU

Class goals:
  • Perform the LNA filter on signal for each antenna

  • read only once LNA data files

  • pre_compute interpolation

compute_for_freqs(freqs_mhz)[source]

compute s-parameters of LNA

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.BalunAfterLNA[source]

Bases: GenericProcessingDU

Class goals:
  • deals with Balun after LNA (inside Nut).

  • Note that balun is placed after LNA in version 1. The same type of balun is placed before matching-network and LNA in version 2.

  • Balun is used in X and Y ports only. No Balun in Z port.

  • Balun without matching network is used in version 1.

compute_for_freqs(freqs_mhz)[source]

Compute ABCD_matrix for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Cable[source]

Bases: GenericProcessingDU

Class goals:
  • pre_compute interpolation

compute_for_freqs(freqs_mhz)[source]

Compute ABCD_matrix for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.VGAFilter(gain=0)[source]

Bases: GenericProcessingDU

Class goals:
  • pre_compute interpolation

compute_for_freqs(freqs_mhz)[source]

Compute ABCD_matrix for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.BalunBeforeADC[source]

Bases: GenericProcessingDU

Class goals: * Pass signal through Balun before Analog to Digitial Converter (ADC) for each antenna * Balun is used in x, y, and z ports * Same data is used for all three ports * read data files only once * pre_compute interpolation * this Balun is referred to as Balun1 Balun2

compute_for_freqs(freqs_mhz)[source]

compute s-parameters and ABCD matrix of Balun before AD chip for freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Rfchain_elements_db(filename='test2.s2p')[source]

Bases: GenericProcessingDU

A chain element whose response is tabulated in decibels.

compute_for_freqs(freqs_mhz)[source]

Computes this stage’s response on the given frequency axis.

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz. The tabulated S-parameters are interpolated onto it, and are zero outside the 30-250 MHz band they were measured over.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Rfchain_elements_db_rad(filename='test2.s2p')[source]

Bases: GenericProcessingDU

A chain element tabulated in decibels, with phase in radians.

compute_for_freqs(freqs_mhz)[source]

Computes this stage’s response on the given frequency axis.

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz. The tabulated S-parameters are interpolated onto it, and are zero outside the 30-250 MHz band they were measured over.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Rfchain_elements(filename='test.s2p')[source]

Bases: GenericProcessingDU

A chain element whose response is tabulated in linear units.

compute_for_freqs(freqs_mhz)[source]

compute s-parameters and ABCD matrix of Balun before AD chip for freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Rfchain_elements_rad(filename='test.s2p')[source]

Bases: GenericProcessingDU

A chain element tabulated linearly, with phase in radians.

compute_for_freqs(freqs_mhz)[source]

compute s-parameters and ABCD matrix of Balun before AD chip for freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Zload_arb(filename='S_balun_AD.s1p')[source]

Bases: GenericProcessingDU

An arbitrary load impedance read from a measurement file.

compute_for_freqs(freqs_mhz)[source]

Computes this stage’s response on the given frequency axis.

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz. The tabulated S-parameters are interpolated onto it, and are zero outside the 30-250 MHz band they were measured over.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.Zload[source]

Bases: GenericProcessingDU

Class goals: * computes input impedance of load due to balun + 200ohm ADC.

compute_for_freqs(freqs_mhz)[source]

compute S-paramters and Zload for freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

class grand.sim.detector.rf_chain.RFChain(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

See also

grand.sim.efield2voltage.Efield2Voltage

Applies this chain to simulated voltages.

grand.sim.detector.rf_chain.s2abcd

Converts measured S-parameters to the cascadable form.

Examples

The magnitude of the transfer function, from the open-circuit voltage to the ADC input, for the three arms, at frequencies in MHz:

import numpy as np
from grand.sim.detector.rf_chain import RFChain

chain = RFChain()
chain.compute_for_freqs(np.array([50.0, 100.0, 150.0, 200.0]))
print(np.round(np.abs(chain.get_tf()), 1))           # (arms, frequencies)
[[46.3 47.  28.5 17. ]
 [45.7 42.6 26.8 22.7]
 [17.9 35.  35.2 22.5]]
compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

Examples

The chain is a net amplifier across the band at 20 dB; Fig. 8 of arXiv:2408.10926 shows this curve.

import numpy as np
from grand.sim.detector.rf_chain import RFChain

freqs = np.arange(30.0, 251.0)
chain = RFChain(vga_gain=20)
chain.compute_for_freqs(freqs)

gain = np.abs(chain.get_tf())
print("peak |V_out/V_oc| per arm:", np.round(gain.max(axis=1), 1))
print("at 100 MHz              :", np.round(gain[:, 70], 1))
peak |V_out/V_oc| per arm: [94.8 93.5 69. ]
at 100 MHz              : [47.  42.6 35. ]
class grand.sim.detector.rf_chain.RFChainNut(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

class grand.sim.detector.rf_chain.RFChain_gaa(vga_gain=0)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

class grand.sim.detector.rf_chain.RFChain_Balun1(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

class grand.sim.detector.rf_chain.RFChain_Match_net(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

class grand.sim.detector.rf_chain.RFChain_Cable_Connectors(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

class grand.sim.detector.rf_chain.RFChain_VGA(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

class grand.sim.detector.rf_chain.RFChain_in_Balun1(vga_gain=20)[source]

Bases: GenericProcessingDU

Facade for all elements in RF chain

compute_for_freqs(freqs_mhz)[source]

Compute transfer function for frequency freqs_mhz

Parameters:

freqs_mhz (ndarray, shape (n_freq,)) – Output frequency axis, in MHz.

Returns:

Results are stored on the instance; the chain reads them when it cascades the stages.

Return type:

None

vout_f(voc_f)[source]

Compute final voltage after propagating signal through RF chain. Input: Voc_f (in frequency domain) Output: Voltage after RF chain in frequency domain. Make sure to run self.compute_for_freqs() before calling this method. RK Note: name ‘vout_f’ is a placeholder. Change it with something better.

Parameters:

voc_f (ndarray, shape (3, n_freq)) – Open-circuit voltage spectrum at the antenna feed point.

Returns:

Voltage spectrum after the chain, for one unit’s three arms.

Return type:

ndarray, shape (3, n_freq)

get_tf()[source]

Return transfer function for all elements in RF chain total transfer function is the output voltage for input Voc of 1. It says by what factor the Voc will be multiplied by the RF chain. @return total TF (complex, (3,N)):

Returns:

The complex transfer function per antenna arm, \(V_{\rm out}/V_{\rm oc}\).

Return type:

ndarray, shape (3, n_freq)

grand.sim.noise.galaxy

Simulation of Galactic radio noise.

grand.sim.noise.galaxy.interpol_at_new_x(a_x, a_y, new_x)[source]

Interpolate a discrete function and return zero outside its definition range.

Parameters:
  • a_x (ndarray) – Sample positions.

  • a_y (ndarray) – Sample values.

  • new_x (ndarray) – Positions to interpolate onto.

Returns:

Cubic interpolation, zero outside the range of a_x.

Return type:

ndarray

grand.sim.noise.galaxy.galactic_noise(f_lst, size_out, freqs_mhz, nb_ant, seed=None, du_type='GP300')[source]

Generate Galactic-noise voltage spectra for detector units.

The precomputed Galactic-noise tables contain available-power spectral density P_L in W/Hz for 30–250 MHz, 72 local-sidereal-time bins (20-minute spacing), and the three antenna ports X, Y, Z. The requested LST is mapped to the nearest available 20-minute bin.

For each port, the open-circuit RMS voltage in a 1 MHz native table bin is reconstructed from

V_oc,RMS**2 = 4 * P_L * Re(Z_ant).

The RMS voltage is then scaled to the requested uniform FFT-bin width, interpolated onto freqs_mhz, and converted to complex rFFT coefficients using the normalization appropriate for SciPy/NumPy’s backward-normalized irfft.

Parameters:
  • f_lst (float) – Local sidereal time in hours. Must satisfy 0 <= f_lst < 24.

  • size_out (int) – Length, in samples, of the time trace the spectrum belongs to. It sets the normalization, so it must be the length the spectrum will be inverted to, whatever part of the frequency axis freqs_mhz covers.

  • freqs_mhz (ndarray, shape (nb_freq,)) – Uniformly spaced output-frequency grid, in MHz: the full rFFT axis of size_out samples (size_out // 2 + 1 points), or a band of it. It is not checked against size_out.

  • nb_ant (int) – Number of detector units for which independent noise is generated.

  • seed (int or None, optional) – Random-number-generator seed. If None, a non-reproducible realization is generated; an integer seed gives reproducible output.

  • du_type (str, optional) – Antenna model used for the Galactic-noise table: "GP300" (HFSS), "GP300_nec" (NEC), or "GP300_mat" (MATLAB).

Returns:

Complex rFFT coefficients of the Galactic noise, for all detector units and antenna arms, in microvolts.

Return type:

ndarray, complex, shape (nb_ant, 3, nb_freq)

Raises:

ValueError – If f_lst is not finite or lies outside [0, 24), if du_type is not one of the three tabulated models, or if freqs_mhz is not uniformly spaced and increasing.

See also

grand.sim.efield2voltage.Efield2Voltage

Adds this noise to simulated voltages.

grand.sim.detector.rf_chain.RFChain

The chain the noise passes through.

Examples

One realization of the noise spectrum for two units at 18 h local sidereal time, on a 1 MHz grid from 30 to 250 MHz:

import numpy as np
from grand.sim.noise.galaxy import galactic_noise

freqs_mhz = np.arange(30.0, 251.0)
spectrum = galactic_noise(18.0, 2048, freqs_mhz, nb_ant=2, seed=1)
print(spectrum.shape)                                # (units, arms, frequencies)
print("|V| at 30, 100 and 250 MHz on unit 0, arm X:",
      np.round(np.abs(spectrum[0, 0, [0, 70, 220]]), 1), "µV")
(2, 3, 221)
|V| at 30, 100 and 250 MHz on unit 0, arm X: [ 793.4 3713.6  364.2] µV

grand.sim.detector.adc

Master module for the ADC in GRAND

class grand.sim.detector.adc.ADC[source]

Bases: object

Class that represents the analog-to-digital converter (ADC) of GRAND. The ADC digitizes an analog voltage that has been processed through the entire RF chain. For GRAND, the ADC has: - a sampling rate of 500 MHz - 14 bits centered around 0 V <-> 0 ADC counts, with 13 positive and 13 negative bits - a saturation at an input voltage of +/- 0.9 V

See also

grand.sim.efield2voltage.Efield2Voltage

Computes the voltages the ADC digitizes.

grand.sim.detector.trigger.t1_du_triggers

The T1 trigger on the digitized traces.

downsample(voltage_trace, input_sampling_rate_mhz)[source]

downsamples the voltage trace to the target sampling rate

Parameters:
  • voltage_trace (np.ndarray[double]) – Array of voltage traces, with shape (N_du,3,N_samples), in µV.

  • input_sampling_rate_mhz (float) – Sampling rate of voltage_trace, in MHz.

Returns:

downsampled_voltage_trace – Array of downsampled voltage traces, with shape (N_du,3,N_samples), in µV.

Return type:

np.ndarray[double]

Examples

Simulated voltages are sampled at 2 GHz; the ADC samples at 500 MHz:

import numpy as np
from grand import ADC

voltage = np.zeros((1, 3, 8192))                     # one unit, 4.096 µs at 2 GHz
print(ADC().downsample(voltage, 2000.0).shape)
(1, 3, 2048)
process(voltage_trace, noise_trace=None)[source]

Processes an analog voltage trace to a digital ADC trace, with an option to add measured noise

Parameters:
  • voltage_trace (np.ndarray[float]) – Array of voltage traces at the ADC level, with shape (N_du,3,N_samples), in µV.

  • noise_trace (np.ndarray[int], optional) – Array of measured noise traces, with shape (N_du,3,N_samples), in ADC counts.

Returns:

adc_trace – Array of ADC traces with shape (N_du,3,N_samples), in ADC counts.

Return type:

np.ndarray[int]

Examples

A signal below one count does not come out small, it comes out absent. A simulation whose voltages land under the step yields an all-zero trace, which reads as a quiet event rather than a scaling error.

import numpy as np
from grand.sim.detector.adc import ADC

adc = ADC()
lsb = adc.max_voltage / adc.max_bit_value
print("one count is %.1f uV" % lsb)

t = np.arange(512) * 0.5
pulse = np.exp(-((t - 100.0) ** 2) / (2 * 5.0 ** 2))

for amplitude in (lsb / 10, lsb * 100):
    trace = np.stack([np.stack([pulse * amplitude] * 3)])
    print("%9.1f uV -> %4d counts"
          % (amplitude, int(np.abs(np.asarray(adc.process(trace))).max())))
one count is 109.9 uV
     11.0 uV ->    0 counts
  10986.3 uV ->  100 counts

grand.sim.detector.trigger

Offline, DAQ-style first-level (T1) trigger on ADC traces.

The algorithm and default parameters are those of scripts/T1_trigger_offline.py; scripts/convert_voltage2adc.py --t1_trigger applies them.

Warning

The default parameters (DEFAULT_T1_CONFIG) are copied verbatim from the offline script. They have not been confirmed by the trigger group as the ones the DAQ uses. See extract_trigger_parameters() for the points that need confirming.

The trigger, per channel

All times are in ns and converted to samples assuming 2 ns per sample (500 MHz, the GRAND ADC):

  1. T1 crossing. The first sample strictly above th1. It needs at least t_quiet/2 samples before it in the trace, all at or below th1 (the quiet time). A first crossing earlier than that is rejected.

  2. Window. The t_period/2 samples starting at the T1 crossing.

  3. T2 crossings. In the window, every upward crossing of +th2 (a sample above th2 whose predecessor is not); the T1 crossing counts as the first one. Negative crossings of -th2 are not counted.

  4. Separation. Each T2 crossing must follow the previous one by less than t_sepmax ns; one that does not rejects the channel.

  5. Count. The number of crossings NC must be within [nc_min, nc_max].

Q (peak / NC) is computed and returned, but, as in the offline script, q_min and q_max are not applied.

The trigger, per detector unit

A DU passes T1 if any of the channels in channels (by default 0, 1, 2, the X, Y and Z ADC channels) passes. t1_du_triggers() evaluates every DU of an event.

grand.sim.detector.trigger.DEFAULT_T1_CONFIG = {'nc_max': 8, 'nc_min': 2, 'q_max': 255, 'q_min': 0, 'sepmax_ends_count': 0, 'sepmax_inclusive': 0, 't_overlap': 64, 't_period': 512, 't_posttrig': 1024, 't_pretrig': 960, 't_quiet': 512, 't_sepmax': 10, 'th1': 100, 'th2': 50}

The trigger parameters of scripts/T1_trigger_offline.py, unchanged. Times are in ns, thresholds in ADC counts. t_pretrig, t_overlap and t_posttrig configure the DAQ readout window and are not used by the trigger itself. To be confirmed by the trigger group.

grand.sim.detector.trigger.DEFAULT_T1_CHANNELS = (0, 1, 2)

The ADC channels the offline script evaluates, (0, 1, 2) = (X, Y, Z).

grand.sim.detector.trigger.T1_TRIGGER_FLAG = 1

Value written in trigger_flag for a DU that passes T1; 0 otherwise. trigger_flag is documented as “same as event_type”, where 0x1000 is the 10 s trigger, 0x8000 the random trigger and anything else a shower trigger; the encoding of a T1 shower trigger is to be confirmed by the trigger group.

grand.sim.detector.trigger.extract_trigger_parameters(trace, trigger_config=None, baseline=0)[source]

Extracts the T1 trigger information from one ADC channel.

The algorithm of scripts/T1_trigger_offline.py, unchanged; see the module docstring for a description.

Parameters:
  • trace (array_like) – One channel’s trace, in ADC counts, sampled at 500 MHz (2 ns).

  • trigger_config (dict, optional) – Trigger parameters overriding DEFAULT_T1_CONFIG.

  • baseline (float, optional) – Subtracted from the peak when computing Q. The thresholds are applied to the raw trace, so it must already be centered on 0.

Returns:

index_T1_crossing (int), index_T2_crossing (array of int, indices in trace, the T1 crossing first), NC (int) and Q.

Return type:

dict

Raises:

ValueError – If the channel does not trigger: no T1 crossing, not enough samples before it for the quiet time, or two T2 crossings more than t_sepmax apart.

Notes

The default parameters are the offline script’s. The trigger group must confirm them, and these points of the algorithm, before the result is taken as the DAQ’s:

  • the thresholds th1 = 100 and th2 = 50 ADC counts, and that they apply to the positive polarity only (-th2 crossings are not counted, and a negative-only pulse never triggers);

  • t_sepmax = 10 ns with a strict <: at 2 ns per sample, T2 crossings must be at most 8 ns apart, so an oscillation of period 10 ns or longer (below 100 MHz) never gives NC >= 2;

  • that a crossing too far apart rejects the channel, rather than ending the count;

  • the quiet time: only the first crossing is tried, and it is rejected if it lies within the first t_quiet/2 samples;

  • q_min/q_max, which are not applied;

  • the channels used (0, 1, 2) and the “any channel” DU logic.

grand.sim.detector.trigger.t1_channel_trigger(trace, trigger_config=None, baseline=0)[source]

Whether one ADC channel passes T1.

It passes if extract_trigger_parameters() finds a T1 crossing and the number of crossings is within [nc_min, nc_max]. The parameters are to be confirmed by the trigger group (see that function).

Return type:

bool

grand.sim.detector.trigger.t1_du_triggers(traces, trigger_config=None, channels=(0, 1, 2), baseline=0)[source]

Evaluates T1 on every detector unit of an event.

Parameters:
  • traces (array_like) – ADC traces with shape (N_du, N_channels, N_samples), e.g. TADC.trace_ch or the output of grand.ADC.process().

  • trigger_config (dict, optional) – Trigger parameters overriding DEFAULT_T1_CONFIG. The defaults are the offline script’s and are to be confirmed by the trigger group.

  • channels (sequence of int, optional) – The channels evaluated; a DU triggers if any of them does.

  • baseline (float, optional) – See extract_trigger_parameters().

Returns:

One value per DU.

Return type:

numpy.ndarray of bool

See also

grand.sim.detector.trigger.t1_channel_trigger

T1 on one channel.

grand.sim.detector.trigger.extract_trigger_parameters

The quantities T1 decides on.

grand.sim.detector.adc.ADC.process

Produces the ADC traces T1 reads.

Examples

Which units of a digitized event pass T1 with the default parameters:

from pathlib import Path

import numpy as np
import grand
from grand.dataio import TADC
from grand.sim.detector.trigger import t1_du_triggers

sample = (Path(grand.__file__).parents[1]
          / "sim2root/Common/sim_Xiaodushan_20221026_000000_RUN1_CD_ZHAireS_0000")
with TADC(str(sample / "adc_1618-13790_L1_0000.root")) as tadc:
    tadc.get_entry(0)
    counts = np.asarray(tadc.trace_ch)
    du_id = np.asarray(tadc.du_id)

passed = t1_du_triggers(counts)
print("units passing T1:", du_id[passed])
units passing T1: [229 245]
grand.sim.detector.trigger.t1_trigger_flags(traces, trigger_config=None, channels=(0, 1, 2), baseline=0)[source]

The trigger_flag values for every DU of an event.

T1_TRIGGER_FLAG for a DU that passes t1_du_triggers(), 0 otherwise. The encoding is to be confirmed by the trigger group.

Return type:

numpy.ndarray of numpy.ushort

grand.sim.shower.gen_shower

class grand.sim.shower.gen_shower.ShowerEvent(energy: float | None = None, zenith: float | None = None, azimuth: float | None = None, primary: ParticleCode | None = None, frame: GRANDCS | LTP | None = None, core: CartesianRepresentation | None = None, geomagnet: CartesianRepresentation | None = None, maximum: CartesianRepresentation | None = None, origin_geoid: list | np.ndarray | None = None, fields: OrderedDict | None = None)[source]

Bases: CheckedFields

The parameters of one air shower.

Direction, energy, core position, depth of maximum and primary type, together with the frame they are expressed in.

Parameters:
  • energy (Optional[float]) – Energy of the primary, in GeV (TShower.energy_primary)

  • zenith (Optional[float]) – Zenith angle of the direction the shower comes from, in degrees

  • azimuth (Optional[float]) – Azimuth of the direction the shower comes from, in degrees, from north toward west

  • primary (Optional[ParticleCode]) – Primary particle type

  • frame (Optional[Union[GRANDCS, LTP]]) – The shower frame: NWU, centered on the core, magnetic north

  • core (Optional[CartesianRepresentation]) – Shower core, in m, in GRANDCS

  • geomagnet (Optional[CartesianRepresentation]) – Geomagnetic field vector, if set; load_root does not set it

  • maximum (Optional[CartesianRepresentation]) – Position of Xmax, in m, in frame

  • origin_geoid (Optional[list, np.ndarray]) – Origin of GRANDCS as [latitude (deg), longitude (deg), height (m)]; set it before load_root

  • fields (Optional[OrderedDict]) – Electric fields by antenna, if set; load_root does not set it

energy: float | None = None

Energy of the primary, in GeV (TShower.energy_primary)

zenith: float | None = None

Zenith angle of the direction the shower comes from, in degrees

azimuth: float | None = None

Azimuth of the direction the shower comes from, in degrees, from north toward west

primary: ParticleCode | None = None

Primary particle type

frame: GRANDCS | LTP | None = None

NWU, centered on the core, magnetic north

Type:

The shower frame

core: CartesianRepresentation | None = None

Shower core, in m, in GRANDCS

geomagnet: CartesianRepresentation | None = None

Geomagnetic field vector, if set; load_root does not set it

maximum: CartesianRepresentation | None = None

Position of Xmax, in m, in frame

origin_geoid: list | np.ndarray | None = None

Origin of GRANDCS as [latitude (deg), longitude (deg), height (m)]; set it before load_root

fields: OrderedDict | None = None

Electric fields by antenna, if set; load_root does not set it

load_root(d_shower)[source]

Returns a shower built from a ROOT shower tree.

Parameters:

d_shower (TShower or TShowerSim) – The tree to read.

Returns:

The shower it describes.

Return type:

ShowerEvent

grand.sim.shower.pdg

class grand.sim.shower.pdg.ParticleCode(*values)[source]

Bases: IntEnum

PDG Monte Carlo particle numbering scheme

Ref: http://pdg.lbl.gov/2007/reviews/montecarlorpp.pdf

GAMMA = 22
Z_0 = 23
W_PLUS = 24
W_MINUS = -24
ELECTRON = 11
ANTI_ELECTRON = -11
NEUTRINO_E = 12
ANTI_NEUTRINO_E = -12
MUON = 13
ANTI_MUON = -13
NEUTRINO_MU = 14
ANTI_NEUTRINO_MU = -14
TAU = 15
ANTI_TAU = -15
NEUTRINO_TAU = 16
ANTI_NEUTRINO_TAU = -16
PION_0 = 111
PION_PLUS = 211
PION_MINUS = -211
PROTON = 2212
NEUTRON = 2112
IRON = 1000260560