PyREx

Custom Sub-Package

While the PyREx package provides a basis for simulation, the real benefits come in customizing the analysis for different purposes. To this end the custom sub-package allows for plug-in style modules to be distributed for different collaborations.

By default PyREx comes with a few custom modules included, listed below. More information about each of these modules can be found in their respective API sections:

Other institutions and research groups are encouraged to create their own custom modules to integrate with PyREx. These modules have full access to PyREx as if they were a native part of the package. When PyREx is loaded it automatically scans for these custom modules in certain parts of the filesystem and includes any modules that it can find. The first place searched is the custom directory in the PyREx package itself. Next, if a .pyrex-custom directory exists in the user’s home directory (note the leading .), its subdirectories are searched for custom directories and any modules in these directories are included. Finally, if a pyrex-custom directory exists in the current working directory (this time without the leading .), its subdirectories are similarly scanned for modules inside custom directories. Note that if any name-clashing occurs, the first result found takes precedence (without warning). Additionally, none of these custom directories should contain an __init__.py file, or else the plug-in system may not work (For more information on the implementation, see PEP 420 and/or David Beazley’s 2015 PyCon talk on Modules and Packages at https://youtu.be/0oTh1CXRaQ0?t=1h25m45s).

As an example, in the following filesystem layout (which is not meant to reflect the actual current modules available to PyREx) the available custom modules are pyrex.custom.pyspice, pyrex.custom.irex, pyrex.custom.ara, pyrex.custom.arianna, and pyrex.custom.my_analysis. Additionally note that the name clash for the ARA module will result in the module included in PyREx being loaded and the ARA module in .pyrex-custom will be ignored.

/path/to/site-packages/pyrex/
|-- __init__.py
|-- signals.py
|-- antenna.py
|-- ...
|-- custom/
|   |-- pyspice.py
|   |-- irex/
|   |   |-- __init__.py
|   |   |-- antenna.py
|   |   |-- ...
|   |-- ara/
|   |   |-- __init__.py
|   |   |-- antenna.py
|   |   |-- ...

/path/to/home_dir/.pyrex-custom/
|-- ara/
|   |-- custom/
|   |   |-- ara/
|   |   |   |-- __init__.py
|   |   |   |-- antenna.py
|   |   |   |-- ...
|-- arianna/
|   |-- custom/
|   |   |-- arianna/
|   |   |   |-- __init__.py
|   |   |   |-- antenna.py
|   |   |   |-- ...

/path/to/cwd/pyrex-custom/
|-- my_analysis_module/
|   |-- custom/
|   |   |-- my_analysis.py

Build Your Own Custom Module

In the course of using PyREx you may wish to change some behavior of parts of the code. Due to the modularity of the code, many behaviors should be customizable by substituting in your own classes inheriting from those already in PyREx. By adding these classes to your own custom module, your code can behave as though it was a native part of the PyREx package. Below the classes which can be easily substituted with your own version are listed, and descriptions of the behavior expected of the classes is outlined.

Askaryan Signal

The AskaryanSignal class is responsible for storing the time-domain signal of the Askaryan signal produced by a particle shower. The __init__() method of an AskaryanSignal-like class must accept the arguments listed below:

Attribute

Description

times

A list-type (usually a numpy array) of time values at which to calculate the amplitude of the Askaryan pulse.

particle

A Particle object representing the neutrino that causes the event. Should have an energy, vertex, id, and an interaction with an em_frac and had_frac.

viewing_angle

The viewing angle in radians measured from the shower axis.

viewing_distance

The distance of the observation point from the shower vertex.

ice

The ice model to be used for describing the medium’s index of refraction.

t0

The starting time of the Askaryan pulse / showers (default 0).

The __init__() method should result in a Signal object with values being a numpy array of amplitudes corresponding to the given times and should have a proper value_type. Additionally, all methods of the Signal class should be implemented (typically by just inheriting from Signal).

Antenna / Antenna System

The Antenna class is primarily responsible for receiving and triggering on Signal objects. The __init__() method of an Antenna-like class must accept a position argument, and any other arguments may be specified as desired. The __init__() method should set the position attribute to the given argument. If not inheriting from Antenna, the following methods and attributes must be implemented and may require the __init__() method to set some other attributes. AntennaSystem-like classes must expose the same required methods and attributes as Antenna-like classes, typically by passing calls down to an underlying Antenna-like object and applying some extra processing.

The signals attribute should contain a list of all pure Signal objects that the antenna has seen. This is different from the all_waveforms attribute, which should contain a list of all waveform (pure signal + noise) Signal objects the antenna has seen. Yet again different from the waveforms attribute, which should contain only those waveforms which have triggered the antenna.

If using the default all_waveforms and waveforms, a _noises attribute and _triggers attribute must be initialized to empty lists in __init__(). Additionally a make_noise() method must be defined which takes a times array and returns a Signal object with noise amplitudes in the values attribute. If using the default make_noise() method, a _noise_master attribute must be set in __init__() to either None or a Signal object that can generate noise waveforms (setting _noise_master to None and handling noise generation with the attributes freq_range and noise_rms, or temperature and resistance, is recommended).

A full_waveform() method is required which will take a times array and return a Signal object of the waveform the antenna sees at those times. If using the default full_waveform(), a noisy attribute is required which contains a boolean value of whether or not the antenna includes noise in its waveforms. If noisy is True then a make_noise() method is also required, as described in the previous paragraph.

An is_hit attribute is required which will be a boolean of whether or not the antenna has been triggered by any waveforms. Similarly an is_hit_during() method is required which will take a times array and return a boolean of whether the antenna is triggered during those times.

The trigger() method of the antenna should take a Signal object and return a boolean of whether or not that signal would trigger the antenna.

The clear() method should reset the antenna to a state of having received no signals (i.e. the state just after initialization), and should accept a boolean for reset_noise which will force the noise waveforms to be recalculated. If using the default clear() method, the _noises and _triggers attributes must be lists.

A receive() method is required which will take a Signal object as signal, a 3-vector (list) as direction, and a 3-vector (list) as polarization. This function doesn’t return anything, but instead processes the input signal and stores it to the signals list (and anything else needed for the antenna to have officially received the signal). This is the final required method, but if using the default receive() method, an antenna_factor attribute is needed to define the conversion from electric field to voltage and an efficiency attribute is required, along with four more methods which must be defined:

The _convert_to_antenna_coordinates() method should take a point in cartesian coordinates and return the r, theta, and phi values of that point relative to the antenna. The directional_gain() method should take theta and phi in radians and return a (complex) gain based on the directional response of the antenna. Similarly the polarization_gain() method should take a polarization 3-vector (list) of an incoming signal and return a (complex) gain based on the polarization response of the antenna. Finally, the response() method should take a list of frequencies and return the (complex) gains of the frequency response of the antenna. This assumes that the directional and frequency responses are separable. If this is not the case then the gains may be better handled with a custom receive() method.

Detector

The preferred method of creating your own detector class is to inherit from the Detector class and then implement the set_positions() method, the triggered() method, and potentially the build_antennas() method. However the only requirement of a Detector-like object is that iterating over it will visit each antenna exactly once. This means that a simple list of antennas is an acceptable rudimentary detector. The advantages of using the Detector class are easy breaking into subsets (a detector could be made up of stations, which in turn are made up of strings) and the simpler triggered() method for trigger checks.

Ice Model

Ice model classes are responsible for describing the properties of the ice as functions of depth and frequency. While not explicitly required, all ice model classes in PyREx are defined only with static and class methods, so no __init__() method is actually necessary. The necessary methods, however, are as follows:

The index() method should take a depth (or numpy array of depths) and return the corresponding index of refraction. Conversely, the depth_with_index() method should take an index of refraction (or numpy array of indices) and return the corresponding depths. In the case of degeneracy here (for example with uniform ice), the recommended behavior is to return the shallowest depth with the given index, though PyREx’s behavior in cases of non-monotonic index functions is not well defined.

The temperature() method should take a depth (or numpy array of depths) and return the corresponding ice temperature in Kelvin.

Finally, the attenuation_length() function should take a depth (or numpy array of depths) and a frequency (or numpy array of frequencies) and return the corresponding attenuation length. In the case of one scalar and one array argument, a simple 1D array should be returned. In the case of both arguments being arrays, the return value should be a 2D array where each row represents different frequencies at a single depth and each column represents different depths at a single frequency.

Ray Tracer / Ray Trace Path

The RayTracer and RayTracePath classes are responsible for handling ray tracing through the ice between shower vertices and antenna positions. The RayTracer class finds the paths between the two points and the RayTracePath calculates values along the path. Due to the potential for high calculation costs, the PyREx RayTracer and RayTracePath classes inherit from a LazyMutableClass which allows the use of a lazy_property() decorator to cache results of attribute calculations. It is recommended that any other ray tracing classes consider doing this as well.

The __init__() method of a RayTracer-like class should take as arguments a 3-vector (list) from_point, a 3-vector (list) to_point, and an IceModel-like ice_model. The only required features of the class are a boolean attribute exists recording whether or not paths exist between the given points, and an iterable attribute solutions which iterates over RayTracePath-like objects between the points.

A RayTracePath-like class will be initialized by a corresponding RayTracer-like object, so there are no requirements on its __init__() method. The path must have emitted_direction and received_direction attributes which are numpy arrays of the cartesian direction the ray is pointing at the from_point and to_point of the ray tracer, respectively. The path must also have attributes for the path_length and tof (time of flight) along the path.

The path class must have a propagate() method which takes a Signal object as its argument and propagates that signal by applying any attenuation and time of flight. This method does not have a return value. Additionally, note that any 1/R factor that the signal could have is not applied in this method, but externally by dividing the signal values by the path_length. If using the default propagate() method, an attenuation() method is required which takes an array of frequencies f and returns the attenuation factors for a signal along the path at those frequencies.

Finally, though not required it is recommended that the path have a coordinates attribute which is a list of lists of the x, y, and z coordinates along the path (with some reasonable step size). This method is used for plotting purposes and does not need to have the accuracy necessary for calculations.

Interaction Model

The interaction model used for Particle interactions in ice handles the cross sections and interaction lengths of neutrinos, as well as the ratios of their interaction types and the resulting shower fractions. An interaction class should inherit from Interaction (preferably keeping its __init__() method) and should implement the following methods:

The cross_section property method should return the neutrino cross section for the Interaction.particle parent, specific to the Interaction.kind. Similarly the total_cross_section property method should return the neutrino cross section for the Interaction.particle parent, but this should be the total cross section for both charged-current and neutral-current interactions. The interaction_length and total_interaction_length properties will convert these cross sections to interaction lengths automatically.

The choose_interaction() method should return a value from Interaction.Type representing the interaction type based on a random choice. Similarly the choose_inelasticity() method should return an inelasticity value based on a random choice, and the choose_shower_fractions() method return calculate electromagnetic and hadronic fractions based on the inelasticity attribute storing the inelasticity value from choose_inelasticity(). The choose_shower_fractions() can be either chosen based on random processes like secondary generation or deterministic.

Particle Generator

The particle generator classes are quite flexible. The only requirement is that they possess an create_event() method which returns a Event object consisting of at least one Particle. The Generator base class provides a solid foundation for basic uniform generators in a volume, requiring only implementation of the get_vertex() and get_exit_points() methods for the specific volume at a minimum.

PyREx

A Python package for simulation of neutrinos and radio antennas in ice. Version 1.10.0

Navigation