"""Classes for gravity-related accelerations."""# from functools import lru_cacheimportnumpyasnpfrom.accelimportAccelas_Accel,_invnormfrom.utilsimportfind_file,normfrom.import_ssapy
[docs]classHarmonicCoefficients:"""Class to hold coefficients for a spherical harmonic expansion of a gravitational potential. """
[docs]@classmethoddeffromEGM(cls,filename):"""Construct a HarmonicCoefficients object from a .egm/.cof file pair as available from https://geographiclib.sourceforge.io/html/gravity.html SSAPy comes with four models for Earth gravity of varying degrees and orders: name | degree | order ---------------------------- WGS84 | 20 | 0 EGM84 | 180 | 180 EGM96 | 360 | 360 EGM2008 | 2190 | 2159 Note that many coefficients with large degree and/or order underflow double precision floats when denormalized for use in computing accelerations and are therefore effectively ignored. Parameters ---------- filename : str Either the name of the .egm file or one of the names listed above. """original_filename=filenametry:filename=find_file(filename,ext=".egm")exceptFileNotFoundError:# For backwards compatibility, also try with .lower()try:filename=find_file(filename.lower(),ext=".egm")exceptFileNotFoundError:raiseFileNotFoundError(original_filename)egm_fn=filenamecof_fn=egm_fn+".cof"# First get metadata filewithopen(egm_fn,"r")asf:radius=NoneMG=Noneforlineinf.readlines():ifline.startswith("ModelRadius"):radius=float(line[11:])ifline.startswith("ModelMass"):MG=float(line[9:])ifradiusisNone:raiseValueError(f"Could not find model radius in file {cof_fn}")ifMGisNone:raiseValueError(f"Could not find model mass in gravity file {cof_fn}")withopen(cof_fn,"rb")asf:# First 8 bytes are namename=f.read(8).decode("ascii")# Next 8 bytes are N, Mn_max=n_max=int.from_bytes(f.read(4),"little")m_max=m_max=int.from_bytes(f.read(4),"little")# Rest of file are normalized coefficientsnC=(m_max+1)*(2*n_max-m_max+2)//2nS=m_max*(2*n_max-m_max+1)//2C=np.array(np.frombuffer(f.read(8*nC)))S=np.array(np.frombuffer(f.read(8*nS)))# Assemble index arraysCn=[]Cm=[]m=0whilelen(Cn)<nC:Cn.extend(range(m,n_max+1))Cm.extend([m]*(n_max+1-m))m+=1Sn=[]Sm=[]m=1whilelen(Sn)<nS:Sn.extend(range(m,n_max+1))Sm.extend([m]*(n_max+1-m))m+=1# De-normalize coefficientsforiinrange(len(C)):C[i]*=_invnorm(Cn[i],Cm[i])foriinrange(len(S)):S[i]*=_invnorm(Sn[i],Sm[i])# Now form the CS matrix to compactly hold both C and S coefficients.# C(n, m) = CS(n, m) and S(n, m) = CS(m-1, n)CS=np.empty((n_max+1,n_max+1))foriinrange(len(C)):CS[Cn[i],Cm[i]]=C[i]foriinrange(len(S)):CS[Sm[i]-1,Sn[i]]=S[i]# We set the Keplerian term to 0.0 so it's easier to examine the# non-central forces directly.CS[0,0]=0.0ret=HarmonicCoefficients.__new__(HarmonicCoefficients)ret.name=nameret.radius=radiusret.MG=MGret.CS=CSret.n_max=n_maxret.m_max=m_max# print(ret, n_max, m_max)returnret
[docs]@classmethoddeffromTAB(cls,filename,n_max=40,m_max=40):"""Construct a HarmonicCoefficients object from a .tab file as available from https://pgda.gsfc.nasa.gov/products/50 """original_filename=filenametry:filename=find_file(filename,ext=".tab")exceptFileNotFoundError:# Reraise with original filenameraiseFileNotFoundError(original_filename)# read headerwithopen(filename,"r")asf:(r_ref_km,GM_km3_s2,dGM_km3_s2,n_max1,m_max1,norm,lon_ref_deg,lat_ref_deg)=np.array(f.readline().replace(',',' ').split()).astype(float)n_max=min(int(n_max1),n_max)m_max=min(int(m_max1),m_max)# read array (TODO: only read relevant rows)max_rows=(n_max+2)*(n_max+1)//2+2degree,order,C,S,dC,dS=np.genfromtxt(filename,skip_header=1,delimiter=',',unpack=True,max_rows=max_rows)degree=degree.astype(int)order=order.astype(int)# denormalizeforiinrange(len(C)):# Only bother for degree <= n_max, order <= m_maxdg,od=degree[i],order[i]ifdg>n_max:break# degree is sorted, so can break here.ifod>m_max:continueC[i]*=_invnorm(dg,od)S[i]*=_invnorm(dg,od)# cached# Now form the CS matrix to compactly hold both C and S coefficients.# C(n, m) = CS(n, m) and S(n, m) = CS(m-1, n)CS=np.full((n_max+1,n_max+1),np.nan)foriinrange(len(C)):dg,od=degree[i],order[i]ifdg>n_max:break# degree is sorted, so can break here.ifod>m_max:continueCS[dg,od]=C[i]ifod!=0:CS[od-1,dg]=S[i]# Set Keplerian term to 0.0 so it's easier to examine the non-central# forces directly.CS[0,0]=0.0ret=HarmonicCoefficients.__new__(HarmonicCoefficients)ret.name=original_filenameret.radius=r_ref_km*1e3ret.MG=GM_km3_s2*1e9ret.CS=CSret.n_max=n_maxret.m_max=m_maxreturnret
# TODO# def fromGFC(cls, filename):# """Construct a HarmonicCoefficients object from a .gfc file as available# from http://icgem.gfz-potsdam.de# """def__hash__(self):returnhash(("HarmonicCoefficients",self.name,self.radius,self.MG,self.n_max,self.m_max,tuple(self.CS.ravel())))def__eq__(self,rhs):ifnotisinstance(rhs,HarmonicCoefficients):returnFalsereturn(self.name==rhs.nameandself.radius==rhs.radiusandself.MG==rhs.MGandself.n_max==rhs.n_maxandself.m_max==rhs.m_maxandnp.array_equal(self.CS,rhs.CS))
[docs]classAccelThirdBody(_Accel):"""Acceleration due to a third body. Parameters ---------- body : ssapy.Body The third body. """def__init__(self,body):super().__init__()self.body=bodydef__call__(self,r,v,t,**kwargs):"""Evaluate acceleration at particular place/moment. Parameters ---------- r : array_like, shape(3, ) Position in meters. v : array_like, shape(3, ) Velocity in meters per second. Unused. t : float Time as GPS seconds. Unused Returns ------- accel : array_like, shape(3,) Acceleration in meters per second^2 """s=self.body.position(t)d=s-rreturn-self.body.mu*(s/norm(s)**3-d/norm(d)**3)
[docs]classAccelHarmonic(_Accel):"""Acceleration due to a harmonic potential. Parameters ---------- body : ssapy.Body The body. n_max : int The maximum degree of the potential. m_max : int The maximum order of the potential. """def__init__(self,body,n_max=None,m_max=None):super().__init__()ifn_maxisNone:n_max=body.harmonics.n_maxifm_maxisNone:m_max=body.harmonics.m_maxself.body=bodyifn_max>body.harmonics.n_max:print(f"WARNING::The provided degree ({n_max}) is higher than the maximum value allowed by the "f"{body.harmonics.name} model ({body.harmonics.n_max}).\nSetting the degree to "f"{body.harmonics.n_max}.")self.n_max=body.harmonics.n_maxelse:self.n_max=n_maxifm_max>body.harmonics.m_max:print(f"WARNING::The provided order ({m_max}) is higher than the maximum value allowed by the "f"{body.harmonics.name} model ({body.harmonics.m_max}).\nSetting the order to "f"{body.harmonics.m_max}.")self.m_max=body.harmonics.m_maxelse:self.m_max=m_maxself._harmonic=_ssapy.AccelHarmonic(self.body.mu,self.body.radius,self.body.harmonics.CS.shape[0],self.body.harmonics.CS.ctypes.data)def__call__(self,r,v,t,_E=None,**kwargs):"""Evaluate acceleration at particular place/moment. Parameters ---------- r : array_like, shape(3, ) Position in meters. v : array_like, shape(3, ) Velocity in meters per second. Unused. t : float Time as GPS seconds. Unused Returns ------- accel : array_like, shape(3,) Acceleration in meters per second^2 """dr=self.body.position(t)if_EisNone:_E=self.body.orientation(t)a=np.empty(3)# Transform to body framerin=_E@(r-dr)# Compute accelerationself._harmonic.accel(self.n_max,self.m_max,rin.ctypes.data,a.ctypes.data)# Transform back to integration framereturn_E.T@a