ararpy 0.1.14__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ararpy/__init__.py +1 -1
- ararpy/calc/age.py +9 -8
- ararpy/calc/basic.py +30 -1
- ararpy/calc/corr.py +35 -22
- ararpy/calc/regression.py +4 -3
- ararpy/files/calc_file.py +24 -15
- ararpy/smp/basic.py +50 -12
- ararpy/smp/corr.py +8 -6
- ararpy/smp/diffusion_funcs.py +73 -99
- ararpy/smp/export.py +216 -86
- ararpy/smp/initial.py +34 -20
- ararpy/smp/plots.py +11 -10
- ararpy/smp/sample.py +14 -9
- ararpy/smp/style.py +13 -5
- ararpy/smp/table.py +19 -21
- ararpy/{argon_diffusion_simulator → thermo}/__init__.py +1 -1
- ararpy/thermo/arrhenius.py +15 -0
- ararpy/{argon_diffusion_simulator/main.py → thermo/atomic_level_random_walk.py} +95 -6
- ararpy/thermo/basic.py +306 -0
- {ararpy-0.1.14.dist-info → ararpy-0.1.17.dist-info}/METADATA +17 -3
- {ararpy-0.1.14.dist-info → ararpy-0.1.17.dist-info}/RECORD +24 -22
- {ararpy-0.1.14.dist-info → ararpy-0.1.17.dist-info}/WHEEL +1 -1
- {ararpy-0.1.14.dist-info → ararpy-0.1.17.dist-info/licenses}/LICENSE +0 -0
- {ararpy-0.1.14.dist-info → ararpy-0.1.17.dist-info}/top_level.txt +0 -0
ararpy/smp/sample.py
CHANGED
|
@@ -743,7 +743,7 @@ DEFAULT_PLOT_STYLES = {
|
|
|
743
743
|
},
|
|
744
744
|
}
|
|
745
745
|
|
|
746
|
-
VERSION = '
|
|
746
|
+
VERSION = '20250301'
|
|
747
747
|
|
|
748
748
|
NAMED_DICT = {
|
|
749
749
|
"unknown": {"header": SAMPLE_INTERCEPT_HEADERS.copy()},
|
|
@@ -846,7 +846,8 @@ class Sample:
|
|
|
846
846
|
# self.__version = '20231116' # change smp parameters
|
|
847
847
|
# self.__version = '20240730' # change parameter table for thermo calculation
|
|
848
848
|
# self.__version = '20241028' # gain correction
|
|
849
|
-
self.__version = '20250102' # gain correction to blanks
|
|
849
|
+
# self.__version = '20250102' # gain correction to blanks
|
|
850
|
+
self.__version = '20250301' # error sigma adjustment
|
|
850
851
|
|
|
851
852
|
@property
|
|
852
853
|
def version(self):
|
|
@@ -943,14 +944,14 @@ class Table:
|
|
|
943
944
|
if header is None:
|
|
944
945
|
header = ['']
|
|
945
946
|
if data is None:
|
|
946
|
-
data = [['']]
|
|
947
|
+
data = [['' for i in range(len(header))]]
|
|
947
948
|
if colcount is None:
|
|
948
949
|
colcount = len(header)
|
|
949
950
|
if rowcount is None:
|
|
950
951
|
rowcount = len(data[0])
|
|
951
952
|
if coltypes is None:
|
|
952
953
|
# Note the difference between [xx] * 10 and [xx for i in range(10)]
|
|
953
|
-
coltypes = [{'type': '
|
|
954
|
+
coltypes = [{'type': 'text'} for i in range(colcount)]
|
|
954
955
|
self.id = id
|
|
955
956
|
self.name = name
|
|
956
957
|
self.colcount = colcount
|
|
@@ -958,12 +959,13 @@ class Table:
|
|
|
958
959
|
self.header = header
|
|
959
960
|
self.data = data
|
|
960
961
|
self.coltypes = coltypes
|
|
961
|
-
self.
|
|
962
|
-
self.
|
|
963
|
-
|
|
964
|
-
|
|
962
|
+
self.text_indexes = text_indexes if text_indexes is not None else []
|
|
963
|
+
self.numeric_indexes = numeric_indexes if numeric_indexes is not None else list(set(range(0, colcount)) - set(self.text_indexes))
|
|
964
|
+
self.decimal_places = 6
|
|
965
|
+
|
|
965
966
|
for k, v in kwargs.items():
|
|
966
967
|
setattr(self, k, v)
|
|
968
|
+
self.set_coltypes()
|
|
967
969
|
|
|
968
970
|
def set_coltypes(self):
|
|
969
971
|
for i in self.text_indexes:
|
|
@@ -971,7 +973,10 @@ class Table:
|
|
|
971
973
|
self.coltypes[i].update({'type': 'text'})
|
|
972
974
|
for i in self.numeric_indexes:
|
|
973
975
|
if i < self.colcount:
|
|
974
|
-
self.coltypes[i].update({
|
|
976
|
+
self.coltypes[i].update({
|
|
977
|
+
'type': 'numeric',
|
|
978
|
+
'numericFormat': {'pattern': {'mantissa': self.decimal_places}},
|
|
979
|
+
})
|
|
975
980
|
|
|
976
981
|
|
|
977
982
|
class Plot:
|
ararpy/smp/style.py
CHANGED
|
@@ -58,6 +58,13 @@ def set_plot_style(smp: Sample):
|
|
|
58
58
|
setattr(figure, 'title', Plot.Text())
|
|
59
59
|
setattr(getattr(figure, 'title'), 'text', f"{suffix} {getattr(figure, 'name', '')}")
|
|
60
60
|
|
|
61
|
+
age_unit = "Undefined"
|
|
62
|
+
try:
|
|
63
|
+
age_unit = str(smp.Info.preference['ageUnit']).capitalize()
|
|
64
|
+
except:
|
|
65
|
+
print(traceback.format_exc())
|
|
66
|
+
smp.AgeSpectraPlot.yaxis.title.text = f"Apparent Age ({age_unit})"
|
|
67
|
+
|
|
61
68
|
|
|
62
69
|
def reset_plot_scale(smp: Sample, only_figure: str = None):
|
|
63
70
|
"""
|
|
@@ -184,9 +191,10 @@ def set_table_style(sample: Sample):
|
|
|
184
191
|
for key, comp in basic.get_components(sample).items():
|
|
185
192
|
if isinstance(comp, Table):
|
|
186
193
|
comp.header = TABLEHEADER(index=int(comp.id))
|
|
187
|
-
comp.
|
|
188
|
-
comp.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
comp.set_coltypes()
|
|
195
|
+
# comp.colcount = len(comp.header)
|
|
196
|
+
# comp.coltypes = [{'type': 'numeric'}] * (comp.colcount)
|
|
197
|
+
# text_indexes = getattr(comp, 'text_indexes', [0]) if hasattr(comp, 'text_indexes') else [0]
|
|
198
|
+
# for i in text_indexes:
|
|
199
|
+
# comp.coltypes[i] = {'type': 'text'}
|
|
192
200
|
|
ararpy/smp/table.py
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
#
|
|
10
10
|
#
|
|
11
11
|
"""
|
|
12
|
+
import ast
|
|
13
|
+
import re
|
|
12
14
|
import copy
|
|
13
15
|
from .. import calc
|
|
14
16
|
from . import (sample as samples, basic)
|
|
@@ -96,6 +98,12 @@ def update_handsontable(smp: Sample, data: list, id: str):
|
|
|
96
98
|
'true': True, 'false': False, 'True': True, 'False': False, '1': True, '0': False, 'none': False,
|
|
97
99
|
}
|
|
98
100
|
return [bools_dict.get(str(col).lower(), False) for col in cols]
|
|
101
|
+
|
|
102
|
+
def _digitize_data(a):
|
|
103
|
+
# pattern = r'^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?$'
|
|
104
|
+
# return [[ast.literal_eval(str(cell)) if re.fullmatch(pattern, str(cell)) else np.nan if str(cell) == "" else cell for cell in row] for row in a]
|
|
105
|
+
return a
|
|
106
|
+
|
|
99
107
|
try:
|
|
100
108
|
smp.SequenceName = data[0]
|
|
101
109
|
except IndexError:
|
|
@@ -104,47 +112,37 @@ def update_handsontable(smp: Sample, data: list, id: str):
|
|
|
104
112
|
update_all_table = False
|
|
105
113
|
try:
|
|
106
114
|
if data[1] != smp.SequenceValue:
|
|
115
|
+
update_all_table = True
|
|
107
116
|
smp.SequenceValue = data[1]
|
|
108
117
|
except IndexError:
|
|
109
118
|
pass
|
|
110
|
-
else:
|
|
111
|
-
update_all_table = True
|
|
112
119
|
|
|
113
120
|
if id == '1': # 样品值
|
|
114
121
|
data = _normalize_data(data, len(samples.SAMPLE_INTERCEPT_HEADERS), 2)
|
|
115
|
-
smp.SampleIntercept = data
|
|
122
|
+
smp.SampleIntercept = _digitize_data(data)
|
|
116
123
|
elif id == '2': # 本底值
|
|
117
124
|
data = _normalize_data(data, len(samples.BLANK_INTERCEPT_HEADERS), 2)
|
|
118
|
-
smp.BlankIntercept = data
|
|
125
|
+
smp.BlankIntercept = _digitize_data(data)
|
|
119
126
|
elif id == '3': # 校正值
|
|
120
127
|
data = _normalize_data(data, len(samples.CORRECTED_HEADERS), 2)
|
|
121
|
-
smp.CorrectedValues = data
|
|
128
|
+
smp.CorrectedValues = _digitize_data(data)
|
|
122
129
|
elif id == '4': # Degas table
|
|
123
130
|
data = _normalize_data(data, len(samples.DEGAS_HEADERS), 2)
|
|
124
|
-
smp.DegasValues = data
|
|
131
|
+
smp.DegasValues = _digitize_data(data)
|
|
125
132
|
elif id == '5': # 发行表
|
|
126
133
|
data = _normalize_data(data, len(samples.PUBLISH_TABLE_HEADERS), 2)
|
|
127
|
-
smp.PublishValues = data
|
|
134
|
+
smp.PublishValues = _digitize_data(data)
|
|
128
135
|
elif id == '6': # 年龄谱
|
|
129
136
|
data = _normalize_data(data, len(samples.SPECTRUM_TABLE_HEADERS), 2)
|
|
130
|
-
smp.ApparentAgeValues = data
|
|
137
|
+
smp.ApparentAgeValues = _digitize_data(data)
|
|
131
138
|
elif id == '7': # 等时线
|
|
132
|
-
smp.IsochronMark = data[2]
|
|
139
|
+
smp.IsochronMark = _digitize_data(data)[2]
|
|
133
140
|
data = _normalize_data(data, len(samples.ISOCHRON_TABLE_HEADERS), 3)
|
|
134
|
-
smp.IsochronValues = data
|
|
135
|
-
smp.
|
|
136
|
-
i for i in range(len(smp.IsochronMark)) if str(smp.IsochronMark[i]) == "1"]
|
|
137
|
-
smp.SelectedSequence2 = [
|
|
138
|
-
i for i in range(len(smp.IsochronMark)) if str(smp.IsochronMark[i]) == "2"]
|
|
139
|
-
smp.UnselectedSequence = [
|
|
140
|
-
i for i in range(len(smp.IsochronMark)) if
|
|
141
|
-
i not in smp.SelectedSequence1 + smp.SelectedSequence2]
|
|
142
|
-
#
|
|
143
|
-
smp.Info.results.selection[0]['data'] = smp.SelectedSequence1
|
|
144
|
-
smp.Info.results.selection[1]['data'] = smp.SelectedSequence2
|
|
145
|
-
smp.Info.results.selection[2]['data'] = smp.UnselectedSequence
|
|
141
|
+
smp.IsochronValues = _digitize_data(data)
|
|
142
|
+
smp.sequence()
|
|
146
143
|
elif id == '8': # 总参数
|
|
147
144
|
data = _normalize_data(data, len(samples.TOTAL_PARAMS_HEADERS), 2)
|
|
145
|
+
data = _digitize_data(data)
|
|
148
146
|
data[101: 112] = [_strToBool(i) for i in data[101: 112]]
|
|
149
147
|
smp.TotalParam = data
|
|
150
148
|
else:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"""
|
|
4
4
|
# ==========================================
|
|
5
5
|
# Copyright 2024 Yang
|
|
6
|
-
#
|
|
6
|
+
# argon atomic_level_random_walk model - main
|
|
7
7
|
# ==========================================
|
|
8
8
|
#
|
|
9
9
|
#
|
|
@@ -53,7 +53,7 @@ def _uniform_sphere_step(step_length, shape):
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def walker(pos, step_length, total_nsteps, min_bound, max_bound, scale=0., compensation=0, remove=True,
|
|
56
|
-
conditions=None, boundary_factor=1):
|
|
56
|
+
conditions=None, boundary_factor=1, decay=0, parent=0):
|
|
57
57
|
|
|
58
58
|
if len(pos) == 0:
|
|
59
59
|
return pos
|
|
@@ -122,7 +122,7 @@ def walker(pos, step_length, total_nsteps, min_bound, max_bound, scale=0., compe
|
|
|
122
122
|
|
|
123
123
|
|
|
124
124
|
def walker2(pos: np.ndarray, duration, step_length, min_bound, max_bound, time_scale, frequency,
|
|
125
|
-
conditions=None, remove: bool = True):
|
|
125
|
+
conditions=None, remove: bool = True, decay=0, parent=0):
|
|
126
126
|
"""
|
|
127
127
|
:return:
|
|
128
128
|
"""
|
|
@@ -132,6 +132,7 @@ def walker2(pos: np.ndarray, duration, step_length, min_bound, max_bound, time_s
|
|
|
132
132
|
|
|
133
133
|
dimension = pos.shape[-1] if len(pos.shape) > 1 else 1
|
|
134
134
|
|
|
135
|
+
|
|
135
136
|
dt = time_scale
|
|
136
137
|
if conditions is None:
|
|
137
138
|
if dimension == 3:
|
|
@@ -141,6 +142,13 @@ def walker2(pos: np.ndarray, duration, step_length, min_bound, max_bound, time_s
|
|
|
141
142
|
|
|
142
143
|
for step in range(int(1e16)):
|
|
143
144
|
|
|
145
|
+
n = parent - int(parent * math.exp(-1 * decay * dt / (3600 * 24 * 365.2425)))
|
|
146
|
+
# print(f"number of new pos = {n}")
|
|
147
|
+
parent = parent - n
|
|
148
|
+
new_pos = np.random.uniform(max(max_bound), min(min_bound), size=(int(n), dimension))
|
|
149
|
+
new_pos = new_pos[np.all((new_pos >= min_bound) & (new_pos <= max_bound), axis=1)]
|
|
150
|
+
pos = np.concatenate((pos, new_pos), axis=0)
|
|
151
|
+
|
|
144
152
|
res = np.zeros(0).reshape(0, dimension)
|
|
145
153
|
for index, each in enumerate(conditions):
|
|
146
154
|
# each = (x1, y1, z1, x2, y2, z2, gamma)
|
|
@@ -235,6 +243,7 @@ class Domain:
|
|
|
235
243
|
# 初始化粒子位置,随机分布在网格内
|
|
236
244
|
# 初始原子个数 1e14 原子密度 个/立方厘米 1e-4是单位换算 cm/µm
|
|
237
245
|
natoms = int(self.atom_density ** (self.dimension / 3) * (abs(max(self.max_bound) - min(self.min_bound)) * 1e-4) ** self.dimension)
|
|
246
|
+
natoms = 1 if natoms == 0 else natoms
|
|
238
247
|
self.positions = np.random.uniform(max(self.max_bound), min(self.min_bound), size=(int(natoms), self.dimension))
|
|
239
248
|
self.positions = self.positions[np.all((self.positions >= self.min_bound) & (self.positions <= self.max_bound), axis=1)]
|
|
240
249
|
for dom in self.inclusions:
|
|
@@ -320,7 +329,7 @@ class DiffSimulation:
|
|
|
320
329
|
|
|
321
330
|
def run_sequence(self, times, temperatures, statuses, targets, domains: List[Domain], nsteps_factor=None,
|
|
322
331
|
simulating: bool = False, epsilon=0.001, start_time=0, k=1.2, use_walker1=True,
|
|
323
|
-
scoring: bool = False):
|
|
332
|
+
scoring: bool = False, decay=0, parent=0):
|
|
324
333
|
pos = copy.deepcopy(self.positions)
|
|
325
334
|
self.seq = [] # 用于记录参数
|
|
326
335
|
|
|
@@ -369,7 +378,8 @@ class DiffSimulation:
|
|
|
369
378
|
_pos = walker(
|
|
370
379
|
copy.deepcopy(pos), step_length=step_length, total_nsteps=total_steps,
|
|
371
380
|
min_bound=self.min_bound, max_bound=self.max_bound, scale=scale,
|
|
372
|
-
compensation=compensation, remove=True, conditions=conditions, boundary_factor=boundary_factor
|
|
381
|
+
compensation=compensation, remove=True, conditions=conditions, boundary_factor=boundary_factor,
|
|
382
|
+
decay=decay, parent=parent
|
|
373
383
|
)
|
|
374
384
|
|
|
375
385
|
# self.seq[index].update({
|
|
@@ -383,7 +393,7 @@ class DiffSimulation:
|
|
|
383
393
|
scale = k
|
|
384
394
|
_pos = walker2(copy.deepcopy(pos), duration=heating_duration, step_length=step_length,
|
|
385
395
|
min_bound=self.min_bound, max_bound=self.max_bound, time_scale=scale,
|
|
386
|
-
frequency=self.frequency, conditions=conditions, remove=True)
|
|
396
|
+
frequency=self.frequency, conditions=conditions, remove=True, decay=decay, parent=parent)
|
|
387
397
|
boundary_factor = 1
|
|
388
398
|
compensation = 0
|
|
389
399
|
|
|
@@ -429,6 +439,8 @@ class DiffSimulation:
|
|
|
429
439
|
|
|
430
440
|
print(f"{index = } {duration} - {heating_duration = } - {temperature = } - {total_steps = } - conc = {len(pos) / self.natoms * 100:.2f}% - {time.time() - _start:.5f}s")
|
|
431
441
|
|
|
442
|
+
self.positions = copy.deepcopy(pos)
|
|
443
|
+
|
|
432
444
|
def run_persecond(self, times, temperatures, statuses, targets, domains: List[Domain], scale=None,
|
|
433
445
|
simulation: bool = False, epsilon=0.001, start_time=0):
|
|
434
446
|
seq = []
|
|
@@ -442,6 +454,83 @@ class DiffSimulation:
|
|
|
442
454
|
|
|
443
455
|
|
|
444
456
|
def run(times, temps, statuses, energies, fractions, ndoms: int = 1, grain_szie=275, atom_density=1e10, frequency=1e13,
|
|
457
|
+
dimension: int = 3, targets: list = None, epsilon: float = 0.001, simulation: bool = False,
|
|
458
|
+
file_name: str = "Y70", ignore_error: bool = False, positions=None, **kwargs):
|
|
459
|
+
"""
|
|
460
|
+
:param times:
|
|
461
|
+
:param temps:
|
|
462
|
+
:param statuses:
|
|
463
|
+
:param energies:
|
|
464
|
+
:param fractions:
|
|
465
|
+
:param dimension:
|
|
466
|
+
:param ndoms:
|
|
467
|
+
:param targets:
|
|
468
|
+
:param epsilon:
|
|
469
|
+
:param simulation:
|
|
470
|
+
:param file_name:
|
|
471
|
+
:param ignore_error:
|
|
472
|
+
:return:
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
demo = DiffSimulation()
|
|
476
|
+
|
|
477
|
+
def _(n, es, fs):
|
|
478
|
+
# fs 应从大到小,父空间在前,子空间在后
|
|
479
|
+
|
|
480
|
+
# demo.grain_size = 300
|
|
481
|
+
# demo.size_scale = 0.05
|
|
482
|
+
# demo.atom_density = 1e14 # 原子密度 个/立方厘米
|
|
483
|
+
demo.dimension = dimension
|
|
484
|
+
|
|
485
|
+
demo.size_scale = 1
|
|
486
|
+
demo.grain_size = grain_szie
|
|
487
|
+
demo.atom_density = atom_density # 原子密度 个/立方厘米
|
|
488
|
+
demo.frequency = frequency
|
|
489
|
+
|
|
490
|
+
# domains应该从外到内
|
|
491
|
+
domains = []
|
|
492
|
+
for i in range(n-1, 0-1, -1):
|
|
493
|
+
size = int(demo.grain_size * fs[i]) * demo.size_scale
|
|
494
|
+
center = np.zeros(demo.dimension)
|
|
495
|
+
if i == 2:
|
|
496
|
+
ad = demo.atom_density * 5 / 4
|
|
497
|
+
else:
|
|
498
|
+
ad = demo.atom_density
|
|
499
|
+
dom = Domain(
|
|
500
|
+
dimension=demo.dimension, atom_density=ad, min_bound=center - size / 2, max_bound=center + size / 2,
|
|
501
|
+
energy=es[i], fraction=fs[i], inclusions=[domains[-1]] if len(domains) >= 1 else []
|
|
502
|
+
)
|
|
503
|
+
domains.append(dom)
|
|
504
|
+
# domains应该从外到内, 上面为了inclusion以及方便不同扩散域设置不同的密度,要按照从小到大的顺序生成,但是后面行走的时候要根据不同条件设置系数,要从外到内
|
|
505
|
+
demo.domains = sorted(domains, key=lambda dom: dom.fraction, reverse=True)
|
|
506
|
+
|
|
507
|
+
demo.setup()
|
|
508
|
+
|
|
509
|
+
demo.name = f"{file_name}"
|
|
510
|
+
|
|
511
|
+
print(f"Total Atoms: {demo.natoms}, atoms in each dom: {[dom.natoms for dom in demo.domains]} filename: {demo.name}")
|
|
512
|
+
|
|
513
|
+
if positions is not None:
|
|
514
|
+
demo.positions = positions
|
|
515
|
+
demo.natoms = len(positions)
|
|
516
|
+
|
|
517
|
+
demo.run_sequence(times=times, temperatures=temps, statuses=statuses, targets=targets, domains=demo.domains,
|
|
518
|
+
epsilon=epsilon, simulating=simulation, **kwargs)
|
|
519
|
+
# demo.run_persecond(times=times, temperatures=temps, domains=demo.domains, targets=target,
|
|
520
|
+
# epsilon=epsilon, simulation=simulation)
|
|
521
|
+
|
|
522
|
+
return demo
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
return _(ndoms, energies, fractions), True
|
|
526
|
+
except OverEpsilonError as e:
|
|
527
|
+
if ignore_error:
|
|
528
|
+
return demo, False
|
|
529
|
+
else:
|
|
530
|
+
raise
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def run_40ar(times, temps, statuses, energies, fractions, ndoms: int = 1, grain_szie=275, atom_density=1e10, frequency=1e13,
|
|
445
534
|
dimension: int = 3, targets: list = None, epsilon: float = 0.001, simulation: bool = False,
|
|
446
535
|
file_name: str = "Y70", ignore_error: bool = False, **kwargs):
|
|
447
536
|
"""
|
ararpy/thermo/basic.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: UTF-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
# ==========================================
|
|
5
|
+
# Copyright 2025 Yang
|
|
6
|
+
# ararpy - basic
|
|
7
|
+
# ==========================================
|
|
8
|
+
#
|
|
9
|
+
#
|
|
10
|
+
#
|
|
11
|
+
"""
|
|
12
|
+
import numpy as np
|
|
13
|
+
from math import log
|
|
14
|
+
|
|
15
|
+
SEC2YEAR = 3600 * 24 * 365.2425
|
|
16
|
+
CAL2JOULE = 4.184
|
|
17
|
+
CM2UM = 10000
|
|
18
|
+
GAS_CONSTANT = 8.31446261815324 # in J / (mol K)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def gammq(a, x):
|
|
22
|
+
if x < 0 or a <= 0:
|
|
23
|
+
raise ValueError("ERROR(GAMMQ): x < 0 or a <= 0")
|
|
24
|
+
|
|
25
|
+
if x < a + 1:
|
|
26
|
+
gamser, gln = gser(a, x)
|
|
27
|
+
return 1.0 - gamser
|
|
28
|
+
else:
|
|
29
|
+
gammcf, gln = gcf(a, x)
|
|
30
|
+
return gammcf
|
|
31
|
+
|
|
32
|
+
def gser(a, x, itmax=100, eps=3e-7):
|
|
33
|
+
gln = gammln(a)
|
|
34
|
+
if x <= 0:
|
|
35
|
+
if x < 0:
|
|
36
|
+
raise ValueError("ERROR(GSER): x < 0")
|
|
37
|
+
return 0.0, gln
|
|
38
|
+
|
|
39
|
+
ap = a
|
|
40
|
+
sum_ = 1.0 / a
|
|
41
|
+
delta = sum_
|
|
42
|
+
for n in range(1, itmax + 1):
|
|
43
|
+
ap += 1
|
|
44
|
+
delta *= x / ap
|
|
45
|
+
sum_ += delta
|
|
46
|
+
if abs(delta) < abs(sum_) * eps:
|
|
47
|
+
break
|
|
48
|
+
else:
|
|
49
|
+
raise RuntimeError("ERROR(GSER): a too large, itmax too small")
|
|
50
|
+
|
|
51
|
+
gamser = sum_ * np.exp(-x + a * np.log(x) - gln)
|
|
52
|
+
return gamser, gln
|
|
53
|
+
|
|
54
|
+
def gcf(a, x, itmax=100, eps=3e-7):
|
|
55
|
+
gln = gammln(a)
|
|
56
|
+
gold = 0.0
|
|
57
|
+
a0 = 1.0
|
|
58
|
+
a1 = x
|
|
59
|
+
b0 = 0.0
|
|
60
|
+
b1 = 1.0
|
|
61
|
+
fac = 1.0
|
|
62
|
+
for n in range(1, itmax + 1):
|
|
63
|
+
an = float(n)
|
|
64
|
+
ana = an - a
|
|
65
|
+
a0 = (a1 + a0 * ana) * fac
|
|
66
|
+
b0 = (b1 + b0 * ana) * fac
|
|
67
|
+
anf = an * fac
|
|
68
|
+
a1 = x * a0 + anf * a1
|
|
69
|
+
b1 = x * b0 + anf * b1
|
|
70
|
+
if a1 != 0:
|
|
71
|
+
fac = 1.0 / a1
|
|
72
|
+
g = b1 * fac
|
|
73
|
+
if abs((g - gold) / g) < eps:
|
|
74
|
+
return g * np.exp(-x + a * np.log(x) - gln), gln
|
|
75
|
+
gold = g
|
|
76
|
+
else:
|
|
77
|
+
raise RuntimeError("ERROR(GCF): a too large, itmax too small")
|
|
78
|
+
|
|
79
|
+
def gammln(xx):
|
|
80
|
+
cof = np.array([76.18009173, -86.50532033, 24.01409822,
|
|
81
|
+
-1.231739516, 0.00120858003, -0.00000536382])
|
|
82
|
+
stp = 2.50662827465
|
|
83
|
+
half = 0.5
|
|
84
|
+
one = 1.0
|
|
85
|
+
fpf = 5.5
|
|
86
|
+
|
|
87
|
+
x = xx - one
|
|
88
|
+
tmp = x + fpf
|
|
89
|
+
tmp = (x + half) * np.log(tmp) - tmp
|
|
90
|
+
ser = one
|
|
91
|
+
for j in range(6):
|
|
92
|
+
x += one
|
|
93
|
+
ser += cof[j] / x
|
|
94
|
+
|
|
95
|
+
return tmp + np.log(stp * ser)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def fit(x, y, sigx, sigy, pho=None):
|
|
99
|
+
"""
|
|
100
|
+
equivalent to York2 regression
|
|
101
|
+
y = a + bx
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
x
|
|
106
|
+
y
|
|
107
|
+
sigx
|
|
108
|
+
sigy
|
|
109
|
+
pho
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
a: intercept
|
|
114
|
+
b: slope
|
|
115
|
+
siga: intercept uncertainty
|
|
116
|
+
sigb: slope uncertainty
|
|
117
|
+
chi2: chi-square value
|
|
118
|
+
q: the incomplete gamma function value, should be large for the best fitting.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
print(f"{x = }")
|
|
123
|
+
print(f"{y = }")
|
|
124
|
+
print(f"{sigx = }")
|
|
125
|
+
print(f"{sigy = }")
|
|
126
|
+
print(f"{pho = }")
|
|
127
|
+
|
|
128
|
+
ndata = len(x)
|
|
129
|
+
if pho is None:
|
|
130
|
+
pho = np.zeros(ndata)
|
|
131
|
+
nd = 20
|
|
132
|
+
imax = 20
|
|
133
|
+
xerr = 0.001
|
|
134
|
+
wt = []
|
|
135
|
+
a = 0
|
|
136
|
+
b = -1.
|
|
137
|
+
siga = 0
|
|
138
|
+
sigb = 0
|
|
139
|
+
chi2 = 0
|
|
140
|
+
q = 0
|
|
141
|
+
iter = 0
|
|
142
|
+
|
|
143
|
+
while iter <= imax:
|
|
144
|
+
sx = 0.
|
|
145
|
+
sy = 0.
|
|
146
|
+
st2 = 0.
|
|
147
|
+
st3 = 0.
|
|
148
|
+
ss = 0.
|
|
149
|
+
b0 = b
|
|
150
|
+
|
|
151
|
+
for i in range(ndata):
|
|
152
|
+
wt.append(0)
|
|
153
|
+
wt[i] = 1. / (sigy[i] ** 2 + b ** 2 * sigx[i] ** 2 - 2 * pho[i] * sigx[i] * sigy[i])
|
|
154
|
+
ss = ss + wt[i]
|
|
155
|
+
sx = sx + x[i] * wt[i]
|
|
156
|
+
sy = sy + y[i] * wt[i]
|
|
157
|
+
|
|
158
|
+
# print(f"{x[i] = }, {y[i] = }, {wt[i] = }")
|
|
159
|
+
|
|
160
|
+
sxoss = sx / ss
|
|
161
|
+
syoss = sy / ss
|
|
162
|
+
|
|
163
|
+
for i in range(ndata):
|
|
164
|
+
t1 = (x[i] - sxoss) * sigy[i] ** 2
|
|
165
|
+
t2 = (y[i] - syoss) * sigx[i] ** 2 * b
|
|
166
|
+
t3 = sigx[i] * sigy[i] * pho[i]
|
|
167
|
+
st2 = st2 + wt[i] ** 2 * (y[i] - syoss) * (t1 + t2 - t3 * (y[i] - syoss))
|
|
168
|
+
st3 = st3 + wt[i] ** 2 * (x[i] - sxoss) * (t1 + t2 - b * t3 * (x[i] - sxoss))
|
|
169
|
+
|
|
170
|
+
b = st2 / st3
|
|
171
|
+
iter = iter + 1
|
|
172
|
+
|
|
173
|
+
# print(f"{sxoss = }, {syoss = }, {b = }, {abs(b0 - b) = }")
|
|
174
|
+
|
|
175
|
+
if abs(b0 - b) > xerr:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
a = (syoss - sxoss * b)
|
|
179
|
+
# print(f"{a = }, {b = }")
|
|
180
|
+
sgt1 = 0.
|
|
181
|
+
sgt2 = 0.
|
|
182
|
+
|
|
183
|
+
for i in range(ndata):
|
|
184
|
+
sgt1 = sgt1 + wt[i] * (x[i] - sxoss) ** 2
|
|
185
|
+
sgt2 = sgt2 + wt[i] * x[i] ** 2
|
|
186
|
+
|
|
187
|
+
sigb = (1. / sgt1) ** 0.5
|
|
188
|
+
siga = sigb * (sgt2 / ss) ** 0.5
|
|
189
|
+
chi2 = 0.
|
|
190
|
+
|
|
191
|
+
for i in range(ndata):
|
|
192
|
+
chi2 = chi2 + wt[i] * (y[i] - a - b * x[i]) ** 2
|
|
193
|
+
|
|
194
|
+
q = gammq(0.5 * (ndata - 2), 0.5 * chi2)
|
|
195
|
+
|
|
196
|
+
if abs(b0 - b) <= xerr:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
return a, b, siga, sigb, chi2, q
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_tc(da2, sda2, E, sE, cooling_rate=10, temp_in_celsius=True, temp=None, A: float = 27.0, R=None, pho=0):
|
|
203
|
+
"""
|
|
204
|
+
E / (R Tc) = ln(A t D0/a2)
|
|
205
|
+
E: diffusion activation energy
|
|
206
|
+
R: gas constant, 8.314 J/(K*mol)
|
|
207
|
+
Tc: closure temperature
|
|
208
|
+
A: geometric constant, A is 55, 27, or 8.7 for volume diffusion from a sphere, cylinder or plane sheet respectively
|
|
209
|
+
tau: time constant, can be determined by
|
|
210
|
+
(1) a pre-defined cooling rate following t = -R Tc2 / E * dT / dt, where dT / dt is the average cooling rate of the local geological body
|
|
211
|
+
(2) the age of dated sample following t = R * age * E * (Tp - Tc), where Tp is the temperature at present that could be 300 K.
|
|
212
|
+
D0: the diffusion coefficient at the given temperature, which will be the frequency factor (the pre-exponential factor) whe the temperature is infinite high
|
|
213
|
+
a: the effective diffusion radius, the characteristic dimension of the system, or the domain size, or roughly the grain size
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
In either way of them, a equation with Tc in both right and left hand will be yielded. Then using iterative calculation, the Tc can be determined.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
-------
|
|
220
|
+
da2: D/a2 in 1/a, pear year
|
|
221
|
+
sda2:
|
|
222
|
+
E: diffusion activation energy
|
|
223
|
+
sE:
|
|
224
|
+
pho: covariance of da2 and E
|
|
225
|
+
temp: temperature of the given diffusion coefficient, in K
|
|
226
|
+
A: geometric constant
|
|
227
|
+
R: gas constant
|
|
228
|
+
cooling_rate: in degree/m.y., degrees per million year
|
|
229
|
+
temp_in_celsius: temperature in celsius, True, else in K, False
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
|
|
234
|
+
"""
|
|
235
|
+
R = 8.314 if R is None else R # in J/K mol
|
|
236
|
+
Tc = 600
|
|
237
|
+
sTc = 0
|
|
238
|
+
Tm = 99999999 if temp is None else temp
|
|
239
|
+
Tm = Tm + 273.15 if temp_in_celsius else Tm
|
|
240
|
+
cooling_rate = cooling_rate / 1000000
|
|
241
|
+
|
|
242
|
+
iter_num = 0
|
|
243
|
+
iter_diff = 100
|
|
244
|
+
while abs(iter_diff) > 0.01 and iter_num < 100:
|
|
245
|
+
tau = R * Tc ** 2 / (E * cooling_rate)
|
|
246
|
+
# new_Tc = (E/R) / log(A * tau * da2)
|
|
247
|
+
new_Tc = (R / E) * log(A * tau * da2) + 1 / Tm #
|
|
248
|
+
d1 = cooling_rate / (A * da2 * Tc ** 2)
|
|
249
|
+
s1 = d1 ** 2 * sda2 ** 2 # da2
|
|
250
|
+
d2 = R / E ** 2 * (log(A * tau * da2) + 1)
|
|
251
|
+
s2 = d2 ** 2 * sE ** 2 # E
|
|
252
|
+
d3 = 2 * R / (E * Tc)
|
|
253
|
+
s3 = d3 ** 2 * sTc ** 2 # Tc
|
|
254
|
+
new_Tc = 1 / new_Tc
|
|
255
|
+
iter_diff = abs(new_Tc - Tc)
|
|
256
|
+
Tc = new_Tc
|
|
257
|
+
sTc = Tc ** 2 * np.sqrt(s1 + s2 + s3 + 2 * d1 * d2 * pho)
|
|
258
|
+
iter_num += 1
|
|
259
|
+
# print(f"Get Tc: {iter_num = }, {Tc = }, {tau = }, {Tm = }")
|
|
260
|
+
return Tc - 273.15 if temp_in_celsius else Tc, sTc
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
"""
|
|
265
|
+
tests
|
|
266
|
+
"""
|
|
267
|
+
# 柴田贤, 钾长石
|
|
268
|
+
print("# 柴田贤, 钾长石")
|
|
269
|
+
print(get_tc(da2=5.6 * 3600 * 24 * 365.24, sda2=0, E=28.8 * 1000, R=1.987, sE=0, cooling_rate=5, A=8.7))
|
|
270
|
+
print(get_tc(da2=5.6 * 3600 * 24 * 365.24, sda2=0, E=28.8 * CAL2JOULE * 1000, sE=0, cooling_rate=5, A=8.7))
|
|
271
|
+
print(get_tc(da2=5.6 * 3600 * 24 * 365.24, sda2=0, E=28.8 * CAL2JOULE * 1000, sE=0, cooling_rate=30, A=8.7))
|
|
272
|
+
# 杨静, 斜钾铁矾 K101-1
|
|
273
|
+
print("# 杨静, 斜钾铁矾 K101-1")
|
|
274
|
+
print(get_tc(da2=10 ** 15.07 * 3600 * 24 * 365.24, sda2=0, E=76.17 * 1000 * CAL2JOULE, sE=0, cooling_rate=5, A=55)) # 这里跟文章中吻合
|
|
275
|
+
print(get_tc(da2=10 ** 15.07 * 3600 * 24 * 365.24, sda2=0, E=76.17 * 1000 * CAL2JOULE, sE=0, cooling_rate=10, A=55)) # 10冷却速率不吻合
|
|
276
|
+
# 杨静, 斜钾铁矾 K101-2
|
|
277
|
+
print("# 杨静, 斜钾铁矾 K101-2")
|
|
278
|
+
print(get_tc(da2=10 ** 12.35 * 3600 * 24 * 365.24, sda2=0, E=66.43 * 1000 * CAL2JOULE, sE=0, cooling_rate=5, A=55)) # 这里跟文章中吻合
|
|
279
|
+
print(get_tc(da2=10 ** 12.35 * 3600 * 24 * 365.24, sda2=0, E=66.43 * 1000 * CAL2JOULE, sE=0, cooling_rate=10, A=55)) # 10冷却速率不吻合
|
|
280
|
+
# 芦武长
|
|
281
|
+
print("# 芦武长")
|
|
282
|
+
print(get_tc(da2=3e-4 * 3600 * 24 * 365.24, sda2=0, E=35 * 1000 * CAL2JOULE, sE=0, cooling_rate=0.1, A=27, temp=600)) # 缺乏样品特征尺寸参数,da2不对,结果不对
|
|
283
|
+
print(get_tc(da2=3e-4 * 3600 * 24 * 365.24, sda2=0, E=35 * 1000 * CAL2JOULE, sE=0, cooling_rate=1, A=27, temp=600)) # 缺乏样品特征尺寸参数,da2不对,结果不对
|
|
284
|
+
# Harrison 1981, 角闪石, A = 55
|
|
285
|
+
print("# Harrison 1981, 角闪石, A = 55")
|
|
286
|
+
print(get_tc(da2=0.024 * SEC2YEAR / 0.0080 ** 2, sda2=0, E=64.1 * 1000 * CAL2JOULE, sE=0, cooling_rate=500, A=55))
|
|
287
|
+
print(get_tc(da2=0.024 * SEC2YEAR / 0.0080 ** 2, sda2=0, E=64.1 * 1000 * CAL2JOULE, sE=0, cooling_rate=5, A=55))
|
|
288
|
+
# Harrison 1985, 黑云母, A = 27
|
|
289
|
+
print("# Harrison 1985, 黑云母, A = 27")
|
|
290
|
+
print(get_tc(da2=0.077 * SEC2YEAR / 0.0150 ** 2, sda2=0, E=47.0 * 1000 * CAL2JOULE, sE=0, cooling_rate=100, A=27))
|
|
291
|
+
print(get_tc(da2=0.077 * SEC2YEAR / 0.0150 ** 2, sda2=0, E=47.0 * 1000 * CAL2JOULE, sE=0, cooling_rate=1, A=27))
|
|
292
|
+
print("# Blereau 2019, 大隅石,Osumilite, A = 8.7")
|
|
293
|
+
print(get_tc(da2=8.34e8 * SEC2YEAR / (0.0175 ** 2), sda2=0, E=461 * 1000, sE=0, cooling_rate=10, A=8.7))
|
|
294
|
+
print(get_tc(da2=6.70e8 * SEC2YEAR / (0.0175 ** 2), sda2=0, E=453 * 1000, sE=0, cooling_rate=10, A=8.7)) # 这两个与原文对不上,且原文描述表格与文字不符
|
|
295
|
+
print(get_tc(da2=4.49e5 * SEC2YEAR / (0.0175 ** 2), sda2=0, E=369 * 1000, sE=0, cooling_rate=10, A=8.7)) # 这两个与原文对不上,且原文描述表格与文字不符
|
|
296
|
+
print("# Thern 2020, 电气石, A = 55") # 大部分都对或者接近,不完全一致的原因大概是他们采用蒙特卡洛方法计算且正负误差不一样
|
|
297
|
+
print(get_tc(da2=4.86e22 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=678 * 1000, sE=0, cooling_rate=10, A=55))
|
|
298
|
+
print(get_tc(da2=8.99e23 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=756 * 1000, sE=0, cooling_rate=10, A=55))
|
|
299
|
+
print(get_tc(da2=3.95e19 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=604 * 1000, sE=0, cooling_rate=10, A=55))
|
|
300
|
+
print(get_tc(da2=6.95e27 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=815 * 1000, sE=0, cooling_rate=10, A=55))
|
|
301
|
+
print(get_tc(da2=2.70e14 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=519 * 1000, sE=0, cooling_rate=10, A=55))
|
|
302
|
+
print(get_tc(da2=1.65e15 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=505 * 1000, sE=0, cooling_rate=10, A=55))
|
|
303
|
+
print(get_tc(da2=9.09e21 * SEC2YEAR / (0.0100 ** 2), sda2=0, E=656 * 1000, sE=0, cooling_rate=10, A=55))
|
|
304
|
+
print("# Shi 2020, 黑云母, A = 55") # 大部分都对或者接近,不完全一致的原因大概是他们采用蒙特卡洛方法计算且正负误差不一样
|
|
305
|
+
print(get_tc(da2=10 ** 18 * SEC2YEAR, sda2=0, E=87 * 1000 * CAL2JOULE, sE=0, cooling_rate=10, A=6)) # 文中的参数可能是cooling_rate=100, A=8.7,但是10和6的组合与文中报导的结果一致
|
|
306
|
+
print(get_tc(da2=10 ** 16.6 * SEC2YEAR, sda2=0, E=81.5 * 1000 * CAL2JOULE, sE=0, cooling_rate=10, A=6))
|