Example #1
0
    def _reset(self, e, e2):
        '''(INTERNAL) Get elliptic functions and pre-compute some
           frequently used values.

           @arg e: Eccentricity (C{float}).
           @arg e2: Eccentricity squared (C{float}).

           @raise EllipticError: No convergence.
        '''
        # assert e2 == e**2
        self._e          = e
        self._e_PI_2     = e *  PI_2
        self._e_PI_4     = e *  PI_4
        self._e_TAYTOL   = e * _TAYTOL

        self._1_e_90     = (1 - e) * 90
        self._1_e_PI_2   = (1 - e) * PI_2
        self._1_e2_PI_2  = (1 - e * 2) * PI_2

        self._mu         =  e2
        self._mu_2_1     = (e2 + 2) * 0.5

        self._Eu         = Elliptic(self._mu)
        self._Eu_cE_1_4  = self._Eu.cE * 0.25
        self._Eu_cK_cE   = self._Eu.cK / self._Eu.cE
        self._Eu_cK_PI_2 = self._Eu.cK / PI_2

        self._mv         = 1 - e2
        self._3_mv       = 3.0 / self._mv
        self._3_mv_e     = self._3_mv / e

        self._Ev         = Elliptic(self._mv)
        self._Ev_cKE_3_4 = self._Ev.cKE * 0.75
        self._Ev_cKE_5_4 = self._Ev.cKE * 1.25
Example #2
0
    def _reset(self, E):
        '''(INTERNAL) Get elliptic functions and pre-compute
           some frequently used values.

           @arg E: An ellipsoid (L{Ellipsoid}).

           @raise EllipticError: No convergence.
        '''
        e, e2 = E.e, E.e2
        # assert e2 == e**2 != _0_0

        self._e_PI_2 = e * PI_2
        self._e_PI_4 = e * PI_4
        self._e_TAYTOL = e * _TAYTOL

        self._1_e_90 = (_1_0 - e) * _90_0
        self._1_e_PI_2 = (_1_0 - e) * PI_2
        self._1_e2_PI_2 = (_1_0 - e * 2) * PI_2

        self._mu = e2
        self._mu_2_1 = (e2 + _2_0) * _0_5

        self._Eu = Elliptic(self._mu)
        self._Eu_cE_1_4 = self._Eu.cE * 0.25
        self._Eu_cK_cE = self._Eu.cK / self._Eu.cE
        self._Eu_cK_PI_2 = self._Eu.cK / PI_2

        self._mv = _1_0 - e2
        self._3_mv = _3_0 / self._mv
        self._3_mv_e = self._3_mv / e

        self._Ev = Elliptic(self._mv)
        self._Ev_cKE_3_4 = self._Ev.cKE * 0.75
        self._Ev_cKE_5_4 = self._Ev.cKE * 1.25

        self._iteration = 0

        self._E = E
        self._k0_a = E.a * self.k0  # see .k0 setter
Example #3
0
    def _reset(self, e, e2):
        '''(INTERNAL) Get elliptic functions and pre-compute
           frequently used values.

           @raise EllipticError: No convergence.
        '''
        # assert e2 == e**2
        self._e = e  # eccentricity = sqrt(f * (2 - f))

        self._e_PI_2    = e *  PI_2
        self._e_PI_4    = e *  PI_4
        self._e_taytol_ = e * _TAYTOL

        self._1_e_90    = (1 - e) * 90
        self._1_e_PI_2  = (1 - e) * PI_2
        self._1_e2_PI_2 = (1 - e * 2) * PI_2

        self._mu     =  e2   # eccentricity**2 = f * (2 - f) = 1 - (b / a)**2
        self._mu_2_1 = (e2 + 2) * 0.5

        self._Eu       = Elliptic(self._mu)
        self._Eu_E     = self._Eu.cE  # constant
        self._Eu_E_1_4 = self._Eu_E * 0.25

        self._Eu_K = self._Eu.cK  # constant

        self._mv     = 1 - e2  # 1 - eccentricity**2 = 1 - e2
        self._3_mv   = 3.0 / self._mv
        self._3_mv_e = self._3_mv / e

        self._Ev   = Elliptic(self._mv)
        self._Ev_E = self._Ev.cE  # constant

        self._Ev_K      = self._Ev.cK   # constant
        self._Ev_KE     = self._Ev.cKE  # constant
        self._Ev_KE_3_4 = self._Ev_KE * 0.75
        self._Ev_KE_5_4 = self._Ev_KE * 1.25
