-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUtilities.py
More file actions
4094 lines (3624 loc) · 171 KB
/
Copy pathUtilities.py
File metadata and controls
4094 lines (3624 loc) · 171 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
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
# ===============================================================================
# Copyright 2021 An-Jun Liu
# Last Modified Date: 12/28/2021
# ===============================================================================
import os
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from math import pi
def r2_score(y_true, y_pred):
"""v3.8.81: local R² (coefficient of determination). pyADR used sklearn
ONLY for this one function, and importing sklearn cost ~7s on a cold
start. This matches sklearn.metrics.r2_score for the 1-D, equal-weight,
single-output case used throughout (fit-quality scoring), so T₀ / fit
selection are numerically unchanged."""
y_true = np.asarray(y_true, dtype=float)
y_pred = np.asarray(y_pred, dtype=float)
ss_res = float(np.sum((y_true - y_pred) ** 2))
ss_tot = float(np.sum((y_true - np.mean(y_true)) ** 2))
if ss_tot == 0.0:
return 1.0 if ss_res == 0.0 else 0.0
return 1.0 - ss_res / ss_tot
import matplotlib.patches as patches
from scipy.stats import norm
import seaborn as sns; sns.set()
from datetime import date
DEBUG = 1
# ===============================================================================
# CSV format normalization (V2.0 88-col K/Ca → V3.7 98-col Ca/K, in-memory)
# ===============================================================================
def normalize_csv_to_v37(data):
"""Detect V2.0 (88 cols, K/Ca) vs V3.7 (98 cols, Ca/K) and return V3.7-form.
V2.0 → V3.7 transformations:
1. col 23-24 K/Ca → Ca/K (1/x with std propagation)
2. Append 10 isochron cols (88-97) computed from raw Ar component cols
V3.7 → unchanged.
Input : list of CSV lines (with newlines)
Output : list of CSV lines (V3.7 format)
"""
if not data:
return data
header = data[0].rstrip()
cols = header.split(',')
n = len(cols)
if n == 98 and len(cols) > 23 and cols[23] == 'Ca/K':
return data # already V3.7
if not (n == 88 and len(cols) > 23 and cols[23] == 'K/Ca'):
return data # unknown format — pass through
# Build new V3.7 header
new_cols = list(cols)
new_cols[23] = 'Ca/K'
new_cols[24] = 'Ca/K_std'
new_cols += [
'normal isochron', '40Ar(m)/36Ar(m)', '40Ar(m)/36Ar(m)_std',
'39Ar(m)/36Ar(m)', '39Ar(m)/36Ar(m)_std',
'inverse isochron', '36Ar(m)/40Ar(m)', '36Ar(m)/40Ar(m)_std',
'39Ar(m)/40Ar(m)', '39Ar(m)/40Ar(m)_std',
]
new_data = [','.join(new_cols) + '\n']
def _f(parts, idx):
try:
return float(parts[idx])
except (ValueError, IndexError):
return 0.0
for line in data[1:]:
if not line.strip():
new_data.append(line)
continue
parts = line.rstrip('\n\r').split(',')
if len(parts) < 88:
new_data.append(line)
continue
# 1. col 23-24 K/Ca → Ca/K
kca = _f(parts, 23)
kca_std = _f(parts, 24)
if kca > 0:
cak = 1.0 / kca
cak_std = kca_std / (kca * kca)
else:
cak, cak_std = 0.0, 0.0
parts[23] = repr(cak)
parts[24] = repr(cak_std)
# 2. compute isochron sums + ratios from raw Ar components
ar36_a, ar36_a_s = _f(parts, 26), _f(parts, 27)
ar36_c, ar36_c_s = _f(parts, 28), _f(parts, 29)
ar36_ca, ar36_ca_s = _f(parts, 30), _f(parts, 31)
ar36_cl, ar36_cl_s = _f(parts, 32), _f(parts, 33)
ar39_k, ar39_k_s = _f(parts, 46), _f(parts, 47)
ar39_ca, ar39_ca_s = _f(parts, 48), _f(parts, 49)
ar40_r, ar40_r_s = _f(parts, 50), _f(parts, 51)
ar40_a, ar40_a_s = _f(parts, 52), _f(parts, 53)
ar40_c, ar40_c_s = _f(parts, 54), _f(parts, 55)
ar40_k, ar40_k_s = _f(parts, 56), _f(parts, 57)
ar36_m = ar36_a + ar36_c + ar36_ca + ar36_cl
# v3.8.1 FIX: correlated-σ — components are derived FROM Ar36_m_raw, so
# σ²_36a = σ²_36m_raw + σ²_36ca + σ²_36cl + σ²_36c. Recover raw σ:
_v36 = ar36_a_s**2 - ar36_c_s**2 - ar36_ca_s**2 - ar36_cl_s**2
ar36_m_s = (_v36 ** 0.5) if _v36 > 0 else abs(ar36_a_s)
ar39_m = ar39_k + ar39_ca
# v3.8.1 FIX: same correction. Ar39_k = Ar39_m_raw − Ar39_ca.
_v39 = ar39_k_s**2 - ar39_ca_s**2
ar39_m_s = (_v39 ** 0.5) if _v39 > 0 else abs(ar39_k_s)
ar40_m = ar40_r + ar40_a + ar40_c + ar40_k
# v3.8.1 FIX: same correction (analogous to v3.7.4-hotfix on this σ in DFN block).
_v40 = ar40_r_s**2 - ar40_a_s**2 - ar40_c_s**2 - ar40_k_s**2
ar40_m_s = (_v40 ** 0.5) if _v40 > 0 else abs(ar40_r_s)
def _ratio(num, num_s, den, den_s):
if den == 0 or num == 0:
return 0.0, 0.0
r = num / den
r_s = abs(r) * ((num_s / num) ** 2 + (den_s / den) ** 2) ** 0.5
return r, r_s
r40_36, r40_36_s = _ratio(ar40_m, ar40_m_s, ar36_m, ar36_m_s)
r39_36, r39_36_s = _ratio(ar39_m, ar39_m_s, ar36_m, ar36_m_s)
r36_40, r36_40_s = _ratio(ar36_m, ar36_m_s, ar40_m, ar40_m_s)
r39_40, r39_40_s = _ratio(ar39_m, ar39_m_s, ar40_m, ar40_m_s)
parts.extend([
'normal isochron', repr(r40_36), repr(r40_36_s),
repr(r39_36), repr(r39_36_s),
'inverse isochron', repr(r36_40), repr(r36_40_s),
repr(r39_40), repr(r39_40_s),
])
new_data.append(','.join(parts) + '\n')
return new_data
# Utilities function
def ratioSigma(mu_y, sigma_y, mu_x, sigma_x,ratio):
return np.sqrt((sigma_y/mu_y)**2 + (sigma_x/mu_x)**2)*ratio
def minusSigma(sigma_x, sigma_y):
return np.sqrt(sigma_x**2 + sigma_y**2)
# v3.8.5: York 2004 unified regression — used by getDFStatistics_sh when
# isochron_method='york'. Reference: York et al. (2004) Am.J.Phys. 72:367.
# Cov formula from Mahon (1996) / Schaen et al. (2021) Eq.14b.
def york_regression(x, sx, y, sy, rho_xy=None, max_iter=50, tol=1e-12):
"""York 2004 unified equations for best-fit line with errors in both
x and y (and optional per-point correlation). Iterates slope b until
converged.
Returns: (slope, intercept, sigma_slope, sigma_intercept, MSWD, cov_ab)"""
x = np.asarray(x, float); y = np.asarray(y, float)
sx = np.asarray(sx, float); sy = np.asarray(sy, float)
n = len(x)
if n < 2:
return 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
if rho_xy is None:
rho_xy = np.zeros(n)
else:
rho_xy = np.asarray(rho_xy, float)
wx = 1.0 / (sx**2 + 1e-300)
wy = 1.0 / (sy**2 + 1e-300)
b = np.polyfit(x, y, 1)[0] # OLS seed for slope
X_bar = Y_bar = 0.0; beta = np.zeros(n); W = np.ones(n)
for _ in range(max_iter):
alpha = np.sqrt(wx * wy)
W = wx * wy / (wx + b*b * wy - 2*b*rho_xy*alpha + 1e-300)
X_bar = np.sum(W*x) / np.sum(W)
Y_bar = np.sum(W*y) / np.sum(W)
U = x - X_bar
V = y - Y_bar
beta = W * (U/wy + b*V/wx - (b*U + V)*rho_xy/alpha)
num = np.sum(W * beta * V)
den = np.sum(W * beta * U)
if abs(den) < 1e-300:
break
b_new = num / den
if abs(b_new - b) < tol * max(1.0, abs(b)):
b = b_new
break
b = b_new
a = Y_bar - b * X_bar
x_adj = X_bar + beta
x_adj_bar = np.sum(W*x_adj) / np.sum(W)
u = x_adj - x_adj_bar
sb2 = 1.0 / np.sum(W*u*u)
sa2 = 1.0/np.sum(W) + x_adj_bar*x_adj_bar*sb2
cov_ab = -x_adj_bar * sb2 # Mahon 1996
if n > 2:
chi2 = np.sum(W * (y - b*x - a)**2)
mswd = chi2 / (n - 2)
else:
mswd = 0.0
return (float(b), float(a),
float(np.sqrt(max(sb2, 0))), float(np.sqrt(max(sa2, 0))),
float(mswd), float(cov_ab))
# T0 regression fitting functions
# ===============================================================================
def linear(x, a, b):
return a*x + b
def average(x, a):
return 0*x + a
fit_func_list = [linear, average]
# functions for button
# ===============================================================================
def calculateT0(fit_function_type, v_t, mask,num):
"""
Input:
1. fit_function_type: 0 for linear, 1 for average
2. v_t: raw voltage-time data
3. mask: table of selected points
Output:
return [status, T0, T0_SIGMA, R^2]
status:
0 success
1 failed at fitting data from which the outliers are removed
"""
# initialization
T0 = np.zeros(5)
T0_SIGMA = np.zeros(5)
R = np.zeros(5)
r = 0
status = 0
fig, axs= plt.subplots(2, 3, figsize = (16,8))
f = fit_func_list[fit_function_type]
# go over Ar 36 to 40
for i in range(5):
# first linear regression
# fit whole raw data (no outlier is removed)
n=0
t = v_t[i, :, 1]
v = v_t[i, :, 0]
popt, _ = curve_fit(f, t, v)
T0[i] = f(0, *popt)
T0_SIGMA[i] = (np.std(np.abs(v - f(t, *popt))))/(np.sqrt(num)) # std of the error
axs[i//3, i%3].plot(t, v, marker = 'o', label = "raw data")
axs[i//3, i%3].plot(t, f(t, *popt), linestyle = '--', label = "fitted line")
R[i] = r2_score(v,f(t, *popt))
axs[i//3, i%3].set(xlabel = "t (sec)", ylabel = "mV")
error = T0_SIGMA[i]
# second linear regression
# remove the manually selected outliers if necessary
if (R[i] <= 0.8):
x = 0
for j in range(num-1):
r = v[j-x]-f(t[j-x], *popt)
if r < 0 :
r = r *-1
if r > error and x < 4:
x = x+1
mask[i, j] = 0
n=n+1
if (mask[i, :] == 0).any():
selected_indices = np.where(mask[i, :] == 1)[0]
removed_indices = np.where(mask[i, :] == 0)[0]
t = v_t[i, selected_indices, 1]
v = v_t[i, selected_indices, 0]
try:
popt, _ = curve_fit(f, t, v)
T0[i] = f(0, *popt)
T0_SIGMA[i] = (np.std(np.abs(v - f(t, *popt))))/(np.sqrt((num-n))) # std of the error of second fit
R[i] = r2_score(v,f(t, *popt))
except:
status = 1
axs[i//3, i%3].plot(v_t[i, removed_indices, 1], v_t[i, removed_indices, 0], marker = 'x', markersize = 12, linestyle = 'None', color = 'r')
axs[i//3, i%3].ticklabel_format(axis='y', style='sci', scilimits=(0,0))
axs[i//3, i%3].plot(t, f(t, *popt), linestyle = '--', label = "fitted line\n(exclude outliers)")
axs[i//3, i%3].legend(bbox_to_anchor=(0.7,1.2), loc='upper left')
axs[i//3, i%3].set_title("Ar {}\n{} = {} \nerror = {}\nR^2 = {}".format(i+36, r'$T_{0}$', '{:0.5e}'.format(T0[i]), '{:0.5e}'.format(T0_SIGMA[i]),'{:0.5e}'.format(R[i])), loc='left')
axs[1,2].axis('off')
# v3.8.37: defensive wrap — matplotlib mathtext parser can ValueError on
# the '$T_{0}$' titles set above when rendering on Anaconda Py 3.13.
# tight_layout / savefig both invoke a renderer pass; if it fails just
# skip the layout polish (figure still saves, just may have tighter or
# cropped margins).
try:
plt.tight_layout()
except Exception:
pass
try:
plt.savefig(".work/LR.png", dpi=200)
except Exception:
pass
plt.clf()
plt.close("all")
return [status, T0, T0_SIGMA, R],mask
def REcalculateT0(fit_function_type, v_t, mask,num):
"""
Input:
1. fit_function_type: 0 for linear, 1 for average
2. v_t: raw voltage-time data
3. mask: table of selected points
Output:
return [status, T0, T0_SIGMA, R^2]
status:
0 success
1 failed at fitting data from which the outliers are removed
"""
# initialization
T0 = np.zeros(5)
T0_SIGMA = np.zeros(5)
R = np.zeros(5)
status = 0
fig, axs = plt.subplots(2, 3, figsize = (16,8))
f = fit_func_list[fit_function_type]
# go over Ar 36 to 40
for i in range(5):
# first linear regression
# fit whole raw data (no outlier is removed)
n=0
t = v_t[i, :, 1]
v = v_t[i, :, 0]
popt, _ = curve_fit(f, t, v)
T0[i] = f(0, *popt)
for j in range(num):
if mask[i,j]==0:
n=n+1
T0_SIGMA[i] = (np.std(np.abs(v - f(t, *popt))))/(np.sqrt((num-n))) # std of the error
axs[i//3, i%3].plot(t, v, marker = 'o', label = "raw data")
axs[i//3, i%3].plot(t, f(t, *popt), linestyle = '--', label = "fitted line")
R[i] = r2_score(v,f(t, *popt))
axs[i//3, i%3].set(xlabel = "t (sec)", ylabel = "mV")
# second linear regression
# remove the manually selected outliers if necessary
if (mask[i, :] == 0).any():
selected_indices = np.where(mask[i, :] == 1)[0]
removed_indices = np.where(mask[i, :] == 0)[0]
t = v_t[i, selected_indices, 1]
v = v_t[i, selected_indices, 0]
try:
popt, _ = curve_fit(f, t, v)
T0[i] = f(0, *popt)
T0_SIGMA[i] = (np.std(np.abs(v - f(t, *popt))))/(np.sqrt((num-n))) # std of the error of second fit
axs[i//3, i%3].plot(t, f(t, *popt), linestyle = '--', label = "fitted line\n(exclude outliers)")
R[i] = r2_score(v,f(t, *popt))
except:
status = 1
axs[i//3, i%3].plot(v_t[i, removed_indices, 1], v_t[i, removed_indices, 0], marker = 'x', markersize = 12, linestyle = 'None', color = 'r')
axs[i//3, i%3].ticklabel_format(axis='y', style='sci', scilimits=(0,0))
axs[i//3, i%3].legend(bbox_to_anchor=(0.7,1.2), loc='upper left')
axs[i//3, i%3].set_title("Ar {}\n{} = {} \nerror = {}\nR^2 = {}".format(i+36, r'$T_{0}$', '{:0.5e}'.format(T0[i]), '{:0.5e}'.format(T0_SIGMA[i]),'{:0.5e}'.format(R[i])), loc='left')
axs[1,2].axis('off')
# v3.8.37: defensive wrap — matplotlib mathtext parser can ValueError on
# the '$T_{0}$' titles set above when rendering on Anaconda Py 3.13.
# tight_layout / savefig both invoke a renderer pass; if it fails just
# skip the layout polish (figure still saves, just may have tighter or
# cropped margins).
try:
plt.tight_layout()
except Exception:
pass
try:
plt.savefig(".work/LR.png", dpi=200)
except Exception:
pass
plt.clf()
plt.close("all")
return [status, T0, T0_SIGMA, R]
def getDFStatistics_ls(file, mask,constants, Ncolor, Nmaker):
fig, n = plt.subplots()
with open(file, 'r') as f:
data = f.readlines()
# Normalize V2.0 (88-col K/Ca) → V3.7 (98-col Ca/K) in memory
data = normalize_csv_to_v37(data)
# Loose header check: 88 or 98 cols accepted
_hdr_cols = data[0].rstrip().split(',') if data else []
if len(_hdr_cols) not in (88, 98):
raise Exception(f"Wrong data format! Expected 88 or 98 cols, got {len(_hdr_cols)}")
i = 0
while i != (len(data)-2):
if data[i].split(',')[17] == "nan":
data.pop(i)
i=i-1
i=i+1
x = np.zeros(len(data)-2)
y = np.zeros(len(x))
x_std = np.zeros(len(x))
y_std = np.zeros(len(x))
T_all = np.zeros(len(x))
T_std_all = np.zeros(len(x))
T_sum = 0
mswd = 0
wma = 0
for i in range (len(data)-2):
x[i] = float(data[i+1].split(',')[46])/float(data[i+1].split(',')[7])
y[i] = float(data[i+1].split(',')[61])/float(data[i+1].split(',')[7])
x_std[i] = float(data[i+1].split(',')[47])/float(data[i+1].split(',')[8])
y_std[i] = float(data[i+1].split(',')[62])/float(data[i+1].split(',')[8])
T_all[i] = float(data[i+1].split(',')[17])
T_std_all[i] = float(data[i+1].split(',')[18])
j = 0
for i in range (len(y)):
if x[i-j] < 0 or y[i-j] < 0:
x = np.delete(x,[i-j])
y = np.delete(y,[i-j])
x_std = np.delete(x_std,[i-j])
y_std = np.delete(y_std,[i-j])
T_all = np.delete(T_all,[i-j])
T_std_all = np.delete(T_std_all,[i-j])
j = j+1
popt, _ = curve_fit(linear, x, y)
n.plot(x,y,marker = Nmaker,linestyle = 'None', label = "data")
n.plot(x, linear(x, *popt), linestyle = '--', label = "fitted line")
n.set_xlabel('39^Ar/36^Ar')
n.set_ylabel('40^Ar/36^Ar')
if (mask[:] == 0).any():
j = 0
for i in range(len(y)):
if(mask[i]==0):
fx = x[i-j]
fy = y[i-j]
x = np.delete(x,[i-j])
y = np.delete(y,[i-j])
x_std = np.delete(x_std,[i-j])
y_std = np.delete(y_std,[i-j])
T_all = np.delete(T_all,[i-j])
T_std_all = np.delete(T_std_all,[i-j])
n.plot(fx, fy, marker = 'x', markersize = 12, linestyle = 'None', color = 'r')
j=j+1
for i in range (len(y)):
t = np.linspace(0, 2*pi, 100)
n.plot( x[i]+x_std[i]*np.cos(t) , y[i]+y_std[i]*np.sin(t),color='lightgray',linestyle='-')
popt, _ = curve_fit(linear, x, y)
n.plot(x, linear(x, *popt), linestyle = '--', label = "fitted line(exclude outliers)", color = Ncolor)
popt, _ = curve_fit(linear, x, y)
n.plot(0, linear(0, *popt), marker = Nmaker,linestyle = 'None', label = "fitted line(exclude outliers)", color = 'r')
n = linear(0,*popt)
popt, _ = curve_fit(linear, x_std, y_std)
n_std = linear(0,*popt)
plt.savefig(".work/DFN.png", dpi = 200)
fig, iv = plt.subplots()
x = np.zeros(len(data)-2)
y = np.zeros(len(x))
x_std = np.zeros(len(x))
y_std = np.zeros(len(x))
for i in range (len(data)-2):
x[i] = float(data[i+1].split(',')[46])/float(data[i+1].split(',')[61])
y[i] = float(data[i+1].split(',')[7])/float(data[i+1].split(',')[61])
x_std[i] = float(data[i+1].split(',')[47])/float(data[i+1].split(',')[62])
y_std[i] = float(data[i+1].split(',')[8])/float(data[i+1].split(',')[62])
j = 0
for i in range (len(y)):
if x[i-j] < 0 or y[i-j] < 0:
x = np.delete(x,[i-j])
y = np.delete(y,[i-j])
x_std = np.delete(x_std,[i-j])
y_std = np.delete(y_std,[i-j])
j = j+1
popt, _ = curve_fit(linear, x, y)
iv.plot(x,y,marker = Nmaker,linestyle = 'None', label = "data")
iv.plot(x, linear(x, *popt), linestyle = '--', label = "fitted line")
iv.set_xlabel('39^Ar/40^Ar')
iv.set_ylabel('36^Ar/40^Ar')
if (mask[:] == 0).any():
j = 0
for i in range(len(y)):
if(mask[i]==0):
fx = x[i-j]
fy = y[i-j]
x = np.delete(x,[i-j])
y = np.delete(y,[i-j])
x_std = np.delete(x_std,[i-j])
y_std = np.delete(y_std,[i-j])
iv.plot(fx, fy, marker = 'x', markersize = 12, linestyle = 'None', color = 'r')
j=j+1
for i in range (len(y)):
t = np.linspace(0, 2*pi, 100)
iv.plot( x[i]+x_std[i]*np.cos(t) , y[i]+y_std[i]*np.sin(t),color='lightgray',linestyle='-')
iv.plot(x, linear(x, *popt), linestyle = '--', label = "fitted line(exclude outliers)",color = Ncolor)
# v3.8 FIX: capture full pcov for proper error propagation
popt_inv, pcov_inv = curve_fit(linear, x, y)
slope_inv = float(popt_inv[0])
intercept_inv = float(popt_inv[1])
slope_inv_std = float(np.sqrt(pcov_inv[0, 0])) if pcov_inv[0, 0] >= 0 else float('nan')
intercept_inv_std = float(np.sqrt(pcov_inv[1, 1])) if pcov_inv[1, 1] >= 0 else float('nan')
cov_si = float(pcov_inv[0, 1]) if np.isfinite(pcov_inv[0, 1]) else 0.0
a = -intercept_inv / slope_inv if slope_inv != 0 else 0.0
iv.plot(a, 0, marker=Nmaker, linestyle='None', label="fitted line(exclude outliers)", color='r')
iv = intercept_inv # Y-intercept = (36/40)_trapped, returned downstream
iv_std = intercept_inv_std
plt.savefig(".work/DFI.png", dpi=200)
# v3.8 FIX: F = -slope/intercept of inverse isochron (Vermeesch 2024 p.398).
# Previously: T = log(1 + J*iv) used Y-intercept (trapped 36/40) — physically wrong.
# Refs: Vermeesch (2024) Geochronology 6:398; Vermeesch (2018) Geosci Frontiers p.8;
# Schaen et al. (2021) GSA Bull. 133:461; Kuiper (2002) EPSL 203:501.
J = float(data[1].split(',')[4])
J_std = float(data[1].split(',')[5])
# v3.9.17 FIX: λ is constants[16]; constants[14] is Atmospheric Ratio 38/36(a)
# (=0.1885). The wrong index was inherited from the NTNU v3.7 fork and survived
# the v3.8.0 rewrite. Sister call sites already use [16]: getSHStatistics (T_total)
# and calcAge. The /1e6 makes Int age Ma, matching every neighbouring age column.
Lambda = float(constants[16])
if abs(intercept_inv) > 1e-300 and Lambda != 0:
F = -slope_inv / intercept_inv
# F = -b/a: dF/db = -1/a, dF/da = b/a^2
varF = (slope_inv_std / intercept_inv) ** 2 \
+ (slope_inv * intercept_inv_std / intercept_inv ** 2) ** 2 \
- 2.0 * (slope_inv / intercept_inv ** 3) * cov_si
F_std = float(np.sqrt(abs(varF)))
T = np.log(1.0 + J * F) / Lambda / 1e6 # yr -> Ma
T_std = np.sqrt((J**2 * F_std**2 + F**2 * J_std**2) / ((Lambda * (1.0 + F * J))**2)) / 1e6
else:
F = float('nan'); F_std = float('nan')
T = float('nan'); T_std = float('nan')
# v3.8 FIX: WMA = Σ(T/σ²) / Σ(1/σ²) (Vermeesch 2018 Eq.5; Schaen 2021 GSA Bull. p.470).
# Previously: loop divided 1/σ² by 1/σ² inside the sum → wma = Σ T_i (pure sum, not weighted mean).
_num = 0.0
_den = 0.0
for i in range(len(y)):
if T_std_all[i] != 0:
_w = 1.0 / (T_std_all[i] ** 2)
_num += _w * T_all[i]
_den += _w
wma = (_num / _den) if _den > 0 else 0.0
# v3.8 FIX: MSWD reference point = WMA (not arithmetic mean) — Schaen 2021 p.470.
if len(y) > 1 and _den > 0:
_m = 0.0
for i in range(len(y)):
if T_std_all[i] != 0:
_m += ((wma - T_all[i]) ** 2) / (T_std_all[i] ** 2)
mswd = _m / (len(y) - 1)
else:
mswd = 0.0
plt.clf()
plt.close("all")
return [n,n_std,iv,iv_std,mswd,wma,T,T_std]
def getDFStatistics_sh(file, mask, constants, Ncolor, Nmaker,
xlim=None, ylim=None, legend_name=None,
return_limits=False, show_temp=False,
show_atm=False, atm_ratio=298.56,
pname=None, style='pyADR',
iso_groups=None, group_colors=None,
return_points=False, show_legend=True,
show_group_fits=True, show_overall_fit=True,
isochron_method='ols', iso_limits=None):
"""
Generate isochron diagrams for step heating data.
Refactored version: preserves original architecture while integrating V2.5 bug fixes.
Parameters:
-----------
file : str
Path to data file
mask : array-like
Mask array for data selection (1=include, 0=exclude)
constants : array-like
Physical constants array
Ncolor : str
Color for fitted line (exclude outliers)
Nmaker : str
Marker style for data points
xlim : tuple, optional
X-axis limits (xmin, xmax)
ylim : tuple, optional
Y-axis limits (ymin, ymax)
legend_name : str, optional
Title for the plot
return_limits : bool, optional
If True, return axis limits
show_temp : bool, optional
If True, show temperature labels
show_atm : bool, optional
If True, show atmospheric value marker
atm_value : float, optional
Atmospheric 40Ar/36Ar value (default: 298.56)
Returns:
--------
list : [n_intercept, n_std, iv_intercept, iv_std, mswd, wma, T, T_std]
dict (optional) : {"DFN": (xlim, ylim), "DFI": (xlim, ylim)} if return_limits=True
"""
# =========================================================
# SETUP: Create output directory and initialize variables
# =========================================================
outdir = os.path.join(os.path.dirname(__file__), ".work")
os.makedirs(outdir, exist_ok=True)
# Set atmospheric value from parameter
atm_value = atm_ratio
# Initialize return values with safe defaults
n = np.nan
n_std = np.nan
iv = np.nan
iv_std = np.nan
mswd = 0
wma = 0
T = np.nan
T_std = np.nan
# Initialize axis limits
lim_DFN = ((0.0, 1.0), (0.0, 1.0))
lim_DFI = ((0.0, 1.0), (0.0, 1.0))
# =========================================================
# READ DATA: Load file with proper encoding
# =========================================================
print(f"\n[DEBUG] Reading file: {file}")
try:
with open(file, 'r', encoding='utf-8', errors='ignore') as f:
data = f.readlines()
# Normalize V2.0 (88-col K/Ca) → V3.7 (98-col Ca/K) in memory
data = normalize_csv_to_v37(data)
print(f"[DEBUG] Successfully read {len(data)} lines from file")
except Exception as e:
print(f"[ERROR] Cannot read file: {e}")
raise Exception(f"Cannot read file: {e}")
if len(data) < 2:
print(f"[ERROR] File too short: only {len(data)} lines")
raise Exception(f"File too short: only {len(data)} lines (need at least 2 lines)")
# Check header - allow both old (88 cols) and new (98 cols) format
print(f"[DEBUG] Checking header format...")
# Base header (first 88 columns)
base_header = "Samp#,Min,IRR,deg C,J,J_std,J_int,36Ar(a),36Ar(a)_std,37Ar(ca),37Ar(ca)_std,38Ar(cl),38Ar(cl)_std,39Ar(k),39Ar(k)_std,40Ar(r),40Ar(r)_std,Age(Ma),Age_std(Ma),40Ar(r)(%),39Ar(k)(%),40Ar(r)(%)(step heating),39Ar(k)(%)(step heating),Ca/K,Ca/K_std,Degassing Patterns,36Ar(a),36Ar(a)_std,36Ar(c),36Ar(c)_std,36Ar(ca),36Ar(ca)_std,36Ar(cl),36Ar(cl)_std,37Ar(ca),37Ar(ca)_std,38Ar(a),38Ar(a)_std,38Ar(c),38Ar(c)_std,38Ar(k),38Ar(k)_std,38Ar(ca),38Ar(ca)_std,38Ar(cl),38Ar(cl)_std,39Ar(k),39Ar(k)_std,39Ar(ca),39Ar(ca)_std,40Ar(r),40Ar(r)_std,40Ar(a),40Ar(a)_std,40Ar(c),40Ar(c)_std,40Ar(k),40Ar(k)_std,Additional Parameters,40(r)/39(k),40(r)/39(k)_std,40(r+a),40(r+a)_std,40Ar/39Ar,40Ar/39Ar_std,37Ar/39Ar,37Ar/39Ar_std,36Ar/39Ar,36Ar/39Ar_std,Parameters,39Ar/37Ar(ca),39Ar/37Ar(ca)_std,36Ar/37Ar(ca),36Ar/37Ar(ca)_std,40Ar/39Ar(k),40Ar/39Ar(k)_std,38Ar/39Ar(k),38Ar/39Ar(k)_std,39Ar/37Ar(k),39Ar/37Ar(k)_std,36Ar/38Ar(cl),36Ar/38Ar(cl)_std,40Ar/36Ar(a),40Ar/36Ar(a)_std,38Ar/36Ar(a),38Ar/36Ar(a)_std,Lambda,numCycle"
# Extended header (98 columns)
extended_header = base_header + ",normal isochron,40Ar(m)/36Ar(m),40Ar(m)/36Ar(m)_std,39Ar(m)/36Ar(m),39Ar(m)/36Ar(m)_std,inverse isochron,36Ar(m)/40Ar(m),36Ar(m)/40Ar(m)_std,39Ar(m)/40Ar(m),39Ar(m)/40Ar(m)_std"
actual_header = data[0].rstrip()
actual_col_count = len(actual_header.split(','))
# Accept both formats
if actual_header == base_header:
print(f"[DEBUG] Using OLD format (88 columns) - will calculate ratios")
elif actual_header == extended_header:
print(f"[DEBUG] Using NEW format (98 columns) - will read pre-calculated ratios")
else:
# Check if it's close enough (same number of columns)
if actual_col_count == 88 or actual_col_count == 98:
print(f"[WARNING] Header text doesn't match exactly but column count is correct ({actual_col_count})")
print(f"[WARNING] Proceeding anyway...")
else:
print(f"[ERROR] Header format mismatch!")
print(f"[ERROR] Expected 88 or 98 columns, got {actual_col_count}")
print(f"[ERROR] First 100 chars of actual header:")
print(f" {actual_header[:100]}")
raise Exception("Wrong data format!")
print(f"[DEBUG] Header check passed ✓")
# Remove rows with nan Age
# FIX (off-by-one): strip trailing blank lines first so detection is robust,
# then iterate over actual data rows (skip header at data[0]).
# Old code: while i != (len(data) - 2): ...
# That assumed exactly one trailing blank row and silently dropped the last
# real data row when the CSV had no trailing blank.
print(f"[DEBUG] Removing rows with nan Age...")
while data and not data[-1].strip():
data.pop()
original_rows = len(data) - 1
i = 1 # start from first data row, skip header
while i < len(data):
parts = data[i].split(',')
if len(parts) > 17 and parts[17].strip() == "nan":
print(f"[DEBUG] Removing row {i}: Age = nan")
data.pop(i)
# do not advance i; next row shifted into this slot
else:
i += 1
nstep = len(data) - 1
removed_rows = original_rows - nstep
print(f"[DEBUG] Removed {removed_rows} rows with nan Age")
print(f"[DEBUG] Valid data rows: {nstep}")
if nstep < 2:
print(f"[ERROR] Not enough valid data rows!")
print(f"[ERROR] Need at least 2 rows, but only have {nstep}")
raise Exception(f"Not enough steps to plot diagram. Need >=2, got {nstep}")
# =========================================================
# MASK HANDLING: Adjust mask size to match data
# =========================================================
mask = np.asarray(mask, dtype=float).copy()
if mask.size != nstep:
if mask.size < nstep:
# Pad with 1s (include by default)
mask = np.pad(mask, (0, nstep - mask.size), constant_values=1.0)
else:
# Truncate
mask = mask[:nstep]
# =========================================================
# HELPER FUNCTION: Apply user-defined axis controls
# =========================================================
def apply_controls(ax, target=None):
"""FIX: apply limits only when target matches pname (or pname is None).
v3.8.64: iso_limits={'DFN':(xl,yl),'DFI':(xl,yl)} (xl/yl tuple or None)
gives DFN and DFI independent axes — needed by AutoPipeline which shows
both isochrons at once. When iso_limits is given the legacy
pname/xlim/ylim path is bypassed (DiagramPlots_SH passes None → unchanged)."""
if iso_limits is not None:
_lim = iso_limits.get(target) or (None, None)
_xl, _yl = _lim
apply_x, apply_y = (_xl is not None), (_yl is not None)
else:
_apply = (pname is None) or (target is None) or (pname == target)
_xl, _yl = xlim, ylim
apply_x = _apply and (_xl is not None)
apply_y = _apply and (_yl is not None)
if apply_x:
xmin, xmax = float(_xl[0]), float(_xl[1])
if (xmax - xmin > 1e6) or (xmax <= xmin):
ax.autoscale(axis='x')
else:
ax.set_xlim(xmin, xmax)
if apply_y:
ymin, ymax = float(_yl[0]), float(_yl[1])
if (ymax - ymin > 1e6) or (ymax <= ymin):
ax.autoscale(axis='y')
else:
ax.set_ylim(ymin, ymax)
if legend_name is not None:
title_str = str(legend_name).strip()
if title_str:
ax.set_title(title_str)
# Classic or pyADR frame / ticks
_ist = _get_style(style)
if _ist.get('classic'):
ax.set_facecolor('white')
ax.tick_params(which='both', direction='out',
top=True, right=True, bottom=True, left=True)
ax.minorticks_on()
for _sp in ax.spines.values():
_sp.set_visible(True); _sp.set_linewidth(1.0); _sp.set_color('black')
else:
# v3.9.14: pyADR = seaborn 薰衣草底 + 白格線(同 DiagramPlot SH),
# 不再覆寫 facecolor
ax.tick_params(which='both', direction='out', top=False, right=False)
def _iso_savefig(fig_obj, outpath):
"""Save isochron figure with correct facecolor."""
_ist = _get_style(style)
fig_obj.savefig(outpath, dpi=300,
facecolor='white' if _ist.get('classic') else 'none')
# =========================================================
# NORMAL ISOCHRON: X = 39Ar(m)/36Ar(m), Y = 40Ar(m)/36Ar(m)
# Where (m) = measured = sum of all components
# =========================================================
fig_n, ax_n = plt.subplots(figsize=(8, 6), dpi=150) # Fixed aspect ratio
# Initialize data arrays
x = np.zeros(nstep)
y = np.zeros(nstep)
x_std = np.zeros(nstep)
y_std = np.zeros(nstep)
T_all = np.zeros(nstep)
T_std_all = np.zeros(nstep)
# Extract data from file
print(f"[DEBUG] Extracting data from {nstep} rows...")
for i in range(nstep):
parts = data[i + 1].split(',')
if len(parts) < 80:
print(f"[ERROR] Row {i+1} has only {len(parts)} columns (expected 80+)")
raise Exception(f"Row {i+1}: Insufficient columns ({len(parts)} < 80)")
# ========== READ ALL 36Ar COMPONENTS ==========
# 36Ar(a) - column 26, 27
try:
Ar36_a = float(parts[26])
Ar36_a_std = float(parts[27])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 36Ar(a)")
raise Exception(f"Row {i+1}: Invalid 36Ar(a) value")
# 36Ar(c) - column 28, 29
try:
Ar36_c = float(parts[28])
Ar36_c_std = float(parts[29])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 36Ar(c)")
raise Exception(f"Row {i+1}: Invalid 36Ar(c) value")
# 36Ar(ca) - column 30, 31
try:
Ar36_ca = float(parts[30])
Ar36_ca_std = float(parts[31])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 36Ar(ca)")
raise Exception(f"Row {i+1}: Invalid 36Ar(ca) value")
# 36Ar(cl) - column 32, 33
try:
Ar36_cl = float(parts[32])
Ar36_cl_std = float(parts[33])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 36Ar(cl)")
raise Exception(f"Row {i+1}: Invalid 36Ar(cl) value")
# Calculate 36Ar(m) = sum of all 36Ar components
Ar36_m = Ar36_a + Ar36_c + Ar36_ca + Ar36_cl
# v3.8.1 FIX: σ_36(a) and σ_36(c/ca/cl) are CORRELATED (see DFI block at L1142
# for full derivation). Recover raw σ: σ²_36m = σ²_36a − σ²_36ca − σ²_36cl − σ²_36c.
_var36m = Ar36_a_std**2 - Ar36_c_std**2 - Ar36_ca_std**2 - Ar36_cl_std**2
Ar36_m_std = float(np.sqrt(_var36m)) if _var36m > 0 else float(abs(Ar36_a_std))
# ========== READ ALL 39Ar COMPONENTS ==========
# 39Ar(k) - column 46, 47
try:
Ar39_k = float(parts[46])
Ar39_k_std = float(parts[47])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 39Ar(k)")
raise Exception(f"Row {i+1}: Invalid 39Ar(k) value")
# 39Ar(ca) - column 48, 49
try:
Ar39_ca = float(parts[48])
Ar39_ca_std = float(parts[49])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 39Ar(ca)")
raise Exception(f"Row {i+1}: Invalid 39Ar(ca) value")
# Calculate 39Ar(m) = sum of all 39Ar components
Ar39_m = Ar39_k + Ar39_ca
# v3.8.1 FIX: σ_39k and σ_39ca are CORRELATED (Ar39_k = Ar39_m_raw − Ar39_ca,
# so σ²_39k = σ²_39m_raw + σ²_39ca). Recover raw σ.
_var39m = Ar39_k_std**2 - Ar39_ca_std**2
Ar39_m_std = float(np.sqrt(_var39m)) if _var39m > 0 else float(abs(Ar39_k_std))
# ✅ BUG FIX: Calculate 40Ar(m) = 40Ar(r) + 40Ar(a) + 40Ar(c) + 40Ar(k)
# Columns: 50-51 (r), 52-53 (a), 54-55 (c), 56-57 (k)
try:
Ar40_r = float(parts[50])
Ar40_r_std = float(parts[51])
Ar40_a = float(parts[52])
Ar40_a_std = float(parts[53])
Ar40_c = float(parts[54])
Ar40_c_std = float(parts[55])
Ar40_k = float(parts[56])
Ar40_k_std = float(parts[57])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse 40Ar components")
for col in [50, 51, 52, 53, 54, 55, 56, 57]:
val = parts[col] if len(parts) > col else 'MISSING'
print(f" parts[{col}]={val}")
raise Exception(f"Row {i+1}: Invalid 40Ar component values")
Ar40_m = Ar40_r + Ar40_a + Ar40_c + Ar40_k
# FIX v3.7.4-hotfix: σ_40r and σ_40a are CORRELATED (both inherit σ_40m_raw via
# partition: 40Ar(r) = 40Ar(m) − 40Ar(air) − 40Ar(K)). Quadrature-summing them
# double-counts σ_40m_raw and inflates σ_40m by ~200×. Recover raw measurement σ:
# σ_40r² = σ_40m_raw² + σ_40air² + σ_40K² → σ_40m_raw² = σ_40r² − σ_40air² − σ_40K²
_var40m = Ar40_r_std**2 - Ar40_a_std**2 - Ar40_c_std**2 - Ar40_k_std**2
Ar40_m_std = float(np.sqrt(_var40m)) if _var40m > 0 else 0.0
if i == 0: # Print first row for debugging
print(f"[DEBUG] First row values:")
print(f" 36Ar(a) = {Ar36_a}")
print(f" 39Ar(k) = {Ar39_k}")
print(f" 40Ar(r) = {Ar40_r}")
print(f" 40Ar(a) = {Ar40_a}")
print(f" 40Ar(c) = {Ar40_c}")
print(f" 40Ar(k) = {Ar40_k}")
print(f" 40Ar(m) = {Ar40_m}")
print(f" 36Ar(m) = {Ar36_m}")
print(f" 39Ar(m) = {Ar39_m}")
# v3.8.1 FIX: ALWAYS recompute σ from raw components (same reason as DFI block
# at L1192). Pre-calculated σ in CSV cols 90/92 from V3.7 toDP / normalize_csv_to_v37
# used the buggy quadrature-sum that double-counts σ_36m_raw → MSWD artificially low.
# Ratio values read from CSV when available, σ always recomputed.
# Column 88 is "normal isochron" separator
_ratios_from_csv_n = False
if len(parts) > 92:
try:
y[i] = float(parts[89]) # 40Ar(m)/36Ar(m) value (OK to read)
x[i] = float(parts[91]) # 39Ar(m)/36Ar(m) value (OK to read)
_ratios_from_csv_n = True
if i == 0:
print(f"[DEBUG] Normal isochron: ratios from CSV cols 89/91, σ recomputed")
except (ValueError, IndexError):
_ratios_from_csv_n = False
if not _ratios_from_csv_n:
x[i] = Ar39_m / Ar36_m if Ar36_m != 0 else np.nan
y[i] = Ar40_m / Ar36_m if Ar36_m != 0 else np.nan
if Ar36_m != 0 and Ar36_m_std != 0 and Ar39_m != 0 and Ar40_m != 0:
x_std[i] = abs(x[i]) * np.sqrt((Ar39_m_std/Ar39_m)**2 + (Ar36_m_std/Ar36_m)**2)
y_std[i] = abs(y[i]) * np.sqrt((Ar40_m_std/Ar40_m)**2 + (Ar36_m_std/Ar36_m)**2)
else:
x_std[i] = np.nan
y_std[i] = np.nan
# Age data
try:
T_all[i] = float(parts[17])
T_std_all[i] = float(parts[18])
except (ValueError, IndexError) as e:
print(f"[ERROR] Row {i+1}: Cannot parse Age")
raise Exception(f"Row {i+1}: Invalid Age value")
print(f"[DEBUG] Data extraction complete")
# ✅ BUG FIX: Use numpy boolean indexing instead of manual deletion
valid = np.isfinite(x) & np.isfinite(y) & (x >= 0) & (y >= 0)
original_indices = np.arange(nstep)[valid] # Store original indices before filtering
x = x[valid]
y = y[valid]
x_std = x_std[valid]
y_std = y_std[valid]
T_all = T_all[valid]
T_std_all = T_std_all[valid]
mask = mask[valid]
# Check if we have enough data points
if len(x) < 2:
ax_n.set_xlabel('$^{39}$Ar/$^{36}$Ar')
ax_n.set_ylabel('$^{40}$Ar/$^{36}$Ar')
apply_controls(ax_n, target="DFN")
lim_DFN = (ax_n.get_xlim(), ax_n.get_ylim())
fig_n.savefig(os.path.join(outdir, "DFN.png"), dpi=300, facecolor=("white" if _get_style(style).get("classic") else "none"))
plt.close('all')
result = [n, n_std, iv, iv_std, mswd, wma, T, T_std]
if return_limits:
return result, {"DFN": lim_DFN, "DFI": lim_DFI, "DFN_pts": [], "DFI_pts": []}