1

2# -*- coding: utf-8 -*-

3

4 u'''Discrete Sine Transforms (AuxDST) in Python, transcoded from I{Karney}'s C++ class

5 U{DST<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1DST.html>}

6 in I{GeographicLib version 2.2+}.

7

8 Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2022-2023) and licensed

9 under the MIT/X11 License. For more information, see the U{GeographicLib

10<https://GeographicLib.SourceForge.io>} documentation.

11

12@note: Class L{AuxDST} requires U{numpy<https://PyPI.org/project/numpy>} to be

13 installed, version 1.16 or newer.

14'''

15# make sure int/int division yields float quotient, see .basics

16 from __future__ import division as _; del _ # PYCHOK semicolon

17

18 from pygeodesy.auxilats.auxily import _Dm

19 from pygeodesy.basics import isodd, neg, _reverange, _xnumpy

20 from pygeodesy.constants import PI_2, PI_4, isfinite, _0_0, _0_5, _naninf

21# from pygeodesy.fsums import Fsum # from .karney

22 from pygeodesy.karney import _2cos2x, _ALL_DOCS, Fsum, property_RO

23# from pygeodesy.lazily import _ALL_DOCS # from .karney

24# from pygeodesy.props import property_RO # from .karney

25

26__all__ = ()

27__version__ = '23.12.02'

28

29

30 class AuxDST(object):

31 '''Discrete Sine Transforms (DST) for I{Auxiliary} latitudes.

32

33 @see: I{Karney}'s C++ class U{DST

34 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1DST.html>}.

35 '''

36 _N = 0

37

38 def __init__(self, N):

39 '''New L{AuxDST} instance.

40

41 @arg N: Size, number of points (C{int}).

42 '''

43 if N > 0:

44 self._N = int(N)

45 # kissfft(N, False) # size, inverse

46

47 @staticmethod

48 def evaluate(sinx, cosx, F, *N):

49 '''Evaluate the Fourier sum given the sine and cosine of the angle,

50 using precision I{Clenshaw} summation.

51

52 @arg sinx: The sin(I{sigma}) (C{float}).

53 @arg cosx: The cos(I{sigma}) (C{float}).

54 @arg F: The Fourier coefficients (C{float}[]).

55 @arg N: Optional, (smaller) number of terms to evaluate (C{int}).

56

57 @return: Precison I{Clenshaw} sum (C{float}).

58

59 @see: Methods C{AuxDST.integral} and C{AuxDST.integral2}.

60 '''

61 a = -_2cos2x(cosx, sinx)

62 if isfinite(a):

63 Y0, Y1 = Fsum(), Fsum()

64 n = _len_N(F, *N)

65 Fn = list(F[:n])

66 _F = Fn.pop

67 if isodd(n):

68 Y0 -= _F()

69 while Fn: # Y0, Y1 negated

70 Y1 -= Y0 * a + _F()

71 Y0 -= Y1 * a + _F()

72 r = float(_Dm(-Y0, Y1, sinx))

73 else:

74 r = _naninf(-a)

75 return r

76

77 @property_RO

78 def _fft_numpy(self):

79 '''(INTERNAL) Get the C{numpy.fft} module, I{once}.

80 '''

81 AuxDST._fft_numpy = fft = _xnumpy(AuxDST, 1, 16).fft # overwrite property_RO

82 return fft

83

84 def _fft_real(self, data):

85 '''(INTERNAL) NumPy's I{kissfft}-like C{transform_real} function,

86 taking C{float}[:N] B{C{data}} and returning C{complex}[:N*2].

87 '''

88 # <https://GitHub.com/mborgerding/kissfft/blob/master/test/testkiss.py>

89 return self._fft_numpy.rfftn(data)

90

91 def _ffts(self, data, cIV):

