diff --git a/.buildinfo b/.buildinfo
new file mode 100644
index 000000000..7dee9557e
--- /dev/null
+++ b/.buildinfo
@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: 3872ab692dbdcde5a5e895926c86e65b
+tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 000000000..e69de29bb
diff --git a/AUTHORS.html b/AUTHORS.html
new file mode 100644
index 000000000..6d5ba033a
--- /dev/null
+++ b/AUTHORS.html
@@ -0,0 +1,144 @@
+
+
+
+
+
pyirf started as part of protopipe by Julien Lefaucher,
+but was largely rewritten in September 2020, making use of code from the
+previous version, the pyfact module and the
+FACT irf package.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_images/notebooks_fact_example_20_2.png b/_images/notebooks_fact_example_20_2.png
new file mode 100644
index 000000000..85db81b67
Binary files /dev/null and b/_images/notebooks_fact_example_20_2.png differ
diff --git a/_images/notebooks_fact_example_25_1.png b/_images/notebooks_fact_example_25_1.png
new file mode 100644
index 000000000..9371fc492
Binary files /dev/null and b/_images/notebooks_fact_example_25_1.png differ
diff --git a/_images/notebooks_fact_example_29_0.png b/_images/notebooks_fact_example_29_0.png
new file mode 100644
index 000000000..2ac695bf5
Binary files /dev/null and b/_images/notebooks_fact_example_29_0.png differ
diff --git a/_modules/astropy/units/core.html b/_modules/astropy/units/core.html
new file mode 100644
index 000000000..d12aba06d
--- /dev/null
+++ b/_modules/astropy/units/core.html
@@ -0,0 +1,2806 @@
+
+
+
+
+
+
+
+ astropy.units.core — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+
+"""
+Core units classes and functions.
+"""
+
+
+importinspect
+importoperator
+importtextwrap
+importwarnings
+
+importnumpyasnp
+
+fromastropy.utils.decoratorsimportlazyproperty
+fromastropy.utils.exceptionsimportAstropyWarning
+fromastropy.utils.miscimportisiterable
+
+from.importformatasunit_format
+from.utilsimport(
+ is_effectively_unity,
+ resolve_fractions,
+ sanitize_power,
+ sanitize_scale,
+ validate_power,
+)
+
+__all__=[
+ "UnitsError",
+ "UnitsWarning",
+ "UnitConversionError",
+ "UnitTypeError",
+ "UnitBase",
+ "NamedUnit",
+ "IrreducibleUnit",
+ "Unit",
+ "CompositeUnit",
+ "PrefixUnit",
+ "UnrecognizedUnit",
+ "def_unit",
+ "get_current_unit_registry",
+ "set_enabled_units",
+ "add_enabled_units",
+ "set_enabled_equivalencies",
+ "add_enabled_equivalencies",
+ "set_enabled_aliases",
+ "add_enabled_aliases",
+ "dimensionless_unscaled",
+ "one",
+]
+
+UNITY=1.0
+
+
+def_flatten_units_collection(items):
+"""
+ Given a list of sequences, modules or dictionaries of units, or
+ single units, return a flat set of all the units found.
+ """
+ ifnotisinstance(items,list):
+ items=[items]
+
+ result=set()
+ foriteminitems:
+ ifisinstance(item,UnitBase):
+ result.add(item)
+ else:
+ ifisinstance(item,dict):
+ units=item.values()
+ elifinspect.ismodule(item):
+ units=vars(item).values()
+ elifisiterable(item):
+ units=item
+ else:
+ continue
+
+ forunitinunits:
+ ifisinstance(unit,UnitBase):
+ result.add(unit)
+
+ returnresult
+
+
+def_normalize_equivalencies(equivalencies):
+"""Normalizes equivalencies ensuring each is a 4-tuple.
+
+ The resulting tuple is of the form::
+
+ (from_unit, to_unit, forward_func, backward_func)
+
+ Parameters
+ ----------
+ equivalencies : list of equivalency pairs
+
+ Raises
+ ------
+ ValueError if an equivalency cannot be interpreted
+ """
+ ifequivalenciesisNone:
+ return[]
+
+ normalized=[]
+
+ fori,equivinenumerate(equivalencies):
+ iflen(equiv)==2:
+ funit,tunit=equiv
+ a=b=lambdax:x
+ eliflen(equiv)==3:
+ funit,tunit,a=equiv
+ b=a
+ eliflen(equiv)==4:
+ funit,tunit,a,b=equiv
+ else:
+ raiseValueError(f"Invalid equivalence entry {i}: {equiv!r}")
+ ifnot(
+ funitisUnit(funit)
+ and(tunitisNoneortunitisUnit(tunit))
+ andcallable(a)
+ andcallable(b)
+ ):
+ raiseValueError(f"Invalid equivalence entry {i}: {equiv!r}")
+ normalized.append((funit,tunit,a,b))
+
+ returnnormalized
+
+
+class_UnitRegistry:
+"""
+ Manages a registry of the enabled units.
+ """
+
+ def__init__(self,init=[],equivalencies=[],aliases={}):
+ ifisinstance(init,_UnitRegistry):
+ # If passed another registry we don't need to rebuild everything.
+ # but because these are mutable types we don't want to create
+ # conflicts so everything needs to be copied.
+ self._equivalencies=init._equivalencies.copy()
+ self._aliases=init._aliases.copy()
+ self._all_units=init._all_units.copy()
+ self._registry=init._registry.copy()
+ self._non_prefix_units=init._non_prefix_units.copy()
+ # The physical type is a dictionary containing sets as values.
+ # All of these must be copied otherwise we could alter the old
+ # registry.
+ self._by_physical_type={
+ k:v.copy()fork,vininit._by_physical_type.items()
+ }
+
+ else:
+ self._reset_units()
+ self._reset_equivalencies()
+ self._reset_aliases()
+ self.add_enabled_units(init)
+ self.add_enabled_equivalencies(equivalencies)
+ self.add_enabled_aliases(aliases)
+
+ def_reset_units(self):
+ self._all_units=set()
+ self._non_prefix_units=set()
+ self._registry={}
+ self._by_physical_type={}
+
+ def_reset_equivalencies(self):
+ self._equivalencies=set()
+
+ def_reset_aliases(self):
+ self._aliases={}
+
+ @property
+ defregistry(self):
+ returnself._registry
+
+ @property
+ defall_units(self):
+ returnself._all_units
+
+ @property
+ defnon_prefix_units(self):
+ returnself._non_prefix_units
+
+ defset_enabled_units(self,units):
+"""
+ Sets the units enabled in the unit registry.
+
+ These units are searched when using
+ `UnitBase.find_equivalent_units`, for example.
+
+ Parameters
+ ----------
+ units : list of sequence, dict, or module
+ This is a list of things in which units may be found
+ (sequences, dicts or modules), or units themselves. The
+ entire set will be "enabled" for searching through by
+ methods like `UnitBase.find_equivalent_units` and
+ `UnitBase.compose`.
+ """
+ self._reset_units()
+ returnself.add_enabled_units(units)
+
+ defadd_enabled_units(self,units):
+"""
+ Adds to the set of units enabled in the unit registry.
+
+ These units are searched when using
+ `UnitBase.find_equivalent_units`, for example.
+
+ Parameters
+ ----------
+ units : list of sequence, dict, or module
+ This is a list of things in which units may be found
+ (sequences, dicts or modules), or units themselves. The
+ entire set will be added to the "enabled" set for
+ searching through by methods like
+ `UnitBase.find_equivalent_units` and `UnitBase.compose`.
+ """
+ units=_flatten_units_collection(units)
+
+ forunitinunits:
+ # Loop through all of the names first, to ensure all of them
+ # are new, then add them all as a single "transaction" below.
+ forstinunit._names:
+ ifstinself._registryandunit!=self._registry[st]:
+ raiseValueError(
+ f"Object with name {st!r} already exists in namespace. "
+ "Filter the set of units to avoid name clashes before "
+ "enabling them."
+ )
+
+ forstinunit._names:
+ self._registry[st]=unit
+
+ self._all_units.add(unit)
+ ifnotisinstance(unit,PrefixUnit):
+ self._non_prefix_units.add(unit)
+
+ hash=unit._get_physical_type_id()
+ self._by_physical_type.setdefault(hash,set()).add(unit)
+
+ defget_units_with_physical_type(self,unit):
+"""
+ Get all units in the registry with the same physical type as
+ the given unit.
+
+ Parameters
+ ----------
+ unit : UnitBase instance
+ """
+ returnself._by_physical_type.get(unit._get_physical_type_id(),set())
+
+ @property
+ defequivalencies(self):
+ returnlist(self._equivalencies)
+
+ defset_enabled_equivalencies(self,equivalencies):
+"""
+ Sets the equivalencies enabled in the unit registry.
+
+ These equivalencies are used if no explicit equivalencies are given,
+ both in unit conversion and in finding equivalent units.
+
+ This is meant in particular for allowing angles to be dimensionless.
+ Use with care.
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ List of equivalent pairs, e.g., as returned by
+ `~astropy.units.equivalencies.dimensionless_angles`.
+ """
+ self._reset_equivalencies()
+ returnself.add_enabled_equivalencies(equivalencies)
+
+ defadd_enabled_equivalencies(self,equivalencies):
+"""
+ Adds to the set of equivalencies enabled in the unit registry.
+
+ These equivalencies are used if no explicit equivalencies are given,
+ both in unit conversion and in finding equivalent units.
+
+ This is meant in particular for allowing angles to be dimensionless.
+ Use with care.
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ List of equivalent pairs, e.g., as returned by
+ `~astropy.units.equivalencies.dimensionless_angles`.
+ """
+ # pre-normalize list to help catch mistakes
+ equivalencies=_normalize_equivalencies(equivalencies)
+ self._equivalencies|=set(equivalencies)
+
+ @property
+ defaliases(self):
+ returnself._aliases
+
+ defset_enabled_aliases(self,aliases):
+"""
+ Set aliases for units.
+
+ Parameters
+ ----------
+ aliases : dict of str, Unit
+ The aliases to set. The keys must be the string aliases, and values
+ must be the `astropy.units.Unit` that the alias will be mapped to.
+
+ Raises
+ ------
+ ValueError
+ If the alias already defines a different unit.
+
+ """
+ self._reset_aliases()
+ self.add_enabled_aliases(aliases)
+
+ defadd_enabled_aliases(self,aliases):
+"""
+ Add aliases for units.
+
+ Parameters
+ ----------
+ aliases : dict of str, Unit
+ The aliases to add. The keys must be the string aliases, and values
+ must be the `astropy.units.Unit` that the alias will be mapped to.
+
+ Raises
+ ------
+ ValueError
+ If the alias already defines a different unit.
+
+ """
+ foralias,unitinaliases.items():
+ ifaliasinself._registryandunit!=self._registry[alias]:
+ raiseValueError(
+ f"{alias} already means {self._registry[alias]}, so "
+ f"cannot be used as an alias for {unit}."
+ )
+ ifaliasinself._aliasesandunit!=self._aliases[alias]:
+ raiseValueError(
+ f"{alias} already is an alias for {self._aliases[alias]}, so "
+ f"cannot be used as an alias for {unit}."
+ )
+
+ foralias,unitinaliases.items():
+ ifaliasnotinself._registryandaliasnotinself._aliases:
+ self._aliases[alias]=unit
+
+
+class_UnitContext:
+ def__init__(self,init=[],equivalencies=[]):
+ _unit_registries.append(_UnitRegistry(init=init,equivalencies=equivalencies))
+
+ def__enter__(self):
+ pass
+
+ def__exit__(self,type,value,tb):
+ _unit_registries.pop()
+
+
+_unit_registries=[_UnitRegistry()]
+
+
+defget_current_unit_registry():
+ return_unit_registries[-1]
+
+
+defset_enabled_units(units):
+"""
+ Sets the units enabled in the unit registry.
+
+ These units are searched when using
+ `UnitBase.find_equivalent_units`, for example.
+
+ This may be used either permanently, or as a context manager using
+ the ``with`` statement (see example below).
+
+ Parameters
+ ----------
+ units : list of sequence, dict, or module
+ This is a list of things in which units may be found
+ (sequences, dicts or modules), or units themselves. The
+ entire set will be "enabled" for searching through by methods
+ like `UnitBase.find_equivalent_units` and `UnitBase.compose`.
+
+ Examples
+ --------
+ >>> from astropy import units as u
+ >>> with u.set_enabled_units([u.pc]):
+ ... u.m.find_equivalent_units()
+ ...
+ Primary name | Unit definition | Aliases
+ [
+ pc | 3.08568e+16 m | parsec ,
+ ]
+ >>> u.m.find_equivalent_units()
+ Primary name | Unit definition | Aliases
+ [
+ AU | 1.49598e+11 m | au, astronomical_unit ,
+ Angstrom | 1e-10 m | AA, angstrom ,
+ cm | 0.01 m | centimeter ,
+ earthRad | 6.3781e+06 m | R_earth, Rearth ,
+ jupiterRad | 7.1492e+07 m | R_jup, Rjup, R_jupiter, Rjupiter ,
+ lsec | 2.99792e+08 m | lightsecond ,
+ lyr | 9.46073e+15 m | lightyear ,
+ m | irreducible | meter ,
+ micron | 1e-06 m | ,
+ pc | 3.08568e+16 m | parsec ,
+ solRad | 6.957e+08 m | R_sun, Rsun ,
+ ]
+ """
+ # get a context with a new registry, using equivalencies of the current one
+ context=_UnitContext(equivalencies=get_current_unit_registry().equivalencies)
+ # in this new current registry, enable the units requested
+ get_current_unit_registry().set_enabled_units(units)
+ returncontext
+
+
+defadd_enabled_units(units):
+"""
+ Adds to the set of units enabled in the unit registry.
+
+ These units are searched when using
+ `UnitBase.find_equivalent_units`, for example.
+
+ This may be used either permanently, or as a context manager using
+ the ``with`` statement (see example below).
+
+ Parameters
+ ----------
+ units : list of sequence, dict, or module
+ This is a list of things in which units may be found
+ (sequences, dicts or modules), or units themselves. The
+ entire set will be added to the "enabled" set for searching
+ through by methods like `UnitBase.find_equivalent_units` and
+ `UnitBase.compose`.
+
+ Examples
+ --------
+ >>> from astropy import units as u
+ >>> from astropy.units import imperial
+ >>> with u.add_enabled_units(imperial):
+ ... u.m.find_equivalent_units()
+ ...
+ Primary name | Unit definition | Aliases
+ [
+ AU | 1.49598e+11 m | au, astronomical_unit ,
+ Angstrom | 1e-10 m | AA, angstrom ,
+ cm | 0.01 m | centimeter ,
+ earthRad | 6.3781e+06 m | R_earth, Rearth ,
+ ft | 0.3048 m | foot ,
+ fur | 201.168 m | furlong ,
+ inch | 0.0254 m | ,
+ jupiterRad | 7.1492e+07 m | R_jup, Rjup, R_jupiter, Rjupiter ,
+ lsec | 2.99792e+08 m | lightsecond ,
+ lyr | 9.46073e+15 m | lightyear ,
+ m | irreducible | meter ,
+ mi | 1609.34 m | mile ,
+ micron | 1e-06 m | ,
+ mil | 2.54e-05 m | thou ,
+ nmi | 1852 m | nauticalmile, NM ,
+ pc | 3.08568e+16 m | parsec ,
+ solRad | 6.957e+08 m | R_sun, Rsun ,
+ yd | 0.9144 m | yard ,
+ ]
+ """
+ # get a context with a new registry, which is a copy of the current one
+ context=_UnitContext(get_current_unit_registry())
+ # in this new current registry, enable the further units requested
+ get_current_unit_registry().add_enabled_units(units)
+ returncontext
+
+
+defset_enabled_equivalencies(equivalencies):
+"""
+ Sets the equivalencies enabled in the unit registry.
+
+ These equivalencies are used if no explicit equivalencies are given,
+ both in unit conversion and in finding equivalent units.
+
+ This is meant in particular for allowing angles to be dimensionless.
+ Use with care.
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ list of equivalent pairs, e.g., as returned by
+ `~astropy.units.equivalencies.dimensionless_angles`.
+
+ Examples
+ --------
+ Exponentiation normally requires dimensionless quantities. To avoid
+ problems with complex phases::
+
+ >>> from astropy import units as u
+ >>> with u.set_enabled_equivalencies(u.dimensionless_angles()):
+ ... phase = 0.5 * u.cycle
+ ... np.exp(1j*phase) # doctest: +FLOAT_CMP
+ <Quantity -1.+1.2246468e-16j>
+ """
+ # get a context with a new registry, using all units of the current one
+ context=_UnitContext(get_current_unit_registry())
+ # in this new current registry, enable the equivalencies requested
+ get_current_unit_registry().set_enabled_equivalencies(equivalencies)
+ returncontext
+
+
+defadd_enabled_equivalencies(equivalencies):
+"""
+ Adds to the equivalencies enabled in the unit registry.
+
+ These equivalencies are used if no explicit equivalencies are given,
+ both in unit conversion and in finding equivalent units.
+
+ This is meant in particular for allowing angles to be dimensionless.
+ Since no equivalencies are enabled by default, generally it is recommended
+ to use `set_enabled_equivalencies`.
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ list of equivalent pairs, e.g., as returned by
+ `~astropy.units.equivalencies.dimensionless_angles`.
+ """
+ # get a context with a new registry, which is a copy of the current one
+ context=_UnitContext(get_current_unit_registry())
+ # in this new current registry, enable the further equivalencies requested
+ get_current_unit_registry().add_enabled_equivalencies(equivalencies)
+ returncontext
+
+
+defset_enabled_aliases(aliases):
+"""
+ Set aliases for units.
+
+ This is useful for handling alternate spellings for units, or
+ misspelled units in files one is trying to read.
+
+ Parameters
+ ----------
+ aliases : dict of str, Unit
+ The aliases to set. The keys must be the string aliases, and values
+ must be the `astropy.units.Unit` that the alias will be mapped to.
+
+ Raises
+ ------
+ ValueError
+ If the alias already defines a different unit.
+
+ Examples
+ --------
+ To temporarily allow for a misspelled 'Angstroem' unit::
+
+ >>> from astropy import units as u
+ >>> with u.set_enabled_aliases({'Angstroem': u.Angstrom}):
+ ... print(u.Unit("Angstroem", parse_strict="raise") == u.Angstrom)
+ True
+
+ """
+ # get a context with a new registry, which is a copy of the current one
+ context=_UnitContext(get_current_unit_registry())
+ # in this new current registry, enable the further equivalencies requested
+ get_current_unit_registry().set_enabled_aliases(aliases)
+ returncontext
+
+
+defadd_enabled_aliases(aliases):
+"""
+ Add aliases for units.
+
+ This is useful for handling alternate spellings for units, or
+ misspelled units in files one is trying to read.
+
+ Since no aliases are enabled by default, generally it is recommended
+ to use `set_enabled_aliases`.
+
+ Parameters
+ ----------
+ aliases : dict of str, Unit
+ The aliases to add. The keys must be the string aliases, and values
+ must be the `astropy.units.Unit` that the alias will be mapped to.
+
+ Raises
+ ------
+ ValueError
+ If the alias already defines a different unit.
+
+ Examples
+ --------
+ To temporarily allow for a misspelled 'Angstroem' unit::
+
+ >>> from astropy import units as u
+ >>> with u.add_enabled_aliases({'Angstroem': u.Angstrom}):
+ ... print(u.Unit("Angstroem", parse_strict="raise") == u.Angstrom)
+ True
+
+ """
+ # get a context with a new registry, which is a copy of the current one
+ context=_UnitContext(get_current_unit_registry())
+ # in this new current registry, enable the further equivalencies requested
+ get_current_unit_registry().add_enabled_aliases(aliases)
+ returncontext
+
+
+classUnitsError(Exception):
+"""
+ The base class for unit-specific exceptions.
+ """
+
+
+classUnitScaleError(UnitsError,ValueError):
+"""
+ Used to catch the errors involving scaled units,
+ which are not recognized by FITS format.
+ """
+
+ pass
+
+
+classUnitConversionError(UnitsError,ValueError):
+"""
+ Used specifically for errors related to converting between units or
+ interpreting units in terms of other units.
+ """
+
+
+classUnitTypeError(UnitsError,TypeError):
+"""
+ Used specifically for errors in setting to units not allowed by a class.
+
+ E.g., would be raised if the unit of an `~astropy.coordinates.Angle`
+ instances were set to a non-angular unit.
+ """
+
+
+classUnitsWarning(AstropyWarning):
+"""
+ The base class for unit-specific warnings.
+ """
+
+
+classUnitBase:
+"""
+ Abstract base class for units.
+
+ Most of the arithmetic operations on units are defined in this
+ base class.
+
+ Should not be instantiated by users directly.
+ """
+
+ # Make sure that __rmul__ of units gets called over the __mul__ of Numpy
+ # arrays to avoid element-wise multiplication.
+ __array_priority__=1000
+
+ _hash=None
+ _type_id=None
+
+ def__deepcopy__(self,memo):
+ # This may look odd, but the units conversion will be very
+ # broken after deep-copying if we don't guarantee that a given
+ # physical unit corresponds to only one instance
+ returnself
+
+ def_repr_latex_(self):
+"""
+ Generate latex representation of unit name. This is used by
+ the IPython notebook to print a unit with a nice layout.
+
+ Returns
+ -------
+ Latex string
+ """
+ returnunit_format.Latex.to_string(self)
+
+ def__bytes__(self):
+"""Return string representation for unit."""
+ returnunit_format.Generic.to_string(self).encode("unicode_escape")
+
+ def__str__(self):
+"""Return string representation for unit."""
+ returnunit_format.Generic.to_string(self)
+
+ def__repr__(self):
+ string=unit_format.Generic.to_string(self)
+
+ returnf'Unit("{string}")'
+
+ def_get_physical_type_id(self):
+"""
+ Returns an identifier that uniquely identifies the physical
+ type of this unit. It is comprised of the bases and powers of
+ this unit, without the scale. Since it is hashable, it is
+ useful as a dictionary key.
+ """
+ ifself._type_idisNone:
+ unit=self.decompose()
+ self._type_id=tuple(zip((base.nameforbaseinunit.bases),unit.powers))
+
+ returnself._type_id
+
+ @property
+ defnames(self):
+"""
+ Returns all of the names associated with this unit.
+ """
+ raiseAttributeError(
+ "Can not get names from unnamed units. Perhaps you meant to_string()?"
+ )
+
+ @property
+ defname(self):
+"""
+ Returns the canonical (short) name associated with this unit.
+ """
+ raiseAttributeError(
+ "Can not get names from unnamed units. Perhaps you meant to_string()?"
+ )
+
+ @property
+ defaliases(self):
+"""
+ Returns the alias (long) names for this unit.
+ """
+ raiseAttributeError(
+ "Can not get aliases from unnamed units. Perhaps you meant to_string()?"
+ )
+
+ @property
+ defscale(self):
+"""
+ Return the scale of the unit.
+ """
+ return1.0
+
+ @property
+ defbases(self):
+"""
+ Return the bases of the unit.
+ """
+ return[self]
+
+ @property
+ defpowers(self):
+"""
+ Return the powers of the unit.
+ """
+ return[1]
+
+ defto_string(self,format=unit_format.Generic,**kwargs):
+r"""Output the unit in the given format as a string.
+
+ Parameters
+ ----------
+ format : `astropy.units.format.Base` instance or str
+ The name of a format or a formatter object. If not
+ provided, defaults to the generic format.
+
+ **kwargs
+ Further options forwarded to the formatter. Currently
+ recognized is ``fraction``, which can take the following values:
+
+ - `False` : display unit bases with negative powers as they are;
+ - 'inline' or `True` : use a single-line fraction;
+ - 'multiline' : use a multiline fraction (available for the
+ 'latex', 'console' and 'unicode' formats only).
+
+ Raises
+ ------
+ TypeError
+ If ``format`` is of the wrong type.
+ ValueError
+ If ``format`` or ``fraction`` are not recognized.
+
+ Examples
+ --------
+ >>> import astropy.units as u
+ >>> kms = u.Unit('km / s')
+ >>> kms.to_string() # Generic uses fraction='inline' by default
+ 'km / s'
+ >>> kms.to_string('latex') # Latex uses fraction='multiline' by default
+ '$\\mathrm{\\frac{km}{s}}$'
+ >>> print(kms.to_string('unicode', fraction=False))
+ km s⁻¹
+ >>> print(kms.to_string('unicode', fraction='inline'))
+ km / s
+ >>> print(kms.to_string('unicode', fraction='multiline'))
+ km
+ ──
+ s
+ """
+ f=unit_format.get_format(format)
+ returnf.to_string(self,**kwargs)
+
+ def__format__(self,format_spec):
+"""Try to format units using a formatter."""
+ try:
+ returnself.to_string(format=format_spec)
+ exceptValueError:
+ returnformat(str(self),format_spec)
+
+ @staticmethod
+ def_normalize_equivalencies(equivalencies):
+"""Normalizes equivalencies, ensuring each is a 4-tuple.
+
+ The resulting tuple is of the form::
+
+ (from_unit, to_unit, forward_func, backward_func)
+
+ Parameters
+ ----------
+ equivalencies : list of equivalency pairs, or None
+
+ Returns
+ -------
+ A normalized list, including possible global defaults set by, e.g.,
+ `set_enabled_equivalencies`, except when `equivalencies`=`None`,
+ in which case the returned list is always empty.
+
+ Raises
+ ------
+ ValueError if an equivalency cannot be interpreted
+ """
+ normalized=_normalize_equivalencies(equivalencies)
+ ifequivalenciesisnotNone:
+ normalized+=get_current_unit_registry().equivalencies
+
+ returnnormalized
+
+ def__pow__(self,p):
+ p=validate_power(p)
+ returnCompositeUnit(1,[self],[p],_error_check=False)
+
+ def__truediv__(self,m):
+ ifisinstance(m,(bytes,str)):
+ m=Unit(m)
+
+ ifisinstance(m,UnitBase):
+ ifm.is_unity():
+ returnself
+ returnCompositeUnit(1,[self,m],[1,-1],_error_check=False)
+
+ try:
+ # Cannot handle this as Unit, re-try as Quantity
+ from.quantityimportQuantity
+
+ returnQuantity(1,self)/m
+ exceptTypeError:
+ returnNotImplemented
+
+ def__rtruediv__(self,m):
+ ifisinstance(m,(bytes,str)):
+ returnUnit(m)/self
+
+ try:
+ # Cannot handle this as Unit. Here, m cannot be a Quantity,
+ # so we make it into one, fasttracking when it does not have a
+ # unit, for the common case of <array> / <unit>.
+ from.quantityimportQuantity
+
+ ifhasattr(m,"unit"):
+ result=Quantity(m)
+ result/=self
+ returnresult
+ else:
+ returnQuantity(m,self**(-1))
+ exceptTypeError:
+ returnNotImplemented
+
+ def__mul__(self,m):
+ ifisinstance(m,(bytes,str)):
+ m=Unit(m)
+
+ ifisinstance(m,UnitBase):
+ ifm.is_unity():
+ returnself
+ elifself.is_unity():
+ returnm
+ returnCompositeUnit(1,[self,m],[1,1],_error_check=False)
+
+ # Cannot handle this as Unit, re-try as Quantity.
+ try:
+ from.quantityimportQuantity
+
+ returnQuantity(1,unit=self)*m
+ exceptTypeError:
+ returnNotImplemented
+
+ def__rmul__(self,m):
+ ifisinstance(m,(bytes,str)):
+ returnUnit(m)*self
+
+ # Cannot handle this as Unit. Here, m cannot be a Quantity,
+ # so we make it into one, fasttracking when it does not have a unit
+ # for the common case of <array> * <unit>.
+ try:
+ from.quantityimportQuantity
+
+ ifhasattr(m,"unit"):
+ result=Quantity(m)
+ result*=self
+ returnresult
+ else:
+ returnQuantity(m,unit=self)
+ exceptTypeError:
+ returnNotImplemented
+
+ def__rlshift__(self,m):
+ try:
+ from.quantityimportQuantity
+
+ returnQuantity(m,self,copy=False,subok=True)
+ exceptException:
+ returnNotImplemented
+
+ def__rrshift__(self,m):
+ warnings.warn(
+ ">> is not implemented. Did you mean to convert "
+ f"to a Quantity with unit {m} using '<<'?",
+ AstropyWarning,
+ )
+ returnNotImplemented
+
+ def__hash__(self):
+ ifself._hashisNone:
+ parts=(
+ [str(self.scale)]
+ +[x.nameforxinself.bases]
+ +[str(x)forxinself.powers]
+ )
+ self._hash=hash(tuple(parts))
+ returnself._hash
+
+ def__getstate__(self):
+ # If we get pickled, we should *not* store the memoized members since
+ # hashes of strings vary between sessions.
+ state=self.__dict__.copy()
+ state.pop("_hash",None)
+ state.pop("_type_id",None)
+ returnstate
+
+ def__eq__(self,other):
+ ifselfisother:
+ returnTrue
+
+ try:
+ other=Unit(other,parse_strict="silent")
+ except(ValueError,UnitsError,TypeError):
+ returnNotImplemented
+
+ # Other is unit-like, but the test below requires it is a UnitBase
+ # instance; if it is not, give up (so that other can try).
+ ifnotisinstance(other,UnitBase):
+ returnNotImplemented
+
+ try:
+ returnis_effectively_unity(self._to(other))
+ exceptUnitsError:
+ returnFalse
+
+ def__ne__(self,other):
+ returnnot(self==other)
+
+ def__le__(self,other):
+ scale=self._to(Unit(other))
+ returnscale<=1.0oris_effectively_unity(scale)
+
+ def__ge__(self,other):
+ scale=self._to(Unit(other))
+ returnscale>=1.0oris_effectively_unity(scale)
+
+ def__lt__(self,other):
+ returnnot(self>=other)
+
+ def__gt__(self,other):
+ returnnot(self<=other)
+
+ def__neg__(self):
+ returnself*-1.0
+
+ defis_equivalent(self,other,equivalencies=[]):
+"""
+ Returns `True` if this unit is equivalent to ``other``.
+
+ Parameters
+ ----------
+ other : `~astropy.units.Unit`, str, or tuple
+ The unit to convert to. If a tuple of units is specified, this
+ method returns true if the unit matches any of those in the tuple.
+
+ equivalencies : list of tuple
+ A list of equivalence pairs to try if the units are not
+ directly convertible. See :ref:`astropy:unit_equivalencies`.
+ This list is in addition to possible global defaults set by, e.g.,
+ `set_enabled_equivalencies`.
+ Use `None` to turn off all equivalencies.
+
+ Returns
+ -------
+ bool
+ """
+ equivalencies=self._normalize_equivalencies(equivalencies)
+
+ ifisinstance(other,tuple):
+ returnany(self.is_equivalent(u,equivalencies)foruinother)
+
+ other=Unit(other,parse_strict="silent")
+
+ returnself._is_equivalent(other,equivalencies)
+
+ def_is_equivalent(self,other,equivalencies=[]):
+"""Returns `True` if this unit is equivalent to `other`.
+ See `is_equivalent`, except that a proper Unit object should be
+ given (i.e., no string) and that the equivalency list should be
+ normalized using `_normalize_equivalencies`.
+ """
+ ifisinstance(other,UnrecognizedUnit):
+ returnFalse
+
+ ifself._get_physical_type_id()==other._get_physical_type_id():
+ returnTrue
+ eliflen(equivalencies):
+ unit=self.decompose()
+ other=other.decompose()
+ fora,b,forward,backwardinequivalencies:
+ ifbisNone:
+ # after canceling, is what's left convertible
+ # to dimensionless (according to the equivalency)?
+ try:
+ (other/unit).decompose([a])
+ returnTrue
+ exceptException:
+ pass
+ elif(a._is_equivalent(unit)andb._is_equivalent(other))or(
+ b._is_equivalent(unit)anda._is_equivalent(other)
+ ):
+ returnTrue
+
+ returnFalse
+
+ def_apply_equivalencies(self,unit,other,equivalencies):
+"""
+ Internal function (used from `_get_converter`) to apply
+ equivalence pairs.
+ """
+
+ defmake_converter(scale1,func,scale2):
+ defconvert(v):
+ returnfunc(_condition_arg(v)/scale1)*scale2
+
+ returnconvert
+
+ forfunit,tunit,a,binequivalencies:
+ iftunitisNone:
+ ratio=other.decompose()/unit.decompose()
+ try:
+ ratio_in_funit=ratio.decompose([funit])
+ returnmake_converter(ratio_in_funit.scale,a,1.0)
+ exceptUnitsError:
+ pass
+ else:
+ try:
+ scale1=funit._to(unit)
+ scale2=tunit._to(other)
+ returnmake_converter(scale1,a,scale2)
+ exceptUnitsError:
+ pass
+ try:
+ scale1=tunit._to(unit)
+ scale2=funit._to(other)
+ returnmake_converter(scale1,b,scale2)
+ exceptUnitsError:
+ pass
+
+ defget_err_str(unit):
+ unit_str=unit.to_string("unscaled")
+ physical_type=unit.physical_type
+ ifphysical_type!="unknown":
+ unit_str=f"'{unit_str}' ({physical_type})"
+ else:
+ unit_str=f"'{unit_str}'"
+ returnunit_str
+
+ unit_str=get_err_str(unit)
+ other_str=get_err_str(other)
+
+ raiseUnitConversionError(f"{unit_str} and {other_str} are not convertible")
+
+ def_get_converter(self,other,equivalencies=[]):
+"""Get a converter for values in ``self`` to ``other``.
+
+ If no conversion is necessary, returns ``unit_scale_converter``
+ (which is used as a check in quantity helpers).
+
+ """
+ # First see if it is just a scaling.
+ try:
+ scale=self._to(other)
+ exceptUnitsError:
+ pass
+ else:
+ ifscale==1.0:
+ returnunit_scale_converter
+ else:
+ returnlambdaval:scale*_condition_arg(val)
+
+ # if that doesn't work, maybe we can do it with equivalencies?
+ try:
+ returnself._apply_equivalencies(
+ self,other,self._normalize_equivalencies(equivalencies)
+ )
+ exceptUnitsErrorasexc:
+ # Last hope: maybe other knows how to do it?
+ # We assume the equivalencies have the unit itself as first item.
+ # TODO: maybe better for other to have a `_back_converter` method?
+ ifhasattr(other,"equivalencies"):
+ forfunit,tunit,a,binother.equivalencies:
+ ifotherisfunit:
+ try:
+ converter=self._get_converter(tunit,equivalencies)
+ exceptException:
+ pass
+ else:
+ returnlambdav:b(converter(v))
+
+ raiseexc
+
+ def_to(self,other):
+"""
+ Returns the scale to the specified unit.
+
+ See `to`, except that a Unit object should be given (i.e., no
+ string), and that all defaults are used, i.e., no
+ equivalencies and value=1.
+ """
+ # There are many cases where we just want to ensure a Quantity is
+ # of a particular unit, without checking whether it's already in
+ # a particular unit. If we're being asked to convert from a unit
+ # to itself, we can short-circuit all of this.
+ ifselfisother:
+ return1.0
+
+ # Don't presume decomposition is possible; e.g.,
+ # conversion to function units is through equivalencies.
+ ifisinstance(other,UnitBase):
+ self_decomposed=self.decompose()
+ other_decomposed=other.decompose()
+
+ # Check quickly whether equivalent. This is faster than
+ # `is_equivalent`, because it doesn't generate the entire
+ # physical type list of both units. In other words it "fails
+ # fast".
+ ifself_decomposed.powers==other_decomposed.powersandall(
+ self_baseisother_base
+ for(self_base,other_base)inzip(
+ self_decomposed.bases,other_decomposed.bases
+ )
+ ):
+ returnself_decomposed.scale/other_decomposed.scale
+
+ raiseUnitConversionError(f"'{self!r}' is not a scaled version of '{other!r}'")
+
+ defto(self,other,value=UNITY,equivalencies=[]):
+"""
+ Return the converted values in the specified unit.
+
+ Parameters
+ ----------
+ other : unit-like
+ The unit to convert to.
+
+ value : int, float, or scalar array-like, optional
+ Value(s) in the current unit to be converted to the
+ specified unit. If not provided, defaults to 1.0
+
+ equivalencies : list of tuple
+ A list of equivalence pairs to try if the units are not
+ directly convertible. See :ref:`astropy:unit_equivalencies`.
+ This list is in addition to possible global defaults set by, e.g.,
+ `set_enabled_equivalencies`.
+ Use `None` to turn off all equivalencies.
+
+ Returns
+ -------
+ values : scalar or array
+ Converted value(s). Input value sequences are returned as
+ numpy arrays.
+
+ Raises
+ ------
+ UnitsError
+ If units are inconsistent
+ """
+ ifotherisselfandvalueisUNITY:
+ returnUNITY
+ else:
+ returnself._get_converter(Unit(other),equivalencies)(value)
+
+ defin_units(self,other,value=1.0,equivalencies=[]):
+"""
+ Alias for `to` for backward compatibility with pynbody.
+ """
+ returnself.to(other,value=value,equivalencies=equivalencies)
+
+ defdecompose(self,bases=set()):
+"""
+ Return a unit object composed of only irreducible units.
+
+ Parameters
+ ----------
+ bases : sequence of UnitBase, optional
+ The bases to decompose into. When not provided,
+ decomposes down to any irreducible units. When provided,
+ the decomposed result will only contain the given units.
+ This will raises a `UnitsError` if it's not possible
+ to do so.
+
+ Returns
+ -------
+ unit : `~astropy.units.CompositeUnit`
+ New object containing only irreducible unit objects.
+ """
+ raiseNotImplementedError()
+
+ def_compose(
+ self,equivalencies=[],namespace=[],max_depth=2,depth=0,cached_results=None
+ ):
+ defis_final_result(unit):
+ # Returns True if this result contains only the expected
+ # units
+ returnall(baseinnamespaceforbaseinunit.bases)
+
+ unit=self.decompose()
+ key=hash(unit)
+
+ cached=cached_results.get(key)
+ ifcachedisnotNone:
+ ifisinstance(cached,Exception):
+ raisecached
+ returncached
+
+ # Prevent too many levels of recursion
+ # And special case for dimensionless unit
+ ifdepth>=max_depth:
+ cached_results[key]=[unit]
+ return[unit]
+
+ # Make a list including all of the equivalent units
+ units=[unit]
+ forfunit,tunit,a,binequivalencies:
+ iftunitisnotNone:
+ ifself._is_equivalent(funit):
+ scale=funit.decompose().scale/unit.scale
+ units.append(Unit(a(1.0/scale)*tunit).decompose())
+ elifself._is_equivalent(tunit):
+ scale=tunit.decompose().scale/unit.scale
+ units.append(Unit(b(1.0/scale)*funit).decompose())
+ else:
+ ifself._is_equivalent(funit):
+ units.append(Unit(unit.scale))
+
+ # Store partial results
+ partial_results=[]
+ # Store final results that reduce to a single unit or pair of
+ # units
+ iflen(unit.bases)==0:
+ final_results=[{unit},set()]
+ else:
+ final_results=[set(),set()]
+
+ fortunitinnamespace:
+ tunit_decomposed=tunit.decompose()
+ foruinunits:
+ # If the unit is a base unit, look for an exact match
+ # to one of the bases of the target unit. If found,
+ # factor by the same power as the target unit's base.
+ # This allows us to factor out fractional powers
+ # without needing to do an exhaustive search.
+ iflen(tunit_decomposed.bases)==1:
+ forbase,powerinzip(u.bases,u.powers):
+ iftunit_decomposed._is_equivalent(base):
+ tunit=tunit**power
+ tunit_decomposed=tunit_decomposed**power
+ break
+
+ composed=(u/tunit_decomposed).decompose()
+ factored=composed*tunit
+ len_bases=len(composed.bases)
+ ifis_final_result(factored)andlen_bases<=1:
+ final_results[len_bases].add(factored)
+ else:
+ partial_results.append((len_bases,composed,tunit))
+
+ # Do we have any minimal results?
+ forfinal_resultinfinal_results:
+ iflen(final_result):
+ results=final_results[0].union(final_results[1])
+ cached_results[key]=results
+ returnresults
+
+ partial_results.sort(key=operator.itemgetter(0))
+
+ # ...we have to recurse and try to further compose
+ results=[]
+ forlen_bases,composed,tunitinpartial_results:
+ try:
+ composed_list=composed._compose(
+ equivalencies=equivalencies,
+ namespace=namespace,
+ max_depth=max_depth,
+ depth=depth+1,
+ cached_results=cached_results,
+ )
+ exceptUnitsError:
+ composed_list=[]
+ forsubcomposedincomposed_list:
+ results.append((len(subcomposed.bases),subcomposed,tunit))
+
+ iflen(results):
+ results.sort(key=operator.itemgetter(0))
+
+ min_length=results[0][0]
+ subresults=set()
+ forlen_bases,composed,tunitinresults:
+ iflen_bases>min_length:
+ break
+
+ factored=composed*tunit
+ ifis_final_result(factored):
+ subresults.add(factored)
+
+ iflen(subresults):
+ cached_results[key]=subresults
+ returnsubresults
+
+ ifnotis_final_result(self):
+ result=UnitsError(
+ f"Cannot represent unit {self} in terms of the given units"
+ )
+ cached_results[key]=result
+ raiseresult
+
+ cached_results[key]=[self]
+ return[self]
+
+ defcompose(
+ self,equivalencies=[],units=None,max_depth=2,include_prefix_units=None
+ ):
+"""
+ Return the simplest possible composite unit(s) that represent
+ the given unit. Since there may be multiple equally simple
+ compositions of the unit, a list of units is always returned.
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ A list of equivalence pairs to also list. See
+ :ref:`astropy:unit_equivalencies`.
+ This list is in addition to possible global defaults set by, e.g.,
+ `set_enabled_equivalencies`.
+ Use `None` to turn off all equivalencies.
+
+ units : set of `~astropy.units.Unit`, optional
+ If not provided, any known units may be used to compose
+ into. Otherwise, ``units`` is a dict, module or sequence
+ containing the units to compose into.
+
+ max_depth : int, optional
+ The maximum recursion depth to use when composing into
+ composite units.
+
+ include_prefix_units : bool, optional
+ When `True`, include prefixed units in the result.
+ Default is `True` if a sequence is passed in to ``units``,
+ `False` otherwise.
+
+ Returns
+ -------
+ units : list of `CompositeUnit`
+ A list of candidate compositions. These will all be
+ equally simple, but it may not be possible to
+ automatically determine which of the candidates are
+ better.
+ """
+ # if units parameter is specified and is a sequence (list|tuple),
+ # include_prefix_units is turned on by default. Ex: units=[u.kpc]
+ ifinclude_prefix_unitsisNone:
+ include_prefix_units=isinstance(units,(list,tuple))
+
+ # Pre-normalize the equivalencies list
+ equivalencies=self._normalize_equivalencies(equivalencies)
+
+ # The namespace of units to compose into should be filtered to
+ # only include units with bases in common with self, otherwise
+ # they can't possibly provide useful results. Having too many
+ # destination units greatly increases the search space.
+
+ defhas_bases_in_common(a,b):
+ iflen(a.bases)==0andlen(b.bases)==0:
+ returnTrue
+ forabina.bases:
+ forbbinb.bases:
+ ifab==bb:
+ returnTrue
+ returnFalse
+
+ defhas_bases_in_common_with_equiv(unit,other):
+ ifhas_bases_in_common(unit,other):
+ returnTrue
+ forfunit,tunit,a,binequivalencies:
+ iftunitisnotNone:
+ ifunit._is_equivalent(funit):
+ ifhas_bases_in_common(tunit.decompose(),other):
+ returnTrue
+ elifunit._is_equivalent(tunit):
+ ifhas_bases_in_common(funit.decompose(),other):
+ returnTrue
+ else:
+ ifunit._is_equivalent(funit):
+ ifhas_bases_in_common(dimensionless_unscaled,other):
+ returnTrue
+ returnFalse
+
+ deffilter_units(units):
+ filtered_namespace=set()
+ fortunitinunits:
+ if(
+ isinstance(tunit,UnitBase)
+ and(include_prefix_unitsornotisinstance(tunit,PrefixUnit))
+ andhas_bases_in_common_with_equiv(decomposed,tunit.decompose())
+ ):
+ filtered_namespace.add(tunit)
+ returnfiltered_namespace
+
+ decomposed=self.decompose()
+
+ ifunitsisNone:
+ units=filter_units(self._get_units_with_same_physical_type(equivalencies))
+ iflen(units)==0:
+ units=get_current_unit_registry().non_prefix_units
+ elifisinstance(units,dict):
+ units=set(filter_units(units.values()))
+ elifinspect.ismodule(units):
+ units=filter_units(vars(units).values())
+ else:
+ units=filter_units(_flatten_units_collection(units))
+
+ defsort_results(results):
+ ifnotlen(results):
+ return[]
+
+ # Sort the results so the simplest ones appear first.
+ # Simplest is defined as "the minimum sum of absolute
+ # powers" (i.e. the fewest bases), and preference should
+ # be given to results where the sum of powers is positive
+ # and the scale is exactly equal to 1.0
+ results=list(results)
+ results.sort(key=lambdax:np.abs(x.scale))
+ results.sort(key=lambdax:np.sum(np.abs(x.powers)))
+ results.sort(key=lambdax:np.sum(x.powers)<0.0)
+ results.sort(key=lambdax:notis_effectively_unity(x.scale))
+
+ last_result=results[0]
+ filtered=[last_result]
+ forresultinresults[1:]:
+ ifstr(result)!=str(last_result):
+ filtered.append(result)
+ last_result=result
+
+ returnfiltered
+
+ returnsort_results(
+ self._compose(
+ equivalencies=equivalencies,
+ namespace=units,
+ max_depth=max_depth,
+ depth=0,
+ cached_results={},
+ )
+ )
+
+ defto_system(self,system):
+"""
+ Converts this unit into ones belonging to the given system.
+ Since more than one result may be possible, a list is always
+ returned.
+
+ Parameters
+ ----------
+ system : module
+ The module that defines the unit system. Commonly used
+ ones include `astropy.units.si` and `astropy.units.cgs`.
+
+ To use your own module it must contain unit objects and a
+ sequence member named ``bases`` containing the base units of
+ the system.
+
+ Returns
+ -------
+ units : list of `CompositeUnit`
+ The list is ranked so that units containing only the base
+ units of that system will appear first.
+ """
+ bases=set(system.bases)
+
+ defscore(compose):
+ # In case that compose._bases has no elements we return
+ # 'np.inf' as 'score value'. It does not really matter which
+ # number we would return. This case occurs for instance for
+ # dimensionless quantities:
+ compose_bases=compose.bases
+ iflen(compose_bases)==0:
+ returnnp.inf
+ else:
+ sum=0
+ forbaseincompose_bases:
+ ifbaseinbases:
+ sum+=1
+
+ returnsum/float(len(compose_bases))
+
+ x=self.decompose(bases=bases)
+ composed=x.compose(units=system)
+ composed=sorted(composed,key=score,reverse=True)
+ returncomposed
+
+ @lazyproperty
+ defsi(self):
+"""
+ Returns a copy of the current `Unit` instance in SI units.
+ """
+ from.importsi
+
+ returnself.to_system(si)[0]
+
+ @lazyproperty
+ defcgs(self):
+"""
+ Returns a copy of the current `Unit` instance with CGS units.
+ """
+ from.importcgs
+
+ returnself.to_system(cgs)[0]
+
+ @property
+ defphysical_type(self):
+"""
+ Physical type(s) dimensionally compatible with the unit.
+
+ Returns
+ -------
+ `~astropy.units.physical.PhysicalType`
+ A representation of the physical type(s) of a unit.
+
+ Examples
+ --------
+ >>> from astropy import units as u
+ >>> u.m.physical_type
+ PhysicalType('length')
+ >>> (u.m ** 2 / u.s).physical_type
+ PhysicalType({'diffusivity', 'kinematic viscosity'})
+
+ Physical types can be compared to other physical types
+ (recommended in packages) or to strings.
+
+ >>> area = (u.m ** 2).physical_type
+ >>> area == u.m.physical_type ** 2
+ True
+ >>> area == "area"
+ True
+
+ `~astropy.units.physical.PhysicalType` objects can be used for
+ dimensional analysis.
+
+ >>> number_density = u.m.physical_type ** -3
+ >>> velocity = (u.m / u.s).physical_type
+ >>> number_density * velocity
+ PhysicalType('particle flux')
+ """
+ from.importphysical
+
+ returnphysical.get_physical_type(self)
+
+ def_get_units_with_same_physical_type(self,equivalencies=[]):
+"""
+ Return a list of registered units with the same physical type
+ as this unit.
+
+ This function is used by Quantity to add its built-in
+ conversions to equivalent units.
+
+ This is a private method, since end users should be encouraged
+ to use the more powerful `compose` and `find_equivalent_units`
+ methods (which use this under the hood).
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ A list of equivalence pairs to also pull options from.
+ See :ref:`astropy:unit_equivalencies`. It must already be
+ normalized using `_normalize_equivalencies`.
+ """
+ unit_registry=get_current_unit_registry()
+ units=set(unit_registry.get_units_with_physical_type(self))
+ forfunit,tunit,a,binequivalencies:
+ iftunitisnotNone:
+ ifself.is_equivalent(funit)andtunitnotinunits:
+ units.update(unit_registry.get_units_with_physical_type(tunit))
+ ifself._is_equivalent(tunit)andfunitnotinunits:
+ units.update(unit_registry.get_units_with_physical_type(funit))
+ else:
+ ifself.is_equivalent(funit):
+ units.add(dimensionless_unscaled)
+ returnunits
+
+ classEquivalentUnitsList(list):
+"""
+ A class to handle pretty-printing the result of
+ `find_equivalent_units`.
+ """
+
+ HEADING_NAMES=("Primary name","Unit definition","Aliases")
+ ROW_LEN=3# len(HEADING_NAMES), but hard-code since it is constant
+ NO_EQUIV_UNITS_MSG="There are no equivalent units"
+
+ def__repr__(self):
+ iflen(self)==0:
+ returnself.NO_EQUIV_UNITS_MSG
+ else:
+ lines=self._process_equivalent_units(self)
+ lines.insert(0,self.HEADING_NAMES)
+ widths=[0]*self.ROW_LEN
+ forlineinlines:
+ fori,colinenumerate(line):
+ widths[i]=max(widths[i],len(col))
+
+ f=" {{0:<{}s}} | {{1:<{}s}} | {{2:<{}s}}".format(*widths)
+ lines=[f.format(*line)forlineinlines]
+ lines=lines[0:1]+["["]+[f"{x} ,"forxinlines[1:]]+["]"]
+ return"\n".join(lines)
+
+ def_repr_html_(self):
+"""
+ Outputs a HTML table representation within Jupyter notebooks.
+ """
+ iflen(self)==0:
+ returnf"<p>{self.NO_EQUIV_UNITS_MSG}</p>"
+ else:
+ # HTML tags to use to compose the table in HTML
+ blank_table='<table style="width:50%">{}</table>'
+ blank_row_container="<tr>{}</tr>"
+ heading_row_content="<th>{}</th>"*self.ROW_LEN
+ data_row_content="<td>{}</td>"*self.ROW_LEN
+
+ # The HTML will be rendered & the table is simple, so don't
+ # bother to include newlines & indentation for the HTML code.
+ heading_row=blank_row_container.format(
+ heading_row_content.format(*self.HEADING_NAMES)
+ )
+ data_rows=self._process_equivalent_units(self)
+ all_rows=heading_row
+ forrowindata_rows:
+ html_row=blank_row_container.format(data_row_content.format(*row))
+ all_rows+=html_row
+ returnblank_table.format(all_rows)
+
+ @staticmethod
+ def_process_equivalent_units(equiv_units_data):
+"""
+ Extract attributes, and sort, the equivalent units pre-formatting.
+ """
+ processed_equiv_units=[]
+ foruinequiv_units_data:
+ irred=u.decompose().to_string()
+ ifirred==u.name:
+ irred="irreducible"
+ processed_equiv_units.append((u.name,irred,", ".join(u.aliases)))
+ processed_equiv_units.sort()
+ returnprocessed_equiv_units
+
+ deffind_equivalent_units(
+ self,equivalencies=[],units=None,include_prefix_units=False
+ ):
+"""
+ Return a list of all the units that are the same type as ``self``.
+
+ Parameters
+ ----------
+ equivalencies : list of tuple
+ A list of equivalence pairs to also list. See
+ :ref:`astropy:unit_equivalencies`.
+ Any list given, including an empty one, supersedes global defaults
+ that may be in effect (as set by `set_enabled_equivalencies`)
+
+ units : set of `~astropy.units.Unit`, optional
+ If not provided, all defined units will be searched for
+ equivalencies. Otherwise, may be a dict, module or
+ sequence containing the units to search for equivalencies.
+
+ include_prefix_units : bool, optional
+ When `True`, include prefixed units in the result.
+ Default is `False`.
+
+ Returns
+ -------
+ units : list of `UnitBase`
+ A list of unit objects that match ``u``. A subclass of
+ `list` (``EquivalentUnitsList``) is returned that
+ pretty-prints the list of units when output.
+ """
+ results=self.compose(
+ equivalencies=equivalencies,
+ units=units,
+ max_depth=1,
+ include_prefix_units=include_prefix_units,
+ )
+ results={
+ x.bases[0]forxinresultsiflen(x.bases)==1andx.powers[0]==1
+ }
+ returnself.EquivalentUnitsList(results)
+
+ defis_unity(self):
+"""
+ Returns `True` if the unit is unscaled and dimensionless.
+ """
+ returnFalse
+
+
+classNamedUnit(UnitBase):
+"""
+ The base class of units that have a name.
+
+ Parameters
+ ----------
+ st : str, list of str, 2-tuple
+ The name of the unit. If a list of strings, the first element
+ is the canonical (short) name, and the rest of the elements
+ are aliases. If a tuple of lists, the first element is a list
+ of short names, and the second element is a list of long
+ names; all but the first short name are considered "aliases".
+ Each name *should* be a valid Python identifier to make it
+ easy to access, but this is not required.
+
+ namespace : dict, optional
+ When provided, inject the unit, and all of its aliases, in the
+ given namespace dictionary. If a unit by the same name is
+ already in the namespace, a ValueError is raised.
+
+ doc : str, optional
+ A docstring describing the unit.
+
+ format : dict, optional
+ A mapping to format-specific representations of this unit.
+ For example, for the ``Ohm`` unit, it might be nice to have it
+ displayed as ``\\Omega`` by the ``latex`` formatter. In that
+ case, `format` argument should be set to::
+
+ {'latex': r'\\Omega'}
+
+ Raises
+ ------
+ ValueError
+ If any of the given unit names are already in the registry.
+
+ ValueError
+ If any of the given unit names are not valid Python tokens.
+ """
+
+ def__init__(self,st,doc=None,format=None,namespace=None):
+ UnitBase.__init__(self)
+
+ ifisinstance(st,(bytes,str)):
+ self._names=[st]
+ self._short_names=[st]
+ self._long_names=[]
+ elifisinstance(st,tuple):
+ ifnotlen(st)==2:
+ raiseValueError("st must be string, list or 2-tuple")
+ self._names=st[0]+[nforninst[1]ifnnotinst[0]]
+ ifnotlen(self._names):
+ raiseValueError("must provide at least one name")
+ self._short_names=st[0][:]
+ self._long_names=st[1][:]
+ else:
+ iflen(st)==0:
+ raiseValueError("st list must have at least one entry")
+ self._names=st[:]
+ self._short_names=[st[0]]
+ self._long_names=st[1:]
+
+ ifformatisNone:
+ format={}
+ self._format=format
+
+ ifdocisNone:
+ doc=self._generate_doc()
+ else:
+ doc=textwrap.dedent(doc)
+ doc=textwrap.fill(doc)
+
+ self.__doc__=doc
+
+ self._inject(namespace)
+
+ def_generate_doc(self):
+"""
+ Generate a docstring for the unit if the user didn't supply
+ one. This is only used from the constructor and may be
+ overridden in subclasses.
+ """
+ names=self.names
+ iflen(self.names)>1:
+ returnf"{names[1]} ({names[0]})"
+ else:
+ returnnames[0]
+
+ defget_format_name(self,format):
+"""
+ Get a name for this unit that is specific to a particular
+ format.
+
+ Uses the dictionary passed into the `format` kwarg in the
+ constructor.
+
+ Parameters
+ ----------
+ format : str
+ The name of the format
+
+ Returns
+ -------
+ name : str
+ The name of the unit for the given format.
+ """
+ returnself._format.get(format,self.name)
+
+ @property
+ defnames(self):
+"""
+ Returns all of the names associated with this unit.
+ """
+ returnself._names
+
+ @property
+ defname(self):
+"""
+ Returns the canonical (short) name associated with this unit.
+ """
+ returnself._names[0]
+
+ @property
+ defaliases(self):
+"""
+ Returns the alias (long) names for this unit.
+ """
+ returnself._names[1:]
+
+ @property
+ defshort_names(self):
+"""
+ Returns all of the short names associated with this unit.
+ """
+ returnself._short_names
+
+ @property
+ deflong_names(self):
+"""
+ Returns all of the long names associated with this unit.
+ """
+ returnself._long_names
+
+ def_inject(self,namespace=None):
+"""
+ Injects the unit, and all of its aliases, in the given
+ namespace dictionary.
+ """
+ ifnamespaceisNone:
+ return
+
+ # Loop through all of the names first, to ensure all of them
+ # are new, then add them all as a single "transaction" below.
+ fornameinself._names:
+ ifnameinnamespaceandself!=namespace[name]:
+ raiseValueError(
+ f"Object with name {name!r} already exists in "
+ f"given namespace ({namespace[name]!r})."
+ )
+
+ fornameinself._names:
+ namespace[name]=self
+
+
+def_recreate_irreducible_unit(cls,names,registered):
+"""
+ This is used to reconstruct units when passed around by
+ multiprocessing.
+ """
+ registry=get_current_unit_registry().registry
+ ifnames[0]inregistry:
+ # If in local registry return that object.
+ returnregistry[names[0]]
+ else:
+ # otherwise, recreate the unit.
+ unit=cls(names)
+ ifregistered:
+ # If not in local registry but registered in origin registry,
+ # enable unit in local registry.
+ get_current_unit_registry().add_enabled_units([unit])
+
+ returnunit
+
+
+classIrreducibleUnit(NamedUnit):
+"""
+ Irreducible units are the units that all other units are defined
+ in terms of.
+
+ Examples are meters, seconds, kilograms, amperes, etc. There is
+ only once instance of such a unit per type.
+ """
+
+ def__reduce__(self):
+ # When IrreducibleUnit objects are passed to other processes
+ # over multiprocessing, they need to be recreated to be the
+ # ones already in the subprocesses' namespace, not new
+ # objects, or they will be considered "unconvertible".
+ # Therefore, we have a custom pickler/unpickler that
+ # understands how to recreate the Unit on the other side.
+ registry=get_current_unit_registry().registry
+ return(
+ _recreate_irreducible_unit,
+ (self.__class__,list(self.names),self.nameinregistry),
+ self.__getstate__(),
+ )
+
+ @property
+ defrepresents(self):
+"""The unit that this named unit represents.
+
+ For an irreducible unit, that is always itself.
+ """
+ returnself
+
+ defdecompose(self,bases=set()):
+ iflen(bases)andselfnotinbases:
+ forbaseinbases:
+ try:
+ scale=self._to(base)
+ exceptUnitsError:
+ pass
+ else:
+ ifis_effectively_unity(scale):
+ returnbase
+ else:
+ returnCompositeUnit(scale,[base],[1],_error_check=False)
+
+ raiseUnitConversionError(
+ f"Unit {self} can not be decomposed into the requested bases"
+ )
+
+ returnself
+
+
+classUnrecognizedUnit(IrreducibleUnit):
+"""
+ A unit that did not parse correctly. This allows for
+ round-tripping it as a string, but no unit operations actually work
+ on it.
+
+ Parameters
+ ----------
+ st : str
+ The name of the unit.
+ """
+
+ # For UnrecognizedUnits, we want to use "standard" Python
+ # pickling, not the special case that is used for
+ # IrreducibleUnits.
+ __reduce__=object.__reduce__
+
+ def__repr__(self):
+ returnf"UnrecognizedUnit({self})"
+
+ def__bytes__(self):
+ returnself.name.encode("ascii","replace")
+
+ def__str__(self):
+ returnself.name
+
+ defto_string(self,format=None):
+ returnself.name
+
+ def_unrecognized_operator(self,*args,**kwargs):
+ raiseValueError(
+ f"The unit {self.name!r} is unrecognized, so all arithmetic operations "
+ "with it are invalid."
+ )
+
+ __pow__=__truediv__=__rtruediv__=__mul__=__rmul__=_unrecognized_operator
+ __lt__=__gt__=__le__=__ge__=__neg__=_unrecognized_operator
+
+ def__eq__(self,other):
+ try:
+ other=Unit(other,parse_strict="silent")
+ except(ValueError,UnitsError,TypeError):
+ returnNotImplemented
+
+ returnisinstance(other,type(self))andself.name==other.name
+
+ def__ne__(self,other):
+ returnnot(self==other)
+
+ defis_equivalent(self,other,equivalencies=None):
+ self._normalize_equivalencies(equivalencies)
+ returnself==other
+
+ def_get_converter(self,other,equivalencies=None):
+ self._normalize_equivalencies(equivalencies)
+ raiseValueError(
+ f"The unit {self.name!r} is unrecognized. It can not be converted "
+ "to other units."
+ )
+
+ defget_format_name(self,format):
+ returnself.name
+
+ defis_unity(self):
+ returnFalse
+
+
+class_UnitMetaClass(type):
+"""
+ This metaclass exists because the Unit constructor should
+ sometimes return instances that already exist. This "overrides"
+ the constructor before the new instance is actually created, so we
+ can return an existing one.
+ """
+
+ def__call__(
+ self,
+ s="",
+ represents=None,
+ format=None,
+ namespace=None,
+ doc=None,
+ parse_strict="raise",
+ ):
+ # Short-circuit if we're already a unit
+ ifhasattr(s,"_get_physical_type_id"):
+ returns
+
+ # turn possible Quantity input for s or represents into a Unit
+ from.quantityimportQuantity
+
+ ifisinstance(represents,Quantity):
+ ifis_effectively_unity(represents.value):
+ represents=represents.unit
+ else:
+ represents=CompositeUnit(
+ represents.value*represents.unit.scale,
+ bases=represents.unit.bases,
+ powers=represents.unit.powers,
+ _error_check=False,
+ )
+
+ ifisinstance(s,Quantity):
+ ifis_effectively_unity(s.value):
+ s=s.unit
+ else:
+ s=CompositeUnit(
+ s.value*s.unit.scale,
+ bases=s.unit.bases,
+ powers=s.unit.powers,
+ _error_check=False,
+ )
+
+ # now decide what we really need to do; define derived Unit?
+ ifisinstance(represents,UnitBase):
+ # This has the effect of calling the real __new__ and
+ # __init__ on the Unit class.
+ returnsuper().__call__(
+ s,represents,format=format,namespace=namespace,doc=doc
+ )
+
+ # or interpret a Quantity (now became unit), string or number?
+ ifisinstance(s,UnitBase):
+ returns
+
+ elifisinstance(s,(bytes,str)):
+ iflen(s.strip())==0:
+ # Return the NULL unit
+ returndimensionless_unscaled
+
+ ifformatisNone:
+ format=unit_format.Generic
+
+ f=unit_format.get_format(format)
+ ifisinstance(s,bytes):
+ s=s.decode("ascii")
+
+ try:
+ returnf.parse(s)
+ exceptNotImplementedError:
+ raise
+ exceptExceptionase:
+ ifparse_strict=="silent":
+ pass
+ else:
+ # Deliberately not issubclass here. Subclasses
+ # should use their name.
+ iffisnotunit_format.Generic:
+ format_clause=f.name+" "
+ else:
+ format_clause=""
+ msg=(
+ f"'{s}' did not parse as {format_clause}unit: {str(e)} "
+ "If this is meant to be a custom unit, "
+ "define it with 'u.def_unit'. To have it "
+ "recognized inside a file reader or other code, "
+ "enable it with 'u.add_enabled_units'. "
+ "For details, see "
+ "https://docs.astropy.org/en/latest/units/combining_and_defining.html"
+ )
+ ifparse_strict=="raise":
+ raiseValueError(msg)
+ elifparse_strict=="warn":
+ warnings.warn(msg,UnitsWarning)
+ else:
+ raiseValueError(
+ "'parse_strict' must be 'warn', 'raise' or 'silent'"
+ )
+ returnUnrecognizedUnit(s)
+
+ elifisinstance(s,(int,float,np.floating,np.integer)):
+ returnCompositeUnit(s,[],[],_error_check=False)
+
+ elifisinstance(s,tuple):
+ from.structuredimportStructuredUnit
+
+ returnStructuredUnit(s)
+
+ elifsisNone:
+ raiseTypeError("None is not a valid Unit")
+
+ else:
+ raiseTypeError(f"{s} can not be converted to a Unit")
+
+
+classUnit(NamedUnit,metaclass=_UnitMetaClass):
+"""
+ The main unit class.
+
+ There are a number of different ways to construct a Unit, but
+ always returns a `UnitBase` instance. If the arguments refer to
+ an already-existing unit, that existing unit instance is returned,
+ rather than a new one.
+
+ - From a string::
+
+ Unit(s, format=None, parse_strict='silent')
+
+ Construct from a string representing a (possibly compound) unit.
+
+ The optional `format` keyword argument specifies the format the
+ string is in, by default ``"generic"``. For a description of
+ the available formats, see `astropy.units.format`.
+
+ The optional ``parse_strict`` keyword controls what happens when an
+ unrecognized unit string is passed in. It may be one of the following:
+
+ - ``'raise'``: (default) raise a ValueError exception.
+
+ - ``'warn'``: emit a Warning, and return an
+ `UnrecognizedUnit` instance.
+
+ - ``'silent'``: return an `UnrecognizedUnit` instance.
+
+ - From a number::
+
+ Unit(number)
+
+ Creates a dimensionless unit.
+
+ - From a `UnitBase` instance::
+
+ Unit(unit)
+
+ Returns the given unit unchanged.
+
+ - From no arguments::
+
+ Unit()
+
+ Returns the dimensionless unit.
+
+ - The last form, which creates a new `Unit` is described in detail
+ below.
+
+ See also: https://docs.astropy.org/en/stable/units/
+
+ Parameters
+ ----------
+ st : str or list of str
+ The name of the unit. If a list, the first element is the
+ canonical (short) name, and the rest of the elements are
+ aliases.
+
+ represents : UnitBase instance
+ The unit that this named unit represents.
+
+ doc : str, optional
+ A docstring describing the unit.
+
+ format : dict, optional
+ A mapping to format-specific representations of this unit.
+ For example, for the ``Ohm`` unit, it might be nice to have it
+ displayed as ``\\Omega`` by the ``latex`` formatter. In that
+ case, `format` argument should be set to::
+
+ {'latex': r'\\Omega'}
+
+ namespace : dict, optional
+ When provided, inject the unit (and all of its aliases) into
+ the given namespace.
+
+ Raises
+ ------
+ ValueError
+ If any of the given unit names are already in the registry.
+
+ ValueError
+ If any of the given unit names are not valid Python tokens.
+ """
+
+ def__init__(self,st,represents=None,doc=None,format=None,namespace=None):
+ represents=Unit(represents)
+ self._represents=represents
+
+ NamedUnit.__init__(self,st,namespace=namespace,doc=doc,format=format)
+
+ @property
+ defrepresents(self):
+"""The unit that this named unit represents."""
+ returnself._represents
+
+ defdecompose(self,bases=set()):
+ returnself._represents.decompose(bases=bases)
+
+ defis_unity(self):
+ returnself._represents.is_unity()
+
+ def__hash__(self):
+ ifself._hashisNone:
+ self._hash=hash((self.name,self._represents))
+ returnself._hash
+
+ @classmethod
+ def_from_physical_type_id(cls,physical_type_id):
+ # get string bases and powers from the ID tuple
+ bases=[cls(base)forbase,_inphysical_type_id]
+ powers=[powerfor_,powerinphysical_type_id]
+
+ iflen(physical_type_id)==1andpowers[0]==1:
+ unit=bases[0]
+ else:
+ unit=CompositeUnit(1,bases,powers,_error_check=False)
+
+ returnunit
+
+
+classPrefixUnit(Unit):
+"""
+ A unit that is simply a SI-prefixed version of another unit.
+
+ For example, ``mm`` is a `PrefixUnit` of ``.001 * m``.
+
+ The constructor is the same as for `Unit`.
+ """
+
+
+classCompositeUnit(UnitBase):
+"""
+ Create a composite unit using expressions of previously defined
+ units.
+
+ Direct use of this class is not recommended. Instead use the
+ factory function `Unit` and arithmetic operators to compose
+ units.
+
+ Parameters
+ ----------
+ scale : number
+ A scaling factor for the unit.
+
+ bases : sequence of `UnitBase`
+ A sequence of units this unit is composed of.
+
+ powers : sequence of numbers
+ A sequence of powers (in parallel with ``bases``) for each
+ of the base units.
+ """
+
+ _decomposed_cache=None
+
+ def__init__(
+ self,
+ scale,
+ bases,
+ powers,
+ decompose=False,
+ decompose_bases=set(),
+ _error_check=True,
+ ):
+ # There are many cases internal to astropy.units where we
+ # already know that all the bases are Unit objects, and the
+ # powers have been validated. In those cases, we can skip the
+ # error checking for performance reasons. When the private
+ # kwarg `_error_check` is False, the error checking is turned
+ # off.
+ if_error_check:
+ forbaseinbases:
+ ifnotisinstance(base,UnitBase):
+ raiseTypeError("bases must be sequence of UnitBase instances")
+ powers=[validate_power(p)forpinpowers]
+
+ ifnotdecomposeandlen(bases)==1andpowers[0]>=0:
+ # Short-cut; with one unit there's nothing to expand and gather,
+ # as that has happened already when creating the unit. But do only
+ # positive powers, since for negative powers we need to re-sort.
+ unit=bases[0]
+ power=powers[0]
+ ifpower==1:
+ scale*=unit.scale
+ self._bases=unit.bases
+ self._powers=unit.powers
+ elifpower==0:
+ self._bases=[]
+ self._powers=[]
+ else:
+ scale*=unit.scale**power
+ self._bases=unit.bases
+ self._powers=[
+ sanitize_power(operator.mul(*resolve_fractions(p,power)))
+ forpinunit.powers
+ ]
+
+ self._scale=sanitize_scale(scale)
+ else:
+ # Regular case: use inputs as preliminary scale, bases, and powers,
+ # then "expand and gather" identical bases, sanitize the scale, &c.
+ self._scale=scale
+ self._bases=bases
+ self._powers=powers
+ self._expand_and_gather(decompose=decompose,bases=decompose_bases)
+
+ def__repr__(self):
+ iflen(self._bases):
+ returnsuper().__repr__()
+ else:
+ ifself._scale!=1.0:
+ returnf"Unit(dimensionless with a scale of {self._scale})"
+ else:
+ return"Unit(dimensionless)"
+
+ @property
+ defscale(self):
+"""
+ Return the scale of the composite unit.
+ """
+ returnself._scale
+
+ @property
+ defbases(self):
+"""
+ Return the bases of the composite unit.
+ """
+ returnself._bases
+
+ @property
+ defpowers(self):
+"""
+ Return the powers of the composite unit.
+ """
+ returnself._powers
+
+ def_expand_and_gather(self,decompose=False,bases=set()):
+ defadd_unit(unit,power,scale):
+ ifbasesandunitnotinbases:
+ forbaseinbases:
+ try:
+ scale*=unit._to(base)**power
+ exceptUnitsError:
+ pass
+ else:
+ unit=base
+ break
+
+ ifunitinnew_parts:
+ a,b=resolve_fractions(new_parts[unit],power)
+ new_parts[unit]=a+b
+ else:
+ new_parts[unit]=power
+ returnscale
+
+ new_parts={}
+ scale=self._scale
+
+ forb,pinzip(self._bases,self._powers):
+ ifdecomposeandbnotinbases:
+ b=b.decompose(bases=bases)
+
+ ifisinstance(b,CompositeUnit):
+ scale*=b._scale**p
+ forb_sub,p_subinzip(b._bases,b._powers):
+ a,b=resolve_fractions(p_sub,p)
+ scale=add_unit(b_sub,a*b,scale)
+ else:
+ scale=add_unit(b,p,scale)
+
+ new_parts=[xforxinnew_parts.items()ifx[1]!=0]
+ new_parts.sort(key=lambdax:(-x[1],getattr(x[0],"name","")))
+
+ self._bases=[x[0]forxinnew_parts]
+ self._powers=[sanitize_power(x[1])forxinnew_parts]
+ self._scale=sanitize_scale(scale)
+
+ def__copy__(self):
+"""
+ For compatibility with python copy module.
+ """
+ returnCompositeUnit(self._scale,self._bases[:],self._powers[:])
+
+ defdecompose(self,bases=set()):
+ iflen(bases)==0andself._decomposed_cacheisnotNone:
+ returnself._decomposed_cache
+
+ forbaseinself.bases:
+ ifnotisinstance(base,IrreducibleUnit)or(
+ len(bases)andbasenotinbases
+ ):
+ break
+ else:
+ iflen(bases)==0:
+ self._decomposed_cache=self
+ returnself
+
+ x=CompositeUnit(
+ self.scale,self.bases,self.powers,decompose=True,decompose_bases=bases
+ )
+ iflen(bases)==0:
+ self._decomposed_cache=x
+ returnx
+
+ defis_unity(self):
+ unit=self.decompose()
+ returnlen(unit.bases)==0andunit.scale==1.0
+
+
+si_prefixes=[
+ (["Q"],["quetta"],1e30),
+ (["R"],["ronna"],1e27),
+ (["Y"],["yotta"],1e24),
+ (["Z"],["zetta"],1e21),
+ (["E"],["exa"],1e18),
+ (["P"],["peta"],1e15),
+ (["T"],["tera"],1e12),
+ (["G"],["giga"],1e9),
+ (["M"],["mega"],1e6),
+ (["k"],["kilo"],1e3),
+ (["h"],["hecto"],1e2),
+ (["da"],["deka","deca"],1e1),
+ (["d"],["deci"],1e-1),
+ (["c"],["centi"],1e-2),
+ (["m"],["milli"],1e-3),
+ (["u"],["micro"],1e-6),
+ (["n"],["nano"],1e-9),
+ (["p"],["pico"],1e-12),
+ (["f"],["femto"],1e-15),
+ (["a"],["atto"],1e-18),
+ (["z"],["zepto"],1e-21),
+ (["y"],["yocto"],1e-24),
+ (["r"],["ronto"],1e-27),
+ (["q"],["quecto"],1e-30),
+]
+
+
+binary_prefixes=[
+ (["Ki"],["kibi"],2**10),
+ (["Mi"],["mebi"],2**20),
+ (["Gi"],["gibi"],2**30),
+ (["Ti"],["tebi"],2**40),
+ (["Pi"],["pebi"],2**50),
+ (["Ei"],["exbi"],2**60),
+]
+
+
+def_add_prefixes(u,excludes=[],namespace=None,prefixes=False):
+"""
+ Set up all of the standard metric prefixes for a unit. This
+ function should not be used directly, but instead use the
+ `prefixes` kwarg on `def_unit`.
+
+ Parameters
+ ----------
+ excludes : list of str, optional
+ Any prefixes to exclude from creation to avoid namespace
+ collisions.
+
+ namespace : dict, optional
+ When provided, inject the unit (and all of its aliases) into
+ the given namespace dictionary.
+
+ prefixes : list, optional
+ When provided, it is a list of prefix definitions of the form:
+
+ (short_names, long_tables, factor)
+ """
+ ifprefixesisTrue:
+ prefixes=si_prefixes
+ elifprefixesisFalse:
+ prefixes=[]
+
+ forshort,full,factorinprefixes:
+ names=[]
+ format={}
+ forprefixinshort:
+ ifprefixinexcludes:
+ continue
+
+ foraliasinu.short_names:
+ names.append(prefix+alias)
+
+ # This is a hack to use Greek mu as a prefix
+ # for some formatters.
+ ifprefix=="u":
+ format["latex"]=r"\mu "+u.get_format_name("latex")
+ format["unicode"]="\N{MICRO SIGN}"+u.get_format_name("unicode")
+
+ forkey,valinu._format.items():
+ format.setdefault(key,prefix+val)
+
+ forprefixinfull:
+ ifprefixinexcludes:
+ continue
+
+ foraliasinu.long_names:
+ names.append(prefix+alias)
+
+ iflen(names):
+ PrefixUnit(
+ names,
+ CompositeUnit(factor,[u],[1],_error_check=False),
+ namespace=namespace,
+ format=format,
+ )
+
+
+defdef_unit(
+ s,
+ represents=None,
+ doc=None,
+ format=None,
+ prefixes=False,
+ exclude_prefixes=[],
+ namespace=None,
+):
+"""
+ Factory function for defining new units.
+
+ Parameters
+ ----------
+ s : str or list of str
+ The name of the unit. If a list, the first element is the
+ canonical (short) name, and the rest of the elements are
+ aliases.
+
+ represents : UnitBase instance, optional
+ The unit that this named unit represents. If not provided,
+ a new `IrreducibleUnit` is created.
+
+ doc : str, optional
+ A docstring describing the unit.
+
+ format : dict, optional
+ A mapping to format-specific representations of this unit.
+ For example, for the ``Ohm`` unit, it might be nice to
+ have it displayed as ``\\Omega`` by the ``latex``
+ formatter. In that case, `format` argument should be set
+ to::
+
+ {'latex': r'\\Omega'}
+
+ prefixes : bool or list, optional
+ When `True`, generate all of the SI prefixed versions of the
+ unit as well. For example, for a given unit ``m``, will
+ generate ``mm``, ``cm``, ``km``, etc. When a list, it is a list of
+ prefix definitions of the form:
+
+ (short_names, long_tables, factor)
+
+ Default is `False`. This function always returns the base
+ unit object, even if multiple scaled versions of the unit were
+ created.
+
+ exclude_prefixes : list of str, optional
+ If any of the SI prefixes need to be excluded, they may be
+ listed here. For example, ``Pa`` can be interpreted either as
+ "petaannum" or "Pascal". Therefore, when defining the
+ prefixes for ``a``, ``exclude_prefixes`` should be set to
+ ``["P"]``.
+
+ namespace : dict, optional
+ When provided, inject the unit (and all of its aliases and
+ prefixes), into the given namespace dictionary.
+
+ Returns
+ -------
+ unit : `~astropy.units.UnitBase`
+ The newly-defined unit, or a matching unit that was already
+ defined.
+ """
+ ifrepresentsisnotNone:
+ result=Unit(s,represents,namespace=namespace,doc=doc,format=format)
+ else:
+ result=IrreducibleUnit(s,namespace=namespace,doc=doc,format=format)
+
+ ifprefixes:
+ _add_prefixes(
+ result,excludes=exclude_prefixes,namespace=namespace,prefixes=prefixes
+ )
+ returnresult
+
+
+def_condition_arg(value):
+"""
+ Validate value is acceptable for conversion purposes.
+
+ Will convert into an array if not a scalar, and can be converted
+ into an array
+
+ Parameters
+ ----------
+ value : int or float value, or sequence of such values
+
+ Returns
+ -------
+ Scalar value or numpy array
+
+ Raises
+ ------
+ ValueError
+ If value is not as expected
+ """
+ ifisinstance(value,(np.ndarray,float,int,complex,np.void)):
+ returnvalue
+
+ avalue=np.array(value)
+ ifavalue.dtype.kindnotin["i","f","c"]:
+ raiseValueError(
+ "Value not scalar compatible or convertible to "
+ "an int, float, or complex array"
+ )
+ returnavalue
+
+
+defunit_scale_converter(val):
+"""Function that just multiplies the value by unity.
+
+ This is a separate function so it can be recognized and
+ discarded in unit conversion.
+ """
+ return1.0*_condition_arg(val)
+
+
+dimensionless_unscaled=CompositeUnit(1,[],[],_error_check=False)
+# Abbreviation of the above, see #1980
+one=dimensionless_unscaled
+
+# Maintain error in old location for backward compatibility
+# TODO: Is this still needed? Should there be a deprecation warning?
+unit_format.fits.UnitScaleError=UnitScaleError
+
+[docs]
+defangular_resolution(
+ events,
+ energy_bins,
+ energy_type="true",
+ quantile=ONE_SIGMA_QUANTILE,
+):
+"""
+ Calculate the angular resolution.
+
+ This implementation corresponds to the 68% containment of the angular
+ distance distribution.
+
+ Passing a list of quantiles results in all the quantiles being calculated.
+
+ Parameters
+ ----------
+ events : astropy.table.QTable
+ Astropy Table object containing the reconstructed events information.
+ energy_bins: numpy.ndarray(dtype=float, ndim=1)
+ Bin edges in energy.
+ energy_type: str
+ Either "true" or "reco" energy.
+ Default is "true".
+ quantile : list(float)
+ Which quantile(s) to use for the angular resolution,
+ by default, the containment of the 1-sigma region
+ of the normal distribution (~68%) is used.
+
+ Returns
+ -------
+ result : astropy.table.QTable
+ QTable containing the 68% containment of the angular
+ distance distribution per each reconstructed energy bin.
+ """
+ # create a table to make use of groupby operations
+ energy_key=f"{energy_type}_energy"
+ table=QTable(events[[energy_key,"theta"]])
+
+ bin_index,valid=calculate_bin_indices(table[energy_key],energy_bins)
+
+ result=QTable()
+ result[f"{energy_key}_low"]=energy_bins[:-1]
+ result[f"{energy_key}_high"]=energy_bins[1:]
+ result[f"{energy_key}_center"]=0.5*(energy_bins[:-1]+energy_bins[1:])
+ result["n_events"]=0
+
+ ifnotisinstance(quantile,Sequence):
+ quantile=[quantile]
+
+ keys=[f"angular_resolution_{value*100:.0f}"forvalueinquantile]
+
+ forkeyinkeys:
+ result[key]=u.Quantity(np.nan,table["theta"].unit)
+
+ # if we get an empty input (no selected events available)
+ # we return the table filled with NaNs
+ iflen(events)==0:
+ returnresult
+
+ # use groupby operations to calculate the percentile in each bin
+ by_bin=table[valid].group_by(bin_index[valid])
+ forbin_idx,groupinzip(by_bin.groups.keys,by_bin.groups):
+ result["n_events"][bin_idx]=len(group)
+ quantile_values=np.nanquantile(group["theta"],quantile)
+ forkey,valueinzip(keys,quantile_values):
+ result[key][bin_idx]=value
+
+ returnresult
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/benchmarks/energy_bias_resolution.html b/_modules/pyirf/benchmarks/energy_bias_resolution.html
new file mode 100644
index 000000000..93160119c
--- /dev/null
+++ b/_modules/pyirf/benchmarks/energy_bias_resolution.html
@@ -0,0 +1,290 @@
+
+
+
+
+
+
+
+ pyirf.benchmarks.energy_bias_resolution — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Source code for pyirf.benchmarks.energy_bias_resolution
+importnumpyasnp
+fromscipy.statsimportnorm
+fromastropy.tableimportQTable
+importastropy.unitsasu
+
+from..binningimportcalculate_bin_indices,UNDERFLOW_INDEX,OVERFLOW_INDEX
+from..compatimportCOPY_IF_NEEDED
+
+
+NORM_LOWER_SIGMA,NORM_UPPER_SIGMA=norm(0,1).cdf([-1,1])
+ONE_SIGMA_COVERAGE=NORM_UPPER_SIGMA-NORM_LOWER_SIGMA
+MEDIAN=0.5
+
+
+defenergy_resolution_absolute_68(rel_error):
+"""Calculate the energy resolution as the central 68% interval.
+
+ Utility function for pyirf.benchmarks.energy_bias_resolution
+
+ Parameters
+ ----------
+ rel_error : numpy.ndarray(dtype=float, ndim=1)
+ Array of float on which the quantile is calculated.
+
+ Returns
+ -------
+ resolution: numpy.ndarray(dtype=float, ndim=1)
+ Array containing the 68% intervals
+ """
+ returnnp.nanquantile(np.abs(rel_error),ONE_SIGMA_COVERAGE)
+
+
+definter_quantile_distance(rel_error):
+"""Calculate the energy resolution as the half of the 68% containment.
+
+ Percentile equivalent of the standard deviation.
+ Utility function for pyirf.benchmarks.energy_bias_resolution
+
+ Parameters
+ ----------
+ rel_error : numpy.ndarray(dtype=float, ndim=1)
+ Array of float on which the quantile is calculated.
+
+ Returns
+ -------
+ resolution: numpy.ndarray(dtype=float, ndim=1)
+ Array containing the resolution values.
+ """
+ upper_sigma=np.nanquantile(rel_error,NORM_UPPER_SIGMA)
+ lower_sigma=np.nanquantile(rel_error,NORM_LOWER_SIGMA)
+ resolution=0.5*(upper_sigma-lower_sigma)
+ returnresolution
+
+
+
+[docs]
+defenergy_bias_resolution(
+ events,
+ energy_bins,
+ energy_type="true",
+ bias_function=np.nanmedian,
+ resolution_function=inter_quantile_distance,
+):
+"""
+ Calculate bias and energy resolution.
+
+ Parameters
+ ----------
+ events: astropy.table.QTable
+ Astropy Table object containing the reconstructed events information.
+ energy_bins: numpy.ndarray(dtype=float, ndim=1)
+ Bin edges in energy.
+ energy_type: str
+ Either "true" or "reco" energy.
+ Default is "true".
+ bias_function: callable
+ Function used to calculate the energy bias
+ resolution_function: callable
+ Function used to calculate the energy resolution
+
+ Returns
+ -------
+ result : astropy.table.QTable
+ QTable containing the energy bias and resolution
+ per each bin in true energy.
+ """
+
+ # create a table to make use of groupby operations
+ table=QTable(events[["true_energy","reco_energy"]],copy=COPY_IF_NEEDED)
+ table["rel_error"]=(events["reco_energy"]/events["true_energy"]).to_value(
+ u.one
+ )-1
+
+ energy_key=f"{energy_type}_energy"
+
+ result=QTable()
+ result[f"{energy_key}_low"]=energy_bins[:-1]
+ result[f"{energy_key}_high"]=energy_bins[1:]
+ result[f"{energy_key}_center"]=0.5*(energy_bins[:-1]+energy_bins[1:])
+
+ result["n_events"]=0
+ result["bias"]=np.nan
+ result["resolution"]=np.nan
+
+ ifnotlen(events):
+ # if we get an empty input (no selected events available)
+ # we return the table filled with NaNs
+ returnresult
+
+ # use groupby operations to calculate the percentile in each bin
+ bin_index,valid=calculate_bin_indices(table[energy_key],energy_bins)
+ by_bin=table.group_by(bin_index)
+
+ # use groupby operations to calculate the percentile in each bin
+ by_bin=table[valid].group_by(bin_index[valid])
+ forbin_idx,groupinzip(by_bin.groups.keys,by_bin.groups):
+ result["n_events"][bin_idx]=len(group)
+ result["bias"][bin_idx]=bias_function(group["rel_error"])
+ result["resolution"][bin_idx]=resolution_function(group["rel_error"])
+ returnresult
+
+
+
+
+[docs]
+defenergy_bias_resolution_from_energy_dispersion(
+ energy_dispersion,
+ migration_bins,
+):
+"""
+ Calculate bias and energy resolution.
+
+ Parameters
+ ----------
+ edisp:
+ Energy dispersion matrix of shape
+ (n_energy_bins, n_migra_bins, n_source_offset_bins)
+ migration_bins: numpy.ndarray
+ Bin edges for the relative energy migration (``reco_energy / true_energy``)
+ """
+
+ bin_width=np.diff(migration_bins)
+ cdf=np.cumsum(energy_dispersion*bin_width[np.newaxis,:,np.newaxis],axis=1)
+
+ n_energy_bins,_,n_fov_bins=energy_dispersion.shape
+
+ bias=np.full((n_energy_bins,n_fov_bins),np.nan)
+ resolution=np.full((n_energy_bins,n_fov_bins),np.nan)
+
+ forenergy_bininrange(n_energy_bins):
+ forfov_bininrange(n_fov_bins):
+ ifnp.count_nonzero(cdf[energy_bin,:,fov_bin])==0:
+ continue
+
+ low,median,high=np.interp(
+ [NORM_LOWER_SIGMA,MEDIAN,NORM_UPPER_SIGMA],
+ cdf[energy_bin,:,fov_bin],
+ migration_bins[1:],# cdf is defined at upper bin edge
+ )
+ bias[energy_bin,fov_bin]=median-1
+ resolution[energy_bin,fov_bin]=0.5*(high-low)
+
+ returnbias,resolution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/binning.html b/_modules/pyirf/binning.html
new file mode 100644
index 000000000..f313ad794
--- /dev/null
+++ b/_modules/pyirf/binning.html
@@ -0,0 +1,450 @@
+
+
+
+
+
+
+
+ pyirf.binning — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+Utility functions for binning
+"""
+
+importnumpyasnp
+fromscipy.interpolateimportinterp1d
+importastropy.unitsasu
+fromastropy.tableimportQTable
+
+from.compatimportCOPY_IF_NEEDED
+
+
+#: Index returned by `calculate_bin_indices` for underflown values
+UNDERFLOW_INDEX=np.iinfo(np.int64).min
+#: Index returned by `calculate_bin_indices` for overflown values
+OVERFLOW_INDEX=np.iinfo(np.int64).max
+
+
+
+[docs]
+defjoin_bin_lo_hi(bin_lo,bin_hi):
+"""
+ Function joins bins into lo and hi part,
+ e.g. [0, 1, 2] and [1, 2, 4] into [0, 1, 2, 4]
+ It works on multidimentional arrays as long as the binning is in the last axis
+
+ Parameters
+ ----------
+ bin_lo: np.array or u.Quantity
+ Lo bin edges array
+ bin_hi: np.array or u.Quantity
+ Hi bin edges array
+
+ Returns
+ -------
+ bins: np.array of u.Quantity
+ The joint bins
+ """
+
+ ifnp.allclose(bin_lo[...,1:],bin_hi[...,:-1],rtol=1.0e-5):
+ last_axis=len(bin_lo.shape)-1
+ bins=np.concatenate((bin_lo,bin_hi[...,-1:]),axis=last_axis)
+ returnbins
+ else:
+ raiseValueError("Not matching bin edges")
+
+
+
+
+[docs]
+defsplit_bin_lo_hi(bins):
+"""
+ Inverted function to join_bin_hi_lo,
+ e.g. it splits [0, 1, 2, 4] into [0, 1, 2] and [1, 2, 4]
+
+ Parameters
+ ----------
+ bins: np.array of u.Quantity
+ The joint bins
+
+ Returns
+ -------
+ bin_lo: np.array or u.Quantity
+ Lo bin edges array
+ bin_hi: np.array or u.Quantity
+ Hi bin edges array
+ """
+ bin_lo=bins[...,:-1]
+ bin_hi=bins[...,1:]
+ returnbin_lo,bin_hi
+
+
+
+
+[docs]
+defadd_overflow_bins(bins,positive=True):
+"""
+ Add under and overflow bins to a bin array.
+
+ Parameters
+ ----------
+ bins: np.array or u.Quantity
+ Bin edges array
+ positive: bool
+ If True, the underflow array will start at 0, if not at ``-np.inf``
+ """
+ lower=0ifpositiveelse-np.inf
+ upper=np.inf
+
+ ifhasattr(bins,"unit"):
+ lower*=bins.unit
+ upper*=bins.unit
+
+ ifbins[0]>lower:
+ bins=np.append(lower,bins)
+
+ ifbins[-1]<upper:
+ bins=np.append(bins,upper)
+
+ returnbins
+
+
+
+
+[docs]
+@u.quantity_input(e_min=u.TeV,e_max=u.TeV)
+defcreate_bins_per_decade(e_min,e_max,bins_per_decade=5):
+"""
+ Create a bin array with bins equally spaced in logarithmic energy
+ with ``bins_per_decade`` bins per decade.
+
+ Parameters
+ ----------
+ e_min: u.Quantity[energy]
+ Minimum energy, inclusive
+ e_max: u.Quantity[energy]
+ Maximum energy, non-inclusive
+ If the endpoint exactly matches the ``n_bins_per_decade`` requirement,
+ it will be included.
+ n_bins_per_decade: int
+ number of bins per decade
+
+ Returns
+ -------
+ bins: u.Quantity[energy]
+ The created bin array, will have units of e_min
+
+ """
+ unit=e_min.unit
+ log_lower=np.log10(e_min.to_value(unit))
+ log_upper=np.log10(e_max.to_value(unit))
+
+ step=1/bins_per_decade
+ # include endpoint if reasonably close
+ eps=step/10000
+ bins=10**np.arange(log_lower,log_upper+eps,step)
+ returnu.Quantity(bins,e_min.unit,copy=COPY_IF_NEEDED)
+
+
+
+
+[docs]
+defcalculate_bin_indices(data,bins):
+"""
+ Calculate bin indices of inidividula entries of the given data array using
+ the supplied binning. Underflow will be indicated by `UNDERFLOW_INDEX` and
+ overflow by `OVERFLOW_INDEX`.
+
+ If the bins already include underflow / overflow bins, e.g.
+ `bins[0] = -np.inf` and `bins[-1] = np.inf`, using the result of this
+ function will always be a valid index into the resulting histogram.
+
+
+ Parameters
+ ----------
+ data: ``~np.ndarray`` or ``~astropy.units.Quantity``
+ Array with the data
+
+ bins: ``~np.ndarray`` or ``~astropy.units.Quantity``
+ Array or Quantity of bin edges. Must have the same unit as ``data`` if a Quantity.
+
+
+ Returns
+ -------
+ bin_index: np.ndarray[int]
+ Indices of the histogram bin the values in data belong to.
+ Under- and overflown values will have values of `UNDERFLOW_INDEX`
+ and `OVERFLOW_INDEX` respectively.
+
+ valid: np.ndarray[bool]
+ Boolean mask indicating if a given value belongs into one of the defined bins.
+ False indicates that an entry fell into the over- or underflow bins.
+ """
+
+ ifhasattr(data,"unit"):
+ ifnothasattr(bins,"unit"):
+ raiseTypeError(f"If ``data`` is a Quantity, so must ``bins``, got {bins}")
+ unit=data.unit
+ data=data.to_value(unit)
+ bins=bins.to_value(unit)
+
+ n_bins=len(bins)-1
+ idx=np.digitize(data,bins)-1
+
+ underflow=idx<0
+ overflow=idx>=n_bins
+ idx[underflow]=UNDERFLOW_INDEX
+ idx[overflow]=OVERFLOW_INDEX
+ valid=~underflow&~overflow
+ returnidx,valid
+
+
+
+
+[docs]
+defcreate_histogram_table(events,bins,key="reco_energy"):
+"""
+ Histogram a variable from events data into an astropy table.
+
+ Parameters
+ ----------
+ events : ``astropy.QTable``
+ Astropy Table object containing the reconstructed events information.
+ bins: ``~np.ndarray`` or ``~astropy.units.Quantity``
+ Array or Quantity of bin edges.
+ It must have the same units as ``data`` if a Quantity.
+ key : ``string``
+ Variable to histogram from the events table.
+
+ Returns
+ -------
+ hist: ``astropy.QTable``
+ Astropy table containg the histogram.
+ """
+ hist=QTable()
+ hist[key+"_low"]=bins[:-1]
+ hist[key+"_high"]=bins[1:]
+ hist[key+"_center"]=0.5*(hist[key+"_low"]+hist[key+"_high"])
+ hist["n"],_=np.histogram(events[key],bins)
+
+ # also calculate weighted number of events
+ if"weight"inevents.colnames:
+ hist["n_weighted"],_=np.histogram(
+ events[key],bins,weights=events["weight"]
+ )
+ else:
+ hist["n_weighted"]=hist["n"]
+
+ # create counts per particle type, only works if there is at least 1 event
+ if"particle_type"inevents.colnamesandlen(events)>0:
+ by_particle=events.group_by("particle_type")
+
+ forgroup_key,groupinzip(by_particle.groups.keys,by_particle.groups):
+ particle=group_key["particle_type"]
+
+ hist["n_"+particle],_=np.histogram(group[key],bins)
+
+ # also calculate weighted number of events
+ col="n_"+particle
+ if"weight"inevents.colnames:
+ hist[col+"_weighted"],_=np.histogram(
+ group[key],bins,weights=group["weight"]
+ )
+ else:
+ hist[col+"_weighted"]=hist[col]
+
+ returnhist
+
+
+
+
+[docs]
+defresample_histogram1d(data,old_edges,new_edges,axis=0):
+"""
+ Rebinning of a histogram by interpolation along a given axis.
+
+ Parameters
+ ----------
+ data : ``numpy.ndarray`` or ``astropy.units.Quantity``
+ Histogram.
+ old_edges : ``numpy.array`` or ``astropy.units.Quantity``
+ Binning used to calculate ``data``.
+ ``len(old_edges) - 1`` needs to equal the length of ``data``
+ along interpolation axis (``axis``).
+ If quantity, needs to be compatible to ``new_edges``.
+ new_edges : ``numpy.array`` or ``astropy.units.Quantity``
+ Binning of new histogram.
+ If quantity, needs to be compatible to ``old_edges``.
+ axis : int
+ Interpolation axis.
+
+ Returns
+ -------
+ ``numpy.ndarray`` or ``astropy.units.Quantity``
+ Interpolated histogram with dimension according to ``data`` and ``new_edges``.
+ If ``data`` is a quantity, this has the same unit.
+ """
+
+ data_unit=None
+ ifisinstance(data,u.Quantity):
+ data_unit=data.unit
+ data=data.to_value(data_unit)
+
+ over_underflow_bin_width=old_edges[-2]-old_edges[1]
+ old_edges=u.Quantity(
+ np.nan_to_num(
+ old_edges,
+ posinf=old_edges[-2]+over_underflow_bin_width,
+ neginf=old_edges[1]-over_underflow_bin_width,
+ )
+ )
+
+ new_edges=u.Quantity(np.nan_to_num(new_edges))
+
+ old_edges=old_edges.to(new_edges.unit)
+
+ cumsum=np.insert(data.cumsum(axis=axis),0,0,axis=axis)
+
+ norm=data.sum(axis=axis,keepdims=True)
+ norm[norm==0]=1
+ cumsum/=norm
+
+ f_integral=interp1d(
+ old_edges,
+ cumsum,
+ bounds_error=False,
+ fill_value=(0,1),
+ kind="quadratic",
+ axis=axis,
+ )
+
+ values=np.diff(f_integral(new_edges),axis=axis)*norm
+ ifdata_unit:
+ values=u.Quantity(values,unit=data_unit)
+
+ returnvalues
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/cut_optimization.html b/_modules/pyirf/cut_optimization.html
new file mode 100644
index 000000000..b8476d3ec
--- /dev/null
+++ b/_modules/pyirf/cut_optimization.html
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+ pyirf.cut_optimization — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+defoptimize_gh_cut(
+ signal,
+ background,
+ reco_energy_bins,
+ gh_cut_efficiencies,
+ theta_cuts,
+ op=operator.ge,
+ fov_offset_min=0*u.deg,
+ fov_offset_max=1*u.deg,
+ alpha=1.0,
+ progress=True,
+ **kwargs
+):
+"""
+ Optimize the gh-score cut in every energy bin of reconstructed energy
+ for best sensitivity.
+
+ This procedure is EventDisplay-like, since it only applies a
+ pre-computed theta cut and then optimizes only the gamma/hadron separation
+ cut.
+
+ Parameters
+ ----------
+ signal: astropy.table.QTable
+ event list of simulated signal events.
+ Required columns are `theta`, `reco_energy`, 'weight', `gh_score`
+ No directional (theta) or gamma/hadron cut should already be applied.
+ background: astropy.table.QTable
+ event list of simulated background events.
+ Required columns are `reco_source_fov_offset`, `reco_energy`,
+ 'weight', `gh_score`.
+ No directional (theta) or gamma/hadron cut should already be applied.
+ reco_energy_bins: astropy.units.Quantity[energy]
+ Bins in reconstructed energy to use for sensitivity computation
+ gh_cut_efficiencies: np.ndarray[float, ndim=1]
+ The cut efficiencies to scan for best sensitivity.
+ theta_cuts: astropy.table.QTable
+ cut definition of the energy dependent theta cut,
+ e.g. as created by ``calculate_percentile_cut``
+ op: comparison function with signature f(a, b) -> bool
+ The comparison function to use for the gamma hadron score.
+ Returning true means an event passes the cut, so is not discarded.
+ E.g. for gammaness-like score, use `operator.ge` (>=) and for a
+ hadroness-like score use `operator.le` (<=).
+ fov_offset_min: astropy.units.Quantity[angle]
+ Minimum distance from the fov center for background events to be taken into account
+ fov_offset_max: astropy.units.Quantity[angle]
+ Maximum distance from the fov center for background events to be taken into account
+ alpha: float
+ Size ratio of off region / on region. Will be used to
+ scale the background rate.
+ progress: bool
+ If True, show a progress bar during cut optimization
+ **kwargs are passed to ``calculate_sensitivity``
+ """
+
+ # we apply each cut for all reco_energy_bins globally, calculate the
+ # sensitivity and then lookup the best sensitivity for each
+ # bin independently
+
+ signal_selected_theta=evaluate_binned_cut(
+ signal['theta'],signal['reco_energy'],theta_cuts,
+ op=operator.le,
+ )
+
+ sensitivities=[]
+ gh_cuts=[]
+ forefficiencyintqdm(gh_cut_efficiencies,disable=notprogress):
+
+ # calculate necessary percentile needed for
+ # ``calculate_percentile_cut`` with the correct efficiency.
+ # Depends on the operator, since we need to invert the
+ # efficiency if we compare using >=, since percentile is
+ # defines as <=.
+ ifop(-1,1):# if operator behaves like "<=", "<" etc:
+ percentile=100*efficiency
+ fill_value=signal['gh_score'].min()
+ else:# operator behaves like ">=", ">"
+ percentile=100*(1-efficiency)
+ fill_value=signal['gh_score'].max()
+
+ gh_cut=calculate_percentile_cut(
+ signal['gh_score'],signal['reco_energy'],
+ bins=reco_energy_bins,
+ fill_value=fill_value,percentile=percentile,
+ )
+ gh_cuts.append(gh_cut)
+
+ # apply the current cut
+ signal_selected=evaluate_binned_cut(
+ signal["gh_score"],signal["reco_energy"],gh_cut,op,
+ )&signal_selected_theta
+
+ background_selected=evaluate_binned_cut(
+ background["gh_score"],background["reco_energy"],gh_cut,op,
+ )
+
+ # create the histograms
+ signal_hist=create_histogram_table(
+ signal[signal_selected],reco_energy_bins,"reco_energy"
+ )
+
+ background_hist=estimate_background(
+ events=background[background_selected],
+ reco_energy_bins=reco_energy_bins,
+ theta_cuts=theta_cuts,
+ alpha=alpha,
+ fov_offset_min=fov_offset_min,
+ fov_offset_max=fov_offset_max,
+ )
+
+ sensitivity=calculate_sensitivity(
+ signal_hist,background_hist,alpha=alpha,
+ **kwargs,
+ )
+ sensitivities.append(sensitivity)
+
+ best_cut_table=QTable()
+ best_cut_table["low"]=reco_energy_bins[0:-1]
+ best_cut_table["center"]=bin_center(reco_energy_bins)
+ best_cut_table["high"]=reco_energy_bins[1:]
+ best_cut_table["cut"]=np.nan
+
+ best_sensitivity=sensitivities[0].copy()
+ forbin_idinrange(len(reco_energy_bins)-1):
+ sensitivities_bin=[s["relative_sensitivity"][bin_id]forsinsensitivities]
+
+ ifnotnp.all(np.isnan(sensitivities_bin)):
+ # nanargmin won't return the index of nan entries
+ best=np.nanargmin(sensitivities_bin)
+ else:
+ # if all are invalid, just use the first one
+ best=0
+
+ best_sensitivity[bin_id]=sensitivities[best][bin_id]
+ best_cut_table["cut"][bin_id]=gh_cuts[best]["cut"][bin_id]
+
+ returnbest_sensitivity,best_cut_table
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/cuts.html b/_modules/pyirf/cuts.html
new file mode 100644
index 000000000..7068912aa
--- /dev/null
+++ b/_modules/pyirf/cuts.html
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+ pyirf.cuts — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+defcalculate_percentile_cut(
+ values,
+ bin_values,
+ bins,
+ fill_value,
+ percentile=68,
+ min_value=None,
+ max_value=None,
+ smoothing=None,
+ min_events=10,
+):
+"""
+ Calculate cuts as the percentile of a given quantity in bins of another
+ quantity.
+
+ Parameters
+ ----------
+ values: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
+ The values for which the cut should be calculated
+ bin_values: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
+ The values used to sort the ``values`` into bins
+ edges: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
+ Bin edges
+ fill_value: float or quantity
+ Value for bins with less than ``min_events``,
+ must have same unit as values
+ percentile: float
+ The percentile to calculate in each bin as a percentage,
+ i.e. 0 <= percentile <= 100.
+ min_value: float or quantity or None
+ If given, cuts smaller than this value are replaced with ``min_value``
+ max_value: float or quantity or None
+ If given, cuts larger than this value are replaced with ``max_value``
+ smoothing: float or None
+ If given, apply a gaussian filter of width ``sigma`` in terms
+ of bins.
+ min_events: int
+ Bins with less events than this number are replaced with ``fill_value``
+ """
+ # create a table to make use of groupby operations
+ # we use a normal table here to avoid astropy/astropy#13840
+ table=Table({"values":values},copy=COPY_IF_NEEDED)
+ unit=table["values"].unit
+
+ # make sure units match
+ ifunitisnotNone:
+ fill_value=u.Quantity(fill_value).to(unit)
+
+ ifmin_valueisnotNone:
+ min_value=u.Quantity(min_value).to_value(unit)
+
+ ifmax_valueisnotNone:
+ max_value=u.Quantity(max_value).to_value(unit)
+
+ bin_index,valid=calculate_bin_indices(bin_values,bins)
+ by_bin=table[valid].group_by(bin_index[valid])
+
+ cut_table=QTable()
+ cut_table["low"]=bins[:-1]
+ cut_table["high"]=bins[1:]
+ cut_table["center"]=bin_center(bins)
+ cut_table["n_events"]=0
+ cut_table["cut"]=np.asanyarray(fill_value,values.dtype)
+
+ forbin_idx,groupinzip(by_bin.groups.keys,by_bin.groups):
+ # replace bins with too few events with fill_value
+ n_events=len(group)
+ cut_table["n_events"][bin_idx]=n_events
+
+ ifn_events<min_events:
+ cut_table["cut"][bin_idx]=fill_value
+ else:
+ value=np.nanpercentile(group["values"],percentile)
+ ifmin_valueisnotNoneormax_valueisnotNone:
+ value=np.clip(value,min_value,max_value)
+
+ cut_table["cut"].value[bin_idx]=value
+
+ ifsmoothingisnotNone:
+ cut_table["cut"].value[:]=gaussian_filter1d(
+ cut_table["cut"].value,
+ smoothing,
+ mode="nearest",
+ )
+
+ returncut_table
+
+
+
+
+[docs]
+defevaluate_binned_cut(values,bin_values,cut_table,op):
+"""
+ Evaluate a binned cut as defined in cut_table on given events.
+
+ Events with bin_values outside the bin edges defined in cut table
+ will be set to False.
+
+ Parameters
+ ----------
+ values: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
+ The values on which the cut should be evaluated
+ bin_values: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
+ The values used to sort the ``values`` into bins
+ cut_table: ``~astropy.table.Table``
+ A table describing the binned cuts, e.g. as created by
+ ``~pyirf.cuts.calculate_percentile_cut``.
+ Required columns:
+ - `low`: lower edges of the bins
+ - `high`: upper edges of the bins,
+ - `cut`: cut value
+ op: callable(a, b) -> bool
+ A function taking two arguments, comparing element-wise and
+ returning an array of booleans.
+ Must support vectorized application.
+
+
+ Returns
+ -------
+ result: np.ndarray[bool]
+ A mask for each entry in ``values`` indicating if the event
+ passes the bin specific cut given in cut table.
+ """
+ ifnotisinstance(cut_table,QTable):
+ raiseValueError("cut_table needs to be an astropy.table.QTable")
+
+ bins=np.append(cut_table["low"],cut_table["high"][-1])
+ bin_index,valid=calculate_bin_indices(bin_values,bins)
+
+ result=np.zeros(len(values),dtype=bool)
+ result[valid]=op(values[valid],cut_table["cut"][bin_index[valid]])
+ returnresult
+
+
+
+
+[docs]
+defcompare_irf_cuts(cuts):
+"""
+ checks if the same cuts have been applied in all of them
+
+ Parameters
+ ----------
+ cuts: list of QTables
+ list of cuts each entry in the list correspond to one set of IRFs
+ Returns
+ -------
+ match: Boolean
+ if the cuts are the same in all the files
+ """
+ foriinrange(len(cuts)-1):
+ if(cuts[i]!=cuts[i+1]).any():
+ returnFalse
+ returnTrue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/gammapy.html b/_modules/pyirf/gammapy.html
new file mode 100644
index 000000000..adf219181
--- /dev/null
+++ b/_modules/pyirf/gammapy.html
@@ -0,0 +1,273 @@
+
+
+
+
+
+
+
+ pyirf.gammapy — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+classParametrizedExtrapolator(BaseExtrapolator):
+"""
+ Base class for all extrapolators used with IRF components that can be
+ treated independently, e.g. parametrized ones like 3Gauss
+ but also AEff. Derived from pyirf.interpolation.BaseExtrapolator
+ """
+
+ def__init__(self,grid_points,params):
+"""ParametrizedExtrapolator
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist
+ params: np.ndarray, shape=(n_points, ..., n_params)
+ Corresponding parameter values at each point in grid_points.
+ First dimesion has to correspond to number of grid_points
+
+ Note
+ ----
+ Also calls pyirf.interpolation.BaseExtrapolators.__init__
+ """
+ super().__init__(grid_points)
+
+ self.params=params
+
+ ifself.params.ndim==1:
+ self.params=self.params[...,np.newaxis]
+
+
+
+
+[docs]
+classDiscretePDFExtrapolator(BaseExtrapolator):
+"""
+ Base class for all extrapolators used with binned IRF components like EDisp.
+ Derived from pyirf.interpolation.BaseExtrapolator
+ """
+
+ def__init__(
+ self,grid_points,bin_edges,binned_pdf,normalization=PDFNormalization.AREA
+ ):
+"""DiscretePDFExtrapolator
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Edges of the data binning
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Content of each bin in bin_edges for
+ each point in grid_points. First dimesion has to correspond to number
+ of grid_points, last dimension has to correspond to number of bins for
+ the quantity that should be extrapolated (e.g. the Migra axis for EDisp)
+ normalization: PDFNormalization
+ How the PDF is normalized
+
+ Note
+ ----
+ Also calls pyirf.interpolation.BaseExtrapolators.__init__
+ """
+ super().__init__(grid_points)
+
+ self.normalization=normalization
+ self.bin_edges=bin_edges
+ self.bin_mids=bin_center(self.bin_edges)
+ self.binned_pdf=binned_pdf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/interpolation/base_interpolators.html b/_modules/pyirf/interpolation/base_interpolators.html
new file mode 100644
index 000000000..3da8d7cc8
--- /dev/null
+++ b/_modules/pyirf/interpolation/base_interpolators.html
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+ pyirf.interpolation.base_interpolators — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+classPDFNormalization(enum.Enum):
+"""How a discrete PDF is normalized"""
+
+ #: PDF is normalized to a "normal" area integral of 1
+ AREA=enum.auto()
+ #: PDF is normalized to 1 over the solid angle integral where the bin
+ #: edges represent the opening angles of cones in radian.
+ CONE_SOLID_ANGLE=enum.auto()
+
+
+
+
+[docs]
+classBaseInterpolator(metaclass=ABCMeta):
+"""
+ Base class for all interpolators, only knowing grid-points,
+ providing a common __call__-interface.
+ """
+
+ def__init__(self,grid_points):
+"""BaseInterpolator
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims):
+ Grid points at which interpolation templates exist
+
+ """
+ self.grid_points=grid_points
+ ifself.grid_points.ndim==1:
+ self.grid_points=self.grid_points.reshape(*self.grid_points.shape,1)
+ self.n_points=self.grid_points.shape[0]
+ self.grid_dim=self.grid_points.shape[1]
+
+
+[docs]
+ @abstractmethod
+ definterpolate(self,target_point):
+"""Overridable function for the actual interpolation code"""
+
+
+
+[docs]
+ def__call__(self,target_point):
+"""Providing a common __call__ interface
+
+ Parameters
+ ----------
+ target_point: np.ndarray, shape=(1, n_dims)
+ Target for inter-/extrapolation
+ When target_point is outside of the grids convex hull but extrapolator is None
+
+ Returns
+ -------
+ Interpolated result.
+ """
+ returnself.interpolate(target_point=target_point)
+
+
+
+
+
+[docs]
+classParametrizedInterpolator(BaseInterpolator):
+"""
+ Base class for all interpolators used with IRF components that can be
+ independently interpolated, e.g. parametrized ones like 3Gauss
+ but also AEff. Derived from pyirf.interpolation.BaseInterpolator
+ """
+
+ def__init__(self,grid_points,params):
+"""ParametrizedInterpolator
+
+ Parameters
+ ----------
+ grid_points, np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist
+ params: np.ndarray, shape=(n_points, ..., n_params)
+ Corresponding parameter values at each point in grid_points.
+ First dimesion has to correspond to number of grid_points
+
+ Note
+ ----
+ Also calls pyirf.interpolation.BaseInterpolators.__init__
+ """
+ super().__init__(grid_points)
+
+ self.params=params
+
+ ifself.params.ndim==1:
+ self.params=self.params[...,np.newaxis]
+
+
+
+
+[docs]
+classDiscretePDFInterpolator(BaseInterpolator):
+"""
+ Base class for all interpolators used with binned IRF components like EDisp.
+ Derived from pyirf.interpolation.BaseInterpolator
+ """
+
+ def__init__(
+ self,grid_points,bin_edges,binned_pdf,normalization=PDFNormalization.AREA
+ ):
+"""DiscretePDFInterpolator
+
+ Parameters
+ ----------
+ grid_points : np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist
+ bin_edges : np.ndarray, shape=(n_bins+1)
+ Edges of the data binning
+ binned_pdf : np.ndarray, shape=(n_points, ..., n_bins)
+ Content of each bin in bin_edges for
+ each point in grid_points. First dimesion has to correspond to number
+ of grid_points, last dimension has to correspond to number of bins for
+ the quantity that should be interpolated (e.g. the Migra axis for EDisp)
+ normalization : PDFNormalization
+ How the PDF is normalized
+
+ Note
+ ----
+ Also calls pyirf.interpolation.BaseInterpolators.__init__
+ """
+ super().__init__(grid_points)
+
+ self.bin_edges=bin_edges
+ self.bin_mids=bin_center(self.bin_edges)
+ self.binned_pdf=binned_pdf
+ self.normalization=normalization
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/interpolation/component_estimators.html b/_modules/pyirf/interpolation/component_estimators.html
new file mode 100644
index 000000000..2e979f9ee
--- /dev/null
+++ b/_modules/pyirf/interpolation/component_estimators.html
@@ -0,0 +1,1023 @@
+
+
+
+
+
+
+
+ pyirf.interpolation.component_estimators — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+classBaseComponentEstimator:
+"""
+ Base class for all Estimators working on specific IRF components.
+
+ While usable, it is encouraged to use the actual class for the respective IRF
+ component as it ensures further checks and if necessary e.g. unit handling.
+ """
+
+ def__init__(self,grid_points):
+"""
+ Base __init__, doing sanity checks on the grid, building a
+ triangulated version of the grid and instantiating inter- and extrapolator.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims):
+
+ Raises
+ ------
+ TypeError:
+ When grid_points is not a np.ndarray of float compatible values
+ TypeError:
+ When grid_point has dtype object
+ ValueError:
+ When there are too few points in grid_points to span a volume
+ in the grid dimension.
+ """
+ ifnotisinstance(grid_points,np.ndarray):
+ raiseTypeError("Input grid_points is not a numpy array.")
+ ifgrid_points.dtype=="O":
+ raiseTypeError("Input grid_points array cannot be of dtype object.")
+ ifnotnp.can_cast(grid_points.dtype,np.float128):
+ raiseTypeError("Input grid_points dtype incompatible with float.")
+
+ self.grid_points=grid_points
+ ifself.grid_points.ndim==1:
+ self.grid_points=self.grid_points.reshape(*self.grid_points.shape,1)
+ self.n_points=self.grid_points.shape[0]
+ self.grid_dim=self.grid_points.shape[1]
+
+ # Check, if number of grid point theoretically suffices to span a volume
+ # in the dimension indicated by grid
+ ifself.n_points<self.grid_dim+1:
+ raiseValueError(
+ f"To few points for grid dimension, grid-dim is {self.grid_dim},"
+ f" while there are only {self.n_points}. At least {self.grid_dim+1}"
+ f" points needed to span a volume in {self.grid_dim} dimensions."
+ )
+
+ # Build triangulation to check if target is inside of the grid for
+ # more then 1 dimension
+ ifself.grid_dim>1:
+ self.triangulation=Delaunay(self.grid_points)
+
+ def_target_in_grid(self,target_point):
+"""Check whether target_point lies within grids convex hull, uses
+ simple comparison for 1D and Delaunay triangulation for >1D."""
+ ifself.grid_dim==1:
+ return(target_point>=self.grid_points.min())and(
+ target_point<=self.grid_points.max()
+ )
+ else:
+ # Delaunay.find_simplex() returns -1 for points outside the grids convex hull
+ simplex_ind=self.triangulation.find_simplex(target_point)
+ returnsimplex_ind>=0
+
+
+[docs]
+ def__call__(self,target_point):
+"""Inter-/ Extrapolation as needed and sanity checking of
+ the target point
+
+ Parameters
+ ----------
+ target_point: np.ndarray, shape=(1, n_dims)
+ Target for inter-/extrapolation
+
+ Raises
+ ------
+ TypeError:
+ When target_point is not an np.ndarray
+ ValueError:
+ When more then one target_point is given
+ ValueError:
+ When target_point and grid_points have miss-matching dimensions
+ ValueError:
+ When target_point is outside of the grids convex hull but extrapolator is None
+ Warning:
+ When target_points need extrapolation
+
+ Returns
+ -------
+ Interpolated or, if necessary extrapolated, result.
+ """
+ ifnotisinstance(target_point,np.ndarray):
+ raiseTypeError("Target point is not a numpy array.")
+
+ iftarget_point.ndim==1:
+ target_point=target_point.reshape(1,*target_point.shape)
+ eliftarget_point.shape[0]!=1:
+ raiseValueError("Only one target_point per call supported.")
+
+ iftarget_point.shape[1]!=self.grid_dim:
+ raiseValueError(
+ "Mismatch between target-point and grid dimension."
+ f" Grid has dimension {self.grid_dim}, target has dimension"
+ f" {target_point.shape[1]}."
+ )
+
+ ifself._target_in_grid(target_point):
+ returnself.interpolator(target_point)
+ elifself.extrapolatorisnotNone:
+ warnings.warn(f"Target point {target_point} has to be extrapolated.")
+ returnself.extrapolator(target_point)
+ else:
+ raiseValueError(
+ "Target point outside grids convex hull and no extrapolator given."
+ )
+
+
+
+
+
+[docs]
+classDiscretePDFComponentEstimator(BaseComponentEstimator):
+"""
+ Base class for all Estimators working on IRF components that represent discretized PDFs.
+
+ While usable, it is encouraged to use the actual class for the respective IRF
+ component as it ensures further checks and if necessary e.g. unit handling.
+ """
+
+ def__init__(
+ self,
+ grid_points,
+ bin_edges,
+ binned_pdf,
+ interpolator_cls=QuantileInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ ):
+"""
+ __init__ for all discrete PDF components, calls BaseComponentEstimator's
+ __init__ and instantiates inter- and extrapolator objects.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims):
+ Grid points at which interpolation templates exist
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Common set of bin-edges for all discretized PDFs.
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Discretized PDFs for all grid points and arbitrary further dimensions
+ (in IRF term e.g. field-of-view offset bins). Actual interpolation dimension,
+ meaning the dimensions that contain actual histograms, have to be along
+ the last axis.
+ interpolator_cls:
+ pyirf interpolator class, defaults to QuantileInterpolator.
+ interpolator_kwargs: dict
+ Dict of all kwargs that are passed to the interpolator, defaults to
+ None which is the same as passing an empty dict.
+ extrapolator_cls:
+ pyirf extrapolator class. Can be and defaults to ``None``,
+ which raises an error if a target_point is outside the grid
+ and extrapolation would be needed.
+ extrapolator_kwargs: dict
+ Dict of all kwargs that are passed to the extrapolator, defaults to
+ None which is the same as passing an empty dict.
+
+ Raises
+ ------
+ TypeError:
+ When bin_edges is not a np.ndarray.
+ TypeError:
+ When binned_pdf is not a np.ndarray.
+ TypeError:
+ When interpolator_cls is not a DiscretePDFInterpolator subclass.
+ TypeError:
+ When extrapolator_cls is not a DiscretePDFExtrapolator subclass.
+ ValueError:
+ When number of bins in bin_edges and contents in binned_pdf is
+ not matching.
+ ValueError:
+ When number of histograms in binned_pdf and points in grid_points
+ is not matching.
+
+ Note
+ ----
+ Also calls pyirf.interpolation.BaseComponentEstimator.__init__
+ """
+
+ super().__init__(
+ grid_points,
+ )
+
+ ifnotisinstance(binned_pdf,np.ndarray):
+ raiseTypeError("Input binned_pdf is not a numpy array.")
+ elifself.n_points!=binned_pdf.shape[0]:
+ raiseValueError(
+ f"Shape mismatch, number of grid_points ({self.n_points}) and "
+ f"number of histograms in binned_pdf ({binned_pdf.shape[0]}) "
+ "not matching."
+ )
+ elifnotisinstance(bin_edges,np.ndarray):
+ raiseTypeError("Input bin_edges is not a numpy array.")
+ elifbinned_pdf.shape[-1]!=(bin_edges.shape[0]-1):
+ raiseValueError(
+ f"Shape mismatch, bin_edges ({bin_edges.shape[0]-1} bins) "
+ f"and binned_pdf ({binned_pdf.shape[-1]} bins) not matching."
+ )
+
+ # Make sure that 1D input is sorted in increasing order
+ ifself.grid_dim==1:
+ sorting_inds=np.argsort(self.grid_points.squeeze())
+
+ self.grid_points=self.grid_points[sorting_inds]
+ binned_pdf=binned_pdf[sorting_inds]
+
+ ifinterpolator_kwargsisNone:
+ interpolator_kwargs={}
+
+ ifextrapolator_kwargsisNone:
+ extrapolator_kwargs={}
+
+ ifnotissubclass(interpolator_cls,DiscretePDFInterpolator):
+ raiseTypeError(
+ f"interpolator_cls must be a DiscretePDFInterpolator subclass, got {interpolator_cls}"
+ )
+
+ self.interpolator=interpolator_cls(
+ self.grid_points,bin_edges,binned_pdf,**interpolator_kwargs
+ )
+
+ ifextrapolator_clsisNone:
+ self.extrapolator=None
+ elifnotissubclass(extrapolator_cls,DiscretePDFExtrapolator):
+ raiseTypeError(
+ f"extrapolator_cls must be a DiscretePDFExtrapolator subclass, got {extrapolator_cls}"
+ )
+ else:
+ self.extrapolator=extrapolator_cls(
+ self.grid_points,bin_edges,binned_pdf,**extrapolator_kwargs
+ )
+
+
+
+
+[docs]
+classParametrizedComponentEstimator(BaseComponentEstimator):
+"""
+ Base class for all Estimators working on IRF components that represent parametrized
+ or scalar quantities.
+
+ While usable, it is encouraged to use the actual class for the respective IRF
+ component as it ensures further checks and if necessary e.g. unit handling.
+ """
+
+ def__init__(
+ self,
+ grid_points,
+ params,
+ interpolator_cls=GridDataInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ ):
+"""
+ __init__ for all parametrized components, calls BaseComponentEstimator's
+ __init__ and instantiates inter- and extrapolator objects.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims):
+ Grid points at which interpolation templates exist
+ params: np.ndarray, shape=(n_points, ..., n_params)
+ Corresponding parameter values at each point in grid_points.
+ First dimension has to correspond to the number of grid_points.
+ interpolator_cls:
+ pyirf interpolator class, defaults to GridDataInterpolator.
+ interpolator_kwargs: dict
+ Dict of all kwargs that are passed to the interpolator, defaults to
+ None which is the same as passing an empty dict.
+ extrapolator_cls:
+ pyirf extrapolator class. Can be and defaults to ``None``,
+ which raises an error if a target_point is outside the grid
+ and extrapolation would be needed.
+ extrapolator_kwargs: dict
+ Dict of all kwargs that are passed to the extrapolator, defaults to
+ None which is the same as passing an empty dict.
+
+ Raises
+ ------
+ TypeError:
+ When interpolator_cls is not a ParametrizedInterpolator subclass.
+ TypeError:
+ When extrapolator_cls is not a ParametrizedExtrapolator subclass.
+ TypeError:
+ When params is not a np.ndarray.
+ ValueError:
+ When number of points grid_points and params do not match.
+
+ Note
+ ----
+ Also calls pyirf.interpolation.BaseComponentEstimator.__init__
+ """
+
+ super().__init__(
+ grid_points,
+ )
+
+ ifnotisinstance(params,np.ndarray):
+ raiseTypeError("Input params is not a numpy array.")
+ elifself.n_points!=params.shape[0]:
+ raiseValueError(
+ "Shape mismatch, number of grid_points and rows in params not matching."
+ )
+
+ # Make sure that 1D input is sorted in increasing order
+ ifself.grid_dim==1:
+ sorting_inds=np.argsort(self.grid_points.squeeze())
+
+ self.grid_points=self.grid_points[sorting_inds]
+ params=params[sorting_inds]
+
+ ifinterpolator_kwargsisNone:
+ interpolator_kwargs={}
+
+ ifextrapolator_kwargsisNone:
+ extrapolator_kwargs={}
+
+ ifnotissubclass(interpolator_cls,ParametrizedInterpolator):
+ raiseTypeError(
+ f"interpolator_cls must be a ParametrizedInterpolator subclass, got {interpolator_cls}"
+ )
+
+ self.interpolator=interpolator_cls(
+ self.grid_points,params,**interpolator_kwargs
+ )
+
+ ifextrapolator_clsisNone:
+ self.extrapolator=None
+ elifnotissubclass(extrapolator_cls,ParametrizedExtrapolator):
+ raiseTypeError(
+ f"extrapolator_cls must be a ParametrizedExtrapolator subclass, got {extrapolator_cls}"
+ )
+ else:
+ self.extrapolator=extrapolator_cls(
+ self.grid_points,params,**extrapolator_kwargs
+ )
+
+
+
+
+[docs]
+classEffectiveAreaEstimator(ParametrizedComponentEstimator):
+"""Estimator class for effective area tables (AEFF_2D)."""
+
+ @u.quantity_input(effective_area=u.m**2,min_effective_area=u.m**2)
+ def__init__(
+ self,
+ grid_points,
+ effective_area,
+ interpolator_cls=GridDataInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ min_effective_area=1*u.m**2,
+ ):
+"""
+ Takes a grid of effective areas for a bunch of different parameters
+ and inter-/extrapolates (log) effective areas to given value of
+ those parameters.
+
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims):
+ Grid points at which interpolation templates exist
+ effective_area: np.ndarray of astropy.units.Quantity[area], shape=(n_points, ...)
+ Grid of effective area. Dimensions but the first can in principle be freely
+ chosen. Class is AEFF2D compatible, which would require
+ shape=(n_points, n_energy_bins, n_fov_offset_bins).
+ interpolator_cls:
+ pyirf interpolator class, defaults to GridDataInterpolator.
+ interpolator_kwargs: dict
+ Dict of all kwargs that are passed to the interpolator, defaults to
+ None which is the same as passing an empty dict.
+ extrapolator_cls:
+ pyirf extrapolator class. Can be and defaults to ``None``,
+ which raises an error if a target_point is outside the grid
+ and extrapolation would be needed.
+ extrapolator_kwargs: dict
+ Dict of all kwargs that are passed to the extrapolator, defaults to
+ None which is the same as passing an empty dict.
+ min_effective_area: astropy.units.Quantity[area]
+ Minimum value of effective area to be considered for interpolation. Values
+ lower than this value are set to this value. Defaults to 1 m**2.
+
+
+ Note
+ ----
+ Also calls __init__ of pyirf.interpolation.BaseComponentEstimator
+ and pyirf.interpolation.ParametrizedEstimator
+ """
+
+ # get rid of units
+ effective_area=effective_area.to_value(u.m**2)
+ min_effective_area=min_effective_area.to_value(u.m**2)
+
+ self.min_effective_area=min_effective_area
+
+ # remove zeros and log it
+ effective_area[effective_area<self.min_effective_area]=(
+ self.min_effective_area
+ )
+ effective_area=np.log(effective_area)
+
+ super().__init__(
+ grid_points=grid_points,
+ params=effective_area,
+ interpolator_cls=interpolator_cls,
+ interpolator_kwargs=interpolator_kwargs,
+ extrapolator_cls=extrapolator_cls,
+ extrapolator_kwargs=extrapolator_kwargs,
+ )
+
+
+[docs]
+ def__call__(self,target_point):
+"""
+ Estimating effective area at target_point, inter-/extrapolates as needed and
+ specified in __init__.
+
+ Parameters
+ ----------
+ target_point: np.ndarray, shape=(1, n_dims)
+ Target for inter-/extrapolation
+
+ Returns
+ -------
+ aeff_interp: np.ndarray of (astropy.units.m)**2, shape=(n_points, ...)
+ Interpolated Effective area array with same shape as input
+ effective areas. For AEFF2D of shape (n_energy_bins, n_fov_offset_bins).
+ Values lower or equal to __init__'s min_effective_area are set
+ to zero.
+ """
+
+ aeff_interp=super().__call__(target_point)
+
+ # exp it and set to zero too low values
+ aeff_interp=np.exp(aeff_interp)
+ # remove entries manipulated by min_effective_area
+ aeff_interp[aeff_interp<self.min_effective_area]=0
+
+ returnu.Quantity(aeff_interp,u.m**2,copy=COPY_IF_NEEDED)
+
+
+
+
+
+[docs]
+classRadMaxEstimator(ParametrizedComponentEstimator):
+"""Estimator class for rad-max tables (RAD_MAX, RAD_MAX_2D)."""
+
+ def__init__(
+ self,
+ grid_points,
+ rad_max,
+ fill_value=None,
+ interpolator_cls=GridDataInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ ):
+"""
+ Takes a grid of rad max values for a bunch of different parameters
+ and inter-/extrapolates rad max values to given value of those parameters.
+
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims):
+ Grid points at which interpolation templates exist
+ rad_max: np.ndarray, shape=(n_points, ...)
+ Grid of theta cuts. Dimensions but the first can in principle be freely
+ chosen. Class is RAD_MAX_2D compatible, which would require
+ shape=(n_points, n_energy_bins, n_fov_offset_bins).
+ fill_val:
+ Indicator of fill-value handling. If None, fill values are regarded as
+ normal values and no special handeling is applied. If "infer", fill values
+ will be infered as max of all values, if a value is provided,
+ it is used to flag fill values. Flagged fill-values are
+ not used for interpolation. Fill-value handling is only supported in
+ up to two grid dimensions.
+ interpolator_cls:
+ pyirf interpolator class, defaults to GridDataInterpolator.
+ interpolator_kwargs: dict
+ Dict of all kwargs that are passed to the interpolator, defaults to
+ None which is the same as passing an empty dict.
+ extrapolator_cls:
+ pyirf extrapolator class. Can be and defaults to ``None``,
+ which raises an error if a target_point is outside the grid
+ and extrapolation would be needed.
+ extrapolator_kwargs: dict
+ Dict of all kwargs that are passed to the extrapolator, defaults to
+ None which is the same as passing an empty dict.
+
+ Note
+ ----
+ Also calls __init__ of pyirf.interpolation.BaseComponentEstimator
+ and pyirf.interpolation.ParametrizedEstimator
+ """
+
+ super().__init__(
+ grid_points=grid_points,
+ params=rad_max,
+ interpolator_cls=interpolator_cls,
+ interpolator_kwargs=interpolator_kwargs,
+ extrapolator_cls=extrapolator_cls,
+ extrapolator_kwargs=extrapolator_kwargs,
+ )
+
+ self.params=rad_max
+
+ iffill_valueisNone:
+ self.fill_val=None
+ eliffill_value=="infer":
+ self.fill_val=np.max(self.params)
+ else:
+ self.fill_val=fill_value
+
+ # Raise error if fill-values should be handled in >=3 dims
+ ifself.fill_valandself.grid_dim>=3:
+ raiseValueError(
+ "Fill-value handling only supported in up to two grid dimensions."
+ )
+
+ # If fill-values should be handled in 2D, construct a trinangulation
+ # to later determine in which simplex the target values is
+ ifself.fill_valand(self.grid_dim==2):
+ self.triangulation=Delaunay(self.grid_points)
+
+
+[docs]
+ def__call__(self,target_point):
+"""
+ Estimating rad max table at target_point, inter-/extrapolates as needed and
+ specified in __init__.
+
+ Parameters
+ ----------
+ target_point: np.ndarray, shape=(1, n_dims)
+ Target for inter-/extrapolation
+
+ Returns
+ -------
+ rad_max_interp: np.ndarray, shape=(n_points, ...)
+ Interpolated RAD_MAX table with same shape as input
+ effective areas. For RAD_MAX_2D of shape (n_energy_bins, n_fov_offset_bins)
+
+ """
+ # First, construct estimation without handling fill-values
+ full_estimation=super().__call__(target_point)
+ # Safeguard against extreme extrapolation cases
+ np.clip(full_estimation,0,None,out=full_estimation)
+
+ # Early exit if fill_values should not be handled
+ ifnotself.fill_val:
+ returnfull_estimation
+
+ # Early exit if a nearest neighbor estimation would be overwritten
+ # Complex setup is needed to catch settings where the user mixes approaches and
+ # e.g. uses nearest neighbors for extrapolation and an actual interpolation otherwise
+ ifself.grid_dim==1:
+ if(
+ (target_point<self.grid_points.min())
+ or(target_point>self.grid_points.max())
+ )andissubclass(self.extrapolator.__class__,BaseNearestNeighborSearcher):
+ returnfull_estimation
+ elifissubclass(self.interpolator.__class__,BaseNearestNeighborSearcher):
+ returnfull_estimation
+ elifself.grid_dim==2:
+ target_simplex=self.triangulation.find_simplex(target_point)
+
+ if(target_simplex==-1)andissubclass(
+ self.extrapolator.__class__,BaseNearestNeighborSearcher
+ ):
+ returnfull_estimation
+ elifissubclass(self.interpolator.__class__,BaseNearestNeighborSearcher):
+ returnfull_estimation
+
+ # Actual fill-value handling
+ ifself.grid_dim==1:
+ # Locate target in grid
+ iftarget_point<self.grid_points.min():
+ segment_inds=np.array([0,1],"int")
+ eliftarget_point>self.grid_points.max():
+ segment_inds=np.array([-2,-1],"int")
+ else:
+ target_bin=np.digitize(
+ target_point.squeeze(),self.grid_points.squeeze()
+ )
+ segment_inds=np.array([target_bin-1,target_bin],"int")
+
+ mask_left=self.params[segment_inds[0]]>=self.fill_val
+ mask_right=self.params[segment_inds[1]]>=self.fill_val
+ # Indicate, wether one of the neighboring entries is a fill-value
+ mask=np.logical_or(mask_left,mask_right)
+ elifself.grid_dim==2:
+ # Locate target
+ target_simplex=self.triangulation.find_simplex(target_point)
+
+ iftarget_simplex==-1:
+ target_simplex=find_nearest_simplex(self.triangulation,target_point)
+
+ simplex_nodes_indices=self.triangulation.simplices[
+ target_simplex
+ ].squeeze()
+
+ mask0=self.params[simplex_nodes_indices[0]]>=self.fill_val
+ mask1=self.params[simplex_nodes_indices[1]]>=self.fill_val
+ mask2=self.params[simplex_nodes_indices[2]]>=self.fill_val
+
+ # This collected mask now counts for each entry in the estimation how many
+ # of the entries used for extrapolation contained fill-values
+ intermediate_mask=(
+ mask0.astype("int")+mask1.astype("int")+mask2.astype("int")
+ )
+ mask=np.full_like(intermediate_mask,True,dtype=bool)
+
+ # Simplest cases: All or none entries were fill-values, so either return
+ # a fill-value or the actual estimation
+ mask[intermediate_mask==0]=False
+ mask[intermediate_mask==3]=True
+
+ # If two out of three values were fill-values return a fill-value as estimate
+ mask[intermediate_mask==2]=True
+
+ # If only one out of three values was a fill-value use the smallest value of the
+ # remaining two
+ mask[intermediate_mask==1]=False
+ full_estimation=np.where(
+ intermediate_mask[np.newaxis,:]==1,
+ np.min(self.params[simplex_nodes_indices],axis=0),
+ full_estimation,
+ )
+
+ # Set all flagged values to fill-value
+ full_estimation[mask[np.newaxis,:]]=self.fill_val
+
+ # Safeguard against extreme extrapolation cases
+ full_estimation[full_estimation>self.fill_val]=self.fill_val
+
+ returnfull_estimation
+
+
+
+
+
+[docs]
+classEnergyDispersionEstimator(DiscretePDFComponentEstimator):
+"""Estimator class for energy dispersions (EDISP_2D)."""
+
+ def__init__(
+ self,
+ grid_points,
+ migra_bins,
+ energy_dispersion,
+ interpolator_cls=QuantileInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ axis=-2,
+ ):
+"""
+ Takes a grid of energy dispersions for a bunch of different parameters and
+ inter-/extrapolates energy dispersions to given value of those parameters.
+
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist
+ migra_bins: np.ndarray, shape=(n_migration_bins+1)
+ Common bin edges along migration axis.
+ energy_dispersion: np.ndarray, shape=(n_points, ..., n_migration_bins, ...)
+ EDISP MATRIX. Class is EDISP_2D compatible, which would require
+ shape=(n_points, n_energy_bins, n_migration_bins, n_fov_offset_bins).
+ This is assumed as default. If these axes are in different order
+ or e.g. missing a fov_offset axis, the axis containing n_migration_bins
+ has to be specified through axis.
+ interpolator_cls:
+ pyirf interpolator class, defaults to QuantileInterpolator.
+ interpolator_kwargs: dict
+ Dict of all kwargs that are passed to the interpolator, defaults to
+ None which is the same as passing an empty dict.
+ extrapolator_cls:
+ pyirf extrapolator class. Can be and defaults to ``None``,
+ which raises an error if a target_point is outside the grid
+ and extrapolation would be needed.
+ extrapolator_kwargs: dict
+ Dict of all kwargs that are passed to the extrapolator, defaults to
+ None which is the same as passing an empty dict.
+ axis:
+ Axis, along which the actual n_migration_bins are. Input is assumed to
+ be EDISP_2D compatible, so this defaults to -2
+
+ Note
+ ----
+ Also calls __init__ of pyirf.interpolation.BaseComponentEstimator
+ and pyirf.interpolation.ParametrizedEstimator
+ """
+
+ self.axis=axis
+
+ super().__init__(
+ grid_points=grid_points,
+ bin_edges=migra_bins,
+ binned_pdf=np.swapaxes(energy_dispersion,axis,-1),
+ interpolator_cls=interpolator_cls,
+ interpolator_kwargs=interpolator_kwargs,
+ extrapolator_cls=extrapolator_cls,
+ extrapolator_kwargs=extrapolator_kwargs,
+ )
+
+
+[docs]
+ def__call__(self,target_point):
+"""
+ Estimating energy dispersions at target_point, inter-/extrapolates as needed and
+ specified in __init__.
+
+ Parameters
+ ----------
+ target_point: np.ndarray, shape=(1, n_dims)
+ Target for inter-/extrapolation
+
+ Returns
+ -------
+ edisp_interp: np.ndarray, shape=(n_points, ..., n_migration_bins, ...)
+ Interpolated EDISP matrix with same shape as input matrices. For EDISP_2D
+ of shape (n_points, n_energy_bins, n_migration_bins, n_fov_offset_bins)
+
+ """
+
+ returnnp.swapaxes(super().__call__(target_point),-1,self.axis)
+
+
+
+
+
+[docs]
+classPSFTableEstimator(DiscretePDFComponentEstimator):
+"""Estimator class for point spread function tables (PSF_TABLE)."""
+
+ @u.quantity_input(psf=u.sr**-1,source_offset_bins=u.deg)
+ def__init__(
+ self,
+ grid_points,
+ source_offset_bins,
+ psf,
+ interpolator_cls=QuantileInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ axis=-1,
+ ):
+"""
+ Takes a grid of psfs or a bunch of different parameters and
+ inter-/extrapolates psfs to given value of those parameters.
+
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist
+ source_offset_bins: np.ndarray, shape=(n_source_offset_bins+1) of astropy.units.Quantity[deg]
+ Common bin edges along source offset axis.
+ psf: np.ndarray, shape=(n_points, ..., n_source_offset_bins) of astropy.units.Quantity[sr**-1]
+ PSF Tables. Class is PSF_TABLE compatible, which would require
+ shape=(n_points, n_energy_bins, n_fov_offset_bins, n_source_offset_bins).
+ This is assumed as default. If these axes are in different order
+ the axis containing n_source_offset_bins has to be specified through axis.
+ interpolator_cls:
+ pyirf interpolator class, defaults to QuantileInterpolator.
+ interpolator_kwargs: dict
+ Dict of all kwargs that are passed to the interpolator, defaults to
+ None which is the same as passing an empty dict.
+ extrapolator_cls:
+ pyirf extrapolator class. Can be and defaults to ``None``,
+ which raises an error if a target_point is outside the grid
+ and extrapolation would be needed.
+ extrapolator_kwargs: dict
+ Dict of all kwargs that are passed to the extrapolator, defaults to
+ None which is the same as passing an empty dict.
+ axis:
+ Axis, along which the actual n_source_offset_bins are. Input is assumed to
+ be PSF_TABLE compatible, so this defaults to -1
+
+ Note
+ ----
+ Also calls __init__ of pyirf.interpolation.BaseComponentEstimator
+ and pyirf.interpolation.ParametrizedEstimator
+ """
+
+ self.axis=axis
+
+ psf=np.swapaxes(psf,axis,-1)
+
+ ifinterpolator_kwargsisNone:
+ interpolator_kwargs={}
+
+ ifextrapolator_kwargsisNone:
+ extrapolator_kwargs={}
+
+ interpolator_kwargs.setdefault(
+ "normalization",PDFNormalization.CONE_SOLID_ANGLE
+ )
+ extrapolator_kwargs.setdefault(
+ "normalization",PDFNormalization.CONE_SOLID_ANGLE
+ )
+
+ super().__init__(
+ grid_points=grid_points,
+ bin_edges=source_offset_bins.to_value(u.rad),
+ binned_pdf=psf,
+ interpolator_cls=interpolator_cls,
+ interpolator_kwargs=interpolator_kwargs,
+ extrapolator_cls=extrapolator_cls,
+ extrapolator_kwargs=extrapolator_kwargs,
+ )
+
+
+[docs]
+ def__call__(self,target_point):
+"""
+ Estimating psf tables at target_point, inter-/extrapolates as needed and
+ specified in __init__.
+
+ Parameters
+ ----------
+ target_point: np.ndarray, shape=(1, n_dims)
+ Target for inter-/extrapolation
+
+ Returns
+ -------
+ psf_interp: u.Quantity[sr-1], shape=(n_points, ..., n_source_offset_bins)
+ Interpolated psf table with same shape as input matrices. For PSF_TABLE
+ of shape (n_points, n_energy_bins, n_fov_offset_bins, n_source_offset_bins)
+
+ """
+
+ interpolated_psf_normed=super().__call__(target_point)
+
+ # Undo normalisation to get a proper PSF and return
+ returnu.Quantity(
+ np.swapaxes(interpolated_psf_normed,-1,self.axis),
+ u.sr**-1,
+ copy=COPY_IF_NEEDED,
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/interpolation/griddata_interpolator.html b/_modules/pyirf/interpolation/griddata_interpolator.html
new file mode 100644
index 000000000..796606c49
--- /dev/null
+++ b/_modules/pyirf/interpolation/griddata_interpolator.html
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+ pyirf.interpolation.griddata_interpolator — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Source code for pyirf.interpolation.griddata_interpolator
+"""
+Simple wrapper around scipy.interpolate.griddata to interpolate parametrized quantities
+"""
+fromscipy.interpolateimportgriddata
+
+from.base_interpolatorsimportParametrizedInterpolator
+
+__all__=["GridDataInterpolator"]
+
+
+
+[docs]
+classGridDataInterpolator(ParametrizedInterpolator):
+""" "Wrapper arounf scipy.interpolate.griddata."""
+
+ def__init__(self,grid_points,params,**griddata_kwargs):
+"""Parametrized Interpolator using scipy.interpolate.griddata
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist
+ params: np.ndarray, shape=(n_points, ..., n_params)
+ Structured array of corresponding parameter values at each
+ point in grid_points.
+ First dimesion has to correspond to number of grid_points
+ griddata_kwargs: dict
+ Keyword-Arguments passed to scipy.griddata [1], e.g.
+ interpolation method. Defaults to None, which uses scipy's
+ defaults
+
+ Raises
+ ------
+ TypeError:
+ When params is not a np.ndarray
+ ValueError:
+ When number of points grid_points and params is not matching
+
+ References
+ ----------
+ .. [1] Scipy Documentation, scipy.interpolate.griddata
+ https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.griddata.html
+
+ """
+ super().__init__(grid_points,params)
+
+ self.griddata_kwargs=griddata_kwargs
+
+
Source code for pyirf.interpolation.moment_morph_interpolator
+importnumpyasnp
+frompyirf.binningimportbin_center,calculate_bin_indices
+fromscipy.spatialimportDelaunay
+
+from.base_interpolatorsimportDiscretePDFInterpolator,PDFNormalization
+from.utilsimportget_bin_width
+
+__all__=[
+ "MomentMorphInterpolator",
+]
+
+
+def_estimate_mean_std(bin_edges,binned_pdf,normalization):
+"""
+ Function to roughly estimate mean and standard deviation from a histogram.
+
+ Parameters
+ ----------
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Array of common bin-edges for binned_pdf
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ PDF values from which to compute mean and std
+ normalization : PDFNormalization
+ How the PDF is normalized
+
+ Returns
+ -------
+ mean: np.ndarray, shape=(n_points, ...)
+ Estimated mean for each input template
+ std: np.ndarray, shape=(n_points, ...)
+ Estimated standard deviation for each input template. Set to width/2 if only one bin in
+ the input template is =/= 0
+ """
+ # Create an 2darray where the 1darray mids is repeated n_template times
+ mids=np.broadcast_to(bin_center(bin_edges),binned_pdf.shape)
+
+ width=get_bin_width(bin_edges,normalization)
+
+ # integrate pdf to get probability in each bin
+ probability=binned_pdf*width
+ # Weighted averages to compute mean and std
+ mean=np.average(mids,weights=probability,axis=-1)
+ std=np.sqrt(
+ np.average((mids-mean[...,np.newaxis])**2,weights=probability,axis=-1)
+ )
+
+ # Set std to 0.5*width for all those templates that have only one bin =/= 0. In those
+ # cases mids-mean = 0 and therefore std = 0. Uses the width of the one bin with
+ # binned_pdf!=0
+ mask=std==0
+ ifnp.any(mask):
+ width=np.diff(bin_edges)
+ # std of a uniform distribution inside the bin
+ uniform_std=np.broadcast_to(np.sqrt(1/12)*width,binned_pdf[mask].shape)
+ std[mask]=uniform_std[binned_pdf[mask,:]!=0]
+
+ returnmean,std
+
+
+def_lookup(bin_edges,binned_pdf,x):
+"""
+ Function to return the bin-height at a desired point.
+
+ Parameters
+ ----------
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Array of common bin-edges for binned_pdf
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Array of bin-entries, actual
+ x: numpy.ndarray, shape=(n_points, ..., n_bins)
+ Array of n_bins points for each input template, where the histogram-value (bin-height) should be found
+
+ Returns
+ -------
+ y: numpy.ndarray, shape=(n_points, ..., n_bins)
+ Array of the bin-heights at the n_bins points x, set to 0 at each point outside the histogram
+
+ """
+ # Find the bin where each point x is located in
+ binnr,valid=calculate_bin_indices(x,bin_edges)
+
+ # Set under/overflow-bins (invalid bins) to 0 to avoid errors below
+ binnr[~valid]=0
+
+ # Loop over every combination of flattend input histograms and flattend binning
+ lu=np.array(
+ [
+ cont[binnr_row]
+ forcont,binnr_rowinzip(
+ binned_pdf.reshape(-1,binned_pdf.shape[-1]),
+ binnr.reshape(-1,binnr.shape[-1]),
+ )
+ ]
+ ).reshape(binned_pdf.shape)
+
+ # Set all invalid bins to 0
+ lu[~valid]=0
+
+ # Set under-/ overflowbins to 0, reshape to original shape
+ returnlu
+
+
+deflinesegment_1D_interpolation_coefficients(grid_points,target_point):
+"""
+ Compute 1D interpolation coefficients for moment morph interpolation,
+ as in eq. (6) of [1]
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(2, 1)
+ Points spanning a triangle in which
+ target_point: numpy.ndarray, shape=(1, 1)
+ Value at which the histogram should be interpolated
+
+ Returns
+ -------
+ coefficients: numpy.ndarray, shape=(2,)
+ Interpolation coefficients for all three interpolation simplex vertices
+ to interpolate to the target_point
+
+ References
+ ----------
+ .. [1] M. Baak, S. Gadatsch, R. Harrington and W. Verkerke (2015). Interpolation between
+ multi-dimensional histograms using a new non-linear moment morphing method
+ Nucl. Instrum. Methods Phys. Res. A 771, 39-48. https://doi.org/10.1016/j.nima.2014.10.033
+ """
+ # Set zeroth grid point as reference value
+ m0=grid_points[0,:]
+
+ # Compute matrix M as in eq. (2) of [1]
+ j=np.arange(0,grid_points.shape[0],1)
+ m_ij=(grid_points-m0)**j
+
+ # Compute coefficients, eq. (6) from [1]
+ returnnp.einsum("...j, ji -> ...i",(target_point-m0)**j,np.linalg.inv(m_ij))
+
+
+defbarycentric_2D_interpolation_coefficients(grid_points,target_point):
+"""
+ Compute barycentric 2D interpolation coefficients for triangular
+ interpolation, see e.g. [1]
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(3, 2)
+ Points spanning a triangle in which
+ target_point: np.ndarray, shape=(1, 2)
+ Value at which barycentric interpolation is needed
+
+ Returns
+ -------
+ coefficients: numpy.ndarray, shape=(3,)
+ Interpolation coefficients for all three interpolation simplex vertices
+ to interpolate to the target_point
+
+ References
+ ----------
+ .. [1] https://codeplea.com/triangular-interpolation
+ """
+ # Compute distance vectors between the grid points
+ d13=grid_points[0,:]-grid_points[2,:]
+ d23=grid_points[1,:]-grid_points[2,:]
+
+ # Compute distance vector between target and third grid point
+ dp3=target_point.squeeze()-grid_points[2,:]
+
+ # Compute first and second weight
+ w1=((d23[1]*dp3[0])+(-d23[0]*dp3[1]))/(
+ (d23[1]*d13[0])+(-d23[0]*d13[1])
+ )
+ w2=((-d13[1]*dp3[0])+(d13[0]*dp3[1]))/(
+ (d23[1]*d13[0])+(-d23[0]*d13[1])
+ )
+
+ # Use w1+w2+w3 = 1 for third weight
+ w3=1-w1-w2
+
+ coefficients=np.array([w1,w2,w3])
+
+ returncoefficients
+
+
+defmoment_morph_estimation(bin_edges,binned_pdf,coefficients,normalization):
+"""
+ Function that wraps up the moment morph procedure [1] adopted for histograms.
+
+ Parameters
+ ----------
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Array of common bin-edges for binned_pdf
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Array of bin-entries, actual
+ coefficients: np.ndarray, shape=(n_points)
+ Estimation coefficients for each entry in binned_pdf
+ normalization : PDFNormalization
+ How the PDF is normalized
+
+ Returns
+ -------
+ f_new: numpy.ndarray, shape=(1, n_bins)
+ Interpolated histogram
+
+ References
+ ----------
+ .. [1] M. Baak, S. Gadatsch, R. Harrington and W. Verkerke (2015). Interpolation between
+ multi-dimensional histograms using a new non-linear moment morphing method
+ Nucl. Instrum. Methods Phys. Res. A 771, 39-48. https://doi.org/10.1016/j.nima.2014.10.033
+ """
+ bin_mids=bin_center(bin_edges)
+
+ # Catch all those templates, where at least one template histogram is all zeros.
+ zero_templates=~np.all(~np.isclose(np.sum(binned_pdf,axis=-1),0),0)
+
+ # Manipulate those templates so that computations pass without error
+ binned_pdf[:,zero_templates]=np.full(len(bin_mids),1/len(bin_mids))
+
+ # Estimate mean and std for each input template histogram. First adaption needed to extend
+ # the moment morph procedure to histograms
+ mus,sigs=_estimate_mean_std(
+ bin_edges=bin_edges,binned_pdf=binned_pdf,normalization=normalization
+ )
+ coefficients=coefficients.reshape(
+ binned_pdf.shape[0],*np.ones(mus.ndim-1,"int")
+ )
+
+ # Transform mean and std as in eq. (11) and (12) in [1]
+ # cs = np.broadcast_to(cs, mus.shape)
+ mu_prime=np.sum(coefficients*mus,axis=0)
+ sig_prime=np.sum(coefficients*sigs,axis=0)
+
+ # Compute slope and offset as in eq. (14) and (15) in [1]
+ aij=sigs/sig_prime
+ bij=mus-mu_prime*aij
+
+ # Transformation as in eq. (13) in [1]
+ mids=np.broadcast_to(bin_mids,binned_pdf.shape)
+ transf_mids=aij[...,np.newaxis]*mids+bij[...,np.newaxis]
+
+ # Compute the morphed historgram according to eq. (18) in [1]. The function "lookup" "resamples"
+ # the histogram at the transformed bin-mids by using the templates historgam value at the transformed
+ # bin-mid as new value for a whole transformed bin. Second adaption needed to extend
+ # the moment morph procedure to histograms, adaptes the behaviour of eq. (16)
+
+ transf_hist=_lookup(bin_edges=bin_edges,binned_pdf=binned_pdf,x=transf_mids)
+
+ f_new=np.sum(
+ np.expand_dims(coefficients,-1)*transf_hist*np.expand_dims(aij,-1),axis=0
+ )
+
+ # Reset interpolation resolts for those templates with partially zero entries from above to 0
+ f_new[zero_templates]=np.zeros(len(bin_mids))
+
+ # Re-Normalize, needed, as the estimation of the std used above is not exact but the result is scaled with
+ # the estimated std
+ bin_widths=get_bin_width(bin_edges,normalization)
+ norm=np.expand_dims(np.sum(f_new*bin_widths,axis=-1),-1)
+
+ returnnp.divide(f_new,norm,out=np.zeros_like(f_new),where=norm!=0).reshape(
+ 1,*binned_pdf.shape[1:]
+ )
+
+
+
+[docs]
+classMomentMorphInterpolator(DiscretePDFInterpolator):
+"""Interpolator class providing Moment Morphing to interpolate discretized PDFs."""
+
+ def__init__(
+ self,grid_points,bin_edges,binned_pdf,normalization=PDFNormalization.AREA
+ ):
+"""
+ Interpolator class using moment morphing.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist. May be one ot two dimensional.
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Edges of the data binning
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Content of each bin in bin_edges for
+ each point in grid_points. First dimesion has to correspond to number
+ of grid_points. Interpolation dimension, meaning the
+ the quantity that should be interpolated (e.g. the Migra axis for EDisp)
+ has to be at axis specified by axis-keyword as well as having entries
+ corresponding to the number of bins given through bin_edges keyword.
+ normalization : PDFNormalization
+ How the PDF is normalized
+
+ Note
+ ----
+ Also calls pyirf.interpolation.DiscretePDFInterpolator.__init__.
+ """
+ super().__init__(grid_points,bin_edges,binned_pdf,normalization)
+
+ ifself.grid_dim==2:
+ self.triangulation=Delaunay(self.grid_points)
+ elifself.grid_dim>2:
+ raiseNotImplementedError(
+ "Interpolation in more then two dimension not impemented."
+ )
+
+ def_interpolate1D(self,target_point):
+"""
+ Function to find target inside 1D self.grid_points and interpolate
+ on this subset.
+ """
+ target_bin=np.digitize(target_point.squeeze(),self.grid_points.squeeze())
+ segment_inds=np.array([target_bin-1,target_bin],"int")
+ coefficients=linesegment_1D_interpolation_coefficients(
+ grid_points=self.grid_points[segment_inds],
+ target_point=target_point,
+ )
+
+ returnmoment_morph_estimation(
+ bin_edges=self.bin_edges,
+ binned_pdf=self.binned_pdf[segment_inds],
+ coefficients=coefficients,
+ normalization=self.normalization,
+ )
+
+ def_interpolate2D(self,target_point):
+"""
+ Function to find target inside 2D self.grid_points and interpolate
+ on this subset.
+ """
+ simplex_inds=self.triangulation.simplices[
+ self.triangulation.find_simplex(target_point)
+ ].squeeze()
+ coefficients=barycentric_2D_interpolation_coefficients(
+ grid_points=self.grid_points[simplex_inds],
+ target_point=target_point,
+ )
+
+ returnmoment_morph_estimation(
+ bin_edges=self.bin_edges,
+ binned_pdf=self.binned_pdf[simplex_inds],
+ coefficients=coefficients,
+ normalization=self.normalization,
+ )
+
+
+[docs]
+ definterpolate(self,target_point):
+"""
+ Takes a grid of binned pdfs for a bunch of different parameters
+ and interpolates it to given value of those parameters.
+ This function calls implementations of the moment morphing interpolation
+ pocedure introduced in [1].
+
+ Parameters
+ ----------
+ target_point: numpy.ndarray, shape=(1, n_dims)
+ Value for which the interpolation is performed (target point)
+
+ Returns
+ -------
+ f_new: numpy.ndarray, shape=(1,...,n_bins)
+ Interpolated and binned pdf
+
+ References
+ ----------
+ .. [1] M. Baak, S. Gadatsch, R. Harrington and W. Verkerke (2015). Interpolation between
+ multi-dimensional histograms using a new non-linear moment morphing method
+ Nucl. Instrum. Methods Phys. Res. A 771, 39-48. https://doi.org/10.1016/j.nima.2014.10.033
+ """
+ ifself.grid_dim==1:
+ interpolant=self._interpolate1D(target_point)
+ elifself.grid_dim==2:
+ interpolant=self._interpolate2D(target_point)
+
+ returninterpolant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/interpolation/nearest_neighbor_searcher.html b/_modules/pyirf/interpolation/nearest_neighbor_searcher.html
new file mode 100644
index 000000000..80eeeb320
--- /dev/null
+++ b/_modules/pyirf/interpolation/nearest_neighbor_searcher.html
@@ -0,0 +1,309 @@
+
+
+
+
+
+
+
+ pyirf.interpolation.nearest_neighbor_searcher — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+classBaseNearestNeighborSearcher(BaseInterpolator):
+"""
+ Dummy NearestNeighbor approach usable instead of
+ actual Interpolation/Extrapolation
+ """
+
+ def__init__(self,grid_points,values,norm_ord=2):
+"""
+ BaseNearestNeighborSearcher
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist
+ values: np.ndarray, shape=(n_points, ...)
+ Corresponding IRF values at grid_points
+ norm_ord: non-zero int
+ Order of the norm which is used to compute the distances,
+ passed to numpy.linalg.norm [1]. Defaults to 2,
+ which uses the euclidean norm.
+
+ Raises
+ ------
+ TypeError:
+ If norm_ord is not non-zero integer
+
+ Notes
+ -----
+ Also calls pyirf.interpolation.BaseInterpolators.__init__
+ """
+ super().__init__(grid_points)
+
+ self.values=values
+
+ # Test wether norm_ord is a number
+ try:
+ norm_ord>0
+ exceptTypeError:
+ raiseValueError(
+ f"Only positiv integers allowed for norm_ord, got {norm_ord}."
+ )
+
+ # Test wether norm_ord is a finite, positive integer
+ if(norm_ord<=0)or~np.isfinite(norm_ord)or(norm_ord!=int(norm_ord)):
+ raiseValueError(
+ f"Only positiv integers allowed for norm_ord, got {norm_ord}."
+ )
+
+ self.norm_ord=norm_ord
+
+
+[docs]
+ definterpolate(self,target_point):
+"""
+ Takes a grid of IRF values for a bunch of different parameters and returns
+ the values at the nearest grid point as seen from the target point.
+
+ Parameters
+ ----------
+ target_point: numpy.ndarray, shape=(1, n_dims)
+ Value for which the nearest neighbor should be found (target point)
+
+ Returns
+ -------
+ content_new: numpy.ndarray, shape=(1, ...)
+ values at nearest neighbor
+
+ Notes
+ -----
+ In case of multiple nearest neighbors, the values corresponding
+ to the first one are returned.
+ """
+
+ iftarget_point.ndim==1:
+ target_point=target_point.reshape(1,*target_point.shape)
+
+ distances=np.linalg.norm(
+ self.grid_points-target_point,ord=self.norm_ord,axis=1
+ )
+
+ index=np.argmin(distances)
+
+ returnself.values[index,:]
+
+
+
+
+
+[docs]
+classDiscretePDFNearestNeighborSearcher(BaseNearestNeighborSearcher):
+"""
+ Dummy NearestNeighbor approach usable instead of
+ actual interpolation/extrapolation.
+ Compatible with discretized PDF IRF component API.
+ """
+
+ def__init__(self,grid_points,bin_edges,binned_pdf,norm_ord=2):
+"""
+ NearestNeighborSearcher compatible with discretized PDF IRF components API
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Edges of the data binning. Ignored for nearest neighbor searching.
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Content of each bin in bin_edges for
+ each point in grid_points. First dimesion has to correspond to number
+ of grid_points, last dimension has to correspond to number of bins for
+ the quantity that should be interpolated (e.g. the Migra axis for EDisp)
+ norm_ord: non-zero int
+ Order of the norm which is used to compute the distances,
+ passed to numpy.linalg.norm [1]. Defaults to 2,
+ which uses the euclidean norm.
+
+ Notes
+ -----
+ Also calls pyirf.interpolation.BaseNearestNeighborSearcher.__init__
+ """
+
+ super().__init__(grid_points=grid_points,values=binned_pdf,norm_ord=norm_ord)
+[docs]
+classParametrizedNearestNeighborSearcher(BaseNearestNeighborSearcher):
+"""
+ Dummy NearestNeighbor approach usable instead of
+ actual interpolation/extrapolation
+ Compatible with parametrized IRF component API.
+ """
+
+ def__init__(self,grid_points,params,norm_ord=2):
+"""
+ NearestNeighborSearcher compatible with parametrized IRF components API
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist
+ params: np.ndarray, shape=(n_points, ..., n_params)
+ Corresponding parameter values at each point in grid_points.
+ First dimesion has to correspond to number of grid_points
+ norm_ord: non-zero int
+ Order of the norm which is used to compute the distances,
+ passed to numpy.linalg.norm [1]. Defaults to 2,
+ which uses the euclidean norm.
+
+ Notes
+ ----
+ Also calls pyirf.interpolation.BaseNearestNeighborSearcher.__init__
+ """
+
+ super().__init__(grid_points=grid_points,values=params,norm_ord=norm_ord)
+[docs]
+classParametrizedNearestSimplexExtrapolator(ParametrizedExtrapolator):
+"""Extrapolator class extending linear or baryzentric interpolation outside a grid's convex hull."""
+
+ def__init__(self,grid_points,params):
+"""
+ Extrapolator class using linear or baryzentric extrapolation in one ore two
+ grid-dimensions.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist. May be one ot two dimensional.
+ Have to be sorted in accending order for 1D.
+ params: np.ndarray, shape=(n_points, ...)
+ Array of corresponding parameter values at each point in grid_points.
+ First dimesion has to correspond to number of grid_points
+
+ Note
+ ----
+ Also calls pyirf.interpolation.ParametrizedInterpolator.__init__.
+ """
+ super().__init__(grid_points,params)
+
+ ifself.grid_dim==2:
+ self.triangulation=Delaunay(self.grid_points)
+ elifself.grid_dim>2:
+ raiseNotImplementedError(
+ "Extrapolation in more then two dimension not impemented."
+ )
+
+ def_extrapolate1D(self,segment_inds,target_point):
+"""
+ Function to compute extrapolation coefficients for a target_point on a
+ specified grid segment and extrapolate from this subset
+ """
+ coefficients=linesegment_1D_interpolation_coefficients(
+ grid_points=self.grid_points[segment_inds],
+ target_point=target_point.squeeze(),
+ )
+
+ # reshape to be broadcastable
+ coefficients=coefficients.reshape(
+ coefficients.shape[0],*np.ones(self.params.ndim-1,"int")
+ )
+
+ returnnp.sum(coefficients*self.params[segment_inds,:],axis=0)[
+ np.newaxis,:
+ ]
+
+ def_extrapolate2D(self,simplex_inds,target_point):
+"""
+ Function to compute extrapolation coeficcients and extrapolate from the nearest
+ simplex by extending barycentric interpolation
+ """
+ coefficients=barycentric_2D_interpolation_coefficients(
+ grid_points=self.grid_points[simplex_inds],
+ target_point=target_point,
+ )
+
+ # reshape to be broadcastable
+ coefficients=coefficients.reshape(
+ coefficients.shape[0],*np.ones(self.params.ndim-1,"int")
+ )
+
+ returnnp.sum(coefficients*self.params[simplex_inds,:],axis=0)[
+ np.newaxis,:
+ ]
+
+
+[docs]
+ defextrapolate(self,target_point):
+"""
+ Takes a grid of scalar values for a bunch of different parameters
+ and extrapolates it to given value of those parameters.
+
+ Parameters
+ ----------
+ target_point: numpy.ndarray, shape=(1, n_dims)
+ Value for which the extrapolation is performed (target point)
+
+ Returns
+ -------
+ values: numpy.ndarray, shape=(1,...)
+ Extrapolated values
+
+ """
+ ifself.grid_dim==1:
+ iftarget_point<self.grid_points.min():
+ segment_inds=np.array([0,1],"int")
+ else:
+ segment_inds=np.array([-2,-1],"int")
+
+ extrapolant=self._extrapolate1D(segment_inds,target_point)
+ elifself.grid_dim==2:
+ nearest_simplex_ind=find_nearest_simplex(
+ self.triangulation,target_point.squeeze()
+ )
+ simplex_indices=self.triangulation.simplices[nearest_simplex_ind]
+
+ extrapolant=self._extrapolate2D(simplex_indices,target_point)
+
+ returnextrapolant
+
+
+
+
+
+[docs]
+classMomentMorphNearestSimplexExtrapolator(DiscretePDFExtrapolator):
+"""Extrapolator class extending moment morphing interpolation outside a grid's convex hull."""
+
+ def__init__(
+ self,grid_points,bin_edges,binned_pdf,normalization=PDFNormalization.AREA
+ ):
+"""
+ Extrapolator class extending/reusing parts of Moment Morphing
+ by allowing for negative extrapolation coefficients computed
+ by from the nearest simplex in 1D or 2D (so either a line-segement
+ or a triangle).
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(n_points, n_dims)
+ Grid points at which templates exist. May be one ot two dimensional.
+ Have to be sorted in accending order for 1D.
+ bin_edges: np.ndarray, shape=(n_bins+1)
+ Edges of the data binning
+ binned_pdf: np.ndarray, shape=(n_points, ..., n_bins)
+ Content of each bin in bin_edges for
+ each point in grid_points. First dimesion has to correspond to number
+ of grid_points. Extrapolation dimension, meaning the
+ the quantity that should be interpolated (e.g. the Migra axis for EDisp)
+ has to be at the last axis as well as having entries
+ corresponding to the number of bins given through bin_edges keyword.
+ normalization : PDFNormalization
+ How the PDF is normalized
+
+ Note
+ ----
+ Also calls pyirf.interpolation.DiscretePDFExtrapolator.__init__.
+ """
+ super().__init__(grid_points,bin_edges,binned_pdf,normalization)
+
+ ifself.grid_dim==2:
+ self.triangulation=Delaunay(self.grid_points)
+ elifself.grid_dim>2:
+ raiseNotImplementedError(
+ "Extrapolation in more then two dimension not impemented."
+ )
+
+ def_extrapolate1D(self,segment_inds,target_point):
+"""
+ Function to compute extrapolation coefficients for a target_point on a
+ specified grid segment and extrapolate from this subset
+ """
+ coefficients=linesegment_1D_interpolation_coefficients(
+ grid_points=self.grid_points[segment_inds],
+ target_point=target_point.squeeze(),
+ )
+
+ returnmoment_morph_estimation(
+ bin_edges=self.bin_edges,
+ binned_pdf=self.binned_pdf[segment_inds],
+ coefficients=coefficients,
+ normalization=self.normalization,
+ )
+
+ def_extrapolate2D(self,simplex_inds,target_point):
+"""
+ Function to compute extrapolation coeficcients and extrapolate from the nearest
+ simplex by extending barycentric interpolation
+ """
+ coefficients=barycentric_2D_interpolation_coefficients(
+ grid_points=self.grid_points[simplex_inds],
+ target_point=target_point,
+ )
+
+ returnmoment_morph_estimation(
+ bin_edges=self.bin_edges,
+ binned_pdf=self.binned_pdf[simplex_inds],
+ coefficients=coefficients,
+ normalization=self.normalization,
+ )
+
+
+[docs]
+ defextrapolate(self,target_point):
+"""
+ Takes a grid of discretized PDFs for a bunch of different parameters
+ and extrapolates it to given value of those parameters.
+
+ Parameters
+ ----------
+ target_point: numpy.ndarray, shape=(1, n_dims)
+ Value for which the extrapolation is performed (target point)
+
+ Returns
+ -------
+ values: numpy.ndarray, shape=(1, ..., n_bins)
+ Extrapolated discretized PDFs
+
+ """
+ ifself.grid_dim==1:
+ iftarget_point<self.grid_points.min():
+ segment_inds=np.array([0,1],"int")
+ else:
+ segment_inds=np.array([-2,-1],"int")
+
+ extrapolant=self._extrapolate1D(segment_inds,target_point)
+ elifself.grid_dim==2:
+ nearest_simplex_ind=find_nearest_simplex(
+ self.triangulation,target_point.squeeze()
+ )
+ simplex_indices=self.triangulation.simplices[nearest_simplex_ind]
+
+ extrapolant=self._extrapolate2D(simplex_indices,target_point)
+
+ ifnotnp.all(extrapolant>=0):
+ warnings.warn(
+ "Some bin-entries where below zero after extrapolation and "
+ "thus cut off. Check result carefully."
+ )
+
+ # cut off values below 0 and re-normalize
+ extrapolant[extrapolant<0]=0
+ bin_width=get_bin_width(self.bin_edges,self.normalization)
+ norm=np.expand_dims(np.sum(extrapolant*bin_width,axis=-1),-1)
+ returnnp.divide(
+ extrapolant,norm,out=np.zeros_like(extrapolant),where=norm!=0
+ ).reshape(1,*self.binned_pdf.shape[1:])
+ else:
+ returnextrapolant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/interpolation/quantile_interpolator.html b/_modules/pyirf/interpolation/quantile_interpolator.html
new file mode 100644
index 000000000..e13d55f7f
--- /dev/null
+++ b/_modules/pyirf/interpolation/quantile_interpolator.html
@@ -0,0 +1,398 @@
+
+
+
+
+
+
+
+ pyirf.interpolation.quantile_interpolator — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Source code for pyirf.interpolation.quantile_interpolator
+importnumpyasnp
+fromscipy.interpolateimportgriddata,interp1d
+
+from.base_interpolatorsimportDiscretePDFInterpolator,PDFNormalization
+from.utilsimportget_bin_width
+
+__all__=["QuantileInterpolator"]
+
+
+defcdf_values(binned_pdf,bin_edges,normalization):
+"""
+ compute cdf values and assure they are normed to unity
+ """
+ bin_widths=get_bin_width(bin_edges,normalization)
+ cdfs=np.cumsum(binned_pdf*bin_widths,axis=-1)
+
+ # assure the last cdf value is 1, ignore errors for empty pdfs as they are reset to 0 by nan_to_num
+ withnp.errstate(invalid="ignore"):
+ cdfs=np.nan_to_num(cdfs/np.max(cdfs,axis=-1)[...,np.newaxis])
+
+ returncdfs
+
+
+defppf_values(bin_mids,cdfs,quantiles):
+"""
+ Compute ppfs from cdfs and interpolate them to the desired interpolation point
+
+ Parameters
+ ----------
+ bin_mids: numpy.ndarray, shape=(n_bins)
+ Bin-mids for each bin along interpolation axis
+
+ cdfs: numpy.ndarray, shape=(n_points,...,n_bins)
+ Corresponding cdf-values for all quantiles
+
+ quantiles: numpy.ndarray, shape=(n_quantiles)
+ Quantiles for which the ppf-values should be estimated
+
+ Returns
+ -------
+ ppfs: numpy.ndarray, shape=(1,...,n_quantiles)
+ Corresponding ppf-values for all quantiles at the target interpolation point
+ """
+
+ defcropped_interp(cdf):
+"""
+ create ppf-values through inverse interpolation of the cdf, avoiding repeating 0 and 1 entries
+ around the first and last bins as well as repeating values due to zero bins
+ in between filled bins. Both cases would result in division by zero errors when computing
+ the interpolation polynom.
+ """
+
+ # Find last 0 and first 1 entry
+ last_0=np.nonzero(cdf==0)[0][-1]ifcdf[0]==0else0
+ first_1=np.nonzero(cdf==1.0)[0][0]
+
+ # Predefine selection mask
+ selection=np.ones(len(cdf),dtype=bool)
+
+ # Keep only the first of subsequently matching values
+ selection[1:]=cdf[1:]!=cdf[:-1]
+
+ # Keep only the last 0 and first 1 entry
+ selection[:last_0]=False
+ selection[last_0]=True
+ selection[first_1+1:]=False
+ selection[first_1]=True
+
+ # create ppf values from selected bins
+ returninterp1d(
+ cdf[selection],
+ bin_mids[selection],
+ bounds_error=False,
+ fill_value="extrapolate",
+ )(quantiles)
+
+ # create ppf values from cdf samples via interpolation of the cdfs
+ # return nan for empty pdfs
+ ppfs=np.apply_along_axis(
+ lambdacdf:cropped_interp(cdf)
+ ifnp.sum(cdf)>0
+ elsenp.full_like(quantiles,np.nan),
+ -1,
+ cdfs,
+ )
+ # nD interpolation of ppf values
+ returnppfs
+
+
+defpdf_from_ppf(bin_edges,interp_ppfs,quantiles):
+"""
+ Reconstruct pdf from ppf and evaluate at desired points.
+
+ Parameters
+ ----------
+ bin_edges: numpy.ndarray, shape=(n_bins+1)
+ Edges of the bins in which the final pdf should be binned
+
+ interp_ppfs: numpy.ndarray, shape=(1,...,n_quantiles)
+ Corresponding ppf-values for all quantiles at the target_point,
+ not to be confused with QunatileInterpolators self.ppfs, the ppfs
+ computed from the input distributions.
+
+ quantiles: numpy.ndarray, shape=(n_quantiles)
+ Quantiles corresponding to the ppf-values in interp_ppfs
+
+ Returns
+ -------
+ pdf_values: numpy.ndarray, shape=(1,...,n_bins)
+ Recomputed, binned pdf at target_point
+ """
+ # recalculate pdf values through numerical differentiation
+ pdf_interpolant=np.nan_to_num(np.diff(quantiles)/np.diff(interp_ppfs,axis=-1))
+
+ # Unconventional solution to make this usable with np.apply_along_axis for readability
+ # The ppf bin-mids are computed since the pdf-values are derived through derivation
+ # from the ppf-values
+ xyconcat=np.concatenate(
+ (interp_ppfs[...,:-1]+np.diff(interp_ppfs)/2,pdf_interpolant),axis=-1
+ )
+
+ definterpolate_ppf(xy):
+ ppf=xy[:len(xy)//2]
+ pdf=xy[len(xy)//2:]
+ interpolate=interp1d(ppf,pdf,bounds_error=False,fill_value=(0,0))
+ result=np.nan_to_num(interpolate(bin_edges[:-1]))
+ returnresult
+
+ # Interpolate pdf samples and evaluate at bin edges, weight with the bin_width to estimate
+ # correct bin height via the midpoint rule formulation of the trapezoidal rule
+ pdf_values=np.apply_along_axis(interpolate_ppf,-1,xyconcat)
+
+ returnpdf_values
+
+
+defnorm_pdf(pdf_values,bin_edges,normalization):
+"""
+ Normalize binned_pdf to a sum of 1
+ """
+ norm=np.sum(pdf_values,axis=-1)
+ width=get_bin_width(bin_edges,normalization)
+
+ # Norm all binned_pdfs to unity that are not empty
+ normed_pdf_values=np.divide(
+ pdf_values,
+ norm[...,np.newaxis]*width,
+ out=np.zeros_like(pdf_values),
+ where=norm[...,np.newaxis]!=0,
+ )
+
+ returnnormed_pdf_values
+
+
+
+[docs]
+classQuantileInterpolator(DiscretePDFInterpolator):
+"""Interpolator class providing quantile interpoalation."""
+
+ def__init__(
+ self,
+ grid_points,
+ bin_edges,
+ binned_pdf,
+ quantile_resolution=1e-3,
+ normalization=PDFNormalization.AREA,
+ ):
+"""
+ Parameters
+ ----------
+ grid_points : np.ndarray, shape=(n_points, n_dims)
+ Grid points at which interpolation templates exist
+ bin_edges : np.ndarray, shape=(n_bins+1)
+ Edges of the data binning
+ binned_pdf : np.ndarray, shape(n_points, ..., n_bins)
+ Content of each bin in bin_edges for
+ each point in grid_points. First dimesion has to correspond to number
+ of grid_points, the last axis has to correspond to number
+ of bins for the quantity that should be interpolated
+ (e.g. the Migra axis for EDisp)
+ quantile_resolution : float
+ Spacing between quantiles
+ normalization : PDFNormalization
+ How the discrete PDF is normalized
+
+ Raises
+ ------
+ ValueError:
+ When last axis in binned_pdf and number of bins are not equal.
+
+ Note
+ ----
+ Also calls __init__ of pyirf.interpolation.BaseInterpolator and
+ DiscretePDFInterpolator
+ """
+ # Remember input shape
+ self.input_shape=binned_pdf.shape
+
+ ifself.input_shape[-1]!=len(bin_edges)-1:
+ raiseValueError(
+ "Number of bins along last axis and those specified by bin_edges not matching."
+ )
+
+ # include 0-bin at first position in each pdf to avoid edge-effects where the CDF would otherwise
+ # start at a value != 0, also extend edges with one bin to the left
+ fill_zeros=np.zeros(shape=binned_pdf.shape[:-1])[...,np.newaxis]
+ binned_pdf=np.concatenate((fill_zeros,binned_pdf),axis=-1)
+ bin_edges=np.append(bin_edges[0]-np.diff(bin_edges)[0],bin_edges)
+
+ # compute quantiles from quantile_resolution
+ self.quantiles=np.linspace(
+ 0,1,int(np.round(1/quantile_resolution,decimals=0))
+ )
+
+ super().__init__(
+ grid_points=grid_points,
+ bin_edges=bin_edges,
+ binned_pdf=binned_pdf,
+ normalization=normalization,
+ )
+
+ # Compute CDF values
+ self.cdfs=cdf_values(self.binned_pdf,self.bin_edges,self.normalization)
+
+ # compute ppf values at quantiles, determine quantile step of [1]
+ self.ppfs=ppf_values(self.bin_mids,self.cdfs,self.quantiles)
+
+
+[docs]
+ definterpolate(self,target_point):
+"""
+ Takes a grid of binned pdfs for a bunch of different parameters
+ and interpolates it to given value of those parameters.
+ This function provides an adapted version of the quantile interpolation introduced
+ in [1].
+ Instead of following this method closely, it implements different approaches to the
+ steps shown in Fig. 5 of [1].
+
+ Parameters
+ ----------
+ target_point: numpy.ndarray, shape=(1, n_dims)
+ Value for which the interpolation is performed (target point)
+
+ Returns
+ -------
+ f_new: numpy.ndarray, shape=(1, ..., n_bins)
+ Interpolated and binned pdf
+
+ References
+ ----------
+ .. [1] B. E. Hollister and A. T. Pang (2013). Interpolation of Non-Gaussian Probability Distributions
+ for Ensemble Visualization
+ https://engineering.ucsc.edu/sites/default/files/technical-reports/UCSC-SOE-13-13.pdf
+ """
+
+ # interpolate ppfs to target point, interpolate quantiles step of [1]
+ interpolated_ppfs=griddata(self.grid_points,self.ppfs,target_point)
+
+ # compute pdf values for all bins, evaluate interpolant PDF values step of [1], drop the earlier
+ # introduced extra bin
+ interpolated_pdfs=pdf_from_ppf(
+ self.bin_edges,interpolated_ppfs,self.quantiles
+ )[...,1:]
+
+ # Renormalize pdf to sum of 1
+ normed_interpolated_pdfs=norm_pdf(
+ interpolated_pdfs,self.bin_edges[1:],self.normalization
+ )
+
+ # Re-swap axes and set all nans to zero
+ returnnp.nan_to_num(normed_interpolated_pdfs).reshape(1,*self.input_shape[1:])
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/interpolation/visible_edges_extrapolator.html b/_modules/pyirf/interpolation/visible_edges_extrapolator.html
new file mode 100644
index 000000000..c28f776eb
--- /dev/null
+++ b/_modules/pyirf/interpolation/visible_edges_extrapolator.html
@@ -0,0 +1,330 @@
+
+
+
+
+
+
+
+ pyirf.interpolation.visible_edges_extrapolator — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Source code for pyirf.interpolation.visible_edges_extrapolator
+"""
+Extrapolators for Parametrized and DiscretePDF components that combine extrapolations
+from all visible simplices (by blending over visible edges) to get a smooth extrapolation
+outside the grids convex hull.
+"""
+importnumpyasnp
+fromscipy.spatialimportDelaunay
+
+from.nearest_simplex_extrapolatorimportParametrizedNearestSimplexExtrapolator
+from.utilsimportfind_simplex_to_facet,point_facet_angle
+
+__all__=["ParametrizedVisibleEdgesExtrapolator"]
+
+
+deffind_visible_facets(grid_points,target_point):
+"""
+ Find all facets of a convex hull visible from an outside point.
+
+ To do so, this function constructs a triangulation containing
+ the target point and returns all facets that span a triangulation simplex with
+ it.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(N, M)
+ Grid points at which templates exist. May be one ot two dimensional.
+ Have to be sorted in accending order for 1D.
+ target_point: numpy.ndarray, shape=(1, M)
+ Value for which the extrapolation is performed (target point)
+
+ Returns
+ -------
+ visible_facets: np.ndarray, shape=(L, M)
+ L visible facets, spanned by a simplex in M-1 dimensions
+ (thus a line for M=2)
+ """
+ # Build a triangulation including the target point
+ full_set=np.vstack((grid_points,target_point))
+ triag=Delaunay(full_set)
+
+ # The target point is included in a simplex with all facets
+ # visible by it
+ simplices=triag.points[triag.simplices]
+ matches_target=np.all(simplices==target_point,axis=-1)
+ target_in_simplex=np.any(matches_target,axis=-1)
+
+ # The visible facets are spanned by those points in the matched
+ # simplices that are not the target
+ facet_point_mask=~matches_target[target_in_simplex]
+ visible_facets=np.array(
+ [
+ triag.points[simplex[mask]]
+ forsimplex,maskinzip(
+ triag.simplices[target_in_simplex],facet_point_mask
+ )
+ ]
+ )
+
+ returnvisible_facets
+
+
+defcompute_extrapolation_weights(visible_facet_points,target_point,m):
+"""
+ Compute extrapolation weight according to [1].
+
+ Parameters
+ ----------
+ visible_facet_points: np.ndarray, shape=(L, M)
+ L facets visible from target_point
+ target_point: numpy.ndarray, shape=(1, M)
+ Value for which the extrapolation is performed (target point)
+
+ Returns
+ -------
+ extrapolation_weights: np.ndarray, shape=(L)
+ Weights for each visible facet, corresponding to the extrapolation
+ weight for the respective triangulation simplex. Weigths sum to unity.
+
+ References
+ ----------
+ .. [1] P. Alfred (1984). Triangular Extrapolation. Technical summary rept.,
+ Univ. of Wisconsin-Madison. https://apps.dtic.mil/sti/pdfs/ADA144660.pdf
+ """
+
+ angles=np.array(
+ [
+ point_facet_angle(line,target_point.squeeze())
+ forlineinvisible_facet_points
+ ]
+ )
+ weights=np.arccos(angles)**(m+1)
+
+ returnweights/weights.sum()
+
+
+
+[docs]
+classParametrizedVisibleEdgesExtrapolator(ParametrizedNearestSimplexExtrapolator):
+"""
+ Extrapolator using blending over visible edges.
+
+ While the ParametrizedNearestSimplexExtrapolator does not result in a smooth
+ extrapolation outside of the grid due to using only the nearest available
+ simplex, this extrapolator blends over all visible edges as discussed in [1].
+ For one grid-dimension this is equal to the ParametrizedNearestSimplexExtrapolator,
+ the same holds for grids consisting of only one simplex or constellations,
+ where only one simplex is visible from a target.
+
+ Parameters
+ ----------
+ grid_points: np.ndarray, shape=(N, ...)
+ Grid points at which templates exist. May be one ot two dimensional.
+ Have to be sorted in accending order for 1D.
+ params: np.ndarray, shape=(N, ...)
+ Array of corresponding parameter values at each point in grid_points.
+ First dimesion has to correspond to number of grid_points
+ m: non-zero int >= 1
+ Degree of smoothness wanted in the extrapolation region. See [1] for
+ additional information. Defaults to 1.
+
+ Raises
+ ------
+ TypeError:
+ If m is not a number
+ ValueError:
+ If m is not a non-zero integer
+
+ Note
+ ----
+ Also calls pyirf.interpolation.ParametrizedNearestSimplexExtrapolator.__init__.
+
+ References
+ ----------
+ .. [1] P. Alfred (1984). Triangular Extrapolation. Technical summary rept.,
+ Univ. of Wisconsin-Madison. https://apps.dtic.mil/sti/pdfs/ADA144660.pdf
+
+ """
+
+ def__init__(self,grid_points,params,m=1):
+ super().__init__(grid_points,params)
+
+ # Test wether m is a number
+ try:
+ m>0
+ exceptTypeError:
+ raiseTypeError(f"Only positive integers allowed for m, got {m}.")
+
+ # Test wether m is a finite, positive integer
+ if(m<=0)or~np.isfinite(m)or(m!=int(m)):
+ raiseValueError(f"Only positive integers allowed for m, got {m}.")
+
+ self.m=m
+
+
+[docs]
+ defextrapolate(self,target_point):
+ ifself.grid_dim==1:
+ returnsuper().extrapolate(target_point)
+ elifself.grid_dim==2:
+ visible_facet_points=find_visible_facets(self.grid_points,target_point)
+
+ ifvisible_facet_points.shape[0]==1:
+ returnsuper().extrapolate(target_point)
+ else:
+ simplices_points=self.triangulation.points[
+ self.triangulation.simplices
+ ]
+
+ visible_simplices_indices=np.array(
+ [
+ find_simplex_to_facet(simplices_points,facet)
+ forfacetinvisible_facet_points
+ ]
+ )
+
+ extrapolation_weigths=compute_extrapolation_weights(
+ visible_facet_points,target_point,self.m
+ )
+
+ extrapolation_weigths=extrapolation_weigths.reshape(
+ extrapolation_weigths.shape[0],
+ *np.ones(self.params.ndim-1,"int"),
+ )
+
+ # Function has to be copied outside list comprehention as the super() short-form
+ # cannot be used inside it (at least until python 3.11)
+ extrapolate2D=super()._extrapolate2D
+
+ simplex_extrapolations=np.array(
+ [
+ extrapolate2D(
+ self.triangulation.simplices[ind],target_point
+ ).squeeze()
+ forindinvisible_simplices_indices
+ ]
+ )
+
+ extrapolant=np.sum(
+ extrapolation_weigths*simplex_extrapolations,axis=0
+ )[np.newaxis,:]
+
+ returnextrapolant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/io/eventdisplay.html b/_modules/pyirf/io/eventdisplay.html
new file mode 100644
index 000000000..8a9476de2
--- /dev/null
+++ b/_modules/pyirf/io/eventdisplay.html
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+ pyirf.io.eventdisplay — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+defread_eventdisplay_fits(infile,use_histogram=True):
+"""
+ Read a DL2 FITS file as produced by the EventDisplay DL2 converter
+ from ROOT files:
+ https://github.com/Eventdisplay/Converters/blob/master/DL2/generate_DL2_file.py
+
+ Parameters
+ ----------
+ infile : str or pathlib.Path
+ Path to the input fits file
+ use_histogram : bool
+ If True, use number of simulated events from histogram provided in fits file,
+ if False, estimate this number from the unique run_id, pointing direction
+ combinations and the number of events per run in the run header.
+ This will fail e.g. for protons with cuts already applied, since many
+ runs will have 0 events surviving cuts.
+
+ Returns
+ -------
+ events: astropy.QTable
+ Astropy Table object containing the reconstructed events information.
+ simulated_events: ``~pyirf.simulations.SimulatedEventsInfo``
+ """
+ log.debug(f"Reading {infile}")
+ events=QTable.read(infile,hdu="EVENTS")
+ sim_events=QTable.read(infile,hdu="SIMULATED EVENTS")
+ run_header=QTable.read(infile,hdu="RUNHEADER")[0]
+
+ fornew,oldinCOLUMN_MAP.items():
+ events.rename_column(old,new)
+
+ n_runs=len(unique(events[['obs_id','pointing_az','pointing_alt']]))
+ log.info(f"Estimated number of runs from obs ids and pointing position: {n_runs}")
+
+ n_showers_guessed=n_runs*run_header["num_use"]*run_header["num_showers"]
+ n_showers_hist=int(sim_events["EVENTS"].sum())
+
+ ifuse_histogram:
+ n_showers=n_showers_hist
+ else:
+ n_showers=n_showers_guessed
+
+ log.debug("Number of events histogram: %d",n_showers_hist)
+ log.debug("Number of events from n_runs and run header: %d",n_showers_guessed)
+ log.debug("Using number of events from %s","histogram"ifuse_histogramelse"guess")
+
+ sim_info=SimulatedEventsInfo(
+ n_showers=n_showers,
+ energy_min=u.Quantity(run_header["E_range"][0],u.TeV),
+ energy_max=u.Quantity(run_header["E_range"][1],u.TeV),
+ max_impact=u.Quantity(run_header["core_range"][1],u.m),
+ spectral_index=run_header["spectral_index"],
+ viewcone_min=u.Quantity(run_header["viewcone"][0],u.deg),
+ viewcone_max=u.Quantity(run_header["viewcone"][1],u.deg),
+ )
+
+ returnevents,sim_info
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/io/gadf.html b/_modules/pyirf/io/gadf.html
new file mode 100644
index 000000000..8234a7812
--- /dev/null
+++ b/_modules/pyirf/io/gadf.html
@@ -0,0 +1,456 @@
+
+
+
+
+
+
+
+ pyirf.io.gadf — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+@u.quantity_input(
+ effective_area=u.m**2,true_energy_bins=u.TeV,fov_offset_bins=u.deg
+)
+defcreate_aeff2d_hdu(
+ effective_area,
+ true_energy_bins,
+ fov_offset_bins,
+ extname="EFFECTIVE AREA",
+ point_like=True,
+ **header_cards,
+):
+"""
+ Create a fits binary table HDU in GADF format for effective area.
+ See the specification at
+ https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/aeff/index.html
+
+ Parameters
+ ----------
+ effective_area: astropy.units.Quantity[area]
+ Effective area array, must have shape (n_energy_bins, n_fov_offset_bins)
+ true_energy_bins: astropy.units.Quantity[energy]
+ Bin edges in true energy
+ fov_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the field of view offset.
+ For Point-Like IRFs, only giving a single bin is appropriate.
+ point_like: bool
+ If the provided effective area was calculated after applying a direction cut,
+ pass ``True``, else ``False`` for a full-enclosure effective area.
+ extname: str
+ Name for BinTableHDU
+ **header_cards
+ Additional metadata to add to the header, use this to set e.g. TELESCOP or
+ INSTRUME.
+ """
+ aeff=QTable()
+ aeff["ENERG_LO"],aeff["ENERG_HI"]=binning.split_bin_lo_hi(true_energy_bins[np.newaxis,:].to(u.TeV))
+ aeff["THETA_LO"],aeff["THETA_HI"]=binning.split_bin_lo_hi(fov_offset_bins[np.newaxis,:].to(u.deg))
+ # transpose because FITS uses opposite dimension order than numpy
+ aeff["EFFAREA"]=effective_area.T[np.newaxis,...].to(u.m**2)
+
+ # required header keywords
+ header=DEFAULT_HEADER.copy()
+ header["HDUCLAS1"]="RESPONSE"
+ header["HDUCLAS2"]="EFF_AREA"
+ header["HDUCLAS3"]="POINT-LIKE"ifpoint_likeelse"FULL-ENCLOSURE"
+ header["HDUCLAS4"]="AEFF_2D"
+ header["DATE"]=Time.now().utc.iso
+ idx=aeff.colnames.index("EFFAREA")+1
+ header[f"CREF{idx}"]="(ENERG_LO:ENERG_HI,THETA_LO:THETA_HI)"
+ _add_header_cards(header,**header_cards)
+
+ returnBinTableHDU(aeff,header=header,name=extname)
+
+
+
+
+[docs]
+@u.quantity_input(
+ psf=u.sr**-1,
+ true_energy_bins=u.TeV,
+ source_offset_bins=u.deg,
+ fov_offset_bins=u.deg,
+)
+defcreate_psf_table_hdu(
+ psf,
+ true_energy_bins,
+ source_offset_bins,
+ fov_offset_bins,
+ extname="PSF",
+ **header_cards,
+):
+"""
+ Create a fits binary table HDU in GADF format for the PSF table.
+ See the specification at
+ https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/psf/psf_table/index.html
+
+ Parameters
+ ----------
+ psf: astropy.units.Quantity[(solid angle)^-1]
+ Point spread function array, must have shape
+ (n_energy_bins, n_fov_offset_bins, n_source_offset_bins)
+ true_energy_bins: astropy.units.Quantity[energy]
+ Bin edges in true energy
+ source_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the source offset.
+ fov_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the field of view offset.
+ For Point-Like IRFs, only giving a single bin is appropriate.
+ extname: str
+ Name for BinTableHDU
+ **header_cards
+ Additional metadata to add to the header, use this to set e.g. TELESCOP or
+ INSTRUME.
+ """
+
+ psf_=QTable()
+ psf_["ENERG_LO"],psf_["ENERG_HI"]=binning.split_bin_lo_hi(true_energy_bins[np.newaxis,:].to(u.TeV))
+ psf_["THETA_LO"],psf_["THETA_HI"]=binning.split_bin_lo_hi(fov_offset_bins[np.newaxis,:].to(u.deg))
+ psf_["RAD_LO"],psf_["RAD_HI"]=binning.split_bin_lo_hi(source_offset_bins[np.newaxis,:].to(u.deg))
+ # transpose as FITS uses opposite dimension order
+ psf_["RPSF"]=psf.T[np.newaxis,...].to(1/u.sr)
+
+ # required header keywords
+ header=DEFAULT_HEADER.copy()
+ header["HDUCLAS1"]="RESPONSE"
+ header["HDUCLAS2"]="PSF"
+ header["HDUCLAS3"]="FULL-ENCLOSURE"
+ header["HDUCLAS4"]="PSF_TABLE"
+ header["DATE"]=Time.now().utc.iso
+ idx=psf_.colnames.index("RPSF")+1
+ header[f"CREF{idx}"]="(ENERG_LO:ENERG_HI,THETA_LO:THETA_HI,RAD_LO:RAD_HI)"
+ _add_header_cards(header,**header_cards)
+
+ returnBinTableHDU(psf_,header=header,name=extname)
+
+
+
+
+[docs]
+@u.quantity_input(
+ true_energy_bins=u.TeV,fov_offset_bins=u.deg,
+)
+defcreate_energy_dispersion_hdu(
+ energy_dispersion,
+ true_energy_bins,
+ migration_bins,
+ fov_offset_bins,
+ point_like=True,
+ extname="EDISP",
+ **header_cards,
+):
+"""
+ Create a fits binary table HDU in GADF format for the energy dispersion.
+ See the specification at
+ https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/edisp/index.html
+
+ Parameters
+ ----------
+ energy_dispersion: numpy.ndarray
+ Energy dispersion array, must have shape
+ (n_energy_bins, n_migra_bins, n_source_offset_bins)
+ true_energy_bins: astropy.units.Quantity[energy]
+ Bin edges in true energy
+ migration_bins: numpy.ndarray
+ Bin edges for the relative energy migration (``reco_energy / true_energy``)
+ fov_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the field of view offset.
+ For Point-Like IRFs, only giving a single bin is appropriate.
+ point_like: bool
+ If the provided effective area was calculated after applying a direction cut,
+ pass ``True``, else ``False`` for a full-enclosure effective area.
+ extname: str
+ Name for BinTableHDU
+ **header_cards
+ Additional metadata to add to the header, use this to set e.g. TELESCOP or
+ INSTRUME.
+ """
+
+ edisp=QTable()
+ edisp["ENERG_LO"],edisp["ENERG_HI"]=binning.split_bin_lo_hi(true_energy_bins[np.newaxis,:].to(u.TeV))
+ edisp["MIGRA_LO"],edisp["MIGRA_HI"]=binning.split_bin_lo_hi(migration_bins[np.newaxis,:])
+ edisp["THETA_LO"],edisp["THETA_HI"]=binning.split_bin_lo_hi(fov_offset_bins[np.newaxis,:].to(u.deg))
+ # transpose as FITS uses opposite dimension order
+ edisp["MATRIX"]=u.Quantity(energy_dispersion.T[np.newaxis,...]).to(u.one)
+
+ # required header keywords
+ header=DEFAULT_HEADER.copy()
+ header["HDUCLAS1"]="RESPONSE"
+ header["HDUCLAS2"]="EDISP"
+ header["HDUCLAS3"]="POINT-LIKE"ifpoint_likeelse"FULL-ENCLOSURE"
+ header["HDUCLAS4"]="EDISP_2D"
+ header["DATE"]=Time.now().utc.iso
+ idx=edisp.colnames.index("MATRIX")+1
+ header[f"CREF{idx}"]="(ENERG_LO:ENERG_HI,MIGRA_LO:MIGRA_HI,THETA_LO:THETA_HI)"
+ _add_header_cards(header,**header_cards)
+
+ returnBinTableHDU(edisp,header=header,name=extname)
+
+
+
+#: Unit to store background rate in GADF format
+#:
+#: see https://github.com/open-gamma-ray-astro/gamma-astro-data-formats/issues/153
+#: for a discussion on why this is MeV not TeV as everywhere else
+GADF_BACKGROUND_UNIT=u.Unit("MeV-1 s-1 sr-1")
+
+
+
+[docs]
+@u.quantity_input(
+ background=GADF_BACKGROUND_UNIT,reco_energy_bins=u.TeV,fov_offset_bins=u.deg,
+)
+defcreate_background_2d_hdu(
+ background_2d,
+ reco_energy_bins,
+ fov_offset_bins,
+ extname="BACKGROUND",
+ **header_cards,
+):
+"""
+ Create a fits binary table HDU in GADF format for the background 2d table.
+ See the specification at
+ https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/bkg/index.html#bkg-2d
+
+ Parameters
+ ----------
+ background_2d: astropy.units.Quantity[(MeV s sr)^-1]
+ Background rate, must have shape
+ (n_energy_bins, n_fov_offset_bins)
+ reco_energy_bins: astropy.units.Quantity[energy]
+ Bin edges in reconstructed energy
+ fov_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the field of view offset.
+ extname: str
+ Name for BinTableHDU
+ **header_cards
+ Additional metadata to add to the header, use this to set e.g. TELESCOP or
+ INSTRUME.
+ """
+
+ bkg=QTable()
+ bkg["ENERG_LO"],bkg["ENERG_HI"]=binning.split_bin_lo_hi(reco_energy_bins[np.newaxis,:].to(u.TeV))
+ bkg["THETA_LO"],bkg["THETA_HI"]=binning.split_bin_lo_hi(fov_offset_bins[np.newaxis,:].to(u.deg))
+ # transpose as FITS uses opposite dimension order
+ bkg["BKG"]=background_2d.T[np.newaxis,...].to(GADF_BACKGROUND_UNIT)
+
+ # required header keywords
+ header=DEFAULT_HEADER.copy()
+ header["HDUCLAS1"]="RESPONSE"
+ header["HDUCLAS2"]="BKG"
+ header["HDUCLAS3"]="FULL-ENCLOSURE"
+ header["HDUCLAS4"]="BKG_2D"
+ header["DATE"]=Time.now().utc.iso
+ idx=bkg.colnames.index("BKG")+1
+ header[f"CREF{idx}"]="(ENERG_LO:ENERG_HI,THETA_LO:THETA_HI)"
+ _add_header_cards(header,**header_cards)
+
+ returnBinTableHDU(bkg,header=header,name=extname)
+
+
+
+
+[docs]
+@u.quantity_input(
+ rad_max=u.deg,
+ reco_energy_bins=u.TeV,
+ fov_offset_bins=u.deg,
+)
+defcreate_rad_max_hdu(
+ rad_max,
+ reco_energy_bins,
+ fov_offset_bins,
+ point_like=True,
+ extname="RAD_MAX",
+ **header_cards,
+):
+"""
+ Create a fits binary table HDU in GADF format for the directional cut.
+ See the specification at
+ https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/point_like/index.html#rad-max
+
+ Parameters
+ ----------
+ rad_max: astropy.units.Quantity[angle]
+ Array of the directional (theta) cut.
+ Must have shape (n_reco_energy_bins, n_fov_offset_bins)
+ reco_energy_bins: astropy.units.Quantity[energy]
+ Bin edges in reconstructed energy
+ fov_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the field of view offset.
+ For Point-Like IRFs, only giving a single bin is appropriate.
+ extname: str
+ Name for BinTableHDU
+ **header_cards
+ Additional metadata to add to the header, use this to set e.g. TELESCOP or
+ INSTRUME.
+ """
+
+ rad_max_table=QTable()
+ rad_max_table["ENERG_LO"],rad_max_table["ENERG_HI"]=binning.split_bin_lo_hi(reco_energy_bins[np.newaxis,:].to(u.TeV))
+ rad_max_table["THETA_LO"],rad_max_table["THETA_HI"]=binning.split_bin_lo_hi(fov_offset_bins[np.newaxis,:].to(u.deg))
+ # transpose as FITS uses opposite dimension order
+ rad_max_table["RAD_MAX"]=rad_max.T[np.newaxis,...].to(u.deg)
+
+ # required header keywords
+ header=DEFAULT_HEADER.copy()
+ header["HDUCLAS1"]="RESPONSE"
+ header["HDUCLAS2"]="RAD_MAX"
+ header["HDUCLAS3"]="POINT-LIKE"
+ header["HDUCLAS4"]="RAD_MAX_2D"
+ header["DATE"]=Time.now().utc.iso
+ idx=rad_max_table.colnames.index("RAD_MAX")+1
+ header[f"CREF{idx}"]="(ENERG_LO:ENERG_HI,THETA_LO:THETA_HI)"
+ _add_header_cards(header,**header_cards)
+
+ returnBinTableHDU(rad_max_table,header=header,name=extname)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/irf/background.html b/_modules/pyirf/irf/background.html
new file mode 100644
index 000000000..415c9fc01
--- /dev/null
+++ b/_modules/pyirf/irf/background.html
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+ pyirf.irf.background — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+importastropy.unitsasu
+importnumpyasnp
+
+from..utilsimportcone_solid_angle
+
+#: Unit of the background rate IRF
+BACKGROUND_UNIT=u.Unit('s-1 TeV-1 sr-1')
+
+
+
+[docs]
+defbackground_2d(events,reco_energy_bins,fov_offset_bins,t_obs):
+"""
+ Calculate background rates in radially symmetric bins in the field of view.
+
+ GADF documentation here:
+ https://gamma-astro-data-formats.readthedocs.io/en/latest/irfs/full_enclosure/bkg/index.html#bkg-2d
+
+ Parameters
+ ----------
+ events: astropy.table.QTable
+ DL2 events table of the selected background events.
+ Needed columns for this function: `reco_source_fov_offset`, `reco_energy`, `weight`
+ reco_energy: astropy.units.Quantity[energy]
+ The bins in reconstructed energy to be used for the IRF
+ fov_offset_bins: astropy.units.Quantity[angle]
+ The bins in the field of view offset to be used for the IRF
+ t_obs: astropy.units.Quantity[time]
+ Observation time. This must match with how the individual event
+ weights are calculated.
+
+ Returns
+ -------
+ bg_rate: astropy.units.Quantity
+ The background rate as particles per energy, time and solid angle
+ in the specified bins.
+
+ Shape: (len(reco_energy_bins) - 1, len(fov_offset_bins) - 1)
+ """
+
+ hist,_,_=np.histogram2d(
+ events["reco_energy"].to_value(u.TeV),
+ events["reco_source_fov_offset"].to_value(u.deg),
+ bins=[
+ reco_energy_bins.to_value(u.TeV),
+ fov_offset_bins.to_value(u.deg),
+ ],
+ weights=events['weight'],
+ )
+
+ # divide all energy bins by their width
+ # hist has shape (n_energy, n_fov_offset) so we need to transpose and then back
+ bin_width_energy=np.diff(reco_energy_bins)
+ per_energy=(hist.T/bin_width_energy).T
+
+ # divide by solid angle in each fov bin and the observation time
+ bin_solid_angle=np.diff(cone_solid_angle(fov_offset_bins))
+ bg_rate=per_energy/t_obs/bin_solid_angle
+
+ returnbg_rate.to(BACKGROUND_UNIT)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/irf/effective_area.html b/_modules/pyirf/irf/effective_area.html
new file mode 100644
index 000000000..336eaa3d3
--- /dev/null
+++ b/_modules/pyirf/irf/effective_area.html
@@ -0,0 +1,331 @@
+
+
+
+
+
+
+
+ pyirf.irf.effective_area — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+@u.quantity_input(area=u.m**2)
+defeffective_area(n_selected,n_simulated,area):
+"""
+ Calculate effective area for histograms of selected and total simulated events
+
+ Parameters
+ ----------
+ n_selected: int or numpy.ndarray[int]
+ The number of surviving (e.g. triggered, analysed, after cuts)
+ n_simulated: int or numpy.ndarray[int]
+ The total number of events simulated
+ area: astropy.units.Quantity[area]
+ Area in which particle's core position was simulated
+ """
+ return(n_selected/n_simulated)*area
+
+
+
+
+[docs]
+defeffective_area_per_energy(selected_events,simulation_info,true_energy_bins):
+"""
+ Calculate effective area in bins of true energy.
+
+ Parameters
+ ----------
+ selected_events: astropy.table.QTable
+ DL2 events table, required columns for this function: `true_energy`.
+ simulation_info: pyirf.simulations.SimulatedEventsInfo
+ The overall statistics of the simulated events
+ true_energy_bins: astropy.units.Quantity[energy]
+ The bin edges in which to calculate effective area.
+ """
+ area=np.pi*simulation_info.max_impact**2
+
+ hist_selected=create_histogram_table(
+ selected_events,true_energy_bins,"true_energy"
+ )
+ hist_simulated=simulation_info.calculate_n_showers_per_energy(true_energy_bins)
+
+ returneffective_area(hist_selected["n"],hist_simulated,area)
+
+
+
+
+[docs]
+defeffective_area_per_energy_and_fov(
+ selected_events,simulation_info,true_energy_bins,fov_offset_bins
+):
+"""
+ Calculate effective area in bins of true energy and field of view offset.
+
+ Parameters
+ ----------
+ selected_events: astropy.table.QTable
+ DL2 events table, required columns for this function:
+ - `true_energy`
+ - `true_source_fov_offset`
+ simulation_info: pyirf.simulations.SimulatedEventsInfo
+ The overall statistics of the simulated events
+ true_energy_bins: astropy.units.Quantity[energy]
+ The true energy bin edges in which to calculate effective area.
+ fov_offset_bins: astropy.units.Quantity[angle]
+ The field of view radial bin edges in which to calculate effective area.
+ """
+ area=np.pi*simulation_info.max_impact**2
+
+ hist_simulated=simulation_info.calculate_n_showers_per_energy_and_fov(
+ true_energy_bins,fov_offset_bins
+ )
+
+ hist_selected,_,_=np.histogram2d(
+ selected_events["true_energy"].to_value(u.TeV),
+ selected_events["true_source_fov_offset"].to_value(u.deg),
+ bins=[
+ true_energy_bins.to_value(u.TeV),
+ fov_offset_bins.to_value(u.deg),
+ ],
+ )
+
+ returneffective_area(hist_selected,hist_simulated,area)
+
+
+
+
+[docs]
+defeffective_area_3d_polar(
+ selected_events,
+ simulation_info,
+ true_energy_bins,
+ fov_offset_bins,
+ fov_position_angle_bins,
+):
+"""
+ Calculate effective area in bins of true energy, field of view offset, and field of view position angle.
+
+ Parameters
+ ----------
+ selected_events: astropy.table.QTable
+ DL2 events table, required columns for this function:
+ - `true_energy`
+ - `true_source_fov_offset`
+ - `true_source_fov_position_angle`
+ simulation_info: pyirf.simulations.SimulatedEventsInfo
+ The overall statistics of the simulated events
+ true_energy_bins: astropy.units.Quantity[energy]
+ The true energy bin edges in which to calculate effective area.
+ fov_offset_bins: astropy.units.Quantity[angle]
+ The field of view radial bin edges in which to calculate effective area.
+ fov_position_angle_bins: astropy.units.Quantity[radian]
+ The field of view azimuthal bin edges in which to calculate effective area.
+ """
+ area=np.pi*simulation_info.max_impact**2
+
+ hist_simulated=simulation_info.calculate_n_showers_3d_polar(
+ true_energy_bins,fov_offset_bins,fov_position_angle_bins
+ )
+
+ hist_selected,_=np.histogramdd(
+ np.column_stack(
+ [
+ selected_events["true_energy"].to_value(u.TeV),
+ selected_events["true_source_fov_offset"].to_value(u.deg),
+ selected_events["true_source_fov_position_angle"].to_value(u.rad),
+ ]
+ ),
+ bins=(
+ true_energy_bins.to_value(u.TeV),
+ fov_offset_bins.to_value(u.deg),
+ fov_position_angle_bins.to_value(u.rad),
+ ),
+ )
+
+ returneffective_area(hist_selected,hist_simulated,area)
+
+
+
+
+[docs]
+defeffective_area_3d_lonlat(
+ selected_events,
+ simulation_info,
+ true_energy_bins,
+ fov_longitude_bins,
+ fov_latitude_bins,
+ subpixels=20,
+):
+"""
+ Calculate effective area in bins of true energy, field of view longitude, and field of view latitude.
+
+ Parameters
+ ----------
+ selected_events: astropy.table.QTable
+ DL2 events table, required columns for this function:
+ - `true_energy`
+ - `true_source_fov_lon`
+ - `true_source_fov_lat`
+ simulation_info: pyirf.simulations.SimulatedEventsInfo
+ The overall statistics of the simulated events
+ true_energy_bins: astropy.units.Quantity[energy]
+ The true energy bin edges in which to calculate effective area.
+ fov_longitude_bins: astropy.units.Quantity[angle]
+ The field of view longitude bin edges in which to calculate effective area.
+ fov_latitude_bins: astropy.units.Quantity[angle]
+ The field of view latitude bin edges in which to calculate effective area.
+ """
+ area=np.pi*simulation_info.max_impact**2
+
+ hist_simulated=simulation_info.calculate_n_showers_3d_lonlat(
+ true_energy_bins,fov_longitude_bins,fov_latitude_bins,subpixels=subpixels
+ )
+
+ selected_columns=np.column_stack(
+ [
+ selected_events["true_energy"].to_value(u.TeV),
+ selected_events["true_source_fov_lon"].to_value(u.deg),
+ selected_events["true_source_fov_lat"].to_value(u.deg),
+ ]
+ )
+ bins=(
+ true_energy_bins.to_value(u.TeV),
+ fov_longitude_bins.to_value(u.deg),
+ fov_latitude_bins.to_value(u.deg),
+ )
+
+ hist_selected,_=np.histogramdd(selected_columns,bins=bins)
+
+ returneffective_area(hist_selected,hist_simulated,area)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/irf/energy_dispersion.html b/_modules/pyirf/irf/energy_dispersion.html
new file mode 100644
index 000000000..39a865855
--- /dev/null
+++ b/_modules/pyirf/irf/energy_dispersion.html
@@ -0,0 +1,346 @@
+
+
+
+
+
+
+
+ pyirf.irf.energy_dispersion — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+importwarnings
+importnumpyasnp
+importastropy.unitsasu
+from..binningimportresample_histogram1d
+
+
+__all__=[
+ "energy_dispersion",
+ "energy_migration_matrix",
+ "energy_dispersion_to_migration",
+]
+
+
+def_normalize_hist(hist,migration_bins):
+ # make sure we do not mutate the input array
+ hist=hist.copy()
+ bin_width=np.diff(migration_bins)
+
+ # calculate number of events along the N_MIGRA axis to get events
+ # per energy per fov
+ norm=hist.sum(axis=1)
+
+ withnp.errstate(invalid="ignore"):
+ # hist shape is (N_E, N_MIGRA, N_FOV), norm shape is (N_E, N_FOV)
+ # so we need to add a new axis in the middle to get (N_E, 1, N_FOV)
+ # bin_width is 1d, so we need newaxis, use the values, newaxis
+ hist=hist/norm[:,np.newaxis,:]/bin_width[np.newaxis,:,np.newaxis]
+
+ returnnp.nan_to_num(hist)
+
+
+
+[docs]
+defenergy_dispersion(
+ selected_events,
+ true_energy_bins,
+ fov_offset_bins,
+ migration_bins,
+):
+"""
+ Calculate energy dispersion for the given DL2 event list.
+ Energy dispersion is defined as the probability of finding an event
+ at a given relative deviation ``(reco_energy / true_energy)`` for a given
+ true energy.
+
+ Parameters
+ ----------
+ selected_events: astropy.table.QTable
+ Table of the DL2 events.
+ Required columns: ``reco_energy``, ``true_energy``, ``true_source_fov_offset``.
+ true_energy_bins: astropy.units.Quantity[energy]
+ Bin edges in true energy
+ migration_bins: astropy.units.Quantity[energy]
+ Bin edges in relative deviation, recommended range: [0.2, 5]
+ fov_offset_bins: astropy.units.Quantity[angle]
+ Bin edges in the field of view offset.
+ For Point-Like IRFs, only giving a single bin is appropriate.
+
+ Returns
+ -------
+ energy_dispersion: numpy.ndarray
+ Energy dispersion matrix
+ with shape (n_true_energy_bins, n_migration_bins, n_fov_ofset_bins)
+ """
+ mu=(selected_events["reco_energy"]/selected_events["true_energy"]).to_value(
+ u.one
+ )
+
+ energy_dispersion,_=np.histogramdd(
+ np.column_stack(
+ [
+ selected_events["true_energy"].to_value(u.TeV),
+ mu,
+ selected_events["true_source_fov_offset"].to_value(u.deg),
+ ]
+ ),
+ bins=[
+ true_energy_bins.to_value(u.TeV),
+ migration_bins,
+ fov_offset_bins.to_value(u.deg),
+ ],
+ )
+
+ energy_dispersion=_normalize_hist(energy_dispersion,migration_bins)
+
+ returnenergy_dispersion
+
+
+
+@u.quantity_input(true_energy_bins=u.TeV,reco_energy_bins=u.TeV,fov_offset_bins=u.deg)
+defenergy_migration_matrix(
+ events,true_energy_bins,reco_energy_bins,fov_offset_bins
+):
+"""Compute the energy migration matrix directly from the events.
+
+ Parameters
+ ----------
+ events : `~astropy.table.QTable`
+ Table of the DL2 events.
+ Required columns: ``reco_energy``, ``true_energy``, ``true_source_fov_offset``.
+ true_energy_bins : `~astropy.units.Quantity`
+ Bin edges in true energy.
+ reco_energy_bins : `~astropy.units.Quantity`
+ Bin edges in reconstructed energy.
+
+ Returns
+ -------
+ matrix : array-like
+ Migration matrix as probabilities along the reconstructed energy axis.
+ energy axis with shape
+ (n_true_energy_bins, n_reco_energy_bins, n_fov_offset_bins)
+ containing energies in TeV.
+ """
+
+ hist,_=np.histogramdd(
+ np.column_stack(
+ [
+ events["true_energy"].to_value(u.TeV),
+ events["reco_energy"].to_value(u.TeV),
+ events["true_source_fov_offset"].to_value(u.deg),
+ ]
+ ),
+ bins=[
+ true_energy_bins.to_value(u.TeV),
+ reco_energy_bins.to_value(u.TeV),
+ fov_offset_bins.to_value(u.deg),
+ ],
+ )
+
+ withnp.errstate(invalid="ignore"):
+ hist/=hist.sum(axis=1)[:,np.newaxis,:]
+ # the nans come from the fact that the sum along the reconstructed energy axis
+ # might sometimes be 0 when there are no events in that given true energy bin
+ # and fov offset bin
+ hist[np.isnan(hist)]=0
+
+ returnhist
+
+
+defenergy_dispersion_to_migration(
+ dispersion_matrix,
+ disp_true_energy_edges,
+ disp_migration_edges,
+ new_true_energy_edges,
+ new_reco_energy_edges,
+):
+"""
+ Construct a energy migration matrix from an energy dispersion matrix.
+
+ Depending on the new energy ranges, the sum over the first axis
+ can be smaller than 1.
+ The new true energy bins need to be a subset of the old range,
+ extrapolation is not supported.
+ New reconstruction bins outside of the old migration range are filled with
+ zeros.
+
+ Parameters
+ ----------
+ dispersion_matrix: numpy.ndarray
+ Energy dispersion_matrix
+ disp_true_energy_edges: astropy.units.Quantity[energy]
+ True energy edges matching the first dimension of the dispersion matrix
+ disp_migration_edges: numpy.ndarray
+ Migration edges matching the second dimension of the dispersion matrix
+ new_true_energy_edges: astropy.units.Quantity[energy]
+ True energy edges matching the first dimension of the output
+ new_reco_energy_edges: astropy.units.Quantity[energy]
+ Reco energy edges matching the second dimension of the output
+
+ Returns
+ -------
+ migration_matrix: numpy.ndarray
+ Three-dimensional energy migration matrix. The third dimension
+ equals the fov offset dimension of the energy dispersion matrix.
+ """
+ migration_matrix=np.zeros(
+ (
+ len(new_true_energy_edges)-1,
+ len(new_reco_energy_edges)-1,
+ dispersion_matrix.shape[2],
+ )
+ )
+
+ migra_width=np.diff(disp_migration_edges)
+ probability=dispersion_matrix*migra_width[np.newaxis,:,np.newaxis]
+
+ true_energy_interpolation=resample_histogram1d(
+ probability,
+ disp_true_energy_edges,
+ new_true_energy_edges,
+ axis=0,
+ )
+
+ norm=np.sum(true_energy_interpolation,axis=1,keepdims=True)
+ norm[norm==0]=1
+ true_energy_interpolation/=norm
+
+ foridx,e_trueinenumerate(
+ (new_true_energy_edges[1:]+new_true_energy_edges[:-1])/2
+ ):
+ # get migration for the new true energy bin
+ e_true_dispersion=true_energy_interpolation[idx]
+
+ withwarnings.catch_warnings():
+ # silence inf/inf division warning
+ warnings.filterwarnings(
+ "ignore","invalid value encountered in true_divide"
+ )
+ interpolation_edges=new_reco_energy_edges/e_true
+
+ y=resample_histogram1d(
+ e_true_dispersion,
+ disp_migration_edges,
+ interpolation_edges,
+ axis=0,
+ )
+
+ migration_matrix[idx,:,:]=y
+
+ returnmigration_matrix
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/irf/psf.html b/_modules/pyirf/irf/psf.html
new file mode 100644
index 000000000..e340d88af
--- /dev/null
+++ b/_modules/pyirf/irf/psf.html
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
+ pyirf.irf.psf — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+Functions to calculate sensitivity
+"""
+
+importnumpyasnp
+fromscipy.optimizeimportbrentq
+importlogging
+
+fromastropy.tableimportQTable
+importastropy.unitsasu
+
+from.compatimportCOPY_IF_NEEDED
+from.statisticsimportli_ma_significance
+from.utilsimportcheck_histograms,cone_solid_angle
+from.binningimportcreate_histogram_table,bin_center
+
+
+__all__=[
+ "relative_sensitivity",
+ "calculate_sensitivity",
+ "estimate_background",
+]
+
+
+log=logging.getLogger(__name__)
+
+
+def_relative_sensitivity(
+ n_on,
+ n_off,
+ alpha,
+ min_significance=5,
+ min_signal_events=10,
+ min_excess_over_background=0.05,
+ significance_function=li_ma_significance,
+):
+ ifnp.isnan(n_on)ornp.isnan(n_off):
+ returnnp.nan
+
+ ifn_on<0orn_off<0:
+ raiseValueError(f"n_on and n_off must be positive, got {n_on}, {n_off}")
+
+ n_background=n_off*alpha
+ n_signal=n_on-n_background
+
+ ifn_signal<=0:
+ returnnp.inf
+
+ defequation(relative_flux):
+ n_on=n_signal*relative_flux+n_background
+ s=significance_function(n_on,n_off,alpha)
+ returns-min_significance
+
+ try:
+ # brentq needs a lower and an upper bound
+ # we will use the simple, analytically solvable significance formula and scale it
+ # with 10 to be sure it's above the Li and Ma solution
+ # so rel * n_signal / sqrt(n_background) = target_significance
+ ifn_off>1:
+ relative_flux_naive=min_significance*np.sqrt(n_background)/n_signal
+ upper_bound=10*relative_flux_naive
+ lower_bound=0.01*relative_flux_naive
+ else:
+ upper_bound=100
+ lower_bound=1e-4
+
+ relative_flux=brentq(equation,lower_bound,upper_bound)
+
+ except(RuntimeError,ValueError)ase:
+ log.warning(
+ "Could not calculate relative significance for"
+ f" n_signal={n_signal:.1f}, n_off={n_off:.1f}, returning nan {e}"
+ )
+ returnnp.nan
+
+ # scale to achieved flux level
+ n_signal=n_signal*relative_flux
+ min_excess=min_excess_over_background*n_background
+ min_signal=max(min_signal_events,min_excess)
+
+ # if we violate the min signal events condition,
+ # increase flux until we meet the requirement
+ ifn_signal<min_signal:
+ scale=min_signal/n_signal
+ else:
+ scale=1.0
+
+ returnrelative_flux*scale
+
+
+_relative_sensitivity_vectorized=np.vectorize(
+ _relative_sensitivity,excluded=["significance_function"]
+)
+
+
+
+[docs]
+defrelative_sensitivity(
+ n_on,
+ n_off,
+ alpha,
+ min_significance=5,
+ min_signal_events=10,
+ min_excess_over_background=0.05,
+ significance_function=li_ma_significance,
+):
+"""
+ Calculate the relative sensitivity defined as the flux
+ relative to the reference source that is detectable with
+ significance ``target_significance``.
+
+ Given measured ``n_on`` and ``n_off``,
+ we estimate the number of gamma events ``n_signal`` as ``n_on - alpha * n_off``.
+
+ The number of background events ``n_background` is estimated as ``n_off * alpha``.
+
+ In the end, we find the relative sensitivity as the scaling factor for ``n_signal``
+ that yields a significance of ``target_significance``.
+
+ The reference time should be incorporated by appropriately weighting the events
+ before calculating ``n_on`` and ``n_off``.
+
+ All input values with the exception of ``significance_function``
+ must be broadcastable to a single, common shape.
+
+ Parameters
+ ----------
+ n_on: int or array-like
+ Number of signal-like events for the on observations
+ n_off: int or array-like
+ Number of signal-like events for the off observations
+ alpha: float or array-like
+ Scaling factor between on and off observations.
+ 1 / number of off regions for wobble observations.
+ min_significance: float or array-like
+ Significance necessary for a detection
+ min_signal_events: int or array-like
+ Minimum number of signal events required.
+ The relative flux will be scaled up from the one yielding ``min_significance``
+ if this condition is violated.
+ min_excess_over_background: float or array-like
+ Minimum number of signal events expressed as the proportion of the
+ background.
+ So the required number of signal events will be
+ ``min_excess_over_background * alpha * n_off``.
+ The relative flux will be scaled up from the one yielding ``min_significance``
+ if this condition is violated.
+ significance_function: function
+ A function f(n_on, n_off, alpha) -> significance in sigma
+ Used to calculate the significance, default is the Li&Ma
+ likelihood ratio test formula.
+ Li, T-P., and Y-Q. Ma.
+ "Analysis methods for results in gamma-ray astronomy."
+ The Astrophysical Journal 272 (1983): 317-324.
+ Formula (17)
+ """
+ return_relative_sensitivity_vectorized(
+ n_on=n_on,
+ n_off=n_off,
+ alpha=alpha,
+ min_significance=min_significance,
+ min_signal_events=min_signal_events,
+ min_excess_over_background=min_excess_over_background,
+ significance_function=significance_function,
+ )
+
+
+
+
+[docs]
+defcalculate_sensitivity(
+ signal_hist,
+ background_hist,
+ alpha,
+ min_significance=5,
+ min_signal_events=10,
+ min_excess_over_background=0.05,
+ significance_function=li_ma_significance,
+):
+"""
+ Calculate sensitivity for DL2 event lists in bins of reconstructed energy.
+
+ Sensitivity is defined as the minimum flux detectable with ``target_significance``
+ sigma significance in a certain time.
+
+ This time must be incorporated into the event weights.
+
+ Two conditions are required for the sensitivity:
+ - At least ten weighted signal events
+ - The weighted signal must be larger than 5 % of the weighted background
+ - At least 5 sigma (so relative_sensitivity > 1)
+
+ If the conditions are not met, the sensitivity will be set to nan.
+
+ Parameters
+ ----------
+ signal_hist: astropy.table.QTable
+ Histogram of detected signal events as a table.
+ Required columns: n and n_weighted.
+ See ``pyirf.binning.create_histogram_table``
+ background_hist: astropy.table.QTable
+ Histogram of detected events as a table.
+ Required columns: n and n_weighted.
+ See ``pyirf.binning.create_histogram_table``
+ alpha: float
+ Size ratio of signal region to background region
+ min_significance: float
+ Significance necessary for a detection
+ min_signal_events: int
+ Minimum number of signal events required.
+ The relative flux will be scaled up from the one yielding ``min_significance``
+ if this condition is violated.
+ min_excess_over_background: float
+ Minimum number of signal events expressed as the proportion of the
+ background.
+ So the required number of signal events will be
+ ``min_excess_over_background * alpha * n_off``.
+ The relative flux will be scaled up from the one yielding ``min_significance``
+ if this condition is violated.
+ significance_function: callable
+ A function with signature (n_on, n_off, alpha) -> significance.
+ Default is the Li & Ma likelihood ratio test.
+
+ Returns
+ -------
+ sensitivity_table: astropy.table.QTable
+ Table with sensitivity information.
+ Contains weighted and unweighted number of signal and background events
+ and the ``relative_sensitivity``, the scaling applied to the signal events
+ that yields ``target_significance`` sigma of significance according to
+ the ``significance_function``
+ """
+ check_histograms(signal_hist,background_hist)
+
+ n_on=signal_hist["n_weighted"]+alpha*background_hist["n_weighted"]
+
+ # convert any quantities to arrays,
+ # since quantitites don't work with vectorize
+ n_on=u.Quantity(n_on,copy=COPY_IF_NEEDED).to_value(u.one)
+ n_off=u.Quantity(background_hist["n_weighted"],copy=COPY_IF_NEEDED).to_value(
+ u.one
+ )
+
+ # calculate sensitivity in each bin
+ rel_sens=relative_sensitivity(
+ n_on=n_on,
+ n_off=n_off,
+ alpha=alpha,
+ min_significance=min_significance,
+ min_signal_events=min_signal_events,
+ min_excess_over_background=min_excess_over_background,
+ significance_function=significance_function,
+ )
+
+ # fill output table
+ s=QTable()
+ forkeyin("low","high","center"):
+ k="reco_energy_"+key
+ s[k]=signal_hist[k]
+
+ withnp.errstate(invalid="ignore"):
+ s["n_signal"]=signal_hist["n"]*rel_sens
+ s["n_signal_weighted"]=signal_hist["n_weighted"]*rel_sens
+ s["n_background"]=background_hist["n"]
+ s["n_background_weighted"]=background_hist["n_weighted"]
+
+ # copy also "n_proton" / "n_electron_weighted" etc. if available
+ forkinfilter(
+ lambdac:c.startswith("n_")andc!="n_weighted",background_hist.colnames
+ ):
+ s[k]=background_hist[k]
+
+ s["significance"]=significance_function(
+ n_on=s["n_signal_weighted"]+alpha*s["n_background_weighted"],
+ n_off=s["n_background_weighted"],
+ alpha=alpha,
+ )
+ s["relative_sensitivity"]=rel_sens
+
+ returns
+
+
+
+
+[docs]
+defestimate_background(
+ events,
+ reco_energy_bins,
+ theta_cuts,
+ alpha,
+ fov_offset_min,
+ fov_offset_max,
+):
+"""
+ Estimate the number of background events for a point-like sensitivity.
+
+ Due to limited statistics, it is often not possible to just apply the same
+ theta cut to the background events as to the signal events around an assumed
+ source position.
+
+ Here we calculate the expected number of background events for the off
+ regions by taking all background events between ``fov_offset_min`` and
+ ``fov_offset_max`` from the camera center and then scale these to the size
+ of the off region, which is scaled by 1 / alpha from the size of the on
+ region given by the theta cuts.
+
+
+ Parameters
+ ----------
+ events: astropy.table.QTable
+ DL2 event list of background surviving event selection
+ and inside ``fov_offset_max`` from the center of the FOV
+ Required columns for this function:
+ - `reco_energy`,
+ - `reco_source_fov_offset`.
+ reco_energy_bins: astropy.units.Quantity[energy]
+ Desired bin edges in reconstructed energy for the background rate
+ theta_cuts: astropy.table.QTable
+ The cuts table for the theta cut,
+ e.g. as returned by ``~pyirf.cuts.calculate_percentile_cut``.
+ Columns `center` and `cut` are required for this function.
+ alpha: float
+ size of the on region divided by the size of the off region.
+ fov_offset_min: astropy.units.Quantity[angle]
+ Minimum distance from the fov center for background events to be taken into account
+ fov_offset_max: astropy.units.Quantity[angle]
+ Maximum distance from the fov center for background events to be taken into account
+ """
+ in_ring=(events["reco_source_fov_offset"]>=fov_offset_min)&(
+ events["reco_source_fov_offset"]<fov_offset_max
+ )
+
+ bg=create_histogram_table(
+ events[in_ring],
+ reco_energy_bins,
+ key="reco_energy",
+ )
+
+ # scale number of background events according to the on region size
+ # background radius and alpha
+ center=bin_center(reco_energy_bins)
+ # interpolate the theta cut to the bins used here
+ theta_cuts_bg_bins=np.interp(center,theta_cuts["center"],theta_cuts["cut"])
+
+ solid_angle_on=cone_solid_angle(theta_cuts_bg_bins)
+ solid_angle_ring=cone_solid_angle(fov_offset_max)-cone_solid_angle(
+ fov_offset_min
+ )
+ size_ratio=(solid_angle_on/solid_angle_ring).to_value(u.one)
+
+ forkeyinfilter(lambdacol:col.startswith("n"),bg.colnames):
+ # *= not possible due to upcast from int to float
+ bg[key]=bg[key]*size_ratio/alpha
+
+ returnbg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/simulations.html b/_modules/pyirf/simulations.html
new file mode 100644
index 000000000..c8edb3bdf
--- /dev/null
+++ b/_modules/pyirf/simulations.html
@@ -0,0 +1,588 @@
+
+
+
+
+
+
+
+ pyirf.simulations — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+classSimulatedEventsInfo:
+"""
+ Information about all simulated events, for calculating event weights.
+
+ Attributes
+ ----------
+ n_showers: int
+ Total number of simulated showers. If reuse was used, this
+ should already include the reuse.
+ energy_min: u.Quantity[energy]
+ Lower limit of the simulated energy range
+ energy_max: u.Quantity[energy]
+ Upper limit of the simulated energy range
+ max_impact: u.Quantity[length]
+ Maximum simulated impact parameter
+ spectral_index: float
+ Spectral Index of the simulated power law with sign included.
+ viewcone_min: u.Quantity[angle]
+ Inner angle of the viewcone
+ viewcone_max: u.Quantity[angle]
+ Outer angle of the viewcone
+ """
+
+ __slots__=(
+ "n_showers",
+ "energy_min",
+ "energy_max",
+ "max_impact",
+ "spectral_index",
+ "viewcone_min",
+ "viewcone_max",
+ )
+
+ @u.quantity_input(
+ energy_min=u.TeV,energy_max=u.TeV,max_impact=u.m,viewcone_min=u.deg,viewcone_max=u.deg
+ )
+ def__init__(
+ self,n_showers,energy_min,energy_max,max_impact,spectral_index,viewcone_min,viewcone_max,
+ ):
+ #: Total number of simulated showers, if reuse was used, this must
+ #: already include reuse
+ self.n_showers=n_showers
+ #: Lower limit of the simulated energy range
+ self.energy_min=energy_min
+ #: Upper limit of the simulated energy range
+ self.energy_max=energy_max
+ #: Maximum simualted impact radius
+ self.max_impact=max_impact
+ #: Spectral index of the simulated power law with sign included
+ self.spectral_index=spectral_index
+ #: Inner viewcone angle
+ self.viewcone_min=viewcone_min
+ #: Outer viewcone angle
+ self.viewcone_max=viewcone_max
+
+ ifspectral_index>-1:
+ raiseValueError("spectral index must be <= -1")
+
+
+[docs]
+ @u.quantity_input(energy_bins=u.TeV)
+ defcalculate_n_showers_per_energy(self,energy_bins):
+"""
+ Calculate number of showers that were simulated in the given energy intervals
+
+ This assumes the events were generated and from a powerlaw
+ like CORSIKA simulates events.
+
+ Parameters
+ ----------
+ energy_bins: astropy.units.Quantity[energy]
+ The interval edges for which to calculate the number of simulated showers
+
+ Returns
+ -------
+ n_showers: numpy.ndarray
+ The expected number of events inside each of the ``energy_bins``.
+ This is a floating point number.
+ The actual numbers will follow a poissionian distribution around this
+ expected value.
+ """
+ bins=energy_bins
+ e_low=bins[:-1]
+ e_high=bins[1:]
+ e_min=self.energy_min
+ e_max=self.energy_max
+
+ integral=_powerlaw_pdf_integral(
+ self.spectral_index,e_low,e_high,e_min,e_max
+ )
+
+ integral[e_high<=e_min]=0
+ integral[e_low>=e_max]=0
+
+ mask=(e_high>e_max)&(e_low<e_max)
+ integral[mask]=_powerlaw_pdf_integral(
+ self.spectral_index,e_low[mask],e_max,e_min,e_max
+ )
+
+ mask=(e_high>e_min)&(e_low<e_min)
+ integral[mask]=_powerlaw_pdf_integral(
+ self.spectral_index,e_min,e_high[mask],e_min,e_max
+ )
+
+ returnself.n_showers*integral
+
+
+
+[docs]
+ defcalculate_n_showers_per_fov(self,fov_bins):
+"""
+ Calculate number of showers that were simulated in the given fov bins.
+
+ This assumes the events were generated uniformly distributed per solid angle,
+ like CORSIKA simulates events with the VIEWCONE option.
+
+ Parameters
+ ----------
+ fov_bins: astropy.units.Quantity[angle]
+ The FOV bin edges for which to calculate the number of simulated showers
+
+ Returns
+ -------
+ n_showers: numpy.ndarray(ndim=2)
+ The expected number of events inside each of the ``fov_bins``.
+ This is a floating point number.
+ The actual numbers will follow a poissionian distribution around this
+ expected value.
+ """
+ fov_bins=fov_bins
+ fov_low=fov_bins[:-1]
+ fov_high=fov_bins[1:]
+ fov_integral=_viewcone_pdf_integral(self.viewcone_min,self.viewcone_max,fov_low,fov_high)
+ returnself.n_showers*fov_integral
+
+
+
+[docs]
+ @u.quantity_input(energy_bins=u.TeV,fov_bins=u.deg)
+ defcalculate_n_showers_per_energy_and_fov(self,energy_bins,fov_bins):
+"""
+ Calculate number of showers that were simulated in the given
+ energy and fov bins.
+
+ This assumes the events were generated uniformly distributed per solid angle,
+ and from a powerlaw in energy like CORSIKA simulates events.
+
+ Parameters
+ ----------
+ energy_bins: astropy.units.Quantity[energy]
+ The energy bin edges for which to calculate the number of simulated showers
+ fov_bins: astropy.units.Quantity[angle]
+ The FOV bin edges for which to calculate the number of simulated showers
+
+ Returns
+ -------
+ n_showers: numpy.ndarray(ndim=2)
+ The expected number of events inside each of the
+ ``energy_bins`` and ``fov_bins``.
+ Dimension (n_energy_bins, n_fov_bins)
+ This is a floating point number.
+ The actual numbers will follow a poissionian distribution around this
+ expected value.
+ """
+ # energy distribution and fov distribution are independent in CORSIKA,
+ # so just multiply both distributions.
+ e_integral=self.calculate_n_showers_per_energy(energy_bins)
+ fov_integral=self.calculate_n_showers_per_fov(fov_bins)
+ returne_integral[:,np.newaxis]*fov_integral/self.n_showers
+
+
+
+[docs]
+ @u.quantity_input(
+ energy_bins=u.TeV,fov_offset_bins=u.deg,fov_position_angle_bins=u.rad
+ )
+ defcalculate_n_showers_3d_polar(
+ self,energy_bins,fov_offset_bins,fov_position_angle_bins
+ ):
+"""
+ Calculate number of showers that were simulated in the given
+ energy and 2D fov bins in polar coordinates.
+
+ This assumes the events were generated uniformly distributed per solid angle,
+ and from a powerlaw in energy like CORSIKA simulates events.
+
+ Parameters
+ ----------
+ energy_bins: astropy.units.Quantity[energy]
+ The energy bin edges for which to calculate the number of simulated showers
+ fov_offset_bins: astropy.units.Quantity[angle]
+ The FOV radial bin edges for which to calculate the number of simulated showers
+ fov_position_angle_bins: astropy.units.Quantity[radian]
+ The FOV azimuthal bin edges for which to calculate the number of simulated showers
+
+ Returns
+ -------
+ n_showers: numpy.ndarray(ndim=3)
+ The expected number of events inside each of the
+ ``energy_bins``, ``fov_offset_bins`` and ``fov_position_angle_bins``.
+ Dimension (n_energy_bins, n_fov_offset_bins, n_fov_position_angle_bins)
+ This is a floating point number.
+ The actual numbers will follow a poissionian distribution around this
+ expected value.
+ """
+ e_fov_offset_integral=self.calculate_n_showers_per_energy_and_fov(
+ energy_bins,fov_offset_bins
+ )
+ viewcone_integral=self.calculate_n_showers_per_fov(
+ [self.viewcone_min,self.viewcone_max]*u.deg
+ )
+
+ n_bins_pa=len(fov_position_angle_bins)-1
+ position_angle_integral=np.full(n_bins_pa,viewcone_integral/n_bins_pa)
+
+ total_integral=e_fov_offset_integral[:,:,np.newaxis]*position_angle_integral
+
+ returntotal_integral/self.n_showers
+
+
+
+[docs]
+ @u.quantity_input(
+ energy_bins=u.TeV,fov_longitude_bins=u.deg,fov_latitude_bins=u.rad
+ )
+ defcalculate_n_showers_3d_lonlat(
+ self,energy_bins,fov_longitude_bins,fov_latitude_bins,subpixels=20
+ ):
+"""
+ Calculate number of showers that were simulated in the given
+ energy and 2D fov bins in nominal coordinates.
+
+ This assumes the events were generated uniformly distributed per solid angle,
+ and from a powerlaw in energy like CORSIKA simulates events.
+
+ Parameters
+ ----------
+ energy_bins: astropy.units.Quantity[energy]
+ The energy bin edges for which to calculate the number of simulated showers
+ fov_longitude_bins: astropy.units.Quantity[angle]
+ The FOV longitude bin edges for which to calculate the number of simulated showers
+ fov_latitude_bins: astropy.units.Quantity[angle]
+ The FOV latitude bin edges for which to calculate the number of simulated showers
+
+ Returns
+ -------
+ n_showers: numpy.ndarray(ndim=3)
+ The expected number of events inside each of the
+ ``energy_bins``, ``fov_longitude_bins`` and ``fov_latitude_bins``.
+ Dimension (n_energy_bins, n_fov_longitude_bins, n_fov_latitude_bins)
+ This is a floating point number.
+ The actual numbers will follow a poissionian distribution around this
+ expected value.
+ """
+
+ fov_overlap=_fov_lonlat_grid_overlap(
+ self,fov_longitude_bins,fov_latitude_bins,subpixels=subpixels
+ )
+
+ bin_grid_lon,bin_grid_lat=np.meshgrid(fov_longitude_bins,fov_latitude_bins)
+ bin_area=rectangle_solid_angle(
+ bin_grid_lon[:-1,:-1],
+ bin_grid_lon[1:,1:],
+ bin_grid_lat[:-1,:-1],
+ bin_grid_lat[1:,1:],
+ )
+ viewcone_area=cone_solid_angle(self.viewcone_max)-cone_solid_angle(self.viewcone_min)
+
+ shower_density=self.n_showers/viewcone_area
+
+ fov_integral=shower_density*bin_area
+ e_integral=self.calculate_n_showers_per_energy(energy_bins)
+
+ fov_integral=fov_overlap*fov_integral
+
+ return(e_integral[:,np.newaxis,np.newaxis]*fov_integral)/self.n_showers
+"""
+Functions and classes for calculating spectral weights
+"""
+
+importsys
+fromimportlib.resourcesimportas_file,files
+
+importastropy.unitsasu
+importnumpyasnp
+fromastropy.tableimportQTable
+fromscipy.interpolateimportinterp1d
+
+from.compatimportCOPY_IF_NEEDED
+from.utilsimportcone_solid_angle
+
+#: Unit of a point source flux
+#:
+#: Number of particles per Energy, time and area
+POINT_SOURCE_FLUX_UNIT=(1/u.TeV/u.s/u.m**2).unit
+
+#: Unit of a diffuse flux
+#:
+#: Number of particles per Energy, time, area and solid_angle
+DIFFUSE_FLUX_UNIT=POINT_SOURCE_FLUX_UNIT/u.sr
+
+
+__all__=[
+ "POINT_SOURCE_FLUX_UNIT",
+ "DIFFUSE_FLUX_UNIT",
+ "calculate_event_weights",
+ "PowerLaw",
+ "LogParabola",
+ "PowerLawWithExponentialGaussian",
+ "CRAB_HEGRA",
+ "CRAB_MAGIC_JHEAP2015",
+ "PDG_ALL_PARTICLE",
+ "IRFDOC_PROTON_SPECTRUM",
+ "IRFDOC_ELECTRON_SPECTRUM",
+ "TableInterpolationSpectrum",
+ "DAMPE_P_He_SPECTRUM",
+]
+
+
+
+[docs]
+@u.quantity_input(true_energy=u.TeV)
+defcalculate_event_weights(true_energy,target_spectrum,simulated_spectrum):
+r"""
+ Calculate event weights
+
+ Events with a certain ``simulated_spectrum`` are reweighted to ``target_spectrum``.
+
+ .. math::
+ w_i = \frac{\Phi_\text{Target}(E_i)}{\Phi_\text{Simulation}(E_i)}
+
+ Parameters
+ ----------
+ true_energy: astropy.units.Quantity[energy]
+ True energy of the event
+ target_spectrum: callable
+ The target spectrum. Must be a allable with signature (energy) -> flux
+ simulated_spectrum: callable
+ The simulated spectrum. Must be a callable with signature (energy) -> flux
+
+ Returns
+ -------
+ weights: numpy.ndarray
+ Weights for each event
+ """
+ return(target_spectrum(true_energy)/simulated_spectrum(true_energy)).to_value(
+ u.one
+ )
+
+
+
+
+[docs]
+classPowerLaw:
+r"""
+ A power law with normalization, reference energy and index.
+ Index includes the sign:
+
+ .. math::
+
+ \Phi(E, \Phi_0, \gamma, E_\text{ref}) =
+ \Phi_0 \left(\frac{E}{E_\text{ref}}\right)^{\gamma}
+
+ Attributes
+ ----------
+ normalization: astropy.units.Quantity[flux]
+ :math:`\Phi_0`,
+ index: float
+ :math:`\gamma`
+ e_ref: astropy.units.Quantity[energy]
+ :math:`E_\text{ref}`
+ """
+
+ @u.quantity_input(
+ normalization=[DIFFUSE_FLUX_UNIT,POINT_SOURCE_FLUX_UNIT],e_ref=u.TeV
+ )
+ def__init__(self,normalization,index,e_ref=1*u.TeV):
+"""Create a new PowerLaw spectrum"""
+ ifindex>0:
+ raiseValueError(f"Index must be < 0, got {index}")
+
+ self.normalization=normalization
+ self.index=index
+ self.e_ref=e_ref
+
+
+[docs]
+classPowerLawWithExponentialGaussian(PowerLaw):
+r"""
+ A power law with an additional Gaussian bump.
+ Beware that the Gaussian is not normalized!
+
+ .. math::
+
+ \Phi(E, \Phi_0, \gamma, f, \mu, \sigma, E_\text{ref}) =
+ \Phi_0 \left(
+ \frac{E}{E_\text{ref}}
+ \right)^{\gamma}
+ \cdot \left(
+ 1 + f \cdot
+ \left(
+ \exp\left(
+ \operatorname{Gauss}(\log_{10}(E / E_\text{ref}), \mu, \sigma)
+ \right) - 1
+ \right)
+ \right)
+
+ Where :math:`\operatorname{Gauss}` is the unnormalized Gaussian distribution:
+
+ .. math::
+ \operatorname{Gauss}(x, \mu, \sigma) = \exp\left(
+ -\frac{1}{2} \left(\frac{x - \mu}{\sigma}\right)^2
+ \right)
+
+ Attributes
+ ----------
+ normalization: astropy.units.Quantity[flux]
+ :math:`\Phi_0`,
+ a: float
+ :math:`\alpha`
+ b: float
+ :math:`\beta`
+ e_ref: astropy.units.Quantity[energy]
+ :math:`E_\text{ref}`
+ """
+
+ @u.quantity_input(
+ normalization=[DIFFUSE_FLUX_UNIT,POINT_SOURCE_FLUX_UNIT],e_ref=u.TeV
+ )
+ def__init__(self,normalization,index,e_ref,f,mu,sigma):
+"""Create a new PowerLawWithExponentialGaussian spectrum"""
+ super().__init__(normalization=normalization,index=index,e_ref=e_ref)
+ self.f=f
+ self.mu=mu
+ self.sigma=sigma
+
+
+[docs]
+ @u.quantity_input(energy=u.TeV)
+ def__call__(self,energy):
+ power=super().__call__(energy)
+ log10_e=np.log10(energy/self.e_ref)
+ # ROOT's TMath::Gauss does not add the normalization
+ # this is missing from the IRFDocs
+ # the code used for the plot can be found here:
+ # https://gitlab.cta-observatory.org/cta-consortium/aswg/irfs-macros/cosmic-rays-spectra/-/blob/master/electron_spectrum.C#L508
+ gauss=np.exp(-0.5*((log10_e-self.mu)/self.sigma)**2)
+ returnpower*(1+self.f*(np.exp(gauss)-1))
+
+
+
+#: Power Law parametrization of the Crab Nebula spectrum as published by HEGRA
+#:
+#: From "The Crab Nebula and Pulsar between 500 GeV and 80 TeV: Observations with the HEGRA stereoscopic air Cherenkov telescopes",
+#: Aharonian et al, 2004, ApJ 614.2
+#: doi.org/10.1086/423931
+CRAB_HEGRA=PowerLaw(
+ normalization=2.83e-11/(u.TeV*u.cm**2*u.s),
+ index=-2.62,
+ e_ref=1*u.TeV,
+)
+
+#: Log-Parabola parametrization of the Crab Nebula spectrum as published by MAGIC
+#:
+#: From "Measurement of the Crab Nebula spectrum over three decades in energy with the MAGIC telescopes",
+#: Aleksìc et al., 2015, JHEAP
+#: https://doi.org/10.1016/j.jheap.2015.01.002
+CRAB_MAGIC_JHEAP2015=LogParabola(
+ normalization=3.23e-11/(u.TeV*u.cm**2*u.s),
+ a=-2.47,
+ b=-0.24,
+)
+
+
+#: All particle spectrum
+#:
+#: (30.2) from "The Review of Particle Physics (2020)"
+#: https://pdg.lbl.gov/2020/reviews/rpp2020-rev-cosmic-rays.pdf
+PDG_ALL_PARTICLE=PowerLaw(
+ normalization=1.8e4/(u.GeV*u.m**2*u.s*u.sr),
+ index=-2.7,
+ e_ref=1*u.GeV,
+)
+
+#: Proton spectrum definition defined in the CTA Prod3b IRF Document
+#:
+#: From "Description of CTA Instrument Response Functions (Production 3b Simulation)", section 4.3.1
+#: https://gitlab.cta-observatory.org/cta-consortium/aswg/documentation/internal_reports/irfs-reports/prod3b-irf-description
+IRFDOC_PROTON_SPECTRUM=PowerLaw(
+ normalization=9.8e-6/(u.cm**2*u.s*u.TeV*u.sr),
+ index=-2.62,
+ e_ref=1*u.TeV,
+)
+
+#: Electron spectrum definition defined in the CTA Prod3b IRF Document
+#:
+#: From "Description of CTA Instrument Response Functions (Production 3b Simulation)", section 4.3.1
+#: https://gitlab.cta-observatory.org/cta-consortium/aswg/documentation/internal_reports/irfs-reports/prod3b-irf-description
+IRFDOC_ELECTRON_SPECTRUM=PowerLawWithExponentialGaussian(
+ normalization=2.385e-9/(u.TeV*u.cm**2*u.s*u.sr),
+ index=-3.43,
+ e_ref=1*u.TeV,
+ mu=-0.101,
+ sigma=0.741,
+ f=1.950,
+)
+
+#: Proton + Helium interpolated from DAMPE measurements
+#:
+#: Datapoints obtained from obtained from:
+#: https://inspirehep.net/files/62efc8374ffced58ea7e3a333bfa1217
+#: Points are from DAMPE, up to 8 TeV.
+#: For higher energies we assume a
+#: flattening of the dF/dE*E^2.7 more or less in the middle of the large
+#: spread of the available data reported on the same proceeding.
+withas_file(files("pyirf")/"resources/dampe_p+he.ecsv")as_path:
+ DAMPE_P_He_SPECTRUM=TableInterpolationSpectrum.from_file(_path)
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/statistics.html b/_modules/pyirf/statistics.html
new file mode 100644
index 000000000..dc6971a7d
--- /dev/null
+++ b/_modules/pyirf/statistics.html
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+ pyirf.statistics — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+defli_ma_significance(n_on,n_off,alpha=0.2):
+"""
+ Calculate the Li & Ma significance.
+
+ Formula (17) in https://doi.org/10.1086/161295
+
+ This functions returns 0 significance when n_on < alpha * n_off
+ instead of the negative sensitivities that would result from naively
+ evaluating the formula.
+
+ Parameters
+ ----------
+ n_on: integer or array like
+ Number of events for the on observations
+ n_off: integer or array like
+ Number of events for the off observations
+ alpha: float
+ Ratio between the on region and the off region size or obstime.
+
+ Returns
+ -------
+ s_lima: float or array
+ The calculated significance
+ """
+
+ scalar=is_scalar(n_on)
+
+ # Cast everything into float64 to avoid numeric instabilties
+ # when multiplying very small and very big numbers to get t1 and t2
+ n_on=np.array(n_on,copy=COPY_IF_NEEDED,ndmin=1,dtype=np.float64)
+ n_off=np.array(n_off,copy=COPY_IF_NEEDED,ndmin=1,dtype=np.float64)
+ alpha=np.float64(alpha)
+
+ withnp.errstate(divide="ignore",invalid="ignore"):
+ p_on=n_on/(n_on+n_off)
+ p_off=n_off/(n_on+n_off)
+
+ t1=n_on*np.log(((1+alpha)/alpha)*p_on)
+ t2=n_off*np.log((1+alpha)*p_off)
+
+ # lim x+->0 (x log(x)) = 0
+ t1[n_on==0]=0
+ t2[n_off==0]=0
+
+ ts=t1+t2
+
+ significance=np.sqrt(ts*2)
+
+ significance[n_on<(alpha*n_off)]=0
+
+ ifscalar:
+ returnsignificance[0]
+
+ returnsignificance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_modules/pyirf/utils.html b/_modules/pyirf/utils.html
new file mode 100644
index 000000000..566fb6f4c
--- /dev/null
+++ b/_modules/pyirf/utils.html
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+
+ pyirf.utils — pyirf documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+defis_scalar(val):
+"""Workaround that also supports astropy quantities
+
+ Parameters
+ ----------
+ val : object
+ Any object (value, list, etc...)
+
+ Returns
+ -------
+ result: bool
+ True is if input object is a scalar, False otherwise.
+ """
+ result=np.array(val,copy=COPY_IF_NEEDED).shape==tuple()
+ returnresult
+
+
+
+
+[docs]
+@u.quantity_input(assumed_source_az=u.deg,assumed_source_alt=u.deg)
+defcalculate_theta(events,assumed_source_az,assumed_source_alt):
+"""Calculate sky separation between assumed and reconstructed positions.
+
+ Parameters
+ ----------
+ events : astropy.QTable
+ Astropy Table object containing the reconstructed events information.
+ assumed_source_az: astropy.units.Quantity
+ Assumed Azimuth angle of the source.
+ assumed_source_alt: astropy.units.Quantity
+ Assumed Altitude angle of the source.
+
+ Returns
+ -------
+ theta: astropy.units.Quantity
+ Angular separation between the assumed and reconstructed positions
+ in the sky.
+ """
+ theta=angular_separation(
+ assumed_source_az,
+ assumed_source_alt,
+ events["reco_az"],
+ events["reco_alt"],
+ )
+
+ returntheta.to(u.deg)
+
+
+
+
+[docs]
+defcalculate_source_fov_offset(events,prefix="true"):
+"""Calculate angular separation between true and pointing positions.
+
+ Parameters
+ ----------
+ events : astropy.QTable
+ Astropy Table object containing the reconstructed events information.
+
+ prefix: str
+ Column prefix for az / alt, can be used to calculate reco or true
+ source fov offset.
+
+ Returns
+ -------
+ theta: astropy.units.Quantity
+ Angular separation between the true and pointing positions
+ in the sky.
+ """
+ theta=angular_separation(
+ events[f"{prefix}_az"],
+ events[f"{prefix}_alt"],
+ events["pointing_az"],
+ events["pointing_alt"],
+ )
+
+ returntheta.to(u.deg)
+
+
+
+
+[docs]
+defcalculate_source_fov_position_angle(events,prefix="true"):
+"""Calculate position_angle of true positions relative to pointing positions.
+
+ Parameters
+ ----------
+ events : astropy.QTable
+ Astropy Table object containing the reconstructed events information.
+
+ prefix: str
+ Column prefix for az / alt, can be used to calculate reco or true
+ source fov position_angle.
+
+ Returns
+ -------
+ phi: astropy.units.Quantity
+ Position angle of the true positions relative to the pointing positions
+ in the sky.
+ """
+ _,phi=gadf_fov_coords_theta_phi(
+ events[f"{prefix}_az"],
+ events[f"{prefix}_alt"],
+ events["pointing_az"],
+ events["pointing_alt"],
+ )
+
+ returnphi.to(u.deg)
+
+
+
+defcalculate_source_fov_lonlat(events,prefix="true"):
+"""Calculate position_angle of true positions relative to pointing positions.
+
+ Parameters
+ ----------
+ events : astropy.QTable
+ Astropy Table object containing the reconstructed events information.
+
+ prefix: str
+ Column prefix for az / alt, can be used to calculate reco or true
+ source fov position_angle.
+
+ Returns
+ -------
+ phi: astropy.units.Quantity
+ Position angle of the true positions relative to the pointing positions
+ in the sky.
+ """
+ lon,lat=gadf_fov_coords_lon_lat(
+ events[f"{prefix}_az"],
+ events[f"{prefix}_alt"],
+ events["pointing_az"],
+ events["pointing_alt"],
+ )
+
+ returnlon.to(u.deg),lat.to(u.deg)
+
+
+
+[docs]
+defcheck_histograms(hist1,hist2,key="reco_energy"):
+"""
+ Check if two histogram tables have the same binning
+
+ Parameters
+ ----------
+ hist1: ``~astropy.table.Table``
+ First histogram table, as created by
+ ``~pyirf.binning.create_histogram_table``
+ hist2: ``~astropy.table.Table``
+ Second histogram table
+ """
+
+ # check binning information and add to output
+ forkin("low","center","high"):
+ k=key+"_"+k
+ ifnotnp.all(hist1[k]==hist2[k]):
+ raiseValueError(
+ "Binning for signal_hist and background_hist must be equal"
+ )
+
+
+
+
+[docs]
+defcone_solid_angle(angle):
+"""Calculate the solid angle of a view cone.
+
+ Parameters
+ ----------
+ angle: astropy.units.Quantity or astropy.coordinates.Angle
+ Opening angle of the view cone.
+
+ Returns
+ -------
+ solid_angle: astropy.units.Quantity
+ Solid angle of a view cone with opening angle ``angle``.
+
+ """
+ solid_angle=2*np.pi*(1-np.cos(angle))*u.sr
+ returnsolid_angle
+
+
+
+defrectangle_solid_angle(lon_low,lon_high,lat_low,lat_high):
+"""Calculate the solid angle of a latitude-longitude rectangle
+
+ Parameters
+ ----------
+ lon_low: astropy.units.Quantity[angle]
+ Lower longitude coordinate of the rectangle corner
+ lat_low: astropy.units.Quantity[angle]
+ Lower latitude coordinate of the rectangle corner
+ lon_high: astropy.units.Quantity[angle]
+ Higher longitude coordinate of the rectangle corner
+ lat_high: astropy.units.Quantity[angle]
+ Higher Latitude coordinates of the rectangle corner
+
+ Returns
+ -------
+ solid angle: astropy.units.Quantity[solid angle]
+
+ """
+ diff_lon=(lon_high-lon_low).to_value(u.rad)
+ diff_lat=np.sin(lat_high.to_value(u.rad))-np.sin(lat_low.to_value(u.rad))
+
+ solid_angle=diff_lon*diff_lat*u.sr
+ returnsolid_angle
+
+
+defcheck_table(table,required_columns=None,required_units=None):
+"""Check a table for required columns and units.
+
+ Parameters
+ ----------
+ table: astropy.table.QTable
+ Table to check
+ required_columns: iterable[str]
+ Column names that are required to be present
+ required_units: Mapping[str->astropy.units.Unit]
+ Required units for columns as a Mapping from column names to units.
+ Checks if the units are convertible, not if they are identical
+
+ Raises
+ ------
+ MissingColumns: If any of the columns specified in ``required_columns`` or
+ as keys in ``required_units are`` not present in the table.
+ WrongColumnUnit: if any column has the wrong unit
+ """
+ ifrequired_columnsisnotNone:
+ missing=set(required_columns)-set(table.colnames)
+ ifmissing:
+ raiseMissingColumns(missing)
+
+ ifrequired_unitsisnotNone:
+ forcol,expectedinrequired_units.items():
+ ifcolnotintable.colnames:
+ raiseMissingColumns(col)
+
+ unit=table[col].unit
+ ifnotexpected.is_equivalent(unit):
+ raiseWrongColumnUnit(col,unit,expected)
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/_sources/AUTHORS.rst.txt b/_sources/AUTHORS.rst.txt
new file mode 100644
index 000000000..39c519851
--- /dev/null
+++ b/_sources/AUTHORS.rst.txt
@@ -0,0 +1,18 @@
+.. _authors:
+
+Authors
+=======
+
+To see who contributed to ``pyirf``, please visit the
+`GitHub contributors page `__
+or run
+
+.. code-block:: bash
+
+ git shortlog -sne
+
+
+``pyirf`` started as part of `protopipe `__ by Julien Lefaucher,
+but was largely rewritten in September 2020, making use of code from the
+previous version, the `pyfact `__ module and the
+`FACT irf `__ package.
diff --git a/_sources/api/pyirf.benchmarks.angular_resolution.rst.txt b/_sources/api/pyirf.benchmarks.angular_resolution.rst.txt
new file mode 100644
index 000000000..d4ae2a44b
--- /dev/null
+++ b/_sources/api/pyirf.benchmarks.angular_resolution.rst.txt
@@ -0,0 +1,6 @@
+angular_resolution
+==================
+
+.. currentmodule:: pyirf.benchmarks
+
+.. autofunction:: angular_resolution
diff --git a/_sources/api/pyirf.benchmarks.energy_bias_resolution.rst.txt b/_sources/api/pyirf.benchmarks.energy_bias_resolution.rst.txt
new file mode 100644
index 000000000..fd2516fb6
--- /dev/null
+++ b/_sources/api/pyirf.benchmarks.energy_bias_resolution.rst.txt
@@ -0,0 +1,6 @@
+energy_bias_resolution
+======================
+
+.. currentmodule:: pyirf.benchmarks
+
+.. autofunction:: energy_bias_resolution
diff --git a/_sources/api/pyirf.benchmarks.energy_bias_resolution_from_energy_dispersion.rst.txt b/_sources/api/pyirf.benchmarks.energy_bias_resolution_from_energy_dispersion.rst.txt
new file mode 100644
index 000000000..7315f58ce
--- /dev/null
+++ b/_sources/api/pyirf.benchmarks.energy_bias_resolution_from_energy_dispersion.rst.txt
@@ -0,0 +1,6 @@
+energy_bias_resolution_from_energy_dispersion
+=============================================
+
+.. currentmodule:: pyirf.benchmarks
+
+.. autofunction:: energy_bias_resolution_from_energy_dispersion
diff --git a/_sources/api/pyirf.binning.add_overflow_bins.rst.txt b/_sources/api/pyirf.binning.add_overflow_bins.rst.txt
new file mode 100644
index 000000000..5f7ac809f
--- /dev/null
+++ b/_sources/api/pyirf.binning.add_overflow_bins.rst.txt
@@ -0,0 +1,6 @@
+add_overflow_bins
+=================
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: add_overflow_bins
diff --git a/_sources/api/pyirf.binning.bin_center.rst.txt b/_sources/api/pyirf.binning.bin_center.rst.txt
new file mode 100644
index 000000000..916be12f0
--- /dev/null
+++ b/_sources/api/pyirf.binning.bin_center.rst.txt
@@ -0,0 +1,6 @@
+bin_center
+==========
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: bin_center
diff --git a/_sources/api/pyirf.binning.calculate_bin_indices.rst.txt b/_sources/api/pyirf.binning.calculate_bin_indices.rst.txt
new file mode 100644
index 000000000..f0bd70f8d
--- /dev/null
+++ b/_sources/api/pyirf.binning.calculate_bin_indices.rst.txt
@@ -0,0 +1,6 @@
+calculate_bin_indices
+=====================
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: calculate_bin_indices
diff --git a/_sources/api/pyirf.binning.create_bins_per_decade.rst.txt b/_sources/api/pyirf.binning.create_bins_per_decade.rst.txt
new file mode 100644
index 000000000..a41cc7798
--- /dev/null
+++ b/_sources/api/pyirf.binning.create_bins_per_decade.rst.txt
@@ -0,0 +1,6 @@
+create_bins_per_decade
+======================
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: create_bins_per_decade
diff --git a/_sources/api/pyirf.binning.create_histogram_table.rst.txt b/_sources/api/pyirf.binning.create_histogram_table.rst.txt
new file mode 100644
index 000000000..d95e8b1ce
--- /dev/null
+++ b/_sources/api/pyirf.binning.create_histogram_table.rst.txt
@@ -0,0 +1,6 @@
+create_histogram_table
+======================
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: create_histogram_table
diff --git a/_sources/api/pyirf.binning.join_bin_lo_hi.rst.txt b/_sources/api/pyirf.binning.join_bin_lo_hi.rst.txt
new file mode 100644
index 000000000..40fec327c
--- /dev/null
+++ b/_sources/api/pyirf.binning.join_bin_lo_hi.rst.txt
@@ -0,0 +1,6 @@
+join_bin_lo_hi
+==============
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: join_bin_lo_hi
diff --git a/_sources/api/pyirf.binning.resample_histogram1d.rst.txt b/_sources/api/pyirf.binning.resample_histogram1d.rst.txt
new file mode 100644
index 000000000..6bc78ee2f
--- /dev/null
+++ b/_sources/api/pyirf.binning.resample_histogram1d.rst.txt
@@ -0,0 +1,6 @@
+resample_histogram1d
+====================
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: resample_histogram1d
diff --git a/_sources/api/pyirf.binning.split_bin_lo_hi.rst.txt b/_sources/api/pyirf.binning.split_bin_lo_hi.rst.txt
new file mode 100644
index 000000000..e216cf7fb
--- /dev/null
+++ b/_sources/api/pyirf.binning.split_bin_lo_hi.rst.txt
@@ -0,0 +1,6 @@
+split_bin_lo_hi
+===============
+
+.. currentmodule:: pyirf.binning
+
+.. autofunction:: split_bin_lo_hi
diff --git a/_sources/api/pyirf.cut_optimization.optimize_gh_cut.rst.txt b/_sources/api/pyirf.cut_optimization.optimize_gh_cut.rst.txt
new file mode 100644
index 000000000..a65ac48bb
--- /dev/null
+++ b/_sources/api/pyirf.cut_optimization.optimize_gh_cut.rst.txt
@@ -0,0 +1,6 @@
+optimize_gh_cut
+===============
+
+.. currentmodule:: pyirf.cut_optimization
+
+.. autofunction:: optimize_gh_cut
diff --git a/_sources/api/pyirf.cuts.calculate_percentile_cut.rst.txt b/_sources/api/pyirf.cuts.calculate_percentile_cut.rst.txt
new file mode 100644
index 000000000..627ed0c39
--- /dev/null
+++ b/_sources/api/pyirf.cuts.calculate_percentile_cut.rst.txt
@@ -0,0 +1,6 @@
+calculate_percentile_cut
+========================
+
+.. currentmodule:: pyirf.cuts
+
+.. autofunction:: calculate_percentile_cut
diff --git a/_sources/api/pyirf.cuts.compare_irf_cuts.rst.txt b/_sources/api/pyirf.cuts.compare_irf_cuts.rst.txt
new file mode 100644
index 000000000..d5ef121fa
--- /dev/null
+++ b/_sources/api/pyirf.cuts.compare_irf_cuts.rst.txt
@@ -0,0 +1,6 @@
+compare_irf_cuts
+================
+
+.. currentmodule:: pyirf.cuts
+
+.. autofunction:: compare_irf_cuts
diff --git a/_sources/api/pyirf.cuts.evaluate_binned_cut.rst.txt b/_sources/api/pyirf.cuts.evaluate_binned_cut.rst.txt
new file mode 100644
index 000000000..1887be904
--- /dev/null
+++ b/_sources/api/pyirf.cuts.evaluate_binned_cut.rst.txt
@@ -0,0 +1,6 @@
+evaluate_binned_cut
+===================
+
+.. currentmodule:: pyirf.cuts
+
+.. autofunction:: evaluate_binned_cut
diff --git a/_sources/api/pyirf.gammapy.create_effective_area_table_2d.rst.txt b/_sources/api/pyirf.gammapy.create_effective_area_table_2d.rst.txt
new file mode 100644
index 000000000..9a053c6f3
--- /dev/null
+++ b/_sources/api/pyirf.gammapy.create_effective_area_table_2d.rst.txt
@@ -0,0 +1,6 @@
+create_effective_area_table_2d
+==============================
+
+.. currentmodule:: pyirf.gammapy
+
+.. autofunction:: create_effective_area_table_2d
diff --git a/_sources/api/pyirf.gammapy.create_energy_dispersion_2d.rst.txt b/_sources/api/pyirf.gammapy.create_energy_dispersion_2d.rst.txt
new file mode 100644
index 000000000..6bc37bb46
--- /dev/null
+++ b/_sources/api/pyirf.gammapy.create_energy_dispersion_2d.rst.txt
@@ -0,0 +1,6 @@
+create_energy_dispersion_2d
+===========================
+
+.. currentmodule:: pyirf.gammapy
+
+.. autofunction:: create_energy_dispersion_2d
diff --git a/_sources/api/pyirf.gammapy.create_psf_3d.rst.txt b/_sources/api/pyirf.gammapy.create_psf_3d.rst.txt
new file mode 100644
index 000000000..ffabae486
--- /dev/null
+++ b/_sources/api/pyirf.gammapy.create_psf_3d.rst.txt
@@ -0,0 +1,6 @@
+create_psf_3d
+=============
+
+.. currentmodule:: pyirf.gammapy
+
+.. autofunction:: create_psf_3d
diff --git a/_sources/api/pyirf.interpolation.BaseComponentEstimator.rst.txt b/_sources/api/pyirf.interpolation.BaseComponentEstimator.rst.txt
new file mode 100644
index 000000000..b675e92b5
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.BaseComponentEstimator.rst.txt
@@ -0,0 +1,17 @@
+BaseComponentEstimator
+======================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: BaseComponentEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~BaseComponentEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.interpolation.BaseExtrapolator.rst.txt b/_sources/api/pyirf.interpolation.BaseExtrapolator.rst.txt
new file mode 100644
index 000000000..c89aa5f40
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.BaseExtrapolator.rst.txt
@@ -0,0 +1,19 @@
+BaseExtrapolator
+================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: BaseExtrapolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~BaseExtrapolator.__call__
+ ~BaseExtrapolator.extrapolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: extrapolate
diff --git a/_sources/api/pyirf.interpolation.BaseInterpolator.rst.txt b/_sources/api/pyirf.interpolation.BaseInterpolator.rst.txt
new file mode 100644
index 000000000..4decf33ba
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.BaseInterpolator.rst.txt
@@ -0,0 +1,19 @@
+BaseInterpolator
+================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: BaseInterpolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~BaseInterpolator.__call__
+ ~BaseInterpolator.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.BaseNearestNeighborSearcher.rst.txt b/_sources/api/pyirf.interpolation.BaseNearestNeighborSearcher.rst.txt
new file mode 100644
index 000000000..9924f2b98
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.BaseNearestNeighborSearcher.rst.txt
@@ -0,0 +1,19 @@
+BaseNearestNeighborSearcher
+===========================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: BaseNearestNeighborSearcher
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~BaseNearestNeighborSearcher.__call__
+ ~BaseNearestNeighborSearcher.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.DiscretePDFComponentEstimator.rst.txt b/_sources/api/pyirf.interpolation.DiscretePDFComponentEstimator.rst.txt
new file mode 100644
index 000000000..4513e25fe
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.DiscretePDFComponentEstimator.rst.txt
@@ -0,0 +1,17 @@
+DiscretePDFComponentEstimator
+=============================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: DiscretePDFComponentEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~DiscretePDFComponentEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.interpolation.DiscretePDFExtrapolator.rst.txt b/_sources/api/pyirf.interpolation.DiscretePDFExtrapolator.rst.txt
new file mode 100644
index 000000000..eb63dabfe
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.DiscretePDFExtrapolator.rst.txt
@@ -0,0 +1,19 @@
+DiscretePDFExtrapolator
+=======================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: DiscretePDFExtrapolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~DiscretePDFExtrapolator.__call__
+ ~DiscretePDFExtrapolator.extrapolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: extrapolate
diff --git a/_sources/api/pyirf.interpolation.DiscretePDFInterpolator.rst.txt b/_sources/api/pyirf.interpolation.DiscretePDFInterpolator.rst.txt
new file mode 100644
index 000000000..66cb12982
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.DiscretePDFInterpolator.rst.txt
@@ -0,0 +1,19 @@
+DiscretePDFInterpolator
+=======================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: DiscretePDFInterpolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~DiscretePDFInterpolator.__call__
+ ~DiscretePDFInterpolator.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.DiscretePDFNearestNeighborSearcher.rst.txt b/_sources/api/pyirf.interpolation.DiscretePDFNearestNeighborSearcher.rst.txt
new file mode 100644
index 000000000..2a2bc9ed3
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.DiscretePDFNearestNeighborSearcher.rst.txt
@@ -0,0 +1,19 @@
+DiscretePDFNearestNeighborSearcher
+==================================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: DiscretePDFNearestNeighborSearcher
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~DiscretePDFNearestNeighborSearcher.__call__
+ ~DiscretePDFNearestNeighborSearcher.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.EffectiveAreaEstimator.rst.txt b/_sources/api/pyirf.interpolation.EffectiveAreaEstimator.rst.txt
new file mode 100644
index 000000000..c05159b99
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.EffectiveAreaEstimator.rst.txt
@@ -0,0 +1,17 @@
+EffectiveAreaEstimator
+======================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: EffectiveAreaEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~EffectiveAreaEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.interpolation.EnergyDispersionEstimator.rst.txt b/_sources/api/pyirf.interpolation.EnergyDispersionEstimator.rst.txt
new file mode 100644
index 000000000..11dfa9904
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.EnergyDispersionEstimator.rst.txt
@@ -0,0 +1,17 @@
+EnergyDispersionEstimator
+=========================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: EnergyDispersionEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~EnergyDispersionEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.interpolation.GridDataInterpolator.rst.txt b/_sources/api/pyirf.interpolation.GridDataInterpolator.rst.txt
new file mode 100644
index 000000000..e33de77c4
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.GridDataInterpolator.rst.txt
@@ -0,0 +1,19 @@
+GridDataInterpolator
+====================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: GridDataInterpolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~GridDataInterpolator.__call__
+ ~GridDataInterpolator.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.MomentMorphInterpolator.rst.txt b/_sources/api/pyirf.interpolation.MomentMorphInterpolator.rst.txt
new file mode 100644
index 000000000..d48ca1572
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.MomentMorphInterpolator.rst.txt
@@ -0,0 +1,19 @@
+MomentMorphInterpolator
+=======================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: MomentMorphInterpolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~MomentMorphInterpolator.__call__
+ ~MomentMorphInterpolator.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.MomentMorphNearestSimplexExtrapolator.rst.txt b/_sources/api/pyirf.interpolation.MomentMorphNearestSimplexExtrapolator.rst.txt
new file mode 100644
index 000000000..14da35cbe
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.MomentMorphNearestSimplexExtrapolator.rst.txt
@@ -0,0 +1,19 @@
+MomentMorphNearestSimplexExtrapolator
+=====================================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: MomentMorphNearestSimplexExtrapolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~MomentMorphNearestSimplexExtrapolator.__call__
+ ~MomentMorphNearestSimplexExtrapolator.extrapolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: extrapolate
diff --git a/_sources/api/pyirf.interpolation.PDFNormalization.rst.txt b/_sources/api/pyirf.interpolation.PDFNormalization.rst.txt
new file mode 100644
index 000000000..4fa6bcd95
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.PDFNormalization.rst.txt
@@ -0,0 +1,19 @@
+PDFNormalization
+================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: PDFNormalization
+ :show-inheritance:
+
+ .. rubric:: Attributes Summary
+
+ .. autosummary::
+
+ ~PDFNormalization.AREA
+ ~PDFNormalization.CONE_SOLID_ANGLE
+
+ .. rubric:: Attributes Documentation
+
+ .. autoattribute:: AREA
+ .. autoattribute:: CONE_SOLID_ANGLE
diff --git a/_sources/api/pyirf.interpolation.PSFTableEstimator.rst.txt b/_sources/api/pyirf.interpolation.PSFTableEstimator.rst.txt
new file mode 100644
index 000000000..a802248ff
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.PSFTableEstimator.rst.txt
@@ -0,0 +1,17 @@
+PSFTableEstimator
+=================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: PSFTableEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~PSFTableEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.interpolation.ParametrizedComponentEstimator.rst.txt b/_sources/api/pyirf.interpolation.ParametrizedComponentEstimator.rst.txt
new file mode 100644
index 000000000..46f5bd72e
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.ParametrizedComponentEstimator.rst.txt
@@ -0,0 +1,17 @@
+ParametrizedComponentEstimator
+==============================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: ParametrizedComponentEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~ParametrizedComponentEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.interpolation.ParametrizedExtrapolator.rst.txt b/_sources/api/pyirf.interpolation.ParametrizedExtrapolator.rst.txt
new file mode 100644
index 000000000..ba2592916
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.ParametrizedExtrapolator.rst.txt
@@ -0,0 +1,19 @@
+ParametrizedExtrapolator
+========================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: ParametrizedExtrapolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~ParametrizedExtrapolator.__call__
+ ~ParametrizedExtrapolator.extrapolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: extrapolate
diff --git a/_sources/api/pyirf.interpolation.ParametrizedInterpolator.rst.txt b/_sources/api/pyirf.interpolation.ParametrizedInterpolator.rst.txt
new file mode 100644
index 000000000..f42a57a6b
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.ParametrizedInterpolator.rst.txt
@@ -0,0 +1,19 @@
+ParametrizedInterpolator
+========================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: ParametrizedInterpolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~ParametrizedInterpolator.__call__
+ ~ParametrizedInterpolator.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.ParametrizedNearestNeighborSearcher.rst.txt b/_sources/api/pyirf.interpolation.ParametrizedNearestNeighborSearcher.rst.txt
new file mode 100644
index 000000000..bd6c20ba0
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.ParametrizedNearestNeighborSearcher.rst.txt
@@ -0,0 +1,19 @@
+ParametrizedNearestNeighborSearcher
+===================================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: ParametrizedNearestNeighborSearcher
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~ParametrizedNearestNeighborSearcher.__call__
+ ~ParametrizedNearestNeighborSearcher.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.ParametrizedNearestSimplexExtrapolator.rst.txt b/_sources/api/pyirf.interpolation.ParametrizedNearestSimplexExtrapolator.rst.txt
new file mode 100644
index 000000000..5b8b88151
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.ParametrizedNearestSimplexExtrapolator.rst.txt
@@ -0,0 +1,19 @@
+ParametrizedNearestSimplexExtrapolator
+======================================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: ParametrizedNearestSimplexExtrapolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~ParametrizedNearestSimplexExtrapolator.__call__
+ ~ParametrizedNearestSimplexExtrapolator.extrapolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: extrapolate
diff --git a/_sources/api/pyirf.interpolation.ParametrizedVisibleEdgesExtrapolator.rst.txt b/_sources/api/pyirf.interpolation.ParametrizedVisibleEdgesExtrapolator.rst.txt
new file mode 100644
index 000000000..fd3e79e42
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.ParametrizedVisibleEdgesExtrapolator.rst.txt
@@ -0,0 +1,19 @@
+ParametrizedVisibleEdgesExtrapolator
+====================================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: ParametrizedVisibleEdgesExtrapolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~ParametrizedVisibleEdgesExtrapolator.__call__
+ ~ParametrizedVisibleEdgesExtrapolator.extrapolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: extrapolate
diff --git a/_sources/api/pyirf.interpolation.QuantileInterpolator.rst.txt b/_sources/api/pyirf.interpolation.QuantileInterpolator.rst.txt
new file mode 100644
index 000000000..8c5dbb171
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.QuantileInterpolator.rst.txt
@@ -0,0 +1,19 @@
+QuantileInterpolator
+====================
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: QuantileInterpolator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~QuantileInterpolator.__call__
+ ~QuantileInterpolator.interpolate
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: interpolate
diff --git a/_sources/api/pyirf.interpolation.RadMaxEstimator.rst.txt b/_sources/api/pyirf.interpolation.RadMaxEstimator.rst.txt
new file mode 100644
index 000000000..884c3ce58
--- /dev/null
+++ b/_sources/api/pyirf.interpolation.RadMaxEstimator.rst.txt
@@ -0,0 +1,17 @@
+RadMaxEstimator
+===============
+
+.. currentmodule:: pyirf.interpolation
+
+.. autoclass:: RadMaxEstimator
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~RadMaxEstimator.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.io.create_aeff2d_hdu.rst.txt b/_sources/api/pyirf.io.create_aeff2d_hdu.rst.txt
new file mode 100644
index 000000000..99afafb6a
--- /dev/null
+++ b/_sources/api/pyirf.io.create_aeff2d_hdu.rst.txt
@@ -0,0 +1,6 @@
+create_aeff2d_hdu
+=================
+
+.. currentmodule:: pyirf.io
+
+.. autofunction:: create_aeff2d_hdu
diff --git a/_sources/api/pyirf.io.create_background_2d_hdu.rst.txt b/_sources/api/pyirf.io.create_background_2d_hdu.rst.txt
new file mode 100644
index 000000000..aa9d2e413
--- /dev/null
+++ b/_sources/api/pyirf.io.create_background_2d_hdu.rst.txt
@@ -0,0 +1,6 @@
+create_background_2d_hdu
+========================
+
+.. currentmodule:: pyirf.io
+
+.. autofunction:: create_background_2d_hdu
diff --git a/_sources/api/pyirf.io.create_energy_dispersion_hdu.rst.txt b/_sources/api/pyirf.io.create_energy_dispersion_hdu.rst.txt
new file mode 100644
index 000000000..0700b7dee
--- /dev/null
+++ b/_sources/api/pyirf.io.create_energy_dispersion_hdu.rst.txt
@@ -0,0 +1,6 @@
+create_energy_dispersion_hdu
+============================
+
+.. currentmodule:: pyirf.io
+
+.. autofunction:: create_energy_dispersion_hdu
diff --git a/_sources/api/pyirf.io.create_psf_table_hdu.rst.txt b/_sources/api/pyirf.io.create_psf_table_hdu.rst.txt
new file mode 100644
index 000000000..30d6aa837
--- /dev/null
+++ b/_sources/api/pyirf.io.create_psf_table_hdu.rst.txt
@@ -0,0 +1,6 @@
+create_psf_table_hdu
+====================
+
+.. currentmodule:: pyirf.io
+
+.. autofunction:: create_psf_table_hdu
diff --git a/_sources/api/pyirf.io.create_rad_max_hdu.rst.txt b/_sources/api/pyirf.io.create_rad_max_hdu.rst.txt
new file mode 100644
index 000000000..bf31f20b5
--- /dev/null
+++ b/_sources/api/pyirf.io.create_rad_max_hdu.rst.txt
@@ -0,0 +1,6 @@
+create_rad_max_hdu
+==================
+
+.. currentmodule:: pyirf.io
+
+.. autofunction:: create_rad_max_hdu
diff --git a/_sources/api/pyirf.io.read_eventdisplay_fits.rst.txt b/_sources/api/pyirf.io.read_eventdisplay_fits.rst.txt
new file mode 100644
index 000000000..4a930ef2b
--- /dev/null
+++ b/_sources/api/pyirf.io.read_eventdisplay_fits.rst.txt
@@ -0,0 +1,6 @@
+read_eventdisplay_fits
+======================
+
+.. currentmodule:: pyirf.io
+
+.. autofunction:: read_eventdisplay_fits
diff --git a/_sources/api/pyirf.irf.background_2d.rst.txt b/_sources/api/pyirf.irf.background_2d.rst.txt
new file mode 100644
index 000000000..e7fe4831d
--- /dev/null
+++ b/_sources/api/pyirf.irf.background_2d.rst.txt
@@ -0,0 +1,6 @@
+background_2d
+=============
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: background_2d
diff --git a/_sources/api/pyirf.irf.effective_area.rst.txt b/_sources/api/pyirf.irf.effective_area.rst.txt
new file mode 100644
index 000000000..775c4b66c
--- /dev/null
+++ b/_sources/api/pyirf.irf.effective_area.rst.txt
@@ -0,0 +1,6 @@
+effective_area
+==============
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: effective_area
diff --git a/_sources/api/pyirf.irf.effective_area_3d_lonlat.rst.txt b/_sources/api/pyirf.irf.effective_area_3d_lonlat.rst.txt
new file mode 100644
index 000000000..e7fab68b0
--- /dev/null
+++ b/_sources/api/pyirf.irf.effective_area_3d_lonlat.rst.txt
@@ -0,0 +1,6 @@
+effective_area_3d_lonlat
+========================
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: effective_area_3d_lonlat
diff --git a/_sources/api/pyirf.irf.effective_area_3d_polar.rst.txt b/_sources/api/pyirf.irf.effective_area_3d_polar.rst.txt
new file mode 100644
index 000000000..e27625e09
--- /dev/null
+++ b/_sources/api/pyirf.irf.effective_area_3d_polar.rst.txt
@@ -0,0 +1,6 @@
+effective_area_3d_polar
+=======================
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: effective_area_3d_polar
diff --git a/_sources/api/pyirf.irf.effective_area_per_energy.rst.txt b/_sources/api/pyirf.irf.effective_area_per_energy.rst.txt
new file mode 100644
index 000000000..ebf22a910
--- /dev/null
+++ b/_sources/api/pyirf.irf.effective_area_per_energy.rst.txt
@@ -0,0 +1,6 @@
+effective_area_per_energy
+=========================
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: effective_area_per_energy
diff --git a/_sources/api/pyirf.irf.effective_area_per_energy_and_fov.rst.txt b/_sources/api/pyirf.irf.effective_area_per_energy_and_fov.rst.txt
new file mode 100644
index 000000000..2e71d01e6
--- /dev/null
+++ b/_sources/api/pyirf.irf.effective_area_per_energy_and_fov.rst.txt
@@ -0,0 +1,6 @@
+effective_area_per_energy_and_fov
+=================================
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: effective_area_per_energy_and_fov
diff --git a/_sources/api/pyirf.irf.energy_dispersion.rst.txt b/_sources/api/pyirf.irf.energy_dispersion.rst.txt
new file mode 100644
index 000000000..3c024a701
--- /dev/null
+++ b/_sources/api/pyirf.irf.energy_dispersion.rst.txt
@@ -0,0 +1,6 @@
+energy_dispersion
+=================
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: energy_dispersion
diff --git a/_sources/api/pyirf.irf.psf_table.rst.txt b/_sources/api/pyirf.irf.psf_table.rst.txt
new file mode 100644
index 000000000..de9d1751a
--- /dev/null
+++ b/_sources/api/pyirf.irf.psf_table.rst.txt
@@ -0,0 +1,6 @@
+psf_table
+=========
+
+.. currentmodule:: pyirf.irf
+
+.. autofunction:: psf_table
diff --git a/_sources/api/pyirf.sensitivity.calculate_sensitivity.rst.txt b/_sources/api/pyirf.sensitivity.calculate_sensitivity.rst.txt
new file mode 100644
index 000000000..1ae75702b
--- /dev/null
+++ b/_sources/api/pyirf.sensitivity.calculate_sensitivity.rst.txt
@@ -0,0 +1,6 @@
+calculate_sensitivity
+=====================
+
+.. currentmodule:: pyirf.sensitivity
+
+.. autofunction:: calculate_sensitivity
diff --git a/_sources/api/pyirf.sensitivity.estimate_background.rst.txt b/_sources/api/pyirf.sensitivity.estimate_background.rst.txt
new file mode 100644
index 000000000..bf9a35a73
--- /dev/null
+++ b/_sources/api/pyirf.sensitivity.estimate_background.rst.txt
@@ -0,0 +1,6 @@
+estimate_background
+===================
+
+.. currentmodule:: pyirf.sensitivity
+
+.. autofunction:: estimate_background
diff --git a/_sources/api/pyirf.sensitivity.relative_sensitivity.rst.txt b/_sources/api/pyirf.sensitivity.relative_sensitivity.rst.txt
new file mode 100644
index 000000000..6412f601d
--- /dev/null
+++ b/_sources/api/pyirf.sensitivity.relative_sensitivity.rst.txt
@@ -0,0 +1,6 @@
+relative_sensitivity
+====================
+
+.. currentmodule:: pyirf.sensitivity
+
+.. autofunction:: relative_sensitivity
diff --git a/_sources/api/pyirf.simulations.SimulatedEventsInfo.rst.txt b/_sources/api/pyirf.simulations.SimulatedEventsInfo.rst.txt
new file mode 100644
index 000000000..37b266d97
--- /dev/null
+++ b/_sources/api/pyirf.simulations.SimulatedEventsInfo.rst.txt
@@ -0,0 +1,47 @@
+SimulatedEventsInfo
+===================
+
+.. currentmodule:: pyirf.simulations
+
+.. autoclass:: SimulatedEventsInfo
+ :show-inheritance:
+
+ .. rubric:: Attributes Summary
+
+ .. autosummary::
+
+ ~SimulatedEventsInfo.energy_max
+ ~SimulatedEventsInfo.energy_min
+ ~SimulatedEventsInfo.max_impact
+ ~SimulatedEventsInfo.n_showers
+ ~SimulatedEventsInfo.spectral_index
+ ~SimulatedEventsInfo.viewcone_max
+ ~SimulatedEventsInfo.viewcone_min
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~SimulatedEventsInfo.calculate_n_showers_3d_lonlat
+ ~SimulatedEventsInfo.calculate_n_showers_3d_polar
+ ~SimulatedEventsInfo.calculate_n_showers_per_energy
+ ~SimulatedEventsInfo.calculate_n_showers_per_energy_and_fov
+ ~SimulatedEventsInfo.calculate_n_showers_per_fov
+
+ .. rubric:: Attributes Documentation
+
+ .. autoattribute:: energy_max
+ .. autoattribute:: energy_min
+ .. autoattribute:: max_impact
+ .. autoattribute:: n_showers
+ .. autoattribute:: spectral_index
+ .. autoattribute:: viewcone_max
+ .. autoattribute:: viewcone_min
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: calculate_n_showers_3d_lonlat
+ .. automethod:: calculate_n_showers_3d_polar
+ .. automethod:: calculate_n_showers_per_energy
+ .. automethod:: calculate_n_showers_per_energy_and_fov
+ .. automethod:: calculate_n_showers_per_fov
diff --git a/_sources/api/pyirf.spectral.CRAB_HEGRA.rst.txt b/_sources/api/pyirf.spectral.CRAB_HEGRA.rst.txt
new file mode 100644
index 000000000..01654c0a9
--- /dev/null
+++ b/_sources/api/pyirf.spectral.CRAB_HEGRA.rst.txt
@@ -0,0 +1,6 @@
+CRAB_HEGRA
+==========
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: CRAB_HEGRA
diff --git a/_sources/api/pyirf.spectral.CRAB_MAGIC_JHEAP2015.rst.txt b/_sources/api/pyirf.spectral.CRAB_MAGIC_JHEAP2015.rst.txt
new file mode 100644
index 000000000..8ddb00858
--- /dev/null
+++ b/_sources/api/pyirf.spectral.CRAB_MAGIC_JHEAP2015.rst.txt
@@ -0,0 +1,6 @@
+CRAB_MAGIC_JHEAP2015
+====================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: CRAB_MAGIC_JHEAP2015
diff --git a/_sources/api/pyirf.spectral.DAMPE_P_He_SPECTRUM.rst.txt b/_sources/api/pyirf.spectral.DAMPE_P_He_SPECTRUM.rst.txt
new file mode 100644
index 000000000..25d659664
--- /dev/null
+++ b/_sources/api/pyirf.spectral.DAMPE_P_He_SPECTRUM.rst.txt
@@ -0,0 +1,6 @@
+DAMPE_P_He_SPECTRUM
+===================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: DAMPE_P_He_SPECTRUM
diff --git a/_sources/api/pyirf.spectral.DIFFUSE_FLUX_UNIT.rst.txt b/_sources/api/pyirf.spectral.DIFFUSE_FLUX_UNIT.rst.txt
new file mode 100644
index 000000000..00a5e7b00
--- /dev/null
+++ b/_sources/api/pyirf.spectral.DIFFUSE_FLUX_UNIT.rst.txt
@@ -0,0 +1,6 @@
+DIFFUSE_FLUX_UNIT
+=================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: DIFFUSE_FLUX_UNIT
diff --git a/_sources/api/pyirf.spectral.IRFDOC_ELECTRON_SPECTRUM.rst.txt b/_sources/api/pyirf.spectral.IRFDOC_ELECTRON_SPECTRUM.rst.txt
new file mode 100644
index 000000000..ecd335bed
--- /dev/null
+++ b/_sources/api/pyirf.spectral.IRFDOC_ELECTRON_SPECTRUM.rst.txt
@@ -0,0 +1,6 @@
+IRFDOC_ELECTRON_SPECTRUM
+========================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: IRFDOC_ELECTRON_SPECTRUM
diff --git a/_sources/api/pyirf.spectral.IRFDOC_PROTON_SPECTRUM.rst.txt b/_sources/api/pyirf.spectral.IRFDOC_PROTON_SPECTRUM.rst.txt
new file mode 100644
index 000000000..d08bb3286
--- /dev/null
+++ b/_sources/api/pyirf.spectral.IRFDOC_PROTON_SPECTRUM.rst.txt
@@ -0,0 +1,6 @@
+IRFDOC_PROTON_SPECTRUM
+======================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: IRFDOC_PROTON_SPECTRUM
diff --git a/_sources/api/pyirf.spectral.LogParabola.rst.txt b/_sources/api/pyirf.spectral.LogParabola.rst.txt
new file mode 100644
index 000000000..c657e4405
--- /dev/null
+++ b/_sources/api/pyirf.spectral.LogParabola.rst.txt
@@ -0,0 +1,17 @@
+LogParabola
+===========
+
+.. currentmodule:: pyirf.spectral
+
+.. autoclass:: LogParabola
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~LogParabola.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.spectral.PDG_ALL_PARTICLE.rst.txt b/_sources/api/pyirf.spectral.PDG_ALL_PARTICLE.rst.txt
new file mode 100644
index 000000000..7ba60b1aa
--- /dev/null
+++ b/_sources/api/pyirf.spectral.PDG_ALL_PARTICLE.rst.txt
@@ -0,0 +1,6 @@
+PDG_ALL_PARTICLE
+================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: PDG_ALL_PARTICLE
diff --git a/_sources/api/pyirf.spectral.POINT_SOURCE_FLUX_UNIT.rst.txt b/_sources/api/pyirf.spectral.POINT_SOURCE_FLUX_UNIT.rst.txt
new file mode 100644
index 000000000..dd6ae397e
--- /dev/null
+++ b/_sources/api/pyirf.spectral.POINT_SOURCE_FLUX_UNIT.rst.txt
@@ -0,0 +1,6 @@
+POINT_SOURCE_FLUX_UNIT
+======================
+
+.. currentmodule:: pyirf.spectral
+
+.. autodata:: POINT_SOURCE_FLUX_UNIT
diff --git a/_sources/api/pyirf.spectral.PowerLaw.rst.txt b/_sources/api/pyirf.spectral.PowerLaw.rst.txt
new file mode 100644
index 000000000..943470780
--- /dev/null
+++ b/_sources/api/pyirf.spectral.PowerLaw.rst.txt
@@ -0,0 +1,21 @@
+PowerLaw
+========
+
+.. currentmodule:: pyirf.spectral
+
+.. autoclass:: PowerLaw
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~PowerLaw.__call__
+ ~PowerLaw.from_simulation
+ ~PowerLaw.integrate_cone
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: from_simulation
+ .. automethod:: integrate_cone
diff --git a/_sources/api/pyirf.spectral.PowerLawWithExponentialGaussian.rst.txt b/_sources/api/pyirf.spectral.PowerLawWithExponentialGaussian.rst.txt
new file mode 100644
index 000000000..381e401ad
--- /dev/null
+++ b/_sources/api/pyirf.spectral.PowerLawWithExponentialGaussian.rst.txt
@@ -0,0 +1,17 @@
+PowerLawWithExponentialGaussian
+===============================
+
+.. currentmodule:: pyirf.spectral
+
+.. autoclass:: PowerLawWithExponentialGaussian
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~PowerLawWithExponentialGaussian.__call__
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
diff --git a/_sources/api/pyirf.spectral.TableInterpolationSpectrum.rst.txt b/_sources/api/pyirf.spectral.TableInterpolationSpectrum.rst.txt
new file mode 100644
index 000000000..701089bea
--- /dev/null
+++ b/_sources/api/pyirf.spectral.TableInterpolationSpectrum.rst.txt
@@ -0,0 +1,21 @@
+TableInterpolationSpectrum
+==========================
+
+.. currentmodule:: pyirf.spectral
+
+.. autoclass:: TableInterpolationSpectrum
+ :show-inheritance:
+
+ .. rubric:: Methods Summary
+
+ .. autosummary::
+
+ ~TableInterpolationSpectrum.__call__
+ ~TableInterpolationSpectrum.from_file
+ ~TableInterpolationSpectrum.from_table
+
+ .. rubric:: Methods Documentation
+
+ .. automethod:: __call__
+ .. automethod:: from_file
+ .. automethod:: from_table
diff --git a/_sources/api/pyirf.spectral.calculate_event_weights.rst.txt b/_sources/api/pyirf.spectral.calculate_event_weights.rst.txt
new file mode 100644
index 000000000..525cb1508
--- /dev/null
+++ b/_sources/api/pyirf.spectral.calculate_event_weights.rst.txt
@@ -0,0 +1,6 @@
+calculate_event_weights
+=======================
+
+.. currentmodule:: pyirf.spectral
+
+.. autofunction:: calculate_event_weights
diff --git a/_sources/api/pyirf.statistics.li_ma_significance.rst.txt b/_sources/api/pyirf.statistics.li_ma_significance.rst.txt
new file mode 100644
index 000000000..964a5c5ed
--- /dev/null
+++ b/_sources/api/pyirf.statistics.li_ma_significance.rst.txt
@@ -0,0 +1,6 @@
+li_ma_significance
+==================
+
+.. currentmodule:: pyirf.statistics
+
+.. autofunction:: li_ma_significance
diff --git a/_sources/api/pyirf.utils.calculate_source_fov_offset.rst.txt b/_sources/api/pyirf.utils.calculate_source_fov_offset.rst.txt
new file mode 100644
index 000000000..2e63dff42
--- /dev/null
+++ b/_sources/api/pyirf.utils.calculate_source_fov_offset.rst.txt
@@ -0,0 +1,6 @@
+calculate_source_fov_offset
+===========================
+
+.. currentmodule:: pyirf.utils
+
+.. autofunction:: calculate_source_fov_offset
diff --git a/_sources/api/pyirf.utils.calculate_source_fov_position_angle.rst.txt b/_sources/api/pyirf.utils.calculate_source_fov_position_angle.rst.txt
new file mode 100644
index 000000000..a7902a846
--- /dev/null
+++ b/_sources/api/pyirf.utils.calculate_source_fov_position_angle.rst.txt
@@ -0,0 +1,6 @@
+calculate_source_fov_position_angle
+===================================
+
+.. currentmodule:: pyirf.utils
+
+.. autofunction:: calculate_source_fov_position_angle
diff --git a/_sources/api/pyirf.utils.calculate_theta.rst.txt b/_sources/api/pyirf.utils.calculate_theta.rst.txt
new file mode 100644
index 000000000..8766065e3
--- /dev/null
+++ b/_sources/api/pyirf.utils.calculate_theta.rst.txt
@@ -0,0 +1,6 @@
+calculate_theta
+===============
+
+.. currentmodule:: pyirf.utils
+
+.. autofunction:: calculate_theta
diff --git a/_sources/api/pyirf.utils.check_histograms.rst.txt b/_sources/api/pyirf.utils.check_histograms.rst.txt
new file mode 100644
index 000000000..194dca33a
--- /dev/null
+++ b/_sources/api/pyirf.utils.check_histograms.rst.txt
@@ -0,0 +1,6 @@
+check_histograms
+================
+
+.. currentmodule:: pyirf.utils
+
+.. autofunction:: check_histograms
diff --git a/_sources/api/pyirf.utils.cone_solid_angle.rst.txt b/_sources/api/pyirf.utils.cone_solid_angle.rst.txt
new file mode 100644
index 000000000..2ca4a1925
--- /dev/null
+++ b/_sources/api/pyirf.utils.cone_solid_angle.rst.txt
@@ -0,0 +1,6 @@
+cone_solid_angle
+================
+
+.. currentmodule:: pyirf.utils
+
+.. autofunction:: cone_solid_angle
diff --git a/_sources/api/pyirf.utils.is_scalar.rst.txt b/_sources/api/pyirf.utils.is_scalar.rst.txt
new file mode 100644
index 000000000..cf693503f
--- /dev/null
+++ b/_sources/api/pyirf.utils.is_scalar.rst.txt
@@ -0,0 +1,6 @@
+is_scalar
+=========
+
+.. currentmodule:: pyirf.utils
+
+.. autofunction:: is_scalar
diff --git a/_sources/benchmarks/index.rst.txt b/_sources/benchmarks/index.rst.txt
new file mode 100644
index 000000000..53badad58
--- /dev/null
+++ b/_sources/benchmarks/index.rst.txt
@@ -0,0 +1,11 @@
+.. _benchmarks:
+
+Benchmarks
+==========
+
+Functions to calculate benchmarks.
+
+-------------
+
+.. automodapi:: pyirf.benchmarks
+ :no-inheritance-diagram:
diff --git a/_sources/binning.rst.txt b/_sources/binning.rst.txt
new file mode 100644
index 000000000..f91b493e8
--- /dev/null
+++ b/_sources/binning.rst.txt
@@ -0,0 +1,11 @@
+.. _binning:
+
+Binning and Histogram Utilities
+===============================
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.binning
+ :no-inheritance-diagram:
diff --git a/_sources/changelog.rst.txt b/_sources/changelog.rst.txt
new file mode 100644
index 000000000..31c6f9c84
--- /dev/null
+++ b/_sources/changelog.rst.txt
@@ -0,0 +1,7 @@
+.. _changelog:
+
+=========
+Changelog
+=========
+
+.. include:: ../CHANGES.rst
diff --git a/_sources/contribute.rst.txt b/_sources/contribute.rst.txt
new file mode 100644
index 000000000..c35121715
--- /dev/null
+++ b/_sources/contribute.rst.txt
@@ -0,0 +1,144 @@
+.. _contribute:
+
+How to contribute
+=================
+
+.. contents:: Current projects
+ :local:
+ :depth: 2
+
+Issue Tracker
+-------------
+
+We use the `GitHub issue tracker `__
+for individual issues and the `GitHub Projects page `_ can give you a quick overview.
+
+If you found a bug or you are missing a feature, please check the existing
+issues and then open a new one or contribute to the existing issue.
+
+Development procedure
+---------------------
+
+
+We use the standard `GitHub workflow `__.
+
+If you are not part of the ``cta-observatory`` organization,
+you need to fork the repository to contribute.
+See the `GitHub tutorial on forks `__ if you are unsure how to do this.
+
+#. When you find something that is wrong or missing
+
+ - Go to the issue tracker and check if an issue already exists for your bug or feature
+ - In general it is always better to anticipate a PR with a new issue and link the two
+
+#. To work on a bug fix or new feature, create a new branch, add commits and open your pull request
+
+ - If you think your pull request is good to go and ready to be reviewed,
+ you can directly open it as normal pull request.
+
+ - You can also open it as a “Draft Pull Request”, if you are not yet finished
+ but want to get early feedback on your ideas.
+
+ - Especially when working on a bug, it makes sense to first add a new
+ test that fails due to the bug and in a later commit add the fix showing
+ that the test is then passing.
+ This helps understanding the bug and will prevent it from reappearing later.
+
+ - Create a changelog entry in ``docs/changes``, please note the ``README.md`` there.
+ Minor changes (on the magnitude of fixing a broken link or renaming a variable) can receive the ``no-changelog-needed`` label.
+ This should, however, be a rare exception.
+
+#. Wait for review comments and then implement or discuss requested changes.
+
+
+We use `Github Actions `__ to
+run the unit tests and documentation building automatically for every pull request.
+Passing unit tests and coverage of the changed code are required for all pull requests.
+
+
+Running the tests and looking at coverage
+-----------------------------------------
+
+For more immediate feedback, you should run the tests locally before pushing,
+as builds on travis take quite long.
+
+To run the tests locally, make sure you have the `tests` extras installed and then
+run
+
+.. code:: bash
+
+ $ pytest -v
+
+
+To also inspect the coverage, run
+
+.. code:: bash
+
+ $ pytest --cov=pyirf --cov-report=html -v
+
+This will create a coverage report in html form in the ``htmlcov`` directory,
+which you can serve locally using
+
+.. code:: bash
+
+ $ python -m http.server -d htmlcov
+
+After this, you can view the report in your browser by visiting the url printed
+to the terminal.
+
+
+Building the documentation
+--------------------------
+
+This documentation uses sphinx and restructured text.
+For an Introduction, see the `Sphinx documentation `_.
+
+To build the docs locally, enter the ``docs`` directory and call:
+
+.. code:: bash
+
+ make html
+
+Some changes require a full remake of the documentation, for that call
+
+.. code:: bash
+
+ make clean html
+
+If you created or deleted file or submodule, you also need to remove the
+``api`` directory, it will be regenerated automatically.
+
+Make sure the docs are built without warnings from sphinx, as these
+will be treated as errors in the build in the CI system as they most often
+result in broken styling.
+
+To look at the docs, use
+
+.. code:: bash
+
+ $ python -m http.server _build/html
+
+and visit the printed URL in your browser.
+
+Making your contribution visible
+--------------------------------
+
+Together with the changes that will come with you PR, you should check that the
+following maintenance files are up-to-date:
+
+- ``.mailmap``
+- ``CODEOWNERS``
+- ``.zenodo.json``
+
+Further details
+---------------
+
+Please also have a look at the
+
+- ``ctapipe`` `development guidelines `__
+- The `Open Gamma-Ray Astronomy data formats `__
+ which also describe the IRF formats and their definitions.
+- ``ctools`` `documentation page on IRFs `__
+- `CTA IRF working group wiki (internal) `__
+
+- `CTA IRF Description Document for Prod3b (internal) `__
diff --git a/_sources/cut_optimization.rst.txt b/_sources/cut_optimization.rst.txt
new file mode 100644
index 000000000..b35363b37
--- /dev/null
+++ b/_sources/cut_optimization.rst.txt
@@ -0,0 +1,11 @@
+.. _cut_optimization:
+
+Cut Optimization
+================
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.cut_optimization
+ :no-inheritance-diagram:
diff --git a/_sources/cuts.rst.txt b/_sources/cuts.rst.txt
new file mode 100644
index 000000000..b86adb71d
--- /dev/null
+++ b/_sources/cuts.rst.txt
@@ -0,0 +1,11 @@
+.. _cuts:
+
+Calculating and Applying Cuts
+=============================
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.cuts
+ :no-inheritance-diagram:
diff --git a/_sources/examples.rst.txt b/_sources/examples.rst.txt
new file mode 100644
index 000000000..2efe6e61b
--- /dev/null
+++ b/_sources/examples.rst.txt
@@ -0,0 +1,44 @@
+.. _examples:
+
+Examples
+========
+
+Calculating Sensitivity and IRFs for EventDisplay DL2 data
+----------------------------------------------------------
+
+The ``examples/calculate_eventdisplay_irfs.py`` file is
+using ``pyirf`` to optimize cuts, calculate sensitivity and IRFs
+and then store these to FITS files for DL2 event lists from EventDisplay.
+
+The ROOT files were provided by Gernot Maier and converted to FITS format
+using `the EventDisplay DL2 converter script `_.
+The resulting FITS files are the input to the example and can be downloaded using:
+
+.. code:: bash
+
+ ./download_private_data.sh
+
+This requires ``curl`` and ``unzip`` to be installed.
+The download is password protected, please ask one of the maintainers for the
+password.
+
+A detailed explanation of the contents of such DL2 files can be found
+`here (internal) `_.
+
+The example can then be run from the root of the repository after installing pyirf
+by running:
+
+.. code:: bash
+
+ python examples/calculate_eventdisplay_irfs.py
+
+
+A jupyter notebook plotting the results and comparing them to the EventDisplay output
+is available in ``examples/comparison_with_EventDisplay.ipynb``
+
+
+Visualization of the included Flux Models
+-----------------------------------------
+
+The ``examples/plot_spectra.py`` visualizes the Flux models included
+in ``pyirf`` for Crab Nebula, cosmic ray and electron flux.
diff --git a/_sources/gammapy.rst.txt b/_sources/gammapy.rst.txt
new file mode 100644
index 000000000..da632d2ed
--- /dev/null
+++ b/_sources/gammapy.rst.txt
@@ -0,0 +1,13 @@
+.. _gammapy:
+
+Gammapy Interoperability
+========================
+
+This module provides functions to convert the ``pyirf`` quantities
+for IRFs and the binning to the corresponding ``gammapy`` classes.
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.gammapy
+ :no-inheritance-diagram:
diff --git a/_sources/index.rst.txt b/_sources/index.rst.txt
new file mode 100644
index 000000000..00d92469a
--- /dev/null
+++ b/_sources/index.rst.txt
@@ -0,0 +1,93 @@
+.. meta::
+ :github_url: https://github.com/cta-observatory/pyirf
+
+Welcome to pyirf's documentation!
+=================================
+
+`pyirf` is a prototype for the generation of Instrument Response Functions (IRFs)
+for the `Cherenkov Telescope Array `__
+(CTA).
+The package is being developed and tested by members of the CTA consortium and
+is a spin-off of the analog sub-process of the
+`pipeline protopype `_.
+
+Its main features are currently to
+
+ * find the best cutoff in gammaness/score, to discriminate between signal
+ and background, as well as the angular cut to obtain the best sensitivity
+ for a given amount of observation time and a given template for the
+ source of interest (:ref:`cut_optimization`)
+ * compute the instrument response functions, effective area,
+ point spread function and energy resolution (:ref:`irf`)
+ * estimate the sensitivity of the array (:ref:`sensitivity`),
+
+with plans to extend its capabilities to reach the requirements of the
+future observatory.
+
+.. Should we add the following or is it too soon? --->
+.. Event though the initial efforts are focused on CTA, it is potentially possible
+.. to extend the capabilities of `pyirf` to other IACTs instruments as well.
+
+The source code is hosted on a `GitHub repository `__, to
+which this documentation is linked.
+
+.. warning::
+ This is not yet stable code, so expect large and rapid changes.
+
+Citing this software
+--------------------
+
+.. |doilatest| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4740755.svg
+ :target: https://doi.org/10.5281/zenodo.4740755
+.. |doi_v0.5.0| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4748994.svg
+ :target: https://doi.org/10.5281/zenodo.4748994
+.. |doi_v0.4.0| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4304466.svg
+ :target: https://doi.org/10.5281/zenodo.4304466
+
+If you use a released version of this software for a publication,
+please cite it by using the corresponding DOI.
+
+- latest : |doilatest|
+- v0.5.0 : |doi_v0.5.0|
+- v0.4.0 : |doi_v0.4.0|
+
+.. toctree::
+ :maxdepth: 1
+ :caption: Overview
+ :name: _pyirf_intro
+
+ install
+ introduction
+ examples
+ notebooks/index
+ contribute
+ changelog
+ AUTHORS
+
+
+.. toctree::
+ :maxdepth: 1
+ :caption: API Documentation
+ :name: _pyirf_api_docs
+
+ irf/index
+ sensitivity
+ benchmarks/index
+ cuts
+ cut_optimization
+ simulation
+ spectral
+ statistics
+ binning
+ io/index
+ interpolation
+ gammapy
+ utils
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/_sources/install.rst.txt b/_sources/install.rst.txt
new file mode 100644
index 000000000..a90f8a648
--- /dev/null
+++ b/_sources/install.rst.txt
@@ -0,0 +1,67 @@
+.. _install:
+
+Installation
+============
+
+``pyirf`` requires Python ≥3.7 and pip, plus the packages defined in
+the ``setup.py``.
+
+Core dependencies are
+
+* ``numpy``
+* ``astropy``
+* ``scipy``
+
+We provide an environment file for Anaconda or Miniconda users.
+
+Installing a released version
+-----------------------------
+
+To install a released version, just install the ``pyirf`` package using
+
+.. code-block:: bash
+
+ $ pip install pyirf
+
+or add it to the dependencies of your project.
+
+Installing for development
+--------------------------
+
+If you want to work on pyirf itself, clone the repository and install the local
+copy of pyirf in development mode.
+
+The dependencies required to perform unit-testing and to build the documentation
+are defined in ``extras`` under ``tests`` and ``docs`` respectively.
+
+These requirements can also be enabled by installing the ``all`` extra:
+
+.. code-block:: bash
+
+ $ pip install -e '.[all]' # or [docs,tests] to install them separately
+
+
+You should isolate your pyirf development environment from the rest of your system.
+Either by using a virtual environment or by using ``conda`` environments.
+``pyirf`` provides a conda ``environment.yml``, that includes all dependencies:
+
+.. code-block:: bash
+
+ $ conda env create -f environment.yml
+ $ conda activate pyirf
+ $ pip install -e '.[all]'
+
+In order to have passing unit-tests you have to download some CTA IRFs
+from `zenodo `. Simply run
+
+.. code-block:: bash
+
+ $ python download_irfs.py
+
+which will download and unpack three IRF files to ``irfs/``.
+
+Run the tests to make sure everything is OK:
+
+.. code-block:: bash
+
+ $ pytest
diff --git a/_sources/interpolation.rst.txt b/_sources/interpolation.rst.txt
new file mode 100644
index 000000000..5ed66dfbc
--- /dev/null
+++ b/_sources/interpolation.rst.txt
@@ -0,0 +1,262 @@
+.. _interpolation:
+
+Interpolation and Extrapolation of IRFs
+=======================================
+
+.. currentmodule:: pyirf.interpolation
+
+This module contains functions to inter- or extrapolate from a set of IRFs for different
+conditions to a new IRF. Implementations of interpolation and extrapolation algorithms
+exist as interpolator and extrapolator classes and are applied by top-level estimator
+classes to IRF components.
+Direct usage of the inter- and extrapolator classes is discouraged, as only the estimator classes
+check the data for consistency.
+
+Most methods support an arbitrary number of interpolation dimensions although it
+is strongly advised to limit those for reasonable results.
+The herein provided functionalities can e.g. be used to interpolate the IRF
+for a zenith angle of 30° from available IRFs at 20° and 40°.
+
+
+IRF Component Estimator Classes
+-------------------------------
+
+.. autosummary::
+ :nosignatures:
+
+ EffectiveAreaEstimator Estimate AEFF tables.
+ RadMaxEstimator Estimate RadMax tables.
+ EnergyDispersionEstimator Estimate 2D EDISPs.
+ PSFTableEstimator Estimate PSF tables.
+
+
+
+Inter- and Extrapolation Classes
+--------------------------------
+
+This module provides inter- and extrapolation classes that can be
+plugged into the estimator classes.
+Not all of these classes support arbitrary grid-dimensions where the grid
+in this context is the grid of e.g. observation parameters like zenith angle and
+magnetic field inclination (this would be a 2D grid) on which template IRFs exist
+and are meant to be inter- or extrapolated.
+
+For parametrized components (Effective Areas and Rad-Max tables) these classes are:
+
+============================================= ================== ============ ==================================================================================================
+**Name** **Type** **Grid-Dim** **Note**
+============================================= ================== ============ ==================================================================================================
+:any:`GridDataInterpolator` Interpolation Arbitrary See also :any:`scipy.interpolate.griddata`.
+:any:`ParametrizedNearestSimplexExtrapolator` Extrapolation 1D or 2D Linear (1D) or baryzentric (2D) extension outside the grid's convex hull from the nearest simplex.
+:any:`ParametrizedVisibleEdgesExtrapolator` Extrapolation 1D or 2D Like :any:`ParametrizedNearestSimplexExtrapolator` but blends over all visible simplices [Alf84]_ and is thus smooth outside the convex hull.
+:any:`ParametrizedNearestNeighborSearcher` Nearest Neighbor Arbitrary Nearest neighbor finder usable instead of inter- and/or extrapolation.
+============================================= ================== ============ ==================================================================================================
+
+For components represented by discretized PDFs (PSF and EDISP tables) these classes are:
+
+============================================= ================== ============ ==============================================================================
+**Name** **Type** **Grid-Dim** **Note**
+============================================= ================== ============ ==============================================================================
+:any:`QuantileInterpolator` Interpolation Arbitrary Adaption of [Hol+13]_ and [Rea99]_ to discretized PDFs.
+:any:`MomentMorphInterpolator` Interpolation 1D or 2D Adaption of [Baa+15]_ to discretized PDFs.
+:any:`MomentMorphNearestSimplexExtrapolator` Extrapolation 1D or 2D Extension of [Baa+15]_ beyond the grid's convex hull from the nearest simplex.
+:any:`DiscretePDFNearestNeighborSearcher` Nearest Neighbor Arbitrary Nearest neighbor finder usable instead of inter- and/or extrapolation.
+============================================= ================== ============ ==============================================================================
+
+.. [Alf84] P. Alfred (1984). Triangular Extrapolation.
+ Technical summary rept., Univ. of Wisconsin-Madison. https://apps.dtic.mil/sti/pdfs/ADA144660.pdf
+.. [Hol+13] B. E. Hollister and A. T. Pang (2013). Interpolation of Non-Gaussian Probability Distributions for Ensemble Visualization.
+ https://engineering.ucsc.edu/sites/default/files/technical-reports/UCSC-SOE-13-13.pdf
+.. [Rea99] A. L. Read (1999). Linear Interpolation of Histograms.
+ Nucl. Instrum. Methods Phys. Res. A 425, 357-360. https://doi.org/10.1016/S0168-9002(98)01347-3
+.. [Baa+15] M. Baak, S. Gadatsch, R. Harrington and W. Verkerke (2015). Interpolation between
+ multi-dimensional histograms using a new non-linear moment morphing method
+ Nucl. Instrum. Methods Phys. Res. A 771, 39-48. https://doi.org/10.1016/j.nima.2014.10.033
+
+
+Using Estimator Classes
+-----------------------
+
+Usage of the estimator classes is simple.
+As an example, consider CTA's Prod5 IRFs [CTA+21]_, they can be downloaded manually or by executing
+``download_irfs.py`` in ``pyirf's`` root directory, which downloads them to ``.../pyirf/irfs/``.
+The estimator classes can simply be used by first creating an instance of the respective class with all
+relevant information and then using the object's ``__call__`` interface the obtain results for a specific
+target point.
+As the energy dispersion represents one of the discretized PDF IRF components, one can use the
+``MomentMorphInterpolator`` for interpolation and the ``DiscretePDFNearestNeighborSearcher``
+for extrapolation.
+
+.. code-block:: python
+
+ import numpy as np
+
+ from gammapy.irf import load_irf_dict_from_file
+ from glob import glob
+ from pyirf.interpolation import (
+ EnergyDispersionEstimator,
+ MomentMorphInterpolator,
+ DiscretePDFNearestNeighborSearcher
+ )
+
+ # Load IRF data, replace path with actual path
+ PROD5_IRF_PATH = "pyirf/irfs/*.fits.gz"
+
+ irfs = [load_irf_dict_from_file(path) for path in sorted(glob(PROD5_IRF_PATH))]
+
+ edisps = np.array([irf["edisp"].quantity for irf in irfs])
+ bin_edges = irfs[0]["edisp"].axes["migra"].edges
+ # IRFs are for zenith distances of 20, 40 and 60 deg
+ zen_pnt = np.array([[20], [40], [60]])
+
+ # Create estimator instance
+ edisp_estimator = EnergyDispersionEstimator(
+ grid_points=zen_pnt,
+ migra_bins=bin_edges,
+ energy_dispersion=edisps,
+ interpolator_cls=MomentMorphInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=DiscretePDFNearestNeighborSearcher,
+ extrapolator_kwargs=None,
+ )
+
+ # Estimate energy dispersions
+ interpolated_edisp = edisp_estimator(np.array([[30]]))
+ extrapolated_edisp = edisp_estimator(np.array([[10]]))
+
+
+.. [CTA+21] Cherenkov Telescope Array Observatory & Cherenkov Telescope Array Consortium. (2021).
+ CTAO Instrument Response Functions - prod5 version v0.1 (v0.1) [Data set]. Zenodo.
+ https://doi.org/10.5281/zenodo.5499840
+
+
+Creating new Estimator Classes
+------------------------------
+
+To create a estimator class for an IRF component not yet implemented, one can simply
+inherit from respective base class.
+There are two, tailored to either parametrized or discrete PDF components.
+
+.. autosummary::
+ :nosignatures:
+
+ ParametrizedComponentEstimator Parametrized components
+ DiscretePDFComponentEstimator Discrete PDF components
+
+Consider an example, where one is interested in an estimator for simple Gaussians.
+As this is already the scope of the ``DiscretePDFComponentEstimator`` base class and
+for the sake of this demonstration, let the Gaussians come with some
+units attached that need handling:
+
+.. code-block:: python
+
+ import astropy.units as u
+ from pyirf.interpolation import (DiscretePDFComponentEstimator,
+ MomentMorphInterpolator)
+
+ class GaussianEstimatior(DiscretePDFComponentEstimator):
+ @u.quantity_input(gaussians=u.m)
+ def __init__(
+ self,
+ grid_points,
+ bin_edges,
+ gaussians,
+ interpolator_cls=MomentMorphInterpolator,
+ interpolator_kwargs=None,
+ extrapolator_cls=None,
+ extrapolator_kwargs=None,
+ ):
+ if interpolator_kwargs is None:
+ interpolator_kwargs = {}
+
+ if extrapolator_kwargs is None:
+ extrapolator_kwargs = {}
+
+ self.unit = gaussians.unit
+
+ super().__init__(
+ grid_points=grid_points,
+ bin_edges=bin_edges,
+ binned_pdf=gaussians.to_value(u.m),
+ interpolator_cls=interpolator_cls,
+ interpolator_kwargs=interpolator_kwargs,
+ extrapolator_cls=extrapolator_cls,
+ extrapolator_kwargs=extrapolator_kwargs,
+ )
+
+ def __call__(self, target_point):
+ res = super().__call__(target_point)
+
+ # Return result with correct unit
+ return u.Quantity(res, u.m, copy=False).to(self.unit)
+
+This new estimator class can now be used just like any other estimator class already
+implemented in ``pyirf.interpolation``.
+While the ``extrapolator_cls`` argument can be empty when creating an instance of
+``GaussianEstimator``, effectively disabling extrapolation and raising an error in
+case it would be needed regardless, assume the desired extrapolation method to be
+``MomentMorphNearestSimplexExtrapolator``:
+
+.. code-block:: python
+
+ import numpy as np
+ from pyirf.interpolation import MomentMorphNearestSimplexExtrapolator
+ from scipy.stats import norm
+
+ bins = np.linspace(-10, 10, 51)
+ grid = np.array([[1], [2], [3]])
+
+ gaussians = np.array([np.diff(norm(loc=x, scale=1/x).cdf(bins))/np.diff(bins) for x in grid])
+
+ estimator = GaussianEstimatior(
+ grid_points = grid,
+ bin_edges = bins,
+ gaussians = gaussians * u.m,
+ interpolator_cls = MomentMorphInterpolator,
+ extrapolator_cls = MomentMorphNearestSimplexExtrapolator
+ )
+
+This estimator object can now easily be used to estimate Gaussians at arbitrary target points:
+
+.. code-block:: python
+
+ targets = np.array([[0.9], [1.5]])
+
+ results = u.Quantity([estimator(target).squeeze() for target in targets])
+
+
+Helper Classes
+--------------
+
+.. autosummary::
+ :nosignatures:
+
+ PDFNormalization
+
+
+Base Classes
+------------
+
+.. autosummary::
+ :nosignatures:
+
+ BaseComponentEstimator
+ ParametrizedComponentEstimator
+ DiscretePDFComponentEstimator
+ BaseInterpolator
+ ParametrizedInterpolator
+ DiscretePDFInterpolator
+ BaseExtrapolator
+ ParametrizedExtrapolator
+ DiscretePDFExtrapolator
+ BaseNearestNeighborSearcher
+
+
+Full API
+--------
+
+.. automodapi:: pyirf.interpolation
+ :no-heading:
+ :no-main-docstr:
+ :inherited-members:
+ :no-inheritance-diagram:
diff --git a/_sources/introduction.rst.txt b/_sources/introduction.rst.txt
new file mode 100644
index 000000000..e13a50753
--- /dev/null
+++ b/_sources/introduction.rst.txt
@@ -0,0 +1,92 @@
+.. _introduction:
+
+Introduction to ``pyirf``
+=========================
+
+
+``pyirf`` aims to provide functions to calculate the Instrument Response Functions (IRFs)
+and sensitivity for Imaging Air Cherenkov Telescopes.
+
+To support a wide range of use cases, ``pyirf`` opts for a library approach of
+composable building blocks with well-defined inputs and outputs.
+
+For more information on IRFs, have a look at the `Specification of the Data Formats for Gamma-Ray Astronomy`_
+or the `ctools documentation on IRFs `_.
+
+
+Currently, ``pyirf`` allows calculation of the usual factorization of the IRFs into:
+
+* Effective area
+* Energy migration
+* Point spread function
+
+Additionally, functions for calculating point-source flux sensitivity are provided.
+Flux sensitivity is defined as the smallest flux an IACT can detect with a certain significance,
+usually 5 σ according to the Li&Ma likelihood ratio test, in a specified amount of time.
+
+``pyirf`` also provides functions to calculate event weights, that are needed
+to translate a set of simulations to a physical flux for calculating sensitivity
+and expected event counts.
+
+Event selection with energy dependent cuts is also supported,
+but at the moment, only rudimentary functions to find optimal cuts are provided.
+
+
+Input formats
+-------------
+
+``pyirf`` does not rely on specific input file formats.
+All functions take ``numpy`` arrays, astropy quantities or astropy tables for the
+required data and also return the results as these objects.
+
+``~pyirf.io`` provides functions to export the internal IRF representation
+to FITS files following the `Specification of the Data Formats for Gamma-Ray Astronomy`_
+
+
+DL2 event lists
+^^^^^^^^^^^^^^^
+
+Most functions for calculating IRFs need DL2 event lists as input.
+We use ``~astropy.table.QTable`` instances for this.
+``QTable`` are very similar to the standard ``~astropy.table.Table``,
+but offer better interoperability with ``astropy.units.Quantity``.
+
+We expect certain columns to be present in the tables with the appropriate units.
+To learn which functions need which columns to be present, have a look at the :ref:`_pyirf_api_docs`
+
+Most functions only need a small subgroup of these columns.
+
+.. table:: Column definitions for DL2 event lists
+
+ +------------------------+--------+----------------------------------------------------+
+ | Column | Unit | Explanation |
+ +========================+========+====================================================+
+ | true_energy | TeV | True energy of the simulated shower |
+ +------------------------+--------+----------------------------------------------------+
+ | weight | | Event weight |
+ +------------------------+--------+----------------------------------------------------+
+ | true_source_fov_offset | deg | Distance of the true origin to the FOV center |
+ +------------------------+--------+----------------------------------------------------+
+ | reco_source_fov_offset | deg | Distance of the reco origin to the FOV center |
+ +------------------------+--------+----------------------------------------------------+
+ | true_alt | deg | True altitude of the shower origin |
+ +------------------------+--------+----------------------------------------------------+
+ | true_az | deg | True azimuth of the shower origin |
+ +------------------------+--------+----------------------------------------------------+
+ | pointing_alt | deg | Altitude of the field of view center |
+ +------------------------+--------+----------------------------------------------------+
+ | pointing_az | deg | Azimuth of the field of view center |
+ +------------------------+--------+----------------------------------------------------+
+ | reco_energy | TeV | Reconstructed energy of the simulated shower |
+ +------------------------+--------+----------------------------------------------------+
+ | reco_alt | deg | Reconstructed altitude of shower origin |
+ +------------------------+--------+----------------------------------------------------+
+ | reco_az | deg | Reconstructed azimuth of shower origin |
+ +------------------------+--------+----------------------------------------------------+
+ | gh_score | | Gamma/Hadron classification output |
+ +------------------------+--------+----------------------------------------------------+
+ | multiplicity | | Number of telescopes used in the reconstruction |
+ +------------------------+--------+----------------------------------------------------+
+
+
+.. _Specification of the Data Formats for Gamma-Ray Astronomy: https://gamma-astro-data-formats.readthedocs.io
diff --git a/_sources/io/index.rst.txt b/_sources/io/index.rst.txt
new file mode 100644
index 000000000..3094fd16d
--- /dev/null
+++ b/_sources/io/index.rst.txt
@@ -0,0 +1,19 @@
+.. _io:
+
+Input / Output
+==============
+
+Introduction
+------------
+
+This module contains functions to read input data and write IRFs in GADF format.
+
+Currently there is only support for reading EventDisplay DL2 FITS files,
+which were converted from the ROOT files by using `EventDisplay DL2 conversion scripts `_.
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.io
+ :no-inheritance-diagram:
diff --git a/_sources/irf/index.rst.txt b/_sources/irf/index.rst.txt
new file mode 100644
index 000000000..0aeb5e6df
--- /dev/null
+++ b/_sources/irf/index.rst.txt
@@ -0,0 +1,42 @@
+.. _irf:
+
+Instrument Response Functions
+=============================
+
+
+Effective Area
+--------------
+
+The collection area, which is proportional to the gamma-ray efficiency
+of detection, is computed as a function of the true energy. The events which
+are considered are the ones passing the threshold of the best cutoff plus
+the angular cuts.
+
+Energy Dispersion Matrix
+------------------------
+
+The energy dispersion matrix, ratio of the reconstructed energy over the true energy
+as a function of the true energy, is computed with the events passing the
+threshold of the best cutoff plus the angular cuts.
+
+The corresponding energy migration matrix can be build from the dispersion matrix.
+
+
+Point Spread Function
+---------------------
+
+The PSF describes the probability of measuring a gamma ray
+of a given true energy and true position at a reconstructed position.
+
+Background rate
+---------------
+
+The background rate is calculated as the number of background-like events per
+second, reconstructed energy and solid angle.
+The current version is computed in radially symmetric bins in the Field Of View.
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.irf
+ :no-inheritance-diagram:
diff --git a/_sources/notebooks/fact_example.ipynb.txt b/_sources/notebooks/fact_example.ipynb.txt
new file mode 100644
index 000000000..f1ed1f245
--- /dev/null
+++ b/_sources/notebooks/fact_example.ipynb.txt
@@ -0,0 +1,506 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "plastic-system",
+ "metadata": {},
+ "source": [
+ "# Using `pyirf` to calculate IRFs from the FACT Open Data\n",
+ "\n",
+ "\n",
+ "**Note** In FACT, we used a different terminology, partly because of being a monoscopic telescope or out of confusion witht the CTA terms, in this context DL3 are reconstructed events, but not necessarily already with the IRF"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "alike-dover",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import astropy.units as u\n",
+ "import matplotlib.pyplot as plt\n",
+ "import subprocess as sp"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "german-carroll",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%matplotlib inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "analyzed-canberra",
+ "metadata": {},
+ "source": [
+ "## Download Data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "joined-experiment",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "path = \"gamma_test_dl3.hdf5\"\n",
+ "url = f\"https://factdata.app.tu-dortmund.de/dl3/FACT-Tools/v1.1.2/{path}\"\n",
+ "ret = sp.run([\"curl\", \"-z\", path, \"-fsSLO\", url], stdout=sp.PIPE, stderr=sp.PIPE, encoding='utf-8')\n",
+ "if ret.returncode != 0:\n",
+ " raise IOError(ret.stderr)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "accredited-count",
+ "metadata": {},
+ "source": [
+ "## Read in the data\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "italian-redhead",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from astropy.table import QTable\n",
+ "import astropy.units as u\n",
+ "import tables"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "healthy-wrapping",
+ "metadata": {},
+ "source": [
+ "### Simulated Event Info\n",
+ "\n",
+ "Currently, pyirf only works with powerlaw simulated events, like CORSIKA does it.\n",
+ "We want to also support arbitrary histograms / event distributions, but that is not yet implemented.\n",
+ "\n",
+ "This can be created from a file with that information, but I will just create it here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "micro-anniversary",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.simulations import SimulatedEventsInfo\n",
+ "\n",
+ "simulation_info = SimulatedEventsInfo(\n",
+ " energy_min=200 * u.GeV,\n",
+ " energy_max=50 * u.TeV,\n",
+ " spectral_index=-2.7,\n",
+ " n_showers=12600000,\n",
+ " max_impact=300 * u.m,\n",
+ " viewcone_min=0 * u.deg,\n",
+ " viewcone_max=0 * u.deg,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "interior-richards",
+ "metadata": {},
+ "source": [
+ "### DL2 Event List\n",
+ "\n",
+ "`pyirf` does not prescribe or use a specific DL2 *file* format.\n",
+ "You need to read the data into an `astropy.table.QTable` following our conventions, detailed in the docs here: \n",
+ "\n",
+ "\n",
+ "\n",
+ "The FACT-Tools / aict-tools analysis chain uses a column-oriented hdf5 file written using h5py. \n",
+ "Unfortunately, units have to be known and are not in the metadata."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "southeast-reform",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gammas = QTable()\n",
+ "\n",
+ "# mapping of : ()\n",
+ "columns = {\n",
+ " 'obs_id': ('run_id', None),\n",
+ " 'event_id': ('event_num', None),\n",
+ " 'reco_energy': ('gamma_energy_prediction', u.GeV),\n",
+ " 'true_energy': ('corsika_event_header_total_energy', u.GeV),\n",
+ " 'true_az': ('source_position_az', u.deg),\n",
+ " 'pointing_az': ('pointing_position_az', u.deg),\n",
+ " 'theta': ('theta_deg', u.deg),\n",
+ " 'gh_score': ('gamma_prediction', None),\n",
+ "}\n",
+ "\n",
+ "with tables.open_file('gamma_test_dl3.hdf5', mode='r') as f:\n",
+ " events = f.root.events\n",
+ " \n",
+ " for col, (name, unit) in columns.items():\n",
+ " if unit is not None:\n",
+ " gammas[col] = u.Quantity(events[name][:], unit, copy=False)\n",
+ " else:\n",
+ " gammas[col] = events[name][:]\n",
+ " \n",
+ " gammas['true_alt'] = u.Quantity(90 - events['source_position_zd'][:], u.deg, copy=False)\n",
+ " gammas['pointing_alt'] = u.Quantity(90 - events['pointing_position_zd'][:], u.deg, copy=False)\n",
+ "\n",
+ " \n",
+ "# make it display nice\n",
+ "for col in gammas.colnames:\n",
+ " if gammas[col].dtype == float:\n",
+ " gammas[col].info.format = '.2f'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "optional-crawford",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gammas[:10]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "virgin-source",
+ "metadata": {},
+ "source": [
+ "### Apply Event Selection\n",
+ "\n",
+ "We remove likely hadronic events by requiring a minimal `gh_score`.\n",
+ "\n",
+ "We will calculate point-like IRFs, that means selecting events in a radius around the \n",
+ "assumed source position."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "proved-store",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "gammas['selected_gh'] = gammas['gh_score'] > 0.8\n",
+ "gammas['selected_theta'] = gammas['theta'] < 0.16 * u.deg\n",
+ "\n",
+ "gammas['selected'] = gammas['selected_gh'] & gammas['selected_theta']\n",
+ "\n",
+ "np.count_nonzero(gammas['selected']) / len(gammas)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "universal-potential",
+ "metadata": {},
+ "source": [
+ "## Calculate IRFs\n",
+ "\n",
+ "### Effective area\n",
+ "\n",
+ "We only have point-like simulations at a specific wobble offset (0.6° for FACT),\n",
+ "so we calculate the effective area for all events at once, equivalent to a single fov offset bin.\n",
+ "\n",
+ "\n",
+ "#### Create the binning"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "failing-exchange",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.binning import create_bins_per_decade, bin_center"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "compact-complaint",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "true_energy_bins = create_bins_per_decade(simulation_info.energy_min, simulation_info.energy_max, 5)\n",
+ "\n",
+ "# single offset bin around the wobble distance\n",
+ "# since we are dealing with point-like simulations \n",
+ "wobble_offset = 0.6 * u.deg\n",
+ "fov_offset_bins = [0.59, 0.61] * u.deg"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "blocked-japan",
+ "metadata": {},
+ "source": [
+ "### Calculate effective area\n",
+ "\n",
+ "\n",
+ "Effective area is calculated before and after cuts, for the IRF, we only need after the event selection\n",
+ "has been applied.\n",
+ "\n",
+ "The difference between point-like IRFs and Full-Enclosure IRFs is if a theta cut has been applied or not."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "frequent-concert",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.irf import effective_area_per_energy\n",
+ "\n",
+ "aeff_all = effective_area_per_energy(gammas, simulation_info, true_energy_bins)\n",
+ "aeff_selected = effective_area_per_energy(gammas[gammas['selected']], simulation_info, true_energy_bins)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "armed-street",
+ "metadata": {},
+ "source": [
+ "Let's use gammapy to plot the IRF"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "norman-personal",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# utility function to converet pyirf Quantities to the gammapy classes\n",
+ "from pyirf.gammapy import create_effective_area_table_2d\n",
+ "\n",
+ "plt.figure()\n",
+ "\n",
+ "for aeff, label in zip((aeff_all, aeff_selected), ('All Events', 'Selected Events')):\n",
+ " aeff_gammapy = create_effective_area_table_2d(\n",
+ " # add a new dimension for the single fov offset bin\n",
+ " effective_area=aeff[..., np.newaxis],\n",
+ " true_energy_bins=true_energy_bins,\n",
+ " fov_offset_bins=fov_offset_bins,\n",
+ " )\n",
+ "\n",
+ "\n",
+ " aeff_gammapy.plot_energy_dependence(label=label, offset=[wobble_offset])\n",
+ "\n",
+ "plt.xlim(true_energy_bins.min().to_value(u.GeV), true_energy_bins.max().to_value(u.GeV)) \n",
+ "plt.yscale('log')\n",
+ "plt.xscale('log')\n",
+ "plt.legend()\n",
+ "\n",
+ "print(aeff_gammapy)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eleven-sessions",
+ "metadata": {},
+ "source": [
+ "### Point Spread Function\n",
+ "\n",
+ "The point spread function describes how well the direction of the gamma rays is estimated."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "spiritual-attention",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.irf import psf_table\n",
+ "from pyirf.utils import calculate_source_fov_offset\n",
+ "\n",
+ "\n",
+ "gammas['true_source_fov_offset'] = calculate_source_fov_offset(gammas)\n",
+ "\n",
+ "\n",
+ "source_offset_bins = np.linspace(0, 3, 100) * u.deg\n",
+ "\n",
+ "# calculate this only for the events after the gamma/hadron separation\n",
+ "psf = psf_table(gammas[gammas['selected_gh']], true_energy_bins, source_offset_bins, fov_offset_bins)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "animated-prescription",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "psf.shape"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "opposed-coordinator",
+ "metadata": {},
+ "source": [
+ "Again, let's use gammapy to plot:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "spoken-shock",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.gammapy import create_psf_3d\n",
+ "\n",
+ "psf_gammapy = create_psf_3d(psf, true_energy_bins, source_offset_bins, fov_offset_bins)\n",
+ "\n",
+ "plt.figure()\n",
+ "psf_gammapy.plot_psf_vs_rad(offset=[wobble_offset], energy_true=[1., 10.]*u.TeV)\n",
+ "plt.legend(plt.gca().lines, ['1 TeV', '10 TeV'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "floral-aquarium",
+ "metadata": {},
+ "source": [
+ "### Energy Dispersion\n",
+ "\n",
+ "Describes how well the energy is estimated"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "north-compatibility",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.irf import energy_dispersion\n",
+ "\n",
+ "# logarithmic space, is \"symmetric\" in terms of ratios 0.1 is a factor of 10 from 1 is a factor of 10 from 10\n",
+ "migration_bins = np.geomspace(0.1, 10, 100)\n",
+ "\n",
+ "edisp = energy_dispersion(\n",
+ " gammas[gammas['selected']],\n",
+ " true_energy_bins=true_energy_bins,\n",
+ " fov_offset_bins=fov_offset_bins,\n",
+ " migration_bins=migration_bins,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "copyrighted-oakland",
+ "metadata": {},
+ "source": [
+ "Plot edisp"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "heard-plate",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from gammapy.irf import EnergyDispersion2D\n",
+ "\n",
+ "plt.figure()\n",
+ "plt.pcolormesh(\n",
+ " true_energy_bins.to_value(u.GeV),\n",
+ " migration_bins,\n",
+ " edisp[:, :, 0].T,\n",
+ " cmap='inferno'\n",
+ ")\n",
+ "\n",
+ "plt.xlabel('$E_\\mathrm{true} / \\mathrm{GeV}$')\n",
+ "plt.ylabel('$\\mu$')\n",
+ "plt.yscale('log')\n",
+ "plt.xscale('log')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "medical-dominican",
+ "metadata": {},
+ "source": [
+ "## Export to GADF FITS files\n",
+ "\n",
+ "We use the classes and methods from `astropy.io.fits` and `pyirf.io.gadf` to write files following the GADF \n",
+ "specification, which can be found here:\n",
+ "\n",
+ "https://gamma-astro-data-formats.readthedocs.io/en/latest/"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "twenty-equity",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from pyirf.io.gadf import create_aeff2d_hdu, create_energy_dispersion_hdu, create_psf_table_hdu\n",
+ "from astropy.io import fits\n",
+ "from astropy.time import Time\n",
+ "from pyirf import __version__\n",
+ "\n",
+ "# set some common meta data for all hdus\n",
+ "meta = dict(\n",
+ " CREATOR='pyirf-v' + __version__,\n",
+ " TELESCOP='FACT',\n",
+ " INSTRUME='FACT',\n",
+ " DATE=Time.now().iso,\n",
+ ")\n",
+ "\n",
+ "hdus = []\n",
+ "\n",
+ "# every fits file has to have an Image HDU as first HDU.\n",
+ "# GADF only uses Binary Table HDUs, so we need to add an empty HDU in front\n",
+ "hdus.append(fits.PrimaryHDU(header=fits.Header(meta)))\n",
+ "\n",
+ "hdus.append(create_aeff2d_hdu(aeff_selected, true_energy_bins, fov_offset_bins, **meta))\n",
+ "hdus.append(create_energy_dispersion_hdu(edisp, true_energy_bins, migration_bins, fov_offset_bins, **meta))\n",
+ "hdus.append(create_psf_table_hdu(psf, true_energy_bins, source_offset_bins, fov_offset_bins, **meta))\n",
+ "\n",
+ "fits.HDUList(hdus).writeto('fact_irf.fits.gz', overwrite=True)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/_sources/notebooks/index.rst.txt b/_sources/notebooks/index.rst.txt
new file mode 100644
index 000000000..cf07b82d5
--- /dev/null
+++ b/_sources/notebooks/index.rst.txt
@@ -0,0 +1,10 @@
+.. _notebooks:
+
+=================
+Example Notebooks
+=================
+
+.. toctree::
+ :maxdepth: 1
+
+ fact_example
diff --git a/_sources/sensitivity.rst.txt b/_sources/sensitivity.rst.txt
new file mode 100644
index 000000000..d1575ff57
--- /dev/null
+++ b/_sources/sensitivity.rst.txt
@@ -0,0 +1,11 @@
+.. _sensitivity:
+
+Sensitivity
+===========
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.sensitivity
+ :no-inheritance-diagram:
diff --git a/_sources/simulation.rst.txt b/_sources/simulation.rst.txt
new file mode 100644
index 000000000..70496415e
--- /dev/null
+++ b/_sources/simulation.rst.txt
@@ -0,0 +1,11 @@
+.. _simulation:
+
+Simulation Information
+======================
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.simulations
+ :no-inheritance-diagram:
diff --git a/_sources/spectral.rst.txt b/_sources/spectral.rst.txt
new file mode 100644
index 000000000..1b8189382
--- /dev/null
+++ b/_sources/spectral.rst.txt
@@ -0,0 +1,13 @@
+.. _spectral:
+
+Event Weighting and Spectrum Definitions
+========================================
+
+
+Reference/API
+-------------
+
+
+.. automodapi:: pyirf.spectral
+ :no-inheritance-diagram:
+ :include-all-objects:
diff --git a/_sources/statistics.rst.txt b/_sources/statistics.rst.txt
new file mode 100644
index 000000000..c527ff3f8
--- /dev/null
+++ b/_sources/statistics.rst.txt
@@ -0,0 +1,11 @@
+.. _statistics:
+
+Statistics
+==========
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.statistics
+ :no-inheritance-diagram:
diff --git a/_sources/utils.rst.txt b/_sources/utils.rst.txt
new file mode 100644
index 000000000..f0b6b6cb8
--- /dev/null
+++ b/_sources/utils.rst.txt
@@ -0,0 +1,11 @@
+.. _utils:
+
+Utility functions
+=================
+
+
+Reference/API
+-------------
+
+.. automodapi:: pyirf.utils
+ :no-inheritance-diagram:
diff --git a/_static/_sphinx_javascript_frameworks_compat.js b/_static/_sphinx_javascript_frameworks_compat.js
new file mode 100644
index 000000000..81415803e
--- /dev/null
+++ b/_static/_sphinx_javascript_frameworks_compat.js
@@ -0,0 +1,123 @@
+/* Compatability shim for jQuery and underscores.js.
+ *
+ * Copyright Sphinx contributors
+ * Released under the two clause BSD licence
+ */
+
+/**
+ * small helper function to urldecode strings
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
+ */
+jQuery.urldecode = function(x) {
+ if (!x) {
+ return x
+ }
+ return decodeURIComponent(x.replace(/\+/g, ' '));
+};
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+ if (typeof s === 'undefined')
+ s = document.location.search;
+ var parts = s.substr(s.indexOf('?') + 1).split('&');
+ var result = {};
+ for (var i = 0; i < parts.length; i++) {
+ var tmp = parts[i].split('=', 2);
+ var key = jQuery.urldecode(tmp[0]);
+ var value = jQuery.urldecode(tmp[1]);
+ if (key in result)
+ result[key].push(value);
+ else
+ result[key] = [value];
+ }
+ return result;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+ function highlight(node, addItems) {
+ if (node.nodeType === 3) {
+ var val = node.nodeValue;
+ var pos = val.toLowerCase().indexOf(text);
+ if (pos >= 0 &&
+ !jQuery(node.parentNode).hasClass(className) &&
+ !jQuery(node.parentNode).hasClass("nohighlight")) {
+ var span;
+ var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
+ if (isInSVG) {
+ span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+ } else {
+ span = document.createElement("span");
+ span.className = className;
+ }
+ span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+ node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+ document.createTextNode(val.substr(pos + text.length)),
+ node.nextSibling));
+ node.nodeValue = val.substr(0, pos);
+ if (isInSVG) {
+ var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ var bbox = node.parentElement.getBBox();
+ rect.x.baseVal.value = bbox.x;
+ rect.y.baseVal.value = bbox.y;
+ rect.width.baseVal.value = bbox.width;
+ rect.height.baseVal.value = bbox.height;
+ rect.setAttribute('class', className);
+ addItems.push({
+ "parent": node.parentNode,
+ "target": rect});
+ }
+ }
+ }
+ else if (!jQuery(node).is("button, select, textarea")) {
+ jQuery.each(node.childNodes, function() {
+ highlight(this, addItems);
+ });
+ }
+ }
+ var addItems = [];
+ var result = this.each(function() {
+ highlight(this, addItems);
+ });
+ for (var i = 0; i < addItems.length; ++i) {
+ jQuery(addItems[i].parent).before(addItems[i].target);
+ }
+ return result;
+};
+
+/*
+ * backward compatibility for jQuery.browser
+ * This will be supported until firefox bug is fixed.
+ */
+if (!jQuery.browser) {
+ jQuery.uaMatch = function(ua) {
+ ua = ua.toLowerCase();
+
+ var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
+ /(webkit)[ \/]([\w.]+)/.exec(ua) ||
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
+ /(msie) ([\w.]+)/.exec(ua) ||
+ ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
+ [];
+
+ return {
+ browser: match[ 1 ] || "",
+ version: match[ 2 ] || "0"
+ };
+ };
+ jQuery.browser = {};
+ jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
+}
diff --git a/_static/basic.css b/_static/basic.css
new file mode 100644
index 000000000..f316efcb4
--- /dev/null
+++ b/_static/basic.css
@@ -0,0 +1,925 @@
+/*
+ * basic.css
+ * ~~~~~~~~~
+ *
+ * Sphinx stylesheet -- basic theme.
+ *
+ * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+ clear: both;
+}
+
+div.section::after {
+ display: block;
+ content: '';
+ clear: left;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+ width: 100%;
+ font-size: 90%;
+}
+
+div.related h3 {
+ display: none;
+}
+
+div.related ul {
+ margin: 0;
+ padding: 0 0 0 10px;
+ list-style: none;
+}
+
+div.related li {
+ display: inline;
+}
+
+div.related li.right {
+ float: right;
+ margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+ padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+ float: left;
+ width: 230px;
+ margin-left: -100%;
+ font-size: 90%;
+ word-wrap: break-word;
+ overflow-wrap : break-word;
+}
+
+div.sphinxsidebar ul {
+ list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+ margin-left: 20px;
+ list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+ margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #98dbcc;
+ font-family: sans-serif;
+ font-size: 1em;
+}
+
+div.sphinxsidebar #searchbox form.search {
+ overflow: hidden;
+}
+
+div.sphinxsidebar #searchbox input[type="text"] {
+ float: left;
+ width: 80%;
+ padding: 0.25em;
+ box-sizing: border-box;
+}
+
+div.sphinxsidebar #searchbox input[type="submit"] {
+ float: left;
+ width: 20%;
+ border-left: none;
+ padding: 0.25em;
+ box-sizing: border-box;
+}
+
+
+img {
+ border: 0;
+ max-width: 100%;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+ margin: 10px 0 0 20px;
+ padding: 0;
+}
+
+ul.search li {
+ padding: 5px 0 5px 20px;
+ background-image: url(file.png);
+ background-repeat: no-repeat;
+ background-position: 0 7px;
+}
+
+ul.search li a {
+ font-weight: bold;
+}
+
+ul.search li p.context {
+ color: #888;
+ margin: 2px 0 0 30px;
+ text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+ font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+ width: 90%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.contentstable p.biglink {
+ line-height: 150%;
+}
+
+a.biglink {
+ font-size: 1.3em;
+}
+
+span.linkdescr {
+ font-style: italic;
+ padding-top: 5px;
+ font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable {
+ width: 100%;
+}
+
+table.indextable td {
+ text-align: left;
+ vertical-align: top;
+}
+
+table.indextable ul {
+ margin-top: 0;
+ margin-bottom: 0;
+ list-style-type: none;
+}
+
+table.indextable > tbody > tr > td > ul {
+ padding-left: 0em;
+}
+
+table.indextable tr.pcap {
+ height: 10px;
+}
+
+table.indextable tr.cap {
+ margin-top: 10px;
+ background-color: #f2f2f2;
+}
+
+img.toggler {
+ margin-right: 3px;
+ margin-top: 3px;
+ cursor: pointer;
+}
+
+div.modindex-jumpbox {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 1em 0 1em 0;
+ padding: 0.4em;
+}
+
+div.genindex-jumpbox {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 1em 0 1em 0;
+ padding: 0.4em;
+}
+
+/* -- domain module index --------------------------------------------------- */
+
+table.modindextable td {
+ padding: 2px;
+ border-collapse: collapse;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+div.body {
+ min-width: 360px;
+ max-width: 800px;
+}
+
+div.body p, div.body dd, div.body li, div.body blockquote {
+ -moz-hyphens: auto;
+ -ms-hyphens: auto;
+ -webkit-hyphens: auto;
+ hyphens: auto;
+}
+
+a.headerlink {
+ visibility: hidden;
+}
+
+a:visited {
+ color: #551A8B;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink,
+caption:hover > a.headerlink,
+p.caption:hover > a.headerlink,
+div.code-block-caption:hover > a.headerlink {
+ visibility: visible;
+}
+
+div.body p.caption {
+ text-align: inherit;
+}
+
+div.body td {
+ text-align: left;
+}
+
+.first {
+ margin-top: 0 !important;
+}
+
+p.rubric {
+ margin-top: 30px;
+ font-weight: bold;
+}
+
+img.align-left, figure.align-left, .figure.align-left, object.align-left {
+ clear: left;
+ float: left;
+ margin-right: 1em;
+}
+
+img.align-right, figure.align-right, .figure.align-right, object.align-right {
+ clear: right;
+ float: right;
+ margin-left: 1em;
+}
+
+img.align-center, figure.align-center, .figure.align-center, object.align-center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+img.align-default, figure.align-default, .figure.align-default {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.align-left {
+ text-align: left;
+}
+
+.align-center {
+ text-align: center;
+}
+
+.align-default {
+ text-align: center;
+}
+
+.align-right {
+ text-align: right;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar,
+aside.sidebar {
+ margin: 0 0 0.5em 1em;
+ border: 1px solid #ddb;
+ padding: 7px;
+ background-color: #ffe;
+ width: 40%;
+ float: right;
+ clear: right;
+ overflow-x: auto;
+}
+
+p.sidebar-title {
+ font-weight: bold;
+}
+
+nav.contents,
+aside.topic,
+div.admonition, div.topic, blockquote {
+ clear: left;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+nav.contents,
+aside.topic,
+div.topic {
+ border: 1px solid #ccc;
+ padding: 7px;
+ margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+ font-size: 1.1em;
+ font-weight: bold;
+ margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ padding: 7px;
+}
+
+div.admonition dt {
+ font-weight: bold;
+}
+
+p.admonition-title {
+ margin: 0px 10px 5px 0px;
+ font-weight: bold;
+}
+
+div.body p.centered {
+ text-align: center;
+ margin-top: 25px;
+}
+
+/* -- content of sidebars/topics/admonitions -------------------------------- */
+
+div.sidebar > :last-child,
+aside.sidebar > :last-child,
+nav.contents > :last-child,
+aside.topic > :last-child,
+div.topic > :last-child,
+div.admonition > :last-child {
+ margin-bottom: 0;
+}
+
+div.sidebar::after,
+aside.sidebar::after,
+nav.contents::after,
+aside.topic::after,
+div.topic::after,
+div.admonition::after,
+blockquote::after {
+ display: block;
+ content: '';
+ clear: both;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border: 0;
+ border-collapse: collapse;
+}
+
+table.align-center {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.align-default {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table caption span.caption-number {
+ font-style: italic;
+}
+
+table caption span.caption-text {
+}
+
+table.docutils td, table.docutils th {
+ padding: 1px 8px 1px 5px;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 1px solid #aaa;
+}
+
+th {
+ text-align: left;
+ padding-right: 5px;
+}
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px;
+}
+
+table.citation td {
+ border-bottom: none;
+}
+
+th > :first-child,
+td > :first-child {
+ margin-top: 0px;
+}
+
+th > :last-child,
+td > :last-child {
+ margin-bottom: 0px;
+}
+
+/* -- figures --------------------------------------------------------------- */
+
+div.figure, figure {
+ margin: 0.5em;
+ padding: 0.5em;
+}
+
+div.figure p.caption, figcaption {
+ padding: 0.3em;
+}
+
+div.figure p.caption span.caption-number,
+figcaption span.caption-number {
+ font-style: italic;
+}
+
+div.figure p.caption span.caption-text,
+figcaption span.caption-text {
+}
+
+/* -- field list styles ----------------------------------------------------- */
+
+table.field-list td, table.field-list th {
+ border: 0 !important;
+}
+
+.field-list ul {
+ margin: 0;
+ padding-left: 1em;
+}
+
+.field-list p {
+ margin: 0;
+}
+
+.field-name {
+ -moz-hyphens: manual;
+ -ms-hyphens: manual;
+ -webkit-hyphens: manual;
+ hyphens: manual;
+}
+
+/* -- hlist styles ---------------------------------------------------------- */
+
+table.hlist {
+ margin: 1em 0;
+}
+
+table.hlist td {
+ vertical-align: top;
+}
+
+/* -- object description styles --------------------------------------------- */
+
+.sig {
+ font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
+}
+
+.sig-name, code.descname {
+ background-color: transparent;
+ font-weight: bold;
+}
+
+.sig-name {
+ font-size: 1.1em;
+}
+
+code.descname {
+ font-size: 1.2em;
+}
+
+.sig-prename, code.descclassname {
+ background-color: transparent;
+}
+
+.optional {
+ font-size: 1.3em;
+}
+
+.sig-paren {
+ font-size: larger;
+}
+
+.sig-param.n {
+ font-style: italic;
+}
+
+/* C++ specific styling */
+
+.sig-inline.c-texpr,
+.sig-inline.cpp-texpr {
+ font-family: unset;
+}
+
+.sig.c .k, .sig.c .kt,
+.sig.cpp .k, .sig.cpp .kt {
+ color: #0033B3;
+}
+
+.sig.c .m,
+.sig.cpp .m {
+ color: #1750EB;
+}
+
+.sig.c .s, .sig.c .sc,
+.sig.cpp .s, .sig.cpp .sc {
+ color: #067D17;
+}
+
+
+/* -- other body styles ----------------------------------------------------- */
+
+ol.arabic {
+ list-style: decimal;
+}
+
+ol.loweralpha {
+ list-style: lower-alpha;
+}
+
+ol.upperalpha {
+ list-style: upper-alpha;
+}
+
+ol.lowerroman {
+ list-style: lower-roman;
+}
+
+ol.upperroman {
+ list-style: upper-roman;
+}
+
+:not(li) > ol > li:first-child > :first-child,
+:not(li) > ul > li:first-child > :first-child {
+ margin-top: 0px;
+}
+
+:not(li) > ol > li:last-child > :last-child,
+:not(li) > ul > li:last-child > :last-child {
+ margin-bottom: 0px;
+}
+
+ol.simple ol p,
+ol.simple ul p,
+ul.simple ol p,
+ul.simple ul p {
+ margin-top: 0;
+}
+
+ol.simple > li:not(:first-child) > p,
+ul.simple > li:not(:first-child) > p {
+ margin-top: 0;
+}
+
+ol.simple p,
+ul.simple p {
+ margin-bottom: 0;
+}
+
+aside.footnote > span,
+div.citation > span {
+ float: left;
+}
+aside.footnote > span:last-of-type,
+div.citation > span:last-of-type {
+ padding-right: 0.5em;
+}
+aside.footnote > p {
+ margin-left: 2em;
+}
+div.citation > p {
+ margin-left: 4em;
+}
+aside.footnote > p:last-of-type,
+div.citation > p:last-of-type {
+ margin-bottom: 0em;
+}
+aside.footnote > p:last-of-type:after,
+div.citation > p:last-of-type:after {
+ content: "";
+ clear: both;
+}
+
+dl.field-list {
+ display: grid;
+ grid-template-columns: fit-content(30%) auto;
+}
+
+dl.field-list > dt {
+ font-weight: bold;
+ word-break: break-word;
+ padding-left: 0.5em;
+ padding-right: 5px;
+}
+
+dl.field-list > dd {
+ padding-left: 0.5em;
+ margin-top: 0em;
+ margin-left: 0em;
+ margin-bottom: 0em;
+}
+
+dl {
+ margin-bottom: 15px;
+}
+
+dd > :first-child {
+ margin-top: 0px;
+}
+
+dd ul, dd table {
+ margin-bottom: 10px;
+}
+
+dd {
+ margin-top: 3px;
+ margin-bottom: 10px;
+ margin-left: 30px;
+}
+
+.sig dd {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+.sig dl {
+ margin-top: 0px;
+ margin-bottom: 0px;
+}
+
+dl > dd:last-child,
+dl > dd:last-child > :last-child {
+ margin-bottom: 0;
+}
+
+dt:target, span.highlighted {
+ background-color: #fbe54e;
+}
+
+rect.highlighted {
+ fill: #fbe54e;
+}
+
+dl.glossary dt {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+.versionmodified {
+ font-style: italic;
+}
+
+.system-message {
+ background-color: #fda;
+ padding: 5px;
+ border: 3px solid red;
+}
+
+.footnote:target {
+ background-color: #ffa;
+}
+
+.line-block {
+ display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+.line-block .line-block {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-left: 1.5em;
+}
+
+.guilabel, .menuselection {
+ font-family: sans-serif;
+}
+
+.accelerator {
+ text-decoration: underline;
+}
+
+.classifier {
+ font-style: oblique;
+}
+
+.classifier:before {
+ font-style: normal;
+ margin: 0 0.5em;
+ content: ":";
+ display: inline-block;
+}
+
+abbr, acronym {
+ border-bottom: dotted 1px;
+ cursor: help;
+}
+
+.translated {
+ background-color: rgba(207, 255, 207, 0.2)
+}
+
+.untranslated {
+ background-color: rgba(255, 207, 207, 0.2)
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+ overflow: auto;
+ overflow-y: hidden; /* fixes display issues on Chrome browsers */
+}
+
+pre, div[class*="highlight-"] {
+ clear: both;
+}
+
+span.pre {
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ -webkit-hyphens: none;
+ hyphens: none;
+ white-space: nowrap;
+}
+
+div[class*="highlight-"] {
+ margin: 1em 0;
+}
+
+td.linenos pre {
+ border: 0;
+ background-color: transparent;
+ color: #aaa;
+}
+
+table.highlighttable {
+ display: block;
+}
+
+table.highlighttable tbody {
+ display: block;
+}
+
+table.highlighttable tr {
+ display: flex;
+}
+
+table.highlighttable td {
+ margin: 0;
+ padding: 0;
+}
+
+table.highlighttable td.linenos {
+ padding-right: 0.5em;
+}
+
+table.highlighttable td.code {
+ flex: 1;
+ overflow: hidden;
+}
+
+.highlight .hll {
+ display: block;
+}
+
+div.highlight pre,
+table.highlighttable pre {
+ margin: 0;
+}
+
+div.code-block-caption + div {
+ margin-top: 0;
+}
+
+div.code-block-caption {
+ margin-top: 1em;
+ padding: 2px 5px;
+ font-size: small;
+}
+
+div.code-block-caption code {
+ background-color: transparent;
+}
+
+table.highlighttable td.linenos,
+span.linenos,
+div.highlight span.gp { /* gp: Generic.Prompt */
+ user-select: none;
+ -webkit-user-select: text; /* Safari fallback only */
+ -webkit-user-select: none; /* Chrome/Safari */
+ -moz-user-select: none; /* Firefox */
+ -ms-user-select: none; /* IE10+ */
+}
+
+div.code-block-caption span.caption-number {
+ padding: 0.1em 0.3em;
+ font-style: italic;
+}
+
+div.code-block-caption span.caption-text {
+}
+
+div.literal-block-wrapper {
+ margin: 1em 0;
+}
+
+code.xref, a code {
+ background-color: transparent;
+ font-weight: bold;
+}
+
+h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+ background-color: transparent;
+}
+
+.viewcode-link {
+ float: right;
+}
+
+.viewcode-back {
+ float: right;
+ font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+ margin: -1px -10px;
+ padding: 0 10px;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+ vertical-align: middle;
+}
+
+div.body div.math p {
+ text-align: center;
+}
+
+span.eqno {
+ float: right;
+}
+
+span.eqno a.headerlink {
+ position: absolute;
+ z-index: 1;
+}
+
+div.math:hover a.headerlink {
+ visibility: visible;
+}
+
+/* -- printout stylesheet --------------------------------------------------- */
+
+@media print {
+ div.document,
+ div.documentwrapper,
+ div.bodywrapper {
+ margin: 0 !important;
+ width: 100%;
+ }
+
+ div.sphinxsidebar,
+ div.related,
+ div.footer,
+ #top-link {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/_static/css/badge_only.css b/_static/css/badge_only.css
new file mode 100644
index 000000000..88ba55b96
--- /dev/null
+++ b/_static/css/badge_only.css
@@ -0,0 +1 @@
+.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}
\ No newline at end of file
diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff b/_static/css/fonts/Roboto-Slab-Bold.woff
new file mode 100644
index 000000000..6cb600001
Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff differ
diff --git a/_static/css/fonts/Roboto-Slab-Bold.woff2 b/_static/css/fonts/Roboto-Slab-Bold.woff2
new file mode 100644
index 000000000..7059e2314
Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Bold.woff2 differ
diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff b/_static/css/fonts/Roboto-Slab-Regular.woff
new file mode 100644
index 000000000..f815f63f9
Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff differ
diff --git a/_static/css/fonts/Roboto-Slab-Regular.woff2 b/_static/css/fonts/Roboto-Slab-Regular.woff2
new file mode 100644
index 000000000..f2c76e5bd
Binary files /dev/null and b/_static/css/fonts/Roboto-Slab-Regular.woff2 differ
diff --git a/_static/css/fonts/fontawesome-webfont.eot b/_static/css/fonts/fontawesome-webfont.eot
new file mode 100644
index 000000000..e9f60ca95
Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.eot differ
diff --git a/_static/css/fonts/fontawesome-webfont.svg b/_static/css/fonts/fontawesome-webfont.svg
new file mode 100644
index 000000000..855c845e5
--- /dev/null
+++ b/_static/css/fonts/fontawesome-webfont.svg
@@ -0,0 +1,2671 @@
+
+
+
diff --git a/_static/css/fonts/fontawesome-webfont.ttf b/_static/css/fonts/fontawesome-webfont.ttf
new file mode 100644
index 000000000..35acda2fa
Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.ttf differ
diff --git a/_static/css/fonts/fontawesome-webfont.woff b/_static/css/fonts/fontawesome-webfont.woff
new file mode 100644
index 000000000..400014a4b
Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff differ
diff --git a/_static/css/fonts/fontawesome-webfont.woff2 b/_static/css/fonts/fontawesome-webfont.woff2
new file mode 100644
index 000000000..4d13fc604
Binary files /dev/null and b/_static/css/fonts/fontawesome-webfont.woff2 differ
diff --git a/_static/css/fonts/lato-bold-italic.woff b/_static/css/fonts/lato-bold-italic.woff
new file mode 100644
index 000000000..88ad05b9f
Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff differ
diff --git a/_static/css/fonts/lato-bold-italic.woff2 b/_static/css/fonts/lato-bold-italic.woff2
new file mode 100644
index 000000000..c4e3d804b
Binary files /dev/null and b/_static/css/fonts/lato-bold-italic.woff2 differ
diff --git a/_static/css/fonts/lato-bold.woff b/_static/css/fonts/lato-bold.woff
new file mode 100644
index 000000000..c6dff51f0
Binary files /dev/null and b/_static/css/fonts/lato-bold.woff differ
diff --git a/_static/css/fonts/lato-bold.woff2 b/_static/css/fonts/lato-bold.woff2
new file mode 100644
index 000000000..bb195043c
Binary files /dev/null and b/_static/css/fonts/lato-bold.woff2 differ
diff --git a/_static/css/fonts/lato-normal-italic.woff b/_static/css/fonts/lato-normal-italic.woff
new file mode 100644
index 000000000..76114bc03
Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff differ
diff --git a/_static/css/fonts/lato-normal-italic.woff2 b/_static/css/fonts/lato-normal-italic.woff2
new file mode 100644
index 000000000..3404f37e2
Binary files /dev/null and b/_static/css/fonts/lato-normal-italic.woff2 differ
diff --git a/_static/css/fonts/lato-normal.woff b/_static/css/fonts/lato-normal.woff
new file mode 100644
index 000000000..ae1307ff5
Binary files /dev/null and b/_static/css/fonts/lato-normal.woff differ
diff --git a/_static/css/fonts/lato-normal.woff2 b/_static/css/fonts/lato-normal.woff2
new file mode 100644
index 000000000..3bf984332
Binary files /dev/null and b/_static/css/fonts/lato-normal.woff2 differ
diff --git a/_static/css/theme.css b/_static/css/theme.css
new file mode 100644
index 000000000..0f14f1064
--- /dev/null
+++ b/_static/css/theme.css
@@ -0,0 +1,4 @@
+html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*!
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block}
\ No newline at end of file
diff --git a/_static/doctools.js b/_static/doctools.js
new file mode 100644
index 000000000..4d67807d1
--- /dev/null
+++ b/_static/doctools.js
@@ -0,0 +1,156 @@
+/*
+ * doctools.js
+ * ~~~~~~~~~~~
+ *
+ * Base JavaScript utilities for all Sphinx HTML documentation.
+ *
+ * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+"use strict";
+
+const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([
+ "TEXTAREA",
+ "INPUT",
+ "SELECT",
+ "BUTTON",
+]);
+
+const _ready = (callback) => {
+ if (document.readyState !== "loading") {
+ callback();
+ } else {
+ document.addEventListener("DOMContentLoaded", callback);
+ }
+};
+
+/**
+ * Small JavaScript module for the documentation.
+ */
+const Documentation = {
+ init: () => {
+ Documentation.initDomainIndexTable();
+ Documentation.initOnKeyListeners();
+ },
+
+ /**
+ * i18n support
+ */
+ TRANSLATIONS: {},
+ PLURAL_EXPR: (n) => (n === 1 ? 0 : 1),
+ LOCALE: "unknown",
+
+ // gettext and ngettext don't access this so that the functions
+ // can safely bound to a different name (_ = Documentation.gettext)
+ gettext: (string) => {
+ const translated = Documentation.TRANSLATIONS[string];
+ switch (typeof translated) {
+ case "undefined":
+ return string; // no translation
+ case "string":
+ return translated; // translation exists
+ default:
+ return translated[0]; // (singular, plural) translation tuple exists
+ }
+ },
+
+ ngettext: (singular, plural, n) => {
+ const translated = Documentation.TRANSLATIONS[singular];
+ if (typeof translated !== "undefined")
+ return translated[Documentation.PLURAL_EXPR(n)];
+ return n === 1 ? singular : plural;
+ },
+
+ addTranslations: (catalog) => {
+ Object.assign(Documentation.TRANSLATIONS, catalog.messages);
+ Documentation.PLURAL_EXPR = new Function(
+ "n",
+ `return (${catalog.plural_expr})`
+ );
+ Documentation.LOCALE = catalog.locale;
+ },
+
+ /**
+ * helper function to focus on search bar
+ */
+ focusSearchBar: () => {
+ document.querySelectorAll("input[name=q]")[0]?.focus();
+ },
+
+ /**
+ * Initialise the domain index toggle buttons
+ */
+ initDomainIndexTable: () => {
+ const toggler = (el) => {
+ const idNumber = el.id.substr(7);
+ const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`);
+ if (el.src.substr(-9) === "minus.png") {
+ el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`;
+ toggledRows.forEach((el) => (el.style.display = "none"));
+ } else {
+ el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`;
+ toggledRows.forEach((el) => (el.style.display = ""));
+ }
+ };
+
+ const togglerElements = document.querySelectorAll("img.toggler");
+ togglerElements.forEach((el) =>
+ el.addEventListener("click", (event) => toggler(event.currentTarget))
+ );
+ togglerElements.forEach((el) => (el.style.display = ""));
+ if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler);
+ },
+
+ initOnKeyListeners: () => {
+ // only install a listener if it is really needed
+ if (
+ !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
+ !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS
+ )
+ return;
+
+ document.addEventListener("keydown", (event) => {
+ // bail for input elements
+ if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return;
+ // bail with special keys
+ if (event.altKey || event.ctrlKey || event.metaKey) return;
+
+ if (!event.shiftKey) {
+ switch (event.key) {
+ case "ArrowLeft":
+ if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
+
+ const prevLink = document.querySelector('link[rel="prev"]');
+ if (prevLink && prevLink.href) {
+ window.location.href = prevLink.href;
+ event.preventDefault();
+ }
+ break;
+ case "ArrowRight":
+ if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break;
+
+ const nextLink = document.querySelector('link[rel="next"]');
+ if (nextLink && nextLink.href) {
+ window.location.href = nextLink.href;
+ event.preventDefault();
+ }
+ break;
+ }
+ }
+
+ // some keyboard layouts may need Shift to get /
+ switch (event.key) {
+ case "/":
+ if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break;
+ Documentation.focusSearchBar();
+ event.preventDefault();
+ }
+ });
+ },
+};
+
+// quick alias for translations
+const _ = Documentation.gettext;
+
+_ready(Documentation.init);
diff --git a/_static/documentation_options.js b/_static/documentation_options.js
new file mode 100644
index 000000000..7e4c114f2
--- /dev/null
+++ b/_static/documentation_options.js
@@ -0,0 +1,13 @@
+const DOCUMENTATION_OPTIONS = {
+ VERSION: '',
+ LANGUAGE: 'en',
+ COLLAPSE_INDEX: false,
+ BUILDER: 'html',
+ FILE_SUFFIX: '.html',
+ LINK_SUFFIX: '.html',
+ HAS_SOURCE: true,
+ SOURCELINK_SUFFIX: '.txt',
+ NAVIGATION_WITH_KEYS: false,
+ SHOW_SEARCH_SUMMARY: true,
+ ENABLE_SEARCH_SHORTCUTS: true,
+};
\ No newline at end of file
diff --git a/_static/file.png b/_static/file.png
new file mode 100644
index 000000000..a858a410e
Binary files /dev/null and b/_static/file.png differ
diff --git a/_static/fonts/Lato/lato-bold.eot b/_static/fonts/Lato/lato-bold.eot
new file mode 100644
index 000000000..3361183a4
Binary files /dev/null and b/_static/fonts/Lato/lato-bold.eot differ
diff --git a/_static/fonts/Lato/lato-bold.ttf b/_static/fonts/Lato/lato-bold.ttf
new file mode 100644
index 000000000..29f691d5e
Binary files /dev/null and b/_static/fonts/Lato/lato-bold.ttf differ
diff --git a/_static/fonts/Lato/lato-bold.woff b/_static/fonts/Lato/lato-bold.woff
new file mode 100644
index 000000000..c6dff51f0
Binary files /dev/null and b/_static/fonts/Lato/lato-bold.woff differ
diff --git a/_static/fonts/Lato/lato-bold.woff2 b/_static/fonts/Lato/lato-bold.woff2
new file mode 100644
index 000000000..bb195043c
Binary files /dev/null and b/_static/fonts/Lato/lato-bold.woff2 differ
diff --git a/_static/fonts/Lato/lato-bolditalic.eot b/_static/fonts/Lato/lato-bolditalic.eot
new file mode 100644
index 000000000..3d4154936
Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.eot differ
diff --git a/_static/fonts/Lato/lato-bolditalic.ttf b/_static/fonts/Lato/lato-bolditalic.ttf
new file mode 100644
index 000000000..f402040b3
Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.ttf differ
diff --git a/_static/fonts/Lato/lato-bolditalic.woff b/_static/fonts/Lato/lato-bolditalic.woff
new file mode 100644
index 000000000..88ad05b9f
Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.woff differ
diff --git a/_static/fonts/Lato/lato-bolditalic.woff2 b/_static/fonts/Lato/lato-bolditalic.woff2
new file mode 100644
index 000000000..c4e3d804b
Binary files /dev/null and b/_static/fonts/Lato/lato-bolditalic.woff2 differ
diff --git a/_static/fonts/Lato/lato-italic.eot b/_static/fonts/Lato/lato-italic.eot
new file mode 100644
index 000000000..3f826421a
Binary files /dev/null and b/_static/fonts/Lato/lato-italic.eot differ
diff --git a/_static/fonts/Lato/lato-italic.ttf b/_static/fonts/Lato/lato-italic.ttf
new file mode 100644
index 000000000..b4bfc9b24
Binary files /dev/null and b/_static/fonts/Lato/lato-italic.ttf differ
diff --git a/_static/fonts/Lato/lato-italic.woff b/_static/fonts/Lato/lato-italic.woff
new file mode 100644
index 000000000..76114bc03
Binary files /dev/null and b/_static/fonts/Lato/lato-italic.woff differ
diff --git a/_static/fonts/Lato/lato-italic.woff2 b/_static/fonts/Lato/lato-italic.woff2
new file mode 100644
index 000000000..3404f37e2
Binary files /dev/null and b/_static/fonts/Lato/lato-italic.woff2 differ
diff --git a/_static/fonts/Lato/lato-regular.eot b/_static/fonts/Lato/lato-regular.eot
new file mode 100644
index 000000000..11e3f2a5f
Binary files /dev/null and b/_static/fonts/Lato/lato-regular.eot differ
diff --git a/_static/fonts/Lato/lato-regular.ttf b/_static/fonts/Lato/lato-regular.ttf
new file mode 100644
index 000000000..74decd9eb
Binary files /dev/null and b/_static/fonts/Lato/lato-regular.ttf differ
diff --git a/_static/fonts/Lato/lato-regular.woff b/_static/fonts/Lato/lato-regular.woff
new file mode 100644
index 000000000..ae1307ff5
Binary files /dev/null and b/_static/fonts/Lato/lato-regular.woff differ
diff --git a/_static/fonts/Lato/lato-regular.woff2 b/_static/fonts/Lato/lato-regular.woff2
new file mode 100644
index 000000000..3bf984332
Binary files /dev/null and b/_static/fonts/Lato/lato-regular.woff2 differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot
new file mode 100644
index 000000000..79dc8efed
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf
new file mode 100644
index 000000000..df5d1df27
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff
new file mode 100644
index 000000000..6cb600001
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2
new file mode 100644
index 000000000..7059e2314
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot
new file mode 100644
index 000000000..2f7ca78a1
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf
new file mode 100644
index 000000000..eb52a7907
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff
new file mode 100644
index 000000000..f815f63f9
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ
diff --git a/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2
new file mode 100644
index 000000000..f2c76e5bd
Binary files /dev/null and b/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ
diff --git a/_static/graphviz.css b/_static/graphviz.css
new file mode 100644
index 000000000..027576e34
--- /dev/null
+++ b/_static/graphviz.css
@@ -0,0 +1,19 @@
+/*
+ * graphviz.css
+ * ~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- graphviz extension.
+ *
+ * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+img.graphviz {
+ border: 0;
+ max-width: 100%;
+}
+
+object.graphviz {
+ max-width: 100%;
+}
diff --git a/_static/jquery.js b/_static/jquery.js
new file mode 100644
index 000000000..c4c6022f2
--- /dev/null
+++ b/_static/jquery.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"
Calculate bin indices of inidividula entries of the given data array using
+the supplied binning. Underflow will be indicated by UNDERFLOW_INDEX and
+overflow by OVERFLOW_INDEX.
+
If the bins already include underflow / overflow bins, e.g.
+bins[0] = -np.inf and bins[-1] = np.inf, using the result of this
+function will always be a valid index into the resulting histogram.
+
+
Parameters:
+
+
data: ``~np.ndarray`` or ``~astropy.units.Quantity``
Array with the data
+
+
bins: ``~np.ndarray`` or ``~astropy.units.Quantity``
Array or Quantity of bin edges. Must have the same unit as data if a Quantity.
+
+
+
+
Returns:
+
+
bin_index: np.ndarray[int]
Indices of the histogram bin the values in data belong to.
+Under- and overflown values will have values of UNDERFLOW_INDEX
+and OVERFLOW_INDEX respectively.
+
+
valid: np.ndarray[bool]
Boolean mask indicating if a given value belongs into one of the defined bins.
+False indicates that an entry fell into the over- or underflow bins.
Function joins bins into lo and hi part,
+e.g. [0, 1, 2] and [1, 2, 4] into [0, 1, 2, 4]
+It works on multidimentional arrays as long as the binning is in the last axis
Rebinning of a histogram by interpolation along a given axis.
+
+
Parameters:
+
+
datanumpy.ndarray or astropy.units.Quantity
Histogram.
+
+
old_edgesnumpy.array or astropy.units.Quantity
Binning used to calculate data.
+len(old_edges)-1 needs to equal the length of data
+along interpolation axis (axis).
+If quantity, needs to be compatible to new_edges.
+
+
new_edgesnumpy.array or astropy.units.Quantity
Binning of new histogram.
+If quantity, needs to be compatible to old_edges.
+
+
axisint
Interpolation axis.
+
+
+
+
Returns:
+
+
numpy.ndarray or astropy.units.Quantity
Interpolated histogram with dimension according to data and new_edges.
+If data is a quantity, this has the same unit.
Optimize the gh-score cut in every energy bin of reconstructed energy
+for best sensitivity.
+
This procedure is EventDisplay-like, since it only applies a
+pre-computed theta cut and then optimizes only the gamma/hadron separation
+cut.
+
+
Parameters:
+
+
signal: astropy.table.QTable
event list of simulated signal events.
+Required columns are theta, reco_energy, ‘weight’, gh_score
+No directional (theta) or gamma/hadron cut should already be applied.
+
+
background: astropy.table.QTable
event list of simulated background events.
+Required columns are reco_source_fov_offset, reco_energy,
+‘weight’, gh_score.
+No directional (theta) or gamma/hadron cut should already be applied.
+
+
reco_energy_bins: astropy.units.Quantity[energy]
Bins in reconstructed energy to use for sensitivity computation
+
+
gh_cut_efficiencies: np.ndarray[float, ndim=1]
The cut efficiencies to scan for best sensitivity.
+
+
theta_cuts: astropy.table.QTable
cut definition of the energy dependent theta cut,
+e.g. as created by calculate_percentile_cut
+
+
op: comparison function with signature f(a, b) -> bool
The comparison function to use for the gamma hadron score.
+Returning true means an event passes the cut, so is not discarded.
+E.g. for gammaness-like score, use operator.ge (>=) and for a
+hadroness-like score use operator.le (<=).
+
+
fov_offset_min: astropy.units.Quantity[angle]
Minimum distance from the fov center for background events to be taken into account
+
+
fov_offset_max: astropy.units.Quantity[angle]
Maximum distance from the fov center for background events to be taken into account
+
+
alpha: float
Size ratio of off region / on region. Will be used to
+scale the background rate.
+
+
progress: bool
If True, show a progress bar during cut optimization
Evaluate a binned cut as defined in cut_table on given events.
+
Events with bin_values outside the bin edges defined in cut table
+will be set to False.
+
+
Parameters:
+
+
values: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
The values on which the cut should be evaluated
+
+
bin_values: ``~numpy.ndarray`` or ``~astropy.units.Quantity``
The values used to sort the values into bins
+
+
cut_table: ``~astropy.table.Table``
A table describing the binned cuts, e.g. as created by
+~pyirf.cuts.calculate_percentile_cut.
+Required columns:
+- low: lower edges of the bins
+- high: upper edges of the bins,
+- cut: cut value
+
+
op: callable(a, b) -> bool
A function taking two arguments, comparing element-wise and
+returning an array of booleans.
+Must support vectorized application.
+
+
+
+
Returns:
+
+
result: np.ndarray[bool]
A mask for each entry in values indicating if the event
+passes the bin specific cut given in cut table.
Base class for all Estimators working on specific IRF components.
+
While usable, it is encouraged to use the actual class for the respective IRF
+component as it ensures further checks and if necessary e.g. unit handling.
Base class for all Estimators working on IRF components that represent discretized PDFs.
+
While usable, it is encouraged to use the actual class for the respective IRF
+component as it ensures further checks and if necessary e.g. unit handling.
Estimating effective area at target_point, inter-/extrapolates as needed and
+specified in __init__.
+
+
Parameters:
+
+
target_point: np.ndarray, shape=(1, n_dims)
Target for inter-/extrapolation
+
+
+
+
Returns:
+
+
aeff_interp: np.ndarray of (astropy.units.m)**2, shape=(n_points, …)
Interpolated Effective area array with same shape as input
+effective areas. For AEFF2D of shape (n_energy_bins, n_fov_offset_bins).
+Values lower or equal to __init__’s min_effective_area are set
+to zero.
Takes a grid of binned pdfs for a bunch of different parameters
+and interpolates it to given value of those parameters.
+This function calls implementations of the moment morphing interpolation
+pocedure introduced in [1].
+
+
Parameters:
+
+
target_point: numpy.ndarray, shape=(1, n_dims)
Value for which the interpolation is performed (target point)
+
+
+
+
Returns:
+
+
f_new: numpy.ndarray, shape=(1,…,n_bins)
Interpolated and binned pdf
+
+
+
+
+
References
+
+
+[1]
+
M. Baak, S. Gadatsch, R. Harrington and W. Verkerke (2015). Interpolation between
+multi-dimensional histograms using a new non-linear moment morphing method
+Nucl. Instrum. Methods Phys. Res. A 771, 39-48. https://doi.org/10.1016/j.nima.2014.10.033
Base class for all Estimators working on IRF components that represent parametrized
+or scalar quantities.
+
While usable, it is encouraged to use the actual class for the respective IRF
+component as it ensures further checks and if necessary e.g. unit handling.
Base class for all extrapolators used with IRF components that can be
+treated independently, e.g. parametrized ones like 3Gauss
+but also AEff. Derived from pyirf.interpolation.BaseExtrapolator
Base class for all interpolators used with IRF components that can be
+independently interpolated, e.g. parametrized ones like 3Gauss
+but also AEff. Derived from pyirf.interpolation.BaseInterpolator
While the ParametrizedNearestSimplexExtrapolator does not result in a smooth
+extrapolation outside of the grid due to using only the nearest available
+simplex, this extrapolator blends over all visible edges as discussed in [1].
+For one grid-dimension this is equal to the ParametrizedNearestSimplexExtrapolator,
+the same holds for grids consisting of only one simplex or constellations,
+where only one simplex is visible from a target.
+
+
Parameters:
+
+
grid_points: np.ndarray, shape=(N, …)
Grid points at which templates exist. May be one ot two dimensional.
+Have to be sorted in accending order for 1D.
+
+
params: np.ndarray, shape=(N, …)
Array of corresponding parameter values at each point in grid_points.
+First dimesion has to correspond to number of grid_points
+
+
m: non-zero int >= 1
Degree of smoothness wanted in the extrapolation region. See [1] for
+additional information. Defaults to 1.
Takes a grid of binned pdfs for a bunch of different parameters
+and interpolates it to given value of those parameters.
+This function provides an adapted version of the quantile interpolation introduced
+in [1].
+Instead of following this method closely, it implements different approaches to the
+steps shown in Fig. 5 of [1].
+
+
Parameters:
+
+
target_point: numpy.ndarray, shape=(1, n_dims)
Value for which the interpolation is performed (target point)
If True, use number of simulated events from histogram provided in fits file,
+if False, estimate this number from the unique run_id, pointing direction
+combinations and the number of events per run in the run header.
+This will fail e.g. for protons with cuts already applied, since many
+runs will have 0 events surviving cuts.
+
+
+
+
Returns:
+
+
events: astropy.QTable
Astropy Table object containing the reconstructed events information.
Calculate energy dispersion for the given DL2 event list.
+Energy dispersion is defined as the probability of finding an event
+at a given relative deviation (reco_energy/true_energy) for a given
+true energy.
+
+
Parameters:
+
+
selected_events: astropy.table.QTable
Table of the DL2 events.
+Required columns: reco_energy, true_energy, true_source_fov_offset.
+
+
true_energy_bins: astropy.units.Quantity[energy]
Bin edges in true energy
+
+
migration_bins: astropy.units.Quantity[energy]
Bin edges in relative deviation, recommended range: [0.2, 5]
+
+
fov_offset_bins: astropy.units.Quantity[angle]
Bin edges in the field of view offset.
+For Point-Like IRFs, only giving a single bin is appropriate.
+
+
+
+
Returns:
+
+
energy_dispersion: numpy.ndarray
Energy dispersion matrix
+with shape (n_true_energy_bins, n_migration_bins, n_fov_ofset_bins)
Calculate sensitivity for DL2 event lists in bins of reconstructed energy.
+
Sensitivity is defined as the minimum flux detectable with target_significance
+sigma significance in a certain time.
+
This time must be incorporated into the event weights.
+
Two conditions are required for the sensitivity:
+- At least ten weighted signal events
+- The weighted signal must be larger than 5 % of the weighted background
+- At least 5 sigma (so relative_sensitivity > 1)
+
If the conditions are not met, the sensitivity will be set to nan.
+
+
Parameters:
+
+
signal_hist: astropy.table.QTable
Histogram of detected signal events as a table.
+Required columns: n and n_weighted.
+See pyirf.binning.create_histogram_table
+
+
background_hist: astropy.table.QTable
Histogram of detected events as a table.
+Required columns: n and n_weighted.
+See pyirf.binning.create_histogram_table
+
+
alpha: float
Size ratio of signal region to background region
+
+
min_significance: float
Significance necessary for a detection
+
+
min_signal_events: int
Minimum number of signal events required.
+The relative flux will be scaled up from the one yielding min_significance
+if this condition is violated.
+
+
min_excess_over_background: float
Minimum number of signal events expressed as the proportion of the
+background.
+So the required number of signal events will be
+min_excess_over_background*alpha*n_off.
+The relative flux will be scaled up from the one yielding min_significance
+if this condition is violated.
+
+
significance_function: callable
A function with signature (n_on, n_off, alpha) -> significance.
+Default is the Li & Ma likelihood ratio test.
+
+
+
+
Returns:
+
+
sensitivity_table: astropy.table.QTable
Table with sensitivity information.
+Contains weighted and unweighted number of signal and background events
+and the relative_sensitivity, the scaling applied to the signal events
+that yields target_significance sigma of significance according to
+the significance_function
Estimate the number of background events for a point-like sensitivity.
+
Due to limited statistics, it is often not possible to just apply the same
+theta cut to the background events as to the signal events around an assumed
+source position.
+
Here we calculate the expected number of background events for the off
+regions by taking all background events between fov_offset_min and
+fov_offset_max from the camera center and then scale these to the size
+of the off region, which is scaled by 1 / alpha from the size of the on
+region given by the theta cuts.
+
+
Parameters:
+
+
events: astropy.table.QTable
DL2 event list of background surviving event selection
+and inside fov_offset_max from the center of the FOV
+Required columns for this function:
+- reco_energy,
+- reco_source_fov_offset.
+
+
reco_energy_bins: astropy.units.Quantity[energy]
Desired bin edges in reconstructed energy for the background rate
+
+
theta_cuts: astropy.table.QTable
The cuts table for the theta cut,
+e.g. as returned by ~pyirf.cuts.calculate_percentile_cut.
+Columns center and cut are required for this function.
+
+
alpha: float
size of the on region divided by the size of the off region.
+
+
fov_offset_min: astropy.units.Quantity[angle]
Minimum distance from the fov center for background events to be taken into account
+
+
fov_offset_max: astropy.units.Quantity[angle]
Maximum distance from the fov center for background events to be taken into account
Calculate the relative sensitivity defined as the flux
+relative to the reference source that is detectable with
+significance target_significance.
+
Given measured n_on and n_off,
+we estimate the number of gamma events n_signal as n_on-alpha*n_off.
+
The number of background events n_background`isestimatedas``n_off*alpha.
+
In the end, we find the relative sensitivity as the scaling factor for n_signal
+that yields a significance of target_significance.
+
The reference time should be incorporated by appropriately weighting the events
+before calculating n_on and n_off.
+
All input values with the exception of significance_function
+must be broadcastable to a single, common shape.
+
+
Parameters:
+
+
n_on: int or array-like
Number of signal-like events for the on observations
+
+
n_off: int or array-like
Number of signal-like events for the off observations
+
+
alpha: float or array-like
Scaling factor between on and off observations.
+1 / number of off regions for wobble observations.
+
+
min_significance: float or array-like
Significance necessary for a detection
+
+
min_signal_events: int or array-like
Minimum number of signal events required.
+The relative flux will be scaled up from the one yielding min_significance
+if this condition is violated.
+
+
min_excess_over_background: float or array-like
Minimum number of signal events expressed as the proportion of the
+background.
+So the required number of signal events will be
+min_excess_over_background*alpha*n_off.
+The relative flux will be scaled up from the one yielding min_significance
+if this condition is violated.
+
+
significance_function: function
A function f(n_on, n_off, alpha) -> significance in sigma
+Used to calculate the significance, default is the Li&Ma
+likelihood ratio test formula.
+Li, T-P., and Y-Q. Ma.
+“Analysis methods for results in gamma-ray astronomy.”
+The Astrophysical Journal 272 (1983): 317-324.
+Formula (17)
Calculate number of showers that were simulated in the given
+energy and 2D fov bins in nominal coordinates.
+
This assumes the events were generated uniformly distributed per solid angle,
+and from a powerlaw in energy like CORSIKA simulates events.
+
+
Parameters:
+
+
energy_bins: astropy.units.Quantity[energy]
The energy bin edges for which to calculate the number of simulated showers
+
+
fov_longitude_bins: astropy.units.Quantity[angle]
The FOV longitude bin edges for which to calculate the number of simulated showers
+
+
fov_latitude_bins: astropy.units.Quantity[angle]
The FOV latitude bin edges for which to calculate the number of simulated showers
+
+
+
+
Returns:
+
+
n_showers: numpy.ndarray(ndim=3)
The expected number of events inside each of the
+energy_bins, fov_longitude_bins and fov_latitude_bins.
+Dimension (n_energy_bins, n_fov_longitude_bins, n_fov_latitude_bins)
+This is a floating point number.
+The actual numbers will follow a poissionian distribution around this
+expected value.
The FOV azimuthal bin edges for which to calculate the number of simulated showers
+
+
+
+
Returns:
+
+
n_showers: numpy.ndarray(ndim=3)
The expected number of events inside each of the
+energy_bins, fov_offset_bins and fov_position_angle_bins.
+Dimension (n_energy_bins, n_fov_offset_bins, n_fov_position_angle_bins)
+This is a floating point number.
+The actual numbers will follow a poissionian distribution around this
+expected value.
Calculate number of showers that were simulated in the given energy intervals
+
This assumes the events were generated and from a powerlaw
+like CORSIKA simulates events.
+
+
Parameters:
+
+
energy_bins: astropy.units.Quantity[energy]
The interval edges for which to calculate the number of simulated showers
+
+
+
+
Returns:
+
+
n_showers: numpy.ndarray
The expected number of events inside each of the energy_bins.
+This is a floating point number.
+The actual numbers will follow a poissionian distribution around this
+expected value.
Calculate number of showers that were simulated in the given
+energy and fov bins.
+
This assumes the events were generated uniformly distributed per solid angle,
+and from a powerlaw in energy like CORSIKA simulates events.
+
+
Parameters:
+
+
energy_bins: astropy.units.Quantity[energy]
The energy bin edges for which to calculate the number of simulated showers
+
+
fov_bins: astropy.units.Quantity[angle]
The FOV bin edges for which to calculate the number of simulated showers
+
+
+
+
Returns:
+
+
n_showers: numpy.ndarray(ndim=2)
The expected number of events inside each of the
+energy_bins and fov_bins.
+Dimension (n_energy_bins, n_fov_bins)
+This is a floating point number.
+The actual numbers will follow a poissionian distribution around this
+expected value.
Calculate number of showers that were simulated in the given fov bins.
+
This assumes the events were generated uniformly distributed per solid angle,
+like CORSIKA simulates events with the VIEWCONE option.
+
+
Parameters:
+
+
fov_bins: astropy.units.Quantity[angle]
The FOV bin edges for which to calculate the number of simulated showers
+
+
+
+
Returns:
+
+
n_showers: numpy.ndarray(ndim=2)
The expected number of events inside each of the fov_bins.
+This is a floating point number.
+The actual numbers will follow a poissionian distribution around this
+expected value.
Power Law parametrization of the Crab Nebula spectrum as published by HEGRA
+
From “The Crab Nebula and Pulsar between 500 GeV and 80 TeV: Observations with the HEGRA stereoscopic air Cherenkov telescopes”,
+Aharonian et al, 2004, ApJ 614.2
+doi.org/10.1086/423931
Log-Parabola parametrization of the Crab Nebula spectrum as published by MAGIC
+
From “Measurement of the Crab Nebula spectrum over three decades in energy with the MAGIC telescopes”,
+Aleksìc et al., 2015, JHEAP
+https://doi.org/10.1016/j.jheap.2015.01.002
This functions returns 0 significance when n_on < alpha * n_off
+instead of the negative sensitivities that would result from naively
+evaluating the formula.
+
+
Parameters:
+
+
n_on: integer or array like
Number of events for the on observations
+
+
n_off: integer or array like
Number of events for the off observations
+
+
alpha: float
Ratio between the on region and the off region size or obstime.
Function joins bins into lo and hi part, e.g. [0, 1, 2] and [1, 2, 4] into [0, 1, 2, 4] It works on multidimentional arrays as long as the binning is in the last axis.
Fix pyirf.irfs.energy_dispersion.energy_dispersion_to_migration.
+This function was not adapted to the now correct normalization of the
+energy dispersion matrix, resulting in wrong results on the now correct
+matrices. [#273]
Fix pyirf.benchmarks.energy_bias_resolution_from_energy_dispersion.
+This function was not adapted to the now correct normalization of the
+energy dispersion matrix, resulting in wrong results on the now correct
+matrices. [#268]
Adds an extrapolator for parametrized compontents utilizing blending over visible edges, resulting
+in a smooth extrapolation compared to the NearestSimplexExtrapolator. [#253]
+
Ignore warnings about invalid floating point operations when calculating n_signal and n_signal_weigthed because the relative sensitivty is frequently NaN. [#264]
+
Add basic combinatoric fill-value handling for RAD_MAX estimation. [#282]
Fix PowerLaw.from_simulation for the new format of SimulatedEventsInformation,
+it was broken since splitting the single viewcone into viewcone_min and viewcone_max. [#258]
In prior versions of pyirf, the energy dispersion matrix was normalized to a
+sum of 1 over the migration axis.
+This is wrong, the correct normalization is to an integral of 1, which is fixed now.
+
The internal API of the interpolation functions had to be adapted to take in additional
+keywords, mainly the bin edges and the kind of normalization (standard or solid angle cone sections). [#250]
+
+
Replace single viewcone argument of SimulationInfo with
+viewcone_min and viewcone_max, e.g. to correctly enable
+ring wobble simulations. [#239]
Change the interpolation API to top-level estimator classes that instantiate
+inter- and extrapolator objects. Drops the interpolate_xyz functions
+originally used to interpolate a xyz IRF component in favour of a XYZEstimator
+class. Moves data checks from intepolator to estimator classes.
+
Direct usage of interpolator objects is now discuraged, use estimator objects instead. [#228]
Correctly fill n_events in angular_resolution, was always 0 before. [#231]
+
Remove condition that relative sensitivity must be > 1.
+This condition was added by error and resulted in returning
+nan if the flux needed to fulfill the conditions is larger than
+the reference flux used to weight the events. [#241]
Add moment morphing as second interpolation method able to handle discretized PDF
+components of IRFs. [#229]
+
Add a base structure for extrapolators similar to the interpolation case
+as well as a first extrapolator for parametrized components, extrapolating from the
+nearest simplex in one or two dimensions. [#236]
+
Add an extrapolator for discretized PDF components, extrapolating from the
+nearest simplex in one or two dimensions utilizing the same approach moment morphing
+interpolation uses. [#237]
+
Add a DiscretePDFNearestNeighborSearcher and a ParametrizedNearestNeighborSearcher to support nearest neighbor approaches
+as alternatives to inter-/ and extrapolation [#232]
Migrating the interpolation methods from pyirf.interpolation to interpolator
+objects, allowing for later inheritance for new algorithms and reusability. [#210]
This release is an important update that introduces three
+changes in the cut optimization, background estimation and sensitivity calculation.
+
Together, these changes bring the calculated sensitivities much closer to the ones calculated by
+EventDisplay.
+
+
Scale the relative flux calculated to reach the target sensitivity
+up if the requirements on the minimum number of signal events are not met.
+Essentially, instead of always calculating the flux that
+yields target_sensitivity and then checking if the two other conditions are met,
+we increase the required flux to meet the other requirements.
+This can result in new sensitivities where before pyirf would report no sensitivities,
+and report better sensitivities everywhere where the event number conditions where not
+met before at the target significance.
+The best sensitivity now is the lowest flux that just barely satisfies all
+requirements (so is at the minimum requirement of one of the three).
+
Differentiate between reco_source_fov_offset and true_source_fov_offset,
+using the former for background rates and the latter for everything concerning
+signal events.
+
Change optimize_gh_cut to do the optimization in terms of efficiency and
+limit this efficiency to max. 80 % in the EventDisplay comparison.
+
+
Smaller improvements also include:
+
+
It is now possible to include a particle_type column in the event lists,
+which will result in additionally reporting all event counts also per particle_type.
+E.g. if particle_type is included in the background table consisting of both
+electrons and protons, estimate_background will not only report n_background(_weighted)
+but also n_electron(_weighted) and n_proton(_weighted)
+
relative_sensitivity now supports vectorized application and broadcasting
+of inputs, as previously wrongly advertized in the docstring.
GammaPy 0.18.0 was released and includes fixes for IRF axis orders.
+The output of pyirf in GADF fits format can now be read by gammapy without
+problems.
+The workarounds for installing GammaPy is also no longer needed.
This release is the result of the IRF sprint week in September 2020.
+Many bug fixes and improvements were made to the code.
+
As the target for the sprint week was to reproduce the approach of EventDisplay and
+the resulting IRFs, one scheme of cut optimization is implemented.
+The examples/calculate_eventdisplay_irfs.py should follow the approach
+of EventDisplay closely and shows what is currently implemented in pyirf.
+In the central and upper energy range, pyirf now reproduces the EventDisplay sensitivity
+exactly, the lower energy bins still show some disagreement.
+The cut optimization seems not yet to be the same as EventDisplay’s and will be further investigated.
+This example could be used as a starting point if you also want to do cut optimization for best sensitivity.
+
At least one version of each IRF is now implemented and can be stored in the GADF format.
+Computation of full-enclosure IRFs should be possible but is of now not yet tested
+on a reference dataset.
For this version, pyirf’s API was completely rewritten from scratch,
+merging code from several projects (pyirf, pyfact, fact-project/irf) to provide a library to compute IACT
+IRFs and sensitivity and store them in the GADF data format.
+
The class based API using a configuration file was replaced by a finer grained
+function based API.
+
Implemented are point-like IRFs and sensitivity.
+
This release was the starting point for the IRF sprint week in September 2020,
+where the refactoring continued.
If you are not part of the cta-observatory organization,
+you need to fork the repository to contribute.
+See the GitHub tutorial on forks if you are unsure how to do this.
+
+
When you find something that is wrong or missing
+
+
+
Go to the issue tracker and check if an issue already exists for your bug or feature
+
In general it is always better to anticipate a PR with a new issue and link the two
+
+
+
+
To work on a bug fix or new feature, create a new branch, add commits and open your pull request
+
+
If you think your pull request is good to go and ready to be reviewed,
+you can directly open it as normal pull request.
+
You can also open it as a “Draft Pull Request”, if you are not yet finished
+but want to get early feedback on your ideas.
+
Especially when working on a bug, it makes sense to first add a new
+test that fails due to the bug and in a later commit add the fix showing
+that the test is then passing.
+This helps understanding the bug and will prevent it from reappearing later.
+
Create a changelog entry in docs/changes, please note the README.md there.
+Minor changes (on the magnitude of fixing a broken link or renaming a variable) can receive the no-changelog-needed label.
+This should, however, be a rare exception.
+
+
+
Wait for review comments and then implement or discuss requested changes.
+
+
We use Github Actions to
+run the unit tests and documentation building automatically for every pull request.
+Passing unit tests and coverage of the changed code are required for all pull requests.
This documentation uses sphinx and restructured text.
+For an Introduction, see the Sphinx documentation.
+
To build the docs locally, enter the docs directory and call:
+
makehtml
+
+
+
Some changes require a full remake of the documentation, for that call
+
makecleanhtml
+
+
+
If you created or deleted file or submodule, you also need to remove the
+api directory, it will be regenerated automatically.
+
Make sure the docs are built without warnings from sphinx, as these
+will be treated as errors in the build in the CI system as they most often
+result in broken styling.
Calculating Sensitivity and IRFs for EventDisplay DL2 data
+
The examples/calculate_eventdisplay_irfs.py file is
+using pyirf to optimize cuts, calculate sensitivity and IRFs
+and then store these to FITS files for DL2 event lists from EventDisplay.
+
The ROOT files were provided by Gernot Maier and converted to FITS format
+using the EventDisplay DL2 converter script.
+The resulting FITS files are the input to the example and can be downloaded using:
+
./download_private_data.sh
+
+
+
This requires curl and unzip to be installed.
+The download is password protected, please ask one of the maintainers for the
+password.
+
A detailed explanation of the contents of such DL2 files can be found
+here (internal).
+
The example can then be run from the root of the repository after installing pyirf
+by running:
+
pythonexamples/calculate_eventdisplay_irfs.py
+
+
+
A jupyter notebook plotting the results and comparing them to the EventDisplay output
+is available in examples/comparison_with_EventDisplay.ipynb
pyirf is a prototype for the generation of Instrument Response Functions (IRFs)
+for the Cherenkov Telescope Array
+(CTA).
+The package is being developed and tested by members of the CTA consortium and
+is a spin-off of the analog sub-process of the
+pipeline protopype.
+
Its main features are currently to
+
+
+
find the best cutoff in gammaness/score, to discriminate between signal
+and background, as well as the angular cut to obtain the best sensitivity
+for a given amount of observation time and a given template for the
+source of interest (Cut Optimization)
+
compute the instrument response functions, effective area,
+point spread function and energy resolution (Instrument Response Functions)
+
estimate the sensitivity of the array (Sensitivity),
+
+
+
with plans to extend its capabilities to reach the requirements of the
+future observatory.
+
The source code is hosted on a GitHub repository, to
+which this documentation is linked.
+
+
Warning
+
This is not yet stable code, so expect large and rapid changes.
If you want to work on pyirf itself, clone the repository and install the local
+copy of pyirf in development mode.
+
The dependencies required to perform unit-testing and to build the documentation
+are defined in extras under tests and docs respectively.
+
These requirements can also be enabled by installing the all extra:
+
$pipinstall-e'.[all]'# or [docs,tests] to install them separately
+
+
+
You should isolate your pyirf development environment from the rest of your system.
+Either by using a virtual environment or by using conda environments.
+pyirf provides a conda environment.yml, that includes all dependencies:
This module contains functions to inter- or extrapolate from a set of IRFs for different
+conditions to a new IRF. Implementations of interpolation and extrapolation algorithms
+exist as interpolator and extrapolator classes and are applied by top-level estimator
+classes to IRF components.
+Direct usage of the inter- and extrapolator classes is discouraged, as only the estimator classes
+check the data for consistency.
+
Most methods support an arbitrary number of interpolation dimensions although it
+is strongly advised to limit those for reasonable results.
+The herein provided functionalities can e.g. be used to interpolate the IRF
+for a zenith angle of 30° from available IRFs at 20° and 40°.
This module provides inter- and extrapolation classes that can be
+plugged into the estimator classes.
+Not all of these classes support arbitrary grid-dimensions where the grid
+in this context is the grid of e.g. observation parameters like zenith angle and
+magnetic field inclination (this would be a 2D grid) on which template IRFs exist
+and are meant to be inter- or extrapolated.
+
For parametrized components (Effective Areas and Rad-Max tables) these classes are:
M. Baak, S. Gadatsch, R. Harrington and W. Verkerke (2015). Interpolation between
+multi-dimensional histograms using a new non-linear moment morphing method
+Nucl. Instrum. Methods Phys. Res. A 771, 39-48. https://doi.org/10.1016/j.nima.2014.10.033
Usage of the estimator classes is simple.
+As an example, consider CTA’s Prod5 IRFs [CTA+21], they can be downloaded manually or by executing
+download_irfs.py in pyirf's root directory, which downloads them to .../pyirf/irfs/.
+The estimator classes can simply be used by first creating an instance of the respective class with all
+relevant information and then using the object’s __call__ interface the obtain results for a specific
+target point.
+As the energy dispersion represents one of the discretized PDF IRF components, one can use the
+MomentMorphInterpolator for interpolation and the DiscretePDFNearestNeighborSearcher
+for extrapolation.
To create a estimator class for an IRF component not yet implemented, one can simply
+inherit from respective base class.
+There are two, tailored to either parametrized or discrete PDF components.
Base class for all Estimators working on IRF components that represent discretized PDFs.
+
+
+
+
Consider an example, where one is interested in an estimator for simple Gaussians.
+As this is already the scope of the DiscretePDFComponentEstimator base class and
+for the sake of this demonstration, let the Gaussians come with some
+units attached that need handling:
This new estimator class can now be used just like any other estimator class already
+implemented in pyirf.interpolation.
+While the extrapolator_cls argument can be empty when creating an instance of
+GaussianEstimator, effectively disabling extrapolation and raising an error in
+case it would be needed regardless, assume the desired extrapolation method to be
+MomentMorphNearestSimplexExtrapolator:
Currently, pyirf allows calculation of the usual factorization of the IRFs into:
+
+
Effective area
+
Energy migration
+
Point spread function
+
+
Additionally, functions for calculating point-source flux sensitivity are provided.
+Flux sensitivity is defined as the smallest flux an IACT can detect with a certain significance,
+usually 5 σ according to the Li&Ma likelihood ratio test, in a specified amount of time.
+
pyirf also provides functions to calculate event weights, that are needed
+to translate a set of simulations to a physical flux for calculating sensitivity
+and expected event counts.
+
Event selection with energy dependent cuts is also supported,
+but at the moment, only rudimentary functions to find optimal cuts are provided.
pyirf does not rely on specific input file formats.
+All functions take numpy arrays, astropy quantities or astropy tables for the
+required data and also return the results as these objects.
Most functions for calculating IRFs need DL2 event lists as input.
+We use ~astropy.table.QTable instances for this.
+QTable are very similar to the standard ~astropy.table.Table,
+but offer better interoperability with astropy.units.Quantity.
+
We expect certain columns to be present in the tables with the appropriate units.
+To learn which functions need which columns to be present, have a look at the API Documentation
+
Most functions only need a small subgroup of these columns.
This module contains functions to read input data and write IRFs in GADF format.
+
Currently there is only support for reading EventDisplay DL2 FITS files,
+which were converted from the ROOT files by using EventDisplay DL2 conversion scripts.
The collection area, which is proportional to the gamma-ray efficiency
+of detection, is computed as a function of the true energy. The events which
+are considered are the ones passing the threshold of the best cutoff plus
+the angular cuts.
The energy dispersion matrix, ratio of the reconstructed energy over the true energy
+as a function of the true energy, is computed with the events passing the
+threshold of the best cutoff plus the angular cuts.
+
The corresponding energy migration matrix can be build from the dispersion matrix.
The background rate is calculated as the number of background-like events per
+second, reconstructed energy and solid angle.
+The current version is computed in radially symmetric bins in the Field Of View.
Using pyirf to calculate IRFs from the FACT Open Data
+
Note In FACT, we used a different terminology, partly because of being a monoscopic telescope or out of confusion witht the CTA terms, in this context DL3 are reconstructed events, but not necessarily already with the IRF
Currently, pyirf only works with powerlaw simulated events, like CORSIKA does it. We want to also support arbitrary histograms / event distributions, but that is not yet implemented.
+
This can be created from a file with that information, but I will just create it here.
pyirf does not prescribe or use a specific DL2 file format. You need to read the data into an astropy.table.QTable following our conventions, detailed in the docs here:
We only have point-like simulations at a specific wobble offset (0.6° for FACT), so we calculate the effective area for all events at once, equivalent to a single fov offset bin.
true_energy_bins=create_bins_per_decade(simulation_info.energy_min,simulation_info.energy_max,5)
+
+# single offset bin around the wobble distance
+# since we are dealing with point-like simulations
+wobble_offset=0.6*u.deg
+fov_offset_bins=[0.59,0.61]*u.deg
+
# utility function to converet pyirf Quantities to the gammapy classes
+frompyirf.gammapyimportcreate_effective_area_table_2d
+
+plt.figure()
+
+foraeff,labelinzip((aeff_all,aeff_selected),('All Events','Selected Events')):
+ aeff_gammapy=create_effective_area_table_2d(
+ # add a new dimension for the single fov offset bin
+ effective_area=aeff[...,np.newaxis],
+ true_energy_bins=true_energy_bins,
+ fov_offset_bins=fov_offset_bins,
+ )
+
+
+ aeff_gammapy.plot_energy_dependence(label=label,offset=[wobble_offset])
+
+plt.xlim(true_energy_bins.min().to_value(u.GeV),true_energy_bins.max().to_value(u.GeV))
+plt.yscale('log')
+plt.xscale('log')
+plt.legend()
+
+print(aeff_gammapy)
+
+
+
+
+
+
+
+
+/opt/hostedtoolcache/Python/3.9.21/x64/lib/python3.9/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
+ from .autonotebook import tqdm as notebook_tqdm
+
The point spread function describes how well the direction of the gamma rays is estimated.
+
+
[13]:
+
+
+
frompyirf.irfimportpsf_table
+frompyirf.utilsimportcalculate_source_fov_offset
+
+
+gammas['true_source_fov_offset']=calculate_source_fov_offset(gammas)
+
+
+source_offset_bins=np.linspace(0,3,100)*u.deg
+
+# calculate this only for the events after the gamma/hadron separation
+psf=psf_table(gammas[gammas['selected_gh']],true_energy_bins,source_offset_bins,fov_offset_bins)
+
frompyirf.irfimportenergy_dispersion
+
+# logarithmic space, is "symmetric" in terms of ratios 0.1 is a factor of 10 from 1 is a factor of 10 from 10
+migration_bins=np.geomspace(0.1,10,100)
+
+edisp=energy_dispersion(
+ gammas[gammas['selected']],
+ true_energy_bins=true_energy_bins,
+ fov_offset_bins=fov_offset_bins,
+ migration_bins=migration_bins,
+)
+
frompyirf.io.gadfimportcreate_aeff2d_hdu,create_energy_dispersion_hdu,create_psf_table_hdu
+fromastropy.ioimportfits
+fromastropy.timeimportTime
+frompyirfimport__version__
+
+# set some common meta data for all hdus
+meta=dict(
+ CREATOR='pyirf-v'+__version__,
+ TELESCOP='FACT',
+ INSTRUME='FACT',
+ DATE=Time.now().iso,
+)
+
+hdus=[]
+
+# every fits file has to have an Image HDU as first HDU.
+# GADF only uses Binary Table HDUs, so we need to add an empty HDU in front
+hdus.append(fits.PrimaryHDU(header=fits.Header(meta)))
+
+hdus.append(create_aeff2d_hdu(aeff_selected,true_energy_bins,fov_offset_bins,**meta))
+hdus.append(create_energy_dispersion_hdu(edisp,true_energy_bins,migration_bins,fov_offset_bins,**meta))
+hdus.append(create_psf_table_hdu(psf,true_energy_bins,source_offset_bins,fov_offset_bins,**meta))
+
+fits.HDUList(hdus).writeto('fact_irf.fits.gz',overwrite=True)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/notebooks/fact_example.ipynb b/notebooks/fact_example.ipynb
new file mode 100644
index 000000000..58baedf82
--- /dev/null
+++ b/notebooks/fact_example.ipynb
@@ -0,0 +1,763 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "plastic-system",
+ "metadata": {},
+ "source": [
+ "# Using `pyirf` to calculate IRFs from the FACT Open Data\n",
+ "\n",
+ "\n",
+ "**Note** In FACT, we used a different terminology, partly because of being a monoscopic telescope or out of confusion witht the CTA terms, in this context DL3 are reconstructed events, but not necessarily already with the IRF"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "alike-dover",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:39.912448Z",
+ "iopub.status.busy": "2025-01-20T14:51:39.912219Z",
+ "iopub.status.idle": "2025-01-20T14:51:40.459172Z",
+ "shell.execute_reply": "2025-01-20T14:51:40.458528Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import astropy.units as u\n",
+ "import matplotlib.pyplot as plt\n",
+ "import subprocess as sp"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "german-carroll",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:40.461544Z",
+ "iopub.status.busy": "2025-01-20T14:51:40.461293Z",
+ "iopub.status.idle": "2025-01-20T14:51:40.479441Z",
+ "shell.execute_reply": "2025-01-20T14:51:40.478910Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "%matplotlib inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "analyzed-canberra",
+ "metadata": {},
+ "source": [
+ "## Download Data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "joined-experiment",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:40.481477Z",
+ "iopub.status.busy": "2025-01-20T14:51:40.481073Z",
+ "iopub.status.idle": "2025-01-20T14:51:48.084824Z",
+ "shell.execute_reply": "2025-01-20T14:51:48.083909Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "path = \"gamma_test_dl3.hdf5\"\n",
+ "url = f\"https://factdata.app.tu-dortmund.de/dl3/FACT-Tools/v1.1.2/{path}\"\n",
+ "ret = sp.run([\"curl\", \"-z\", path, \"-fsSLO\", url], stdout=sp.PIPE, stderr=sp.PIPE, encoding='utf-8')\n",
+ "if ret.returncode != 0:\n",
+ " raise IOError(ret.stderr)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "accredited-count",
+ "metadata": {},
+ "source": [
+ "## Read in the data\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "italian-redhead",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:48.087505Z",
+ "iopub.status.busy": "2025-01-20T14:51:48.087054Z",
+ "iopub.status.idle": "2025-01-20T14:51:49.534886Z",
+ "shell.execute_reply": "2025-01-20T14:51:49.534044Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from astropy.table import QTable\n",
+ "import astropy.units as u\n",
+ "import tables"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "healthy-wrapping",
+ "metadata": {},
+ "source": [
+ "### Simulated Event Info\n",
+ "\n",
+ "Currently, pyirf only works with powerlaw simulated events, like CORSIKA does it.\n",
+ "We want to also support arbitrary histograms / event distributions, but that is not yet implemented.\n",
+ "\n",
+ "This can be created from a file with that information, but I will just create it here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "micro-anniversary",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:49.537367Z",
+ "iopub.status.busy": "2025-01-20T14:51:49.537119Z",
+ "iopub.status.idle": "2025-01-20T14:51:49.709588Z",
+ "shell.execute_reply": "2025-01-20T14:51:49.708942Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from pyirf.simulations import SimulatedEventsInfo\n",
+ "\n",
+ "simulation_info = SimulatedEventsInfo(\n",
+ " energy_min=200 * u.GeV,\n",
+ " energy_max=50 * u.TeV,\n",
+ " spectral_index=-2.7,\n",
+ " n_showers=12600000,\n",
+ " max_impact=300 * u.m,\n",
+ " viewcone_min=0 * u.deg,\n",
+ " viewcone_max=0 * u.deg,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "interior-richards",
+ "metadata": {},
+ "source": [
+ "### DL2 Event List\n",
+ "\n",
+ "`pyirf` does not prescribe or use a specific DL2 *file* format.\n",
+ "You need to read the data into an `astropy.table.QTable` following our conventions, detailed in the docs here: \n",
+ "\n",
+ "\n",
+ "\n",
+ "The FACT-Tools / aict-tools analysis chain uses a column-oriented hdf5 file written using h5py. \n",
+ "Unfortunately, units have to be known and are not in the metadata."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "southeast-reform",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:49.712570Z",
+ "iopub.status.busy": "2025-01-20T14:51:49.712178Z",
+ "iopub.status.idle": "2025-01-20T14:51:49.793698Z",
+ "shell.execute_reply": "2025-01-20T14:51:49.793008Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "gammas = QTable()\n",
+ "\n",
+ "# mapping of : ()\n",
+ "columns = {\n",
+ " 'obs_id': ('run_id', None),\n",
+ " 'event_id': ('event_num', None),\n",
+ " 'reco_energy': ('gamma_energy_prediction', u.GeV),\n",
+ " 'true_energy': ('corsika_event_header_total_energy', u.GeV),\n",
+ " 'true_az': ('source_position_az', u.deg),\n",
+ " 'pointing_az': ('pointing_position_az', u.deg),\n",
+ " 'theta': ('theta_deg', u.deg),\n",
+ " 'gh_score': ('gamma_prediction', None),\n",
+ "}\n",
+ "\n",
+ "with tables.open_file('gamma_test_dl3.hdf5', mode='r') as f:\n",
+ " events = f.root.events\n",
+ " \n",
+ " for col, (name, unit) in columns.items():\n",
+ " if unit is not None:\n",
+ " gammas[col] = u.Quantity(events[name][:], unit, copy=False)\n",
+ " else:\n",
+ " gammas[col] = events[name][:]\n",
+ " \n",
+ " gammas['true_alt'] = u.Quantity(90 - events['source_position_zd'][:], u.deg, copy=False)\n",
+ " gammas['pointing_alt'] = u.Quantity(90 - events['pointing_position_zd'][:], u.deg, copy=False)\n",
+ "\n",
+ " \n",
+ "# make it display nice\n",
+ "for col in gammas.colnames:\n",
+ " if gammas[col].dtype == float:\n",
+ " gammas[col].info.format = '.2f'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "optional-crawford",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2025-01-20T14:51:49.796047Z",
+ "iopub.status.busy": "2025-01-20T14:51:49.795673Z",
+ "iopub.status.idle": "2025-01-20T14:51:49.804388Z",
+ "shell.execute_reply": "2025-01-20T14:51:49.803730Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "