pypsbuilder 2.5.1__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.
- AUTHORS.md +9 -0
- CHANGELOG.md +215 -0
- CONTRIBUTING.md +73 -0
- pypsbuilder/__init__.py +29 -0
- pypsbuilder/images/__init__.py +0 -0
- pypsbuilder/images/ptbuilder.png +0 -0
- pypsbuilder/images/pxbuilder.png +0 -0
- pypsbuilder/images/pypsbuilder.png +0 -0
- pypsbuilder/images/txbuilder.png +0 -0
- pypsbuilder/psbuilders.py +5289 -0
- pypsbuilder/psclasses.py +1093 -0
- pypsbuilder/psexplorer.py +3494 -0
- pypsbuilder/tcapi.py +1681 -0
- pypsbuilder/tcinit.py +992 -0
- pypsbuilder/tests/__init__.py +0 -0
- pypsbuilder/tests/test_pclasses.py +279 -0
- pypsbuilder/ui/addinv.ui +113 -0
- pypsbuilder/ui/adduni.ui +119 -0
- pypsbuilder/ui/compile_ui.sh +9 -0
- pypsbuilder/ui/ptbuilder.ui +1265 -0
- pypsbuilder/ui/pxbuilder.ui +1245 -0
- pypsbuilder/ui/txbuilder.ui +1251 -0
- pypsbuilder/ui/uniguess.ui +85 -0
- pypsbuilder/ui_addinv.py +67 -0
- pypsbuilder/ui_adduni.py +67 -0
- pypsbuilder/ui_ptbuilder.py +934 -0
- pypsbuilder/ui_pxbuilder.py +920 -0
- pypsbuilder/ui_txbuilder.py +922 -0
- pypsbuilder/ui_uniguess.py +52 -0
- pypsbuilder-2.5.1.dist-info/AUTHORS.md +9 -0
- pypsbuilder-2.5.1.dist-info/LICENSE +22 -0
- pypsbuilder-2.5.1.dist-info/METADATA +95 -0
- pypsbuilder-2.5.1.dist-info/RECORD +35 -0
- pypsbuilder-2.5.1.dist-info/WHEEL +4 -0
- pypsbuilder-2.5.1.dist-info/entry_points.txt +10 -0
|
@@ -0,0 +1,3494 @@
|
|
|
1
|
+
"""Module to postprocess pseudocestions constructed with builders.
|
|
2
|
+
|
|
3
|
+
This module contains tools to postprocess and explore pseudosections
|
|
4
|
+
calculated by available builders.
|
|
5
|
+
|
|
6
|
+
It provides a tools to visualize pseudosections constructed with
|
|
7
|
+
builders, calculate compositional variations within multivariant fields, and
|
|
8
|
+
plot compositional isopleths.
|
|
9
|
+
|
|
10
|
+
It provides four command line scipts `psgrid`, `psdrawpd`, `psshow` and `psiso`,
|
|
11
|
+
or could be used interactively, e.g. within jupyter notebooks.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
|
|
15
|
+
>>> from pypsbuilder import PTPS
|
|
16
|
+
>>> pt = PTPS('/path/to/myproject.ptb')
|
|
17
|
+
>>> pt.show()
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# author: Ondrej Lexa 2020
|
|
22
|
+
# website: https://github.com/ondrolexa/pypsbuilder
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
# import os
|
|
28
|
+
try:
|
|
29
|
+
import cPickle as pickle
|
|
30
|
+
except ImportError:
|
|
31
|
+
import pickle
|
|
32
|
+
import gzip
|
|
33
|
+
import ast
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from collections import OrderedDict
|
|
37
|
+
import warnings
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
import matplotlib.pyplot as plt
|
|
41
|
+
from matplotlib.colors import ListedColormap, BoundaryNorm, Normalize
|
|
42
|
+
from matplotlib.collections import LineCollection
|
|
43
|
+
from matplotlib.colorbar import ColorbarBase
|
|
44
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
45
|
+
from matplotlib.table import table
|
|
46
|
+
from matplotlib import ticker
|
|
47
|
+
import matplotlib.colors as mcolors
|
|
48
|
+
import matplotlib.patches as mpatches
|
|
49
|
+
|
|
50
|
+
from shapely.geometry import MultiPoint, Point, LineString
|
|
51
|
+
from shapely.ops import linemerge
|
|
52
|
+
from scipy.interpolate import Rbf, interp1d, SmoothBivariateSpline
|
|
53
|
+
from scipy.linalg import LinAlgWarning, lstsq
|
|
54
|
+
from scipy.interpolate import griddata # interp2d
|
|
55
|
+
from tqdm import tqdm, trange
|
|
56
|
+
|
|
57
|
+
from .tcapi import get_tcapi
|
|
58
|
+
from .psclasses import PTsection, TXsection, PXsection # InvPoint, UniLine
|
|
59
|
+
from .psclasses import polymorphs, PolygonPatch
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PS:
|
|
63
|
+
"""Base class for PTPS, TXPS and PXPS classes"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *args, **kwargs):
|
|
66
|
+
"""Create PTPS class instance from builder project file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
projfile (str, Path): psbuilder project file or files
|
|
70
|
+
tolerance (float): if not None, simplification tolerance. Default None
|
|
71
|
+
origwd (bool): If True TCAPI uses original stored working directory
|
|
72
|
+
Default False.
|
|
73
|
+
"""
|
|
74
|
+
projfiles = [
|
|
75
|
+
Path(projfile).resolve() for projfile in args if Path(projfile).exists()
|
|
76
|
+
]
|
|
77
|
+
assert len(projfiles) > 0, "You have to provide existing filename."
|
|
78
|
+
assert hasattr(
|
|
79
|
+
self, "section_class"
|
|
80
|
+
), "You can not instantiate base class directly. Use PTPS, TXPS or PXPS."
|
|
81
|
+
# parse kwargs
|
|
82
|
+
tolerance = kwargs.get("tolerance", None)
|
|
83
|
+
origwd = kwargs.get("origwd", False)
|
|
84
|
+
# individual based (keys are 0, 1...)
|
|
85
|
+
self.projfiles = {}
|
|
86
|
+
self.sections = {}
|
|
87
|
+
self.grids = {}
|
|
88
|
+
self._shapes = {}
|
|
89
|
+
self.unilists = {}
|
|
90
|
+
self._variance = {}
|
|
91
|
+
self.abbr = {}
|
|
92
|
+
self.show_errors = kwargs.get("show_errors", False)
|
|
93
|
+
# common
|
|
94
|
+
self.tolerance = tolerance
|
|
95
|
+
self.tc = None
|
|
96
|
+
bulk = None
|
|
97
|
+
# read
|
|
98
|
+
for ix, projfile in enumerate(projfiles):
|
|
99
|
+
self.projfiles[ix] = projfile
|
|
100
|
+
with gzip.open(str(projfile), "rb") as stream:
|
|
101
|
+
data = pickle.load(stream)
|
|
102
|
+
# check section type
|
|
103
|
+
assert (
|
|
104
|
+
type(data["section"]) == self.section_class
|
|
105
|
+
), "The provided project file is not {}.".format(
|
|
106
|
+
self.section_class.__name__
|
|
107
|
+
)
|
|
108
|
+
self.sections[ix] = data["section"]
|
|
109
|
+
# check if workdir exists
|
|
110
|
+
if "workdir" not in data:
|
|
111
|
+
data["workdir"] = str(projfile.parent)
|
|
112
|
+
# check workdit compatibility
|
|
113
|
+
if self.tc is None:
|
|
114
|
+
if origwd:
|
|
115
|
+
tc, ok = get_tcapi(Path(data["workdir"]))
|
|
116
|
+
assert (
|
|
117
|
+
ok
|
|
118
|
+
), "Error during initialization of THERMOCALC in {}\n{}".format(
|
|
119
|
+
data["workdir"], tc
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
tc, ok = get_tcapi(projfile.parent)
|
|
123
|
+
assert (
|
|
124
|
+
ok
|
|
125
|
+
), "Error during initialization of THERMOCALC in {}\n{}".format(
|
|
126
|
+
projfile.parent, tc
|
|
127
|
+
)
|
|
128
|
+
self.tc = tc
|
|
129
|
+
else:
|
|
130
|
+
if origwd:
|
|
131
|
+
assert data["workdir"] == str(
|
|
132
|
+
tc.workdir
|
|
133
|
+
), "Workdirs of merged profiles must be same."
|
|
134
|
+
else:
|
|
135
|
+
assert (
|
|
136
|
+
projfile.parent == tc.workdir
|
|
137
|
+
), "Workdirs of merged profiles must be same."
|
|
138
|
+
|
|
139
|
+
self._shapes[ix], self.unilists[ix], log = self.sections[ix].create_shapes(
|
|
140
|
+
tolerance=self.tolerance
|
|
141
|
+
)
|
|
142
|
+
if log:
|
|
143
|
+
print("\n".join(log))
|
|
144
|
+
# process variances
|
|
145
|
+
if "variance" in data:
|
|
146
|
+
self._variance[ix] = data["variance"]
|
|
147
|
+
else:
|
|
148
|
+
# # calculate variance
|
|
149
|
+
# variance = {}
|
|
150
|
+
# calcs = ['calcP {} {}'.format(*self.tc.prange),
|
|
151
|
+
# 'calcT {} {}'.format(*self.tc.trange),
|
|
152
|
+
# 'with someof {}'.format(' '.join(self.tc.phases - self.tc.excess)),
|
|
153
|
+
# 'acceptvar no']
|
|
154
|
+
# old_calcs = self.tc.update_scriptfile(get_old_calcs=True, calcs=calcs)
|
|
155
|
+
# for key in self._shapes[ix]:
|
|
156
|
+
# ans = '{}\nkill\n\n'.format(' '.join(key))
|
|
157
|
+
# tcout = self.tc.runtc(ans)
|
|
158
|
+
# try:
|
|
159
|
+
# for ln in tcout.splitlines():
|
|
160
|
+
# if 'variance of required equilibrium' in ln:
|
|
161
|
+
# break
|
|
162
|
+
# variance[key] = int(ln[ln.index('(') + 1:ln.index('?')])
|
|
163
|
+
# except Exception:
|
|
164
|
+
# variance[key] = 0
|
|
165
|
+
# print('Variance calculation failed for {} field.'.format(key))
|
|
166
|
+
# if self.show_errors:
|
|
167
|
+
# print(tcout)
|
|
168
|
+
# self._variance[ix] = variance
|
|
169
|
+
# self.tc.update_scriptfile(calcs=old_calcs)
|
|
170
|
+
variance = {}
|
|
171
|
+
for key in self._shapes[ix]:
|
|
172
|
+
variance[key] = self.tc.calc_variance(key)
|
|
173
|
+
self._variance[ix] = variance
|
|
174
|
+
# bulk
|
|
175
|
+
if bulk is None:
|
|
176
|
+
if "bulk" in data:
|
|
177
|
+
bulk = data["bulk"]
|
|
178
|
+
else:
|
|
179
|
+
bulk = self.tc.bulk
|
|
180
|
+
else:
|
|
181
|
+
if "bulk" in data:
|
|
182
|
+
assert bulk == data["bulk"], "Bulks in merged projects must be same"
|
|
183
|
+
# already gridded?
|
|
184
|
+
if "grid" in data:
|
|
185
|
+
self.grids[ix] = data["grid"]
|
|
186
|
+
# union _shapes
|
|
187
|
+
self.shapes = {}
|
|
188
|
+
for shapes in self._shapes.values():
|
|
189
|
+
for key, shape in shapes.items():
|
|
190
|
+
if key in self.shapes:
|
|
191
|
+
self.shapes[key] = self.shapes[key].union(shape)
|
|
192
|
+
else:
|
|
193
|
+
self.shapes[key] = shape
|
|
194
|
+
# update variable lookup table
|
|
195
|
+
self.collect_all_data_keys()
|
|
196
|
+
|
|
197
|
+
def __repr__(self):
|
|
198
|
+
reprs = ["{} explorer".format(type(self).__name__)]
|
|
199
|
+
reprs.append(repr(self.tc))
|
|
200
|
+
for ps in self.sections.values():
|
|
201
|
+
reprs.append(repr(ps))
|
|
202
|
+
reprs.append("Areas: {}".format(len(self.shapes)))
|
|
203
|
+
if self.gridded:
|
|
204
|
+
for grid in self.grids.values():
|
|
205
|
+
reprs.append(repr(grid))
|
|
206
|
+
return "\n".join(reprs)
|
|
207
|
+
|
|
208
|
+
def __iter__(self):
|
|
209
|
+
return iter(self.shapes)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def xrange(self):
|
|
213
|
+
return min(ps.xrange[0] for ps in self.sections.values()), max(
|
|
214
|
+
ps.xrange[1] for ps in self.sections.values()
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def yrange(self):
|
|
219
|
+
return min(ps.yrange[0] for ps in self.sections.values()), max(
|
|
220
|
+
ps.yrange[1] for ps in self.sections.values()
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def x_var(self):
|
|
225
|
+
return self.sections[0].x_var
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def y_var(self):
|
|
229
|
+
return self.sections[0].y_var
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def gridxstep(self):
|
|
233
|
+
if self.gridded:
|
|
234
|
+
v = [grid.xstep for grid in self.grids.values()]
|
|
235
|
+
else:
|
|
236
|
+
v = [(self.xrange[1] - self.xrange[0]) / 50]
|
|
237
|
+
return sum(v) / len(v)
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def gridystep(self):
|
|
241
|
+
if self.gridded:
|
|
242
|
+
v = [grid.ystep for grid in self.grids.values()]
|
|
243
|
+
else:
|
|
244
|
+
v = [(self.yrange[1] - self.yrange[0]) / 50]
|
|
245
|
+
return sum(v) / len(v)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def ratio(self):
|
|
249
|
+
return (self.xrange[1] - self.xrange[0]) / (self.yrange[1] - self.yrange[0])
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def name(self):
|
|
253
|
+
"""Get project directory name."""
|
|
254
|
+
return self.projfiles[0].parent.stem
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def gridded(self):
|
|
258
|
+
"""True when compositional grid(s) is calculated, otherwise False"""
|
|
259
|
+
return all([i in self.grids for i in range(len(self.sections))])
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def phases(self):
|
|
263
|
+
"""Returns set of all phases present in pseudosection"""
|
|
264
|
+
return {phase for key in self.keys for phase in key}
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def endmembers(self):
|
|
268
|
+
"""Returns dictionary with phases and their end-members names"""
|
|
269
|
+
em = {}
|
|
270
|
+
for comp in (
|
|
271
|
+
set(self.all_data_keys.keys())
|
|
272
|
+
.difference(self.phases)
|
|
273
|
+
.difference(set(["bulk", "sys"]))
|
|
274
|
+
):
|
|
275
|
+
k, v = comp.split(")")[0].split("(")
|
|
276
|
+
if k in em:
|
|
277
|
+
em[k].append(v)
|
|
278
|
+
else:
|
|
279
|
+
em[k] = [v]
|
|
280
|
+
return em
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def variance(self):
|
|
284
|
+
"""Returns dictionary of variances"""
|
|
285
|
+
v = self._variance[0].copy()
|
|
286
|
+
for ix in range(1, len(self._variance)):
|
|
287
|
+
v.update(self._variance[ix])
|
|
288
|
+
return v
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def keys(self):
|
|
292
|
+
"""Returns set of all existing multivariant fields. Fields are
|
|
293
|
+
identified by frozenset of present phases called key."""
|
|
294
|
+
keylist = []
|
|
295
|
+
for shapes in self._shapes.values():
|
|
296
|
+
keylist.extend(list(shapes.keys()))
|
|
297
|
+
return set(keylist)
|
|
298
|
+
|
|
299
|
+
def check_phase_expr(self, phase, expr):
|
|
300
|
+
if phase in self.all_data_keys:
|
|
301
|
+
if expr is None:
|
|
302
|
+
msg = "Missing expression argument. Available variables for phase {} are:\n{}"
|
|
303
|
+
print(msg.format(phase, " ".join(self.all_data_keys[phase])))
|
|
304
|
+
if phase in self.endmembers:
|
|
305
|
+
print(
|
|
306
|
+
"Available end-members for {}: {}".format(
|
|
307
|
+
phase, " ".join(self.endmembers[phase])
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
return False
|
|
311
|
+
else:
|
|
312
|
+
return True
|
|
313
|
+
else:
|
|
314
|
+
print("Unknown phase {}".format(phase))
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def get_section_id(self, x, y):
|
|
318
|
+
"""Return index of pseudosection and grid containing point"""
|
|
319
|
+
ix_ok = None
|
|
320
|
+
for ix, ps in enumerate(self.sections.values()):
|
|
321
|
+
_, area = ps.range_shapes
|
|
322
|
+
if area.contains(Point(x, y)):
|
|
323
|
+
ix_ok = ix
|
|
324
|
+
break
|
|
325
|
+
return ix_ok
|
|
326
|
+
|
|
327
|
+
def invs_from_unilist(self, ix, unilist):
|
|
328
|
+
"""Return set of IDs of invariant points associated with unilines.
|
|
329
|
+
lines.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
unilist (iterable): list of (section_id, uni_id) pairs
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
set: set of associated invariant points
|
|
336
|
+
"""
|
|
337
|
+
return (
|
|
338
|
+
{self.sections[ix].unilines[ed].begin for ed in unilist}
|
|
339
|
+
.union({self.sections[ix].unilines[ed].end for ed in unilist})
|
|
340
|
+
.difference({0})
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def save(self):
|
|
344
|
+
"""Save gridded copositions and constructed divariant fields into
|
|
345
|
+
psbuilder project file.
|
|
346
|
+
|
|
347
|
+
Note that once project is edited with psbuilder, calculated compositions
|
|
348
|
+
are removed and need to be recalculated using `PTPS.calculate_composition`
|
|
349
|
+
method.
|
|
350
|
+
"""
|
|
351
|
+
if self.gridded:
|
|
352
|
+
for ix, projfile in self.projfiles.items():
|
|
353
|
+
# put to dict
|
|
354
|
+
with gzip.open(str(projfile), "rb") as stream:
|
|
355
|
+
data = pickle.load(stream)
|
|
356
|
+
data["variance"] = self._variance[ix]
|
|
357
|
+
data["grid"] = self.grids[ix]
|
|
358
|
+
# do save
|
|
359
|
+
with gzip.open(str(projfile), "wb") as stream:
|
|
360
|
+
pickle.dump(data, stream)
|
|
361
|
+
else:
|
|
362
|
+
print("Not yet gridded...")
|
|
363
|
+
|
|
364
|
+
def create_masks(self):
|
|
365
|
+
"""Update grid masks from existing divariant fields"""
|
|
366
|
+
if self.gridded:
|
|
367
|
+
for ix, grid in self.grids.items():
|
|
368
|
+
# Create data masks
|
|
369
|
+
points = MultiPoint(list(zip(grid.xg.flatten(), grid.yg.flatten())))
|
|
370
|
+
shapes = self._shapes[ix]
|
|
371
|
+
for key in shapes:
|
|
372
|
+
grid.masks[key] = np.array(
|
|
373
|
+
list(map(shapes[key].contains, points.geoms))
|
|
374
|
+
).reshape(grid.xg.shape)
|
|
375
|
+
else:
|
|
376
|
+
print("Not yet gridded...")
|
|
377
|
+
|
|
378
|
+
def common_grid_and_masks(self, **kwargs):
|
|
379
|
+
"""Initialize common grid and mask for all partial grids"""
|
|
380
|
+
nx = kwargs.get(
|
|
381
|
+
"nx", np.round(np.diff(self.xrange)[0] / self.gridxstep).astype(int)
|
|
382
|
+
)
|
|
383
|
+
ny = kwargs.get(
|
|
384
|
+
"ny", np.round(np.diff(self.yrange)[0] / self.gridystep).astype(int)
|
|
385
|
+
)
|
|
386
|
+
self.xstep = np.diff(self.xrange)[0] / nx
|
|
387
|
+
self.ystep = np.diff(self.yrange)[0] / ny
|
|
388
|
+
self.xspace = np.linspace(
|
|
389
|
+
self.xrange[0] + self.xstep / 2, self.xrange[1] - self.xstep / 2, nx
|
|
390
|
+
)
|
|
391
|
+
self.yspace = np.linspace(
|
|
392
|
+
self.yrange[0] + self.ystep / 2, self.yrange[1] - self.ystep / 2, ny
|
|
393
|
+
)
|
|
394
|
+
self.xg, self.yg = np.meshgrid(self.xspace, self.yspace)
|
|
395
|
+
# Create data masks
|
|
396
|
+
self.masks = {}
|
|
397
|
+
points = MultiPoint(list(zip(self.xg.flatten(), self.yg.flatten())))
|
|
398
|
+
for key in self.shapes:
|
|
399
|
+
self.masks[key] = np.array(
|
|
400
|
+
list(map(self.shapes[key].contains, points.geoms))
|
|
401
|
+
).reshape(self.xg.shape)
|
|
402
|
+
|
|
403
|
+
def collect_all_data_keys(self):
|
|
404
|
+
"""Collect all phases and variables calculated on grid.
|
|
405
|
+
|
|
406
|
+
Result is stored in `all_data_keys` property as dictionary of
|
|
407
|
+
dictionaries.
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
To get list of all variables calculated for phase 'g' or end-member
|
|
411
|
+
'g(alm)' use::
|
|
412
|
+
|
|
413
|
+
>>> pt.all_data_keys['g']
|
|
414
|
+
['mode', 'x', 'z', 'm', 'f', 'xMgX', 'xFeX', 'xMnX', 'xCaX', 'xAlY',
|
|
415
|
+
'xFe3Y', 'H2O', 'SiO2', 'Al2O3', 'CaO', 'MgO', 'FeO', 'K2O', 'Na2O',
|
|
416
|
+
'TiO2', 'MnO', 'O', 'factor', 'G', 'H', 'S', 'V', 'rho']
|
|
417
|
+
>>> pt.all_data_keys['g(alm)']
|
|
418
|
+
['ideal', 'gamma', 'activity', 'prop', 'mu', 'RTlna']
|
|
419
|
+
"""
|
|
420
|
+
data = dict()
|
|
421
|
+
valid_phases = self.phases
|
|
422
|
+
for ix, ps in self.sections.items():
|
|
423
|
+
for inv in ps.invpoints.values():
|
|
424
|
+
if not inv.manual:
|
|
425
|
+
for comp in inv.datakeys():
|
|
426
|
+
k = comp.split(")")[0].split("(")
|
|
427
|
+
if k[0] in valid_phases:
|
|
428
|
+
data[comp] = inv.datakeys(comp)
|
|
429
|
+
|
|
430
|
+
if not valid_phases.issubset(data.keys()):
|
|
431
|
+
# Search in unilines
|
|
432
|
+
for ix, ps in self.sections.items():
|
|
433
|
+
for uni in ps.unilines.values():
|
|
434
|
+
if not uni.manual:
|
|
435
|
+
for comp in uni.datakeys():
|
|
436
|
+
k = comp.split(")")[0].split("(")
|
|
437
|
+
if k[0] in valid_phases:
|
|
438
|
+
data[comp] = uni.datakeys(comp)
|
|
439
|
+
|
|
440
|
+
if not valid_phases.issubset(data.keys()):
|
|
441
|
+
# Search in griddata NEED CHECK AND FIX
|
|
442
|
+
for ix, grid in self.grids.items():
|
|
443
|
+
shapes = self._shapes[ix]
|
|
444
|
+
for key in shapes:
|
|
445
|
+
results = grid.gridcalcs[grid.masks[key] & (grid.status == 1)]
|
|
446
|
+
if len(results) > 0:
|
|
447
|
+
for comp in results[0].phases:
|
|
448
|
+
k = comp.split(")")[0].split("(")
|
|
449
|
+
if k[0] in valid_phases:
|
|
450
|
+
data[comp] = list(results[0][comp].keys())
|
|
451
|
+
|
|
452
|
+
if not valid_phases.issubset(data.keys()):
|
|
453
|
+
print("Some phases not calculated.")
|
|
454
|
+
# if self.gridded:
|
|
455
|
+
# for ix, grid in self.grids.items():
|
|
456
|
+
# shapes = self._shapes[ix]
|
|
457
|
+
# for key in shapes:
|
|
458
|
+
# res = grid.gridcalcs[grid.masks[key] & (grid.status == 1)]
|
|
459
|
+
# if len(res) > 0:
|
|
460
|
+
# for k in res[0]['data'].keys():
|
|
461
|
+
# data[k] = list(res[0]['data'][k].keys())
|
|
462
|
+
self.all_data_keys = data
|
|
463
|
+
|
|
464
|
+
def collect_inv_data(self, key, phase, expr):
|
|
465
|
+
"""Retrieve value of variables based expression for given phase for
|
|
466
|
+
all invariant points surrounding divariant field identified by key.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
key (frozenset): Key identifying divariant field
|
|
470
|
+
phase (str): Phase or end-member named
|
|
471
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
472
|
+
existing for given phase. Check `all_data_keys` property for
|
|
473
|
+
possible variables.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
Dictionary with 'pts' key storing list of coordinate tuples(t,p) and
|
|
477
|
+
'data' key storing list of thermocalc results.
|
|
478
|
+
"""
|
|
479
|
+
dt = dict(pts=[], data=[])
|
|
480
|
+
for ix, ps in self.sections.items():
|
|
481
|
+
if key in self.unilists[ix]:
|
|
482
|
+
for id_inv in self.invs_from_unilist(ix, self.unilists[ix][key]):
|
|
483
|
+
inv = ps.invpoints[id_inv]
|
|
484
|
+
if not inv.manual:
|
|
485
|
+
if phase in inv.results.phases:
|
|
486
|
+
if self.shapes[key].intersects(Point(inv._x, inv._y)):
|
|
487
|
+
dt["pts"].append((inv._x, inv._y))
|
|
488
|
+
dt["data"].append(
|
|
489
|
+
eval_expr(expr, inv.results[0][phase])
|
|
490
|
+
)
|
|
491
|
+
return dt
|
|
492
|
+
|
|
493
|
+
def collect_uni_data(self, key, phase, expr):
|
|
494
|
+
"""Retrieve values of expression for given phase for
|
|
495
|
+
all univariant lines surrounding divariant field identified by key.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
key (frozenset): Key identifying divariant field
|
|
499
|
+
phase (str): Phase or end-member named
|
|
500
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
501
|
+
existing for given phase. Check `all_data_keys` property for
|
|
502
|
+
possible variables.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Dictionary with 'pts' key storing list of coordinate tuples(t,p) and
|
|
506
|
+
'data' key storing list of thermocalc results.
|
|
507
|
+
"""
|
|
508
|
+
dt = dict(pts=[], data=[])
|
|
509
|
+
for ix, ps in self.sections.items():
|
|
510
|
+
if key in self.unilists[ix]:
|
|
511
|
+
for id_uni in self.unilists[ix][key]:
|
|
512
|
+
uni = ps.unilines[id_uni]
|
|
513
|
+
if not uni.manual:
|
|
514
|
+
if phase in uni.results.phases:
|
|
515
|
+
edt = zip(
|
|
516
|
+
uni._x[uni.used],
|
|
517
|
+
uni._y[uni.used],
|
|
518
|
+
uni.results[uni.used],
|
|
519
|
+
)
|
|
520
|
+
for x, y, res in edt:
|
|
521
|
+
if self.shapes[key].intersects(Point(x, y)):
|
|
522
|
+
dt["pts"].append((x, y))
|
|
523
|
+
dt["data"].append(eval_expr(expr, res[phase]))
|
|
524
|
+
return dt
|
|
525
|
+
|
|
526
|
+
def collect_grid_data(self, key, phase, expr):
|
|
527
|
+
"""Retrieve values of expression for given phase for
|
|
528
|
+
all GridData points within divariant field identified by key.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
key (frozenset): Key identifying divariant field
|
|
532
|
+
phase (str): Phase or end-member named
|
|
533
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
534
|
+
existing for given phase. Check `all_data_keys` property for
|
|
535
|
+
possible variables.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Dictionary with 'pts' key storing list of coordinate tuples(t,p) and
|
|
539
|
+
'data' key storing list of thermocalc results.
|
|
540
|
+
"""
|
|
541
|
+
dt = dict(pts=[], data=[])
|
|
542
|
+
if self.gridded:
|
|
543
|
+
for ix, grid in self.grids.items():
|
|
544
|
+
if key in grid.masks:
|
|
545
|
+
ok = grid.masks[key] & (grid.status == 1)
|
|
546
|
+
results = grid.gridcalcs[ok]
|
|
547
|
+
if len(results) > 0:
|
|
548
|
+
if phase in results[0].phases:
|
|
549
|
+
gdt = zip(
|
|
550
|
+
grid.xg[ok],
|
|
551
|
+
grid.yg[ok],
|
|
552
|
+
results,
|
|
553
|
+
grid.status[ok],
|
|
554
|
+
)
|
|
555
|
+
for x, y, res, status in gdt:
|
|
556
|
+
if status == 1:
|
|
557
|
+
dt["pts"].append((x, y))
|
|
558
|
+
dt["data"].append(eval_expr(expr, res[phase]))
|
|
559
|
+
# else:
|
|
560
|
+
# print('Not yet gridded...')
|
|
561
|
+
return dt
|
|
562
|
+
|
|
563
|
+
def get_nearest_grid_data(self, x, y):
|
|
564
|
+
"""Retrieve nearest results from GridData to point.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
x (float): x-coordinate of point
|
|
568
|
+
y (float): y-coordiante of point
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
THERMOCALC result set a assemblage key
|
|
572
|
+
"""
|
|
573
|
+
dt = dict(pts=[], data=[])
|
|
574
|
+
if self.gridded:
|
|
575
|
+
ix = self.get_section_id(x, y)
|
|
576
|
+
if ix is not None:
|
|
577
|
+
r, c = self.grids[ix].get_indexes(x, y)
|
|
578
|
+
dt = self.grids[ix].gridcalcs[r, c]
|
|
579
|
+
return dt
|
|
580
|
+
else:
|
|
581
|
+
print("Not yet gridded...")
|
|
582
|
+
|
|
583
|
+
def collect_data(self, key, phase, expr, which=7):
|
|
584
|
+
"""Convinient function to retrieve values of expression
|
|
585
|
+
for given phase for user-defined combination of results of divariant
|
|
586
|
+
field identified by key.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
key (frozenset): Key identifying divariant field
|
|
590
|
+
|
|
591
|
+
phase (str): Phase or end-member named
|
|
592
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
593
|
+
existing for given phase. Check `all_data_keys` property for
|
|
594
|
+
possible variables.
|
|
595
|
+
which (int): Bitopt defining from where data are collected. 0 bit -
|
|
596
|
+
invariant points, 1 bit - uniariant lines and 2 bit - GridData
|
|
597
|
+
points
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Dictionary with 'pts' key storing list of coordinate tuples(t,p) and
|
|
601
|
+
'data' key storing list of thermocalc results.
|
|
602
|
+
"""
|
|
603
|
+
dt = dict(pts=[], data=[])
|
|
604
|
+
# check if phase or end-member is in assemblage
|
|
605
|
+
if phase in self.all_data_keys:
|
|
606
|
+
if which & (1 << 0):
|
|
607
|
+
d = self.collect_inv_data(key, phase, expr)
|
|
608
|
+
dt["pts"].extend(d["pts"])
|
|
609
|
+
dt["data"].extend(d["data"])
|
|
610
|
+
if which & (1 << 1):
|
|
611
|
+
d = self.collect_uni_data(key, phase, expr)
|
|
612
|
+
dt["pts"].extend(d["pts"])
|
|
613
|
+
dt["data"].extend(d["data"])
|
|
614
|
+
if which & (1 << 2):
|
|
615
|
+
d = self.collect_grid_data(key, phase, expr)
|
|
616
|
+
dt["pts"].extend(d["pts"])
|
|
617
|
+
dt["data"].extend(d["data"])
|
|
618
|
+
return dt
|
|
619
|
+
|
|
620
|
+
def merge_data(self, phase, expr, which=7):
|
|
621
|
+
"""Returns merged data obtained by `collect_data` method for all
|
|
622
|
+
divariant fields.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
phase (str): Phase or end-member named
|
|
626
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
627
|
+
existing for given phase. Check `all_data_keys` property for
|
|
628
|
+
possible variables.
|
|
629
|
+
which (int): Bitopt defining from where data are collected. 0 bit -
|
|
630
|
+
invariant points, 1 bit - uniariant lines and 2 bit - GridData
|
|
631
|
+
points
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Dictionary with 'pts' key storing list of coordinate tuples(t,p) and
|
|
635
|
+
'data' key storing list of thermocalc results.
|
|
636
|
+
"""
|
|
637
|
+
mn, mx = sys.float_info.max, -sys.float_info.max
|
|
638
|
+
recs = OrderedDict()
|
|
639
|
+
for key in self:
|
|
640
|
+
if phase.split("(")[0] in key:
|
|
641
|
+
d = self.collect_data(key, phase, expr, which=which)
|
|
642
|
+
z = d["data"]
|
|
643
|
+
if z:
|
|
644
|
+
recs[key] = d
|
|
645
|
+
mn = min(mn, min(z))
|
|
646
|
+
mx = max(mx, max(z))
|
|
647
|
+
# res = self.grid.gridcalcs[self.grid.masks[key]]
|
|
648
|
+
# if len(res) > 0:
|
|
649
|
+
# if phase in res[0]['data']:
|
|
650
|
+
# d = self.collect_data(key, phase, expr, which=which)
|
|
651
|
+
# z = d['data']
|
|
652
|
+
# if z:
|
|
653
|
+
# recs[key] = d
|
|
654
|
+
# mn = min(mn, min(z))
|
|
655
|
+
# mx = max(mx, max(z))
|
|
656
|
+
return recs, mn, mx
|
|
657
|
+
|
|
658
|
+
def show(self, **kwargs):
|
|
659
|
+
"""Method to draw PT pseudosection.
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
663
|
+
out (str or list): Highligt zero-mode lines for given phases.
|
|
664
|
+
high (frozenset or list): Highlight divariant fields identified
|
|
665
|
+
by key(s).
|
|
666
|
+
cmap (str): matplotlib colormap used to divariant fields coloring.
|
|
667
|
+
Colors are based on variance. Default 'Purples'.
|
|
668
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
669
|
+
labelfs (float): Size of label font. Default 6
|
|
670
|
+
bulk (bool): Whether to show bulk composition on top of diagram.
|
|
671
|
+
Default False.
|
|
672
|
+
alpha (float): alpha value for colors. Default 0.6
|
|
673
|
+
connect (bool): Whether mouse click echo stable assemblage to STDOUT.
|
|
674
|
+
Default False.
|
|
675
|
+
show_vertices (bool): Whether to show vertices of drawn areas.
|
|
676
|
+
Default False.
|
|
677
|
+
fig (Figure): If not None, axes are added to fig and returned.
|
|
678
|
+
Default None
|
|
679
|
+
fig_kw: dict passed to subplots method.
|
|
680
|
+
ax (Axes): Axes to be used. Default None
|
|
681
|
+
filename: If not None, figure is saved to file
|
|
682
|
+
save_kw: dict passed to savefig method.
|
|
683
|
+
show (bool): When False, Axes are returned, otherwise plot is shown.
|
|
684
|
+
Default True
|
|
685
|
+
"""
|
|
686
|
+
out = kwargs.get("out", None)
|
|
687
|
+
cmap = kwargs.get("cmap", "Purples")
|
|
688
|
+
alpha = kwargs.get("alpha", 0.6)
|
|
689
|
+
label = kwargs.get("label", False)
|
|
690
|
+
skiplabels = kwargs.get("skiplabels", 0)
|
|
691
|
+
labelfs = kwargs.get("labelfs", 6)
|
|
692
|
+
bulk = kwargs.get("bulk", False)
|
|
693
|
+
high = kwargs.get("high", [])
|
|
694
|
+
connect = kwargs.get("connect", False)
|
|
695
|
+
show_vertices = kwargs.get("show_vertices", False)
|
|
696
|
+
fig = kwargs.get("fig", None)
|
|
697
|
+
fig_kw = kwargs.get("fig_kw", {})
|
|
698
|
+
ax = kwargs.get("ax", None)
|
|
699
|
+
filename = kwargs.get("filename", None)
|
|
700
|
+
save_kw = kwargs.get("save_kw", {})
|
|
701
|
+
show = kwargs.get("show", None)
|
|
702
|
+
|
|
703
|
+
if self.shapes:
|
|
704
|
+
if isinstance(out, str):
|
|
705
|
+
out = [out]
|
|
706
|
+
vari = [self.variance[k] for k in self]
|
|
707
|
+
poc = max(vari) - min(vari) + 1
|
|
708
|
+
# skip extreme values to visually differs from empty areas
|
|
709
|
+
pscolors = plt.get_cmap(cmap)(np.linspace(0, 1, poc + 2))[1:-1, :]
|
|
710
|
+
# Set alpha
|
|
711
|
+
pscolors[:, -1] = alpha
|
|
712
|
+
pscmap = ListedColormap(pscolors)
|
|
713
|
+
norm = BoundaryNorm(
|
|
714
|
+
np.arange(min(vari) - 0.5, max(vari) + 1.5), poc, clip=True
|
|
715
|
+
)
|
|
716
|
+
if fig is None:
|
|
717
|
+
if ax is None:
|
|
718
|
+
fig, ax = plt.subplots(**fig_kw)
|
|
719
|
+
else:
|
|
720
|
+
if show is None:
|
|
721
|
+
show = False
|
|
722
|
+
fig = ax.get_figure()
|
|
723
|
+
else:
|
|
724
|
+
if show is None:
|
|
725
|
+
show = False
|
|
726
|
+
ax = fig.add_subplot()
|
|
727
|
+
for k, shape in self.shapes.items():
|
|
728
|
+
patch = PolygonPatch(
|
|
729
|
+
shape, fc=pscmap(norm(self.variance[k])), ec="none"
|
|
730
|
+
)
|
|
731
|
+
ax.add_patch(patch)
|
|
732
|
+
if show_vertices:
|
|
733
|
+
x, y = zip(*patch.get_path().vertices)
|
|
734
|
+
ax.plot(x, y, "k.", ms=3)
|
|
735
|
+
ax.autoscale_view()
|
|
736
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
737
|
+
if out:
|
|
738
|
+
for o in out:
|
|
739
|
+
xy = []
|
|
740
|
+
for ix, ps in self.sections.items():
|
|
741
|
+
for uni in ps.unilines.values():
|
|
742
|
+
if o in uni.out:
|
|
743
|
+
xy.append(np.array(uni.shape().coords).T)
|
|
744
|
+
for poly in polymorphs:
|
|
745
|
+
if poly.issubset(uni.phases):
|
|
746
|
+
if o in poly:
|
|
747
|
+
if poly.difference({o}).issubset(uni.out):
|
|
748
|
+
xy.append(np.array(uni.shape().coords).T)
|
|
749
|
+
if xy:
|
|
750
|
+
ax.plot(
|
|
751
|
+
np.hstack([(*seg[0], np.nan) for seg in xy]),
|
|
752
|
+
np.hstack([(*seg[1], np.nan) for seg in xy]),
|
|
753
|
+
lw=2,
|
|
754
|
+
label=o,
|
|
755
|
+
)
|
|
756
|
+
# Shrink current axis's width
|
|
757
|
+
box = ax.get_position()
|
|
758
|
+
ax.set_position(
|
|
759
|
+
[box.x0 + box.width * 0.07, box.y0, box.width * 0.95, box.height]
|
|
760
|
+
)
|
|
761
|
+
# Put a legend below current axis
|
|
762
|
+
ax.legend(
|
|
763
|
+
loc="upper right",
|
|
764
|
+
bbox_to_anchor=(-0.08, 1),
|
|
765
|
+
title="Out",
|
|
766
|
+
borderaxespad=0,
|
|
767
|
+
frameon=False,
|
|
768
|
+
)
|
|
769
|
+
divider = make_axes_locatable(ax)
|
|
770
|
+
cax = divider.append_axes("right", size="4%", pad=0.05)
|
|
771
|
+
cbar = ColorbarBase(
|
|
772
|
+
ax=cax,
|
|
773
|
+
cmap=pscmap,
|
|
774
|
+
norm=norm,
|
|
775
|
+
orientation="vertical",
|
|
776
|
+
ticks=np.arange(min(vari), max(vari) + 1),
|
|
777
|
+
)
|
|
778
|
+
cbar.set_label("Variance")
|
|
779
|
+
ax.set_xlim(self.xrange)
|
|
780
|
+
ax.set_ylim(self.yrange)
|
|
781
|
+
ax.set_xlabel(self.x_var)
|
|
782
|
+
ax.set_ylabel(self.y_var)
|
|
783
|
+
# Show highlight. Change to list if only single key
|
|
784
|
+
if not isinstance(high, list):
|
|
785
|
+
high = [high]
|
|
786
|
+
for k in high:
|
|
787
|
+
if isinstance(k, str):
|
|
788
|
+
k = frozenset(k.split())
|
|
789
|
+
k = k.union(self.tc.excess)
|
|
790
|
+
if k in self.keys:
|
|
791
|
+
ax.add_patch(
|
|
792
|
+
PolygonPatch(self.shapes[k], fc="none", ec="red", lw=2)
|
|
793
|
+
)
|
|
794
|
+
else:
|
|
795
|
+
print("Field {} not found.".format(" ".join(k)))
|
|
796
|
+
# Show bulk
|
|
797
|
+
if bulk:
|
|
798
|
+
if label:
|
|
799
|
+
ax.set_xlabel(
|
|
800
|
+
self.name
|
|
801
|
+
+ (len(self.tc.excess) * " +{}").format(*self.tc.excess)
|
|
802
|
+
)
|
|
803
|
+
else:
|
|
804
|
+
ax.set_xlabel(self.name)
|
|
805
|
+
# bulk composition
|
|
806
|
+
if self.section_class.__name__ == "PTsection":
|
|
807
|
+
if self.tc.usedbulk is not None:
|
|
808
|
+
ox, val = self.tc.usedbulk
|
|
809
|
+
else:
|
|
810
|
+
ox, val = self.tc.bulk
|
|
811
|
+
table(ax=ax, cellText=[val], colLabels=ox, loc="top")
|
|
812
|
+
else:
|
|
813
|
+
if self.tc.usedbulk is not None:
|
|
814
|
+
ox, val1, val2 = self.tc.usedbulk
|
|
815
|
+
else:
|
|
816
|
+
ox, val1, val2 = self.tc.bulk
|
|
817
|
+
table(ax=ax, cellText=[val1, val2], colLabels=ox, loc="top")
|
|
818
|
+
else:
|
|
819
|
+
if label:
|
|
820
|
+
ax.set_title(
|
|
821
|
+
self.name
|
|
822
|
+
+ (len(self.tc.excess) * " +{}").format(*self.tc.excess)
|
|
823
|
+
)
|
|
824
|
+
else:
|
|
825
|
+
ax.set_title(self.name)
|
|
826
|
+
# coords
|
|
827
|
+
ax.format_coord = self.format_coord
|
|
828
|
+
# connect button press
|
|
829
|
+
if connect:
|
|
830
|
+
fig.canvas.mpl_connect("button_press_event", self.onclick)
|
|
831
|
+
if filename is not None:
|
|
832
|
+
plt.savefig(filename, **save_kw)
|
|
833
|
+
plt.close(fig)
|
|
834
|
+
else:
|
|
835
|
+
if show is None or show is True:
|
|
836
|
+
plt.show()
|
|
837
|
+
else:
|
|
838
|
+
return ax
|
|
839
|
+
else:
|
|
840
|
+
print(
|
|
841
|
+
"There is no single area defined in your pseudosection. Check topology."
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
def format_coord(self, x, y):
|
|
845
|
+
prec = 2
|
|
846
|
+
point = Point(x, y)
|
|
847
|
+
phases = ""
|
|
848
|
+
for key, shape in self.shapes.items():
|
|
849
|
+
if shape.contains(point):
|
|
850
|
+
phases = " ".join(sorted(list(key.difference(self.tc.excess))))
|
|
851
|
+
break
|
|
852
|
+
return "{}={:.{prec}f} {}={:.{prec}f} {}".format(
|
|
853
|
+
self.x_var, x, self.y_var, y, phases, prec=prec
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
def add_overlay(self, ax, fc="none", ec="k", label=False, skiplabels=0, fontsize=6):
|
|
857
|
+
area = (self.xrange[1] - self.xrange[0]) * (self.yrange[1] - self.yrange[0])
|
|
858
|
+
for k, shape in self.shapes.items():
|
|
859
|
+
ax.add_patch(PolygonPatch(shape, ec=ec, fc=fc, lw=0.5))
|
|
860
|
+
if label and (100 * shape.area / area > skiplabels):
|
|
861
|
+
# multiline for long labels
|
|
862
|
+
tl = sorted(list(k.difference(self.tc.excess)))
|
|
863
|
+
extra = self.tc.excess.difference(self.tc.excess.intersection(k))
|
|
864
|
+
# if excess in scriptfile is not accurate
|
|
865
|
+
if extra:
|
|
866
|
+
tl += ["-{}".format(pp) for pp in extra]
|
|
867
|
+
wp = len(tl) // 4 + int(len(tl) % 4 > 1)
|
|
868
|
+
txt = "\n".join(
|
|
869
|
+
[
|
|
870
|
+
" ".join([self.abbr.get(sa, sa) for sa in s])
|
|
871
|
+
for s in [
|
|
872
|
+
tl[i * len(tl) // wp : (i + 1) * len(tl) // wp]
|
|
873
|
+
for i in range(wp)
|
|
874
|
+
]
|
|
875
|
+
]
|
|
876
|
+
)
|
|
877
|
+
if shape.geom_type == "MultiPolygon":
|
|
878
|
+
for part in shape:
|
|
879
|
+
xy = part.representative_point().coords[0]
|
|
880
|
+
ax.annotate(
|
|
881
|
+
text=txt,
|
|
882
|
+
xy=xy,
|
|
883
|
+
weight="bold",
|
|
884
|
+
fontsize=fontsize,
|
|
885
|
+
ha="center",
|
|
886
|
+
va="center",
|
|
887
|
+
)
|
|
888
|
+
else:
|
|
889
|
+
xy = shape.representative_point().coords[0]
|
|
890
|
+
ax.annotate(
|
|
891
|
+
text=txt,
|
|
892
|
+
xy=xy,
|
|
893
|
+
weight="bold",
|
|
894
|
+
fontsize=fontsize,
|
|
895
|
+
ha="center",
|
|
896
|
+
va="center",
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
def show_data(self, key, phase, expr=None, which=7):
|
|
900
|
+
"""Convinient function to show values of expression
|
|
901
|
+
for given phase for user-defined combination of results of divariant
|
|
902
|
+
field identified by key.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
key (frozenset): Key identifying divariant field
|
|
906
|
+
phase (str): Phase or end-member named
|
|
907
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
908
|
+
existing for given phase. Check `all_data_keys` property for
|
|
909
|
+
possible variables.
|
|
910
|
+
which (int): Bitopt defining from where data are collected. 0 bit -
|
|
911
|
+
invariant points, 1 bit - uniariant lines and 2 bit - GridData
|
|
912
|
+
points
|
|
913
|
+
|
|
914
|
+
Returns:
|
|
915
|
+
Dictionary with 'pts' key storing list of coordinate tuples(t,p) and
|
|
916
|
+
'data' key storing list of thermocalc results.
|
|
917
|
+
"""
|
|
918
|
+
if self.check_phase_expr(phase, expr):
|
|
919
|
+
dt = self.collect_data(key, phase, expr, which=which)
|
|
920
|
+
if dt["pts"]:
|
|
921
|
+
x, y = np.array(dt["pts"]).T
|
|
922
|
+
fig, ax = plt.subplots()
|
|
923
|
+
pts = ax.scatter(x, y, c=dt["data"])
|
|
924
|
+
ax.set_title("{} - {}({})".format(" ".join(key), phase, expr))
|
|
925
|
+
plt.colorbar(pts)
|
|
926
|
+
plt.show()
|
|
927
|
+
else:
|
|
928
|
+
print("No data collected")
|
|
929
|
+
|
|
930
|
+
def show_grid(
|
|
931
|
+
self, phase, expr=None, interpolation=None, label=False, skiplabels=0, labelfs=6
|
|
932
|
+
):
|
|
933
|
+
"""Convinient function to show values of expression for given phase only
|
|
934
|
+
from Grid Data.
|
|
935
|
+
|
|
936
|
+
Args:
|
|
937
|
+
phase (str): Phase or end-member named
|
|
938
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
939
|
+
existing for given phase. Check `all_data_keys` property for
|
|
940
|
+
possible variables.
|
|
941
|
+
interpolation (str): matplotlib imshow interpolation method.
|
|
942
|
+
Default None.
|
|
943
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
944
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
945
|
+
labelfs (float): Size of label font. Default 6
|
|
946
|
+
"""
|
|
947
|
+
if self.gridded:
|
|
948
|
+
if self.check_phase_expr(phase, expr):
|
|
949
|
+
fig, ax = plt.subplots()
|
|
950
|
+
cgd = {}
|
|
951
|
+
mn, mx = sys.float_info.max, -sys.float_info.max
|
|
952
|
+
for ix, grid in self.grids.items():
|
|
953
|
+
gd = np.empty(grid.xg.shape, dtype=float)
|
|
954
|
+
gd[:] = np.nan
|
|
955
|
+
for key in grid.masks:
|
|
956
|
+
results = grid.gridcalcs[grid.masks[key] & (grid.status == 1)]
|
|
957
|
+
if len(results) > 0:
|
|
958
|
+
if phase in results[0].phases:
|
|
959
|
+
rows, cols = np.nonzero(grid.masks[key])
|
|
960
|
+
for r, c in zip(rows, cols):
|
|
961
|
+
if grid.status[r, c] == 1:
|
|
962
|
+
gd[r, c] = eval_expr(
|
|
963
|
+
expr, grid.gridcalcs[r, c][phase]
|
|
964
|
+
)
|
|
965
|
+
cgd[ix] = gd
|
|
966
|
+
mn = min(np.nanmin(gd), mn)
|
|
967
|
+
mx = max(np.nanmax(gd), mx)
|
|
968
|
+
for ix, grid in self.grids.items():
|
|
969
|
+
im = ax.imshow(
|
|
970
|
+
cgd[ix],
|
|
971
|
+
extent=grid.extent,
|
|
972
|
+
interpolation=interpolation,
|
|
973
|
+
aspect="auto",
|
|
974
|
+
origin="lower",
|
|
975
|
+
vmin=mn,
|
|
976
|
+
vmax=mx,
|
|
977
|
+
)
|
|
978
|
+
self.add_overlay(
|
|
979
|
+
ax, label=label, skiplabels=skiplabels, fontsize=labelfs
|
|
980
|
+
)
|
|
981
|
+
ax.set_xlim(self.xrange)
|
|
982
|
+
ax.set_ylim(self.yrange)
|
|
983
|
+
fig.colorbar(im)
|
|
984
|
+
ax.set_title("{}({})".format(phase, expr))
|
|
985
|
+
fig.tight_layout()
|
|
986
|
+
plt.show()
|
|
987
|
+
else:
|
|
988
|
+
print("Not yet gridded...")
|
|
989
|
+
|
|
990
|
+
def search_composition(
|
|
991
|
+
self,
|
|
992
|
+
*args,
|
|
993
|
+
interpolation=None,
|
|
994
|
+
colors=None,
|
|
995
|
+
label=False,
|
|
996
|
+
skiplabels=0,
|
|
997
|
+
labelfs=6,
|
|
998
|
+
logerr=True,
|
|
999
|
+
maxerr=10,
|
|
1000
|
+
geterror=False,
|
|
1001
|
+
getpt=False,
|
|
1002
|
+
fig=None,
|
|
1003
|
+
fig_kw={},
|
|
1004
|
+
isopleths=False,
|
|
1005
|
+
which=7,
|
|
1006
|
+
smooth=0,
|
|
1007
|
+
alpha=0.5,
|
|
1008
|
+
lw=2,
|
|
1009
|
+
filename=None,
|
|
1010
|
+
save_kw={},
|
|
1011
|
+
):
|
|
1012
|
+
"""Function to plot root squared error of calculated and estimated values.
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
phase (str): Phase or end-member named
|
|
1016
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
1017
|
+
existing for given phase. Check `all_data_keys` property for
|
|
1018
|
+
possible variables.
|
|
1019
|
+
val (float): searched value
|
|
1020
|
+
interpolation (str): matplotlib imshow interpolation method.
|
|
1021
|
+
Default None.
|
|
1022
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
1023
|
+
logerr (bool): When True, plot logarithms of error. Default True
|
|
1024
|
+
geterror (bool): When True, calculated RMSE array is returned. Otherwise
|
|
1025
|
+
error is plotted. Deafult False
|
|
1026
|
+
getpt (bool): When True return tuple of (p, T, err) where error is minimal.
|
|
1027
|
+
Default False
|
|
1028
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
1029
|
+
labelfs (float): Size of label font. Default 6
|
|
1030
|
+
fig (Figure): If not None, axes are added to fig and returned.
|
|
1031
|
+
Default None
|
|
1032
|
+
fig_kw: dict passed to subplots method.
|
|
1033
|
+
filename: If not None, figure is saved to file
|
|
1034
|
+
save_kw: dict passed to savefig method.
|
|
1035
|
+
isopleths (bool): When True, searched isopleths are shown. Default False
|
|
1036
|
+
which (int): Bitopt defining from where data are collected. 0 bit -
|
|
1037
|
+
invariant points, 1 bit - uniariant lines and 2 bit - GridData
|
|
1038
|
+
points
|
|
1039
|
+
|
|
1040
|
+
Example:
|
|
1041
|
+
>>> pt.search_composition(
|
|
1042
|
+
'g', 'xMgX', 0.076,
|
|
1043
|
+
'g', 'xFeX', 0.525,
|
|
1044
|
+
'g', 'xCaX', 0.126,
|
|
1045
|
+
'g', 'xMnX', 0.268
|
|
1046
|
+
)
|
|
1047
|
+
"""
|
|
1048
|
+
if len(args) % 3 == 0:
|
|
1049
|
+
if self.gridded:
|
|
1050
|
+
err = []
|
|
1051
|
+
if colors is None:
|
|
1052
|
+
colors = list(mcolors.TABLEAU_COLORS.keys())[: len(args) // 3]
|
|
1053
|
+
for phase, expr, val in zip(args[::3], args[1::3], args[2::3]):
|
|
1054
|
+
if self.check_phase_expr(phase, expr):
|
|
1055
|
+
err.append(
|
|
1056
|
+
(
|
|
1057
|
+
self.get_gridded(
|
|
1058
|
+
phase, expr, which=which, smooth=smooth
|
|
1059
|
+
)
|
|
1060
|
+
- val
|
|
1061
|
+
)
|
|
1062
|
+
** 2
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
err = np.sqrt(sum(err)) / len(err)
|
|
1066
|
+
err[err > maxerr] = np.nan
|
|
1067
|
+
r, c = np.unravel_index(np.nanargmin(err), err.shape)
|
|
1068
|
+
p = self.yspace[r]
|
|
1069
|
+
T = self.xspace[c]
|
|
1070
|
+
minerr = err[r, c]
|
|
1071
|
+
if geterror:
|
|
1072
|
+
return err
|
|
1073
|
+
elif getpt:
|
|
1074
|
+
return p, T, minerr
|
|
1075
|
+
else:
|
|
1076
|
+
if fig is None:
|
|
1077
|
+
fig, ax = plt.subplots(**fig_kw)
|
|
1078
|
+
else:
|
|
1079
|
+
ax = fig.add_subplot()
|
|
1080
|
+
im = ax.imshow(
|
|
1081
|
+
err,
|
|
1082
|
+
extent=self.xrange + self.yrange,
|
|
1083
|
+
interpolation=interpolation,
|
|
1084
|
+
aspect="auto",
|
|
1085
|
+
origin="lower",
|
|
1086
|
+
alpha=alpha,
|
|
1087
|
+
)
|
|
1088
|
+
if logerr:
|
|
1089
|
+
ax.contour(self.xspace, self.yspace, np.log(err), colors="w")
|
|
1090
|
+
else:
|
|
1091
|
+
ax.contour(self.xspace, self.yspace, err, colors="w")
|
|
1092
|
+
if isopleths:
|
|
1093
|
+
for phase, expr, val, cc in zip(
|
|
1094
|
+
args[::3],
|
|
1095
|
+
args[1::3],
|
|
1096
|
+
args[2::3],
|
|
1097
|
+
colors,
|
|
1098
|
+
):
|
|
1099
|
+
if self.check_phase_expr(phase, expr):
|
|
1100
|
+
ax = self.isopleths_vector(
|
|
1101
|
+
phase, expr, levels=[val], ax=ax, lw=lw, color=cc
|
|
1102
|
+
)
|
|
1103
|
+
ax.plot(T, p, "r*", ms=20)
|
|
1104
|
+
self.add_overlay(
|
|
1105
|
+
ax, label=label, skiplabels=skiplabels, fontsize=labelfs
|
|
1106
|
+
)
|
|
1107
|
+
ax.set_xlim(self.xrange)
|
|
1108
|
+
ax.set_ylim(self.yrange)
|
|
1109
|
+
fig.colorbar(im)
|
|
1110
|
+
|
|
1111
|
+
ax.set_title(f"RMSE - MinErr={minerr:g} at {T:g} {p:g}")
|
|
1112
|
+
fig.tight_layout()
|
|
1113
|
+
|
|
1114
|
+
if filename is not None:
|
|
1115
|
+
plt.savefig(filename, **save_kw)
|
|
1116
|
+
plt.close(fig)
|
|
1117
|
+
else:
|
|
1118
|
+
plt.show()
|
|
1119
|
+
else:
|
|
1120
|
+
print("Not yet gridded...")
|
|
1121
|
+
else:
|
|
1122
|
+
print("The number of argumets must be multiple of 3...")
|
|
1123
|
+
|
|
1124
|
+
def show_status(self, label=False, skiplabels=0, labelfs=6):
|
|
1125
|
+
"""Shows status of grid calculations"""
|
|
1126
|
+
if self.gridded:
|
|
1127
|
+
fig, ax = plt.subplots()
|
|
1128
|
+
im = {}
|
|
1129
|
+
cmap = ListedColormap(["orangered", "limegreen"])
|
|
1130
|
+
bounds = [-0.5, 0.5, 1.5]
|
|
1131
|
+
norm = BoundaryNorm(bounds, cmap.N)
|
|
1132
|
+
for ix, grid in self.grids.items():
|
|
1133
|
+
im[ix] = ax.imshow(
|
|
1134
|
+
grid.status,
|
|
1135
|
+
extent=grid.extent,
|
|
1136
|
+
aspect="auto",
|
|
1137
|
+
origin="lower",
|
|
1138
|
+
cmap=cmap,
|
|
1139
|
+
norm=norm,
|
|
1140
|
+
)
|
|
1141
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
1142
|
+
ax.set_xlim(self.xrange)
|
|
1143
|
+
ax.set_ylim(self.yrange)
|
|
1144
|
+
ax.set_title("Gridding status - {}".format(self.name))
|
|
1145
|
+
cbar = fig.colorbar(im[0], boundaries=bounds, ticks=[0, 1])
|
|
1146
|
+
cbar.ax.set_yticklabels(["Failed", "OK"])
|
|
1147
|
+
fig.tight_layout()
|
|
1148
|
+
plt.show()
|
|
1149
|
+
else:
|
|
1150
|
+
print("Not yet gridded...")
|
|
1151
|
+
|
|
1152
|
+
def show_delta(self, label=False, pointsec=False, skiplabels=0, labelfs=6):
|
|
1153
|
+
"""Shows THERMOCALC execution time for all grid points.
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
pointsec (bool): Whether to show points/sec or secs/point. Default False.
|
|
1157
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
1158
|
+
"""
|
|
1159
|
+
if self.gridded:
|
|
1160
|
+
fig, ax = plt.subplots()
|
|
1161
|
+
cval = {}
|
|
1162
|
+
mn, mx = sys.float_info.max, -sys.float_info.max
|
|
1163
|
+
for ix, grid in self.grids.items():
|
|
1164
|
+
if pointsec:
|
|
1165
|
+
val = 1 / grid.delta
|
|
1166
|
+
lbl = "points/sec"
|
|
1167
|
+
tit = "THERMOCALC calculation rate - {}"
|
|
1168
|
+
else:
|
|
1169
|
+
val = grid.delta
|
|
1170
|
+
lbl = "secs/point"
|
|
1171
|
+
tit = "THERMOCALC execution time - {}"
|
|
1172
|
+
cval[ix] = val
|
|
1173
|
+
mn = min(np.nanmin(val), mn)
|
|
1174
|
+
mx = max(np.nanmax(val), mx)
|
|
1175
|
+
for ix, grid in self.grids.items():
|
|
1176
|
+
im = ax.imshow(
|
|
1177
|
+
cval[ix],
|
|
1178
|
+
extent=grid.extent,
|
|
1179
|
+
aspect="auto",
|
|
1180
|
+
origin="lower",
|
|
1181
|
+
vmin=mn,
|
|
1182
|
+
vmax=mx,
|
|
1183
|
+
)
|
|
1184
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
1185
|
+
ax.set_xlim(self.xrange)
|
|
1186
|
+
ax.set_ylim(self.yrange)
|
|
1187
|
+
cbar = fig.colorbar(im)
|
|
1188
|
+
cbar.set_label(lbl)
|
|
1189
|
+
ax.set_title(tit.format(self.name))
|
|
1190
|
+
fig.tight_layout()
|
|
1191
|
+
plt.show()
|
|
1192
|
+
else:
|
|
1193
|
+
print("Not yet gridded...")
|
|
1194
|
+
|
|
1195
|
+
def identify(self, x, y):
|
|
1196
|
+
"""Return key (frozenset) of divariant field for given temperature and pressure.
|
|
1197
|
+
|
|
1198
|
+
Args:
|
|
1199
|
+
x (float): x coord
|
|
1200
|
+
y (float): y coord
|
|
1201
|
+
"""
|
|
1202
|
+
key = None
|
|
1203
|
+
for k, shape in self.shapes.items():
|
|
1204
|
+
if shape.contains(Point(x, y)):
|
|
1205
|
+
key = k
|
|
1206
|
+
break
|
|
1207
|
+
return key
|
|
1208
|
+
|
|
1209
|
+
def gidentify(self, label=False, skiplabels=0, labelfs=6):
|
|
1210
|
+
"""Visual version of `identify` method. PT point is provided by mouse click.
|
|
1211
|
+
|
|
1212
|
+
Args:
|
|
1213
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
1214
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
1215
|
+
labelfs (float): Size of label font. Default 6
|
|
1216
|
+
"""
|
|
1217
|
+
fig, ax = plt.subplots()
|
|
1218
|
+
ax.autoscale_view()
|
|
1219
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
1220
|
+
ax.set_xlim(self.xrange)
|
|
1221
|
+
ax.set_ylim(self.yrange)
|
|
1222
|
+
ax.format_coord = self.format_coord
|
|
1223
|
+
x, y = plt.ginput(1)[0]
|
|
1224
|
+
plt.close(fig)
|
|
1225
|
+
return self.identify(x, y)
|
|
1226
|
+
|
|
1227
|
+
def glabel(self, label=False, skiplabels=0, labelfs=6):
|
|
1228
|
+
"""Return formatted string of assamblage at PT point provided by mouse click.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
1232
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
1233
|
+
labelfs (float): Size of label font. Default 6
|
|
1234
|
+
"""
|
|
1235
|
+
fig, ax = plt.subplots()
|
|
1236
|
+
ax.autoscale_view()
|
|
1237
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
1238
|
+
ax.set_xlim(self.xrange)
|
|
1239
|
+
ax.set_ylim(self.yrange)
|
|
1240
|
+
ax.format_coord = self.format_coord
|
|
1241
|
+
pts = plt.ginput(0)
|
|
1242
|
+
plt.close(fig)
|
|
1243
|
+
for ix, (x, y) in enumerate(pts):
|
|
1244
|
+
print(
|
|
1245
|
+
f'{ix+1}: {" ".join(sorted([self.abbr.get(sa, sa) for sa in self.identify(x, y)]))}'
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
def pointcalc(self, label=False, skiplabels=0, labelfs=6):
|
|
1249
|
+
fig, ax = plt.subplots()
|
|
1250
|
+
ax.autoscale_view()
|
|
1251
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
1252
|
+
ax.set_xlim(self.xrange)
|
|
1253
|
+
ax.set_ylim(self.yrange)
|
|
1254
|
+
ax.format_coord = self.format_coord
|
|
1255
|
+
x, y = plt.ginput(1)[0]
|
|
1256
|
+
plt.close(fig)
|
|
1257
|
+
k = self.identify(x, y)
|
|
1258
|
+
if k is not None:
|
|
1259
|
+
last_inv = 0
|
|
1260
|
+
for ix, ps in self.sections.items():
|
|
1261
|
+
# update guesses from closest inv point
|
|
1262
|
+
dst = sys.float_info.max
|
|
1263
|
+
for id_inv, inv in ps.invpoints.items():
|
|
1264
|
+
d2 = (inv._x - x) ** 2 + (inv._y - y) ** 2
|
|
1265
|
+
if d2 < dst:
|
|
1266
|
+
dst = d2
|
|
1267
|
+
id_close = id_inv
|
|
1268
|
+
if id_close != last_inv and not ps.invpoints[id_close].manual:
|
|
1269
|
+
self.tc.update_scriptfile(guesses=ps.invpoints[id_close].ptguess())
|
|
1270
|
+
last_inv = id_close
|
|
1271
|
+
tcout, ans = self.tc.calc_assemblage(k.difference(self.tc.excess), y, x)
|
|
1272
|
+
status, res, output = self.tc.parse_logfile()
|
|
1273
|
+
if res is None:
|
|
1274
|
+
for ix, ps in self.sections.items():
|
|
1275
|
+
# update guesses from closest uni line point
|
|
1276
|
+
dst = sys.float_info.max
|
|
1277
|
+
for id_uni in self.unilists[ix][k]:
|
|
1278
|
+
uni = ps.unilines[id_uni]
|
|
1279
|
+
if not uni.manual:
|
|
1280
|
+
for vix in list(range(len(uni._x))[uni.used]):
|
|
1281
|
+
d2 = (uni._x[vix] - x) ** 2 + (uni._y[vix] - y) ** 2
|
|
1282
|
+
if d2 < dst:
|
|
1283
|
+
dst = d2
|
|
1284
|
+
id_close = id_uni
|
|
1285
|
+
vix_close = vix
|
|
1286
|
+
self.tc.update_scriptfile(
|
|
1287
|
+
guesses=ps.unilines[id_close].ptguess(idx=vix_close)
|
|
1288
|
+
)
|
|
1289
|
+
tcout, ans = self.tc.calc_assemblage(k.difference(self.tc.excess), y, x)
|
|
1290
|
+
status, res, output = self.tc.parse_logfile()
|
|
1291
|
+
if res is None:
|
|
1292
|
+
print("Calculation failed")
|
|
1293
|
+
else:
|
|
1294
|
+
print(output)
|
|
1295
|
+
return res[0]
|
|
1296
|
+
|
|
1297
|
+
def remove_grid_data(self, key, phase, expr):
|
|
1298
|
+
"""Remove calculated data from grid and mark as failed
|
|
1299
|
+
|
|
1300
|
+
Note: Click on all data to be discarded by mouse and finish be Enter
|
|
1301
|
+
|
|
1302
|
+
Args:
|
|
1303
|
+
key (frozenset): Key identifying divariant field
|
|
1304
|
+
phase (str): Phase or end-member named
|
|
1305
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
1306
|
+
existing for given phase. Check `all_data_keys` property for
|
|
1307
|
+
possible variables.
|
|
1308
|
+
"""
|
|
1309
|
+
|
|
1310
|
+
if self.gridded:
|
|
1311
|
+
dt = self.collect_data(key, phase, expr, which=4)
|
|
1312
|
+
x, y = np.array(dt["pts"]).T
|
|
1313
|
+
fig, ax = plt.subplots()
|
|
1314
|
+
pts = ax.scatter(x, y, c=dt["data"])
|
|
1315
|
+
plt.colorbar(pts)
|
|
1316
|
+
xy = plt.ginput(0)
|
|
1317
|
+
plt.close(fig)
|
|
1318
|
+
for x, y in xy:
|
|
1319
|
+
ix = self.get_section_id(x, y)
|
|
1320
|
+
if ix is not None:
|
|
1321
|
+
r, c = self.grids[ix].get_indexes(x, y)
|
|
1322
|
+
self.grids[ix].status[r, c] = 0
|
|
1323
|
+
# self.grids[ix].masks[key][r, c] = False
|
|
1324
|
+
self.grids[ix].gridcalcs[r, c] = None
|
|
1325
|
+
self.grids[ix].delta[r, c] = np.nan
|
|
1326
|
+
else:
|
|
1327
|
+
print("Not yet gridded...")
|
|
1328
|
+
|
|
1329
|
+
def ginput_path(self, label=False, skiplabels=0, labelfs=6):
|
|
1330
|
+
"""Collect Path data by mouse digitizing.
|
|
1331
|
+
|
|
1332
|
+
Args:
|
|
1333
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
1334
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
1335
|
+
labelfs (float): Size of label font. Default 6
|
|
1336
|
+
"""
|
|
1337
|
+
fig, ax = plt.subplots()
|
|
1338
|
+
ax.autoscale_view()
|
|
1339
|
+
self.add_overlay(ax, label=label, skiplabels=skiplabels, fontsize=labelfs)
|
|
1340
|
+
ax.set_xlim(self.xrange)
|
|
1341
|
+
ax.set_ylim(self.yrange)
|
|
1342
|
+
ax.format_coord = self.format_coord
|
|
1343
|
+
xy = plt.ginput(0)
|
|
1344
|
+
return np.array(xy).T
|
|
1345
|
+
|
|
1346
|
+
def onclick(self, event):
|
|
1347
|
+
if event.button == 1:
|
|
1348
|
+
if event.inaxes:
|
|
1349
|
+
key = self.identify(event.xdata, event.ydata)
|
|
1350
|
+
if key:
|
|
1351
|
+
print(" ".join(sorted([self.abbr.get(sa, sa) for sa in key])))
|
|
1352
|
+
|
|
1353
|
+
def isopleths(self, phase, expr=None, **kwargs):
|
|
1354
|
+
"""Method to draw compositional isopleths.
|
|
1355
|
+
|
|
1356
|
+
Isopleths are drawn as contours for values evaluated from provided
|
|
1357
|
+
expression. Individual divariant fields are contoured separately, so
|
|
1358
|
+
final plot allows sharp changes accross univariant lines. Within
|
|
1359
|
+
divariant field the selected interpolation is used.
|
|
1360
|
+
|
|
1361
|
+
Args:
|
|
1362
|
+
phase (str): Phase or end-member named
|
|
1363
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
1364
|
+
existing for given phase. Check `all_data_keys` property for
|
|
1365
|
+
possible variables.
|
|
1366
|
+
N (int): Max number of contours. Default 10.
|
|
1367
|
+
step (int): Step between contour levels. If defined, N is ignored.
|
|
1368
|
+
Default None.
|
|
1369
|
+
cdf (bool): When True contour levels are percentile based.
|
|
1370
|
+
Default False.
|
|
1371
|
+
alpha (float): Alpha value for filled contours. Default 1
|
|
1372
|
+
levels (list): User-defined contour levels. If defined, N and step
|
|
1373
|
+
is ignored.
|
|
1374
|
+
which (int): Bitopt defining from where data are collected. 0 bit -
|
|
1375
|
+
invariant points, 1 bit - uniariant lines and 2 bit - GridData
|
|
1376
|
+
points. Default 7 (all data)
|
|
1377
|
+
colorbar (bool): Whether to show colorbar. Default True
|
|
1378
|
+
method: Interpolation method. Default is 'rbf', other option is
|
|
1379
|
+
'quadratic', which uses least-square fit to quadratic surface
|
|
1380
|
+
or 'spline' for bivariate spline interpolation.
|
|
1381
|
+
rbf_func: Default 'thin_plate'. See scipy.interpolation.Rbf
|
|
1382
|
+
smooth (int): Values greater than zero increase the smoothness
|
|
1383
|
+
of the approximation. 0 is for interpolation (default),
|
|
1384
|
+
the function will always go through the nodal points.
|
|
1385
|
+
epsilon (int): Adjustable constant for gaussian or multiquadrics
|
|
1386
|
+
functions - defaults to approximate average distance between
|
|
1387
|
+
nodes (which is a good start).
|
|
1388
|
+
degree (int): Degrees of the bivariate spline. Default is 3.
|
|
1389
|
+
refine (int): Degree of grid refinement. Default 1
|
|
1390
|
+
filter_outliers (bool): Whether to filter outliers. Defaut False.
|
|
1391
|
+
filled (bool): Whether to contours should be filled. Defaut True.
|
|
1392
|
+
filled_over (bool): Whether to overlay contourline over filled
|
|
1393
|
+
contours. Defaut False.
|
|
1394
|
+
out (str or list): Highligt zero-mode lines for given phases.
|
|
1395
|
+
high (frozenset or list): Highlight divariant fields identified
|
|
1396
|
+
by key(s).
|
|
1397
|
+
cmap (str): matplotlib colormap used for contours. Default 'viridis'.
|
|
1398
|
+
bulk (bool): Whether to show bulk composition on top of diagram.
|
|
1399
|
+
Default False.
|
|
1400
|
+
labelkeys (frozenset or list): Keys of divariant fields where contours
|
|
1401
|
+
should be labeled.
|
|
1402
|
+
nosplit (bool): Controls whether the contour underlying labels are
|
|
1403
|
+
removed or not. Defaut True
|
|
1404
|
+
gradient (bool): Whether the first derivate of values should be used.
|
|
1405
|
+
Default False.
|
|
1406
|
+
dt (bool): Whether the gradient should be calculated along
|
|
1407
|
+
temperature or pressure. Default True.
|
|
1408
|
+
fig (Figure): If not None, axes are added to fig and returned.
|
|
1409
|
+
Default None
|
|
1410
|
+
fig_kw: dict passed to subplots method.
|
|
1411
|
+
ax (Axes): Axes to be used. Default None
|
|
1412
|
+
filename: If not None, figure is saved to file
|
|
1413
|
+
save_kw: dict passed to savefig method.
|
|
1414
|
+
show (bool): When False, Axes are returned, otherwise plot is shown.
|
|
1415
|
+
Default True
|
|
1416
|
+
"""
|
|
1417
|
+
if self.check_phase_expr(phase, expr):
|
|
1418
|
+
# parse kwargs
|
|
1419
|
+
which = kwargs.get("which", 7)
|
|
1420
|
+
smooth = kwargs.get("smooth", 0)
|
|
1421
|
+
epsilon = kwargs.get("epsilon", None)
|
|
1422
|
+
filled = kwargs.get("filled", True)
|
|
1423
|
+
filled_over = kwargs.get("filled_over", False)
|
|
1424
|
+
out = kwargs.get("out", None)
|
|
1425
|
+
bulk = kwargs.get("bulk", False)
|
|
1426
|
+
high = kwargs.get("high", [])
|
|
1427
|
+
nosplit = kwargs.get("nosplit", False)
|
|
1428
|
+
step = kwargs.get("step", None)
|
|
1429
|
+
N = kwargs.get("N", 10)
|
|
1430
|
+
cdf = kwargs.get("cdf", False)
|
|
1431
|
+
gradient = kwargs.get("gradient", False)
|
|
1432
|
+
dx = kwargs.get("dx", True)
|
|
1433
|
+
only = kwargs.get("only", None)
|
|
1434
|
+
filter_outliers = kwargs.get("only", False)
|
|
1435
|
+
degree = kwargs.get("degree", 3)
|
|
1436
|
+
refine = kwargs.get("refine", 1)
|
|
1437
|
+
method = kwargs.get("method", "rbf")
|
|
1438
|
+
rbf_func = kwargs.get("rbf_func", "thin_plate")
|
|
1439
|
+
cmap = kwargs.get("cmap", "viridis")
|
|
1440
|
+
colors = kwargs.get("colors", None)
|
|
1441
|
+
colorbar = kwargs.get("colorbar", True)
|
|
1442
|
+
alpha = kwargs.get("alpha", 1)
|
|
1443
|
+
labelkeys = kwargs.get("labelkeys", [])
|
|
1444
|
+
fig = kwargs.get("fig", None)
|
|
1445
|
+
fig_kw = kwargs.get("fig_kw", {})
|
|
1446
|
+
ax = kwargs.get("ax", None)
|
|
1447
|
+
filename = kwargs.get("filename", None)
|
|
1448
|
+
save_kw = kwargs.get("save_kw", {})
|
|
1449
|
+
show = kwargs.get("show", None)
|
|
1450
|
+
|
|
1451
|
+
if not self.gridded:
|
|
1452
|
+
print(
|
|
1453
|
+
"Collecting only from uni lines and inv points. Not yet gridded..."
|
|
1454
|
+
)
|
|
1455
|
+
# fix labelkeys
|
|
1456
|
+
if not isinstance(labelkeys, list):
|
|
1457
|
+
labelkeys = [labelkeys]
|
|
1458
|
+
labelkyes_ok = []
|
|
1459
|
+
for lbl in labelkeys:
|
|
1460
|
+
if isinstance(lbl, str):
|
|
1461
|
+
lbl = frozenset(lbl.split())
|
|
1462
|
+
labelkyes_ok.append(lbl.union(self.tc.excess))
|
|
1463
|
+
if isinstance(out, str):
|
|
1464
|
+
out = [out]
|
|
1465
|
+
if only is not None:
|
|
1466
|
+
recs = OrderedDict()
|
|
1467
|
+
d = self.collect_data(only, phase, expr, which=which)
|
|
1468
|
+
z = d["data"]
|
|
1469
|
+
if z:
|
|
1470
|
+
recs[only] = d
|
|
1471
|
+
mn = min(z)
|
|
1472
|
+
mx = max(z)
|
|
1473
|
+
else:
|
|
1474
|
+
recs, mn, mx = self.merge_data(phase, expr, which=which)
|
|
1475
|
+
if step:
|
|
1476
|
+
cntv = np.arange(0, mx + step, step)
|
|
1477
|
+
cntv = cntv[cntv >= mn - step]
|
|
1478
|
+
else:
|
|
1479
|
+
if cdf:
|
|
1480
|
+
dd = []
|
|
1481
|
+
for key in recs:
|
|
1482
|
+
dd.extend(recs[key]["data"])
|
|
1483
|
+
cntv = np.unique(np.percentile(dd, np.linspace(0, 100, N)))
|
|
1484
|
+
else:
|
|
1485
|
+
ml = ticker.MaxNLocator(nbins=N)
|
|
1486
|
+
cntv = ml.tick_values(vmin=mn, vmax=mx)
|
|
1487
|
+
cntv = kwargs.get("levels", cntv)
|
|
1488
|
+
# Thin-plate contouring of areas
|
|
1489
|
+
if fig is None:
|
|
1490
|
+
if ax is None:
|
|
1491
|
+
fig, ax = plt.subplots(**fig_kw)
|
|
1492
|
+
else:
|
|
1493
|
+
if show is None:
|
|
1494
|
+
show = False
|
|
1495
|
+
fig = ax.get_figure()
|
|
1496
|
+
else:
|
|
1497
|
+
if show is None:
|
|
1498
|
+
show = False
|
|
1499
|
+
ax = fig.add_subplot()
|
|
1500
|
+
for key in recs:
|
|
1501
|
+
phase_parts = phase.split(")")[0].split("(")
|
|
1502
|
+
if phase_parts[0] in key:
|
|
1503
|
+
tmin, pmin, tmax, pmax = self.shapes[key].bounds
|
|
1504
|
+
# ttspace = self.xspace[np.logical_and(self.xspace >= tmin - self.xstep, self.xspace <= tmax + self.xstep)]
|
|
1505
|
+
# ppspace = self.yspace[np.logical_and(self.yspace >= pmin - self.ystep, self.yspace <= pmax + self.ystep)]
|
|
1506
|
+
ttspace = np.arange(
|
|
1507
|
+
tmin - self.gridxstep,
|
|
1508
|
+
tmax + self.gridxstep,
|
|
1509
|
+
self.gridxstep / refine,
|
|
1510
|
+
)
|
|
1511
|
+
ppspace = np.arange(
|
|
1512
|
+
pmin - self.gridystep,
|
|
1513
|
+
pmax + self.gridystep,
|
|
1514
|
+
self.gridystep / refine,
|
|
1515
|
+
)
|
|
1516
|
+
tg, pg = np.meshgrid(ttspace, ppspace)
|
|
1517
|
+
pts = np.array(recs[key]["pts"])
|
|
1518
|
+
x, y = pts.T
|
|
1519
|
+
data = np.array(recs[key]["data"])
|
|
1520
|
+
# filter outliers
|
|
1521
|
+
if filter_outliers:
|
|
1522
|
+
A = np.c_[np.ones_like(x), x, y, x * y, x**2, y**2]
|
|
1523
|
+
C, _, _, _ = lstsq(A, data)
|
|
1524
|
+
# evaluate it on a grid
|
|
1525
|
+
vals = np.dot(
|
|
1526
|
+
np.c_[np.ones_like(x), x, y, x * y, x**2, y**2], C
|
|
1527
|
+
)
|
|
1528
|
+
err = abs(vals - data)
|
|
1529
|
+
ok = err < err.std()
|
|
1530
|
+
pts = pts[ok, :]
|
|
1531
|
+
x, y = pts.T
|
|
1532
|
+
data = data[ok]
|
|
1533
|
+
try:
|
|
1534
|
+
if method == "quadratic":
|
|
1535
|
+
tgg = tg.flatten()
|
|
1536
|
+
pgg = pg.flatten()
|
|
1537
|
+
A = np.c_[np.ones_like(x), x, y, x * y, x**2, y**2]
|
|
1538
|
+
C, _, _, _ = lstsq(A, data)
|
|
1539
|
+
# evaluate it on a grid
|
|
1540
|
+
zg = np.dot(
|
|
1541
|
+
np.c_[
|
|
1542
|
+
np.ones_like(tgg),
|
|
1543
|
+
tgg,
|
|
1544
|
+
pgg,
|
|
1545
|
+
tgg * pgg,
|
|
1546
|
+
tgg**2,
|
|
1547
|
+
pgg**2,
|
|
1548
|
+
],
|
|
1549
|
+
C,
|
|
1550
|
+
).reshape(tg.shape)
|
|
1551
|
+
elif method == "spline":
|
|
1552
|
+
interp = SmoothBivariateSpline(
|
|
1553
|
+
x, self.ratio * y, data, kx=degree, ky=degree
|
|
1554
|
+
)
|
|
1555
|
+
zg = interp(tg, self.ratio * pg)
|
|
1556
|
+
else:
|
|
1557
|
+
with warnings.catch_warnings():
|
|
1558
|
+
warnings.filterwarnings("error")
|
|
1559
|
+
rbf = Rbf(
|
|
1560
|
+
x,
|
|
1561
|
+
self.ratio * y,
|
|
1562
|
+
data,
|
|
1563
|
+
function=rbf_func,
|
|
1564
|
+
smooth=smooth,
|
|
1565
|
+
epsilon=epsilon,
|
|
1566
|
+
)
|
|
1567
|
+
zg = rbf(tg, self.ratio * pg)
|
|
1568
|
+
except Exception as e:
|
|
1569
|
+
if self.show_errors:
|
|
1570
|
+
print(e)
|
|
1571
|
+
try:
|
|
1572
|
+
zg = griddata(
|
|
1573
|
+
np.array(pts),
|
|
1574
|
+
data,
|
|
1575
|
+
(tg, pg),
|
|
1576
|
+
method="nearest",
|
|
1577
|
+
rescale=True,
|
|
1578
|
+
)
|
|
1579
|
+
if method == "rbf":
|
|
1580
|
+
# locate valid data
|
|
1581
|
+
ri, ci = np.nonzero(np.isfinite(zg))
|
|
1582
|
+
x, y, z = np.array(
|
|
1583
|
+
[
|
|
1584
|
+
[tg[r, c], pg[r, c], zg[r, c]]
|
|
1585
|
+
for r, c in zip(ri, ci)
|
|
1586
|
+
]
|
|
1587
|
+
).T
|
|
1588
|
+
# do Rbf extrapolation
|
|
1589
|
+
with warnings.catch_warnings():
|
|
1590
|
+
warnings.filterwarnings(
|
|
1591
|
+
"ignore", category=LinAlgWarning
|
|
1592
|
+
)
|
|
1593
|
+
rbf = Rbf(
|
|
1594
|
+
x,
|
|
1595
|
+
self.ratio * y,
|
|
1596
|
+
z,
|
|
1597
|
+
function=rbf_func,
|
|
1598
|
+
smooth=smooth,
|
|
1599
|
+
epsilon=epsilon,
|
|
1600
|
+
)
|
|
1601
|
+
zg = rbf(tg, self.ratio * pg)
|
|
1602
|
+
if method == "spline":
|
|
1603
|
+
# locate valid data
|
|
1604
|
+
ri, ci = np.nonzero(np.isfinite(zg))
|
|
1605
|
+
x, y, z = np.array(
|
|
1606
|
+
[
|
|
1607
|
+
[tg[r, c], pg[r, c], zg[r, c]]
|
|
1608
|
+
for r, c in zip(ri, ci)
|
|
1609
|
+
]
|
|
1610
|
+
).T
|
|
1611
|
+
interp = SmoothBivariateSpline(
|
|
1612
|
+
x, self.ratio * y, z, kx=degree, ky=degree
|
|
1613
|
+
)
|
|
1614
|
+
zg = interp(tg, self.ratio * pg)
|
|
1615
|
+
except Exception:
|
|
1616
|
+
print(
|
|
1617
|
+
"Using nearest method in {}".format(
|
|
1618
|
+
" ".join(sorted(list(key)))
|
|
1619
|
+
)
|
|
1620
|
+
)
|
|
1621
|
+
# experimental
|
|
1622
|
+
if gradient:
|
|
1623
|
+
grd = np.gradient(zg, self.gridxstep, self.gridystep)
|
|
1624
|
+
if dx:
|
|
1625
|
+
zg = grd[0]
|
|
1626
|
+
else:
|
|
1627
|
+
zg = -grd[1]
|
|
1628
|
+
if N:
|
|
1629
|
+
cntv = N
|
|
1630
|
+
else:
|
|
1631
|
+
cntv = 10
|
|
1632
|
+
# ------------
|
|
1633
|
+
with warnings.catch_warnings():
|
|
1634
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
1635
|
+
if filled:
|
|
1636
|
+
if colors is not None:
|
|
1637
|
+
cont = ax.contourf(
|
|
1638
|
+
tg, pg, zg, cntv, colors=colors, alpha=alpha
|
|
1639
|
+
)
|
|
1640
|
+
if filled_over:
|
|
1641
|
+
contover = ax.contour(
|
|
1642
|
+
tg, pg, zg, cntv, colors=colors
|
|
1643
|
+
)
|
|
1644
|
+
else:
|
|
1645
|
+
cont = ax.contourf(
|
|
1646
|
+
tg, pg, zg, cntv, cmap=cmap, alpha=alpha
|
|
1647
|
+
)
|
|
1648
|
+
if filled_over:
|
|
1649
|
+
contover = ax.contour(
|
|
1650
|
+
tg, pg, zg, cntv, colors="whitesmoke"
|
|
1651
|
+
)
|
|
1652
|
+
else:
|
|
1653
|
+
if colors is not None:
|
|
1654
|
+
cont = ax.contour(tg, pg, zg, cntv, colors=colors)
|
|
1655
|
+
else:
|
|
1656
|
+
cont = ax.contour(tg, pg, zg, cntv, cmap=cmap)
|
|
1657
|
+
patch = PolygonPatch(self.shapes[key], fc="none", ec="none")
|
|
1658
|
+
ax.add_patch(patch)
|
|
1659
|
+
# label if needed
|
|
1660
|
+
if (not filled or filled_over) and key in labelkyes_ok:
|
|
1661
|
+
positions = []
|
|
1662
|
+
contlbl = contover if filled_over else cont
|
|
1663
|
+
for col in contlbl.collections:
|
|
1664
|
+
for seg in col.get_paths(): # get_segments():
|
|
1665
|
+
inside = np.fromiter(
|
|
1666
|
+
map(
|
|
1667
|
+
self.shapes[key].contains,
|
|
1668
|
+
MultiPoint(seg.vertices).geoms,
|
|
1669
|
+
),
|
|
1670
|
+
dtype=bool,
|
|
1671
|
+
)
|
|
1672
|
+
if np.any(inside):
|
|
1673
|
+
positions.append(seg.vertices[inside].mean(axis=0))
|
|
1674
|
+
ax.clabel(
|
|
1675
|
+
contlbl,
|
|
1676
|
+
fontsize=9,
|
|
1677
|
+
manual=positions,
|
|
1678
|
+
fmt="%g",
|
|
1679
|
+
inline_spacing=3,
|
|
1680
|
+
inline=not nosplit,
|
|
1681
|
+
)
|
|
1682
|
+
for col in cont.collections:
|
|
1683
|
+
col.set_clip_path(patch)
|
|
1684
|
+
if filled and filled_over:
|
|
1685
|
+
for col in contover.collections:
|
|
1686
|
+
col.set_clip_path(patch)
|
|
1687
|
+
|
|
1688
|
+
if only is None:
|
|
1689
|
+
self.add_overlay(ax)
|
|
1690
|
+
# zero mode lines
|
|
1691
|
+
if out:
|
|
1692
|
+
for o in out:
|
|
1693
|
+
xy = []
|
|
1694
|
+
for ps in self.sections.values():
|
|
1695
|
+
for uni in ps.unilines.values():
|
|
1696
|
+
if o in uni.out:
|
|
1697
|
+
xy.append((uni.x, uni.y))
|
|
1698
|
+
for poly in polymorphs:
|
|
1699
|
+
if poly.issubset(uni.phases):
|
|
1700
|
+
if o in poly:
|
|
1701
|
+
if poly.difference({o}).issubset(uni.out):
|
|
1702
|
+
xy.append((uni.x, uni.y))
|
|
1703
|
+
if xy:
|
|
1704
|
+
ax.plot(
|
|
1705
|
+
np.hstack([(*seg[0], np.nan) for seg in xy]),
|
|
1706
|
+
np.hstack([(*seg[1], np.nan) for seg in xy]),
|
|
1707
|
+
lw=2,
|
|
1708
|
+
)
|
|
1709
|
+
if colorbar:
|
|
1710
|
+
try:
|
|
1711
|
+
fig.colorbar(cont)
|
|
1712
|
+
except Exception as e:
|
|
1713
|
+
if self.show_errors:
|
|
1714
|
+
print(e)
|
|
1715
|
+
# Show highlight. Change to list if only single key
|
|
1716
|
+
if not isinstance(high, list):
|
|
1717
|
+
high = [high]
|
|
1718
|
+
if only is None:
|
|
1719
|
+
for k in high:
|
|
1720
|
+
if isinstance(k, str):
|
|
1721
|
+
k = frozenset(k.split())
|
|
1722
|
+
k = k.union(self.tc.excess)
|
|
1723
|
+
if k in self.shapes:
|
|
1724
|
+
ax.add_patch(
|
|
1725
|
+
PolygonPatch(self.shapes[k], fc="none", ec="red", lw=2)
|
|
1726
|
+
)
|
|
1727
|
+
else:
|
|
1728
|
+
print("Field {} not found.".format(" ".join(k)))
|
|
1729
|
+
# bulk
|
|
1730
|
+
if bulk:
|
|
1731
|
+
if only is None:
|
|
1732
|
+
ax.set_xlim(self.xrange)
|
|
1733
|
+
ax.set_ylim(self.yrange)
|
|
1734
|
+
ax.set_xlabel("{}({})".format(phase, expr))
|
|
1735
|
+
else:
|
|
1736
|
+
ax.set_xlabel("{} - {}({})".format(" ".join(only), phase, expr))
|
|
1737
|
+
# bulk composition
|
|
1738
|
+
if self.section_class.__name__ == "PTsection":
|
|
1739
|
+
ox, val = self.tc.bulk
|
|
1740
|
+
table(ax=ax, cellText=[val], colLabels=ox, loc="top")
|
|
1741
|
+
else:
|
|
1742
|
+
ox, val1, val2 = self.tc.bulk
|
|
1743
|
+
table(ax=ax, cellText=[val1, val2], colLabels=ox, loc="top")
|
|
1744
|
+
else:
|
|
1745
|
+
if only is None:
|
|
1746
|
+
# if show is None or show is True:
|
|
1747
|
+
ax.set_xlim(self.xrange)
|
|
1748
|
+
ax.set_ylim(self.yrange)
|
|
1749
|
+
ax.set_title("{}({})".format(phase, expr))
|
|
1750
|
+
else:
|
|
1751
|
+
ax.set_title("{} - {}({})".format(" ".join(only), phase, expr))
|
|
1752
|
+
# coords
|
|
1753
|
+
ax.format_coord = self.format_coord
|
|
1754
|
+
# connect button press
|
|
1755
|
+
# cid = fig.canvas.mpl_connect('button_press_event', self.onclick)
|
|
1756
|
+
if filename is not None:
|
|
1757
|
+
plt.savefig(filename, **save_kw)
|
|
1758
|
+
plt.close(fig)
|
|
1759
|
+
else:
|
|
1760
|
+
if show is None or show is True:
|
|
1761
|
+
plt.show()
|
|
1762
|
+
else:
|
|
1763
|
+
return ax
|
|
1764
|
+
|
|
1765
|
+
def overlap_isopleths(self, *args, **kwargs):
|
|
1766
|
+
"""Function to overlaped isopleths ranges.
|
|
1767
|
+
|
|
1768
|
+
Args:
|
|
1769
|
+
phase (str): Phase or end-member named
|
|
1770
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
1771
|
+
existing for given phase. Check `all_data_keys` property for
|
|
1772
|
+
possible variables.
|
|
1773
|
+
levels (tuple): tuple of min an max values for isopleth band
|
|
1774
|
+
alpha (float): Alpha value for filled contours. Default 1
|
|
1775
|
+
fig (Figure): If not None, axes are added to fig and returned.
|
|
1776
|
+
Default None
|
|
1777
|
+
fig_kw: dict passed to subplots method.
|
|
1778
|
+
filename: If not None, figure is saved to file
|
|
1779
|
+
save_kw: dict passed to savefig method.
|
|
1780
|
+
show (bool): When False, Axes are returned, otherwise plot is shown.
|
|
1781
|
+
Default True
|
|
1782
|
+
|
|
1783
|
+
Example:
|
|
1784
|
+
>>> pt.overlap_isopleths(
|
|
1785
|
+
'g', 'xFeX', (0.51, 0.65),
|
|
1786
|
+
'g', 'xMgX', (0.07, 0.13),
|
|
1787
|
+
'g', 'xMnX', (0.16, 0.27),
|
|
1788
|
+
'g', 'xCaX', (0.06, 0.15),
|
|
1789
|
+
)
|
|
1790
|
+
"""
|
|
1791
|
+
if len(args) % 3 == 0:
|
|
1792
|
+
if self.gridded:
|
|
1793
|
+
fig = kwargs.get("fig", None)
|
|
1794
|
+
fig_kw = kwargs.get("fig_kw", {})
|
|
1795
|
+
ax = kwargs.get("ax", None)
|
|
1796
|
+
alpha = kwargs.get("alpha", 0.5)
|
|
1797
|
+
filename = kwargs.get("filename", None)
|
|
1798
|
+
save_kw = kwargs.get("save_kw", {})
|
|
1799
|
+
show = kwargs.get("show", None)
|
|
1800
|
+
colors = kwargs.get("colors", None)
|
|
1801
|
+
if colors is None:
|
|
1802
|
+
colors = list(mcolors.TABLEAU_COLORS.keys())[: len(args) // 3]
|
|
1803
|
+
handles = []
|
|
1804
|
+
if fig is None:
|
|
1805
|
+
if ax is None:
|
|
1806
|
+
fig, ax = plt.subplots(**fig_kw)
|
|
1807
|
+
else:
|
|
1808
|
+
if show is None:
|
|
1809
|
+
show = False
|
|
1810
|
+
fig = ax.get_figure()
|
|
1811
|
+
else:
|
|
1812
|
+
if show is None:
|
|
1813
|
+
show = False
|
|
1814
|
+
ax = fig.add_subplot()
|
|
1815
|
+
for phase, expr, levels, color in zip(
|
|
1816
|
+
args[::3], args[1::3], args[2::3], colors
|
|
1817
|
+
):
|
|
1818
|
+
ax = self.isopleths(
|
|
1819
|
+
phase,
|
|
1820
|
+
expr,
|
|
1821
|
+
levels=levels,
|
|
1822
|
+
colorbar=False,
|
|
1823
|
+
filled_over=True,
|
|
1824
|
+
colors=[color],
|
|
1825
|
+
alpha=alpha,
|
|
1826
|
+
show=False,
|
|
1827
|
+
ax=ax,
|
|
1828
|
+
)
|
|
1829
|
+
handles.append(mpatches.Patch(color=color, alpha=alpha, label=expr))
|
|
1830
|
+
self.add_overlay(ax)
|
|
1831
|
+
ax.legend(handles=handles)
|
|
1832
|
+
# coords
|
|
1833
|
+
ax.format_coord = self.format_coord
|
|
1834
|
+
# connect button press
|
|
1835
|
+
# cid = fig.canvas.mpl_connect('button_press_event', self.onclick)
|
|
1836
|
+
ax.set_title(self.name)
|
|
1837
|
+
if filename is not None:
|
|
1838
|
+
plt.savefig(filename, **save_kw)
|
|
1839
|
+
plt.close(fig)
|
|
1840
|
+
else:
|
|
1841
|
+
if show is None or show is True:
|
|
1842
|
+
plt.show()
|
|
1843
|
+
else:
|
|
1844
|
+
return ax
|
|
1845
|
+
else:
|
|
1846
|
+
print("Not yet gridded...")
|
|
1847
|
+
else:
|
|
1848
|
+
print("The number of argumets must be multiple of 3...")
|
|
1849
|
+
|
|
1850
|
+
def isopleths_vector(self, phase, expr=None, **kwargs):
|
|
1851
|
+
"""Method to draw vectorized compositional isopleths.
|
|
1852
|
+
|
|
1853
|
+
Isopleths are drawn as lines for values evaluated from provided
|
|
1854
|
+
expression. Individual divariant fields are contoured separately, so
|
|
1855
|
+
final plot allows sharp changes accross univariant lines. Within
|
|
1856
|
+
divariant field the selected interpolation is used.
|
|
1857
|
+
|
|
1858
|
+
Args:
|
|
1859
|
+
phase (str): Phase or end-member named
|
|
1860
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
1861
|
+
existing for given phase. Check `all_data_keys` property for
|
|
1862
|
+
possible variables.
|
|
1863
|
+
N (int): Max number of contours. Default 10.
|
|
1864
|
+
step (int): Step between contour levels. If defined, N is ignored.
|
|
1865
|
+
Default None.
|
|
1866
|
+
cdf (bool): When True contour levels are percentile based.
|
|
1867
|
+
Default False.
|
|
1868
|
+
levels (list): User-defined contour levels. If defined, N and step
|
|
1869
|
+
is ignored.
|
|
1870
|
+
which (int): Bitopt defining from where data are collected. 0 bit -
|
|
1871
|
+
invariant points, 1 bit - uniariant lines and 2 bit - GridData
|
|
1872
|
+
points. Default 7 (all data)
|
|
1873
|
+
colorbar (bool): Whether to show colorbar. Default True
|
|
1874
|
+
method: Interpolation method. Default is 'rbf', other option is
|
|
1875
|
+
'quadratic', which uses least-square fit to quadratic surface.
|
|
1876
|
+
rbf_func: Default 'thin_plate'. See scipy.interpolation.Rbf
|
|
1877
|
+
smooth (int): Values greater than zero increase the smoothness
|
|
1878
|
+
of the approximation. 0 is for interpolation (default),
|
|
1879
|
+
the function will always go through the nodal points.
|
|
1880
|
+
epsilon (int): Adjustable constant for gaussian or multiquadrics
|
|
1881
|
+
functions - defaults to approximate average distance between
|
|
1882
|
+
nodes (which is a good start).
|
|
1883
|
+
degree (int): Degrees of the bivariate spline. Default is 3.
|
|
1884
|
+
refine (int): Degree of grid refinement. Default 1
|
|
1885
|
+
filter_outliers (bool): Whether to filter outliers. Defaut False.
|
|
1886
|
+
out (str or list): Highligt zero-mode lines for given phases.
|
|
1887
|
+
overlay (bool): Whether to show assemblage fields. Default True
|
|
1888
|
+
high (frozenset or list): Highlight divariant fields identified
|
|
1889
|
+
by key(s).
|
|
1890
|
+
cmap (str): matplotlib colormap used isopleths coloring. Default 'viridis'.
|
|
1891
|
+
bulk (bool): Whether to show bulk composition on top of diagram.
|
|
1892
|
+
Default False.
|
|
1893
|
+
fig (Figure): If not None, axes are added to fig and returned.
|
|
1894
|
+
Default None
|
|
1895
|
+
fig_kw: dict passed to subplots method.
|
|
1896
|
+
lw (float): Linewidth of isopleths. Default 1.0
|
|
1897
|
+
ax (Axes): Axes to be used. Default None
|
|
1898
|
+
filename: If not None, figure is saved to file
|
|
1899
|
+
save_kw: dict passed to savefig method.
|
|
1900
|
+
color: matplotlib color for isopleths. If None cmap is used. Default None.
|
|
1901
|
+
show (bool): When False, Axes are returned, otherwise plot is shown.
|
|
1902
|
+
Default True
|
|
1903
|
+
"""
|
|
1904
|
+
from skimage import measure
|
|
1905
|
+
from matplotlib.cm import ScalarMappable
|
|
1906
|
+
|
|
1907
|
+
if self.check_phase_expr(phase, expr):
|
|
1908
|
+
# parse kwargs
|
|
1909
|
+
which = kwargs.get("which", 7)
|
|
1910
|
+
smooth = kwargs.get("smooth", 0)
|
|
1911
|
+
epsilon = kwargs.get("epsilon", None)
|
|
1912
|
+
out = kwargs.get("out", None)
|
|
1913
|
+
bulk = kwargs.get("bulk", False)
|
|
1914
|
+
high = kwargs.get("high", [])
|
|
1915
|
+
step = kwargs.get("step", None)
|
|
1916
|
+
N = kwargs.get("N", 10)
|
|
1917
|
+
cdf = kwargs.get("cdf", False)
|
|
1918
|
+
overlay = kwargs.get("overlay", True)
|
|
1919
|
+
only = kwargs.get("only", None)
|
|
1920
|
+
filter_outliers = kwargs.get("only", False)
|
|
1921
|
+
degree = kwargs.get("degree", 3)
|
|
1922
|
+
refine = kwargs.get("refine", 1)
|
|
1923
|
+
method = kwargs.get("method", "rbf")
|
|
1924
|
+
rbf_func = kwargs.get("rbf_func", "thin_plate")
|
|
1925
|
+
cmap = kwargs.get("cmap", "viridis")
|
|
1926
|
+
color = kwargs.get("color", None)
|
|
1927
|
+
colorbar = kwargs.get("colorbar", True)
|
|
1928
|
+
fig = kwargs.get("fig", None)
|
|
1929
|
+
fig_kw = kwargs.get("fig_kw", {})
|
|
1930
|
+
lw = kwargs.get("lw", 1.0)
|
|
1931
|
+
ax = kwargs.get("ax", None)
|
|
1932
|
+
filename = kwargs.get("filename", None)
|
|
1933
|
+
save_kw = kwargs.get("save_kw", {})
|
|
1934
|
+
show = kwargs.get("show", None)
|
|
1935
|
+
|
|
1936
|
+
if not self.gridded:
|
|
1937
|
+
print(
|
|
1938
|
+
"Collecting only from uni lines and inv points. Not yet gridded..."
|
|
1939
|
+
)
|
|
1940
|
+
if isinstance(out, str):
|
|
1941
|
+
out = [out]
|
|
1942
|
+
if only is not None:
|
|
1943
|
+
recs = OrderedDict()
|
|
1944
|
+
d = self.collect_data(only, phase, expr, which=which)
|
|
1945
|
+
z = d["data"]
|
|
1946
|
+
if z:
|
|
1947
|
+
recs[only] = d
|
|
1948
|
+
mn = min(z)
|
|
1949
|
+
mx = max(z)
|
|
1950
|
+
else:
|
|
1951
|
+
recs, mn, mx = self.merge_data(phase, expr, which=which)
|
|
1952
|
+
mapper = ScalarMappable(
|
|
1953
|
+
norm=Normalize(vmin=mn, vmax=mx, clip=True), cmap=cmap
|
|
1954
|
+
)
|
|
1955
|
+
if step:
|
|
1956
|
+
cntv = np.arange(0, mx + step, step)
|
|
1957
|
+
cntv = cntv[cntv >= mn - step]
|
|
1958
|
+
else:
|
|
1959
|
+
if cdf:
|
|
1960
|
+
dd = []
|
|
1961
|
+
for key in recs:
|
|
1962
|
+
dd.extend(recs[key]["data"])
|
|
1963
|
+
cntv = np.percentile(dd, np.linspace(0, 100, N))
|
|
1964
|
+
else:
|
|
1965
|
+
ml = ticker.MaxNLocator(nbins=N)
|
|
1966
|
+
cntv = ml.tick_values(vmin=mn, vmax=mx)
|
|
1967
|
+
cntv = kwargs.get("levels", cntv)
|
|
1968
|
+
# Thin-plate contouring of areas
|
|
1969
|
+
if fig is None:
|
|
1970
|
+
if ax is None:
|
|
1971
|
+
fig, ax = plt.subplots(**fig_kw)
|
|
1972
|
+
else:
|
|
1973
|
+
if show is None:
|
|
1974
|
+
show = False
|
|
1975
|
+
fig = ax.get_figure()
|
|
1976
|
+
else:
|
|
1977
|
+
if show is None:
|
|
1978
|
+
show = False
|
|
1979
|
+
ax = fig.add_subplot()
|
|
1980
|
+
for key in recs:
|
|
1981
|
+
phase_parts = phase.split(")")[0].split("(")
|
|
1982
|
+
if phase_parts[0] in key:
|
|
1983
|
+
tmin, pmin, tmax, pmax = self.shapes[key].bounds
|
|
1984
|
+
# ttspace = self.xspace[np.logical_and(self.xspace >= tmin - self.xstep, self.xspace <= tmax + self.xstep)]
|
|
1985
|
+
# ppspace = self.yspace[np.logical_and(self.yspace >= pmin - self.ystep, self.yspace <= pmax + self.ystep)]
|
|
1986
|
+
ttspace = np.arange(
|
|
1987
|
+
tmin - self.gridxstep,
|
|
1988
|
+
tmax + self.gridxstep,
|
|
1989
|
+
self.gridxstep / refine,
|
|
1990
|
+
)
|
|
1991
|
+
ppspace = np.arange(
|
|
1992
|
+
pmin - self.gridystep,
|
|
1993
|
+
pmax + self.gridystep,
|
|
1994
|
+
self.gridystep / refine,
|
|
1995
|
+
)
|
|
1996
|
+
tg, pg = np.meshgrid(ttspace, ppspace)
|
|
1997
|
+
x, y = np.array(recs[key]["pts"]).T
|
|
1998
|
+
pts = recs[key]["pts"]
|
|
1999
|
+
data = recs[key]["data"]
|
|
2000
|
+
# filter outliers
|
|
2001
|
+
if filter_outliers:
|
|
2002
|
+
A = np.c_[np.ones_like(x), x, y, x * y, x**2, y**2]
|
|
2003
|
+
C, _, _, _ = lstsq(A, data)
|
|
2004
|
+
# evaluate it on a grid
|
|
2005
|
+
vals = np.dot(
|
|
2006
|
+
np.c_[np.ones_like(x), x, y, x * y, x**2, y**2], C
|
|
2007
|
+
)
|
|
2008
|
+
err = abs(vals - data)
|
|
2009
|
+
ok = err < err.std()
|
|
2010
|
+
pts = pts[ok, :]
|
|
2011
|
+
x, y = pts.T
|
|
2012
|
+
data = data[ok]
|
|
2013
|
+
try:
|
|
2014
|
+
if method == "quadratic":
|
|
2015
|
+
tgg = tg.flatten()
|
|
2016
|
+
pgg = pg.flatten()
|
|
2017
|
+
A = np.c_[np.ones_like(x), x, y, x * y, x**2, y**2]
|
|
2018
|
+
C, _, _, _ = lstsq(A, data)
|
|
2019
|
+
# evaluate it on a grid
|
|
2020
|
+
zg = np.dot(
|
|
2021
|
+
np.c_[
|
|
2022
|
+
np.ones_like(tgg),
|
|
2023
|
+
tgg,
|
|
2024
|
+
pgg,
|
|
2025
|
+
tgg * pgg,
|
|
2026
|
+
tgg**2,
|
|
2027
|
+
pgg**2,
|
|
2028
|
+
],
|
|
2029
|
+
C,
|
|
2030
|
+
).reshape(tg.shape)
|
|
2031
|
+
elif method == "spline":
|
|
2032
|
+
interp = SmoothBivariateSpline(
|
|
2033
|
+
x, self.ratio * y, data, kx=degree, ky=degree
|
|
2034
|
+
)
|
|
2035
|
+
zg = interp(tg, self.ratio * pg)
|
|
2036
|
+
else:
|
|
2037
|
+
with warnings.catch_warnings():
|
|
2038
|
+
warnings.filterwarnings("error")
|
|
2039
|
+
rbf = Rbf(
|
|
2040
|
+
x,
|
|
2041
|
+
self.ratio * y,
|
|
2042
|
+
data,
|
|
2043
|
+
function=rbf_func,
|
|
2044
|
+
smooth=smooth,
|
|
2045
|
+
epsilon=epsilon,
|
|
2046
|
+
)
|
|
2047
|
+
zg = rbf(tg, self.ratio * pg)
|
|
2048
|
+
except Exception as e:
|
|
2049
|
+
if self.show_errors:
|
|
2050
|
+
print(e)
|
|
2051
|
+
try:
|
|
2052
|
+
zg = griddata(
|
|
2053
|
+
np.array(pts),
|
|
2054
|
+
data,
|
|
2055
|
+
(tg, pg),
|
|
2056
|
+
method="nearest",
|
|
2057
|
+
rescale=True,
|
|
2058
|
+
)
|
|
2059
|
+
if method == "rbf":
|
|
2060
|
+
# locate valid data
|
|
2061
|
+
ri, ci = np.nonzero(np.isfinite(zg))
|
|
2062
|
+
x, y, z = np.array(
|
|
2063
|
+
[
|
|
2064
|
+
[tg[r, c], pg[r, c], zg[r, c]]
|
|
2065
|
+
for r, c in zip(ri, ci)
|
|
2066
|
+
]
|
|
2067
|
+
).T
|
|
2068
|
+
# do Rbf extrapolation
|
|
2069
|
+
with warnings.catch_warnings():
|
|
2070
|
+
warnings.filterwarnings(
|
|
2071
|
+
"ignore", category=LinAlgWarning
|
|
2072
|
+
)
|
|
2073
|
+
rbf = Rbf(
|
|
2074
|
+
x,
|
|
2075
|
+
self.ratio * y,
|
|
2076
|
+
z,
|
|
2077
|
+
function=rbf_func,
|
|
2078
|
+
smooth=smooth,
|
|
2079
|
+
epsilon=epsilon,
|
|
2080
|
+
)
|
|
2081
|
+
zg = rbf(tg, self.ratio * pg)
|
|
2082
|
+
if method == "spline":
|
|
2083
|
+
# locate valid data
|
|
2084
|
+
ri, ci = np.nonzero(np.isfinite(zg))
|
|
2085
|
+
x, y, z = np.array(
|
|
2086
|
+
[
|
|
2087
|
+
[tg[r, c], pg[r, c], zg[r, c]]
|
|
2088
|
+
for r, c in zip(ri, ci)
|
|
2089
|
+
]
|
|
2090
|
+
).T
|
|
2091
|
+
interp = SmoothBivariateSpline(
|
|
2092
|
+
x, self.ratio * y, z, kx=degree, ky=degree
|
|
2093
|
+
)
|
|
2094
|
+
zg = interp(tg, self.ratio * pg)
|
|
2095
|
+
except Exception:
|
|
2096
|
+
print(
|
|
2097
|
+
"Using nearest method in {}".format(
|
|
2098
|
+
" ".join(sorted(list(key)))
|
|
2099
|
+
)
|
|
2100
|
+
)
|
|
2101
|
+
# ------------
|
|
2102
|
+
scx = (tmax - tmin + 2 * self.gridxstep) / zg.shape[1]
|
|
2103
|
+
scy = (pmax - pmin + 2 * self.gridystep) / zg.shape[0]
|
|
2104
|
+
for v in cntv:
|
|
2105
|
+
contours = measure.find_contours(zg, v)
|
|
2106
|
+
for contour in contours:
|
|
2107
|
+
cnt = np.array(
|
|
2108
|
+
(
|
|
2109
|
+
contour[:, 1] * scx + tmin - self.gridxstep,
|
|
2110
|
+
contour[:, 0] * scy + pmin - self.gridystep,
|
|
2111
|
+
)
|
|
2112
|
+
).T
|
|
2113
|
+
lnc = LineString(cnt)
|
|
2114
|
+
if self.shapes[key].intersects(lnc):
|
|
2115
|
+
ln = self.shapes[key].intersection(lnc)
|
|
2116
|
+
if ln.geom_type == "MultiLineString":
|
|
2117
|
+
for lnp in ln.geoms:
|
|
2118
|
+
lnp = lnp.simplify(tolerance=0.01)
|
|
2119
|
+
x, y = np.array(lnp.coords).T
|
|
2120
|
+
if color is None:
|
|
2121
|
+
ax.plot(
|
|
2122
|
+
x, y, color=mapper.to_rgba(v), lw=lw
|
|
2123
|
+
)
|
|
2124
|
+
else:
|
|
2125
|
+
ax.plot(x, y, color=color, lw=lw)
|
|
2126
|
+
else:
|
|
2127
|
+
ln = ln.simplify(tolerance=0.01)
|
|
2128
|
+
x, y = np.array(ln.coords).T
|
|
2129
|
+
if color is None:
|
|
2130
|
+
ax.plot(x, y, color=mapper.to_rgba(v), lw=lw)
|
|
2131
|
+
else:
|
|
2132
|
+
ax.plot(x, y, color=color, lw=lw)
|
|
2133
|
+
if only is None:
|
|
2134
|
+
if overlay:
|
|
2135
|
+
self.add_overlay(ax)
|
|
2136
|
+
# zero mode lines
|
|
2137
|
+
if out:
|
|
2138
|
+
for o in out:
|
|
2139
|
+
xy = []
|
|
2140
|
+
for ps in self.sections.values():
|
|
2141
|
+
for uni in ps.unilines.values():
|
|
2142
|
+
if o in uni.out:
|
|
2143
|
+
xy.append((uni.x, uni.y))
|
|
2144
|
+
for poly in polymorphs:
|
|
2145
|
+
if poly.issubset(uni.phases):
|
|
2146
|
+
if o in poly:
|
|
2147
|
+
if poly.difference({o}).issubset(uni.out):
|
|
2148
|
+
xy.append((uni.x, uni.y))
|
|
2149
|
+
if xy:
|
|
2150
|
+
ax.plot(
|
|
2151
|
+
np.hstack([(*seg[0], np.nan) for seg in xy]),
|
|
2152
|
+
np.hstack([(*seg[1], np.nan) for seg in xy]),
|
|
2153
|
+
lw=2,
|
|
2154
|
+
)
|
|
2155
|
+
if colorbar:
|
|
2156
|
+
try:
|
|
2157
|
+
cbar = fig.colorbar(mapper)
|
|
2158
|
+
cbar.ax.set_yticks(cntv)
|
|
2159
|
+
except Exception as e:
|
|
2160
|
+
if self.show_errors:
|
|
2161
|
+
print(e)
|
|
2162
|
+
# Show highlight. Change to list if only single key
|
|
2163
|
+
if not isinstance(high, list):
|
|
2164
|
+
high = [high]
|
|
2165
|
+
if only is None:
|
|
2166
|
+
for k in high:
|
|
2167
|
+
if isinstance(k, str):
|
|
2168
|
+
k = frozenset(k.split())
|
|
2169
|
+
k = k.union(self.tc.excess)
|
|
2170
|
+
if k in self.shapes:
|
|
2171
|
+
ax.add_patch(
|
|
2172
|
+
PolygonPatch(self.shapes[k], fc="none", ec="red", lw=2)
|
|
2173
|
+
)
|
|
2174
|
+
else:
|
|
2175
|
+
print("Field {} not found.".format(" ".join(k)))
|
|
2176
|
+
# bulk
|
|
2177
|
+
if bulk:
|
|
2178
|
+
if only is None:
|
|
2179
|
+
ax.set_xlim(self.xrange)
|
|
2180
|
+
ax.set_ylim(self.yrange)
|
|
2181
|
+
ax.set_xlabel("{}({})".format(phase, expr))
|
|
2182
|
+
else:
|
|
2183
|
+
ax.set_xlabel("{} - {}({})".format(" ".join(only), phase, expr))
|
|
2184
|
+
# bulk composition
|
|
2185
|
+
if self.section_class.__name__ == "PTsection":
|
|
2186
|
+
ox, val = self.tc.bulk
|
|
2187
|
+
table(ax=ax, cellText=[val], colLabels=ox, loc="top")
|
|
2188
|
+
else:
|
|
2189
|
+
ox, val1, val2 = self.tc.bulk
|
|
2190
|
+
table(ax=ax, cellText=[val1, val2], colLabels=ox, loc="top")
|
|
2191
|
+
else:
|
|
2192
|
+
if only is None:
|
|
2193
|
+
if show is None or show is True:
|
|
2194
|
+
ax.set_xlim(self.xrange)
|
|
2195
|
+
ax.set_ylim(self.yrange)
|
|
2196
|
+
ax.set_title("{}({})".format(phase, expr))
|
|
2197
|
+
else:
|
|
2198
|
+
ax.set_title("{} - {}({})".format(" ".join(only), phase, expr))
|
|
2199
|
+
# coords
|
|
2200
|
+
ax.format_coord = self.format_coord
|
|
2201
|
+
# connect button press
|
|
2202
|
+
# cid = fig.canvas.mpl_connect('button_press_event', self.onclick)
|
|
2203
|
+
if filename is not None:
|
|
2204
|
+
plt.savefig(filename, **save_kw)
|
|
2205
|
+
plt.close(fig)
|
|
2206
|
+
else:
|
|
2207
|
+
if show is None or show is True:
|
|
2208
|
+
plt.show()
|
|
2209
|
+
else:
|
|
2210
|
+
return ax
|
|
2211
|
+
|
|
2212
|
+
def gendrawpd(self, export_areas=True):
|
|
2213
|
+
"""Method to write drawpd file
|
|
2214
|
+
|
|
2215
|
+
Args:
|
|
2216
|
+
export_areas (bool): Whether to include constructed areas. Default True.
|
|
2217
|
+
"""
|
|
2218
|
+
with self.tc.drawpdfile.open("w", encoding=self.tc.TCenc) as output:
|
|
2219
|
+
output.write("% Generated by pypsbuilder (c) Ondrej Lexa 2020\n")
|
|
2220
|
+
output.write(
|
|
2221
|
+
"2 % no. of variables in each line of data, in this case P, T\n"
|
|
2222
|
+
)
|
|
2223
|
+
exc = frozenset.intersection(*self.keys)
|
|
2224
|
+
nc = frozenset.union(*self.keys)
|
|
2225
|
+
# ex.insert(0, '')
|
|
2226
|
+
output.write("{}".format(len(nc) - len(exc)) + "\n")
|
|
2227
|
+
output.write("2 1 %% which columns to be x,y in phase diagram\n")
|
|
2228
|
+
output.write("\n")
|
|
2229
|
+
output.write("% Points\n")
|
|
2230
|
+
# global numbering
|
|
2231
|
+
all_points = dict()
|
|
2232
|
+
all_points_number = 0
|
|
2233
|
+
for ix, ps in self.sections.items():
|
|
2234
|
+
all_points[ix] = dict()
|
|
2235
|
+
for inv in ps.invpoints.values():
|
|
2236
|
+
all_points_number += 1
|
|
2237
|
+
all_points[ix][inv.id] = all_points_number
|
|
2238
|
+
output.write("% ------------------------------\n")
|
|
2239
|
+
output.write(
|
|
2240
|
+
"i{} {}\n".format(
|
|
2241
|
+
all_points_number, inv.label(excess=ps.excess)
|
|
2242
|
+
)
|
|
2243
|
+
)
|
|
2244
|
+
output.write("\n")
|
|
2245
|
+
output.write("{} {}\n".format(inv._y, inv._x))
|
|
2246
|
+
output.write("\n")
|
|
2247
|
+
output.write("% Lines\n")
|
|
2248
|
+
# global numbering
|
|
2249
|
+
all_lines = dict()
|
|
2250
|
+
all_lines_number = 0
|
|
2251
|
+
all_lines_topology = dict()
|
|
2252
|
+
for ix, ps in self.sections.items():
|
|
2253
|
+
all_lines[ix] = dict()
|
|
2254
|
+
for uni in ps.unilines.values():
|
|
2255
|
+
all_lines_number += 1
|
|
2256
|
+
all_lines[ix][uni.id] = all_lines_number
|
|
2257
|
+
output.write("% ------------------------------\n")
|
|
2258
|
+
output.write(
|
|
2259
|
+
"u{} {}\n".format(
|
|
2260
|
+
all_lines_number, uni.label(excess=ps.excess)
|
|
2261
|
+
)
|
|
2262
|
+
)
|
|
2263
|
+
output.write("\n")
|
|
2264
|
+
if uni.begin == 0:
|
|
2265
|
+
b1 = "begin"
|
|
2266
|
+
else:
|
|
2267
|
+
b1 = "i{}".format(all_points[ix][uni.begin])
|
|
2268
|
+
if uni.end == 0:
|
|
2269
|
+
b2 = "end"
|
|
2270
|
+
else:
|
|
2271
|
+
b2 = "i{}".format(all_points[ix][uni.end])
|
|
2272
|
+
all_lines_topology[all_lines_number] = uni
|
|
2273
|
+
if uni.manual:
|
|
2274
|
+
output.write("{} {} connect\n".format(b1, b2))
|
|
2275
|
+
output.write("\n")
|
|
2276
|
+
else:
|
|
2277
|
+
output.write("{} {}\n".format(b1, b2))
|
|
2278
|
+
output.write("\n")
|
|
2279
|
+
for p, t in zip(uni.y, uni.x):
|
|
2280
|
+
output.write("{} {}\n".format(p, t))
|
|
2281
|
+
output.write("\n")
|
|
2282
|
+
output.write("*\n")
|
|
2283
|
+
output.write("% ----------------------------------------------\n\n")
|
|
2284
|
+
if export_areas:
|
|
2285
|
+
output.write("% Areas\n")
|
|
2286
|
+
output.write("% ------------------------------\n")
|
|
2287
|
+
mxv, mnv = sys.float_info.min, sys.float_info.max
|
|
2288
|
+
for key in self.shapes:
|
|
2289
|
+
if self.variance[key] < mnv:
|
|
2290
|
+
mnv = self.variance[key]
|
|
2291
|
+
if self.variance[key] > mxv:
|
|
2292
|
+
mxv = self.variance[key]
|
|
2293
|
+
shades = np.linspace(1, 0, mxv - mnv + 3)[
|
|
2294
|
+
1:-1
|
|
2295
|
+
] # exclude extreme values
|
|
2296
|
+
for key in self.shapes:
|
|
2297
|
+
uids = [
|
|
2298
|
+
all_lines[ix][uid]
|
|
2299
|
+
for ix in self.unilists
|
|
2300
|
+
if key in self.unilists[ix]
|
|
2301
|
+
for uid in self.unilists[ix][key]
|
|
2302
|
+
if uid in all_lines[ix]
|
|
2303
|
+
]
|
|
2304
|
+
poly = linemerge([all_lines_topology[uid].shape() for uid in uids])
|
|
2305
|
+
positions = [
|
|
2306
|
+
poly.project(Point(*all_lines_topology[uid].get_label_point()))
|
|
2307
|
+
for uid in uids
|
|
2308
|
+
]
|
|
2309
|
+
orderix = sorted(range(len(positions)), key=lambda k: positions[k])
|
|
2310
|
+
d = "{:.2f} {} % {}\n".format(
|
|
2311
|
+
shades[self.variance[key] - mnv],
|
|
2312
|
+
" ".join(["u{}".format(uids[ix]) for ix in orderix]),
|
|
2313
|
+
" ".join(sorted(key)),
|
|
2314
|
+
)
|
|
2315
|
+
output.write(d)
|
|
2316
|
+
output.write("\n")
|
|
2317
|
+
output.write("*\n")
|
|
2318
|
+
output.write("\n")
|
|
2319
|
+
output.write("window {} {} {} {}\n\n".format(*self.xrange, *self.yrange))
|
|
2320
|
+
output.write("darkcolour 56 16 101\n\n")
|
|
2321
|
+
dt = self.xrange[1] - self.xrange[0]
|
|
2322
|
+
dp = self.yrange[1] - self.yrange[0]
|
|
2323
|
+
ts = np.power(10, int(np.log10(dt)))
|
|
2324
|
+
ps = np.power(10, int(np.log10(dp)))
|
|
2325
|
+
tg = np.arange(0, self.xrange[1] + ts, ts)
|
|
2326
|
+
tg = tg[tg >= self.xrange[0]]
|
|
2327
|
+
pg = np.arange(0, self.yrange[1] + ps, ps)
|
|
2328
|
+
pg = pg[pg >= self.yrange[0]]
|
|
2329
|
+
output.write(
|
|
2330
|
+
"bigticks {} {} {} {}\n\n".format(
|
|
2331
|
+
tg[1] - tg[0], tg[0], pg[1] - pg[0], pg[0]
|
|
2332
|
+
)
|
|
2333
|
+
)
|
|
2334
|
+
output.write(
|
|
2335
|
+
"smallticks {} {}\n\n".format(
|
|
2336
|
+
(tg[1] - tg[0]) / 10, (pg[1] - pg[0]) / 10
|
|
2337
|
+
)
|
|
2338
|
+
)
|
|
2339
|
+
output.write("numbering yes\n\n")
|
|
2340
|
+
if export_areas:
|
|
2341
|
+
output.write("doareas yes\n\n")
|
|
2342
|
+
output.write("*\n")
|
|
2343
|
+
print("Drawpd file generated successfully.")
|
|
2344
|
+
|
|
2345
|
+
if self.tc.drexe is not None:
|
|
2346
|
+
if self.tc.rundr():
|
|
2347
|
+
print("Drawpd sucessfully executed.")
|
|
2348
|
+
else:
|
|
2349
|
+
print("Drawpd error!")
|
|
2350
|
+
|
|
2351
|
+
def save_tab(self, comps, tabfile=None):
|
|
2352
|
+
"""Export gridded values to Perple_X tab format. Could be used in pywerami.
|
|
2353
|
+
|
|
2354
|
+
Note: Gridded values are interpolated from raw results
|
|
2355
|
+
|
|
2356
|
+
Args:
|
|
2357
|
+
comps (list): List of (phase, expr) tuples.
|
|
2358
|
+
phase (str): Phase or end-member named
|
|
2359
|
+
expr (str): Expression to evaluate. It could use any
|
|
2360
|
+
variable existing for given phase. Check
|
|
2361
|
+
`all_data_keys` property for possible
|
|
2362
|
+
variables.
|
|
2363
|
+
tabfile (str): filename for tabfile. Default pseudosection name.tab
|
|
2364
|
+
"""
|
|
2365
|
+
if not tabfile:
|
|
2366
|
+
tabfile = self.name + ".tab"
|
|
2367
|
+
data = []
|
|
2368
|
+
comps_labels = []
|
|
2369
|
+
for phase, expr in tqdm(comps, desc="Collecting data..."):
|
|
2370
|
+
data.append(self.get_gridded(phase, expr).flatten())
|
|
2371
|
+
comps_labels.append("{}({})".format(phase, expr))
|
|
2372
|
+
with Path(tabfile).open("wb") as f:
|
|
2373
|
+
head = [
|
|
2374
|
+
"ptbuilder",
|
|
2375
|
+
self.name + ".tab",
|
|
2376
|
+
"{:12d}".format(2),
|
|
2377
|
+
"T(°C)",
|
|
2378
|
+
" {:16.16f}".format(self.xrange[0])[:19],
|
|
2379
|
+
" {:16.16f}".format(self.xstep)[:19],
|
|
2380
|
+
"{:12d}".format(len(self.xspace)),
|
|
2381
|
+
"p(kbar)",
|
|
2382
|
+
" {:16.16f}".format(self.yrange[0])[:19],
|
|
2383
|
+
" {:16.16f}".format(self.ystep)[:19],
|
|
2384
|
+
"{:12d}".format(len(self.yspace)),
|
|
2385
|
+
"{:12d}".format(len(data)),
|
|
2386
|
+
(len(data) * "{:15s}").format(*comps_labels),
|
|
2387
|
+
]
|
|
2388
|
+
for ln in head:
|
|
2389
|
+
f.write(bytes(ln + "\n", "utf-8"))
|
|
2390
|
+
np.savetxt(f, np.transpose(data), fmt="%15.6f", delimiter="")
|
|
2391
|
+
print("Saved.")
|
|
2392
|
+
|
|
2393
|
+
def get_gridded(self, phase, expr=None, which=7, smooth=0):
|
|
2394
|
+
if self.gridded:
|
|
2395
|
+
if self.check_phase_expr(phase, expr):
|
|
2396
|
+
if not hasattr(self, "masks"):
|
|
2397
|
+
self.common_grid_and_masks()
|
|
2398
|
+
# interpolate on common grid
|
|
2399
|
+
recs, mn, mx = self.merge_data(phase, expr, which=which)
|
|
2400
|
+
gd = np.empty(self.xg.shape, dtype=float)
|
|
2401
|
+
gd[:] = np.nan
|
|
2402
|
+
for key in recs:
|
|
2403
|
+
xmin, ymin, xmax, ymax = self.shapes[key].bounds
|
|
2404
|
+
xxind = np.logical_and(
|
|
2405
|
+
self.xspace >= xmin - self.xstep,
|
|
2406
|
+
self.xspace <= xmax + self.xstep,
|
|
2407
|
+
)
|
|
2408
|
+
yyind = np.logical_and(
|
|
2409
|
+
self.yspace >= ymin - self.ystep,
|
|
2410
|
+
self.yspace <= ymax + self.ystep,
|
|
2411
|
+
)
|
|
2412
|
+
slc = np.ix_(yyind, xxind)
|
|
2413
|
+
tg, pg = self.xg[slc], self.yg[slc]
|
|
2414
|
+
x, y = np.array(recs[key]["pts"]).T
|
|
2415
|
+
# Use scaling
|
|
2416
|
+
try:
|
|
2417
|
+
rbf = Rbf(
|
|
2418
|
+
x,
|
|
2419
|
+
self.ratio * y,
|
|
2420
|
+
recs[key]["data"],
|
|
2421
|
+
function="linear",
|
|
2422
|
+
smooth=smooth,
|
|
2423
|
+
)
|
|
2424
|
+
zg = rbf(tg, self.ratio * pg)
|
|
2425
|
+
gd[self.masks[key]] = zg[self.masks[key][slc]]
|
|
2426
|
+
except Exception:
|
|
2427
|
+
pass # pass silently
|
|
2428
|
+
return gd
|
|
2429
|
+
else:
|
|
2430
|
+
print("Not yet gridded...")
|
|
2431
|
+
|
|
2432
|
+
def get_grids(self, phase, expr=None):
|
|
2433
|
+
"""Convinient function to get dictionary of grids.
|
|
2434
|
+
|
|
2435
|
+
Args:
|
|
2436
|
+
phase (str): Phase or end-member named
|
|
2437
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
2438
|
+
existing for given phase. Check `all_data_keys` property for
|
|
2439
|
+
possible variables.
|
|
2440
|
+
"""
|
|
2441
|
+
if self.gridded:
|
|
2442
|
+
if self.check_phase_expr(phase, expr):
|
|
2443
|
+
cgd = {}
|
|
2444
|
+
for ix, grid in self.grids.items():
|
|
2445
|
+
gd = np.empty(grid.xg.shape)
|
|
2446
|
+
gd[:] = np.nan
|
|
2447
|
+
for key in grid.masks:
|
|
2448
|
+
results = grid.gridcalcs[grid.masks[key] & (grid.status == 1)]
|
|
2449
|
+
if len(results) > 0:
|
|
2450
|
+
if phase in results[0].phases:
|
|
2451
|
+
rows, cols = np.nonzero(grid.masks[key])
|
|
2452
|
+
for r, c in zip(rows, cols):
|
|
2453
|
+
if grid.status[r, c] == 1:
|
|
2454
|
+
gd[r, c] = eval_expr(
|
|
2455
|
+
expr, grid.gridcalcs[r, c][phase]
|
|
2456
|
+
)
|
|
2457
|
+
cgd[ix] = gd
|
|
2458
|
+
return cgd
|
|
2459
|
+
|
|
2460
|
+
|
|
2461
|
+
class PTPS(PS):
|
|
2462
|
+
"""Class to postprocess ptbuilder project"""
|
|
2463
|
+
|
|
2464
|
+
def __init__(self, *args, **kwargs):
|
|
2465
|
+
self.section_class = PTsection
|
|
2466
|
+
super(PTPS, self).__init__(*args, **kwargs)
|
|
2467
|
+
|
|
2468
|
+
def calculate_composition(self, nx=50, ny=50):
|
|
2469
|
+
"""Method to calculate compositional variations on grid.
|
|
2470
|
+
|
|
2471
|
+
A compositions are calculated for stable assemblages in regular grid
|
|
2472
|
+
covering pT range of pseudosection. A stable assemblage is identified
|
|
2473
|
+
from constructed divariant fields. Results are stored in `grid` property
|
|
2474
|
+
as `GridData` instance. A property `all_data_keys` is updated.
|
|
2475
|
+
|
|
2476
|
+
Before any grid point calculation, ptguesses are updated from nearest
|
|
2477
|
+
invariant point. If calculation fails, nearest solution from univariant
|
|
2478
|
+
line is used to update ptguesses. Finally, if solution is still not found,
|
|
2479
|
+
the method `fix_solutions` is called and neigbouring grid calculations are
|
|
2480
|
+
used to provide ptguess.
|
|
2481
|
+
|
|
2482
|
+
Args:
|
|
2483
|
+
nx (int): Number of grid points along x direction (T)
|
|
2484
|
+
ny (int): Number of grid points along y direction (p)
|
|
2485
|
+
"""
|
|
2486
|
+
axr = self.xrange
|
|
2487
|
+
ayr = self.yrange
|
|
2488
|
+
gpleft = 0
|
|
2489
|
+
for ix, ps in self.sections.items():
|
|
2490
|
+
paxr = ps.xrange
|
|
2491
|
+
payr = ps.yrange
|
|
2492
|
+
grid = GridData(
|
|
2493
|
+
ps,
|
|
2494
|
+
nx=round(nx * (paxr[1] - paxr[0]) / (axr[1] - axr[0])),
|
|
2495
|
+
ny=round(ny * (payr[1] - payr[0]) / (ayr[1] - ayr[0])),
|
|
2496
|
+
)
|
|
2497
|
+
last_inv = 0
|
|
2498
|
+
for r, c in tqdm(
|
|
2499
|
+
np.ndindex(grid.xg.shape),
|
|
2500
|
+
desc="Gridding {}/{}".format(ix + 1, len(self.sections)),
|
|
2501
|
+
total=np.prod(grid.xg.shape),
|
|
2502
|
+
):
|
|
2503
|
+
x, y = grid.xg[r, c], grid.yg[r, c]
|
|
2504
|
+
k = self.identify(x, y)
|
|
2505
|
+
if k is not None:
|
|
2506
|
+
# update guesses from closest inv point
|
|
2507
|
+
dst = sys.float_info.max
|
|
2508
|
+
for id_inv, inv in ps.invpoints.items():
|
|
2509
|
+
d2 = (inv._x - x) ** 2 + (inv._y - y) ** 2
|
|
2510
|
+
if d2 < dst:
|
|
2511
|
+
dst = d2
|
|
2512
|
+
id_close = id_inv
|
|
2513
|
+
if id_close != last_inv and not ps.invpoints[id_close].manual:
|
|
2514
|
+
self.tc.update_scriptfile(
|
|
2515
|
+
guesses=ps.invpoints[id_close].ptguess()
|
|
2516
|
+
)
|
|
2517
|
+
last_inv = id_close
|
|
2518
|
+
grid.status[r, c] = 0
|
|
2519
|
+
start_time = time.time()
|
|
2520
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2521
|
+
k.difference(self.tc.excess), y, x
|
|
2522
|
+
)
|
|
2523
|
+
delta = time.time() - start_time
|
|
2524
|
+
status, res, output = self.tc.parse_logfile()
|
|
2525
|
+
if res is not None:
|
|
2526
|
+
grid.gridcalcs[r, c] = res[0]
|
|
2527
|
+
grid.status[r, c] = 1
|
|
2528
|
+
grid.delta[r, c] = delta
|
|
2529
|
+
else:
|
|
2530
|
+
# update guesses from closest uni line point
|
|
2531
|
+
dst = sys.float_info.max
|
|
2532
|
+
for id_uni in self.unilists[ix][k]:
|
|
2533
|
+
uni = ps.unilines[id_uni]
|
|
2534
|
+
if not uni.manual:
|
|
2535
|
+
for vix in list(range(len(uni._x))[uni.used]):
|
|
2536
|
+
d2 = (uni._x[vix] - x) ** 2 + (uni._y[vix] - y) ** 2
|
|
2537
|
+
if d2 < dst:
|
|
2538
|
+
dst = d2
|
|
2539
|
+
id_close = id_uni
|
|
2540
|
+
vix_close = vix
|
|
2541
|
+
self.tc.update_scriptfile(
|
|
2542
|
+
guesses=ps.unilines[id_close].ptguess(idx=vix_close)
|
|
2543
|
+
)
|
|
2544
|
+
start_time = time.time()
|
|
2545
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2546
|
+
k.difference(self.tc.excess), y, x
|
|
2547
|
+
)
|
|
2548
|
+
delta = time.time() - start_time
|
|
2549
|
+
status, res, output = self.tc.parse_logfile()
|
|
2550
|
+
if res is not None:
|
|
2551
|
+
grid.gridcalcs[r, c] = res[0]
|
|
2552
|
+
grid.status[r, c] = 1
|
|
2553
|
+
grid.delta[r, c] = delta
|
|
2554
|
+
else:
|
|
2555
|
+
grid.gridcalcs[r, c] = None
|
|
2556
|
+
grid.status[r, c] = 0
|
|
2557
|
+
else:
|
|
2558
|
+
grid.gridcalcs[r, c] = None
|
|
2559
|
+
print(
|
|
2560
|
+
"Grid search done. {} empty points left.".format(
|
|
2561
|
+
len(np.flatnonzero(grid.status == 0))
|
|
2562
|
+
)
|
|
2563
|
+
)
|
|
2564
|
+
gpleft += len(np.flatnonzero(grid.status == 0))
|
|
2565
|
+
self.grids[ix] = grid
|
|
2566
|
+
if gpleft > 0:
|
|
2567
|
+
self.fix_solutions()
|
|
2568
|
+
self.create_masks()
|
|
2569
|
+
# save
|
|
2570
|
+
self.save()
|
|
2571
|
+
# update variable lookup table
|
|
2572
|
+
self.collect_all_data_keys()
|
|
2573
|
+
|
|
2574
|
+
def fix_solutions(self):
|
|
2575
|
+
"""Method try to find solution for grid points with failed status.
|
|
2576
|
+
|
|
2577
|
+
Ptguesses are used from successfully calculated neighboring points until
|
|
2578
|
+
solution is find. Otherwise ststus remains failed.
|
|
2579
|
+
"""
|
|
2580
|
+
if self.gridded:
|
|
2581
|
+
for ix, grid in self.grids.items():
|
|
2582
|
+
log = []
|
|
2583
|
+
ri, ci = np.nonzero(grid.status == 0)
|
|
2584
|
+
fixed, ftot = 0, len(ri)
|
|
2585
|
+
tq = trange(ftot, desc="Fix ({}/{})".format(fixed, ftot))
|
|
2586
|
+
for ind in tq:
|
|
2587
|
+
r, c = ri[ind], ci[ind]
|
|
2588
|
+
x, y = grid.xg[r, c], grid.yg[r, c]
|
|
2589
|
+
k = self.identify(x, y)
|
|
2590
|
+
if k is not None:
|
|
2591
|
+
# search already done grid neighs
|
|
2592
|
+
for rn, cn in grid.neighs(r, c):
|
|
2593
|
+
if grid.status[rn, cn] == 1:
|
|
2594
|
+
self.tc.update_scriptfile(
|
|
2595
|
+
guesses=grid.gridcalcs[rn, cn].ptguess
|
|
2596
|
+
)
|
|
2597
|
+
start_time = time.time()
|
|
2598
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2599
|
+
k.difference(self.tc.excess), y, x
|
|
2600
|
+
)
|
|
2601
|
+
delta = time.time() - start_time
|
|
2602
|
+
status, res, output = self.tc.parse_logfile()
|
|
2603
|
+
if res is not None:
|
|
2604
|
+
grid.gridcalcs[r, c] = res[0]
|
|
2605
|
+
grid.status[r, c] = 1
|
|
2606
|
+
grid.delta[r, c] = delta
|
|
2607
|
+
fixed += 1
|
|
2608
|
+
tq.set_description(
|
|
2609
|
+
desc="Fix ({}/{})".format(fixed, ftot)
|
|
2610
|
+
)
|
|
2611
|
+
break
|
|
2612
|
+
if grid.status[r, c] == 0:
|
|
2613
|
+
log.append("No solution find for {}, {}".format(x, y))
|
|
2614
|
+
log.append(
|
|
2615
|
+
"Fix done. {} empty grid points left.".format(
|
|
2616
|
+
len(np.flatnonzero(grid.status == 0))
|
|
2617
|
+
)
|
|
2618
|
+
)
|
|
2619
|
+
print("\n".join(log))
|
|
2620
|
+
else:
|
|
2621
|
+
print("Not yet gridded...")
|
|
2622
|
+
|
|
2623
|
+
def collect_ptpath(self, tpath, ppath, N=100, kind="quadratic"):
|
|
2624
|
+
"""Method to collect THERMOCALC calculations along defined PT path.
|
|
2625
|
+
|
|
2626
|
+
PT path is interpolated from provided points using defined method. For
|
|
2627
|
+
each point THERMOCALC seek for solution using ptguess from nearest
|
|
2628
|
+
`GridData` point.
|
|
2629
|
+
|
|
2630
|
+
Args:
|
|
2631
|
+
tpath (numpy.array): 1D array of temperatures for given PT path
|
|
2632
|
+
ppath (numpy.array): 1D array of pressures for given PT path
|
|
2633
|
+
N (int): Number of calculation steps. Default 100.
|
|
2634
|
+
kind (str): Kind of interpolation. See scipy.interpolate.interp1d
|
|
2635
|
+
|
|
2636
|
+
Returns:
|
|
2637
|
+
PTpath: returns instance of PTpath class storing all calculations
|
|
2638
|
+
along PT path.
|
|
2639
|
+
"""
|
|
2640
|
+
if self.gridded:
|
|
2641
|
+
tpath, ppath = np.asarray(tpath), np.asarray(ppath)
|
|
2642
|
+
assert (
|
|
2643
|
+
tpath.shape == ppath.shape
|
|
2644
|
+
), "Shape of temperatures and pressures should be same."
|
|
2645
|
+
assert (
|
|
2646
|
+
tpath.ndim == 1
|
|
2647
|
+
), "Temperatures and pressures should be 1D array like data."
|
|
2648
|
+
gpath = np.arange(tpath.shape[0], dtype=float)
|
|
2649
|
+
gpath /= gpath[-1]
|
|
2650
|
+
if gpath.size < 3:
|
|
2651
|
+
kind = "linear"
|
|
2652
|
+
splt = interp1d(gpath, tpath, kind=kind)
|
|
2653
|
+
splp = interp1d(gpath, ppath, kind=kind)
|
|
2654
|
+
err = 0
|
|
2655
|
+
points, results = [], []
|
|
2656
|
+
for step in tqdm(np.linspace(0, 1, N), desc="Calculating"):
|
|
2657
|
+
t, p = splt(step), splp(step)
|
|
2658
|
+
key = self.identify(t, p)
|
|
2659
|
+
ix = self.get_section_id(t, p)
|
|
2660
|
+
if (ix is not None) and (key is not None):
|
|
2661
|
+
r, c = self.grids[ix].get_indexes(t, p)
|
|
2662
|
+
calc = None
|
|
2663
|
+
if self.grids[ix].status[r, c] == 1:
|
|
2664
|
+
calc = self.grids[ix].gridcalcs[r, c]
|
|
2665
|
+
else:
|
|
2666
|
+
for rn, cn in self.grids[ix].neighs(r, c):
|
|
2667
|
+
if self.grids[ix].status[rn, cn] == 1:
|
|
2668
|
+
calc = self.grids[ix].gridcalcs[rn, cn]
|
|
2669
|
+
break
|
|
2670
|
+
if calc is not None:
|
|
2671
|
+
self.tc.update_scriptfile(guesses=calc.ptguess)
|
|
2672
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2673
|
+
key.difference(self.tc.excess), p, t
|
|
2674
|
+
)
|
|
2675
|
+
status, res, output = self.tc.parse_logfile()
|
|
2676
|
+
if res is not None:
|
|
2677
|
+
points.append((t, p))
|
|
2678
|
+
results.append(res[0])
|
|
2679
|
+
else:
|
|
2680
|
+
err += 1
|
|
2681
|
+
else:
|
|
2682
|
+
err += 1
|
|
2683
|
+
if err > 0:
|
|
2684
|
+
print("Solution not found on {} points".format(err))
|
|
2685
|
+
if points:
|
|
2686
|
+
return PTpath(points, results)
|
|
2687
|
+
else:
|
|
2688
|
+
print("No results collected.")
|
|
2689
|
+
else:
|
|
2690
|
+
print("Not yet gridded...")
|
|
2691
|
+
|
|
2692
|
+
def show_path_data(
|
|
2693
|
+
self,
|
|
2694
|
+
ptpath,
|
|
2695
|
+
phase,
|
|
2696
|
+
expr=None,
|
|
2697
|
+
label=False,
|
|
2698
|
+
pathwidth=4,
|
|
2699
|
+
allpath=True,
|
|
2700
|
+
skiplabels=0,
|
|
2701
|
+
labelfs=6,
|
|
2702
|
+
):
|
|
2703
|
+
"""Show values of expression for given phase calculated along PTpath.
|
|
2704
|
+
|
|
2705
|
+
It plots colored strip on PT space. Strips arenot drawn accross fields,
|
|
2706
|
+
where 'phase' is not present.
|
|
2707
|
+
|
|
2708
|
+
Args:
|
|
2709
|
+
ptpath (PTpath): Results obtained by `collect_ptpath` method.
|
|
2710
|
+
phase (str): Phase or end-member named
|
|
2711
|
+
expr (str): Expression to evaluate. It could use any variable
|
|
2712
|
+
existing for given phase. Check `all_data_keys` property for
|
|
2713
|
+
possible variables.
|
|
2714
|
+
label (bool): Whether to label divariant fields. Default False.
|
|
2715
|
+
skiplabels (float): Minimal area fraction of fields to be labelled
|
|
2716
|
+
labelfs (float): Size of label font. Default 6
|
|
2717
|
+
pathwidth (int): Width of colored strip. Default 4.
|
|
2718
|
+
allpath (bool): Whether to plot full PT path (dashed line).
|
|
2719
|
+
"""
|
|
2720
|
+
if self.check_phase_expr(phase, expr):
|
|
2721
|
+
ex = ptpath.get_path_data(phase, expr)
|
|
2722
|
+
fig, ax = plt.subplots()
|
|
2723
|
+
if allpath:
|
|
2724
|
+
ax.plot(ptpath.t, ptpath.p, "--", color="grey", lw=1)
|
|
2725
|
+
# Create a continuous norm to map from data points to colors
|
|
2726
|
+
norm = Normalize(np.nanmin(ex), np.nanmax(ex))
|
|
2727
|
+
|
|
2728
|
+
for s in np.ma.clump_unmasked(np.ma.masked_invalid(ex)):
|
|
2729
|
+
ts, ps, exs = ptpath.t[s], ptpath.p[s], ex[s]
|
|
2730
|
+
points = np.array([ts, ps]).T.reshape(-1, 1, 2)
|
|
2731
|
+
segments = np.concatenate([points[:-1], points[1:]], axis=1)
|
|
2732
|
+
lc = LineCollection(segments, cmap="viridis", norm=norm)
|
|
2733
|
+
# Set the values used for colormapping
|
|
2734
|
+
lc.set_array(exs)
|
|
2735
|
+
lc.set_linewidth(pathwidth)
|
|
2736
|
+
line = ax.add_collection(lc)
|
|
2737
|
+
self.add_overlay(
|
|
2738
|
+
ax, label=label, skiplabels=skiplabels, fontsize=labelfs
|
|
2739
|
+
)
|
|
2740
|
+
cbar = fig.colorbar(line, ax=ax)
|
|
2741
|
+
cbar.set_label("{}[{}]".format(phase, expr))
|
|
2742
|
+
ax.set_xlim(self.xrange)
|
|
2743
|
+
ax.set_ylim(self.yrange)
|
|
2744
|
+
ax.set_title("PT path - {}".format(self.name))
|
|
2745
|
+
plt.show()
|
|
2746
|
+
|
|
2747
|
+
def show_path_modes(self, ptpath, exclude=[], cmap="tab20"):
|
|
2748
|
+
"""Show stacked area diagram of phase modes along PT path
|
|
2749
|
+
|
|
2750
|
+
Args:
|
|
2751
|
+
ptpath (PTpath): Results obtained by `collect_ptpath` method.
|
|
2752
|
+
exclude (list): List of phases to exclude. Included phases area
|
|
2753
|
+
normalized to 100%.
|
|
2754
|
+
cmap (str): matplotlib colormap. Default 'tab20'
|
|
2755
|
+
"""
|
|
2756
|
+
if not isinstance(exclude, list):
|
|
2757
|
+
exclude = [exclude]
|
|
2758
|
+
steps = len(ptpath.t)
|
|
2759
|
+
nd = np.linspace(0, 1, steps)
|
|
2760
|
+
splt = interp1d(nd, ptpath.t, kind="quadratic")
|
|
2761
|
+
splp = interp1d(nd, ptpath.p, kind="quadratic")
|
|
2762
|
+
pset = set()
|
|
2763
|
+
for res in ptpath.results:
|
|
2764
|
+
for key in res.phases:
|
|
2765
|
+
if key not in exclude and "mode" in res[key]:
|
|
2766
|
+
pset.add(key)
|
|
2767
|
+
phases = sorted(list(pset))
|
|
2768
|
+
modes = np.array(
|
|
2769
|
+
[
|
|
2770
|
+
[
|
|
2771
|
+
res[phase]["mode"] if phase in res.phases else 0
|
|
2772
|
+
for res in ptpath.results
|
|
2773
|
+
]
|
|
2774
|
+
for phase in phases
|
|
2775
|
+
]
|
|
2776
|
+
)
|
|
2777
|
+
modes = 100 * modes / modes.sum(axis=0)
|
|
2778
|
+
cm = plt.get_cmap(cmap)
|
|
2779
|
+
fig, ax = plt.subplots(figsize=(12, 5))
|
|
2780
|
+
ax.set_prop_cycle(color=[cm(i / len(phases)) for i in range(len(phases))])
|
|
2781
|
+
bottom = np.zeros_like(modes[0])
|
|
2782
|
+
bars = []
|
|
2783
|
+
for n, mode in enumerate(modes):
|
|
2784
|
+
h = ax.bar(nd, mode, bottom=bottom, width=nd[1] - nd[0])
|
|
2785
|
+
bars.append(h[0])
|
|
2786
|
+
bottom += mode
|
|
2787
|
+
|
|
2788
|
+
ax.format_coord = lambda x, y: "T={:.2f} p={:.2f}".format(splt(x), splp(x))
|
|
2789
|
+
ax.set_xlim(0, 1)
|
|
2790
|
+
ax.set_xlabel("Normalized distance along path")
|
|
2791
|
+
ax.set_ylabel("Mode [%]")
|
|
2792
|
+
box = ax.get_position()
|
|
2793
|
+
ax.set_position([box.x0, box.y0, box.width * 0.9, box.height])
|
|
2794
|
+
# Put a legend to the right of the current axis
|
|
2795
|
+
ax.legend(
|
|
2796
|
+
bars, phases, fancybox=True, loc="center left", bbox_to_anchor=(1.05, 0.5)
|
|
2797
|
+
)
|
|
2798
|
+
plt.show()
|
|
2799
|
+
|
|
2800
|
+
|
|
2801
|
+
class TXPS(PS):
|
|
2802
|
+
"""Class to postprocess txbuilder project"""
|
|
2803
|
+
|
|
2804
|
+
def __init__(self, *args, **kwargs):
|
|
2805
|
+
self.section_class = TXsection
|
|
2806
|
+
super(TXPS, self).__init__(*args, **kwargs)
|
|
2807
|
+
|
|
2808
|
+
def calculate_composition(self, nx=50, ny=50):
|
|
2809
|
+
"""Method to calculate compositional variations on grid.
|
|
2810
|
+
|
|
2811
|
+
A compositions are calculated for stable assemblages in regular grid
|
|
2812
|
+
covering pT range of pseudosection. A stable assemblage is identified
|
|
2813
|
+
from constructed divariant fields. Results are stored in `grid` property
|
|
2814
|
+
as `GridData` instance. A property `all_data_keys` is updated.
|
|
2815
|
+
|
|
2816
|
+
Before any grid point calculation, ptguesses are updated from nearest
|
|
2817
|
+
invariant point. If calculation fails, nearest solution from univariant
|
|
2818
|
+
line is used to update ptguesses. Finally, if solution is still not found,
|
|
2819
|
+
the method `fix_solutions` is called and neigbouring grid calculations are
|
|
2820
|
+
used to provide ptguess.
|
|
2821
|
+
|
|
2822
|
+
Args:
|
|
2823
|
+
nx (int): Number of grid points along x direction (T)
|
|
2824
|
+
ny (int): Number of grid points along y direction (p)
|
|
2825
|
+
"""
|
|
2826
|
+
axr = self.xrange
|
|
2827
|
+
ayr = self.yrange
|
|
2828
|
+
gpleft = 0
|
|
2829
|
+
for ix, ps in self.sections.items():
|
|
2830
|
+
paxr = ps.xrange
|
|
2831
|
+
payr = ps.yrange
|
|
2832
|
+
grid = GridData(
|
|
2833
|
+
ps,
|
|
2834
|
+
nx=round(nx * (paxr[1] - paxr[0]) / (axr[1] - axr[0])),
|
|
2835
|
+
ny=round(ny * (payr[1] - payr[0]) / (ayr[1] - ayr[0])),
|
|
2836
|
+
)
|
|
2837
|
+
last_inv = 0
|
|
2838
|
+
with tqdm(
|
|
2839
|
+
desc="Gridding {}/{}".format(ix + 1, len(self.sections)),
|
|
2840
|
+
total=np.prod(grid.xg.shape),
|
|
2841
|
+
) as pbar:
|
|
2842
|
+
pm = (self.tc.prange[0] + self.tc.prange[1]) / 2
|
|
2843
|
+
for r in range(len(grid.yspace)):
|
|
2844
|
+
for c in range(len(grid.xspace)):
|
|
2845
|
+
x, y = grid.xg[r, c], grid.yg[r, c]
|
|
2846
|
+
k = self.identify(x, y)
|
|
2847
|
+
if k is not None:
|
|
2848
|
+
# update guesses from closest inv point
|
|
2849
|
+
dst = sys.float_info.max
|
|
2850
|
+
for id_inv, inv in ps.invpoints.items():
|
|
2851
|
+
d2 = (inv._x - x) ** 2 + (inv._y - y) ** 2
|
|
2852
|
+
if d2 < dst:
|
|
2853
|
+
dst = d2
|
|
2854
|
+
id_close = id_inv
|
|
2855
|
+
if (
|
|
2856
|
+
id_close != last_inv
|
|
2857
|
+
and not ps.invpoints[id_close].manual
|
|
2858
|
+
):
|
|
2859
|
+
self.tc.update_scriptfile(
|
|
2860
|
+
guesses=ps.invpoints[id_close].ptguess()
|
|
2861
|
+
)
|
|
2862
|
+
last_inv = id_close
|
|
2863
|
+
grid.status[r, c] = 0
|
|
2864
|
+
start_time = time.time()
|
|
2865
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2866
|
+
k.difference(self.tc.excess), pm, x, onebulk=y
|
|
2867
|
+
)
|
|
2868
|
+
delta = time.time() - start_time
|
|
2869
|
+
status, res, output = self.tc.parse_logfile()
|
|
2870
|
+
if res is not None:
|
|
2871
|
+
grid.gridcalcs[r, c] = res[0]
|
|
2872
|
+
grid.status[r, c] = 1
|
|
2873
|
+
grid.delta[r, c] = delta
|
|
2874
|
+
else:
|
|
2875
|
+
# update guesses from closest uni line point
|
|
2876
|
+
dst = sys.float_info.max
|
|
2877
|
+
for id_uni in self.unilists[ix][k]:
|
|
2878
|
+
uni = ps.unilines[id_uni]
|
|
2879
|
+
if not uni.manual:
|
|
2880
|
+
for vix in list(range(len(uni._x))[uni.used]):
|
|
2881
|
+
d2 = (uni._x[vix] - x) ** 2 + (
|
|
2882
|
+
uni._y[vix] - y
|
|
2883
|
+
) ** 2
|
|
2884
|
+
if d2 < dst:
|
|
2885
|
+
dst = d2
|
|
2886
|
+
id_close = id_uni
|
|
2887
|
+
vix_close = vix
|
|
2888
|
+
self.tc.update_scriptfile(
|
|
2889
|
+
guesses=ps.unilines[id_close].ptguess(idx=vix_close)
|
|
2890
|
+
)
|
|
2891
|
+
start_time = time.time()
|
|
2892
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2893
|
+
k.difference(self.tc.excess), pm, x, onebulk=y
|
|
2894
|
+
)
|
|
2895
|
+
delta = time.time() - start_time
|
|
2896
|
+
status, res, output = self.tc.parse_logfile()
|
|
2897
|
+
if res is not None:
|
|
2898
|
+
grid.gridcalcs[r, c] = res[0]
|
|
2899
|
+
grid.status[r, c] = 1
|
|
2900
|
+
grid.delta[r, c] = delta
|
|
2901
|
+
else:
|
|
2902
|
+
grid.gridcalcs[r, c] = None
|
|
2903
|
+
grid.status[r, c] = 0
|
|
2904
|
+
else:
|
|
2905
|
+
grid.gridcalcs[r, c] = None
|
|
2906
|
+
pbar.update(1)
|
|
2907
|
+
print(
|
|
2908
|
+
"Grid search done. {} empty points left.".format(
|
|
2909
|
+
len(np.flatnonzero(grid.status == 0))
|
|
2910
|
+
)
|
|
2911
|
+
)
|
|
2912
|
+
gpleft += len(np.flatnonzero(grid.status == 0))
|
|
2913
|
+
self.grids[ix] = grid
|
|
2914
|
+
if gpleft > 0:
|
|
2915
|
+
self.fix_solutions()
|
|
2916
|
+
self.create_masks()
|
|
2917
|
+
# update variable lookup table
|
|
2918
|
+
self.collect_all_data_keys()
|
|
2919
|
+
# save
|
|
2920
|
+
self.save()
|
|
2921
|
+
|
|
2922
|
+
def fix_solutions(self):
|
|
2923
|
+
"""Method try to find solution for grid points with failed status.
|
|
2924
|
+
|
|
2925
|
+
Ptguesses are used from successfully calculated neighboring points until
|
|
2926
|
+
solution is find. Otherwise ststus remains failed.
|
|
2927
|
+
"""
|
|
2928
|
+
if self.gridded:
|
|
2929
|
+
for ix, grid in self.grids.items():
|
|
2930
|
+
log = []
|
|
2931
|
+
ri, ci = np.nonzero(grid.status == 0)
|
|
2932
|
+
fixed, ftot = 0, len(ri)
|
|
2933
|
+
pm = (self.tc.prange[0] + self.tc.prange[1]) / 2
|
|
2934
|
+
tq = trange(ftot, desc="Fix ({}/{})".format(fixed, ftot))
|
|
2935
|
+
for ind in tq:
|
|
2936
|
+
r, c = ri[ind], ci[ind]
|
|
2937
|
+
x, y = grid.xg[r, c], grid.yg[r, c]
|
|
2938
|
+
k = self.identify(x, y)
|
|
2939
|
+
if k is not None:
|
|
2940
|
+
# search already done grid neighs
|
|
2941
|
+
for rn, cn in grid.neighs(r, c):
|
|
2942
|
+
if grid.status[rn, cn] == 1:
|
|
2943
|
+
self.tc.update_scriptfile(
|
|
2944
|
+
guesses=grid.gridcalcs[rn, cn]["ptguess"]
|
|
2945
|
+
)
|
|
2946
|
+
start_time = time.time()
|
|
2947
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
2948
|
+
k.difference(self.tc.excess), pm, x, onebulk=y
|
|
2949
|
+
)
|
|
2950
|
+
delta = time.time() - start_time
|
|
2951
|
+
status, res, output = self.tc.parse_logfile()
|
|
2952
|
+
if res is not None:
|
|
2953
|
+
grid.gridcalcs[r, c] = res[0]
|
|
2954
|
+
grid.status[r, c] = 1
|
|
2955
|
+
grid.delta[r, c] = delta
|
|
2956
|
+
fixed += 1
|
|
2957
|
+
tq.set_description(
|
|
2958
|
+
desc="Fix ({}/{})".format(fixed, ftot)
|
|
2959
|
+
)
|
|
2960
|
+
break
|
|
2961
|
+
if grid.status[r, c] == 0:
|
|
2962
|
+
log.append("No solution find for {}, {}".format(x, y))
|
|
2963
|
+
log.append(
|
|
2964
|
+
"Fix done. {} empty grid points left.".format(
|
|
2965
|
+
len(np.flatnonzero(grid.status == 0))
|
|
2966
|
+
)
|
|
2967
|
+
)
|
|
2968
|
+
print("\n".join(log))
|
|
2969
|
+
else:
|
|
2970
|
+
print("Not yet gridded...")
|
|
2971
|
+
|
|
2972
|
+
|
|
2973
|
+
class PXPS(PS):
|
|
2974
|
+
"""Class to postprocess pxbuilder project"""
|
|
2975
|
+
|
|
2976
|
+
def __init__(self, *args, **kwargs):
|
|
2977
|
+
self.section_class = PXsection
|
|
2978
|
+
super(PXPS, self).__init__(*args, **kwargs)
|
|
2979
|
+
|
|
2980
|
+
def calculate_composition(self, nx=50, ny=50):
|
|
2981
|
+
"""Method to calculate compositional variations on grid.
|
|
2982
|
+
|
|
2983
|
+
A compositions are calculated for stable assemblages in regular grid
|
|
2984
|
+
covering pT range of pseudosection. A stable assemblage is identified
|
|
2985
|
+
from constructed divariant fields. Results are stored in `grid` property
|
|
2986
|
+
as `GridData` instance. A property `all_data_keys` is updated.
|
|
2987
|
+
|
|
2988
|
+
Before any grid point calculation, ptguesses are updated from nearest
|
|
2989
|
+
invariant point. If calculation fails, nearest solution from univariant
|
|
2990
|
+
line is used to update ptguesses. Finally, if solution is still not found,
|
|
2991
|
+
the method `fix_solutions` is called and neigbouring grid calculations are
|
|
2992
|
+
used to provide ptguess.
|
|
2993
|
+
|
|
2994
|
+
Args:
|
|
2995
|
+
nx (int): Number of grid points along x direction (T)
|
|
2996
|
+
ny (int): Number of grid points along y direction (p)
|
|
2997
|
+
"""
|
|
2998
|
+
axr = self.xrange
|
|
2999
|
+
ayr = self.yrange
|
|
3000
|
+
gpleft = 0
|
|
3001
|
+
for ix, ps in self.sections.items():
|
|
3002
|
+
paxr = ps.xrange
|
|
3003
|
+
payr = ps.yrange
|
|
3004
|
+
grid = GridData(
|
|
3005
|
+
ps,
|
|
3006
|
+
nx=round(nx * (paxr[1] - paxr[0]) / (axr[1] - axr[0])),
|
|
3007
|
+
ny=round(ny * (payr[1] - payr[0]) / (ayr[1] - ayr[0])),
|
|
3008
|
+
)
|
|
3009
|
+
last_inv = 0
|
|
3010
|
+
with tqdm(desc="Gridding", total=np.prod(grid.xg.shape)) as pbar:
|
|
3011
|
+
tm = (self.tc.trange[0] + self.tc.trange[1]) / 2
|
|
3012
|
+
for c in range(len(grid.xspace)):
|
|
3013
|
+
for r in range(len(grid.yspace)):
|
|
3014
|
+
x, y = grid.xg[r, c], grid.yg[r, c]
|
|
3015
|
+
k = self.identify(x, y)
|
|
3016
|
+
if k is not None:
|
|
3017
|
+
# update guesses from closest inv point
|
|
3018
|
+
dst = sys.float_info.max
|
|
3019
|
+
for id_inv, inv in ps.invpoints.items():
|
|
3020
|
+
d2 = (inv._x - x) ** 2 + (inv._y - y) ** 2
|
|
3021
|
+
if d2 < dst:
|
|
3022
|
+
dst = d2
|
|
3023
|
+
id_close = id_inv
|
|
3024
|
+
if (
|
|
3025
|
+
id_close != last_inv
|
|
3026
|
+
and not ps.invpoints[id_close].manual
|
|
3027
|
+
):
|
|
3028
|
+
self.tc.update_scriptfile(
|
|
3029
|
+
guesses=ps.invpoints[id_close].ptguess()
|
|
3030
|
+
)
|
|
3031
|
+
last_inv = id_close
|
|
3032
|
+
grid.status[r, c] = 0
|
|
3033
|
+
start_time = time.time()
|
|
3034
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
3035
|
+
k.difference(self.tc.excess), y, tm, onebulk=x
|
|
3036
|
+
)
|
|
3037
|
+
delta = time.time() - start_time
|
|
3038
|
+
status, res, output = self.tc.parse_logfile()
|
|
3039
|
+
if res is not None:
|
|
3040
|
+
grid.gridcalcs[r, c] = res[0]
|
|
3041
|
+
grid.status[r, c] = 1
|
|
3042
|
+
grid.delta[r, c] = delta
|
|
3043
|
+
else:
|
|
3044
|
+
# update guesses from closest uni line point
|
|
3045
|
+
dst = sys.float_info.max
|
|
3046
|
+
for id_uni in self.unilists[ix][k]:
|
|
3047
|
+
uni = ps.unilines[id_uni]
|
|
3048
|
+
if not uni.manual:
|
|
3049
|
+
for vix in list(range(len(uni._x))[uni.used]):
|
|
3050
|
+
d2 = (uni._x[vix] - x) ** 2 + (
|
|
3051
|
+
uni._y[vix] - y
|
|
3052
|
+
) ** 2
|
|
3053
|
+
if d2 < dst:
|
|
3054
|
+
dst = d2
|
|
3055
|
+
id_close = id_uni
|
|
3056
|
+
vix_close = vix
|
|
3057
|
+
self.tc.update_scriptfile(
|
|
3058
|
+
guesses=ps.unilines[id_close].ptguess(idx=vix_close)
|
|
3059
|
+
)
|
|
3060
|
+
start_time = time.time()
|
|
3061
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
3062
|
+
k.difference(self.tc.excess), y, tm, onebulk=x
|
|
3063
|
+
)
|
|
3064
|
+
delta = time.time() - start_time
|
|
3065
|
+
status, res, output = self.tc.parse_logfile()
|
|
3066
|
+
if res is not None:
|
|
3067
|
+
grid.gridcalcs[r, c] = res[0]
|
|
3068
|
+
grid.status[r, c] = 1
|
|
3069
|
+
grid.delta[r, c] = delta
|
|
3070
|
+
else:
|
|
3071
|
+
grid.gridcalcs[r, c] = None
|
|
3072
|
+
grid.status[r, c] = 0
|
|
3073
|
+
else:
|
|
3074
|
+
grid.gridcalcs[r, c] = None
|
|
3075
|
+
pbar.update(1)
|
|
3076
|
+
print(
|
|
3077
|
+
"Grid search done. {} empty points left.".format(
|
|
3078
|
+
len(np.flatnonzero(grid.status == 0))
|
|
3079
|
+
)
|
|
3080
|
+
)
|
|
3081
|
+
gpleft += len(np.flatnonzero(grid.status == 0))
|
|
3082
|
+
self.grids[ix] = grid
|
|
3083
|
+
if gpleft > 0:
|
|
3084
|
+
self.fix_solutions()
|
|
3085
|
+
self.create_masks()
|
|
3086
|
+
# update variable lookup table
|
|
3087
|
+
self.collect_all_data_keys()
|
|
3088
|
+
# save
|
|
3089
|
+
self.save()
|
|
3090
|
+
|
|
3091
|
+
def fix_solutions(self):
|
|
3092
|
+
"""Method try to find solution for grid points with failed status.
|
|
3093
|
+
|
|
3094
|
+
Ptguesses are used from successfully calculated neighboring points until
|
|
3095
|
+
solution is find. Otherwise ststus remains failed.
|
|
3096
|
+
"""
|
|
3097
|
+
if self.gridded:
|
|
3098
|
+
for ix, grid in self.grids.items():
|
|
3099
|
+
log = []
|
|
3100
|
+
ri, ci = np.nonzero(grid.status == 0)
|
|
3101
|
+
fixed, ftot = 0, len(ri)
|
|
3102
|
+
tm = (self.tc.trange[0] + self.tc.trange[1]) / 2
|
|
3103
|
+
tq = trange(ftot, desc="Fix ({}/{})".format(fixed, ftot))
|
|
3104
|
+
for ind in tq:
|
|
3105
|
+
r, c = ri[ind], ci[ind]
|
|
3106
|
+
x, y = grid.xg[r, c], grid.yg[r, c]
|
|
3107
|
+
k = self.identify(x, y)
|
|
3108
|
+
if k is not None:
|
|
3109
|
+
# search already done grid neighs
|
|
3110
|
+
for rn, cn in grid.neighs(r, c):
|
|
3111
|
+
if grid.status[rn, cn] == 1:
|
|
3112
|
+
start_time = time.time()
|
|
3113
|
+
tcout, ans = self.tc.calc_assemblage(
|
|
3114
|
+
k.difference(self.tc.excess), y, tm, onebulk=x
|
|
3115
|
+
)
|
|
3116
|
+
delta = time.time() - start_time
|
|
3117
|
+
status, res, output = self.tc.parse_logfile()
|
|
3118
|
+
if res is not None:
|
|
3119
|
+
grid.gridcalcs[r, c] = res[0]
|
|
3120
|
+
grid.status[r, c] = 1
|
|
3121
|
+
grid.delta[r, c] = delta
|
|
3122
|
+
fixed += 1
|
|
3123
|
+
tq.set_description(
|
|
3124
|
+
desc="Fix ({}/{})".format(fixed, ftot)
|
|
3125
|
+
)
|
|
3126
|
+
break
|
|
3127
|
+
if grid.status[r, c] == 0:
|
|
3128
|
+
log.append("No solution find for {}, {}".format(x, y))
|
|
3129
|
+
log.append(
|
|
3130
|
+
"Fix done. {} empty grid points left.".format(
|
|
3131
|
+
len(np.flatnonzero(grid.status == 0))
|
|
3132
|
+
)
|
|
3133
|
+
)
|
|
3134
|
+
print("\n".join(log))
|
|
3135
|
+
else:
|
|
3136
|
+
print("Not yet gridded...")
|
|
3137
|
+
|
|
3138
|
+
|
|
3139
|
+
class GridData:
|
|
3140
|
+
"""Class to store gridded calculations.
|
|
3141
|
+
|
|
3142
|
+
Attributes:
|
|
3143
|
+
xspace (numpy.array): Array of x coordinates used for gridding
|
|
3144
|
+
yspace (numpy.array): Array of y coordinates used for gridding
|
|
3145
|
+
gridcalcs (numpy.array): 2D array of THERMOCALC Results
|
|
3146
|
+
status (numpy.array): 2D array indicating status of calculation. The
|
|
3147
|
+
values are 1 - OK, 0 - Failed, NaN - not calculated (outside of any
|
|
3148
|
+
divariant field)
|
|
3149
|
+
delta (numpy.array): 2D array of time needed for THERMOCALC calculation
|
|
3150
|
+
masks (dict): Dictionaty associating divariant field key (frozenset) and
|
|
3151
|
+
binary mask for `gridcalcs`, `status` and `delta` arrays. Masks are
|
|
3152
|
+
used to retrieve results for individual divariant fields.
|
|
3153
|
+
|
|
3154
|
+
"""
|
|
3155
|
+
|
|
3156
|
+
def __init__(self, ps, nx, ny):
|
|
3157
|
+
dx = (ps.xrange[1] - ps.xrange[0]) / nx
|
|
3158
|
+
self.xspace = np.linspace(ps.xrange[0] + dx / 2, ps.xrange[1] - dx / 2, nx)
|
|
3159
|
+
dy = (ps.yrange[1] - ps.yrange[0]) / ny
|
|
3160
|
+
self.yspace = np.linspace(ps.yrange[0] + dy / 2, ps.yrange[1] - dy / 2, ny)
|
|
3161
|
+
self.xg, self.yg = np.meshgrid(self.xspace, self.yspace)
|
|
3162
|
+
self.gridcalcs = np.empty(self.xg.shape, np.dtype(object))
|
|
3163
|
+
self.status = np.empty(self.xg.shape)
|
|
3164
|
+
self.status[:] = np.nan
|
|
3165
|
+
self.delta = np.empty(self.xg.shape)
|
|
3166
|
+
self.delta[:] = np.nan
|
|
3167
|
+
self.masks = OrderedDict()
|
|
3168
|
+
|
|
3169
|
+
def __repr__(self):
|
|
3170
|
+
tmpl = "Grid {}x{} with ok/failed/none solutions {}/{}/{}"
|
|
3171
|
+
ok = len(np.flatnonzero(self.status == 1))
|
|
3172
|
+
fail = len(np.flatnonzero(self.status == 0))
|
|
3173
|
+
return tmpl.format(
|
|
3174
|
+
len(self.xspace),
|
|
3175
|
+
len(self.yspace),
|
|
3176
|
+
ok,
|
|
3177
|
+
fail,
|
|
3178
|
+
np.prod(self.xg.shape) - ok - fail,
|
|
3179
|
+
)
|
|
3180
|
+
|
|
3181
|
+
def get_indexes(self, x, y):
|
|
3182
|
+
"""Return row and column index tuple of nearest grid point
|
|
3183
|
+
|
|
3184
|
+
Args:
|
|
3185
|
+
x (float): x-coordinate of point
|
|
3186
|
+
y (float): y-coordiante of point
|
|
3187
|
+
|
|
3188
|
+
"""
|
|
3189
|
+
# c = np.searchsorted(self.xspace, x)
|
|
3190
|
+
c = np.argmin((self.xspace - x) ** 2)
|
|
3191
|
+
# r = np.searchsorted(self.yspace, y)
|
|
3192
|
+
r = np.argmin((self.yspace - y) ** 2)
|
|
3193
|
+
return r, c
|
|
3194
|
+
|
|
3195
|
+
def contains(self, x, y):
|
|
3196
|
+
xmin, xmax, ymin, ymax = self.extent
|
|
3197
|
+
return (x >= xmin) & (x < xmax) & (y >= ymin) & (y < ymax)
|
|
3198
|
+
|
|
3199
|
+
def neighs(self, r, c):
|
|
3200
|
+
"""Returns list of row, column tuples of neighbouring points on grid.
|
|
3201
|
+
|
|
3202
|
+
Args:
|
|
3203
|
+
r (int): Row index
|
|
3204
|
+
c (int): Column index
|
|
3205
|
+
"""
|
|
3206
|
+
m = np.array(
|
|
3207
|
+
[
|
|
3208
|
+
[(r - 1, c - 1), (r - 1, c), (r - 1, c + 1)],
|
|
3209
|
+
[(r, c - 1), (None, None), (r, c + 1)],
|
|
3210
|
+
[(r + 1, c - 1), (r + 1, c), (r + 1, c + 1)],
|
|
3211
|
+
]
|
|
3212
|
+
)
|
|
3213
|
+
if r < 1:
|
|
3214
|
+
m = m[1:, :]
|
|
3215
|
+
if r > len(self.yspace) - 2:
|
|
3216
|
+
m = m[:-1, :]
|
|
3217
|
+
if c < 1:
|
|
3218
|
+
m = m[:, 1:]
|
|
3219
|
+
if c > len(self.xspace) - 2:
|
|
3220
|
+
m = m[:, :-1]
|
|
3221
|
+
return zip(
|
|
3222
|
+
[i for i in m[:, :, 0].flat if i is not None],
|
|
3223
|
+
[i for i in m[:, :, 1].flat if i is not None],
|
|
3224
|
+
)
|
|
3225
|
+
|
|
3226
|
+
@property
|
|
3227
|
+
def xstep(self):
|
|
3228
|
+
"""Returns spacing along temperature axis"""
|
|
3229
|
+
return self.xspace[1] - self.xspace[0]
|
|
3230
|
+
|
|
3231
|
+
@property
|
|
3232
|
+
def ystep(self):
|
|
3233
|
+
"""Returns spacing along pressure axis"""
|
|
3234
|
+
return self.yspace[1] - self.yspace[0]
|
|
3235
|
+
|
|
3236
|
+
@property
|
|
3237
|
+
def extent(self):
|
|
3238
|
+
"""Returns extend of grid (Note that grid is cell centered)"""
|
|
3239
|
+
return (
|
|
3240
|
+
self.xspace[0] - self.xstep / 2,
|
|
3241
|
+
self.xspace[-1] + self.xstep / 2,
|
|
3242
|
+
self.yspace[0] - self.ystep / 2,
|
|
3243
|
+
self.yspace[-1] + self.ystep / 2,
|
|
3244
|
+
)
|
|
3245
|
+
|
|
3246
|
+
|
|
3247
|
+
class PTpath:
|
|
3248
|
+
"""Class to store THERMOCALC calculations along PT paths.
|
|
3249
|
+
|
|
3250
|
+
Attributes:
|
|
3251
|
+
t (numpy.array): 1D array of temperatures.
|
|
3252
|
+
p (numpy.array): 1D array of pressures.
|
|
3253
|
+
results (list): List of THERMOCALC results dictionaries.
|
|
3254
|
+
"""
|
|
3255
|
+
|
|
3256
|
+
def __init__(self, points, results):
|
|
3257
|
+
self.t, self.p = np.array(points).T
|
|
3258
|
+
self.results = results
|
|
3259
|
+
|
|
3260
|
+
def get_path_data(self, phase, expr):
|
|
3261
|
+
ex = np.array(
|
|
3262
|
+
[
|
|
3263
|
+
eval_expr(expr, res[phase]) if phase in res.phases else np.nan
|
|
3264
|
+
for res in self.results
|
|
3265
|
+
]
|
|
3266
|
+
)
|
|
3267
|
+
return ex
|
|
3268
|
+
|
|
3269
|
+
|
|
3270
|
+
def eval_expr(expr, dt):
|
|
3271
|
+
"""Evaluate expression using THERMOCALC output variables.
|
|
3272
|
+
|
|
3273
|
+
Args:
|
|
3274
|
+
expr (str): expression to be evaluated
|
|
3275
|
+
dt (dict): dictionary of all available variables and their values
|
|
3276
|
+
|
|
3277
|
+
Returns:
|
|
3278
|
+
float: value evaluated from epxression
|
|
3279
|
+
|
|
3280
|
+
Example:
|
|
3281
|
+
>>> ps = pt.sections[0]
|
|
3282
|
+
>>> eval_expr('mode', ps.invpoints[5].results['g'])
|
|
3283
|
+
0.02496698
|
|
3284
|
+
>>> eval_expr('xMgX/(xFeX+xMgX)', ps.invpoints[5].results['g'])
|
|
3285
|
+
0.12584215591915301
|
|
3286
|
+
"""
|
|
3287
|
+
|
|
3288
|
+
def eval_(node):
|
|
3289
|
+
if isinstance(node, ast.Num): # number
|
|
3290
|
+
return node.n
|
|
3291
|
+
if isinstance(node, ast.Name): # variable
|
|
3292
|
+
return dt[node.id]
|
|
3293
|
+
elif isinstance(node, ast.BinOp): # <left> <operator> <right>
|
|
3294
|
+
return ops[type(node.op)](eval_(node.left), eval_(node.right))
|
|
3295
|
+
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
|
|
3296
|
+
return ops[type(node.op)](eval_(node.operand))
|
|
3297
|
+
else:
|
|
3298
|
+
raise TypeError(node)
|
|
3299
|
+
|
|
3300
|
+
ops = {
|
|
3301
|
+
ast.Add: np.add,
|
|
3302
|
+
ast.Sub: np.subtract,
|
|
3303
|
+
ast.Mult: np.multiply,
|
|
3304
|
+
ast.Div: np.divide,
|
|
3305
|
+
ast.Pow: np.power,
|
|
3306
|
+
}
|
|
3307
|
+
return eval_(ast.parse(expr, mode="eval").body)
|
|
3308
|
+
|
|
3309
|
+
|
|
3310
|
+
explorers = {".ptb": PTPS, ".txb": TXPS, ".pxb": PXPS}
|
|
3311
|
+
|
|
3312
|
+
|
|
3313
|
+
def ps_show():
|
|
3314
|
+
parser = argparse.ArgumentParser(description="Draw pseudosection from project file")
|
|
3315
|
+
parser.add_argument("project", type=str, nargs="+", help="builder project file(s)")
|
|
3316
|
+
parser.add_argument(
|
|
3317
|
+
"-o", "--out", nargs="+", help="highlight out lines for given phases"
|
|
3318
|
+
)
|
|
3319
|
+
parser.add_argument("-l", "--label", action="store_true", help="show area labels")
|
|
3320
|
+
parser.add_argument(
|
|
3321
|
+
"--origwd", action="store_true", help="use stored original working directory"
|
|
3322
|
+
)
|
|
3323
|
+
parser.add_argument(
|
|
3324
|
+
"-b", "--bulk", action="store_true", help="show bulk composition on figure"
|
|
3325
|
+
)
|
|
3326
|
+
parser.add_argument(
|
|
3327
|
+
"--cmap", type=str, default="Purples", help="name of the colormap"
|
|
3328
|
+
)
|
|
3329
|
+
parser.add_argument("--alpha", type=float, default=0.6, help="alpha of colormap")
|
|
3330
|
+
parser.add_argument(
|
|
3331
|
+
"--connect",
|
|
3332
|
+
action="store_true",
|
|
3333
|
+
help="whether mouse click echo stable assemblage",
|
|
3334
|
+
)
|
|
3335
|
+
parser.add_argument(
|
|
3336
|
+
"--high",
|
|
3337
|
+
action="append",
|
|
3338
|
+
default=[],
|
|
3339
|
+
help="highlight field defined by set of phases",
|
|
3340
|
+
)
|
|
3341
|
+
parser.add_argument(
|
|
3342
|
+
"--tolerance",
|
|
3343
|
+
type=float,
|
|
3344
|
+
default=None,
|
|
3345
|
+
help="tolerance to simplify univariant lines",
|
|
3346
|
+
)
|
|
3347
|
+
args = parser.parse_args()
|
|
3348
|
+
PSOK = explorers.get(Path(args.project[0]).suffix, None)
|
|
3349
|
+
if PSOK is not None:
|
|
3350
|
+
ps = PSOK(*args.project, tolerance=args.tolerance, origwd=args.origwd)
|
|
3351
|
+
sys.exit(
|
|
3352
|
+
ps.show(
|
|
3353
|
+
out=args.out,
|
|
3354
|
+
label=args.label,
|
|
3355
|
+
bulk=args.bulk,
|
|
3356
|
+
high=args.high,
|
|
3357
|
+
cmap=args.cmap,
|
|
3358
|
+
alpha=args.alpha,
|
|
3359
|
+
connect=args.connect,
|
|
3360
|
+
)
|
|
3361
|
+
)
|
|
3362
|
+
else:
|
|
3363
|
+
print("Project file not recognized...")
|
|
3364
|
+
sys.exit(1)
|
|
3365
|
+
|
|
3366
|
+
|
|
3367
|
+
def ps_grid():
|
|
3368
|
+
parser = argparse.ArgumentParser(description="Calculate compositions in grid")
|
|
3369
|
+
parser.add_argument("project", type=str, nargs="+", help="builder project file(s)")
|
|
3370
|
+
parser.add_argument("--nx", type=int, default=50, help="number of T steps")
|
|
3371
|
+
parser.add_argument("--ny", type=int, default=50, help="number of P steps")
|
|
3372
|
+
parser.add_argument(
|
|
3373
|
+
"--origwd", action="store_true", help="use stored original working directory"
|
|
3374
|
+
)
|
|
3375
|
+
parser.add_argument(
|
|
3376
|
+
"--tolerance",
|
|
3377
|
+
type=float,
|
|
3378
|
+
default=None,
|
|
3379
|
+
help="tolerance to simplify univariant lines",
|
|
3380
|
+
)
|
|
3381
|
+
args = parser.parse_args()
|
|
3382
|
+
PSOK = explorers.get(Path(args.project[0]).suffix, None)
|
|
3383
|
+
if PSOK is not None:
|
|
3384
|
+
ps = PSOK(*args.project, tolerance=args.tolerance, origwd=args.origwd)
|
|
3385
|
+
sys.exit(ps.calculate_composition(nx=args.nx, ny=args.ny))
|
|
3386
|
+
else:
|
|
3387
|
+
print("Project file not recognized...")
|
|
3388
|
+
sys.exit(1)
|
|
3389
|
+
|
|
3390
|
+
|
|
3391
|
+
def ps_iso():
|
|
3392
|
+
parser = argparse.ArgumentParser(description="Draw isopleth diagrams")
|
|
3393
|
+
parser.add_argument("project", type=str, nargs="+", help="builder project file(s)")
|
|
3394
|
+
parser.add_argument("phase", type=str, help="phase used for contouring")
|
|
3395
|
+
parser.add_argument(
|
|
3396
|
+
"-e",
|
|
3397
|
+
"--expr",
|
|
3398
|
+
type=str,
|
|
3399
|
+
default=None,
|
|
3400
|
+
help="expression evaluated to calculate values",
|
|
3401
|
+
)
|
|
3402
|
+
parser.add_argument(
|
|
3403
|
+
"-f", "--filled", action="store_true", help="filled contours", default=False
|
|
3404
|
+
)
|
|
3405
|
+
parser.add_argument(
|
|
3406
|
+
"--origwd", action="store_true", help="use stored original working directory"
|
|
3407
|
+
)
|
|
3408
|
+
parser.add_argument(
|
|
3409
|
+
"-o", "--out", nargs="+", help="highlight out lines for given phases"
|
|
3410
|
+
)
|
|
3411
|
+
parser.add_argument(
|
|
3412
|
+
"--nosplit",
|
|
3413
|
+
action="store_true",
|
|
3414
|
+
help="controls whether the underlying contour is removed or not",
|
|
3415
|
+
)
|
|
3416
|
+
parser.add_argument(
|
|
3417
|
+
"-b", "--bulk", action="store_true", help="show bulk composition on figure"
|
|
3418
|
+
)
|
|
3419
|
+
parser.add_argument("--step", type=float, default=None, help="contour step")
|
|
3420
|
+
parser.add_argument("--ncont", type=int, default=10, help="number of contours")
|
|
3421
|
+
parser.add_argument("--colors", type=str, default=None, help="color for all levels")
|
|
3422
|
+
parser.add_argument("--cmap", type=str, default=None, help="name of the colormap")
|
|
3423
|
+
parser.add_argument(
|
|
3424
|
+
"--smooth", type=float, default=0, help="smoothness of the approximation"
|
|
3425
|
+
)
|
|
3426
|
+
parser.add_argument(
|
|
3427
|
+
"--labelkey",
|
|
3428
|
+
action="append",
|
|
3429
|
+
default=[],
|
|
3430
|
+
help="label contours in field defined by set of phases",
|
|
3431
|
+
)
|
|
3432
|
+
parser.add_argument(
|
|
3433
|
+
"--high",
|
|
3434
|
+
action="append",
|
|
3435
|
+
default=[],
|
|
3436
|
+
help="highlight field defined by set of phases",
|
|
3437
|
+
)
|
|
3438
|
+
parser.add_argument(
|
|
3439
|
+
"--tolerance",
|
|
3440
|
+
type=float,
|
|
3441
|
+
default=None,
|
|
3442
|
+
help="tolerance to simplify univariant lines",
|
|
3443
|
+
)
|
|
3444
|
+
args = parser.parse_args()
|
|
3445
|
+
PSOK = explorers.get(Path(args.project[0]).suffix, None)
|
|
3446
|
+
if PSOK is not None:
|
|
3447
|
+
ps = PSOK(*args.project, tolerance=args.tolerance, origwd=args.origwd)
|
|
3448
|
+
sys.exit(
|
|
3449
|
+
ps.isopleths(
|
|
3450
|
+
args.phase,
|
|
3451
|
+
expr=args.expr,
|
|
3452
|
+
filled=args.filled,
|
|
3453
|
+
smooth=args.smooth,
|
|
3454
|
+
step=args.step,
|
|
3455
|
+
bulk=args.bulk,
|
|
3456
|
+
N=args.ncont,
|
|
3457
|
+
labelkeys=args.labelkey,
|
|
3458
|
+
nosplit=args.nosplit,
|
|
3459
|
+
colors=args.colors,
|
|
3460
|
+
cmap=args.cmap,
|
|
3461
|
+
out=args.out,
|
|
3462
|
+
high=args.high,
|
|
3463
|
+
)
|
|
3464
|
+
)
|
|
3465
|
+
else:
|
|
3466
|
+
print("Project file not recognized...")
|
|
3467
|
+
sys.exit(1)
|
|
3468
|
+
|
|
3469
|
+
|
|
3470
|
+
def ps_drawpd():
|
|
3471
|
+
parser = argparse.ArgumentParser(description="Generate drawpd file from project")
|
|
3472
|
+
parser.add_argument(
|
|
3473
|
+
"project", type=str, nargs="+", help="psbuilder project file(s)"
|
|
3474
|
+
)
|
|
3475
|
+
parser.add_argument(
|
|
3476
|
+
"-a", "--areas", action="store_true", help="export also areas", default=True
|
|
3477
|
+
)
|
|
3478
|
+
parser.add_argument(
|
|
3479
|
+
"--origwd", action="store_true", help="use stored original working directory"
|
|
3480
|
+
)
|
|
3481
|
+
parser.add_argument(
|
|
3482
|
+
"--tolerance",
|
|
3483
|
+
type=float,
|
|
3484
|
+
default=None,
|
|
3485
|
+
help="tolerance to simplify univariant lines",
|
|
3486
|
+
)
|
|
3487
|
+
args = parser.parse_args()
|
|
3488
|
+
PSOK = explorers.get(Path(args.project[0]).suffix, None)
|
|
3489
|
+
if PSOK is not None:
|
|
3490
|
+
ps = PSOK(*args.project, tolerance=args.tolerance, origwd=args.origwd)
|
|
3491
|
+
sys.exit(ps.gendrawpd(export_areas=args.areas))
|
|
3492
|
+
else:
|
|
3493
|
+
print("Project file not recognized...")
|
|
3494
|
+
sys.exit(1)
|