92 '''(INTERNAL) Compute the DST-III or DST-IV FFTransforms.

93

94 @arg data: Elements DST-III[0:N+1] or DST-IV[0:N] (C{float}[])

95 with DST_III[0] = 0.

96 @arg cIV: If C{True} DST-IV, otherwise DST-III.

97

98 @return: FFTransforms (C{float}[0:N]).

99 '''

100 T, N = (), self.N

101 if N > 0:

102 N2 = N * 2

103 d = tuple(data)

104 # assert len(d) == N + (0 if cIV else 1)

105

106 if cIV: # DST-IV

107 from cmath import exp as _cexp

108

109 def _cF(c, j, r=-PI_4 / N):

110 return c * _cexp(complex(0, r * j))

111

112 i = 0

113 else: # DST-III

114 i = 1

115 # assert d[0] == _0_0

116

117 def _cF(c, *unused): # PYCHOK redef

118 return c

119

120 d += tuple(reversed(d[i: N])) # i == len(d) - N

121 d += tuple(map(neg, d[:N2]))

122 c = self._fft_real(d) # complex[0:N*2]

123 n2 = float(-N2)

124 T = tuple(_cF(c[j], j).imag / n2 for j in range(1, N2, 2))

125 return T

126

127 def _ffts2(self, data, F):

128 '''(INTERNAL) Doubled FFTransforms.

129

130 @arg data: Grid centers (C{float}[N]).

131 @arg F: The transforms (C{float}[N])

132

133 @return: Doubled FFTransforms (C{float}[N*2]).

134 '''

135 __2 = _0_5 # N = self._N

136 # copy DST-IV order N transform to D[0:N]

137 D = self._ffts(data, True)

138 # assert len(D) == N and len(F) >= N

139 # (DST-IV order N - DST-III order N) / 2

140 M = tuple((d - f) * __2 for d, f in zip(D, F)) # strict=False

141 # (DST-IV order N + DST-III order N) / 2

142 P = tuple((d + f) * __2 for d, f in zip(D, F)) # strict=False

143 # assert len(M) == len(P) == self._N

144 return P + tuple(reversed(M))

145

146 @staticmethod

147 def integral(sinx, cosx, F, *N):

148 '''Evaluate the integral of Fourier sum given the sine and

149 cosine of the angle, using precision I{Clenshaw} summation.

150

151 @arg sinx: The sin(I{sigma}) (C{float}).

152 @arg cosx: The cos(I{sigma}) (C{float}).

153 @arg F: The Fourier coefficients (C{float}[]).

154 @arg N: Optional, C{len(B{F})} or a (smaller) number of

155 terms to evaluate (C{int}).

156

157 @return: Precison I{Clenshaw} intergral (C{float}).

158

159 @see: Methods C{AuxDST.evaluate} and C{AuxDST.integral2}.

160 '''

161 a = _2cos2x(cosx - sinx, cosx + sinx)

162 if isfinite(a):

163 Y0, Y1 = Fsum(), Fsum()

164 for r in _reverscaled(F, *N):

165 Y1 -= Y0 * a + r

166 Y1, Y0 = Y0, -Y1

167 r = float(_Dm(Y1, Y0, cosx))

168 else:

169 r = _naninf(a)

170 return r

171

172 @staticmethod

173 def integral2(sinx, cosx, siny, cosy, F, *N): # PYCHOK no cover

174 '''Compute the definite integral of Fourier sum given the

175 sine and cosine of the angles at the end points, using

176 precision I{Clenshaw} summation.

177

178 @arg sinx: The sin(I{sigma1}) (C{float}).

179 @arg cosx: The cos(I{sigma1}) (C{float}).

180 @arg siny: The sin(I{sigma2}) (C{float}).

181 @arg cosy: The cos(I{sigma2}) (C{float}).

182 @arg F: The Fourier coefficients (C{float}[]).

183 @arg N: Optional, C{len(B{F})} or a (smaller) number of

184 terms to evaluate (C{int}).

185

186 @return: Precison I{Clenshaw} integral (C{float}).

187

188 @see: Methods C{AuxDST.evaluate} and C{AuxDST.integral}.

189 '''