Example #4
0
class ExactTransverseMercator(_NamedBase):
    '''A Python version of Karney's U{TransverseMercatorExact
       <https://GeographicLib.SourceForge.io/html/TransverseMercatorExact_8cpp_source.html>}
       C++ class, a numerically exact transverse mercator projection,
       referred to as C{TMExact} here.

       @see: C{TMExact(real a, real f, real k0, bool extendp)}.
    '''
    _a       = 0     # major radius
    _datum   = None  # Datum
    _e       = 0     # eccentricity
    _E       = None  # Ellipsoid
    _extendp = False
    _f       = 0     # flattening
    _k0      = 1     # central scale factor
    _k0_a    = 0
    _lon0    = 0     # central meridian
    _trips_  = _TRIPS

    def __init__(self, datum=Datums.WGS84, lon0=0, k0=_K0, extendp=True, name=''):
        '''New L{ExactTransverseMercator} projection.

           @keyword datum: The datum and ellipsoid to use (C{Datum}).
           @keyword lon0: The central meridian (C{degrees180}).
           @keyword k0: The central scale factor (C{float}).
           @keyword extendp: Use the extended domain (C{bool}).
           @keyword name: Optional name for the projection (C{str}).

           @raise EllipticError: No convergence.

           @raise ETMError: Invalid B{C{k0}}.

           @raise TypeError: Invalid B{C{datum}}.

           @note: The maximum error for all 255.5K U{TMcoords.dat
                  <https://Zenodo.org/record/32470>} tests (with
                  C{0 <= lat <= 84} and C{0 <= lon}) is C{5.2e-08
                  .forward} or 52 nano-meter easting and northing
                  and C{3.8e-13 .reverse} or 0.38 pico-degrees lat-
                  and longitude (with Python 3.7.3, 2.7.16, PyPy6
                  3.5.3 and PyPy6 2.7.13, all in 64-bit on macOS
                  10.13.6 High Sierra).
        '''
        if extendp:
            self._extendp = bool(extendp)
        if name:
            self.name = name

        self.datum = datum
        self.lon0  = lon0
        self.k0    = k0

    @property
    def datum(self):
        '''Get the datum (L{Datum}) or C{None}.
        '''
        return self._datum

    @datum.setter  # PYCHOK setter!
    def datum(self, datum):
        '''Set the datum and ellipsoid (L{Datum}).

           @raise EllipticError: No convergence.

           @raise TypeError: Invalid B{C{datum}}.
        '''
        _TypeError(Datum, datum=datum)

        E = datum.ellipsoid
        self._reset(E.e, E.e2)
        self._a = E.a
        self._f = E.f  # flattening = (a - b) / a

        self._datum = datum
        self._E     = E

    @property_RO
    def extendp(self):
        '''Get using the extended domain (C{bool}).
        '''
        return self._extendp

    @property_RO
    def flattening(self):
        '''Get the flattening (C{float}).
        '''
        return self._f

    def forward(self, lat, lon, lon0=None):  # MCCABE 13
        '''Forward projection, from geographic to transverse Mercator.

           @param lat: Latitude of point (C{degrees}).
           @param lon: Longitude of point (C{degrees}).
           @keyword lon0: Central meridian of the projection (C{degrees}).

           @return: L{EasNorExact4Tuple}C{(easting, northing,
                    convergence, scale)} in C{meter}, C{meter},
                    C{degrees} and C{scalar}.

           @see: C{void TMExact::Forward(real lon0, real lat, real lon,
                                         real &x, real &y,
                                         real &gamma, real &k)}.

           @raise EllipticError: No convergence.
        '''
        lat = _fix90(lat)
        lon, _ = _diff182((self._lon0 if lon0 is None else lon0), lon)
        # Explicitly enforce the parity
        _lat = _lon = backside = False
        if not self.extendp:
            if lat < 0:
                _lat, lat = True, -lat
            if lon < 0:
                _lon, lon = True, -lon
            if lon > 90:
                backside = True
                if lat == 0:
                    _lat = True
                lon = 180 - lon

        # u,v = coordinates for the Thompson TM, Lee 54
        if lat == 90:
            u, v = self._Eu_K, 0
        elif lat == 0 and lon == self._1_e_90:
            u, v = 0, self._Ev_K
        else:  # tau = tan(phi), taup = sinh(psi)
            tau, lam = tan(radians(lat)), radians(lon)
            u, v = self._zetaInv(self._E.es_taupf(tau), lam)

        snu, cnu, dnu = self._Eu.sncndn(u)
        snv, cnv, dnv = self._Ev.sncndn(v)
        xi, eta, _ = self._sigma3(v, snu, cnu, dnu, snv, cnv, dnv)
        if backside:
            xi = 2 * self._Eu_E - xi
        y = xi  * self._k0_a
        x = eta * self._k0_a

        if lat == 90:
            g, k = lon, self._k0
        else:  # Recompute (T, L) from (u, v) to improve accuracy of Scale
            tau, lam, d = self._zeta3(  snu, cnu, dnu, snv, cnv, dnv)
            tau = self._E.es_tauf(tau)
            g, k = self._scaled(tau, d, snu, cnu, dnu, snv, cnv, dnv)

        if backside:
            g = 180 - g
        if _lat:
            y, g = -y, -g
        if _lon:
            x, g = -x, -g
        return EasNorExact4Tuple(x, y, g, k)

    @property
    def k0(self):
        '''Get the central scale factor (C{float}), aka I{C{scale0}}.
        '''
        return self._k0  # aka scale0

    @k0.setter  # PYCHOK setter!
    def k0(self, k0):
        '''Set the central scale factor (C{float}), aka I{C{scale0}}.

           @raise EllipticError: Invalid B{C{k0}}.
        '''
        self._k0 = float(k0)
        if not 0 < self._k0 <= 1:
            raise ETMError('%s invalid: %r' % ('k0', k0))
        self._k0_a = self._k0 * self._a

    @property
    def lon0(self):
        '''Get the central meridian (C{degrees180}).
        '''
        return self._lon0

    @lon0.setter  # PYCHOK setter!
    def lon0(self, lon0):
        '''Set the central meridian (C{degrees180}).
        '''
        self._lon0 = _wrap180(lon0)

    @property_RO
    def majoradius(self):
        '''Get the major (equatorial) radius, semi-axis (C{float}).
        '''
        return self._a

    def _reset(self, e, e2):
        '''(INTERNAL) Get elliptic functions and pre-compute
           frequently used values.

           @raise EllipticError: No convergence.
        '''
        # assert e2 == e**2
        self._e = e  # eccentricity = sqrt(f * (2 - f))

        self._e_PI_2    = e *  PI_2
        self._e_PI_4    = e *  PI_4
        self._e_taytol_ = e * _TAYTOL

        self._1_e_90    = (1 - e) * 90
        self._1_e_PI_2  = (1 - e) * PI_2
        self._1_e2_PI_2 = (1 - e * 2) * PI_2

        self._mu     =  e2   # eccentricity**2 = f * (2 - f) = 1 - (b / a)**2
        self._mu_2_1 = (e2 + 2) * 0.5

        self._Eu       = Elliptic(self._mu)
        self._Eu_E     = self._Eu.cE  # constant
        self._Eu_E_1_4 = self._Eu_E * 0.25

        self._Eu_K = self._Eu.cK  # constant

        self._mv     = 1 - e2  # 1 - eccentricity**2 = 1 - e2
        self._3_mv   = 3.0 / self._mv
        self._3_mv_e = self._3_mv / e

        self._Ev   = Elliptic(self._mv)
        self._Ev_E = self._Ev.cE  # constant

        self._Ev_K      = self._Ev.cK   # constant
        self._Ev_KE     = self._Ev.cKE  # constant
        self._Ev_KE_3_4 = self._Ev_KE * 0.75
        self._Ev_KE_5_4 = self._Ev_KE * 1.25

    def reverse(self, x, y, lon0=None):
        '''Reverse projection, from transverse Mercator to geographic.

           @param x: Easting of point (C{meters}).
           @param y: Northing of point (C{meters}).
           @keyword lon0: Central meridian of the projection (C{degrees}).

           @return: L{LatLonExact4Tuple}C{(lat, lon, convergence, scale)}
                    in C{degrees}, C{degrees180}, C{degrees} and C{scalar}.

           @see: C{void TMExact::Reverse(real lon0, real x, real y,
                                         real &lat, real &lon,
                                         real &gamma, real &k)}

           @raise EllipticError: No convergence.
        '''
        # undoes the steps in .forward.
        xi  = y / self._k0_a
        eta = x / self._k0_a
        _lat = _lon = backside = False
        if not self.extendp:  # enforce the parity
            if y < 0:
                _lat, xi  = True, -xi
            if x < 0:
                _lon, eta = True, -eta
            if xi > self._Eu_E:
                backside = True
                xi = 2 * self._Eu_E - xi

        # u,v = coordinates for the Thompson TM, Lee 54
        if xi == 0 and eta == self._Ev_KE:
            u, v = 0, self._Ev_K
        else:
            u, v = self._sigmaInv(xi, eta)

        if v != 0 or u != self._Eu_K:
            snu, cnu, dnu = self._Eu.sncndn(u)
            snv, cnv, dnv = self._Ev.sncndn(v)
            tau, lam, d = self._zeta3(  snu, cnu, dnu, snv, cnv, dnv)
            tau = self._E.es_tauf(tau)
            lat, lon = degrees(atan(tau)), degrees(lam)
            g, k = self._scaled(tau, d, snu, cnu, dnu, snv, cnv, dnv)
        else:
            lat, lon = 90, 0
            g, k = 0, self._k0

        if backside:
            lon, g = 180 - lon, 180 - g
        if _lat:
            lat, g = -lat, -g
        if _lon:
            lon, g = -lon, -g

        lat = _wrap180(lat)
        lon = _wrap180(lon + (self._lon0 if lon0 is None else _wrap180(lon0)))
        return LatLonExact4Tuple(lat, lon, g, k)

    def _scaled(self, tau, d2, snu, cnu, dnu, snv, cnv, dnv):
        '''
           @note: Argument B{C{d2}} is C{_mu * cnu**2 + _mv * cnv**2}
                  from C{._sigma3} or C{._zeta3}.

           @return: 2-Tuple C{(convergence, scale)}.

           @see: C{void TMExact::Scale(real tau, real /*lam*/,
                                       real snu, real cnu, real dnu,
                                       real snv, real cnv, real dnv,
                                       real &gamma, real &k)}.
        '''
        mu, mv = self._mu, self._mv
        cnudnv = cnu * dnv
        # Lee 55.12 -- negated for our sign convention.  g gives
        # the bearing (clockwise from true north) of grid north
        g = atan2(mv * cnv * snv * snu, cnudnv * dnu)
        # Lee 55.13 with nu given by Lee 9.1 -- in sqrt change
        # the numerator from
        #
        #  (1 - snu^2 * dnv^2) to (_mv * snv^2 + cnu^2 * dnv^2)
        #
        # to maintain accuracy near phi = 90 and change the
        # denomintor from
        #  (dnu^2 + dnv^2 - 1) to (_mu * cnu^2 + _mv * cnv^2)
        #
        # to maintain accuracy near phi = 0, lam = 90 * (1 - e).
        # Similarly rewrite sqrt term in 9.1 as
        #
        #  _mv + _mu * c^2 instead of 1 - _mu * sin(phi)^2
        sec2 = 1 + tau**2  # sec(phi)^2
        q2 = (mv * snv**2 + cnudnv**2) / d2
        k = sqrt(mv + mu / sec2) * sqrt(sec2) * sqrt(q2)
        return degrees(g), k * self._k0

    def _sigma3(self, v, snu, cnu, dnu, snv, cnv, dnv):  # PYCHOK unused
        '''
           @return: 3-Tuple C{(xi, eta, d2)}.

           @see: C{void TMExact::sigma(real /*u*/, real snu, real cnu, real dnu,
                                       real   v,   real snv, real cnv, real dnv,
                                       real &xi, real &eta)}.

           @raise EllipticError: No convergence.
        '''
        # Lee 55.4 writing
        # dnu^2 + dnv^2 - 1 = _mu * cnu^2 + _mv * cnv^2
        d2 = self._mu * cnu**2 + self._mv * cnv**2
        xi =      self._Eu.fE(snu, cnu, dnu) - self._mu * snu * cnu * dnu / d2
        eta = v - self._Ev.fE(snv, cnv, dnv) + self._mv * snv * cnv * dnv / d2
        return xi, eta, d2

    def _sigmaDwd(self, snu, cnu, dnu, snv, cnv, dnv):
        '''
           @return: 2-Tuple C{(du, dv)}.

           @see: C{void TMExact::dwdsigma(real /*u*/, real snu, real cnu, real dnu,
                                          real /*v*/, real snv, real cnv, real dnv,
                                          real &du, real &dv)}.
        '''
        snuv = snu * snv
        # Reciprocal of 55.9: dw / ds = dn(w)^2/_mv,
        # expanding complex dn(w) using A+S 16.21.4
        d = self._mv * (cnv**2 + self._mu * snuv**2)**2
        r =  cnv * dnu * dnv
        i = -cnu * snuv * self._mu
        du = (r**2 - i**2) / d
        dv = 2 * r * i / d
        return du, dv

    def _sigmaInv(self, xi, eta):
        '''Invert C{sigma} using Newton's method.

           @return: 2-Tuple C{(u, v)}.

           @see: C{void TMExact::sigmainv(real xi, real eta,
                                          real &u, real &v)}.

           @raise EllipticError: No convergence.
        '''
        u, v, trip = self._sigmaInv0(xi, eta)
        if not trip:
            U, V = Fsum(u), Fsum(v)
            # min iterations = 2, max = 7, mean = 3.9
            for _ in range(self._trips_):  # GEOGRAPHICLIB_PANIC
                snu, cnu, dnu = self._Eu.sncndn(u)
                snv, cnv, dnv = self._Ev.sncndn(v)
                X, E, _ = self._sigma3(v, snu, cnu, dnu, snv, cnv, dnv)
                dw, dv  = self._sigmaDwd( snu, cnu, dnu, snv, cnv, dnv)
                X  = xi - X
                E -= eta
                u, du = U.fsum2_(X * dw,  E * dv)
                v, dv = V.fsum2_(X * dv, -E * dw)
                if trip:
                    break
                trip = (du**2 + dv**2) < _TOL_10
            else:
                raise EllipticError('no %s convergence' % ('sigmaInv',))
        return u, v

    def _sigmaInv0(self, xi, eta):
        '''Starting point for C{sigmaInv}.

           @return: 3-Tuple C{(u, v, trip)}.

           @see: C{bool TMExact::sigmainv0(real xi, real eta,
                                           real &u, real &v)}.
        '''
        trip = False
        if eta > self._Ev_KE_5_4 or xi < min(- self._Eu_E_1_4,
                                         eta - self._Ev_KE):
            # sigma as a simple pole at
            #  w = w0 = Eu.K() + i * Ev.K()
            # and sigma is approximated by
            #  sigma = (Eu.E() + i * Ev.KE()) + 1/(w - w0)
            x = xi  - self._Eu_E
            y = eta - self._Ev_KE
            d = x**2 + y**2
            u = self._Eu_K + x / d
            v = self._Ev_K - y / d

        elif eta > self._Ev_KE or (eta > self._Ev_KE_3_4 and
                                    xi < self._Eu_E_1_4):
            # At w = w0 = i * Ev.K(), we have
            #  sigma  = sigma0  = i * Ev.KE()
            #  sigma' = sigma'' = 0
            #
            # including the next term in the Taylor series gives:
            #  sigma = sigma0 - _mv / 3 * (w - w0)^3
            #
            # When inverting this, we map arg(w - w0) = [-pi/2, -pi/6]
            # to arg(sigma - sigma0) = [-pi/2, pi/2]
            # mapping arg = [-pi/2, -pi/6] to [-pi/2, pi/2]

            d = eta - self._Ev_KE
            r = hypot(xi, d)
            # Error using this guess is about 0.068 * rad^(5/3)
            trip = r < _TAYTOL2
            # Map the range [-90, 180] in sigma space to [-90, 0] in
            # w space.  See discussion in zetainv0 on the cut for ang.
            r = cbrt(r * self._3_mv)
            a = atan2(d - xi, xi + d) / 3.0 - PI_4
            s, c = sincos2(a)
            u = r * c
            v = r * s + self._Ev_K

        else:  # use w = sigma * Eu.K/Eu.E (correct in the limit _e -> 0)
            r = self._Eu_K / self._Eu_E
            u = xi  * r
            v = eta * r
        return u, v, trip

    def _zeta3(self, snu, cnu, dnu, snv, cnv, dnv):
        '''
           @return: 3-Tuple C{(taup, lambda, d2)}.

           @see: C{void TMExact::zeta(real /*u*/, real snu, real cnu, real dnu,
                                      real /*v*/, real snv, real cnv, real dnv,
                                      real &taup, real &lam)}
        '''
        e = self._e
        # Lee 54.17 but write
        # atanh(snu * dnv)      = asinh(snu * dnv / sqrt(cnu^2 + _mv * snu^2 * snv^2))
        # atanh(_e * snu / dnv) = asinh(_e * snu / sqrt(_mu * cnu^2 + _mv * cnv^2))
        d1 = cnu**2 + self._mv * (snu * snv)**2
        d2 = self._mu * cnu**2 + self._mv * cnv**2
        # Overflow value s.t. atan(overflow) = pi/2
        t1 = t2 = copysign(_OVERFLOW, snu)
        if d1 > 0:
            t1 = snu * dnv / sqrt(d1)
        if d2 > 0:
            t2 = sinh(e * asinh(e * snu / sqrt(d2)))
        # psi = asinh(t1) - asinh(t2)
        # taup = sinh(psi)
        taup = t1 * hypot1(t2) - t2 * hypot1(t1)
        lam  = (atan2(    dnu * snv, cnu * cnv) - e *
                atan2(e * cnu * snv, dnu * cnv)) if (d1 > 0 and
                                                     d2 > 0) else 0
        return taup, lam, d2

    def _zetaDwd(self, snu, cnu, dnu, snv, cnv, dnv):
        '''
           @return: 2-Tuple C{(du, dv)}.

           @see: C{void TMExact::dwdzeta(real /*u*/, real snu, real cnu, real dnu,
                                         real /*v*/, real snv, real cnv, real dnv,
                                         real &du, real &dv)}.
        '''
        cnu2  = cnu**2 * self._mu
        cnv2  = cnv**2
        dnuv  = dnu * dnv
        dnuv2 = dnuv**2
        snuv  = snu * snv
        snuv2 = snuv**2 * self._mu
        # Lee 54.21 but write
        # (1 - dnu^2 * snv^2) = (cnv^2 + _mu * snu^2 * snv^2)
        # (see A+S 16.21.4)
        d  =  self._mv   * (cnv2 + snuv2)**2
        du =  cnu * dnuv * (cnv2 - snuv2) / d
        dv = -cnv * snuv * (cnu2 + dnuv2) / d
        return du, dv

    def _zetaInv(self, taup, lam):
        '''Invert C{zeta} using Newton's method.

           @return: 2-Tuple C{(u, v)}.

           @see: C{void TMExact::zetainv(real taup, real lam,
                                         real &u, real &v)}.

           @raise EllipticError: No convergence.
        '''
        psi = asinh(taup)
        sca = 1.0 / hypot1(taup)
        u, v, trip = self._zetaInv0(psi, lam)
        if not trip:
            stol2 = _TOL_10 / max(psi, 1.0)**2
            U, V = Fsum(u), Fsum(v)
            # min iterations = 2, max = 6, mean = 4.0
            for _ in range(self._trips_):  # GEOGRAPHICLIB_PANIC
                snu, cnu, dnu = self._Eu.sncndn(u)
                snv, cnv, dnv = self._Ev.sncndn(v)
                T, L, _ = self._zeta3(  snu, cnu, dnu, snv, cnv, dnv)
                dw, dv  = self._zetaDwd(snu, cnu, dnu, snv, cnv, dnv)
                T  = (taup - T) * sca
                L -= lam
                u, du = U.fsum2_(T * dw,  L * dv)
                v, dv = V.fsum2_(T * dv, -L * dw)
                if trip:
                    break
                trip = (du**2 + dv**2) < stol2
            else:
                raise EllipticError('no %s convergence' % ('zetaInv',))
        return u, v

    def _zetaInv0(self, psi, lam):
        '''Starting point for C{zetaInv}.

           @return: 3-Tuple C{(u, v, trip)}.

           @see: C{bool TMExact::zetainv0(real psi, real lam,  # radians
                                          real &u, real &v)}.
        '''
        trip = False
        if (psi < -self._e_PI_4 and lam > self._1_e2_PI_2
                                and psi < lam - self._1_e_PI_2):
            # N.B. this branch is normally not taken because psi < 0
            # is converted psi > 0 by Forward.
            #
            # There's a log singularity at w = w0 = Eu.K() + i * Ev.K(),
            # corresponding to the south pole, where we have, approximately
            #
            #  psi = _e + i * pi/2 - _e * atanh(cos(i * (w - w0)/(1 + _mu/2)))
            #
            # Inverting this gives:
            h = sinh(1 - psi / self._e)
            a = (PI_2 - lam) / self._e
            s, c = sincos2(a)
            u = self._Eu_K - asinh(s / hypot(c, h)) * self._mu_2_1
            v = self._Ev_K - atan2(c, h) * self._mu_2_1

        elif (psi < self._e_PI_2 and lam > self._1_e2_PI_2):
            # At w = w0 = i * Ev.K(), we have
            #
            #  zeta  = zeta0  = i * (1 - _e) * pi/2
            #  zeta' = zeta'' = 0
            #
            # including the next term in the Taylor series gives:
            #
            #  zeta = zeta0 - (_mv * _e) / 3 * (w - w0)^3
            #
            # When inverting this, we map arg(w - w0) = [-90, 0] to
            # arg(zeta - zeta0) = [-90, 180]

            d = lam - self._1_e_PI_2
            r = hypot(psi, d)
            # Error using this guess is about 0.21 * (rad/e)^(5/3)
            trip = r < self._e_taytol_
            # atan2(dlam-psi, psi+dlam) + 45d gives arg(zeta - zeta0)
            # in range [-135, 225).  Subtracting 180 (since multiplier
            # is negative) makes range [-315, 45).  Multiplying by 1/3
            # (for cube root) gives range [-105, 15).  In particular
            # the range [-90, 180] in zeta space maps to [-90, 0] in
            # w space as required.
            r = cbrt(r * self._3_mv_e)
            a = atan2(d - psi, psi + d) / 3.0 - PI_4
            s, c = sincos2(a)
            u = r * c
            v = r * s + self._Ev_K

        else:
            # Use spherical TM, Lee 12.6 -- writing C{atanh(sin(lam) /
            # cosh(psi)) = asinh(sin(lam) / hypot(cos(lam), sinh(psi)))}.
            # This takes care of the log singularity at C{zeta = Eu.K()},
            # corresponding to the north pole.
            s, c = sincos2(lam)
            h, r = sinh(psi), self._Eu_K / PI_2
            # But scale to put 90, 0 on the right place
            u = r * atan2(h, c)
            v = r * asinh(s / hypot(c, h))
        return u, v, trip
