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/smp/sample.py CHANGED
@@ -743,7 +743,7 @@ DEFAULT_PLOT_STYLES = {
743
743
  },
744
744
  }
745
745
 
746
- VERSION = '20250102'
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': 'numeric'} for i in range(colcount)]
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.numeric_indexes = numeric_indexes
962
- self.text_indexes = text_indexes
963
- if text_indexes is not None and numeric_indexes is not None:
964
- self.set_coltypes()
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({'type': 'numeric'})
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.colcount = len(comp.header)
188
- comp.coltypes = [{'type': 'numeric'}] * (comp.colcount)
189
- textindexs = getattr(comp, 'textindexs', [0]) if hasattr(comp, 'textindexs') else [0]
190
- for i in textindexs:
191
- comp.coltypes[i] = {'type': 'text'}
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.SelectedSequence1 = [
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:
@@ -9,4 +9,4 @@
9
9
  #
10
10
  #
11
11
  """
12
- from . import main
12
+ from . import atomic_level_random_walk as arw, basic
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: UTF-8 -*-
3
+ """
4
+ # ==========================================
5
+ # Copyright 2025 Yang
6
+ # ararpy - arrhenius
7
+ # ==========================================
8
+ #
9
+ #
10
+ #
11
+ """
12
+
13
+ from . import basic
14
+
15
+
@@ -3,7 +3,7 @@
3
3
  """
4
4
  # ==========================================
5
5
  # Copyright 2024 Yang
6
- # ArgonDiffusionRandomWalk - main
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))