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.
@@ -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)