''' Geometric factors for MEX/IMA.
.. note::
Among a lot of definitions of geometric factor, the current best knowledge of MEX/IMA at IRF is
to use :func:`gfactor_func`. This is used for the official data archiving at PSA.
It is not straightforward, both theoretically and practically.
Here are some implementations, but carefully assess and use when you use them.
The current best proposal of the geometric factor for MEX IMA is only to use :func:`gfactor_func`.
'''
import numpy as np
from irfpy.mima import energy as _energy
[docs]class GL_CR4:
r''' Geometric factor defined in MEX-ASP-CR-0004.
P22 gives us impression on some values of g-factor.
This is :math:`G_0/\Delta\Phi` [cm2 rad eV/eV].
I am not very sure what the absolute value indicate here,
but the relative value should at least be meaningful.
>>> gl_h0 = GL_CR4.Proton.PAC0()
>>> print(gl_h0([0, 2000, 5000, 10000, 30000])) # doctest: +NORMALIZE_WHITESPACE
[0.00000000e+00 1.55148151e-07 4.89672821e-06 4.48326188e-05
4.48326188e-05]
'''
[docs] class Proton:
[docs] class PAC0:
def __init__(self):
self.mid = lambda e: 5.6856e-20 * np.power(e, 3.7673)
self.mid0 = 2000.
self.mid1 = 9000.
def __call__(self, energy):
''' Get approximated gfactor for PACC0.
'''
earray = np.array(energy)
garray = np.zeros_like(earray)
garray = np.where(earray < self.mid0, 0, garray)
garray = np.where(np.logical_and(earray >= self.mid0, earray < self.mid1),
self.mid(earray), garray)
garray = np.where(earray >= self.mid1, self.mid(self.mid1) * np.ones_like(garray), garray)
return garray
[docs] class PAC1:
def __init__(self):
self.mid0 = 800.
self.mid1 = 9000.
self.mid = lambda e: 7.75129e-13 * np.power(e, 1.9619)
def __call__(self, energy):
earray = np.array(energy)
garray = np.zeros_like(earray)
garray = np.where(earray < self.mid0, 0, garray)
garray = np.where(np.logical_and(earray >= self.mid0, earray < self.mid1),
self.mid(earray), garray)
garray = np.where(earray >= self.mid1, self.mid(self.mid1) * np.ones_like(garray), garray)
return garray
[docs] class PAC2:
def __init__(self):
self.mid0 = 100.
self.mid1 = 5665.
self.mid2 = 9000.
self.mid01 = lambda e: 1.5975e-8 * np.power(e, 0.8747146)
self.mid12 = lambda e: 3.3118e-17 * np.power(e, 3.18833)
def __call__(self, energy):
earray = np.array(energy)
garray = np.zeros_like(earray)
garray = np.where(earray < self.mid0, 0, garray)
garray = np.where(np.logical_and(earray >= self.mid0, earray < self.mid1),
self.mid01(earray), garray)
garray = np.where(np.logical_and(earray >= self.mid1, earray < self.mid2),
self.mid12(earray), garray)
garray = np.where(earray >= self.mid2, self.mid12(self.mid2) * np.ones_like(garray), garray)
return garray
[docs] class Alpha:
[docs] class PAC0:
def __init__(self):
e0 = 4343.47288876
g0 = 7.11715324658e-06
e1 = 9521.30113195
g1 = 0.000100127259076
# Follow g = k0 * E ^ k1
k1 = np.log(g1 / g0) / np.log(e1 / e0)
k0 = g0 / np.power(e0, k1)
self.func0 = lambda e: k0 * np.power(e, k1)
self.e0 = 2000.
self.e1 = 9000.
def __call__(self, energy):
earray = np.array(energy)
garray = np.zeros_like(earray)
garray = np.where(earray < self.e0, 0, garray)
garray = np.where(np.logical_and(earray >= self.e0, earray < self.e1),
self.func0(earray), garray)
garray = np.where(earray >= self.e1, self.func0(self.e1) * np.ones_like(garray), garray)
return garray
[docs] class PAC1:
def __init__(self):
e0 = 591.676042159
g0 = 1.87182187345e-06
e1 = 8639.4700344
g1 = 4.1855209508e-05
# Follow g = k0 * E ^ k1
k1 = np.log(g1 / g0) / np.log(e1 / e0)
k0 = g0 / np.power(e0, k1)
self.func0 = lambda e: k0 * np.power(e, k1)
self.e0 = 800.
self.e1 = 9000.
def __call__(self, energy):
earray = np.array(energy)
garray = np.zeros_like(earray)
garray = np.where(earray < self.e0, 0, garray)
garray = np.where(np.logical_and(earray >= self.e0, earray < self.e1),
self.func0(earray), garray)
garray = np.where(earray >= self.e1, self.func0(self.e1) * np.ones_like(garray), garray)
return garray
[docs] class PAC2:
def __init__(self):
e0, g0, e1, g1, e2, g2 = [float(val) for val in
'''275.384500319 4.03092208896e-06
612.651125795 1.30086655237e-05
9288.46528267 5.44660332838e-05'''.split()]
# Fisst func. Follow g = k0 * E ^ k1
k1 = np.log(g1 / g0) / np.log(e1 / e0)
k0 = g0 / np.power(e0, k1)
self.func0 = lambda e: k0 * np.power(e, k1)
k3 = np.log(g2 / g1) / np.log(e2 / e1)
k2 = g1 / np.power(e1, k3)
self.func1 = lambda e: k2 * np.power(e, k3)
self.e0 = 100
self.e1 = np.exp(np.log(k2 / k0) / (k1 - k3))
self.e2 = 9000
def __call__(self, energy):
earray = np.array(energy)
garray = np.zeros_like(earray)
garray = np.where(earray < self.e0, 0, garray)
garray = np.where(np.logical_and(earray >= self.e0, earray < self.e1),
self.func0(earray), garray)
garray = np.where(np.logical_and(earray >= self.e1, earray < self.e2),
self.func1(earray), garray)
garray = np.where(earray >= self.e2, self.func1(self.e2) * np.ones_like(garray), garray)
return garray
def _logE(energy):
return np.log(energy)
def _indexbelow(energy, e0):
return np.where(np.array(energy) < e0)
def _indexabove(energy, e0):
return np.where(np.array(energy) > e0)
def _indexbetween(energy, e0, e1):
earray = np.array(energy)
return np.where(np.logical_and(earray >= e0, earray < e1))
class _LogLinearfunc:
"""log linear function from the data points
log y = (log y1 - log y0) / (log x1 - log x0) (log x - log x0) + log y0
"""
def __init__(self, x0, y0, x1, y1):
lx0 = np.log10(x0)
ly0 = np.log10(y0)
lx1 = np.log10(x1)
ly1 = np.log10(y1)
self._inclination = (ly1 - ly0) / (lx1 - lx0)
self._lx0 = lx0
self._ly0 = ly0
def __call__(self, val):
log_y = self._inclination * (np.log10(val) - self._lx0) + self._ly0
return np.power(10, log_y)
def __str__(self):
return 'log(y) = {:g} x (log(x) - {:g}) + ({:g})'.format(self._inclination, self._lx0, self._ly0)
[docs]class GF_SWM_draft:
""" Geometric factor used in SWM report, Figure 6 middle. Read from the figure.
The geometric factor used in SWM project.
.. warning::
Do not use. Use :func:`gfactor_func` instead.
Only for H+ and He++. The unit is in ``cm-2 eV/eV`` which implies the ``omega`` is already multiplied.
Only PAC1 (pacclevel4) supported.
"""
_stolen_x = [327, 543, 622, 1390, 1560, 2800, 106, 297, 15200, 845, 2045, 15000, ]
_stolen_y = [0.000006870000, 0.000017400000, 0.000018000000, 0.000018000000, 0.000016100000, 0.000003380000,
0.000636000000, 0.000636000000, 0.000071700000, 0.000016400000, 0.000218000000, 0.000071700000, ]
_A = _LogLinearfunc(_stolen_x[0], _stolen_y[0], _stolen_x[1], _stolen_y[1]) # Proton ghost, roughly 300 - 500 eV
_B = _LogLinearfunc(_stolen_x[2], _stolen_y[2], _stolen_x[3], _stolen_y[3]) # Proton ghost, roughly 500 - 1500 eV
_C = _LogLinearfunc(_stolen_x[4], _stolen_y[4], _stolen_x[5], _stolen_y[5]) # Proton ghost, roughly 1500 - 3000 eV
_D = _LogLinearfunc(_stolen_x[6], _stolen_y[6], _stolen_x[7], _stolen_y[7]) # Alpha, roughly below 300 eV
_E = _LogLinearfunc(_stolen_x[7], _stolen_y[7], _stolen_x[8], _stolen_y[8]) # Alpha, roughly above 300 eV
_F = _LogLinearfunc(_stolen_x[9], _stolen_y[9], _stolen_x[10], _stolen_y[10]) # Proton, roughly 800 - 2000 eV
_G = _LogLinearfunc(_stolen_x[10], _stolen_y[10], _stolen_x[11], _stolen_y[11]) # Proton, roughly 2000 eV or higher
[docs] class Proton:
[docs] class PAC1:
def __init__(self):
pass
def __call__(self, energy):
g0 = GF_SWM_draft._F(energy)
g1 = GF_SWM_draft._G(energy)
g = np.min([g0, g1], axis=0)
g = np.where(g < 1e-5, 0, g)
return g
[docs] class ProtonGhost:
[docs] class PAC1:
def __init__(self):
pass
def __call__(self, energy):
g0 = GF_SWM_draft._A(energy)
g0 = np.where(g0 < 6e-6, 0, g0)
g1 = GF_SWM_draft._B(energy)
g2 = GF_SWM_draft._C(energy)
g = np.min([g0, g1, g2], axis=0)
return g
[docs] class Alpha:
[docs] class PAC1:
def __init__(self):
pass
def __call__(self, energy):
g0 = GF_SWM_draft._D(energy)
g1 = GF_SWM_draft._E(energy)
g = np.min([g0, g1], axis=0)
return g
# This shall be changed after the formulation is available.
GF_SWM = GF_SWM_draft
""" SWM gfactor, but deprecated. Use :func:`gfactor_func`.
"""
[docs]def gfactor_func(species, eeprom, pacc):
""" Geometric factor used in MEX IMA PSA archiving (2021-01-27)
:param species: Species. Eitherof ['heavy', 'proton', 'alpha', 'ghost']
:param eeprom: Prom sectin number, 0-16.
:param pacc: PACC, either (0, 1, 2)
:returns: Geometric factor in cm2 sr eV/eV.
:exception ValueError: Given the unsupported species name.
.. note::
This is the current (2021-01) best knowledge of geometric factor at IRF
with the "normal" definition (in cm2 sr eV/eV) including the efficiency.
Provides the G factor [cm2 sr eV/eV] for a species at a specific eeprom and pacc level.
Use pacc 0, 1, or 2 (corresponding to 0, 4, 7).
The G-factor is used as follows:
diff_flux = counts / (tau[s] * energy[eV] * G)
to calculate differential flux of unit [cm-2 s-1 sr-1 eV-1].
This version of the G-factor was used for the MEx IMA PSA archiving project
as of 2021-01-27.
>>> from irfpy.mima import gfactor0
>>> gf = gfactor0.gfactor_func('proton', 16, 1)
>>> print(gf.shape) # GEometric factor for each energy step is returned, so the shape should be (96,)
(96,)
>>> print('{:.2e}'.format(gf[20])) # Geometric factor for energy step 20
4.12e-06
>>> gf = gfactor0.gfactor_func('oxygen', 16, 2) # PACC level 2 (=7)
>>> print(gf.shape)
(96,)
>>> print("{:.2e}".format(gf[20]))
7.80e-06
"""
etbl = _energy.get_default_table(eeprom, keep_negative=True)
Ebreakpoint = [np.nan, 300, 600] # Value determined after IMA Kiruna workshop 2007
dphi = 22.5 * np.pi / 180 # Azimuth sector size to go from rad to sr
# Heavy ions
es = etbl.shape[0]
Gl = np.ones((3, es)) * np.nan
Glh = Gl.copy()
Gl[0, :] = np.exp(-0.378481 * np.log(etbl[:es]) - 9.05248)
Glh[0, :] = Gl[0, :]
Gl[1, :] = 7.0e-5 * np.ones((1, es))
Glh[1, :] = np.exp(-0.69 * np.log(etbl[:es]) - 5.63)
Gl[2, :] = 1e-4
Glh[2, :] = np.exp(-0.611844 * np.log(etbl[:es]) - 5.31592) # from Andrei
aa = etbl >= Ebreakpoint[1]
Gl[1, aa] = Glh[1, aa]
aa = etbl >= Ebreakpoint[2]
Gl[2, aa] = Glh[2, aa]
if species.lower() in ['heavy', 'oxygen', 'o', 'op', 'o2', 'o2p']:
return Gl[pacc, :] * dphi / 2 # Multiply with azimuth sector size to get the correct unit
elif species.lower() in ['proton', 'hydrogen', 'h', 'p', 'hp']:
# H+
GH = Gl.copy()
aa = etbl[:es] > 2000
GH[1, aa] = np.exp(-0.554935 * np.log(etbl[aa]) - 6.40179)
aa = (etbl[:es] < 2000) & (etbl[:es] > 700)
GH[1, aa] = np.exp(3.0440526 * np.log(etbl[aa]) - 33.7573)
aa = etbl > 1500
GH[2, aa] = np.exp(-0.611844 * np.log(etbl[aa]) - 5.31592)
return GH[pacc, :] * dphi / 2 # Multiply with azimuth sector size to get the correct unit
elif species.lower() in ['alpha', 'helium', 'he', 'hepp', 'he2p', 'al']:
# He++
GHe = Gl.copy()
aa = etbl[:es] > 300
GHe[1, aa] = np.exp(-0.554935 * np.log(etbl[aa]) - 6.40179)
return GHe[pacc, :] * dphi / 2 # Multiply with azimuth sector size to get the correct unit
elif species.lower() in ['ghost', 'protonghost', 'g', 'hg', 'pg']:
# H+ ghosts
Gghost = np.ones((3, es)) * np.nan
aa = (etbl[:es] > 300) & (etbl[:es] < 800)
Gghost[1, aa] = 2 * np.exp((14.0 - 13.1224) / (6.68461 - 6.21461) * (np.log(etbl[aa]) - 6.21461) - 14.0)
aa = (etbl[:es] >= 800) & (etbl[:es] < 1500)
Gghost[1, aa] = 2.0e-6
aa = (etbl[:es] >= 1500) & (etbl[:es] < 3000)
Gghost[1, aa] = np.exp((13.1224 - 14.4902) / (7.82405 - 7.31322) * (np.log(etbl[aa]) - 7.31322) - 13.1224)
# From old code for low and high pacc
Gghost[0, :] = 1.6e-6 * np.ones((1, es)) * 16 / np.pi # Version from Markus, second IMA
Gghost[2, :] = 2.8e-6 * np.ones((1, es)) * 16 / np.pi
aa = etbl[:es] < 1500
Gghost[2, aa] = np.exp(1.44064 * np.log(etbl[aa]) - 22.7525)
Gghost[Gghost < 0] = np.nan
return Gghost[pacc, :] * dphi / 2 # Multiply with azimuth sector size to get the correct unit
else:
raise ValueError('Species not recognised.')