Source code for ibpmodel.ibpcalc

#!/usr/bin/python3 -q
"""This module contains the main functions and the auxiliary/control functions needed to calculate the IBP index.
"""

from   scipy import special
import numpy as np
import cdflib
import os
from datetime import datetime

#=== UTILS =====================================================================

[docs]def tiler(*args): """Provides mixed combinations of arguments. Creates copies of the arguments that have length equal to the product of the length of the arguments. This results in an ordered set of all combinations of the individual values from each of the arguements. Parameters ---------- *args : array-likes Each argument is an array-like of possibly different length. Returns ------- list of array-likes A list of ordered combination of the input arguments. Examples -------- >>> from ibpmodel import ibpcalc >>> ibpcalc.tiler([1, 2, 3],['A', 'B']) [array([1, 1, 2, 2, 3, 3]), array(['A', 'B', 'A', 'B', 'A', 'B'], dtype='<U1')] >>> ibpcalc.tiler([17,13],[1, 2, 3],['A', 'B']) [array([17, 17, 17, 17, 17, 17, 13, 13, 13, 13, 13, 13]), array([1, 1, 2, 2, 3, 3, 1, 1, 2, 2, 3, 3]), array(['A', 'B', 'A', 'B', 'A', 'B', 'A', 'B', 'A', 'B', 'A', 'B'], dtype='<U1')] """ arg_number = len(args) if arg_number < 1: return None elif arg_number == 1: return np.array(args[0]) lengths = list(map(len,args)) maybe_tile = lambda arg,i: np.tile(arg,np.product(lengths[:i])) if i > 0 else arg return [ maybe_tile( np.repeat(args[i], np.product(lengths[(i + 1):])) if i < arg_number - 1 else args[i], i) for i in range(len(args)) ]
[docs]def tile_aggregate(result,*args,aggregator=np.mean): """Compresses tiles and aggregate results on the last axis of tiled ranges Parameters ---------- result : numpy.array Array that is aggregated. *args : list or numpy.array Along the last element is crompressed. aggregator : function, optional Specifies how to aggregate. the defaults is `np.mean`. Returns ------- list of numpy.array last element is the compressed result Example ------- >>> from ibpmodel import ibpcalc >>> ibpcalc.tile_aggregate(np.array([3,2,3,3,2,1,2,1]), [10,12], [20,21,22,24]) (10, 12, array([2.75, 1.5 ])) >>> ibpcalc.tile_aggregate(np.array([3,2]), np.array([12]), np.array([20,21])) (12, array([2.5])) >>> ibpcalc.tile_aggregate( np.array([3,2,2,1,4,2,3,3,1,4,8,5,9,6,7,4,5,2,1,6,7,9,5,7]), [9,10,11], [20,21]), [7,8,5,6]) (array([ 9, 9, 10, 10, 11, 11]), array([20, 21, 20, 21, 20, 21]), array([2. , 3. , 4.5, 6.5, 3.5, 7. ])) """ if len(args) <= 1: return aggregator(result) *preserved, collapesed = args reshaped= result.reshape(np.product(list(map(len,preserved))),len(collapesed)) return (*tiler(*preserved), aggregator(reshaped, axis=1))
[docs]def doyFromMonth(month): '''Calculate day of year from the 15th of the month Parameters ---------- month : int Value as the month in the year, with january being month 1, ``1 <= month <= 12`` Returns ------- int Day of year. Example ------- >>> ibpcalc.doyFromMonth(7) 196 ''' doy_month = [15, 46, 74, 105, 135, 166, 196, 227, 258, 288, 319, 349] if isinstance(month, (int,np.integer)) and month in range(1,13): return doy_month[month-1] else: raise ValueError("Value " + str(month) + " out of range or wrong type!")
[docs]def monthFromDoy(doy): '''Calculate month from day of the year Parameters ---------- doy : int Day of the year, ``1 <= doy <= 365``. Returns ------- int Value as the month in the year, with january being month 1. Example ------- >>> ibpcalc.monthFromDoy(275) 10 ''' if isinstance(doy, (int,np.integer)) and doy in range(1,366): return int(datetime.strptime(str(doy), '%j').month) else: raise ValueError("Value " + str(doy) + " out of range or wrong type!")
[docs]def monthfromString(month_str): '''Convert abbreviated month name to *int* Parameters ---------- month_str : str Abbreviated month name. *['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']* Returns ------- int Value as the month in the year, with january being month 1. Example ------- >>> ibpcalc.monthfromString('Mar') 3 ''' months = [ datetime(2000,m,1).strftime("%b") for m in range(1,13) ] if isinstance(month_str, str) and month_str in months: return months.index(month_str)+1 else: raise ValueError("Wrong month string: " + str(month_str))
[docs]def checkDoyMonth(day_month): '''Control if input is day of the year (*int*) or abbreviated month name (*str*). Parameters ---------- day_month : int or str or list Value to be checked Returns ------- doy_out : list of int(s) list of days of the year Example ------- >>> ibpcalc.checkDoyMonth(['Mar',200,1]) [74, 200, 1] >>> ibpcalc.checkDoyMonth('Dec') [349] ''' if not isinstance(day_month, list): day_month = [day_month] doy_out = [] for e in day_month: if isinstance(e, str): doy_out.append(doyFromMonth(monthfromString(e))) elif isinstance(e, int) and e in range(1,366): doy_out.append(e) else: raise ValueError("Wrong day_month value: " + str(e)) return doy_out
[docs]def checkParameter(para, para_range): '''Check if *para* or element of *para* in *para_range*. Parameters ---------- para : int or float or list Value to be checked para_range : range searching range Returns ------- numpy.array Numpy.array contains *para*. Example ------- >>> ibpcalc.checkParameter(3,range(0,5)) array([3]) >>> ibpcalc.checkParameter([0,1,2,3,4],range(0,5)) array([0, 1, 2, 3, 4]) ''' if isinstance(para, (list,np.ndarray)) and not False in list(map(lambda i: True if para_range[0] <= i <= para_range[-1] else False, para)): return np.array(para) elif isinstance(para, (float, int)) and para_range[0] <= para <= para_range[-1]: return np.array([para]) else: raise ValueError("Value " + str(para) + " out of range or wrong type!")
#=== IBP ROUTINES =============================================================
[docs]def read_model_file(file=None): """Load CDF content into a dictionary. If no file is declared, the CDF file included in the package will be used. (SW_TEST_IBP_CLI_2__00000000T000000_99999999T999999_0001.cdf) Parameters ---------- file : file path, optional Path to cdf file containing parametors for the model. The default is None. Returns ------- dict Contains the content of the CDF file. """ if file == None: basepath = os.path.dirname(__file__) file = os.path.abspath(os.path.join(basepath,'SW_TEST_IBP_CLI_2__00000000T000000_99999999T999999_0001.cdf')) data = {} cdf = cdflib.CDF(file) for key in [ 'Parameters', 'Intensity', 'Monthly_LT_Shift', 'Density_Estimators', 'Density_Estimator_Lons' ]: data[key] = cdf.varget(key) cdf.close() return data
[docs]def compute_probability(time, expected_bubbles, expected_lifetime, mu, sigma): """Computes the probability of Ionospheric Plasma Bubbles. Parameters ---------- time : float or ndarray of floats The local time, can be eiter as an array or not. expected_bubbles : float or ndarray of floats The expected amount of bubbles, can be eiter as an array or not. expected_lifetime : float or ndarray of floats The expected lifetime of each bubble, can be eiter as an array or not. mu : float or ndarray of floats The mean position of the probability distribution in the model, can be eiter as an array or not. sigma : float or ndarray of floats The standard deviation of the probability distribution in the model, can be eiter as an array or not. Returns ------- float or ndarray of floats The Ionospheric Bubble Index, value(s) between 0.0 and 1.0, it has the same length as all of the inputs Note ---- Plasma Bubbles are large-scale depletions compared to the background ionosphere, occurring in the equatorial F-region, in particular after sunset. They are assumably driven by Rayleigh-Taylor instability and already in the past extensively studied by different techniques, showing occurrence probabilities depending on evironmental parameters as season, location, local time and sun activity. For a climatologic model of these dependencies, extracted from fairly long time series of distortions in the magnetic field readings of the LEO satellites CHAMP (2000-2010) and Swarm (since 2013) the function calculates a probability density. Examples -------- >>> import numpy as np >>> from ibpmodel import ibpcalc >>> ibpcalc.compute_probability(0.0,0.5,0.5,0.5,0.5) 0.049701856965716384 >>> a = np.arange(24)+0.5 >>> ibpcalc.compute_probability(a,a,a,a,a) array([0.12259724, 0.32454412, 0.48001002, 0.5996932 , 0.69182957, 0.76275943, 0.81736377, 0.85940013, 0.89176121, 0.91667393, 0.93585262, 0.95061706, 0.96198326, 0.97073336, 0.9774695 , 0.98265522, 0.98664737, 0.98972067, 0.99208661, 0.99390799, 0.99531015, 0.99638959, 0.99722058, 0.9978603 ]) >>> ibpcalc.compute_probability(a,2*a,1.2,1.3,0.4) array([2.00069488e-02, 7.81272947e-01, 8.55868576e-01, 6.93670227e-01, 4.83704476e-01, 2.96120013e-01, 1.65026216e-01, 8.64714939e-02, 4.35684741e-02, 2.14048463e-02, 1.03395302e-02, 4.93490050e-03, 2.33423700e-03, 1.09629097e-03, 5.11888048e-04, 2.37840684e-04, 1.10040886e-04, 5.07234746e-05, 2.33043267e-05, 1.06755465e-05, 4.87751438e-06, 2.22316484e-06, 1.01112283e-06, 4.58962618e-07]) """ expected_bubbles = np.maximum(0,np.array(expected_bubbles, dtype = 'float')) lambda_1 = np.array(expected_bubbles, dtype = 'float') lambda_2 = np.array(1 / expected_lifetime, dtype = 'float') mu = np.array(mu, dtype = 'float') sigma = np.array(sigma, dtype = 'float') time = np.array(time, dtype = 'float') # Transform problem to be purely in terms of error function # (by Ask Neve Gamby). Based on rewriting the integrant to an # exponential of a quadric polynomia, and then using coordinate # transformation and scaling of y axis to go to a basic # exp(-x**2) form, which can be easily evaluated by an error function outer_mult = -1. / (2 * np.pi * sigma**2)**0.5 inner_mult = -1. / (2*sigma**2) x0 = mu - lambda_2 / (2*inner_mult) y0 = lambda_2 * (mu - time - lambda_2 / (4 * inner_mult)) inner_mult_sqrt = (-inner_mult)**0.5 outer_mult_corrected2 = (np.pi)**0.5 * outer_mult * np.exp(y0) / inner_mult_sqrt integrated = (0.5 + special.erf( (time - x0) * inner_mult_sqrt ) * 0.5) * outer_mult_corrected2 return 1.0 - np.exp(lambda_1 * integrated)
[docs]def fourier_model(coeffients, theta, periode = 365.0): """Computes a value based on a model of fourier components up to 2nd degree. Parameters ---------- coeffients : array-like of numbers The coefficients of the fourier series up to second degree, with even numbers representing cosinus and odd sinus. The shape of `coefficients` must be *(5,*theta.shape)*. theta : number or ndarray of numbers The part representing the phase in the fourier expansion. It is in the same units as the periode. periode : number, optional The amount of `theta` need for one periode of the 1st degree of the fourier expansion. Defaults to 365 (the days in a year). Returns ------- number or ndarray of numbers The shape of this is equivalent to the shape of `theta`. Examples -------- >>> from ibpmodel import ibpcalc >>> import numpy as np >>> ibpcalc.fourier_model([0,1,-1,0,0],180) 1.0420963481067607 >>> ibpcalc.fourier_model([0,1,-1,-0.5,0.7],1,periode=10) -0.48044810416758793 >>> ibpcalc.fourier_model([0,1,-1,-0.5,0.7],np.arange(11),10) array([-0.3 , -0.4804481 , -0.218165 , 0.98765424, 2.0886424 , 1.7 , -0.03798462, -1.50224404, -1.53249278, -0.70496209, -0.3 ]) """ base = theta * (np.pi * 2. / periode) base2 = base * 2 return ( coeffients[0] + coeffients[1] * np.sin(base) + coeffients[2] * np.cos(base) + coeffients[3] * np.sin(base2) + coeffients[4] * np.cos(base2) )
[docs]def compute_lambda(longitude, params, f107, gosc_val, density, month = 0): """Computes the 'lambda' parameter. Lambda is the intensity, one of the parameters describing the final probability and modeled as a Poisson process. Parameters ---------- longitude : float or ndarray of floats The longitude(s) of the point(s) we calculate lambda for. params : array-like Parameter values from the model (CDF-file). f107 : float or ndarray of floats The Solar Radio Flux (F10.7 index). gosc_val : float or ndarray of floats result of method `fourier_model()` density : ndarray of floats Density values from the model (CDF-file). month : int or ndarray of ints, optional The number of months since the year started, meaning January would be 0. The default is 0. Returns ------- float or ndarray of floats Examples -------- >>> from ibpmodel import ibpcalc >>> import numpy as np >>> ibpcalc.compute_lambda(1,[-232.54229262, 4.67324294], 123.4, 17.3, np.array([2.1,3.0]) ) 1084.307658528 >>> params = np.array([-232.54229262, 4.67324294, 1.34695254, -1.31448553, 1.09712955]) >>> densi_once = np.sin(np.arange(360)*np.pi/180) * 3 + 5 >>> ibpcalc.compute_lambda(1,[-232.54229262, 4.67324294], 123.4, 17.3, densi_once ) 1788.2556529203105 >>> densi_year = np.array([ (densi_once -3)* np.cos(month*np.pi/6) + 8 for month in range(12)]) >>> ibpcalc.compute_lambda(1,[-232.54229262, 4.67324294], 123.4, 17.3, densi_year, month=5) 1851.5944384882494 >>> months = np.array((np.sin(np.arange(20))+1) % 12, dtype='int') >>> ibpcalc.compute_lambda(1,[-232.54229262, 4.67324294], 123.4, 17.3, densi_year[:,months], month=months) array([2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391, 2149.6915391]) """ number_of_density = np.prod(density.shape) selection = np.array( (longitude + 180.) * (number_of_density / 360) + month, dtype = 'int' ) selected_density = density.reshape(number_of_density)[selection] return np.maximum( 0, selected_density * (params[0] + f107 * params[1] + gosc_val) )
[docs]def align_time_of_year(day_of_year, month): """Guess day of year and month from each other. Parameters ---------- day_of_year : int or ndarray of ints Time as the day of year, restricted to ``0 < day_of_year <= 356``, with the value 0 meaning it should be calculated based on `month` (which gives the median day of the `month`). month : int or ndarray of ints Time as the month in the year, with january being month 1, so ``0 < month <= 12``. Returns ------- day_of_year : int or ndarray of ints A recalculated possible copy of the `day_of_year` parameter. month : int or ndarray of ints A recalculated copy of the `month` parameter. Examples -------- >>> from ibpmodel import ibpcalc >>> import numpy as np >>> ibpcalc.align_time_of_year(0,6) (166, 6) >>> ibpcalc.align_time_of_year(162,3) (162, 6) >>> ibpcalc.align_time_of_year(0,np.arange(12)+1) (array([ 16, 45, 75, 105, 136, 166, 197, 228, 258, 289, 319, 350]), array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])) >>> ibpcalc.align_time_of_year( np.array([0,33,21,17,267,0,115,96,315,0,172,256]),np.arange(12)+1) (array([ 16, 33, 21, 17, 267, 166, 115, 96, 315, 289, 172, 256]), array([ 1, 2, 1, 1, 9, 6, 4, 4, 11, 10, 6, 9])) """ # This is a side-effect free version by Ask Neve Gamby. # # 2019-03-05: Now always recalculating 'month' (clumsy type handling?), # Martin Rother (rother@gfz-potsdam.de). is_array = isinstance(day_of_year,np.ndarray) if is_array != isinstance(month,np.ndarray): #ensure that both now are arrays if is_array: month = np.array([month] * len(day_of_year)) else: day_of_year = np.array([day_of_year] * len(month)) is_array = True days_in_month = np.array([31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]) days_at_month = np.cumsum([0, *days_in_month]) if ( not is_array and day_of_year == 0 or is_array and any(day_of_year == 0) ): day_of_year_guess = days_at_month[:-1] + days_in_month // 2 + (days_in_month % 2 > 0 ) guesses = day_of_year_guess[month - 1] if is_array: day_of_year = np.array(day_of_year) day_of_year[day_of_year == 0] = guesses[day_of_year == 0] else: day_of_year = guesses month = sum(day_of_year > dat for dat in days_at_month) return (day_of_year, month)
[docs]def compute_probability_exp(day_of_year, month, longitude, local_time, f107, data): """Compute the Ionospheric Bubble index. Core routine to return a probability of the occurence of a bubble. Parameters ---------- day_of_year : int or ndarray of ints Time as the day of year, restricted to ``0 < day_of_year <= 356``, with the value 0 meaning it should be calculated based on month (which gives the median day of the month). month : int or ndarray of ints Time as the month in the year, with january being month 1, ``0 < month <= 12``. longitude : float or ndarray of floats The geographical longitude(s), ``-180 <= longitude <= 180``. local_time : float or ndarray of floats The local time, can be eiter as an array or not, ``-6.0 <= local_time <= 24``. f107 : float or ndarray of floats The Solar Radio Flux (F10.7 index), ``0.0 <= f107 <= 200.0``. data : dict Containing the parameters of the model (CDF-file). Returns ------- float or ndarray of floats The Ionospheric Bubble Index, value(s) between 0.0 and 1.0, it has the same length as all of the inputs Examples -------- >>> from ibpmodel import ibpcalc >>> import numpy as np >>> data = ibpcalc.read_model_file() >>> ibpcalc.compute_probability_exp(0, 3, 12, 2, 17.3, data) 0.0016535983798842135 >>> ibpcalc.compute_probability_exp(0, 2, 12, 2, 17.3, data) 0.0 >>> parts = ibpcalc.tiler(np.arange(2,5), np.array([12,124]), np.arange(6)) >>> ibpcalc.compute_probability_exp(0, *parts, f107=17.3, data=data) array([0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 7.24797932e-03, 3.47038779e-03, 1.65359838e-03, 7.87395045e-04, 3.74847713e-04, 1.78430902e-04, 2.71409055e-03, 1.30214944e-03, 6.20242441e-04, 2.95263158e-04, 1.40545020e-04, 6.68965929e-05, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00]) Note ---- Rearranged/rewritten and optimized by Ask Neve Gamby <aknvg@space.dtu.dk>. The resolution of the function `gosc` is higher than monthly. If a `day_of_year` is known, the results can be more precise. It is now possible to calculate with either ndarrays or single values, for all parameters except data (which has a special structure). Note that this version is, compared with the initial version more closely resembling the original 'R' code, much more efficient due to vectorization. """ # CHECKS: if np.any( (local_time < -6.0) | (local_time > 24.0) ): raise ValueError("Local time(s) or hour to midnight out of range!") if np.any( (f107 < 0.0) | (f107 > 200.0) ): raise ValueError("F10.7 parameter(s) out of valid range!") # Normalize time selected (day_of_year, month) = align_time_of_year(day_of_year, month) # Extraction density = np.array(data['Density_Estimators']) shifts = np.array(data['Monthly_LT_Shift' ]) params = np.array(data['Parameters' ]) gosc = np.array(data['Intensity' ]) gosc_val = fourier_model(gosc, day_of_year, 365.0) # Force LT to be between -6 and 6 (needed by model) local_time = ((local_time + 12) % 24) - 12 lambda0 = compute_lambda( longitude, params, f107, gosc_val, density, month - 1 ) shifttime = fourier_model(shifts[:, month - 1], longitude, 360.0) return compute_probability( local_time, lambda0, params[2], params[3] + shifttime, params[4] )
#===============================================================================