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 |
---|---|
|
A list-type (usually a numpy array) of time values at which to calculate the amplitude of the Askaryan pulse. |
|
A |
|
The viewing angle in radians measured from the shower axis. |
|
The distance of the observation point from the shower vertex. |
|
The ice model to be used for describing the medium’s index of refraction. |
|
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.