-
Notifications
You must be signed in to change notification settings - Fork 202
Expand file tree
/
Copy pathcore.py
More file actions
executable file
·851 lines (732 loc) · 29.6 KB
/
core.py
File metadata and controls
executable file
·851 lines (732 loc) · 29.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
# coding: utf-8
"""
PyKrige
=======
Code by Benjamin S. Murphy and the PyKrige Developers
bscott.murphy@gmail.com
Summary
-------
Methods used by multiple classes.
References
----------
[1] P.K. Kitanidis, Introduction to Geostatistcs: Applications in Hydrogeology,
(Cambridge University Press, 1997) 272 p.
[2] T. Vincenty, Direct and Inverse Solutions of Geodesics on the Ellipsoid
with Application of Nested Equations, Survey Review 23 (176),
(Directorate of Overseas Survey, Kingston Road, Tolworth, Surrey 1975)
Copyright (c) 2015-2020, PyKrige Developers
"""
import numpy as np
import scipy.linalg as spl
from scipy.optimize import least_squares
from scipy.spatial.distance import cdist, pdist, squareform
eps = 1.0e-10 # Cutoff for comparison to zero
P_INV = {"pinv": spl.pinv, "pinvh": spl.pinvh}
def great_circle_distance(lon1, lat1, lon2, lat2):
"""Calculate the great circle distance between one or multiple pairs of
points given in spherical coordinates. Spherical coordinates are expected
in degrees. Angle definition follows standard longitude/latitude definition.
This uses the arctan version of the great-circle distance function
(en.wikipedia.org/wiki/Great-circle_distance) for increased
numerical stability.
Parameters
----------
lon1: float scalar or numpy array
Longitude coordinate(s) of the first element(s) of the point
pair(s), given in degrees.
lat1: float scalar or numpy array
Latitude coordinate(s) of the first element(s) of the point
pair(s), given in degrees.
lon2: float scalar or numpy array
Longitude coordinate(s) of the second element(s) of the point
pair(s), given in degrees.
lat2: float scalar or numpy array
Latitude coordinate(s) of the second element(s) of the point
pair(s), given in degrees.
Calculation of distances follows numpy elementwise semantics, so if
an array of length N is passed, all input parameters need to be
arrays of length N or scalars.
Returns
-------
distance: float scalar or numpy array
The great circle distance(s) (in degrees) between the
given pair(s) of points.
"""
# Convert to radians:
lat1 = np.array(lat1) * np.pi / 180.0
lat2 = np.array(lat2) * np.pi / 180.0
dlon = (lon1 - lon2) * np.pi / 180.0
# Evaluate trigonometric functions that need to be evaluated more
# than once:
c1 = np.cos(lat1)
s1 = np.sin(lat1)
c2 = np.cos(lat2)
s2 = np.sin(lat2)
cd = np.cos(dlon)
# This uses the arctan version of the great-circle distance function
# from en.wikipedia.org/wiki/Great-circle_distance for increased
# numerical stability.
# Formula can be obtained from [2] combining eqns. (14)-(16)
# for spherical geometry (f=0).
return (
180.0
/ np.pi
* np.arctan2(
np.sqrt((c2 * np.sin(dlon)) ** 2 + (c1 * s2 - s1 * c2 * cd) ** 2),
s1 * s2 + c1 * c2 * cd,
)
)
def euclid3_to_great_circle(euclid3_distance):
"""Convert euclidean distance between points on a unit sphere to
the corresponding great circle distance.
Parameters
----------
euclid3_distance: float scalar or numpy array
The euclidean three-space distance(s) between points on a
unit sphere, thus between [0,2].
Returns
-------
great_circle_dist: float scalar or numpy array
The corresponding great circle distance(s) between the points.
"""
# Eliminate some possible numerical errors:
euclid3_distance[euclid3_distance > 2.0] = 2.0
return 180.0 - 360.0 / np.pi * np.arccos(0.5 * euclid3_distance)
def _adjust_for_anisotropy(X, center, scaling, angle):
"""Adjusts data coordinates to take into account anisotropy.
Can also be used to take into account data scaling. Angles are CCW about
specified axes. Scaling is applied in rotated coordinate system.
Parameters
----------
X : ndarray
float array [n_samples, n_dim], the input array of coordinates
center : ndarray
float array [n_dim], the coordinate of centers
scaling : ndarray
float array [n_dim - 1], the scaling of last two dimensions
angle : ndarray
float array [2*n_dim - 3], the anisotropy angle (degrees)
Returns
-------
X_adj : ndarray
float array [n_samples, n_dim], the X array adjusted for anisotropy.
"""
center = np.asarray(center)[None, :]
angle = np.asarray(angle) * np.pi / 180
X -= center
Ndim = X.shape[1]
if Ndim == 1:
raise NotImplementedError("Not implemnented yet?")
elif Ndim == 2:
stretch = np.array([[1, 0], [0, scaling[0]]])
rot_tot = np.array(
[
[np.cos(-angle[0]), -np.sin(-angle[0])],
[np.sin(-angle[0]), np.cos(-angle[0])],
]
)
elif Ndim == 3:
stretch = np.array(
[[1.0, 0.0, 0.0], [0.0, scaling[0], 0.0], [0.0, 0.0, scaling[1]]]
)
rotate_x = np.array(
[
[1.0, 0.0, 0.0],
[0.0, np.cos(-angle[0]), -np.sin(-angle[0])],
[0.0, np.sin(-angle[0]), np.cos(-angle[0])],
]
)
rotate_y = np.array(
[
[np.cos(-angle[1]), 0.0, np.sin(-angle[1])],
[0.0, 1.0, 0.0],
[-np.sin(-angle[1]), 0.0, np.cos(-angle[1])],
]
)
rotate_z = np.array(
[
[np.cos(-angle[2]), -np.sin(-angle[2]), 0.0],
[np.sin(-angle[2]), np.cos(-angle[2]), 0.0],
[0.0, 0.0, 1.0],
]
)
rot_tot = np.dot(rotate_z, np.dot(rotate_y, rotate_x))
else:
raise ValueError(
"Adjust for anisotropy function doesn't support ND spaces where N>3"
)
X_adj = np.dot(stretch, np.dot(rot_tot, X.T)).T
X_adj += center
return X_adj
def _make_variogram_parameter_list(variogram_model, variogram_model_parameters):
"""Converts the user input for the variogram model parameters into the
format expected in the rest of the code.
Makes a list of variogram model parameters in the expected order if the
user has provided the model parameters. If not, returns None, which
will ensure that the automatic variogram estimation routine is
triggered.
Parameters
----------
variogram_model : str
specifies the variogram model type
variogram_model_parameters : list, dict, or None
parameters provided by the user, can also be None if the user
did not specify the variogram model parameters; if None,
this function returns None, that way the automatic variogram
estimation routine will kick in down the road...
Returns
-------
parameter_list : list
variogram model parameters stored in a list in the expected order;
if variogram_model is 'custom', model parameters should already
be encapsulated in a list, so the list is returned unaltered;
if variogram_model_parameters was not specified by the user,
None is returned; order for internal variogram models is as follows...
linear - [slope, nugget]
power - [scale, exponent, nugget]
gaussian - [psill, range, nugget]
spherical - [psill, range, nugget]
exponential - [psill, range, nugget]
hole-effect - [psill, range, nugget]
"""
if variogram_model_parameters is None:
parameter_list = None
elif type(variogram_model_parameters) is dict:
if variogram_model in ["linear"]:
if (
"slope" not in variogram_model_parameters.keys()
or "nugget" not in variogram_model_parameters.keys()
):
raise KeyError(
"'linear' variogram model requires 'slope' "
"and 'nugget' specified in variogram model "
"parameter dictionary."
)
else:
parameter_list = [
variogram_model_parameters["slope"],
variogram_model_parameters["nugget"],
]
elif variogram_model in ["power"]:
if (
"scale" not in variogram_model_parameters.keys()
or "exponent" not in variogram_model_parameters.keys()
or "nugget" not in variogram_model_parameters.keys()
):
raise KeyError(
"'power' variogram model requires 'scale', "
"'exponent', and 'nugget' specified in "
"variogram model parameter dictionary."
)
else:
parameter_list = [
variogram_model_parameters["scale"],
variogram_model_parameters["exponent"],
variogram_model_parameters["nugget"],
]
elif variogram_model in ["gaussian", "spherical", "exponential", "hole-effect"]:
if (
"range" not in variogram_model_parameters.keys()
or "nugget" not in variogram_model_parameters.keys()
):
raise KeyError(
"'%s' variogram model requires 'range', "
"'nugget', and either 'sill' or 'psill' "
"specified in variogram model parameter "
"dictionary." % variogram_model
)
else:
if "sill" in variogram_model_parameters.keys():
parameter_list = [
variogram_model_parameters["sill"]
- variogram_model_parameters["nugget"],
variogram_model_parameters["range"],
variogram_model_parameters["nugget"],
]
elif "psill" in variogram_model_parameters.keys():
parameter_list = [
variogram_model_parameters["psill"],
variogram_model_parameters["range"],
variogram_model_parameters["nugget"],
]
else:
raise KeyError(
"'%s' variogram model requires either "
"'sill' or 'psill' specified in "
"variogram model parameter "
"dictionary." % variogram_model
)
elif variogram_model in ["custom"]:
raise TypeError(
"For user-specified custom variogram model, "
"parameters must be specified in a list, "
"not a dict."
)
else:
raise ValueError(
"Specified variogram model must be one of the "
"following: 'linear', 'power', 'gaussian', "
"'spherical', 'exponential', 'hole-effect', "
"'custom'."
)
elif type(variogram_model_parameters) is list:
if variogram_model in ["linear"]:
if len(variogram_model_parameters) != 2:
raise ValueError(
"Variogram model parameter list must have "
"exactly two entries when variogram model "
"set to 'linear'."
)
parameter_list = variogram_model_parameters
elif variogram_model in ["power"]:
if len(variogram_model_parameters) != 3:
raise ValueError(
"Variogram model parameter list must have "
"exactly three entries when variogram model "
"set to 'power'."
)
parameter_list = variogram_model_parameters
elif variogram_model in ["gaussian", "spherical", "exponential", "hole-effect"]:
if len(variogram_model_parameters) != 3:
raise ValueError(
"Variogram model parameter list must have "
"exactly three entries when variogram model "
"set to '%s'." % variogram_model
)
parameter_list = [
variogram_model_parameters[0] - variogram_model_parameters[2],
variogram_model_parameters[1],
variogram_model_parameters[2],
]
elif variogram_model in ["custom"]:
parameter_list = variogram_model_parameters
else:
raise ValueError(
"Specified variogram model must be one of the "
"following: 'linear', 'power', 'gaussian', "
"'spherical', 'exponential', 'hole-effect', "
"'custom'."
)
else:
raise TypeError(
"Variogram model parameters must be provided in either "
"a list or a dict when they are explicitly specified."
)
return parameter_list
def _initialize_variogram_model(
X,
y,
variogram_model,
variogram_model_parameters,
variogram_function,
nlags,
weight,
coordinates_type,
):
"""Initializes the variogram model for kriging. If user does not specify
parameters, calls automatic variogram estimation routine.
Returns lags, semivariance, and variogram model parameters.
Parameters
----------
X: ndarray
float array [n_samples, n_dim], the input array of coordinates
y: ndarray
float array [n_samples], the input array of values to be kriged
variogram_model: str
user-specified variogram model to use
variogram_model_parameters: list
user-specified parameters for variogram model
variogram_function: callable
function that will be called to evaluate variogram model
(only used if user does not specify variogram model parameters)
nlags: int
integer scalar, number of bins into which to group inter-point distances
weight: bool
boolean flag that indicates whether the semivariances at smaller lags
should be weighted more heavily in the automatic variogram estimation
coordinates_type: str
type of coordinates in X array, can be 'euclidean' for standard
rectangular coordinates or 'geographic' if the coordinates are lat/lon
Returns
-------
lags: ndarray
float array [nlags], distance values for bins into which the
semivariances were grouped
semivariance: ndarray
float array [nlags], averaged semivariance for each bin
variogram_model_parameters: list
parameters for the variogram model, either returned unaffected if the
user specified them or returned from the automatic variogram
estimation routine
"""
# distance calculation for rectangular coords now leverages
# scipy.spatial.distance's pdist function, which gives pairwise distances
# in a condensed distance vector (distance matrix flattened to a vector)
# to calculate semivariances...
if coordinates_type == "euclidean":
d = pdist(X, metric="euclidean")
g = 0.5 * pdist(y[:, None], metric="sqeuclidean")
# geographic coordinates only accepted if the problem is 2D
# assume X[:, 0] ('x') => lon, X[:, 1] ('y') => lat
# old method of distance calculation is retained here...
# could be improved in the future
elif coordinates_type == "geographic":
if X.shape[1] != 2:
raise ValueError(
"Geographic coordinate type only supported for 2D datasets."
)
x1, x2 = np.meshgrid(X[:, 0], X[:, 0], sparse=True)
y1, y2 = np.meshgrid(X[:, 1], X[:, 1], sparse=True)
z1, z2 = np.meshgrid(y, y, sparse=True)
d = great_circle_distance(x1, y1, x2, y2)
g = 0.5 * (z1 - z2) ** 2.0
indices = np.indices(d.shape)
d = d[(indices[0, :, :] > indices[1, :, :])]
g = g[(indices[0, :, :] > indices[1, :, :])]
else:
raise ValueError(
"Specified coordinate type '%s' is not supported." % coordinates_type
)
# Equal-sized bins are now implemented. The upper limit on the bins
# is appended to the list (instead of calculated as part of the
# list comprehension) to avoid any numerical oddities
# (specifically, say, ending up as 0.99999999999999 instead of 1.0).
# Appending dmax + 0.001 ensures that the largest distance value
# is included in the semivariogram calculation.
dmax = np.amax(d)
dmin = np.amin(d)
dd = (dmax - dmin) / nlags
bins = [dmin + n * dd for n in range(nlags)]
dmax += 0.001
bins.append(dmax)
# This old binning method was experimental and doesn't seem
# to work too well. Bins were computed such that there are more
# at shorter lags. This effectively weights smaller distances more
# highly in determining the variogram. As Kitanidis points out,
# the variogram fit to the data at smaller lag distances is more
# important. However, the value at the largest lag probably ends up
# being biased too high for the larger values and thereby throws off
# automatic variogram calculation and confuses comparison of the
# semivariogram with the variogram model.
#
# dmax = np.amax(d)
# dmin = np.amin(d)
# dd = dmax - dmin
# bins = [dd*(0.5**n) + dmin for n in range(nlags, 1, -1)]
# bins.insert(0, dmin)
# bins.append(dmax)
lags = np.zeros(nlags)
semivariance = np.zeros(nlags)
for n in range(nlags):
# This 'if... else...' statement ensures that there are data
# in the bin so that numpy can actually find the mean. If we
# don't test this first, then Python kicks out an annoying warning
# message when there is an empty bin and we try to calculate the mean.
if d[(d >= bins[n]) & (d < bins[n + 1])].size > 0:
lags[n] = np.mean(d[(d >= bins[n]) & (d < bins[n + 1])])
semivariance[n] = np.mean(g[(d >= bins[n]) & (d < bins[n + 1])])
else:
lags[n] = np.nan
semivariance[n] = np.nan
lags = lags[~np.isnan(semivariance)]
semivariance = semivariance[~np.isnan(semivariance)]
# a few tests the make sure that, if the variogram_model_parameters
# are supplied, they have been supplied as expected...
# if variogram_model_parameters was not defined, then estimate the variogram
if variogram_model_parameters is not None:
if variogram_model == "linear" and len(variogram_model_parameters) != 2:
raise ValueError(
"Exactly two parameters required for linear variogram model."
)
elif (
variogram_model
in ["power", "spherical", "exponential", "gaussian", "hole-effect"]
and len(variogram_model_parameters) != 3
):
raise ValueError(
"Exactly three parameters required for "
"%s variogram model" % variogram_model
)
else:
if variogram_model == "custom":
raise ValueError(
"Variogram parameters must be specified when "
"implementing custom variogram model."
)
else:
variogram_model_parameters = _calculate_variogram_model(
lags, semivariance, variogram_model, variogram_function, weight
)
return lags, semivariance, variogram_model_parameters
def _variogram_residuals(params, x, y, variogram_function, weight):
"""Function used in variogram model estimation. Returns residuals between
calculated variogram and actual data (lags/semivariance).
Called by _calculate_variogram_model.
Parameters
----------
params: list or 1D array
parameters for calculating the model variogram
x: ndarray
lags (distances) at which to evaluate the model variogram
y: ndarray
experimental semivariances at the specified lags
variogram_function: callable
the actual funtion that evaluates the model variogram
weight: bool
flag for implementing the crude weighting routine, used in order to
fit smaller lags better
Returns
-------
resid: 1d array
residuals, dimension same as y
"""
# this crude weighting routine can be used to better fit the model
# variogram to the experimental variogram at smaller lags...
# the weights are calculated from a logistic function, so weights at small
# lags are ~1 and weights at the longest lags are ~0;
# the center of the logistic weighting is hard-coded to be at 70% of the
# distance from the shortest lag to the largest lag
if weight:
drange = np.amax(x) - np.amin(x)
k = 2.1972 / (0.1 * drange)
x0 = 0.7 * drange + np.amin(x)
weights = 1.0 / (1.0 + np.exp(-k * (x0 - x)))
weights /= np.sum(weights)
resid = (variogram_function(params, x) - y) * weights
else:
resid = variogram_function(params, x) - y
return resid
def _calculate_variogram_model(
lags, semivariance, variogram_model, variogram_function, weight
):
"""Function that fits a variogram model when parameters are not specified.
Returns variogram model parameters that minimize the RMSE between the
specified variogram function and the actual calculated variogram points.
Parameters
----------
lags: 1d array
binned lags/distances to use for variogram model parameter estimation
semivariance: 1d array
binned/averaged experimental semivariances to use for variogram model
parameter estimation
variogram_model: str/unicode
specified variogram model to use for parameter estimation
variogram_function: callable
the actual funtion that evaluates the model variogram
weight: bool
flag for implementing the crude weighting routine, used in order to fit
smaller lags better this is passed on to the residual calculation
cfunction, where weighting is actually applied...
Returns
-------
res: list
list of estimated variogram model parameters
NOTE that the estimation routine works in terms of the partial sill
(psill = sill - nugget) -- setting bounds such that psill > 0 ensures that
the sill will always be greater than the nugget...
"""
if variogram_model == "linear":
x0 = [
(np.amax(semivariance) - np.amin(semivariance))
/ (np.amax(lags) - np.amin(lags)),
np.amin(semivariance),
]
bnds = ([0.0, 0.0], [np.inf, np.amax(semivariance)])
elif variogram_model == "power":
x0 = [
(np.amax(semivariance) - np.amin(semivariance))
/ (np.amax(lags) - np.amin(lags)),
1.1,
np.amin(semivariance),
]
bnds = ([0.0, 0.001, 0.0], [np.inf, 1.999, np.amax(semivariance)])
else:
x0 = [
np.amax(semivariance) - np.amin(semivariance),
0.25 * np.amax(lags),
np.amin(semivariance),
]
bnds = (
[0.0, 0.0, 0.0],
[10.0 * np.amax(semivariance), np.amax(lags), np.amax(semivariance)],
)
# use 'soft' L1-norm minimization in order to buffer against
# potential outliers (weird/skewed points)
res = least_squares(
_variogram_residuals,
x0,
bounds=bnds,
loss="soft_l1",
args=(lags, semivariance, variogram_function, weight),
)
return res.x
def _krige(
X,
y,
coords,
variogram_function,
variogram_model_parameters,
coordinates_type,
pseudo_inv=False,
):
"""Sets up and solves the ordinary kriging system for the given
coordinate pair. This function is only used for the statistics calculations.
Parameters
----------
X: ndarray
float array [n_samples, n_dim], the input array of coordinates
y: ndarray
float array [n_samples], the input array of measurement values
coords: ndarray
float array [1, n_dim], point at which to evaluate the kriging system
variogram_function: callable
function that will be called to evaluate variogram model
variogram_model_parameters: list
user-specified parameters for variogram model
coordinates_type: str
type of coordinates in X array, can be 'euclidean' for standard
rectangular coordinates or 'geographic' if the coordinates are lat/lon
pseudo_inv : :class:`bool`, optional
Whether the kriging system is solved with the pseudo inverted
kriging matrix. If `True`, this leads to more numerical stability
and redundant points are averaged. But it can take more time.
Default: False
Returns
-------
zinterp: float
kriging estimate at the specified point
sigmasq: float
mean square error of the kriging estimate
"""
zero_index = None
zero_value = False
# calculate distance between points... need a square distance matrix
# of inter-measurement-point distances and a vector of distances between
# measurement points (X) and the kriging point (coords)
if coordinates_type == "euclidean":
d = squareform(pdist(X, metric="euclidean"))
bd = np.asarray(cdist(X, coords[None, :], metric="euclidean")).ravel()
# geographic coordinate distances still calculated in the old way...
# assume X[:, 0] ('x') => lon, X[:, 1] ('y') => lat
# also assume problem is 2D; check done earlier in initializing variogram
elif coordinates_type == "geographic":
x1, x2 = np.meshgrid(X[:, 0], X[:, 0], sparse=True)
y1, y2 = np.meshgrid(X[:, 1], X[:, 1], sparse=True)
d = great_circle_distance(x1, y1, x2, y2)
bd = np.asarray(
great_circle_distance(
X[:, 0],
X[:, 1],
coords[0] * np.ones(X.shape[0]),
coords[1] * np.ones(X.shape[0]),
)
).ravel()
# this check is done when initializing variogram, but kept here anyways...
else:
raise ValueError(
"Specified coordinate type '%s' is not supported." % coordinates_type
)
# check if kriging point overlaps with measurement point
if np.any(np.absolute(bd) <= 1e-10):
zero_value = True
zero_index = int(np.flatnonzero(bd <= 1e-10)[0])
# set up kriging matrix
n = X.shape[0]
a = np.zeros((n + 1, n + 1))
a[:n, :n] = -variogram_function(variogram_model_parameters, d)
np.fill_diagonal(a, 0.0)
a[n, :] = 1.0
a[:, n] = 1.0
a[n, n] = 0.0
# set up RHS
b = np.zeros((n + 1, 1))
b[:n, 0] = -variogram_function(variogram_model_parameters, bd)
if zero_value:
b[zero_index, 0] = 0.0
b[n, 0] = 1.0
# solve
if pseudo_inv:
res = np.linalg.lstsq(a, b, rcond=None)[0]
else:
res = np.linalg.solve(a, b)
zinterp = np.sum(res[:n, 0] * y)
sigmasq = np.sum(res[:, 0] * -b[:, 0])
return zinterp, sigmasq
def _find_statistics(
X,
y,
variogram_function,
variogram_model_parameters,
coordinates_type,
pseudo_inv=False,
):
"""Calculates variogram fit statistics.
Returns the delta, sigma, and epsilon values for the variogram fit.
These arrays are used for statistics calculations.
Parameters
----------
X: ndarray
float array [n_samples, n_dim], the input array of coordinates
y: ndarray
float array [n_samples], the input array of measurement values
variogram_function: callable
function that will be called to evaluate variogram model
variogram_model_parameters: list
user-specified parameters for variogram model
coordinates_type: str
type of coordinates in X array, can be 'euclidean' for standard
rectangular coordinates or 'geographic' if the coordinates are lat/lon
pseudo_inv : :class:`bool`, optional
Whether the kriging system is solved with the pseudo inverted
kriging matrix. If `True`, this leads to more numerical stability
and redundant points are averaged. But it can take more time.
Default: False
Returns
-------
delta: ndarray
residuals between observed values and kriged estimates for those values
sigma: ndarray
mean error in kriging estimates
epsilon: ndarray
residuals normalized by their mean error
"""
delta = np.zeros(y.shape)
sigma = np.zeros(y.shape)
for i in range(y.shape[0]):
# skip the first value in the kriging problem
if i == 0:
continue
else:
k, ss = _krige(
X[:i, :],
y[:i],
X[i, :],
variogram_function,
variogram_model_parameters,
coordinates_type,
pseudo_inv,
)
# if the estimation error is zero, it's probably because
# the evaluation point X[i, :] is really close to one of the
# kriging system points in X[:i, :]...
# in the case of zero estimation error, the results are not stored
if np.absolute(ss) < eps:
continue
delta[i] = y[i] - k
sigma[i] = np.sqrt(ss)
# only use non-zero entries in these arrays... sigma is used to pull out
# non-zero entries in both cases because it is guaranteed to be positive,
# whereas delta can be either positive or negative
delta = delta[sigma > eps]
sigma = sigma[sigma > eps]
epsilon = delta / sigma
return delta, sigma, epsilon
def calcQ1(epsilon):
"""Returns the Q1 statistic for the variogram fit (see [1])."""
return abs(np.sum(epsilon) / (epsilon.shape[0] - 1))
def calcQ2(epsilon):
"""Returns the Q2 statistic for the variogram fit (see [1])."""
return np.sum(epsilon**2) / (epsilon.shape[0] - 1)
def calc_cR(Q2, sigma):
"""Returns the cR statistic for the variogram fit (see [1])."""
return Q2 * np.exp(np.sum(np.log(sigma**2)) / sigma.shape[0])