Coverage for pygeodesy/auxilats/auxDST.py: 97%
98 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
2# -*- coding: utf-8 -*-
4u'''Discrete Sine Transforms (AuxDST) in Python, transcoded from I{Karney}'s C++ class
5U{DST<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1DST.html>}
6in I{GeographicLib version 2.2+}.
8Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2022-2024) and licensed
9under the MIT/X11 License. For more information, see the U{GeographicLib
10<https://GeographicLib.SourceForge.io>} documentation.
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
16from __future__ import division as _; del _ # PYCHOK semicolon
18from pygeodesy.auxilats.auxily import _Dm
19from pygeodesy.basics import isodd, neg, _reverange, _xnumpy
20from pygeodesy.constants import PI_2, PI_4, isfinite, _0_0, _0_5, _naninf
21from pygeodesy.fsums import Fsum as _Fsum
22from pygeodesy.karney import _2cos2x, _ALL_DOCS
23# from pygeodesy.lazily import _ALL_DOCS # from .karney
24from pygeodesy.props import property_RO, property_ROver
26__all__ = ()
27__version__ = '24.08.13'
30class AuxDST(object):
31 '''Discrete Sine Transforms (DST) for I{Auxiliary} latitudes.
33 @see: I{Karney}'s C++ class U{DST
34 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1DST.html>}.
35 '''
36 _N = 0
38 def __init__(self, N):
39 '''New L{AuxDST} instance.
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
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.
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}).
57 @return: Precison I{Clenshaw} sum (C{float}).
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
77 @property_ROver
78 def _fft_numpy(self):
79 '''(INTERNAL) Get the C{numpy.fft} module, I{once}.
80 '''
81 return _xnumpy(AuxDST, 1, 16).fft # overwrite property_ROver
83 def _fft_real(self, data):
84 '''(INTERNAL) NumPy's I{kissfft}-like C{transform_real} function,
85 taking C{float}[:N] B{C{data}} and returning C{complex}[:N*2].
86 '''
87 # <https://GitHub.com/mborgerding/kissfft/blob/master/test/testkiss.py>
88 return self._fft_numpy.rfftn(data)
90 def _ffts(self, data, cIV):
91 '''(INTERNAL) Compute the DST-III or DST-IV FFTransforms.
93 @arg data: Elements DST-III[0:N+1] or DST-IV[0:N] (C{float}[])
94 with DST_III[0] = 0.
95 @arg cIV: If C{True}, DST-IV, otherwise DST-III.
97 @return: FFTransforms (C{float}[0:N]).
98 '''
99 T, N = (), self.N
100 if N > 0:
101 N2 = N * 2
102 d = tuple(data)
103 # assert len(d) == N + (0 if cIV else 1)
105 if cIV: # DST-IV
106 from cmath import exp as _cexp
108 def _cF(c, j, r=-PI_4 / N):
109 return c * _cexp(complex(0, r * j))
111 i = 0
112 else: # DST-III
113 i = 1
114 # assert d[0] == _0_0
116 def _cF(c, *unused): # PYCHOK redef
117 return c
119 d += tuple(reversed(d[i:N])) # i == len(d) - N
120 d += tuple(map(neg, d[:N2]))
121 c = self._fft_real(d) # complex[0:N*2]
122 n2 = float(-N2)
123 T = tuple(_cF(c[j], j).imag / n2 for j in range(1, N2, 2))
124 return T
126 def _ffts2(self, data, F):
127 '''(INTERNAL) Doubled FFTransforms.
129 @arg data: Grid centers (C{float}[N]).
130 @arg F: The transforms (C{float}[N])
132 @return: Doubled FFTransforms (C{float}[N*2]).
133 '''
134 __2 = _0_5 # N = self._N
135 # copy DST-IV order N transform to D[0:N]
136 D = self._ffts(data, True)
137 # assert len(D) == N and len(F) >= N
138 # (DST-IV order N - DST-III order N) / 2
139 M = tuple((d - f) * __2 for d, f in zip(D, F)) # strict=False
140 # (DST-IV order N + DST-III order N) / 2
141 P = tuple((d + f) * __2 for d, f in zip(D, F)) # strict=False
142 # assert len(M) == len(P) == self._N
143 return P + tuple(reversed(M))
145 @staticmethod
146 def integral(sinx, cosx, F, *N):
147 '''Evaluate the integral of Fourier sum given the sine and
148 cosine of the angle, using precision I{Clenshaw} summation.
150 @arg sinx: The sin(I{sigma}) (C{float}).
151 @arg cosx: The cos(I{sigma}) (C{float}).
152 @arg F: The Fourier coefficients (C{float}[]).
153 @arg N: Optional, C{len(B{F})} or a (smaller) number of
154 terms to evaluate (C{int}).
156 @return: Precison I{Clenshaw} intergral (C{float}).
158 @see: Methods C{AuxDST.evaluate} and C{AuxDST.integral2}.
159 '''
160 a = _2cos2x(cosx - sinx, cosx + sinx)
161 if isfinite(a):
162 Y0, Y1 = _Fsum(), _Fsum()
163 for r in _reverscaled(F, *N):
164 Y1 -= Y0 * a + r
165 Y1, Y0 = Y0, -Y1
166 r = float(_Dm(Y1, Y0, cosx))
167 else:
168 r = _naninf(a)
169 return r
171 @staticmethod
172 def integral2(sinx, cosx, siny, cosy, F, *N): # PYCHOK no cover
173 '''Compute the definite integral of Fourier sum given the
174 sine and cosine of the angles at the end points, using
175 precision I{Clenshaw} summation.
177 @arg sinx: The sin(I{sigma1}) (C{float}).
178 @arg cosx: The cos(I{sigma1}) (C{float}).
179 @arg siny: The sin(I{sigma2}) (C{float}).
180 @arg cosy: The cos(I{sigma2}) (C{float}).
181 @arg F: The Fourier coefficients (C{float}[]).
182 @arg N: Optional, C{len(B{F})} or a (smaller) number of
183 terms to evaluate (C{int}).
185 @return: Precison I{Clenshaw} integral (C{float}).
187 @see: Methods C{AuxDST.evaluate} and C{AuxDST.integral}.
188 '''
189 # 2 * cos(y - x) * cos(y + x) -> 2 * cos(2 * x)
190 c = _2cos2x(cosy * cosx, siny * sinx)
191 # -2 * sin(y - x) * sin(y + x) -> 0
192 s = -_2cos2x(siny * cosx, cosy * sinx)
193 if isfinite(c) and isfinite(s):
194 Y0, Y1 = _Fsum(), _Fsum()
195 Z0, Z1 = _Fsum(), _Fsum()
196 for r in _reverscaled(F, *N):
197 Y1 -= Y0 * c + Z0 * s + r
198 Z1 -= Y0 * s + Z0 * c
199 Y1, Y0 = Y0, -Y1
200 Z1, Z0 = Z0, -Z1
201 r = float(_Dm(Y1, Y0, cosy - cosx) +
202 _Dm(Z1, Z0, cosy + cosx))
203 else:
204 r = _naninf(c, s)
205 return r
207 @property_RO
208 def N(self):
209 '''Get this DST's size, number of points (C{int}).
210 '''
211 return self._N
213 def refine(self, f, F, *sentinel):
214 '''Refine the Fourier series by doubling the sampled points.
216 @arg f: Single-argument callable (C{B{f}(sigma)}).
217 @arg F: Initial Fourier series coefficients (C{float}[:N]).
218 @arg sentinel: Optional coefficient(s) to append (C{float}(s)).
220 @return: Fourier series coefficients (C{float}[:N*2]).
222 @note: Any initial C{B{F}[N:]} sentinel coefficients are ignored.
223 '''
224 def _data(_f, N): # [:N]
225 if N > 0:
226 r = PI_4 / N
227 for j in range(1, N*2, 2):
228 yield _f(r * j)
230 # F = F[:self.N] handled by zip strict=False in ._ffts2 above
231 return self._ffts2(_data(f, self.N), F) + sentinel
233 def reset(self, N):
234 '''Reset this DST.
236 @arg N: Size, number of points (C{int}).
238 @return: The new size (C{int}, non-negative).
239 '''
240 self._N = N = max(0, N)
241 # kissfft.assign(N*2, False) # "reset" size, inverse
242 return N
244 def transform(self, f):
245 '''Determine C{[N + 1]} terms in the Fourier series.
247 @arg f: Single-argument callable (C{B{f}(sigma)}).
249 @return: Fourier series coefficients (C{float}[:N+1],
250 leading 0).
251 '''
252 def _data(_f, N): # [:N + 1]
253 yield _0_0 # data[0] = 0
254 if N > 0:
255 r = PI_2 / N
256 for i in range(1, N + 1):
257 yield _f(r * i)
259 return self._ffts(_data(f, self.N), False)
262def _len_N(F, *N):
263 # Adjusted C{len(B{F})}.
264 return min(len(F), *N) if N else len(F)
267def _reverscaled(F, *N):
268 # Yield F[:N], reversed and scaled
269 for n in _reverange(_len_N(F, *N)):
270 yield F[n] / float(n * 2 + 1)
273__all__ += _ALL_DOCS(AuxDST)
275# **) MIT License
276#
277# Copyright (C) 2023-2025 -- mrJean1 at Gmail -- All Rights Reserved.
278#
279# Permission is hereby granted, free of charge, to any person obtaining a
280# copy of this software and associated documentation files (the "Software"),
281# to deal in the Software without restriction, including without limitation
282# the rights to use, copy, modify, merge, publish, distribute, sublicense,
283# and/or sell copies of the Software, and to permit persons to whom the
284# Software is furnished to do so, subject to the following conditions:
285#
286# The above copyright notice and this permission notice shall be included
287# in all copies or substantial portions of the Software.
288#
289# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
290# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
291# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
292# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
293# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
294# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
295# OTHER DEALINGS IN THE SOFTWARE.