Example #5
0
class ExactTransverseMercator(_NamedBase):
    '''A Python version of Karney's U{TransverseMercatorExact
       <https://GeographicLib.SourceForge.io/html/TransverseMercatorExact_8cpp_source.html>}
       C++ class, a numerically exact transverse mercator projection, here referred to as
       C{TMExact}.

       @see: C{U{TMExact(real a, real f, real k0, bool extendp)<https://GeographicLib.SourceForge.io/
             html/classGeographicLib_1_1TransverseMercatorExact.html#a72ffcc89eee6f30a6d1f4d061518a6f1>}}.
    '''
    _a         = 0     # major radius
    _datum     = None  # Datum
    _e         = 0     # eccentricity
    _E         = None  # Ellipsoid
    _extendp   = True
    _f         = 0     # flattening
    _iteration = 0     # ._sigmaInv and ._zetaInv
    _k0        = 1     # central scale factor
    _k0_a      = 0
    _lon0      = 0     # central meridian

#   see ._reset() below:

#   _e_PI_2     = _e *  PI_2
#   _e_PI_4     = _e *  PI_4
#   _e_TAYTOL   = _e * _TAYTOL

#   _1_e_90     = (1 - _e) * 90
#   _1_e_PI_2   = (1 - _e) * PI_2
#   _1_e2_PI_2  = (1 - _e * 2) * PI_2

