Coverage for pyrdnap/rdnap2018.py: 91%
228 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-14 16:01 -0400
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-14 16:01 -0400
2# -*- coding: utf-8 -*-
4u'''Main classes L{RDNAP2018v1} and L{RDNAP2018v2} follow C{variant 1} respectively C{variant 2}
5of the specification U{RDNAPTRANS(tm)2018<https://formulieren.kadaster.nl/aanvragen_rdnaptrans>}.
6Each provide a C{forward} method to convert geodetic lat-/longitudes and heights to C{RD}
7coodinates and C{NAP} heights and a C{reverse} method for converting the other way.
9The C{forward} and C{reverse} results of L{RDNAP2018v1} meet the C{RDNAPTRANS(tm)2018_v220627}
10self-validation requirements of C{0.000000010 degrees} and C{0.0010 meter} for tests inside
11the C{RD} region, see B{C{Note below}}. Class L{RDNAP2018v2} does not and is not required to.
13The original C{RDNAPTRANS(tm)2018_v220627} grid files for both variants are I{not included}
14in C{PyPRDNAP} and C{pyrdnap} due to the size of those files. Instead, the grid files for each
15variant I{include only} the C{lat_corr_}, C{lon_corr_} and C{_NAP_quasi_geoid_height_...} columns,
16extracted from the original grid files with leading and trailing zeros removed and formatted as
17row-order matrices.
19@note: L{RDNAP2018v1}, C{PyRDNAP} and C{pyrdnap} have B{not been formally validated} and are
20 B{not certified} to carry the trademark name C{RDNAPTRANS(tm)}.
21'''
22# make sure int/int division yields float quotient, see .basics
23from __future__ import division as _; del _ # noqa: E702 ;
25from pyrdnap.rd0 import _RD, _RD0 as A0, RDNAP7Tuple
26from pyrdnap.v_grids import RDNAPError, _V_grid, _v_gridz_import
27from pyrdnap.__pygeodesy import (_0_0, _0_5, _1_0, _2_0,
28 _isNAN, _earth_datum,
29 _ALL_DOCS, _ALL_OTHER, _FOR_DOCS,
30 _NamedBase, notOverloaded)
31from pygeodesy import (EPS0, EPS1, NAN, PI_2, PI, PI2, # "consterns"
32 Datum, Datums, Ellipsoid, # datums, ellipsoids
33 property_RO, property_ROnce, # props
34 Lamd, Phid, # units
35 sincos2, sincos2d) # utily
37from math import asin, atan, copysign, degrees, exp, \
38 fabs, floor, hypot, radians, sin, sqrt
40__all__ = ()
41__version__ = '26.05.14'
43_TOL_D = 1e-9 # degrees 2.3.3f+
44_TOL_M = 1e-6 # meter
45_TOL_R = radians(_TOL_D) # 2e-11
46_TRIPS = 16 # 5..6 sufficient
49class _RDNAPbase(_NamedBase):
50 '''(INTERNAL) L{RDNAP2018v1}C{/-v2} base class.
51 '''
52 _datum = None # forward, v1 reverse Datum, lazily
53 _EETRS = None # forward, v1 reverse Ellipsoid, lazily
54 _raiser = False
56 def __init__(self, a_ellipsoid=None, f=None, raiser=False, **name):
57 '''New C{RDNAP2018v1} or C{-v2} instance.
59 @kwarg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or the ellipsoid's equatorial
60 radius (C{scalar}, conventionally in C{meter}), see B{C{f}}
61 or a datum (L{Datum}). Default C{Datums.GRS80}.
62 @kwarg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}} is
63 specified as C{scalar}, ignored otherwise.
64 @kwarg raiser: If C{True} raise an L{RDNAPError} for lat-/longitudes outside
65 the C{RD} region (C{bool}).
66 @kwarg name: Optional name (C{str}).
68 @raise RDNAPError: Ellipsoid (or datum) is not oblate (i.e. is spherical or
69 prolate) or the datum's C{transform} is not C{unity}.
70 '''
71 if a_ellipsoid is f is None:
72 self._datum = Datums.GRS80
73 else:
74 _earth_datum(a_ellipsoid, f, **name) # sets self._datum
75 E = self._datum.ellipsoid
76 if not E.isOblate:
77 raise RDNAPError('not oblate: %r' % (E,))
78 self._EETRS = E
79 if raiser:
80 T = self._datum.transform
81 if not T.isunity:
82 raise RDNAPError('not unity: %r' % (T,))
83 self._raiser = True
84 if name:
85 self.name = name
87 def forward(self, lat, lon, height=0, raiser=None, name='forward'):
88 '''Convert GRS80 (ETRS98) geodetic C{(B{lat}, B{lon})} and B{C{height}}
89 to local C{(RDx, RDy)} coordinates and C{NAPh} quasi-geoid-height.
91 @arg lat: Latitude (C{degrees} geodetic).
92 @arg lon: Longitude (C{degrees} geodetic).
93 @kwarg height: Height, optional (C{meter} above geoid) or C{NAN}
94 to ignore C{NAPh} interpolation.
95 @kwarg raiser: If C{True} raise an L{RDNAPError} if B{C{lat}} or
96 B{C{lon}} is outside the C{RD} region (C{bool}),
97 if C{False} don't, overriding property C{raiser}.
98 @kwarg name: Optional name (C{str}).
100 @return: An L{RDNAP7Tuple}C{(RDx, RDy, NAPh, lat, lon, height, datum)}
101 with local C{RDx}, C{RDy} coordinates and C{NAPh} height.
102 '''
103 lat0, lon0 = \
104 lat_, lon_ = self._forwardXform(lat, lon, raiser)
105 for _ in range(_TRIPS): # 2.3.3a-f, 1..2
106 latc, lonc = self._rdlatlon2(lat_, lon_, lat0, lon0)
107 if fabs(latc - lat_) < _TOL_D and \
108 fabs(lonc - lon_) < _TOL_D:
109 break
110 lat_, lon_ = latc, lonc
112 phiClamC = _ellipsoidal2spherical(latc, lonc)
113 RDx, RDy = _spherical2oblique(*phiClamC)
114 NAPh = NAN if _isNAN(height) else (height - self.rdNAPh(lat, lon)) # 2.5.2
115 return RDNAP7Tuple(RDx, RDy, NAPh, lat, lon, height, self.forwardDatum, name=name)
117 @property_RO
118 def forwardDatum(self):
119 '''Get the C{forward} datum (L{Datum}, default GRS80).
120 '''
121 return self._datum
123 def _inside2(self, lat, lon, raiser):
124 # default and variant 2: no datum Xform
125 if (raiser or (raiser is None and self._raiser)) and \
126 not _RD.isinside(lat, lon):
127 raise self._outsidError(lat, lon)
128 return lat, lon
130 _forwardXform = _inside2 # no datum Xform
132 def isinside(self, lat, lon, eps=0):
133 '''Is C{(B{lat}, B{lon})} inside the C{RD} region (C{bool})?
135 @kwarg eps: Over-/undersize the C{RD} region (C{degrees}).
136 '''
137 return _RD.isinside(lat, lon, eps)
139 def _outsidError(self, *lat_lon):
140 # format an RDNAPError for C{lat_lon} outside C{RD} region
141 return RDNAPError('%r outside %s' % (lat_lon, self.region))
143 @property_RO
144 def _rdgrid(self):
145 raise notOverloaded(self)
147 def _rdlatlon2(self, lat, lon, lat0=None, lon0=None): # 2.3.2
148 # return the RD-corrected C{(lat, lon)}
149 if _RD.isinside(lat, lon):
150 c_f_N_f6 = _RD.c_f_N_f6(lat, lon)
151 lat_corr = _bilinear(self._rdgrid._lat_corr, *c_f_N_f6)
152 lon_corr = _bilinear(self._rdgrid._lon_corr, *c_f_N_f6)
154 if lat0 is lon0 is None: # reverse
155 lat += lat_corr
156 lon += lon_corr
157 else: # forward
158 lat = lat0 - lat_corr
159 lon = lon0 - lon_corr
160 return lat, lon # NAN, NAN?
162 def rdNAPh(self, lat, lon, raiser=False): # 2.5.1 and 3.5
163 '''Interpolate the C{NAPh} quasi-geoid-height I{within}
164 the C{RD} region.
166 @arg lat: Latitude (C{degrees} GRS80 (ETRS89), geodetic).
167 @arg lon: Longitude (C{degrees} GRS80 (ETRS89), geodetic).
168 @kwarg raiser: If C{True} raise an L{RDNAPError} if B{C{lat}} or
169 B{C{lon}} is outside the C{RD} region (C{bool}),
170 otherwise don't and return C{NAN}.
172 @return: C{NAPh} (C{meter}) or C{NAN} if C{B{raiser} is False}
173 and B{C{lat}} or B{C{lon}} is outside the C{RD} region.
174 '''
175 if _RD.isinside(lat, lon):
176 c_f_N_f6 = _RD.c_f_N_f6(lat, lon)
177 return _bilinear(self._rdgrid._NAP_h, *c_f_N_f6)
178 elif raiser:
179 raise self._outsidError(lat, lon)
180 return NAN # c0 2.5.1e+
182 @property_RO
183 def region(self):
184 '''Get the C{RD} region as L{RDregion4Tuple}C{(S, W, N, E)}, all C{GRS80 (ETRS89) degrees}.
185 '''
186 return _RD.region
188 def _reverse(self, RDx, RDy, NAPh, raiser=None, name='reverse'):
189 '''(INTERNAL) Convert local C{(B{RDx}, B{RDy})} coordinates and
190 B{C{NAPh}} quasi-geoid-height to GRS80 (ETRS89) or Bessel1841
191 (RD-Bessel) geodetic C{lat}, C{lon} and C{height}.
192 '''
193 philCamC = _oblique2spherical(RDx, RDy)
194 lat, lon = _spherical2ellipsoidal(*philCamC)
196 lat, lon = self._rdlatlon2(lat, lon)
197 lat, lon = self._reverseXform(lat, lon, raiser)
198 h = NAN if _isNAN(NAPh) else (NAPh + self.rdNAPh(lat, lon))
199 return RDNAP7Tuple(RDx, RDy, NAPh, lat, lon, h,
200 self.reverseDatum, name=name)
202 @property_RO
203 def reverseDatum(self):
204 '''Get the C{reverse} datum (L{Datum}), GRS80 or Bessel1841.
205 '''
206 return {1: self._datum,
207 2: A0.D0}.get(self.variant)
209 _reverseXform = _inside2 # no datum Xform
211 def toStr(self, prec=9, **unused): # PYCHOK signature
212 '''Return this C{RDNAP2018#v} instance as a string.
214 @kwarg prec: Precision, number of decimal digits (0..9).
216 @return: This C{RDNAP2018#v} (C{str}).
217 '''
218 return self.attrs('name', 'variant', 'forwardDatum', prec=prec) # _ellipsoid_, _name__
220 @property_RO
221 def variant(self):
222 raise None
225class RDNAP2018v1(_RDNAPbase):
226 '''Transformer implementing RD NAP 2018 C{variant 1}.
228 @note: Method L{RDNAP2018v1.reverse} returns B{not GRS80 (ETRS89)}
229 geodetic lat- and longitudes.
230 '''
231 if _FOR_DOCS:
232 __init__ = _RDNAPbase.__init__
233 forward = _RDNAPbase.forward
235 def _forwardXform(self, lat, lon, raiser):
236 # transform C{(lat, lon)} from GRS80 (ETRS89) to RD-Bessel datum
237 x, y, z = _geodetic2cartesian(lat, lon, self._EETRS, A0.H0_ETRS)
238 x, y, z = _RD._xETRS2RD.transform(x, y, z)
239 lat, lon = _cartesian2geodetic(x, y, z, A0.E0)
240 return self._inside2(lat, lon, raiser)
242 @property_ROnce
243 def _rdgrid(self):
244 try:
245 from pyrdnap import v1grid
246 except (AttributeError, ImportError, RDNAPError):
247 v1grid = _v_gridz_import(self.variant)
248 return v1grid
250 def reverse(self, RDx, RDy, NAPh=0, **raiser_name): # RDNAP to GRS80 (ETRS89)
251 '''Convert a local C{(B{RDx}, B{RDy})} point and B{C{NAPh}} height to
252 B{GRS80 (ETRS89)} geodetic C{(lat, lon, height)}.
254 @arg RDx: Local C{RD} X (C{meter}, conventionally).
255 @arg RDy: Local C{RD} Y (C{meter}, conventionally).
256 @kwarg NAPh: C{NAP} quasi-geoid-height (C{meter}, conventionally)
257 or C{NAN} to ignore C{NAPh} interpolation.
258 @kwarg raiser_name: Like the C{forward} method, C{B{raiser}=None}
259 (C{bool}) and optional C{B{name}='reverse'} (C{str}).
261 @return: An L{RDNAP7Tuple}C{(RDx, RDy, NAPh, lat, lon, height, datum)}
262 with geodetic C{lat} and C{lon}, C{height} and C{datum}
263 B{GRS80 (ETRS89)}.
264 '''
265 return self._reverse(RDx, RDy, NAPh, **raiser_name)
267 def _reverseXform(self, lat, lon, raiser):
268 # transform C{(lat, lon)} from RD-Bessel to GRS80 (ETRS89) datum
269 x, y, z = _geodetic2cartesian(lat, lon, A0.E0, A0.H0)
270 x, y, z = _RD._xRD2ETRS.transform(x, y, z)
271 lat, lon = _cartesian2geodetic(x, y, z, self._EETRS)
272 return self._inside2(lat, lon, raiser)
274 def similarity(self, inverse=False):
275 '''Get the similarity transform (C{Similarity}).
277 @kwarg inverse: Use C{True} for the C{reverse} or C{False}
278 for the C{forward} transform (C{bool}).
279 '''
280 return _RD._xRD2ETRS if inverse else _RD._xETRS2RD
282 @property_ROnce
283 def variant(self):
284 '''Get this C{RDNAP2018}'s variant (C{int}).
285 '''
286 return 1
289class RDNAP2018v2(_RDNAPbase):
290 '''Transformer implementing RD NAP 2018 C{variant 2}.
292 @note: Method L{RDNAP2018v2.reverse} returns B{Bessel1841 (RD-Bessel)}
293 and B{not GRS80 (ETRS89)} geodetic lat- and longitudes.
294 '''
295 if _FOR_DOCS:
296 __init__ = _RDNAPbase.__init__
297 forward = _RDNAPbase.forward
299 @property_ROnce
300 def _rdgrid(self):
301 try:
302 from pyrdnap import v2grid
303 except (AttributeError, ImportError, RDNAPError):
304 v2grid = _v_gridz_import(self.variant)
305 return v2grid
307 def reverse(self, RDx, RDy, NAPh=0, **raiser_name): # RDNAP to RD-Bessel
308 '''Convert a local C{(B{RDx}, B{RDy})} point and B{C{NAPh}} height
309 to B{Bessel1841 (RD-Bessel)} geodetic C{(lat, lon, height)}.
311 @arg RDx: Local C{RD} X (C{meter}, conventionally).
312 @arg RDy: Local C{RD} Y (C{meter}, conventionally).
313 @kwarg NAPh: C{NAP} quasi-geoid-height (C{meter}, conventionally)
314 or C{NAN} to ignore C{NAPh} interpolation.
315 @kwarg raiser_name: Like the C{forward} method, C{B{raiser}=None}
316 (C{bool}) and optional C{B{name}='reverse'} (C{str}).
318 @return: An L{RDNAP7Tuple}C{(RDx, RDy, NAPh, lat, lon, height, datum)}
319 with geodetic C{lat} and C{lon}, C{height} and C{datum}
320 B{Bessel1841 (RD-Bessel)}.
321 '''
322 return self._reverse(RDx, RDy, NAPh, **raiser_name)
324 def similarity(self, *unused): # PYCHOK signature
325 '''Get the similarity transform, always C{None}.
326 '''
327 return None
329 @property_ROnce
330 def variant(self):
331 '''Get this C{RDNAP2018}'s variant (C{int}).
332 '''
333 return 2
336def _atan3(y, x, x0): # 2.2.3e and 3.1.1i
337 # equiv to math.atan2 iff x0 is y
338 if x > 0:
339 r = atan(y / x)
340 elif x < 0:
341 r = atan(y / x) + copysign(PI, x0)
342 else:
343 r = copysign(PI_2, x0) if x0 else _0_0
344 return r
347def _atan_exp(w): # 2.4.1c
348 return atan(exp(w)) * _2_0 - PI_2
351def _bilinear(v_grid, c_latI, f_latI, latN_f, # 2.3.1f and g
352 c_lonI, f_lonI, lonN_f):
353 # interpolate a lat_corr_, lon_corr_ or NAP_...
354 assert isinstance(v_grid, _V_grid)
355 nw = v_grid(c_latI, f_lonI)
356 ne = v_grid(c_latI, c_lonI)
357 sw = v_grid(f_latI, f_lonI)
358 se = v_grid(f_latI, c_lonI)
359 lonN_f1 = _1_0 - lonN_f # == 1 - (lonN - f_lonN)
360 return (nw * lonN_f1 + ne * lonN_f) * latN_f + \
361 (sw * lonN_f1 + se * lonN_f) * (_1_0 - latN_f)
364def _cartesian2geodetic(x, y, z, E): # 2.2.3 == EcefUPC.reverse?
365 # convert cartesian C{(x, y, z)} to C{E}-geodetic C{(lat, lon)}
366 r = hypot(x, y)
367 if r > _TOL_M:
368 a = E.a * E.e2
369 phi_ = atan(z / r)
370 for _ in range(_TRIPS): # 4..6
371 s = sin(phi_)
372 s *= a / sqrt(_1_0 - s**2 * E.e2)
373 phi = atan((z + s) / r)
374 if fabs(phi - phi_) < _TOL_R:
375 break
376 phi_ = phi
377 else:
378 phi = copysign(PI_2, z)
379 lam = _atan3(y, x, y)
380 return degrees(phi), degrees(lam)
383def _ellipsoidal2spherical(lat, lon): # 2.4.1
384 # convert geodetic C{(lat, lon)} to spherical C{(𝛷, 𝛬)}
385 phiC = phi = Phid(lat)
386 if PI_2 > phi > -PI_2: # 2.4.1c
387 q = A0.ln_tan(phi) - A0.ln_e_2(phi)
388 w = A0.N0 * q + A0.M0 # 2.4.1b
389 phiC = _atan_exp(w)
390 lamC = (Lamd(lon) - A0.LAM0) * A0.N0 + A0.LAM0C # 2.4.1d
391 return phiC, lamC # -Capital
394def _eq0(r, r0=_0_0):
395 return fabs(r - r0) < _TOL_R
398# def _eq0d(d, d0=_0_0):
399# return fabs(d - d0) < _TOL_D
402def _geodetic2cartesian(lat, lon, E, h0=0): # 2.2.1
403 # convert C{E}-geodetic C{(lat, lon)} to cartesian C{(x, y, z)}
404 y, x = sincos2d(lon)
405 z, c = sincos2d(lat)
406 n = E.a / sqrt(_1_0 - z**2 * E.e2)
407 c *= n + h0
408 x *= c
409 y *= c
410 z *= n * (_1_0 - E.e2) + h0
411 return x, y, z
414def _ne0(r, r0=_0_0):
415 return fabs(r - r0) > _TOL_R
418# def _ne0d(d, d0=_0_0):
419# return fabs(d - d0) > _TOL_D
422def _oblique2spherical(x, y): # 3.1.1
423 # inverse oblique stereographic conformal projection
424 # from 2-D C{(x, y)} to spherical C{(𝛷, 𝛬)}
425 x -= A0.X0
426 y -= A0.Y0
427 r = hypot(x, y)
428 if r > _TOL_M: # x and y
429 s0, c0 = A0.sincos2PHI0C
430 sp, cp = sincos2(atan(r / A0.RK2) * _2_0) # psi
431 ca = sp * y / r
432 xN = cp * c0 - ca * s0
433 yN = sp * x / r
434 zN = ca * c0 + cp * s0
435 phiC = asin(zN)
436 else:
437 _, xN = A0.sincos2PHI0C
438 yN = _0_0
439 phiC = A0.PHI0C # asin(sin(PHI0C))
440 lamC = _atan3(yN, xN, x) + A0.LAM0C
441 return phiC, lamC # -Capital
444def _spherical2ellipsoidal(phiC, lamC): # 3.1.2
445 # inverse Gauss conformal projection from
446 # spherical C{(𝛷, 𝛬)} to geodetic C{(lat, lon)}
447 phi = phiC
448 if PI_2 > phi > -PI_2:
449 q = (A0.ln_tan(phi) - A0.M0) / A0.N0
450# w = A0.ln_tan(phi)
451 for _ in range(_TRIPS): # 3..6
452 phi_ = phi
453 phi = _atan_exp(A0.ln_e_2(phi) + q)
454 if fabs(phi - phi_) < _TOL_R:
455 break
456 lam = (lamC - A0.LAM0C) / A0.N0 + A0.LAM0
457 lam = floor((PI - lam) / PI2) * PI2 + lam
458 return degrees(phi), degrees(lam)
461def _spherical2oblique(phiC, lamC): # 2.4.2
462 # oblique stereographic conformal projection
463 # from spherical C{(𝛷, 𝛬)} to 2-D C{(x, y)}
464 a = phiC - A0.PHI0C # 𝛷 - 𝛷0
465 b = lamC - A0.LAM0C # 𝛬 - 𝛬0
466 if (_ne0(a) or _ne0(b)) and (_ne0(phiC, -A0.PHI0C) or
467 _ne0(lamC, -A0.LAM0C + PI)):
468 s0, c0 = A0.sincos2PHI0C # sin(𝛷0), cos(𝛷0)
469 s, c = sincos2(phiC) # sin(𝛷), cos(𝛷)
470 sp_22 = sin(a * _0_5)**2 + \
471 sin(b * _0_5)**2 * c * c0 # sin(𝜓/2)**2
472 if EPS0 < sp_22 < EPS1:
473 # r = 2kR * tan(𝜓/2)
474 # q = r / (sin(𝜓/2) * cos(𝜓/2) * 2)
475 # = 2kR * sin(𝜓/2) / (sin(𝜓/2) * cos(𝜓/2)**2 * 2)
476 # = 2kR / (cos(𝜓/2)**2 * 2)
477 # = 2kR / ((1 - sin(𝜓/2)**2) * 2)
478 # = 2kR / (2 - sin(𝜓/2)**2 * 2)
479 t = sp_22 * _2_0 # 0 < t < 2
480 q = A0.RK2 / (_2_0 - t)
481 x = q * (c * sin(b))
482 y = q * (s - s0 + s0 * t) / c0
483 else:
484 x = y = _0_0 # NAN?
485 x += A0.X0
486 y += A0.Y0
487 elif _eq0(phiC, A0.PHI0C) and _eq0(lamC, A0.LAM0C):
488 x = A0.X0 # x0 2.4.2g
489 y = A0.Y0 # y0 2.4.2h
490 else: # if _eq0(phiC, -A0.PHI0C) and _eq0(lamC, A0.LAM0C - PI):
491 x = y = NAN
492# else:
493# raise RDNAPError(str((phiC, lamC)))
494 return x, y
497__all__ += _ALL_DOCS(_RDNAPbase)
498__all__ += _ALL_OTHER(RDNAP2018v1, RDNAP2018v2, RDNAPError,
499 Datum, Datums, Ellipsoid) # passed along from PyGeodesy
501# **) MIT License
502#
503# Copyright (C) 2026-2026 -- mrJean1 at Gmail -- All Rights Reserved.
504#
505# Permission is hereby granted, free of charge, to any person obtaining a
506# copy of this software and associated documentation files (the "Software"),
507# to deal in the Software without restriction, including without limitation
508# the rights to use, copy, modify, merge, publish, distribute, sublicense,
509# and/or sell copies of the Software, and to permit persons to whom the
510# Software is furnished to do so, subject to the following conditions:
511#
512# The above copyright notice and this permission notice shall be included
513# in all copies or substantial portions of the Software.
514#
515# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
516# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
517# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
518# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
519# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
520# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
521# OTHER DEALINGS IN THE SOFTWARE.