-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNTNU_DataReduction.py
More file actions
6079 lines (5390 loc) · 308 KB
/
Copy pathNTNU_DataReduction.py
File metadata and controls
6079 lines (5390 loc) · 308 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
# ===============================================================================
# Copyright 2021 An-Jun Liu
# Last Modified Date: 12/28/2021
# ===============================================================================
# import python module
import sys
import os
from PyQt5 import QtCore, QtGui, QtWidgets
# v3.8.83: show the splash IMMEDIATELY — BEFORE the multi-second heavy imports
# (numpy, pandas, matplotlib, Utilities, AutoPipeline) — so launching pyADR
# gives instant visual feedback instead of a blank console while modules load.
# The QApplication created here is reused by App below via .instance(); the
# splash object is handed to App, which swaps in the version-overlaid pixmap.
_BOOT_APP = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
_BOOT_SPLASH = None
_BOOT_T0 = None
# v3.8.93: only show the boot splash when this module is the launched program
# (run as __main__). AutoPipeline lazily `import NTNU_DataReduction` on
# Help → Formulas; without this guard that import would re-trigger the boot
# splash and pop it up on top of the Help dialog.
if __name__ == '__main__':
try:
import time as _bt
_sp = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.work', 'splash.png')
if os.path.exists(_sp):
_spm = QtGui.QPixmap(_sp)
if not _spm.isNull():
_spm = _spm.copy(0, 0, _spm.width(), 455) # crop grey footer (v3.8.19)
_BOOT_SPLASH = QtWidgets.QSplashScreen(_spm, QtCore.Qt.WindowStaysOnTopHint)
_BOOT_SPLASH.setMask(_spm.mask())
_BOOT_SPLASH.show()
_BOOT_APP.processEvents()
_BOOT_T0 = _bt.monotonic()
except Exception:
_BOOT_SPLASH = None
import numpy as np
import requests
import shutil
from winotify import Notification
from tkinter import *
from datetime import date
import logging # BUG FIX: A8 - Add logging support
import threading # for background update check
# Setup logger
logger = logging.getLogger(__name__)
# import UI
import UI.HomePage
import UI.LinearRegression
import UI.T0Statistics
import UI.MassRatio
import UI.JCalculation
import UI.ReselectDialog
import UI.ParameterSetting
import UI.AirRatioStatistics
import UI.AgeCalculation
import UI.TypeSelect
import UI.SaltCalculation
import UI.JSelect
import UI.StatSelect
import UI.JStatistics
import UI.SaltSelect
import UI.SaltStatSelect
import UI.SaltStat
import UI.DiagramPlots_LS
import UI.DiagramPlots_SH
import UI.DiagramSelect
import UI.DatumSelect
# import utilities
import Utilities
import AutoPipeline
import PlaneFit3D
import ExcelChartExporter # V3.4.1: Excel native chart export
# v3.8.7: helper to add a standard "Return" button on the left edge of
# select-style sub-windows that didn't ship with one in their UI file.
# Matches the style/position used by other pages (return_2, QRect(0, 200, 91, 51)).
# The wrapper class still needs the App side to connect btn.clicked → toMain.
def _add_return_button(window, y=200):
"""Create window.return_2 button at standard left-side position. Idempotent."""
if getattr(window, 'return_2', None) is not None:
return
if not hasattr(window, 'centralwidget'):
return
btn = QtWidgets.QPushButton(window.centralwidget)
btn.setGeometry(QtCore.QRect(0, y, 91, 51))
btn.setObjectName("return_2")
btn.setText("Return")
window.return_2 = btn
# v3.8.7: helper to keep buttons / title-label horizontally centered when the
# user resizes a select-style sub-window (TypeSelect, StatSelect, JSelect,
# SaltSelect, DiagramSelect, DatumSelect). These UI files use absolute
# QRect positioning (x=210, button width 421, designed for an 800-px-wide
# window) so the buttons drift left when the window is resized larger.
# HomePage UI uses QHBoxLayout/addStretch already → this helper skips it.
def _make_select_page_responsive(window):
"""For each big QPushButton / QLabel in centralwidget, recompute its x
so it stays horizontally centered when the window resizes. No-op if
centralwidget already has a layout (assume that layout handles it)."""
cw = window.centralWidget()
if cw is None or cw.layout() is not None:
return
def _targets():
ts = []
for w in cw.findChildren(QtWidgets.QPushButton):
if w.geometry().width() > 100:
ts.append(w)
for w in cw.findChildren(QtWidgets.QLabel):
if w.geometry().width() > 100:
ts.append(w)
return ts
def _recenter():
w_total = cw.width()
if w_total <= 0:
return
for widget in _targets():
geo = widget.geometry()
widget.move((w_total - geo.width()) // 2, geo.y())
_orig_resize = window.resizeEvent
def _on_resize(event):
_orig_resize(event)
_recenter()
window.resizeEvent = _on_resize
# Deferred initial recenter — runs after Qt has done its first layout pass
QtCore.QTimer.singleShot(0, _recenter)
# v3.8.6: shared help dialog used by DiagramPlot SH page + AutoPipeline window.
# Content focuses on what each displayed number means and which formula / paper
# it comes from — so users can defend the numbers in a paper / meeting.
# v3.8.90: Help dialog language, remembered across opens within a session.
_HELP_LANG = "en" # "en" or "zh"
def _show_diagram_plot_help(parent):
"""Open a tabbed Help dialog (English / 中文 toggle) covering plateau /
isochron / MSWD / σ(T0) / age math. v3.8.90: bilingual."""
global _HELP_LANG
dlg = QtWidgets.QDialog(parent)
dlg.setWindowTitle("pyADR — Formulas & References")
dlg.resize(820, 660)
lay = QtWidgets.QVBoxLayout(dlg)
# (en_title, zh_title, en_html, zh_html)
tab_defs = [
("Plateau / WMA", "平台 / 加權平均", _HELP_PLATEAU_HTML, _HELP_PLATEAU_HTML_ZH),
("Isochron", "等時線", _HELP_ISOCHRON_HTML, _HELP_ISOCHRON_HTML_ZH),
("MSWD", "MSWD", _HELP_MSWD_HTML, _HELP_MSWD_HTML_ZH),
("σ(T₀) methods", "σ(T₀) 兩種慣例", _HELP_SIGMAT0_HTML, _HELP_SIGMAT0_HTML_ZH),
("Age formula", "年齡公式", _HELP_AGE_HTML, _HELP_AGE_HTML_ZH),
("Ar components", "Ar 成分分解", _HELP_AR_COMP_HTML, _HELP_AR_COMP_HTML_ZH),
("3D Plane Fit", "3D 平面擬合", _HELP_PLANE3D_HTML, _HELP_PLANE3D_HTML_ZH),
("References", "參考文獻", _HELP_REFS_HTML, _HELP_REFS_HTML_ZH),
]
tabs = QtWidgets.QTabWidget()
lay.addWidget(tabs)
browsers = [] # [(browser, en_title, zh_title, en_html, zh_html), ...]
for en_t, zh_t, en_h, zh_h in tab_defs:
w = QtWidgets.QTextBrowser()
w.setOpenExternalLinks(True)
tabs.addTab(w, en_t)
browsers.append((w, en_t, zh_t, en_h, zh_h))
# v3.8.91: bottom bar — CN/EN language toggle (bottom-left), Close (right).
btn_lang = QtWidgets.QPushButton()
btn_lang.setFixedWidth(56)
btn_lang.setToolTip("Switch language / 切換中英文")
btn_close = QtWidgets.QPushButton("Close")
btn_close.clicked.connect(dlg.accept)
bottom = QtWidgets.QHBoxLayout()
bottom.addWidget(btn_lang)
bottom.addStretch()
bottom.addWidget(btn_close)
lay.addLayout(bottom)
def _apply_lang(lang):
global _HELP_LANG
_HELP_LANG = lang
for i, (w, en_t, zh_t, en_h, zh_h) in enumerate(browsers):
w.setHtml(en_h if lang == "en" else zh_h)
tabs.setTabText(i, en_t if lang == "en" else zh_t)
btn_close.setText("Close" if lang == "en" else "關閉")
# button shows the language you switch TO (EN → click for CN, vice-versa)
btn_lang.setText("CN" if lang == "en" else "EN")
btn_lang.clicked.connect(
lambda: _apply_lang("zh" if _HELP_LANG == "en" else "en"))
_apply_lang(_HELP_LANG)
dlg.exec_()
_HELP_PLATEAU_HTML = """
<h2>Weighted Mean Age (WMA) & Plateau</h2>
<p>The plateau age summarises a contiguous block of step ages that are
mutually consistent within their analytical uncertainties.</p>
<h3>Weighted Mean Formula</h3>
<p style="margin-left:20px"><b>WMA = Σ(T<sub>i</sub> / σ<sub>i</sub><sup>2</sup>) /
Σ(1 / σ<sub>i</sub><sup>2</sup>)</b></p>
<p style="margin-left:20px"><b>σ<sub>WMA, internal</sub> = 1 / √Σ(1/σ<sub>i</sub><sup>2</sup>)</b></p>
<p>Equivalent to maximum-likelihood estimator under Gaussian errors.
Vermeesch (2018) IsoplotR Eq. 5; Schaen et al. (2021) GSA Bull. p.470.</p>
<h3>External σ (Wendt & Carl 1991)</h3>
<p>When MSWD > 1, expand the internal σ to capture excess scatter:</p>
<p style="margin-left:20px"><b>σ<sub>WMA, external</sub> = σ<sub>WMA, internal</sub> · √MSWD</b>
(only when MSWD > 1)</p>
<p>If MSWD ≤ 1, just use the internal σ.</p>
<h3>Total Fusion Age</h3>
<p>Treat the whole sample as if degassed in one step. Sum all radiogenic
<sup>40</sup>Ar and all K-derived <sup>39</sup>Ar:</p>
<p style="margin-left:20px"><b>F<sub>total</sub> = Σ<sup>40</sup>Ar*<sub>i</sub> / Σ<sup>39</sup>Ar<sub>K,i</sub></b></p>
<p style="margin-left:20px"><b>T<sub>total</sub> = ln(1 + J · F<sub>total</sub>) / λ</b></p>
<p>Equivalent to a K/Ar age — ignores step structure. Useful as a
cross-check against plateau age.</p>
"""
_HELP_ISOCHRON_HTML = """
<h2>Isochron Regression</h2>
<p>An isochron is a mixing line between two end-members on a ratio plot.
Two parametrisations are common in Ar/Ar:</p>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th></th><th>Normal isochron (DFN)</th><th>Inverse isochron (DFI)</th></tr>
<tr><td>X axis</td><td><sup>39</sup>Ar / <sup>36</sup>Ar</td><td><sup>39</sup>Ar / <sup>40</sup>Ar</td></tr>
<tr><td>Y axis</td><td><sup>40</sup>Ar / <sup>36</sup>Ar</td><td><sup>36</sup>Ar / <sup>40</sup>Ar</td></tr>
<tr><td>Y-intercept</td><td>(<sup>40</sup>/<sup>36</sup>)<sub>trapped</sub></td><td>(<sup>36</sup>/<sup>40</sup>)<sub>trapped</sub> = 1/(<sup>40</sup>/<sup>36</sup>)<sub>trapped</sub></td></tr>
<tr><td>Slope</td><td>F = <sup>40</sup>Ar* / <sup>39</sup>Ar<sub>K</sub></td><td>−F · (<sup>36</sup>/<sup>40</sup>)<sub>trapped</sub></td></tr>
<tr><td>F formula</td><td>F = slope</td><td><b>F = −slope / intercept</b></td></tr>
</table>
<p>For Y = a + bX (York convention, a = intercept, b = slope):</p>
<p style="margin-left:20px">Normal: F = b</p>
<p style="margin-left:20px">Inverse: F = −b / a (Vermeesch 2024 Eq. 2; Li & Vermeesch 2021 Eq. 5)</p>
<h3>Regression methods (toggle in Plot Controls)</h3>
<p><b>OLS (Ordinary Least Squares)</b> — <code>scipy.curve_fit(linear, x, y)</code>.
Assumes σ<sub>x</sub> = 0 (all error in Y). Older Ar/Ar convention.</p>
<p><b>York 2004</b> — Bivariate weighted regression: accounts for
σ<sub>x</sub> and σ<sub>y</sub> (and, optionally, a per-point x–y
correlation ρ). Iteratively solves slope until convergence.
Schaen et al. (2021) Ar/Ar standard; IsoplotR default.</p>
<p>York generally gives smaller-magnitude slope (when σ<sub>x</sub> is
non-trivial) than OLS, so F and the resulting age can differ. Toggle to compare.</p>
<h3>Why σ<sub>x</sub> cannot be ignored (regression dilution)</h3>
<p>On an isochron both axes are <i>ratios sharing a common denominator</i>
(inverse: x = <sup>39</sup>Ar/<sup>40</sup>Ar, y = <sup>36</sup>Ar/<sup>40</sup>Ar —
both divided by <sup>40</sup>Ar). The <sup>40</sup>Ar measurement error therefore
enters <b>both</b> x and y, so the two axes are genuinely correlated and
σ<sub>x</sub> is never zero.</p>
<p>OLS, by assuming σ<sub>x</sub> = 0, suffers <b>regression dilution
(attenuation bias)</b>: scatter along x flattens the fitted line toward zero
slope. This does not only bias the slope (hence F → age) — because the
fit is anchored through the data centroid, a flattened slope also
<b>shifts the y-intercept</b>, so the trapped (<sup>40</sup>/<sup>36</sup>)<sub>trapped</sub>
is biased too. York removes this bias by weighting each point with its full
2-D error structure; it is the correct estimator for the error-in-both-variables
problem and should be preferred for any reported result.</p>
<p><i>Implementation note:</i> the genuine x–y error correlation from the
shared <sup>40</sup>Ar denominator is real, but pyADR currently calls York with
ρ = 0 (it propagates σ<sub>x</sub>, σ<sub>y</sub> only). Supplying
the analytical ρ would tighten the fit further; this is a known refinement.</p>
<h3>σ<sub>F</sub> for inverse isochron</h3>
<p>F = −b/a, so by Gaussian error propagation including
slope-intercept covariance:</p>
<p style="margin-left:20px"><b>σ<sub>F</sub><sup>2</sup> = (σ<sub>b</sub>/a)<sup>2</sup>
+ (b · σ<sub>a</sub> / a<sup>2</sup>)<sup>2</sup>
− 2 (b/a<sup>3</sup>) · cov(a, b)</b></p>
<p>cov(a, b) is typically negative for inverse isochrons (when slope steepens,
intercept drops), so the cross-term reduces σ<sub>F</sub>. pyADR computes
this from <code>pcov</code> (OLS) or York's analytical formula (Mahon 1996, Schaen 2021 Eq. 14b).</p>
"""
_HELP_MSWD_HTML = """
<h2>MSWD — two flavours, not interchangeable</h2>
<p>pyADR reports two MSWD numbers that look similar but measure different
things. They can diverge for hetero / disturbed samples.</p>
<h3>Plateau MSWD <i>(right-panel stats)</i></h3>
<p style="margin-left:20px"><b>MSWD<sub>plateau</sub> = Σ((T<sub>i</sub> − WMA) /
σ<sub>T,i</sub>)<sup>2</sup> / (N − 1)</b></p>
<p>Measures the spread of step <i>ages</i> around the weighted-mean age.
Reflects age homogeneity (sample coherence in time).
df = N−1 (one free parameter: the mean).</p>
<h3>Regression MSWD <i>(persistent label above DFN/DFI diagram)</i></h3>
<p style="margin-left:20px"><b>MSWD<sub>regression</sub> = Σ((y<sub>i</sub> − a − b·x<sub>i</sub>) /
σ<sub>y,i</sub>)<sup>2</sup> / (N − 2)</b></p>
<p>Measures the scatter of <i>data points</i> around the isochron line.
Reflects how well the linear mixing model fits the data.
df = N−2 (slope + intercept).</p>
<h3>What different combinations imply</h3>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th></th><th>MSWD<sub>plateau</sub> ≈ 1</th><th>MSWD<sub>plateau</sub> > 1</th></tr>
<tr><th>MSWD<sub>reg</sub> ≈ 1</th><td>Ideal — well-behaved sample</td><td>Ages disagree but points fall on isochron line — trapped composition consistent, but ages dispersed (partial reset?)</td></tr>
<tr><th>MSWD<sub>reg</sub> > 1</th><td>Step ages agree but points scatter on isochron — trapped composition varies between steps</td><td>Both spread — significant geological complexity (excess Ar, hetero, partial loss)</td></tr>
</table>
<h3>Critical MSWD — two ways to get the threshold</h3>
<p><b>Normal approximation</b> (Wendt & Carl, 1991; quoted as Schaen et al. 2021 Eq. 2):
<b>MSWD<sub>crit</sub> ≈ 1 + 2√(2/df)</b>. For df = 8, MSWD<sub>crit</sub> ≈ 2.0.
This is the large-df asymptotic limit of the exact distribution below and is
systematically <i>too strict</i> at the low df typical of Ar-Ar step-heating
(df = 5: 2.26 vs. the exact 2.57 — a ~14% gap).</p>
<p><b>Exact (chi-square quantile)</b>: MSWD ~ χ²(df)/df, so the exact
95% upper bound is <code>scipy.stats.chi2.ppf(0.975, df) / df</code>. This is
the correct distribution — no large-df assumption needed.</p>
<p style="background:#e8f4ea;padding:6px;border:1px solid #2e7d52;">
<b>pyADR (from v3.9.15) uses the exact χ² quantile everywhere</b>
(<code>PlaneFit3D._mswd_ci</code>, and <code>AutoPipeline._mswd_verdict</code>
after the diagram-info color verdict was switched off the normal
approximation). Through v3.8.82, <code>AutoPipeline._mswd_verdict</code> used
the Wendt-Carl normal approximation <i>and</i> hardcoded df = N−2 even
when called for the plateau MSWD (which has df = N−1) — two
independent bugs that happened to partially cancel. Both are fixed as of
v3.9.15: the verdict color now takes an explicit <code>df</code> argument from
the caller (N−1 for plateau, N−2 for isochron regression) and
compares against the exact quantile.</p>
<p>When MSWD > 1, pyADR (per Wendt & Carl 1991) still expands the
internal σ by √MSWD to give an “external” σ that
captures the excess dispersion — that convention is unaffected by the
critical-value fix above, which only changes the color-coded pass/fail
threshold, not the σ-inflation rule.</p>
"""
_HELP_AGE_HTML = """
<h2>Ar/Ar Age Formula</h2>
<p>From the radioactive decay of <sup>40</sup>K to <sup>40</sup>Ar:</p>
<p style="margin-left:20px"><b>T = (1/λ) · ln(1 + J · F)</b></p>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th>Symbol</th><th>Meaning</th><th>Where it comes from</th></tr>
<tr><td>T</td><td>Age (yr)</td><td>What we want</td></tr>
<tr><td>λ</td><td>Total <sup>40</sup>K decay constant (1/yr)</td><td>Read from <code>parameters.csv</code> ‘λ for age calculation’. pyADR default 5.49e-10 (between Steiger-Jäger 1977 = 5.543e-10 and Renne 2010 = 5.5305e-10).</td></tr>
<tr><td>J</td><td>Irradiation parameter</td><td>From co-irradiated standards (J Calculation page).</td></tr>
<tr><td>F</td><td><sup>40</sup>Ar* / <sup>39</sup>Ar<sub>K</sub></td><td>From step (plateau path) or isochron slope (DFN/DFI).</td></tr>
</table>
<h3>σ<sub>T</sub> propagation (Renne 2010 partial derivatives)</h3>
<p style="margin-left:20px"><b>σ<sub>T</sub><sup>2</sup> = ((J · σ<sub>F</sub>)<sup>2</sup>
+ (F · σ<sub>J</sub>)<sup>2</sup>) / (λ(1+JF))<sup>2</sup></b></p>
<p>pyADR does <i>not</i> include σ<sub>λ</sub> by default (typically < 0.5% for
geologically young samples).</p>
<h3>Decay constants used elsewhere</h3>
<ul>
<li><b><sup>37</sup>Ar half-life</b> = 35.011 d (LAMBDA_37 in AutoPipeline).
Used to back-correct interfering <sup>37</sup>Ar between irradiation and analysis.</li>
<li><b><sup>39</sup>Ar half-life</b> = 269 yr (LAMBDA_39). Same purpose, much
slower decay so usually negligible over months but corrected anyway.</li>
</ul>
"""
_HELP_AR_COMP_HTML = """
<h2>Ar isotope component breakdown</h2>
<p>From measured <sup>36, 37, 38, 39, 40</sup>Ar, pyADR deconvolves trapped,
production-related, and radiogenic components using <code>parameters.csv</code>
production ratios.</p>
<h3>Order of corrections (calcAge in Utilities.py)</h3>
<ol>
<li><b><sup>37</sup>Ar(Ca)</b> = measured <sup>37</sup>Ar — from Ca interference, decay-corrected to irradiation midpoint.</li>
<li><b><sup>36</sup>Ar(Ca)</b> = <sup>37</sup>Ar(Ca) × PR(<sup>36</sup>/<sup>37</sup>Ca)</li>
<li><b><sup>36</sup>Ar(air)</b> = measured <sup>36</sup>Ar − <sup>36</sup>Ar(Ca). (NTNU lab: <sup>36</sup>Ar(Cl) treated as negligible.)</li>
<li><b><sup>39</sup>Ar(Ca)</b> = <sup>37</sup>Ar(Ca) × PR(<sup>39</sup>/<sup>37</sup>Ca)</li>
<li><b><sup>39</sup>Ar(K)</b> = measured <sup>39</sup>Ar − <sup>39</sup>Ar(Ca)</li>
<li><b><sup>40</sup>Ar(air)</b> = <sup>36</sup>Ar(air) × R(<sup>40</sup>/<sup>36</sup>)<sub>atm</sub> (R = 298.56 by default)</li>
<li><b><sup>40</sup>Ar(K)</b> = <sup>39</sup>Ar(K) × PR(<sup>40</sup>/<sup>39</sup>K)</li>
<li><b><sup>40</sup>Ar*</b> = measured <sup>40</sup>Ar − <sup>40</sup>Ar(air) − <sup>40</sup>Ar(K)</li>
</ol>
<p>F = <sup>40</sup>Ar* / <sup>39</sup>Ar(K). Age T = ln(1 + JF)/λ.</p>
<h3>Ca/K ratio</h3>
<p style="margin-left:20px"><b>Ca/K = (<sup>37</sup>Ar(Ca) / <sup>39</sup>Ar(K)) · R<sub>Ca/K</sub></b></p>
<p>R<sub>Ca/K</sub> = 0.52 in pyADR (NTNU reactor calibration, hardcoded).
Literature standard is 1.83 (McDougall & Harrison 1999 Eq. 4.30) but
that's for a different reactor.</p>
"""
_HELP_PLANE3D_HTML = """
<h2>3D Plane Fit (PlaneFit3D.py)</h2>
<p>Alternative to 2D isochron projection: regress the data directly in
(³⁶Ar, ³⁹Ar, ⁴⁰Ar) space. Avoids error accumulation from ratio computation
and lets you see the spatial structure of isotope systems within a sample.</p>
<p><b>Reference</b>: Kent et al. (1990) maximum-likelihood plane regression,
implemented per Wu C.-Y. (2007) NTU MSc thesis (R94224113, advisor Ching-Hua Lo).
Validation sample SYL31 (Sylhet Trap basalt, 115.4 ± 3.9 Ma).</p>
<h3>Plane equation</h3>
<p style="margin-left:20px">
<b><sup>40</sup>Ar = α · <sup>36</sup>Ar + β · <sup>39</sup>Ar</b></p>
<ul>
<li><b>α</b> = (<sup>40</sup>Ar/<sup>36</sup>Ar)<sub>trapped</sub>
— initial (air / inherited) composition (≈ 298.56 for pure air)</li>
<li><b>β</b> = <sup>40</sup>Ar* / <sup>39</sup>Ar<sub>K</sub> = F —
radiogenic-to-K ratio (feeds into the age formula)</li>
</ul>
<p>Age: <b>T = (1/λ) · ln(1 + β · J)</b></p>
<h3>Maximum-likelihood objective</h3>
<p>Each data point x<sub>i</sub> = (<sup>36</sup>Ar, <sup>39</sup>Ar, <sup>40</sup>Ar)
is modelled as N<sub>3</sub>(μ<sub>i</sub>, A<sub>i</sub>) where μ<sub>i</sub>
lies on the plane. Define plane normal γ = [α, β, −1]<sup>T</sup>:</p>
<p style="margin-left:20px">s<sub>i</sub> = γ<sup>T</sup>x<sub>i</sub> = α·<sup>36</sup>Ar<sub>i</sub> + β·<sup>39</sup>Ar<sub>i</sub> − <sup>40</sup>Ar<sub>i</sub> (signed distance)</p>
<p style="margin-left:20px">q<sub>i</sub> = γ<sup>T</sup>A<sub>i</sub>γ (variance projected onto normal)</p>
<p>After Lagrange-multiplier elimination of μ<sub>i</sub> (Wu 2007 eq 3-5 → 3-7),
the <b>profile log-likelihood</b>:</p>
<p style="margin-left:20px"><b>L<sub>p</sub>(δ) = −½ Σ<sub>i</sub> s<sub>i</sub>² / q<sub>i</sub></b>
where δ = [α, β]<sup>T</sup></p>
<p>Maximising L<sub>p</sub> = weighted least squares with weights from all three isotope axes
(no axis preferred over others).</p>
<h3>Per-point covariance matrix A<sub>i</sub> (3×3)</h3>
<table border="1" cellpadding="6" cellspacing="0">
<tr><td>σ<sub>36</sub>²</td><td>0</td><td>0</td></tr>
<tr><td>0</td><td>σ<sub>39</sub>²</td><td>−k<sub>0</sub>·σ<sub>39</sub>²</td></tr>
<tr><td>0</td><td>−k<sub>0</sub>·σ<sub>39</sub>²</td><td>σ<sub>40</sub>²</td></tr>
</table>
<p>where k<sub>0</sub> = PR(<sup>40</sup>Ar/<sup>39</sup>Ar)<sub>K</sub> (default 0.025004).
Off-diagonal cov(<sup>39</sup>, <sup>40</sup>) captures the anti-correlation from the
⁴⁰Ar(K) back-correction.</p>
<h3>Newton–Raphson optimisation</h3>
<p>Starting from OLS δ<sub>0</sub>, iterate:</p>
<p style="margin-left:20px"><b>δ<sub>k+1</sub> = δ<sub>k</sub> + H<sup>−1</sup>·g</b></p>
<p>with gradient g = ∂L<sub>p</sub>/∂δ and Hessian H = −∂²L<sub>p</sub>/∂δ∂δ<sup>T</sup>
(positive-definite at the MLE, = observed Fisher information).</p>
<p><b>Backtracking line search</b> (pyADR v3.4 addition):
halve step up to 20× until L<sub>p</sub> strictly increases. Prevents Newton overshoot.
Convergence: |Δδ|/|δ| < 10<sup>−10</sup>.</p>
<h3>MSWD and 95% CI</h3>
<p style="margin-left:20px"><b>S² = Σ<sub>i</sub> s<sub>i</sub>² / q<sub>i</sub>,
df = n − 2, MSWD = S² / df</b></p>
<p>(Wu 2007 eq 3-24; Mahon 1996.)</p>
<p>95% CI computed exactly via χ<sup>2</sup><sub>df</sub> quantiles (scipy.stats.chi2),
not the normal approximation. If MSWD > upper bound, pyADR applies Wendt-Carl
σ-expansion:</p>
<p style="margin-left:20px">τ² = MSWD, σ<sub>δ</sub> → √τ² · σ<sub>δ</sub></p>
<h3>Parameter covariance</h3>
<p style="margin-left:20px"><b>cov(δ̂) = τ² · H<sup>−1</sup></b></p>
<p>1σ uncertainties: σ<sub>α</sub> = √cov<sub>11</sub>, σ<sub>β</sub> = √cov<sub>22</sub>.</p>
<p style="background:#fff4d0;padding:6px;border:1px solid #c0a020;">
<b>⚠ Wu (2007) eq 3-27 sign-error correction</b>: the thesis writes
cov(δ̂) = τ² · (∂²L<sub>p</sub>/∂δ∂δ<sup>T</sup>)<sup>−1</sup>.
At the MLE, ∂²L<sub>p</sub> is <i>negative</i>-definite, so its inverse gives
<i>negative variances</i> — clearly wrong. The correct ML asymptotic covariance is
τ² · (−∂²L<sub>p</sub>/∂δ∂δ<sup>T</sup>)<sup>−1</sup> = τ² · H<sup>−1</sup>.
pyADR uses the corrected form.</p>
<h3>Age error propagation (Renne 1998, Min 2000)</h3>
<p style="margin-left:20px"><b>σ<sub>T</sub>² = (∂T/∂β)² σ<sub>β</sub>²
+ (∂T/∂J)² σ<sub>J</sub>²
+ (∂T/∂λ)² σ<sub>λ</sub>²</b></p>
<p>with</p>
<ul>
<li>∂T/∂β = J / [λ(1 + βJ)]</li>
<li>∂T/∂J = β / [λ(1 + βJ)]</li>
<li>∂T/∂λ = −ln(1 + βJ) / λ²</li>
</ul>
<h3>Mahon (1996) σ-cap (optional)</h3>
<p>For background-dominated steps where σ<sub>i</sub>/|x<sub>i</sub>| >> 1, classical
Kent weights give those points outsize influence. Set per-axis caps to limit:</p>
<p style="margin-left:20px"><b>σ<sub>i,eff</sub> = min(σ<sub>i</sub>, c · |x<sub>i</sub>|)</b></p>
<p>Typical c = 0.2–0.5. None (default) disables (classical Kent).</p>
<p>See FORMULAS.md §11 for full derivations.</p>
"""
_HELP_REFS_HTML = """
<h2>References</h2>
<h3>3D plane fit (PlaneFit3D.py)</h3>
<ul>
<li>Kent J.T., Watson G.S., Onstott T.C. (1990) <i>Maximum likelihood estimation
of a plane in three dimensions.</i> Statistics 21: 411–426.</li>
<li>Wu C.-Y. (2007) <i>3-D Plane-fitting Program in 40Ar/39Ar Dating.</i>
MSc thesis, NTU Geosciences (R94224113), advisor Ching-Hua Lo. Math derivations
in Chapter 3.</li>
<li>Titterington D.M., Halliday A.N. (1979) <i>On the fitting of parallel isochrons
and the method of maximum likelihood.</i> Chem. Geol. 26: 183–195.</li>
<li>Mahon K.I. (1996) <i>The new "York" regression.</i> Int. Geol. Rev. 38: 293–303.
(MSWD & modified weighting)</li>
<li>Renne P.R. et al. (1998) <i>Intercalibration of standards, absolute ages and
uncertainties in 40Ar/39Ar dating.</i> Chem. Geol. 145: 117–152.</li>
<li>Min K. et al. (2000) <i>A test for systematic errors in 40Ar/39Ar
geochronology.</i> GCA 64: 73–98.</li>
<li>Koppers A.A.P. (2002) <i>ArArCALC — software for 40Ar/39Ar age calculations.</i>
Comput. Geosci. 28: 605–619.</li>
</ul>
<h3>Isochron regression</h3>
<ul>
<li>York D., Evensen N.M., Martínez M.L., De Basabe Delgado J. (2004)
<i>Unified equations for the slope, intercept, and standard errors of the best straight line.</i>
Am. J. Phys. 72: 367–375.</li>
<li>Vermeesch P. (2018) <i>IsoplotR: A free and open toolbox for geochronology.</i>
Geoscience Frontiers 9: 1479–1493. doi:10.1016/j.gsf.2018.04.001</li>
<li>Vermeesch P. (2024) <i>Errorchrons and anchored isochrons in IsoplotR.</i>
Geochronology 6: 397–407. doi:10.5194/gchron-6-397-2024</li>
<li>Li Y., Vermeesch P. (2021) <i>Short communication: Inverse isochron regression
for Re–Os, K–Ca and other chronometers.</i> Geochronology 3: 415–420.
doi:10.5194/gchron-3-415-2021</li>
<li>Mahon K.I. (1996) <i>The new "York" regression: application of an improved
statistical method to geochemistry.</i> Int. Geol. Rev. 38: 293–303.
(slope-intercept covariance formula)</li>
</ul>
<h3>MSWD / weighted-mean statistics</h3>
<ul>
<li>Wendt I., Carl C. (1991) <i>The statistical distribution of the mean squared
weighted deviation.</i> Chem. Geol. 86: 275–285.
(√MSWD external-σ expansion)</li>
<li>Schaen A.J. et al. (2021) <i>Interpreting and reporting <sup>40</sup>Ar/<sup>39</sup>Ar
geochronologic data.</i> GSA Bulletin 133: 461–487.
doi:10.1130/B35560.1 (Ar/Ar community standard)</li>
</ul>
<h3>Ar/Ar method & decay constants</h3>
<ul>
<li>McDougall I., Harrison T.M. (1999) <i>Geochronology and Thermochronology
by the <sup>40</sup>Ar/<sup>39</sup>Ar Method</i>, 2nd ed., Oxford University Press.
(Ar component math standard)</li>
<li>Renne P.R. et al. (2010) <i>Joint determination of <sup>40</sup>K decay constants and
<sup>40</sup>Ar*/<sup>40</sup>K for the Fish Canyon sanidine standard.</i> GCA 74: 5349–5367.
(modern λ values)</li>
<li>Renne P.R. et al. (2011) <i>Response to the comment by W.H. Schwarz et al.
on "Joint determination of...".</i> GCA 75: 5097–5100.</li>
<li>Steiger R.H., Jäger E. (1977) <i>Subcommission on geochronology: Convention
on the use of decay constants in geo- and cosmochronology.</i> EPSL 36: 359–362.
(historical λ = 5.543e-10/yr)</li>
<li>Kuiper K.F. (2002) <i>The interpretation of inverse isochron diagrams in
<sup>40</sup>Ar/<sup>39</sup>Ar geochronology.</i> EPSL 203: 499–506.</li>
</ul>
<h3>Atmospheric composition</h3>
<ul>
<li>Lee J.-Y. et al. (2006) <i>A redetermination of the isotopic abundances of
atmospheric Ar.</i> GCA 70: 4507–4512. (<sup>40</sup>Ar/<sup>36</sup>Ar = 298.56)</li>
</ul>
"""
# ===========================================================================
# v3.8.90: σ(T0) conventions tab (EN) + Chinese (ZH) translations of all tabs.
# Formula lines are language-neutral and kept identical across EN/ZH so the two
# render the same math; only the prose is translated.
# ===========================================================================
_HELP_SIGMAT0_HTML = """
<h2>σ(T<sub>0</sub>): two conventions in pyADR</h2>
<p>T<sub>0</sub> is the signal extrapolated back to the inlet time (t = 0) by
fitting the voltage–time decay (linear or average) and evaluating the fit
at t = 0. pyADR computes the uncertainty σ(T<sub>0</sub>) of that
extrapolated value in <b>two different ways</b>, depending on the subprogram.</p>
<h3>1. Residual scatter (Calculate T<sub>0</sub> page)</h3>
<p style="margin-left:20px"><b>σ(T<sub>0</sub>) = std(|v<sub>i</sub> − f(t<sub>i</sub>)|) / √n</b></p>
<p>Standard deviation of the absolute fit residuals divided by √n.
Used in <code>NTNU_DataReduction.calculateT0</code> (the Calculate T<sub>0</sub>
page). <b>Retained on the explicit instruction of Prof. Jian-Cheng Lee</b> and
must not be changed without his approval.</p>
<h3>2. SE-from-covariance (AutoPipeline)</h3>
<p style="margin-left:20px"><b>σ(T<sub>0</sub>) = √(pcov[intercept, intercept])</b></p>
<p>The standard error of the intercept taken directly from the least-squares
covariance matrix (Li et al. 2019 Eq. 1). Closed-form equivalent:</p>
<p style="margin-left:20px"><b>σ(T<sub>0</sub>) = s · √(1/n + x̄<sup>2</sup> / S<sub>xx</sub>)</b>,
S<sub>xx</sub> = Σ(t<sub>i</sub> − t̄)<sup>2</sup></p>
<p>Used in AutoPipeline (v3.8.2+), selectable via the <code>SIGMA_METHOD</code>
toggle ('standard' = this; 'calc_t0' = method 1).</p>
<h3>Why they differ (~4×)</h3>
<p>Method 1 treats the points as repeated estimates of a single mean and ignores
that t = 0 lies <i>outside</i> the measured time window. Extrapolation inflates
the true uncertainty through the lever-arm term x̄<sup>2</sup>/S<sub>xx</sub>,
which method 2 includes but method 1 omits. Empirically (v3.8.2) method 1
underestimates σ(T<sub>0</sub>) by a factor of ~4 relative to the
covariance SE.</p>
<p style="background:#fff4d0;padding:6px;border:1px solid #c0a020;">
<b>⚠ Do not port the SE-from-covariance change back into the Calculate
T<sub>0</sub> page.</b> The two subprograms intentionally use different
conventions: AutoPipeline reports the statistically rigorous SE; the Calculate
T<sub>0</sub> page keeps Prof. Lee's residual-scatter definition for continuity
with the NTNU workflow.</p>
"""
_HELP_SIGMAT0_HTML_ZH = """
<h2>σ(T<sub>0</sub>):pyADR 的兩種慣例</h2>
<p>T<sub>0</sub> 是把訊號擬合電壓–時間衰減(linear 或 average)後外插回進樣時刻(t = 0)、
並在 t = 0 取值得到。pyADR 依子程式不同,用<b>兩種方式</b>計算這個外插值的不確定度
σ(T<sub>0</sub>)。</p>
<h3>1. 殘差散布(Calculate T<sub>0</sub> 頁)</h3>
<p style="margin-left:20px"><b>σ(T<sub>0</sub>) = std(|v<sub>i</sub> − f(t<sub>i</sub>)|) / √n</b></p>
<p>擬合殘差絕對值的標準差除以 √n。用於 <code>NTNU_DataReduction.calculateT0</code>
(Calculate T<sub>0</sub> 頁)。<b>依李建成教授明確指示保留</b>,未經他同意不得更改。</p>
<h3>2. 共變異 SE(AutoPipeline)</h3>
<p style="margin-left:20px"><b>σ(T<sub>0</sub>) = √(pcov[intercept, intercept])</b></p>
<p>直接取最小二乘共變異矩陣中截距的標準誤(Li et al. 2019 Eq. 1)。閉式等價:</p>
<p style="margin-left:20px"><b>σ(T<sub>0</sub>) = s · √(1/n + x̄<sup>2</sup> / S<sub>xx</sub>)</b>,
S<sub>xx</sub> = Σ(t<sub>i</sub> − t̄)<sup>2</sup></p>
<p>用於 AutoPipeline(v3.8.2+),由 <code>SIGMA_METHOD</code> 切換
('standard' = 此法;'calc_t0' = 方法 1)。</p>
<h3>為何兩者差約 4×</h3>
<p>方法 1 把各點當成對同一平均值的重複估計,忽略了 t = 0 落在量測時間窗<i>之外</i>。
外插會透過槓桿項 x̄<sup>2</sup>/S<sub>xx</sub> 放大真實不確定度,方法 2 含此項、方法 1 沒有。
實測(v3.8.2)方法 1 相對共變異 SE 低估約 4 倍。</p>
<p style="background:#fff4d0;padding:6px;border:1px solid #c0a020;">
<b>⚠ 不要把共變異 SE 的改動 port 回 Calculate T<sub>0</sub> 頁。</b>
兩個子程式刻意採不同慣例:AutoPipeline 報統計上嚴謹的 SE;Calculate T<sub>0</sub> 頁保留
李教授的殘差散布定義,以延續 NTNU 工作流程。</p>
"""
_HELP_PLATEAU_HTML_ZH = """
<h2>加權平均年齡(WMA)與平台</h2>
<p>平台年齡彙整一段連續、且在各自分析誤差內彼此一致的階段年齡。</p>
<h3>加權平均公式</h3>
<p style="margin-left:20px"><b>WMA = Σ(T<sub>i</sub> / σ<sub>i</sub><sup>2</sup>) /
Σ(1 / σ<sub>i</sub><sup>2</sup>)</b></p>
<p style="margin-left:20px"><b>σ<sub>WMA, internal</sub> = 1 / √Σ(1/σ<sub>i</sub><sup>2</sup>)</b></p>
<p>高斯誤差下等於最大概似估計。Vermeesch (2018) IsoplotR Eq. 5;Schaen et al. (2021) GSA Bull. p.470。</p>
<h3>外部 σ(Wendt & Carl 1991)</h3>
<p>當 MSWD > 1,放大 internal σ 以涵蓋多餘散布:</p>
<p style="margin-left:20px"><b>σ<sub>WMA, external</sub> = σ<sub>WMA, internal</sub> · √MSWD</b>
(僅當 MSWD > 1)</p>
<p>若 MSWD ≤ 1,直接用 internal σ。</p>
<h3>全熔年齡(Total Fusion)</h3>
<p>把整顆樣品當成一次脫氣,加總所有放射成因 <sup>40</sup>Ar 與所有 K 來源 <sup>39</sup>Ar:</p>
<p style="margin-left:20px"><b>F<sub>total</sub> = Σ<sup>40</sup>Ar*<sub>i</sub> / Σ<sup>39</sup>Ar<sub>K,i</sub></b></p>
<p style="margin-left:20px"><b>T<sub>total</sub> = ln(1 + J · F<sub>total</sub>) / λ</b></p>
<p>等同 K/Ar 年齡,忽略階段結構,可作為平台年齡的交叉檢核。</p>
"""
_HELP_ISOCHRON_HTML_ZH = """
<h2>等時線回歸</h2>
<p>等時線是比值圖上兩端元之間的混合線。Ar/Ar 常用兩種參數化:</p>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th></th><th>正等時線(DFN)</th><th>反等時線(DFI)</th></tr>
<tr><td>X 軸</td><td><sup>39</sup>Ar / <sup>36</sup>Ar</td><td><sup>39</sup>Ar / <sup>40</sup>Ar</td></tr>
<tr><td>Y 軸</td><td><sup>40</sup>Ar / <sup>36</sup>Ar</td><td><sup>36</sup>Ar / <sup>40</sup>Ar</td></tr>
<tr><td>Y 截距</td><td>(<sup>40</sup>/<sup>36</sup>)<sub>trapped</sub></td><td>(<sup>36</sup>/<sup>40</sup>)<sub>trapped</sub> = 1/(<sup>40</sup>/<sup>36</sup>)<sub>trapped</sub></td></tr>
<tr><td>斜率</td><td>F = <sup>40</sup>Ar* / <sup>39</sup>Ar<sub>K</sub></td><td>−F · (<sup>36</sup>/<sup>40</sup>)<sub>trapped</sub></td></tr>
<tr><td>F 公式</td><td>F = slope</td><td><b>F = −slope / intercept</b></td></tr>
</table>
<p>以 Y = a + bX(York 慣例,a = 截距,b = 斜率):</p>
<p style="margin-left:20px">正: F = b</p>
<p style="margin-left:20px">反:F = −b / a (Vermeesch 2024 Eq. 2;Li & Vermeesch 2021 Eq. 5)</p>
<h3>回歸方法(Plot Controls 可切換)</h3>
<p><b>OLS(普通最小二乘)</b> — <code>scipy.curve_fit(linear, x, y)</code>。
假設 σ<sub>x</sub> = 0(誤差全在 Y)。舊式 Ar/Ar 慣例。</p>
<p><b>York 2004</b> — 雙變量加權回歸:同時考慮 σ<sub>x</sub> 與 σ<sub>y</sub>
(並可選每點 x–y 相關係數 ρ)。迭代求斜率至收斂。Schaen et al. (2021) Ar/Ar 標準;IsoplotR 預設。</p>
<p>當 σ<sub>x</sub> 不可忽略時,York 給的斜率通常比 OLS 小,因此 F 與年齡會不同,可切換比較。</p>
<h3>為何不能忽略 σ<sub>x</sub>(回歸稀釋)</h3>
<p>等時線兩軸都是<i>共用同一分母的比值</i>(反等時線:x = <sup>39</sup>Ar/<sup>40</sup>Ar、
y = <sup>36</sup>Ar/<sup>40</sup>Ar,皆除以 <sup>40</sup>Ar)。<sup>40</sup>Ar 的量測誤差因此<b>同時</b>進入
x 與 y,兩軸真實相關,σ<sub>x</sub> 永遠不為零。</p>
<p>OLS 假設 σ<sub>x</sub> = 0,會造成<b>回歸稀釋(衰減偏差)</b>:x 方向的散布把擬合線壓向較平的斜率。
這不只偏到斜率(進而 F → 年齡),因為擬合錨定在資料重心,壓平的斜率也會<b>移動 y 截距</b>,
所以 trapped (<sup>40</sup>/<sup>36</sup>)<sub>trapped</sub> 同樣偏掉。York 以每點完整二維誤差加權移除此偏差,
是 error-in-both-variables 問題的正確估計,凡要報告的結果都應採用。</p>
<p><i>實作註記:</i>共用 <sup>40</sup>Ar 分母造成的 x–y 誤差相關真實存在,但 pyADR 目前以
ρ = 0 呼叫 York(只傳 σ<sub>x</sub>、σ<sub>y</sub>)。補上解析 ρ 可讓擬合更緊,屬已知可改進項。</p>
<h3>反等時線的 σ<sub>F</sub></h3>
<p>F = −b/a,依高斯誤差傳播(含斜率–截距共變異):</p>
<p style="margin-left:20px"><b>σ<sub>F</sub><sup>2</sup> = (σ<sub>b</sub>/a)<sup>2</sup>
+ (b · σ<sub>a</sub> / a<sup>2</sup>)<sup>2</sup>
− 2 (b/a<sup>3</sup>) · cov(a, b)</b></p>
<p>反等時線的 cov(a, b) 通常為負(斜率變陡時截距下降),所以交叉項會縮小 σ<sub>F</sub>。
pyADR 由 <code>pcov</code>(OLS)或 York 解析公式(Mahon 1996、Schaen 2021 Eq. 14b)算得。</p>
"""
_HELP_MSWD_HTML_ZH = """
<h2>MSWD — 兩種,不可混用</h2>
<p>pyADR 會報兩個看起來像、其實量的是不同東西的 MSWD。對不均質/受擾動的樣品,兩者可能背離。</p>
<h3>平台 MSWD <i>(右側統計面板)</i></h3>
<p style="margin-left:20px"><b>MSWD<sub>plateau</sub> = Σ((T<sub>i</sub> − WMA) /
σ<sub>T,i</sub>)<sup>2</sup> / (N − 1)</b></p>
<p>量的是各階段<i>年齡</i>相對加權平均年齡的散布,反映年齡均質性(時間上的一致性)。
自由度 = N−1(一個自由參數:平均)。</p>
<h3>回歸 MSWD <i>(DFN/DFI 圖上方的常駐標籤)</i></h3>
<p style="margin-left:20px"><b>MSWD<sub>regression</sub> = Σ((y<sub>i</sub> − a − b·x<sub>i</sub>) /
σ<sub>y,i</sub>)<sup>2</sup> / (N − 2)</b></p>
<p>量的是<i>資料點</i>相對等時線的散布,反映線性混合模型擬合得好不好。
自由度 = N−2(斜率+截距)。</p>
<h3>不同組合的意義</h3>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th></th><th>MSWD<sub>plateau</sub> ≈ 1</th><th>MSWD<sub>plateau</sub> > 1</th></tr>
<tr><th>MSWD<sub>reg</sub> ≈ 1</th><td>理想 — 樣品乖巧</td><td>年齡不一致但點落在等時線上 — trapped 組成一致、年齡分散(部分重置?)</td></tr>
<tr><th>MSWD<sub>reg</sub> > 1</th><td>階段年齡一致但點在等時線上散開 — 各階段 trapped 組成不同</td><td>兩者皆散 — 明顯地質複雜性(過剩 Ar、不均質、部分流失)</td></tr>
</table>
<h3>臨界 MSWD — 兩種算門檻的方式</h3>
<p><b>常態近似</b>(Wendt & Carl, 1991;Schaen et al. 2021 Eq. 2 轉引):
<b>MSWD<sub>crit</sub> ≈ 1 + 2√(2/df)</b>。df = 8 時 ≈ 2.0。
這是下面精確分布在 df 很大時的漸近極限,在 Ar-Ar step-heating 常見的低 df
時系統性<i>偏嚴</i>(df = 5:常態近似 2.26 vs 精確值 2.57,差約 14%)。</p>
<p><b>精確(卡方分位數)</b>:MSWD ~ χ²(df)/df,精確 95% 上界為
<code>scipy.stats.chi2.ppf(0.975, df) / df</code>。這是正確的分布,不需要
大樣本假設。</p>
<p style="background:#e8f4ea;padding:6px;border:1px solid #2e7d52;">
<b>pyADR 自 v3.9.15 起全面採精確 χ² 分位數</b>
(<code>PlaneFit3D._mswd_ci</code>;<code>AutoPipeline._mswd_verdict</code>
的圖表資訊面板顏色判準也已改用同一套,不再用常態近似)。v3.8.82 以前,
<code>AutoPipeline._mswd_verdict</code> 用常態近似,<i>而且</i>不論呼叫者是
plateau(df 應為 N−1)還是 isochron 迴歸,內部都硬寫死 df = N−2
——是兩個各自獨立、恰好部分互相抵消的 bug。v3.9.15 已一併修正:verdict
顏色函式現在接受呼叫者明確傳入的 <code>df</code>(plateau 傳 N−1、
isochron 迴歸傳 N−2),並對照精確分位數判色。</p>
<p>當 MSWD > 1,pyADR(依 Wendt & Carl 1991)仍把 internal σ 乘
√MSWD 得到「external」σ 以涵蓋多餘散布——這個慣例不受上面臨界值
修正影響,修正的只是顏色判準的通過/警示門檻,不是 σ 放大規則本身。</p>
"""
_HELP_AGE_HTML_ZH = """
<h2>Ar/Ar 年齡公式</h2>
<p>來自 <sup>40</sup>K 衰變為 <sup>40</sup>Ar:</p>
<p style="margin-left:20px"><b>T = (1/λ) · ln(1 + J · F)</b></p>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th>符號</th><th>意義</th><th>來源</th></tr>
<tr><td>T</td><td>年齡(yr)</td><td>所求</td></tr>
<tr><td>λ</td><td><sup>40</sup>K 總衰變常數(1/yr)</td><td>讀自 <code>parameters.csv</code> 的「λ for age calculation」。pyADR 預設 5.49e-10(介於 Steiger-Jäger 1977 = 5.543e-10 與 Renne 2010 = 5.5305e-10 之間)。</td></tr>
<tr><td>J</td><td>照射參數</td><td>由共照標準品求得(J Calculation 頁)。</td></tr>
<tr><td>F</td><td><sup>40</sup>Ar* / <sup>39</sup>Ar<sub>K</sub></td><td>來自階段(平台路徑)或等時線斜率(DFN/DFI)。</td></tr>
</table>
<h3>σ<sub>T</sub> 傳播(Renne 2010 偏導)</h3>
<p style="margin-left:20px"><b>σ<sub>T</sub><sup>2</sup> = ((J · σ<sub>F</sub>)<sup>2</sup>
+ (F · σ<sub>J</sub>)<sup>2</sup>) / (λ(1+JF))<sup>2</sup></b></p>
<p>pyADR 預設<i>不</i>納入 σ<sub>λ</sub>(對地質上年輕的樣品通常 < 0.5%)。</p>
<h3>其他處用到的衰變常數</h3>
<ul>
<li><b><sup>37</sup>Ar 半衰期</b> = 35.011 天(AutoPipeline 的 LAMBDA_37)。用於把干擾性 <sup>37</sup>Ar 從照射回推到分析時刻。</li>
<li><b><sup>39</sup>Ar 半衰期</b> = 269 年(LAMBDA_39)。同樣用途,衰變慢,數月內通常可忽略但仍校正。</li>
</ul>
"""
_HELP_AR_COMP_HTML_ZH = """
<h2>Ar 同位素成分分解</h2>
<p>從量測的 <sup>36, 37, 38, 39, 40</sup>Ar,pyADR 用 <code>parameters.csv</code> 的生成比值,
解出 trapped、生成相關與放射成因各成分。</p>
<h3>校正順序(Utilities.py 的 calcAge)</h3>
<ol>
<li><b><sup>37</sup>Ar(Ca)</b> = 量測 <sup>37</sup>Ar — 來自 Ca 干擾,衰變校正回照射中點。</li>
<li><b><sup>36</sup>Ar(Ca)</b> = <sup>37</sup>Ar(Ca) × PR(<sup>36</sup>/<sup>37</sup>Ca)</li>
<li><b><sup>36</sup>Ar(air)</b> = 量測 <sup>36</sup>Ar − <sup>36</sup>Ar(Ca)。(NTNU 實驗室:<sup>36</sup>Ar(Cl) 視為可忽略。)</li>
<li><b><sup>39</sup>Ar(Ca)</b> = <sup>37</sup>Ar(Ca) × PR(<sup>39</sup>/<sup>37</sup>Ca)</li>
<li><b><sup>39</sup>Ar(K)</b> = 量測 <sup>39</sup>Ar − <sup>39</sup>Ar(Ca)</li>
<li><b><sup>40</sup>Ar(air)</b> = <sup>36</sup>Ar(air) × R(<sup>40</sup>/<sup>36</sup>)<sub>atm</sub>(預設 R = 298.56)</li>
<li><b><sup>40</sup>Ar(K)</b> = <sup>39</sup>Ar(K) × PR(<sup>40</sup>/<sup>39</sup>K)</li>
<li><b><sup>40</sup>Ar*</b> = 量測 <sup>40</sup>Ar − <sup>40</sup>Ar(air) − <sup>40</sup>Ar(K)</li>
</ol>
<p>F = <sup>40</sup>Ar* / <sup>39</sup>Ar(K)。年齡 T = ln(1 + JF)/λ。</p>
<h3>Ca/K 比值</h3>
<p style="margin-left:20px"><b>Ca/K = (<sup>37</sup>Ar(Ca) / <sup>39</sup>Ar(K)) · R<sub>Ca/K</sub></b></p>
<p>pyADR 的 R<sub>Ca/K</sub> = 0.52(NTNU 反應器校正,硬寫)。文獻標準為 1.83
(McDougall & Harrison 1999 Eq. 4.30),但那是不同反應器。</p>
"""
_HELP_PLANE3D_HTML_ZH = """
<h2>3D 平面擬合(PlaneFit3D.py)</h2>
<p>2D 等時線投影的替代法:直接在 (³⁶Ar, ³⁹Ar, ⁴⁰Ar) 空間回歸。避免比值計算的誤差累積,
並可看見樣品內同位素系統的空間結構。</p>
<p><b>參考</b>:Kent et al. (1990) 最大概似平面回歸,依 Wu C.-Y. (2007) 台大碩論
(R94224113,指導 羅清華)實作。驗證樣品 SYL31(Sylhet Trap 玄武岩,115.4 ± 3.9 Ma)。</p>
<h3>平面方程</h3>
<p style="margin-left:20px">
<b><sup>40</sup>Ar = α · <sup>36</sup>Ar + β · <sup>39</sup>Ar</b></p>
<ul>
<li><b>α</b> = (<sup>40</sup>Ar/<sup>36</sup>Ar)<sub>trapped</sub>
— 初始(大氣/繼承)組成(純大氣 ≈ 298.56)</li>
<li><b>β</b> = <sup>40</sup>Ar* / <sup>39</sup>Ar<sub>K</sub> = F —
放射成因對 K 的比值(進入年齡公式)</li>
</ul>
<p>年齡: <b>T = (1/λ) · ln(1 + β · J)</b></p>
<h3>最大概似目標函數</h3>
<p>每個資料點 x<sub>i</sub> = (<sup>36</sup>Ar, <sup>39</sup>Ar, <sup>40</sup>Ar)
建模為 N<sub>3</sub>(μ<sub>i</sub>, A<sub>i</sub>),其中 μ<sub>i</sub> 落在平面上。
定義平面法向量 γ = [α, β, −1]<sup>T</sup>:</p>
<p style="margin-left:20px">s<sub>i</sub> = γ<sup>T</sup>x<sub>i</sub> = α·<sup>36</sup>Ar<sub>i</sub> + β·<sup>39</sup>Ar<sub>i</sub> − <sup>40</sup>Ar<sub>i</sub>(帶號距離)</p>
<p style="margin-left:20px">q<sub>i</sub> = γ<sup>T</sup>A<sub>i</sub>γ(投影到法向量的變異)</p>
<p>消去 μ<sub>i</sub>(Lagrange 乘子,Wu 2007 eq 3-5 → 3-7)後,得<b>剖面對數概似</b>:</p>
<p style="margin-left:20px"><b>L<sub>p</sub>(δ) = −½ Σ<sub>i</sub> s<sub>i</sub>² / q<sub>i</sub></b>
其中 δ = [α, β]<sup>T</sup></p>
<p>最大化 L<sub>p</sub> = 以三個同位素軸共同決定權重的加權最小二乘(不偏好任一軸)。</p>
<h3>每點共變異矩陣 A<sub>i</sub>(3×3)</h3>
<table border="1" cellpadding="6" cellspacing="0">
<tr><td>σ<sub>36</sub>²</td><td>0</td><td>0</td></tr>
<tr><td>0</td><td>σ<sub>39</sub>²</td><td>−k<sub>0</sub>·σ<sub>39</sub>²</td></tr>
<tr><td>0</td><td>−k<sub>0</sub>·σ<sub>39</sub>²</td><td>σ<sub>40</sub>²</td></tr>
</table>
<p>其中 k<sub>0</sub> = PR(<sup>40</sup>Ar/<sup>39</sup>Ar)<sub>K</sub>(預設 0.025004)。
非對角的 cov(<sup>39</sup>, <sup>40</sup>) 反映 ⁴⁰Ar(K) 回扣造成的反相關。</p>
<h3>Newton–Raphson 最佳化</h3>
<p>從 OLS δ<sub>0</sub> 起,迭代:</p>
<p style="margin-left:20px"><b>δ<sub>k+1</sub> = δ<sub>k</sub> + H<sup>−1</sup>·g</b></p>
<p>梯度 g = ∂L<sub>p</sub>/∂δ,Hessian H = −∂²L<sub>p</sub>/∂δ∂δ<sup>T</sup>
(在 MLE 為正定,= 觀測 Fisher information)。</p>
<p><b>回溯線搜尋</b>(pyADR v3.4 加入):步長最多減半 20 次,直到 L<sub>p</sub> 嚴格上升,
避免 Newton 過衝。收斂:|Δδ|/|δ| < 10<sup>−10</sup>。</p>
<h3>MSWD 與 95% CI</h3>
<p style="margin-left:20px"><b>S² = Σ<sub>i</sub> s<sub>i</sub>² / q<sub>i</sub>,
df = n − 2, MSWD = S² / df</b></p>
<p>(Wu 2007 eq 3-24;Mahon 1996。)</p>
<p>95% CI 以 χ<sup>2</sup><sub>df</sub> 分位數精確計算(scipy.stats.chi2),非常態近似。
若 MSWD > 上界,pyADR 套用 Wendt-Carl σ 放大:</p>
<p style="margin-left:20px">τ² = MSWD, σ<sub>δ</sub> → √τ² · σ<sub>δ</sub></p>
<h3>參數共變異</h3>
<p style="margin-left:20px"><b>cov(δ̂) = τ² · H<sup>−1</sup></b></p>
<p>1σ 不確定度:σ<sub>α</sub> = √cov<sub>11</sub>,σ<sub>β</sub> = √cov<sub>22</sub>。</p>
<p style="background:#fff4d0;padding:6px;border:1px solid #c0a020;">
<b>⚠ Wu (2007) eq 3-27 符號錯誤修正</b>:論文寫
cov(δ̂) = τ² · (∂²L<sub>p</sub>/∂δ∂δ<sup>T</sup>)<sup>−1</sup>。
在 MLE 處 ∂²L<sub>p</sub> 為<i>負</i>定,其逆給出<i>負變異</i>,顯然錯誤。正確的 ML 漸近共變異為
τ² · (−∂²L<sub>p</sub>/∂δ∂δ<sup>T</sup>)<sup>−1</sup> = τ² · H<sup>−1</sup>。
pyADR 用修正後形式。</p>
<h3>年齡誤差傳播(Renne 1998、Min 2000)</h3>
<p style="margin-left:20px"><b>σ<sub>T</sub>² = (∂T/∂β)² σ<sub>β</sub>²
+ (∂T/∂J)² σ<sub>J</sub>²
+ (∂T/∂λ)² σ<sub>λ</sub>²</b></p>
<p>其中</p>
<ul>
<li>∂T/∂β = J / [λ(1 + βJ)]</li>
<li>∂T/∂J = β / [λ(1 + βJ)]</li>
<li>∂T/∂λ = −ln(1 + βJ) / λ²</li>
</ul>
<h3>Mahon (1996) σ 上限(選用)</h3>
<p>對背景主導、σ<sub>i</sub>/|x<sub>i</sub>| >> 1 的階段,古典 Kent 權重會讓這些點影響過大。
設每軸上限以限制:</p>
<p style="margin-left:20px"><b>σ<sub>i,eff</sub> = min(σ<sub>i</sub>, c · |x<sub>i</sub>|)</b></p>
<p>典型 c = 0.2–0.5。None(預設)停用(古典 Kent)。</p>
<p>完整推導見 FORMULAS.md §11。</p>
"""
_HELP_REFS_HTML_ZH = """
<h2>參考文獻</h2>
<h3>3D 平面擬合(PlaneFit3D.py)</h3>
<ul>
<li>Kent J.T., Watson G.S., Onstott T.C. (1990) <i>Maximum likelihood estimation
of a plane in three dimensions.</i> Statistics 21: 411–426.</li>
<li>Wu C.-Y. (2007) <i>3-D Plane-fitting Program in 40Ar/39Ar Dating.</i>
台大地質所碩論(R94224113),指導 羅清華。數學推導見第 3 章。</li>
<li>Titterington D.M., Halliday A.N. (1979) <i>On the fitting of parallel isochrons
and the method of maximum likelihood.</i> Chem. Geol. 26: 183–195.</li>
<li>Mahon K.I. (1996) <i>The new "York" regression.</i> Int. Geol. Rev. 38: 293–303.
(MSWD 與修正權重)</li>
<li>Renne P.R. et al. (1998) <i>Intercalibration of standards, absolute ages and
uncertainties in 40Ar/39Ar dating.</i> Chem. Geol. 145: 117–152.</li>
<li>Min K. et al. (2000) <i>A test for systematic errors in 40Ar/39Ar
geochronology.</i> GCA 64: 73–98.</li>
<li>Koppers A.A.P. (2002) <i>ArArCALC — software for 40Ar/39Ar age calculations.</i>
Comput. Geosci. 28: 605–619.</li>
</ul>
<h3>等時線回歸</h3>
<ul>
<li>York D., Evensen N.M., Martínez M.L., De Basabe Delgado J. (2004)
<i>Unified equations for the slope, intercept, and standard errors of the best straight line.</i>
Am. J. Phys. 72: 367–375.</li>
<li>Vermeesch P. (2018) <i>IsoplotR: A free and open toolbox for geochronology.</i>
Geoscience Frontiers 9: 1479–1493. doi:10.1016/j.gsf.2018.04.001</li>
<li>Vermeesch P. (2024) <i>Errorchrons and anchored isochrons in IsoplotR.</i>
Geochronology 6: 397–407. doi:10.5194/gchron-6-397-2024</li>
<li>Li Y., Vermeesch P. (2021) <i>Short communication: Inverse isochron regression
for Re–Os, K–Ca and other chronometers.</i> Geochronology 3: 415–420.
doi:10.5194/gchron-3-415-2021</li>
<li>Mahon K.I. (1996) <i>The new "York" regression: application of an improved
statistical method to geochemistry.</i> Int. Geol. Rev. 38: 293–303.
(斜率–截距共變異公式)</li>
</ul>
<h3>MSWD / 加權平均統計</h3>
<ul>
<li>Wendt I., Carl C. (1991) <i>The statistical distribution of the mean squared
weighted deviation.</i> Chem. Geol. 86: 275–285.
(√MSWD 外部 σ 放大)</li>
<li>Schaen A.J. et al. (2021) <i>Interpreting and reporting <sup>40</sup>Ar/<sup>39</sup>Ar
geochronologic data.</i> GSA Bulletin 133: 461–487.
doi:10.1130/B35560.1(Ar/Ar 社群標準)</li>
</ul>
<h3>Ar/Ar 方法與衰變常數</h3>
<ul>
<li>McDougall I., Harrison T.M. (1999) <i>Geochronology and Thermochronology
by the <sup>40</sup>Ar/<sup>39</sup>Ar Method</i>, 2nd ed., Oxford University Press.
(Ar 成分數學標準)</li>
<li>Renne P.R. et al. (2010) <i>Joint determination of <sup>40</sup>K decay constants and
<sup>40</sup>Ar*/<sup>40</sup>K for the Fish Canyon sanidine standard.</i> GCA 74: 5349–5367.
(現代 λ 值)</li>
<li>Renne P.R. et al. (2011) <i>Response to the comment by W.H. Schwarz et al.
on "Joint determination of...".</i> GCA 75: 5097–5100.</li>
<li>Steiger R.H., Jäger E. (1977) <i>Subcommission on geochronology: Convention
on the use of decay constants in geo- and cosmochronology.</i> EPSL 36: 359–362.
(歷史 λ = 5.543e-10/yr)</li>
<li>Kuiper K.F. (2002) <i>The interpretation of inverse isochron diagrams in
<sup>40</sup>Ar/<sup>39</sup>Ar geochronology.</i> EPSL 203: 499–506.</li>
</ul>
<h3>大氣組成</h3>
<ul>
<li>Lee J.-Y. et al. (2006) <i>A redetermination of the isotopic abundances of
atmospheric Ar.</i> GCA 70: 4507–4512.(<sup>40</sup>Ar/<sup>36</sup>Ar = 298.56)</li>
</ul>
"""
# load UI
# ===============================================================================
class HomePage(QtWidgets.QMainWindow, UI.HomePage.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
#def resizeEvent(self, event):
class TypeSelect(QtWidgets.QMainWindow, UI.TypeSelect.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
_add_return_button(self)
_make_select_page_responsive(self)
class LinearRegressionPage(QtWidgets.QMainWindow, UI.LinearRegression.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def resizeEvent(self, event):
self.Isize = [100, 230, 670+event.size().width()-800, 450+event.size().height()-700]
self.photo.setGeometry(QtCore.QRect(self.Isize[0], self.Isize[1], self.Isize[2], self.Isize[3]))
class StatSelect(QtWidgets.QMainWindow, UI.StatSelect.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
_add_return_button(self)
_make_select_page_responsive(self)
class JStatistics(QtWidgets.QMainWindow, UI.JStatistics.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def resizeEvent(self, event):
self.Isize = [100, 230, 670+event.size().width()-800, 450+event.size().height()-700]
self.photo.setGeometry(QtCore.QRect(self.Isize[0], self.Isize[1], self.Isize[2], self.Isize[3]))
class T0Statistics(QtWidgets.QMainWindow, UI.T0Statistics.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def resizeEvent(self, event):
self.Isize = [150, 175, 600+event.size().width()-800, 250+event.size().height()-700]
self.photo.setGeometry(QtCore.QRect(self.Isize[0], self.Isize[1], self.Isize[2], self.Isize[3]))
self.Tsize =[150, 470+event.size().height()-700, 591, 101]
self.tableWidget.setGeometry(QtCore.QRect(self.Tsize[0], self.Tsize[1], self.Tsize[2], self.Tsize[3]))
self.numSelectedFiles.setGeometry(QtCore.QRect(150, 580+event.size().height()-700, 200, 31))
class AirRatioStatistics(QtWidgets.QMainWindow, UI.AirRatioStatistics.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def resizeEvent(self, event):
self.Isize = [200, 200, 350+event.size().width()-800, 275+event.size().height()-700]
self.photo.setGeometry(QtCore.QRect(self.Isize[0], self.Isize[1], self.Isize[2], self.Isize[3]))
self.Tsize =[210, 490+event.size().height()-700, 301, 111]
self.RatioTable.setGeometry(QtCore.QRect(self.Tsize[0], self.Tsize[1], self.Tsize[2], self.Tsize[3]))
self.numSelectedFiles.setGeometry(QtCore.QRect(150, 580+event.size().height()-700, 200, 31))
class MassRatio(QtWidgets.QMainWindow, UI.MassRatio.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
class JCalculation(QtWidgets.QMainWindow, UI.JCalculation.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
class JSelect(QtWidgets.QMainWindow, UI.JSelect.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
_add_return_button(self)
_make_select_page_responsive(self)
class ReselectTable(QtWidgets.QDialog, UI.ReselectDialog.Ui_Dialog):
def __init__(self, parent=None):
QtWidgets.QDialog.__init__(self, parent)
self.setupUi(self)
class ParameterSetting(QtWidgets.QMainWindow, UI.ParameterSetting.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def resizeEvent(self, event):
self.Tsize = [220, 200, 351+event.size().width()-800, 391+event.size().height()-700]
self.ParameetrTable.setGeometry(QtCore.QRect(self.Tsize[0], self.Tsize[1], self.Tsize[2], self.Tsize[3]))
class AgeCalculation(QtWidgets.QMainWindow, UI.AgeCalculation.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
class SaltCalculation(QtWidgets.QMainWindow, UI.SaltCalculation.Ui_MainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
def resizeEvent(self, event):
self.Tsize =[200, 200, 445+event.size().width()-800, 90+event.size().height()-700]