#   _mu         =  _e**2
#   _mu_2_1     = (_e**2 + 2) * 0.5

#   _Eu         =  Elliptic(_mu)
#   _Eu_cE_1_4  = _Eu.cE * 0.25
#   _Eu_cK_cE   = _Eu.cK / _Eu.cE
#   _Eu_cK_PI_2 = _Eu.cK / PI_2

#   _mv         =  1   - _mu
#   _3_mv       =  3.0 / _mv
#   _3_mv_e     = _3_mv / _e

#   _Ev         =  Elliptic(_mv)
#   _Ev_cKE_3_4 = _Ev.cKE * 0.75
#   _Ev_cKE_5_4 = _Ev.cKE * 1.25

    def __init__(self, datum=Datums.WGS84, lon0=0, k0=_K0, extendp=True, name=NN):
        '''New L{ExactTransverseMercator} projection.

           @kwarg datum: The datum, ellipsoid to use (L{Datum},
                         L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}).
           @kwarg lon0: The central meridian (C{degrees180}).
           @kwarg k0: The central scale factor (C{float}).
           @kwarg extendp: Use the extended domain (C{bool}).
           @kwarg name: Optional name for the projection (C{str}).

           @raise EllipticError: No convergence.

           @raise ETMError: Invalid B{C{k0}}.

           @raise TypeError: Invalid B{C{datum}}.

           @raise ValueError: Invalid B{C{lon0}} or B{C{k0}}.

           @note: The maximum error for all 255.5K U{TMcoords.dat
                  <https://Zenodo.org/record/32470>} tests (with
                  C{0 <= lat <= 84} and C{0 <= lon}) is C{5.2e-08
                  .forward} or 52 nano-meter easting and northing
                  and C{3.8e-13 .reverse} or 0.38 pico-degrees lat-
                  and longitude (with Python 3.7.3, 2.7.16, PyPy6
                  3.5.3 and PyPy6 2.7.13, all in 64-bit on macOS
                  10.13.6 High Sierra).
        '''
        if not extendp:
            self._extendp = False
        if name:
            self.name = name

        self.datum = datum
        self.lon0  = lon0
        self.k0    = k0

    @property_doc_(''' the datum (L{Datum}).''')
    def datum(self):
        '''Get the datum (L{Datum}) or C{None}.
        '''
        return self._datum

    @datum.setter  # PYCHOK setter!
    def datum(self, datum):
        '''Set the datum and ellipsoid (L{Datum}, L{Ellipsoid},
           L{Ellipsoid2} or L{a_f2Tuple}).

           @raise EllipticError: No convergence.

           @raise TypeError: Invalid B{C{datum}}.
        '''
        d = _ellipsoidal_datum(datum, name=self.name)
        E = d.ellipsoid
        self._reset(E.e, E.e2)
        self._a = E.a
        self._f = E.f  # flattening = (a - b) / a

        self._datum = d
        self._E     = E

    @property_RO
    def equatoradius(self):
        '''Get the equatorial (major) radius, semi-axis (C{meter}).
        '''
        return self._a

    majoradius = equatoradius  # for backward compatibility
    '''DEPRECATED, use C{equatoradius}.'''

    @property_RO
    def extendp(self):
        '''Get using the extended domain (C{bool}).
        '''
        return self._extendp

    @property_RO
    def flattening(self):
        '''Get the flattening (C{float}).
        '''
        return self._f

    def forward(self, lat, lon, lon0=None):  # MCCABE 13
        '''Forward projection, from geographic to transverse Mercator.

           @arg lat: Latitude of point (C{degrees}).
           @arg lon: Longitude of point (C{degrees}).
           @kwarg lon0: Central meridian of the projection (C{degrees}).

           @return: L{EasNorExact4Tuple}C{(easting, northing,
                    convergence, scale)} in C{meter}, C{meter},
                    C{degrees} and C{scalar}.

           @see: C{void TMExact::Forward(real lon0, real lat, real lon,
                                         real &x, real &y,
                                         real &gamma, real &k)}.

           @raise EllipticError: No convergence.
        '''
        lat = _fix90(lat)
        lon, _ = _diff182((self._lon0 if lon0 is None else lon0), lon)
        # Explicitly enforce the parity
        backside = _lat = _lon = False
        if not self.extendp:
            if lat < 0:
                _lat, lat = True, -lat
            if lon < 0:
                _lon, lon = True, -lon
            if lon > 90:
                backside = True
                if lat == 0:
                    _lat = True
                lon = 180 - lon

        # u,v = coordinates for the Thompson TM, Lee 54
        if lat == 90:
            u = self._Eu.cK
            v = self._iteration = 0
        elif lat == 0 and lon == self._1_e_90:
            u = self._iteration = 0
            v = self._Ev.cK
        else:  # tau = tan(phi), taup = sinh(psi)
            tau, lam = tan(radians(lat)), radians(lon)
            u, v = self._zetaInv(self._E.es_taupf(tau), lam)

        sncndn6 = self._sncndn6(u, v)
        xi, eta, _ = self._sigma3(v, *sncndn6)
        if backside:
            xi = 2 * self._Eu.cE - xi
        y = xi  * self._k0_a
        x = eta * self._k0_a

        if lat == 90:
            g, k = lon, self._k0
        else:
            g, k = self._zetaScaled(sncndn6, ll=False)

        if backside:
            g = 180 - g
        if _lat:
            y, g = -y, -g
        if _lon:
            x, g = -x, -g

        r = EasNorExact4Tuple(x, y, g, k)
        r._iteration = self._iteration
        return r

    @property_RO
    def iteration(self):
        '''Get the most recent C{ExactTransverseMercator.forward}
           or C{ExactTransverseMercator.reverse} iteration number
           (C{int} or C{0} if not available/applicable).
        '''
        return self._iteration

    @property_doc_(''' the central scale factor (C{float}).''')
    def k0(self):
        '''Get the central scale factor (C{float}), aka I{C{scale0}}.
        '''
        return self._k0  # aka scale0

    @k0.setter  # PYCHOK setter!
    def k0(self, k0):
        '''Set the central scale factor (C{float}), aka I{C{scale0}}.

           @raise ETMError: Invalid B{C{k0}}.
        '''
        self._k0 = Scalar_(k0, name=_k0_, Error=ETMError, low=_TOL_10, high=1.0)
        # if not self._k0 > 0:
        #     raise Scalar_.Error_(Scalar_, k0, name=_k0_, Error=ETMError)
        self._k0_a = self._k0 * self._a

    @property_doc_(''' the central meridian (C{degrees180}).''')
    def lon0(self):
        '''Get the central meridian (C{degrees180}).
        '''
        return self._lon0

    @lon0.setter  # PYCHOK setter!
    def lon0(self, lon0):
        '''Set the central meridian (C{degrees180}).

           @raise ValueError: Invalid B{C{lon0}}.
        '''
        self._lon0 = _norm180(Lon(lon0, name=_lon0_))

    def _reset(self, e, e2):
        '''(INTERNAL) Get elliptic functions and pre-compute some
           frequently used values.

           @arg e: Eccentricity (C{float}).
           @arg e2: Eccentricity squared (C{float}).

           @raise EllipticError: No convergence.
        '''
        # assert e2 == e**2
        self._e          = e
        self._e_PI_2     = e *  PI_2
        self._e_PI_4     = e *  PI_4
        self._e_TAYTOL   = e * _TAYTOL

        self._1_e_90     = (1 - e) * 90
        self._1_e_PI_2   = (1 - e) * PI_2
        self._1_e2_PI_2  = (1 - e * 2) * PI_2

        self._mu         =  e2
        self._mu_2_1     = (e2 + 2) * 0.5

        self._Eu         = Elliptic(self._mu)
        self._Eu_cE_1_4  = self._Eu.cE * 0.25
        self._Eu_cK_cE   = self._Eu.cK / self._Eu.cE
        self._Eu_cK_PI_2 = self._Eu.cK / PI_2

        self._mv         = 1 - e2
        self._3_mv       = 3.0 / self._mv
        self._3_mv_e     = self._3_mv / e

        self._Ev         = Elliptic(self._mv)
        self._Ev_cKE_3_4 = self._Ev.cKE * 0.75
        self._Ev_cKE_5_4 = self._Ev.cKE * 1.25

        self._iteration  = 0

    def reverse(self, x, y, lon0=None):
        '''Reverse projection, from Transverse Mercator to geographic.

           @arg x: Easting of point (C{meters}).
           @arg y: Northing of point (C{meters}).
           @kwarg lon0: Central meridian of the projection (C{degrees}).

           @return: L{LatLonExact4Tuple}C{(lat, lon, convergence, scale)}
                    in C{degrees}, C{degrees180}, C{degrees} and C{scalar}.

           @see: C{void TMExact::Reverse(real lon0, real x, real y,
                                         real &lat, real &lon,
                                         real &gamma, real &k)}

           @raise EllipticError: No convergence.
        '''
        # undoes the steps in .forward.
        xi  = y / self._k0_a
        eta = x / self._k0_a
        backside = _lat = _lon = False
        if not self.extendp:  # enforce the parity
            if y < 0:
                _lat, xi  = True, -xi
            if x < 0:
                _lon, eta = True, -eta
            if xi > self._Eu.cE:
                xi = 2 * self._Eu.cE - xi
                backside = True

        # u,v = coordinates for the Thompson TM, Lee 54
        if xi != 0 or eta != self._Ev.cKE:
            u, v = self._sigmaInv(xi, eta)
        else:
            u = self._iteration = 0
            v = self._Ev.cK

        if v != 0 or u != self._Eu.cK:
            g, k, lat, lon = self._zetaScaled(self._sncndn6(u, v))
        else:
            g, k, lat, lon = 0, self._k0, 90, 0

        if backside:
            lon, g = (180 - lon), (180 - g)
        if _lat:
            lat, g = -lat, -g
        if _lon:
            lon, g = -lon, -g

        lon += self._lon0 if lon0 is None else _norm180(lon0)
        r = LatLonExact4Tuple(_norm180(lat), _norm180(lon), g, k)
        r._iteration = self._iteration
        return r

    def _scaled(self, tau, d2, snu, cnu, dnu, snv, cnv, dnv):
        '''(INTERNAL) C{scaled}.

           @note: Argument B{C{d2}} is C{_mu * cnu**2 + _mv * cnv**2}
                  from C{._sigma3} or C{._zeta3}.

           @return: 2-Tuple C{(convergence, scale)}.

           @see: C{void TMExact::Scale(real tau, real /*lam*/,
                                       real snu, real cnu, real dnu,
                                       real snv, real cnv, real dnv,
                                       real &gamma, real &k)}.
        '''
        mu, mv = self._mu, self._mv
        cnudnv = cnu * dnv
        # Lee 55.12 -- negated for our sign convention.  g gives
        # the bearing (clockwise from true north) of grid north
        g = atan2(mv * cnv * snv * snu, cnudnv * dnu)
        # Lee 55.13 with nu given by Lee 9.1 -- in sqrt change
        # the numerator from
        #
        #  (1 - snu^2 * dnv^2) to (_mv * snv^2 + cnu^2 * dnv^2)
        #
        # to maintain accuracy near phi = 90 and change the
        # denomintor from
        #  (dnu^2 + dnv^2 - 1) to (_mu * cnu^2 + _mv * cnv^2)
        #
        # to maintain accuracy near phi = 0, lam = 90 * (1 - e).
        # Similarly rewrite sqrt term in 9.1 as
        #
        #  _mv + _mu * c^2 instead of 1 - _mu * sin(phi)^2
        q2 = (mv * snv**2 + cnudnv**2) / d2
        # originally: sec2 = 1 + tau**2  # sec(phi)^2
        # k = sqrt(mv + mu / sec2) * sqrt(sec2) * sqrt(q2)
        #   = sqrt(mv + mv * tau**2 + mu) * sqrt(q2)
        k = sqrt(fsum_(mu, mv, mv * tau**2)) * sqrt(q2)
        return degrees(g), k * self._k0

    def _sigma3(self, v, snu, cnu, dnu, snv, cnv, dnv):  # PYCHOK unused
        '''(INTERNAL) C{sigma}.

           @return: 3-Tuple C{(xi, eta, d2)}.

           @see: C{void TMExact::sigma(real /*u*/, real snu, real cnu, real dnu,
                                       real   v,   real snv, real cnv, real dnv,
                                       real &xi, real &eta)}.

           @raise EllipticError: No convergence.
        '''
        # Lee 55.4 writing
        # dnu^2 + dnv^2 - 1 = _mu * cnu^2 + _mv * cnv^2
        d2 = self._mu * cnu**2 + self._mv * cnv**2
        xi =      self._Eu.fE(snu, cnu, dnu) - self._mu * snu * cnu * dnu / d2
        eta = v - self._Ev.fE(snv, cnv, dnv) + self._mv * snv * cnv * dnv / d2
        return xi, eta, d2

    def _sigmaDwd(self, snu, cnu, dnu, snv, cnv, dnv):
        '''(INTERNAL) C{sigmaDwd}.

           @return: 2-Tuple C{(du, dv)}.

           @see: C{void TMExact::dwdsigma(real /*u*/, real snu, real cnu, real dnu,
                                          real /*v*/, real snv, real cnv, real dnv,
                                          real &du, real &dv)}.
        '''
        snuv = snu * snv
        # Reciprocal of 55.9: dw / ds = dn(w)^2/_mv,
        # expanding complex dn(w) using A+S 16.21.4
        d = self._mv * (cnv**2 + self._mu * snuv**2)**2
        r =  cnv * dnu * dnv
        i = -cnu * snuv * self._mu
        du = (r**2 - i**2) / d
        dv = 2 * r * i / d
        return du, dv

    def _sigmaInv(self, xi, eta):
        '''(INTERNAL) Invert C{sigma} using Newton's method.

           @return: 2-Tuple C{(u, v)}.

           @see: C{void TMExact::sigmainv(real xi, real eta,
                                          real &u, real &v)}.

           @raise EllipticError: No convergence.
        '''
        u, v, trip = self._sigmaInv0(xi, eta)
        if trip:
            self._iteration = 0
        else:
            U, V = Fsum(u), Fsum(v)
            # min iterations = 2, max = 7, mean = 3.9
            for self._iteration in range(1, _TRIPS):  # GEOGRAPHICLIB_PANIC
                sncndn6 = self._sncndn6(u, v)
                X, E, _ = self._sigma3(v, *sncndn6)
                dw, dv  = self._sigmaDwd( *sncndn6)
                X  = xi - X
                E -= eta
                u, du = U.fsum2_(X * dw,  E * dv)
                v, dv = V.fsum2_(X * dv, -E * dw)
                if trip:
                    break
                trip = hypot2(du, dv) < _TOL_10
            else:
                t = unstr(self._sigmaInv.__name__, xi, eta)
                raise EllipticError(_no_convergence_, txt=t)
        return u, v

    def _sigmaInv0(self, xi, eta):
        '''(INTERNAL) Starting point for C{sigmaInv}.

           @return: 3-Tuple C{(u, v, trip)}.

           @see: C{bool TMExact::sigmainv0(real xi, real eta,
                                           real &u, real &v)}.
        '''
        trip = False
        if eta > self._Ev_cKE_5_4 or xi < min(- self._Eu_cE_1_4,
                                          eta - self._Ev.cKE):
            # sigma as a simple pole at
            #  w = w0 = Eu.K() + i * Ev.K()
            # and sigma is approximated by
            #  sigma = (Eu.E() + i * Ev.KE()) + 1 / (w - w0)
            x = xi  - self._Eu.cE
            y = eta - self._Ev.cKE
            d = hypot2(x, y)
            u = self._Eu.cK + x / d
            v = self._Ev.cK - y / d

        elif eta > self._Ev.cKE or (xi < self._Eu_cE_1_4 and
                                   eta > self._Ev_cKE_3_4):
            # At w = w0 = i * Ev.K(), we have
            #  sigma  = sigma0  = i * Ev.KE()
            #  sigma' = sigma'' = 0
            #
            # including the next term in the Taylor series gives:
            #  sigma = sigma0 - _mv / 3 * (w - w0)^3
            #
            # When inverting this, we map arg(w - w0) = [-pi/2, -pi/6]
            # to arg(sigma - sigma0) = [-pi/2, pi/2]
            # mapping arg = [-pi/2, -pi/6] to [-pi/2, pi/2]
            d = eta - self._Ev.cKE
            r = hypot(xi, d)
            # Error using this guess is about 0.068 * rad^(5/3)
            trip = r < _TAYTOL2
            # Map the range [-90, 180] in sigma space to [-90, 0] in
            # w space.  See discussion in zetainv0 on the cut for ang.
            r = cbrt(r * self._3_mv)
            a = atan2(d - xi, xi + d) / 3.0 - PI_4
            s, c = sincos2(a)
            u = r * c
            v = r * s + self._Ev.cK

        else:  # use w = sigma * Eu.K/Eu.E (correct in the limit _e -> 0)
            u = xi  * self._Eu_cK_cE
            v = eta * self._Eu_cK_cE

        return u, v, trip

    def _sncndn6(self, u, v):
        '''(INTERNAL) Get 6-tuple C{(snu, cnu, dnu, snv, cnv, dnv)}.
        '''
        # snu, cnu, dnu = self._Eu.sncndn(u)
        # snv, cnv, dnv = self._Ev.sncndn(v)
        return self._Eu.sncndn(u) + self._Ev.sncndn(v)

    def toStr(self, **kwds):
        '''Return a C{str} representation.

           @arg kwds: Optional, overriding keyword arguments.
        '''
        d = dict(name=self.name) if self.name else {}
        d = dict(datum=self.datum.name, lon0=self.lon0,
                 k0=self.k0, extendp=self.extendp, **d)
        return _COMMA_SPACE_.join(pairs(d, **kwds))

    def _zeta3(self, snu, cnu, dnu, snv, cnv, dnv):
        '''(INTERNAL) C{zeta}.

           @return: 3-Tuple C{(taup, lambda, d2)}.

           @see: C{void TMExact::zeta(real /*u*/, real snu, real cnu, real dnu,
                                      real /*v*/, real snv, real cnv, real dnv,
                                      real &taup, real &lam)}
        '''
        e = self._e
        # Lee 54.17 but write
        # atanh(snu * dnv)      = asinh(snu * dnv / sqrt(cnu^2 + _mv * snu^2 * snv^2))
        # atanh(_e * snu / dnv) = asinh(_e * snu / sqrt(_mu * cnu^2 + _mv * cnv^2))
        d1 = cnu**2 + self._mv * (snu * snv)**2
        d2 = self._mu * cnu**2 + self._mv * cnv**2
        # Overflow value s.t. atan(overflow) = pi/2
        t1 = t2 = copysign(_OVERFLOW, snu)
        if d1 > 0:
            t1 = snu * dnv / sqrt(d1)
        lam = 0
        if d2 > 0:
            t2 = sinh(e * asinh(e * snu / sqrt(d2)))
            if d1 > 0:
                lam = atan2(dnu * snv    , cnu * cnv) - \
                      atan2(cnu * snv * e, dnu * cnv) * e
        # psi = asinh(t1) - asinh(t2)
        # taup = sinh(psi)
        taup = t1 * hypot1(t2) - t2 * hypot1(t1)
        return taup, lam, d2

    def _zetaDwd(self, snu, cnu, dnu, snv, cnv, dnv):
        '''(INTERNAL) C{zetaDwd}.

           @return: 2-Tuple C{(du, dv)}.

           @see: C{void TMExact::dwdzeta(real /*u*/, real snu, real cnu, real dnu,
                                         real /*v*/, real snv, real cnv, real dnv,
                                         real &du, real &dv)}.
        '''
        cnu2  = cnu**2 * self._mu
        cnv2  = cnv**2
        dnuv  = dnu * dnv
        dnuv2 = dnuv**2
        snuv  = snu * snv
        snuv2 = snuv**2 * self._mu
        # Lee 54.21 but write
        # (1 - dnu^2 * snv^2) = (cnv^2 + _mu * snu^2 * snv^2)
        # (see A+S 16.21.4)
        d  =  self._mv   * (cnv2 + snuv2)**2
        du =  cnu * dnuv * (cnv2 - snuv2) / d
        dv = -cnv * snuv * (cnu2 + dnuv2) / d
        return du, dv

    def _zetaInv(self, taup, lam):
        '''(INTERNAL) Invert C{zeta} using Newton's method.

           @return: 2-Tuple C{(u, v)}.

           @see: C{void TMExact::zetainv(real taup, real lam,
                                         real &u, real &v)}.

           @raise EllipticError: No convergence.
        '''
        psi = asinh(taup)
        sca = 1.0 / hypot1(taup)
        u, v, trip = self._zetaInv0(psi, lam)
        if trip:
            self._iteration = 0
        else:
            stol2 = _TOL_10 / max(psi**2, 1.0)
            U, V = Fsum(u), Fsum(v)
            # min iterations = 2, max = 6, mean = 4.0
            for self._iteration in range(1, _TRIPS):  # GEOGRAPHICLIB_PANIC
                sncndn6 = self._sncndn6(u, v)
                T, L, _ = self._zeta3(  *sncndn6)
                dw, dv  = self._zetaDwd(*sncndn6)
                T  = (taup - T) * sca
                L -= lam
                u, du = U.fsum2_(T * dw,  L * dv)
                v, dv = V.fsum2_(T * dv, -L * dw)
                if trip:
                    break
                trip = hypot2(du, dv) < stol2
            else:
                t = unstr(self._zetaInv.__name__, taup, lam)
                raise EllipticError(_no_convergence_, txt=t)
        return u, v

    def _zetaInv0(self, psi, lam):
        '''(INTERNAL) Starting point for C{zetaInv}.

           @return: 3-Tuple C{(u, v, trip)}.

           @see: C{bool TMExact::zetainv0(real psi, real lam,  # radians
                                          real &u, real &v)}.
        '''
        trip = False
        if psi < -self._e_PI_4 and lam >        self._1_e2_PI_2 \
                               and psi < (lam - self._1_e_PI_2):
            # N.B. this branch is normally not taken because psi < 0
            # is converted psi > 0 by Forward.
            #
            # There's a log singularity at w = w0 = Eu.K() + i * Ev.K(),
            # corresponding to the south pole, where we have, approximately
            #
            #  psi = _e + i * pi/2 - _e * atanh(cos(i * (w - w0)/(1 + _mu/2)))
            #
            # Inverting this gives:
            h = sinh(1 - psi / self._e)
            a = (PI_2 - lam) / self._e
            s, c = sincos2(a)
            u = self._Eu.cK - asinh(s / hypot(c, h)) * self._mu_2_1
            v = self._Ev.cK - atan2(c, h) * self._mu_2_1

        elif psi < self._e_PI_2 and lam > self._1_e2_PI_2:
            # At w = w0 = i * Ev.K(), we have
            #
            #  zeta  = zeta0  = i * (1 - _e) * pi/2
            #  zeta' = zeta'' = 0
            #
            # including the next term in the Taylor series gives:
            #
            #  zeta = zeta0 - (_mv * _e) / 3 * (w - w0)^3
            #
            # When inverting this, we map arg(w - w0) = [-90, 0] to
            # arg(zeta - zeta0) = [-90, 180]

            d = lam - self._1_e_PI_2
            r = hypot(psi, d)
            # Error using this guess is about 0.21 * (rad/e)^(5/3)
            trip = r < self._e_TAYTOL
            # atan2(dlam-psi, psi+dlam) + 45d gives arg(zeta - zeta0)
            # in range [-135, 225).  Subtracting 180 (since multiplier
            # is negative) makes range [-315, 45).  Multiplying by 1/3
            # (for cube root) gives range [-105, 15).  In particular
            # the range [-90, 180] in zeta space maps to [-90, 0] in
            # w space as required.
            r = cbrt(r * self._3_mv_e)
            a = atan2(d - psi, psi + d) / 3.0 - PI_4
            s, c = sincos2(a)
            u = r * c
            v = r * s + self._Ev.cK

        else:
            # Use spherical TM, Lee 12.6 -- writing C{atanh(sin(lam) /
            # cosh(psi)) = asinh(sin(lam) / hypot(cos(lam), sinh(psi)))}.
            # This takes care of the log singularity at C{zeta = Eu.K()},
            # corresponding to the north pole.
            s, c = sincos2(lam)
            h, r = sinh(psi), self._Eu_cK_PI_2
            # But scale to put 90, 0 on the right place
            u = r * atan2(h, c)
            v = r * asinh(s / hypot(c, h))

        return u, v, trip

    def _zetaScaled(self, sncndn6, ll=True):
        '''(INTERNAL) Recompute (T, L) from (u, v) to improve accuracy of Scale.

           @arg sncndn6: 6-Tuple C{(snu, cnu, dnu, snv, cnv, dnv)}.

           @return: 2-Tuple C{(g, k)} if B{C{ll}} is C{False} else
                    4-tuple C{(g, k, lat, lon)}.
        '''
        t, lam, d2 = self._zeta3( *sncndn6)
        tau = self._E.es_tauf(t)
        r = self._scaled(tau, d2, *sncndn6)
        if ll:
            r += degrees(atan(tau)), degrees(lam)
        return r