190 # 2 * cos(y - x) * cos(y + x) -> 2 * cos(2 * x)

191 c = _2cos2x(cosy * cosx, siny * sinx)

192 # -2 * sin(y - x) * sin(y + x) -> 0

193 s = -_2cos2x(siny * cosx, cosy * sinx)

194 if isfinite(c) and isfinite(s):

195 Y0, Y1 = Fsum(), Fsum()

196 Z0, Z1 = Fsum(), Fsum()

197 for r in _reverscaled(F, *N):

198 Y1 -= Y0 * c + Z0 * s + r

199 Z1 -= Y0 * s + Z0 * c

200 Y1, Y0 = Y0, -Y1

201 Z1, Z0 = Z0, -Z1

202 r = float(_Dm(Y1, Y0, cosy - cosx) +

203 _Dm(Z1, Z0, cosy + cosx))

204 else:

205 r = _naninf(c, s)

206 return r

207

208 @property_RO

209 def N(self):

210 '''Get this DST's size, number of points (C{int}).

211 '''

212 return self._N

213

214 def refine(self, f, F, *sentinel):

215 '''Refine the Fourier series by doubling the sampled points.

216

217 @arg f: Single-argument callable (C{B{f}(sigma)}).

218 @arg F: Initial Fourier series coefficients (C{float}[:N]).

219 @arg sentinel: Optional coefficient(s) to append (C{float}(s)).

220

221 @return: Fourier series coefficients (C{float}[:N*2]).

222

223 @note: Any initial C{B{F}[N:]} sentinel coefficients are ignored.

224 '''

225 def _data(_f, N): # [:N]

226 if N > 0:

227 r = PI_4 / N

228 for j in range(1, N*2, 2):

229 yield _f(r * j)

230

231 # F = F[:self.N] handled by zip strict=False in ._ffts2 above

232 return self._ffts2(_data(f, self.N), F) + sentinel

233

234 def reset(self, N):

235 '''Reset this DST.

236

237 @arg N: Size, number of points (C{int}).

238

239 @return: The new size (C{int}, non-negative).

240 '''

241 self._N = N = max(0, N)

242 # kissfft.assign(N*2, False) # "reset" size, inverse

243 return N

244

245 def transform(self, f):

246 '''Determine C{[N + 1]} terms in the Fourier series.

247

248 @arg f: Single-argument callable (C{B{f}(sigma)}).

249

250 @return: Fourier series coefficients (C{float}[:N+1],

251 leading 0).

252 '''

253 def _data(_f, N): # [:N + 1]

254 yield _0_0 # data[0] = 0

255 if N > 0:

256 r = PI_2 / N

257 for i in range(1, N + 1):

258 yield _f(r * i)

259

260 return self._ffts(_data(f, self.N), False)

261

262

263 def _len_N(F, *N):

264 # Adjusted C{len(B{F})}.

265 return min(len(F), *N) if N else len(F)

266

267

268 def _reverscaled(F, *N):

269 # Yield F[:N], reversed and scaled

270 for n in _reverange(_len_N(F, *N)):

271 yield F[n] / float(n * 2 + 1)

272

273

274__all__ += _ALL_DOCS(AuxDST)

275

276# **) MIT License

277#

278# Copyright (C) 2023-2024 -- mrJean1 at Gmail -- All Rights Reserved.

279#

280# Permission is hereby granted, free of charge, to any person obtaining a

281# copy of this software and associated documentation files (the "Software"),

282# to deal in the Software without restriction, including without limitation

283# the rights to use, copy, modify, merge, publish, distribute, sublicense,

284# and/or sell copies of the Software, and to permit persons to whom the

285# Software is furnished to do so, subject to the following conditions:

286#

287# The above copyright notice and this permission notice shall be included

288# in all copies or substantial portions of the Software.

289#

290# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

291# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

292# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

293# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

294# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

295# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

296# OTHER DEALINGS IN THE SOFTWARE.