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,1093 @@
1
+ """pypsbuilder classes used by builders.
2
+
3
+ This module contains classes and tools providing API to THERMOCALC, parsing of
4
+ outputs and storage of calculated invariant points and univariant lines.
5
+
6
+ """
7
+
8
+ # author: Ondrej Lexa
9
+ # website: https://github.com/ondrolexa/pypsbuilder
10
+
11
+ try:
12
+ import cPickle as pickle
13
+ except ImportError:
14
+ import pickle
15
+ import gzip
16
+ import numpy as np
17
+ import matplotlib.pyplot as plt
18
+ from matplotlib.patches import PathPatch
19
+ from matplotlib.path import Path
20
+ from shapely.geometry import LineString, Point, MultiPoint
21
+ from shapely.ops import polygonize, linemerge # unary_union
22
+
23
+ polymorphs = [
24
+ {"sill", "and"},
25
+ {"ky", "and"},
26
+ {"sill", "ky"},
27
+ {"q", "coe"},
28
+ {"diam", "gph"},
29
+ ]
30
+ """list: List of two-element sets containing polymorphs."""
31
+
32
+
33
+ class PseudoBase:
34
+ """Base class with common methods for InvPoint and UniLine."""
35
+
36
+ def label(self, excess={}):
37
+ """str: full label with space delimeted phases - zero mode phase."""
38
+ phases_lbl = " ".join(sorted(list(self.phases.difference(excess))))
39
+ out_lbl = " ".join(sorted(list(self.out)))
40
+ return "{} - {}".format(phases_lbl, out_lbl)
41
+
42
+ def annotation(self, state=0):
43
+ """str: String representation of ID with possible zermo mode phase."""
44
+ if state == 1:
45
+ return "{}".format(" ".join(self.out))
46
+ elif state == 2:
47
+ return "{:d} {}".format(self.id, " ".join(self.out))
48
+ else:
49
+ return "{:d}".format(self.id)
50
+
51
+ def ptguess(self, **kwargs):
52
+ """list: Get stored ptguesses.
53
+
54
+ InvPoint has just single ptguess, but for UniLine idx need to be
55
+ specified. If omitted, the middle point from calculated ones is used.
56
+
57
+ Args:
58
+ idx (int): index which guesses to get.
59
+ """
60
+ idx = kwargs.get("idx", self.midix)
61
+ return self.results[idx].ptguess
62
+
63
+ def datakeys(self, phase=None):
64
+ """list: Get list of variables for phase.
65
+
66
+ Args:
67
+ phase (str): name of phase
68
+ """
69
+ if phase is None:
70
+ return list(self.results[self.midix].data.keys())
71
+ else:
72
+ return list(self.results[self.midix].data[phase].keys())
73
+
74
+
75
+ class InvPoint(PseudoBase):
76
+ """Class to store invariant point
77
+
78
+ Attributes:
79
+ id (int): Invariant point identification
80
+ phases (set): set of present phases
81
+ out (set): set of zero mode phases
82
+ cmd (str): THERMOCALC standard input to calculate this point
83
+ variance (int): variance
84
+ x (numpy.array): Array of x coordinates
85
+ (even if only one, it is stored as array)
86
+ y (numpy.array): Array of x coordinates
87
+ (even if only one, it is stored as array)
88
+ results (list): List of results dicts with data and ptgues keys.
89
+ output (str): Full THERMOCALC output
90
+ manual (bool): True when inavariant point is user-defined and not
91
+ calculated
92
+ """
93
+
94
+ def __init__(self, **kwargs):
95
+ assert "phases" in kwargs, "Set of phases must be provided"
96
+ assert "out" in kwargs, "Set of zero phase must be provided"
97
+ self.id = kwargs.get("id", 0)
98
+ self.phases = kwargs.get("phases")
99
+ self.out = kwargs.get("out")
100
+ self.cmd = kwargs.get("cmd", "")
101
+ self.variance = kwargs.get("variance", 0)
102
+ self.x = kwargs.get("x", [])
103
+ self.y = kwargs.get("y", [])
104
+ self.results = kwargs.get("results", None)
105
+ self.output = kwargs.get("output", "User-defined")
106
+ self.manual = kwargs.get("manual", False)
107
+
108
+ def __repr__(self):
109
+ return "Inv: {}".format(self.label())
110
+
111
+ @property
112
+ def midix(self):
113
+ return 0
114
+
115
+ @property
116
+ def _x(self):
117
+ """X coordinate as float"""
118
+ return self.x[0]
119
+
120
+ @property
121
+ def _y(self):
122
+ """Y coordinate as float"""
123
+ return self.y[0]
124
+
125
+ def shape(self):
126
+ """Return shapely Point representing invariant point."""
127
+ return Point(self._x, self._y)
128
+
129
+ def all_unilines(self):
130
+ """Return four tuples (phases, out) indicating possible four
131
+ univariant lines passing trough this invariant point"""
132
+ a, b = self.out
133
+ aset, bset = set([a]), set([b])
134
+ aphases, bphases = self.phases.difference(aset), self.phases.difference(bset)
135
+ # Check for polymorphs
136
+ fix = False
137
+ for poly in polymorphs:
138
+ if poly.issubset(self.phases):
139
+ fix = True
140
+ break
141
+ triple = set(["ky", "and", "sill"])
142
+ if fix and (poly != self.out) and not triple.issubset(self.phases):
143
+ yespoly = poly.intersection(self.out)
144
+ nopoly = self.out.difference(yespoly)
145
+ aphases = self.phases.difference(yespoly)
146
+ bphases = self.phases.difference(poly.difference(self.out))
147
+ return (
148
+ (aphases, nopoly),
149
+ (bphases, nopoly),
150
+ (self.phases, yespoly),
151
+ (self.phases.difference(nopoly), yespoly),
152
+ )
153
+ else:
154
+ return (
155
+ (self.phases, aset),
156
+ (self.phases, bset),
157
+ (bphases, aset),
158
+ (aphases, bset),
159
+ )
160
+
161
+
162
+ class UniLine(PseudoBase):
163
+ """Class to store univariant line
164
+
165
+ Attributes:
166
+ id (int): Invariant point identification
167
+ phases (set): set of present phases
168
+ out (set): set of zero mode phase
169
+ cmd (str): THERMOCALC standard input to calculate this point
170
+ variance (int): variance
171
+ _x (numpy.array): Array of x coordinates (all calculated)
172
+ _y (numpy.array): Array of x coordinates (all calculated)
173
+ results (list): List of results dicts with data and ptgues keys.
174
+ output (str): Full THERMOCALC output
175
+ manual (bool): True when inavariant point is user-defined and not
176
+ calculated
177
+ begin (int): id of invariant point defining begining of the line.
178
+ 0 for no begin
179
+ end (int): id of invariant point defining end of the line.
180
+ 0 for no end
181
+ used (slice): slice indicating which point on calculated line are
182
+ between begin and end
183
+ """
184
+
185
+ def __init__(self, **kwargs):
186
+ assert "phases" in kwargs, "Set of phases must be provided"
187
+ assert "out" in kwargs, "Set of zero phase must be provided"
188
+ self.id = kwargs.get("id", 0)
189
+ self.phases = kwargs.get("phases")
190
+ self.out = kwargs.get("out")
191
+ self.cmd = kwargs.get("cmd", "")
192
+ self.variance = kwargs.get("variance", 0)
193
+ self._x = kwargs.get("x", np.array([]))
194
+ self._y = kwargs.get("y", np.array([]))
195
+ self.results = kwargs.get("results", None)
196
+ self.output = kwargs.get("output", "User-defined")
197
+ self.manual = kwargs.get("manual", False)
198
+ self.begin = kwargs.get("begin", 0)
199
+ self.end = kwargs.get("end", 0)
200
+ self.used = slice(0, len(self._x))
201
+ self.x = self._x.copy()
202
+ self.y = self._y.copy()
203
+
204
+ def __repr__(self):
205
+ return "Uni: {}".format(self.label())
206
+
207
+ @property
208
+ def midix(self):
209
+ return int((self.used.start + self.used.stop) // 2)
210
+
211
+ @property
212
+ def connected(self):
213
+ return 2 - [self.begin, self.end].count(0)
214
+
215
+ def _shape(self, ratio=None, tolerance=None):
216
+ """Return shapely LineString representing univariant line.
217
+
218
+ This method is using all calculated points.
219
+
220
+ Args:
221
+ ratio: y-coordinate multiplier to scale coordinates. Default None
222
+ tolerance: tolerance x coordinates. Simplified object will be within
223
+ the tolerance distance of the original geometry. Default None
224
+ """
225
+ if ratio is None:
226
+ return LineString(np.array([self._x, self._y]).T)
227
+ else:
228
+ if tolerance is None:
229
+ return LineString(np.array([self._x, self._y]).T)
230
+ else:
231
+ ln = LineString(np.array([self._x, ratio * self._y]).T).simplify(
232
+ tolerance
233
+ )
234
+ x, y = np.array(ln.coords).T
235
+ return LineString(np.array([x, y / ratio]).T)
236
+
237
+ def shape(self, ratio=None, tolerance=None):
238
+ """Return shapely LineString representing univariant line.
239
+
240
+ This method is using trimmed points.
241
+
242
+ Args:
243
+ ratio: y-coordinate multiplier to scale coordinates. Default None
244
+ tolerance: tolerance x coordinates. Simplified object will be within
245
+ the tolerance distance of the original geometry. Default None
246
+ """
247
+ if ratio is None:
248
+ return LineString(np.array([self.x, self.y]).T)
249
+ else:
250
+ if tolerance is None:
251
+ return LineString(np.array([self.x, self.y]).T)
252
+ else:
253
+ ln = LineString(np.array([self.x, ratio * self.y]).T).simplify(
254
+ tolerance
255
+ )
256
+ x, y = np.array(ln.coords).T
257
+ return LineString(np.array([x, y / ratio]).T)
258
+
259
+ def contains_inv(self, ip):
260
+ """Check whether invariant point theoretically belong to univariant line.
261
+
262
+ Args:
263
+ ip (InvPoint): Invariant point
264
+
265
+ Returns:
266
+ bool: True for yes, False for no. Note that metastability is not checked.
267
+ """
268
+
269
+ def checkme(uphases, uout, iphases, iout):
270
+ triple = set(["ky", "and", "sill"])
271
+ if triple.issubset(iphases):
272
+ iphases = iphases.difference(triple.difference(iout))
273
+ a, b = iout
274
+ aset, bset = set([a]), set([b])
275
+ aphases, bphases = iphases.difference(aset), iphases.difference(bset)
276
+ candidate = False
277
+ if iphases == uphases and len(iout.difference(uout)) == 1:
278
+ candidate = True
279
+ if bphases == uphases and aset == uout:
280
+ candidate = True
281
+ if aphases == uphases and bset == uout:
282
+ candidate = True
283
+ return candidate
284
+
285
+ # Check for polymorphs
286
+ for poly in polymorphs:
287
+ fixi, fixu = False, False
288
+ if (
289
+ poly.issubset(ip.phases)
290
+ and (poly != ip.out)
291
+ and (not ip.out.isdisjoint(poly))
292
+ ):
293
+ fixi = True
294
+ if poly.issubset(self.phases) and not self.out.isdisjoint(poly):
295
+ fixu = True
296
+ # check invs
297
+ candidate = checkme(self.phases, self.out, ip.phases, ip.out)
298
+ if fixi and not candidate:
299
+ candidate = checkme(
300
+ self.phases,
301
+ self.out,
302
+ ip.phases,
303
+ ip.out.difference(poly).union(poly.difference(ip.out)),
304
+ )
305
+ if fixu and not candidate:
306
+ candidate = checkme(
307
+ self.phases, poly.difference(self.out), ip.phases, ip.out
308
+ )
309
+ if candidate:
310
+ break
311
+ return candidate
312
+
313
+ def get_label_point(self):
314
+ """Returns coordinate tuple of labeling point for univariant line."""
315
+ if len(self.x) > 1:
316
+ dx = np.diff(self.x)
317
+ dy = np.diff(self.y)
318
+ d = np.sqrt(dx**2 + dy**2)
319
+ sd = np.sum(d)
320
+ if sd > 0:
321
+ cl = np.append([0], np.cumsum(d))
322
+ ix = np.interp(sd / 2, cl, range(len(cl)))
323
+ cix = int(ix)
324
+ return (
325
+ self.x[cix] + (ix - cix) * dx[cix],
326
+ self.y[cix] + (ix - cix) * dy[cix],
327
+ )
328
+ else:
329
+ return self.x[0], self.y[0]
330
+ else:
331
+ return self.x[0], self.y[0]
332
+
333
+
334
+ class Dogmin:
335
+ def __init__(self, **kwargs):
336
+ assert "output" in kwargs, "Dogmin output must be provided"
337
+ assert "resic" in kwargs, "ic file content must be provided"
338
+ self.id = kwargs.get("id", 0)
339
+ self.phases = set()
340
+ output = kwargs.get("output")
341
+ if "assemblage" in output:
342
+ self.output = output
343
+ self.resic = kwargs.get("resic")
344
+ self.phases = set(
345
+ self.output.split("assemblage")[1].split("\n")[0].split()
346
+ ) # TC 35
347
+ else:
348
+ self.resic = ""
349
+ asm = output.split("Gibbs energy minimisation info\n")[-1]
350
+ if len(asm.split("\n #")) > 1:
351
+ best = int(asm.split("\n #")[1].splitlines()[1].split()[0])
352
+ for blk in output.split("phases :")[1:]:
353
+ head = blk.splitlines()[0]
354
+ if "#" in head.split()[-1]:
355
+ if best == int(head.split()[-1].replace("#", "")):
356
+ if "or" in head:
357
+ head = head.split("or")[0]
358
+ else:
359
+ head = head.split("#")[0]
360
+ if "(" in head:
361
+ head = head.split("(")[0]
362
+ self.phases = set(head.split())
363
+ self.output = (
364
+ "Gibbs energy minimisation info\n"
365
+ + asm
366
+ + "\n"
367
+ + "phases :"
368
+ + blk
369
+ )
370
+ break
371
+ if (
372
+ not self.phases
373
+ ): # If last block is the best, use it (TC do not output #number)
374
+ if "or" in head:
375
+ head = head.split("or")[0]
376
+ else:
377
+ head = head.split("#")[0]
378
+ if "(" in head:
379
+ head = head.split("(")[0]
380
+ self.phases = set(head.split())
381
+ self.output = (
382
+ "Gibbs energy minimisation info\n"
383
+ + asm
384
+ + "\n"
385
+ + "phases :"
386
+ + blk
387
+ )
388
+ self.x = kwargs.get("x", None)
389
+ self.y = kwargs.get("y", None)
390
+
391
+ @property
392
+ def out(self):
393
+ return set()
394
+
395
+ def label(self, excess={}):
396
+ """str: full label with space delimeted phases."""
397
+ return " ".join(sorted(list(self.phases.difference(excess))))
398
+
399
+ def annotation(self, show_out=False, excess={}):
400
+ """str: String representation of ID with possible zermo mode phase."""
401
+ if show_out:
402
+ return self.label(excess=excess)
403
+ else:
404
+ return "{:d}".format(self.id)
405
+
406
+ def ptguess(self):
407
+ block = [ln for ln in self.output.splitlines() if ln != ""]
408
+ xyz = [ix for ix, ln in enumerate(block) if ln.startswith("xyzguess")]
409
+ gixs = [ix for ix, ln in enumerate(block) if ln.startswith("ptguess")][0] - 1
410
+ gixe = xyz[-1] + 2
411
+ return block[gixs:gixe]
412
+
413
+
414
+ class TCResult:
415
+ def __init__(self, T, p, variance=0, c=0, data={}, ptguess=[""]):
416
+ self.data = data
417
+ self.ptguess = ptguess
418
+ self.T = T
419
+ self.p = p
420
+ self.variance = variance
421
+ self.c = c
422
+
423
+ @classmethod
424
+ def from_block(cls, block, ptguess):
425
+ info, ax, sf, bulk, rbi, mode, factor, td, sys, *mems, pems = block.split(
426
+ "\n\n"
427
+ )
428
+ if "var = 2; seen" in info:
429
+ # no step in bulk
430
+ info, ax, sf, rbi, mode, factor, td, sys, *mems, pems = block.split("\n\n")
431
+ bulk = "\n".join(rbi.split("\n")[:3])
432
+ rbi = "\n".join(rbi.split("\n")[3:])
433
+ # heading
434
+ data = {phase: {} for phase in info.split("{")[0].split()}
435
+ p, T = (float(v.strip()) for v in info.split("{")[1].split("}")[0].split(","))
436
+ # var or ovar?
437
+ variance = int(info.split("var = ")[1].split(" ")[0].replace(";", ""))
438
+ # a-x variables
439
+ for head, vals in zip(ax.split("\n")[::2], ax.split("\n")[1::2]):
440
+ phase, *names = head.split()
441
+ data[phase].update(
442
+ {
443
+ name.replace("({})".format(phase), ""): float(val)
444
+ for name, val in zip(names, vals.split())
445
+ }
446
+ )
447
+ # site fractions
448
+ for head, vals in zip(
449
+ sf.split("\n")[1::2], sf.split("\n")[2::2]
450
+ ): # skip site fractions row
451
+ phase, *names = head.split()
452
+ data[phase].update(
453
+ {name: float(val) for name, val in zip(names, vals.split())}
454
+ )
455
+ # bulk composition
456
+ bulk_vals = {}
457
+ oxhead, vals = bulk.split("\n")[1:] # skip oxide compositions row
458
+ for ox, val in zip(oxhead.split(), vals.split()[1:]):
459
+ bulk_vals[ox] = float(val)
460
+ data["bulk"] = bulk_vals
461
+ # x for TX and pX
462
+ if "step" in vals:
463
+ c = float(vals.split("step")[1].split(", x =")[1])
464
+ else:
465
+ c = 0
466
+ # rbi
467
+ for row in rbi.split("\n"):
468
+ phase, *vals = row.split()
469
+ data[phase].update(
470
+ {ox: float(val) for ox, val in zip(oxhead.split(), vals)}
471
+ )
472
+ # modes (zero mode is empty field in tc350 !!!)
473
+ head, vals = mode.split("\n")
474
+ phases = head.split()[1:]
475
+ # fixed width parsing !!!
476
+ valsf = [
477
+ (
478
+ float(vals[6:][12 * i : 12 * (i + 1)].strip())
479
+ if vals[6:][12 * i : 12 * (i + 1)].strip() != ""
480
+ else 0.0
481
+ )
482
+ for i in range(len(phases))
483
+ ]
484
+ for phase, val in zip(phases, valsf):
485
+ data[phase].update({"mode": float(val)})
486
+ # factors
487
+ head, vals = factor.split("\n")
488
+ phases = head.split()[1:]
489
+ valsf = [
490
+ (
491
+ float(vals[6:][12 * i : 12 * (i + 1)].strip())
492
+ if vals[6:][12 * i : 12 * (i + 1)].strip() != ""
493
+ else 0.0
494
+ )
495
+ for i in range(len(phases))
496
+ ]
497
+ for phase, val in zip(phases, valsf):
498
+ data[phase].update({"factor": float(val)})
499
+ # thermodynamic state
500
+ head, *rows = td.split("\n")
501
+ for row in rows:
502
+ phase, *vals = row.split()
503
+ data[phase].update(
504
+ {name: float(val) for name, val in zip(head.split(), vals)}
505
+ )
506
+ # bulk thermodynamics
507
+ sys = {}
508
+ for name, val in zip(head.split(), row.split()[1:]):
509
+ sys[name] = float(val)
510
+ data["sys"] = sys
511
+ # model end-members
512
+ if len(mems) > 0:
513
+ _, mem0 = mems[0].split("\n", maxsplit=1)
514
+ head = ["ideal", "gamma", "activity", "prop", "mu", "RTlna"]
515
+ mems[0] = mem0
516
+ for mem in mems:
517
+ ems = mem.split("\n")
518
+ phase, ems0 = ems[0].split(maxsplit=1)
519
+ ems[0] = ems0
520
+ for row in ems:
521
+ em, *vals = row.split()
522
+ phase_em = "{}({})".format(phase, em)
523
+ data[phase_em] = {name: float(val) for name, val in zip(head, vals)}
524
+ # pure end-members
525
+ for row in pems.split("\n")[:-1]:
526
+ pem, val = row.split()
527
+ data[pem].update({"mu": float(val)})
528
+ # Finally
529
+ return cls(T, p, variance=variance, c=c, data=data, ptguess=ptguess)
530
+
531
+ def __repr__(self):
532
+ return "p:{:g} T:{:g} V:{} c:{:g}, Phases: {}".format(
533
+ self.p, self.T, self.variance, self.c, " ".join(self.phases)
534
+ )
535
+
536
+ def __getitem__(self, key):
537
+ if isinstance(key, str):
538
+ if key not in self.phases:
539
+ raise IndexError("The index ({}) do not exists.".format(key))
540
+ return self.data[key]
541
+ else:
542
+ raise TypeError("Invalid argument type.")
543
+
544
+ @property
545
+ def phases(self):
546
+ return set(self.data.keys())
547
+
548
+ def rename_phase(self, old, new):
549
+ self.data[new] = self.data.pop(old)
550
+ for ix, ln in enumerate(self.ptguess):
551
+ self.ptguess[ix] = ln.replace("({})".format(old), "({})".format(new))
552
+
553
+
554
+ class TCResultSet:
555
+ def __init__(self, results):
556
+ self.results = results
557
+
558
+ def __repr__(self):
559
+ return "{} results".format(len(self.results))
560
+
561
+ def __len__(self):
562
+ return len(self.results)
563
+
564
+ def __getitem__(self, key):
565
+ if isinstance(key, slice):
566
+ # Get the start, stop, and step from the slice
567
+ return TCResultSet(self.results[key])
568
+ elif isinstance(key, int):
569
+ if key < 0: # Handle negative indices
570
+ key += len(self.results)
571
+ if key < 0 or key >= len(self.results):
572
+ raise IndexError("The index ({}) is out of range.".format(key))
573
+ return self.results[key]
574
+ elif isinstance(key, list):
575
+ return TCResultSet([self.results[ix] for ix in key])
576
+ else:
577
+ raise TypeError("Invalid argument type.")
578
+
579
+ @property
580
+ def x(self):
581
+ return np.array([res.T for res in self.results])
582
+
583
+ @property
584
+ def y(self):
585
+ return np.array([res.p for res in self.results])
586
+
587
+ @property
588
+ def variance(self):
589
+ return self.results[0].variance
590
+
591
+ @property
592
+ def c(self):
593
+ return np.array([res.c for res in self.results])
594
+
595
+ @property
596
+ def phases(self):
597
+ return self.results[0].phases
598
+
599
+ def ptguess(self, ix):
600
+ try:
601
+ return self.results[ix].ptguess
602
+ except Exception:
603
+ return None
604
+
605
+ def rename_phase(self, old, new):
606
+ for r in self.results:
607
+ r.rename_phase(old, new)
608
+
609
+ def insert(self, ix, result):
610
+ self.results.insert(ix, result)
611
+
612
+
613
+ class SectionBase:
614
+ """Base class for PTsection, TXsection and PXsection"""
615
+
616
+ def __init__(self, **kwargs):
617
+ self.excess = kwargs.get("excess", set())
618
+ self.invpoints = {}
619
+ self.unilines = {}
620
+ self.dogmins = {}
621
+
622
+ def __repr__(self):
623
+ return "\n".join(
624
+ [
625
+ "{}".format(self.type),
626
+ "Univariant lines: {}".format(len(self.unilines)),
627
+ "Invariant points: {}".format(len(self.invpoints)),
628
+ "{} range: {} {}".format(self.x_var, *self.xrange),
629
+ "{} range: {} {}".format(self.y_var, *self.yrange),
630
+ ]
631
+ )
632
+
633
+ @property
634
+ def type(self):
635
+ return type(self).__name__
636
+
637
+ @property
638
+ def ratio(self):
639
+ return (self.xrange[1] - self.xrange[0]) / (self.yrange[1] - self.yrange[0])
640
+
641
+ @property
642
+ def range_shapes(self):
643
+ # default p-t range boundary
644
+ bnd = [
645
+ LineString(
646
+ [(self.xrange[0], self.yrange[0]), (self.xrange[1], self.yrange[0])]
647
+ ),
648
+ LineString(
649
+ [(self.xrange[1], self.yrange[0]), (self.xrange[1], self.yrange[1])]
650
+ ),
651
+ LineString(
652
+ [(self.xrange[1], self.yrange[1]), (self.xrange[0], self.yrange[1])]
653
+ ),
654
+ LineString(
655
+ [(self.xrange[0], self.yrange[1]), (self.xrange[0], self.yrange[0])]
656
+ ),
657
+ ]
658
+ return bnd, polygonize(bnd)[0]
659
+
660
+ def add_inv(self, id, inv):
661
+ if inv.manual:
662
+ inv.results = None
663
+ else: # temporary compatibility with 2.2.0
664
+ if not isinstance(inv.results, TCResultSet):
665
+ inv.results = TCResultSet(
666
+ [
667
+ TCResult(
668
+ float(x),
669
+ float(y),
670
+ variance=inv.variance,
671
+ data=r["data"],
672
+ ptguess=r["ptguess"],
673
+ )
674
+ for r, x, y in zip(inv.results, inv.x, inv.y)
675
+ ]
676
+ )
677
+ self.invpoints[id] = inv
678
+ self.invpoints[id].id = id
679
+
680
+ def add_uni(self, id, uni):
681
+ if uni.manual:
682
+ uni.results = None
683
+ else: # temporary compatibility with 2.2.0
684
+ if not isinstance(uni.results, TCResultSet):
685
+ uni.results = TCResultSet(
686
+ [
687
+ TCResult(
688
+ float(x),
689
+ float(y),
690
+ variance=uni.variance,
691
+ data=r["data"],
692
+ ptguess=r["ptguess"],
693
+ )
694
+ for r, x, y in zip(uni.results, uni._x, uni._y)
695
+ ]
696
+ )
697
+ self.unilines[id] = uni
698
+ self.unilines[id].id = id
699
+
700
+ def add_dogmin(self, id, dgm):
701
+ self.dogmins[id] = dgm
702
+ self.dogmins[id].id = id
703
+
704
+ def cleanup_data(self):
705
+ for id_uni, uni in self.unilines.items():
706
+ if not uni.manual:
707
+ keep = slice(
708
+ max(uni.used.start - 1, 0), min(uni.used.stop + 1, len(uni._x))
709
+ )
710
+ uni._x = uni._x[keep]
711
+ uni._y = uni._y[keep]
712
+ uni.results = uni.results[keep]
713
+ else:
714
+ uni.cmd = ""
715
+ uni.variance = 0
716
+ uni._x = np.array([])
717
+ uni._y = np.array([])
718
+ uni.results = [dict(data=None, ptguess=None)]
719
+ uni.output = "User-defined"
720
+ uni.used = slice(0, 0)
721
+ uni.x = np.array([])
722
+ uni.y = np.array([])
723
+ self.trim_uni(id_uni)
724
+
725
+ def getidinv(self, inv=None):
726
+ """Return id of either new or existing invariant point"""
727
+ ids = 0
728
+ # collect polymorphs identities
729
+ if inv is not None:
730
+ outs = [inv.out]
731
+ for poly in polymorphs:
732
+ if poly.issubset(inv.phases):
733
+ switched = inv.out.difference(poly).union(poly.difference(inv.out))
734
+ if switched:
735
+ outs.append(switched)
736
+
737
+ for iid, cinv in self.invpoints.items():
738
+ if inv is not None:
739
+ if cinv.phases == inv.phases:
740
+ if cinv.out in outs:
741
+ inv.out = cinv.out # switch to already used ??? Needed ???
742
+ return False, iid
743
+ ids = max(ids, iid)
744
+ return True, ids + 1
745
+
746
+ def getiduni(self, uni=None):
747
+ """Return id of either new or existing univariant line"""
748
+ ids = 0
749
+ # collect polymorphs identities
750
+ if uni is not None:
751
+ outs = [uni.out]
752
+ for poly in polymorphs:
753
+ if poly.issubset(uni.phases):
754
+ outs.append(poly.difference(uni.out))
755
+
756
+ for uid, cuni in self.unilines.items():
757
+ if uni is not None:
758
+ if cuni.phases == uni.phases:
759
+ if cuni.out in outs:
760
+ uni.out = cuni.out # switch to already used ??? Needed ???
761
+ return False, uid
762
+ ids = max(ids, uid)
763
+ return True, ids + 1
764
+
765
+ def trim_uni(self, id):
766
+ uni = self.unilines[id]
767
+ if not uni.manual:
768
+ if uni.begin > 0:
769
+ p1 = Point(
770
+ self.invpoints[uni.begin].x,
771
+ self.ratio * self.invpoints[uni.begin].y,
772
+ )
773
+ else:
774
+ p1 = Point(uni._x[0], self.ratio * uni._y[0])
775
+ if uni.end > 0:
776
+ p2 = Point(
777
+ self.invpoints[uni.end].x, self.ratio * self.invpoints[uni.end].y
778
+ )
779
+ else:
780
+ p2 = Point(uni._x[-1], self.ratio * uni._y[-1])
781
+ #
782
+ xy = np.array([uni._x, self.ratio * uni._y]).T
783
+ line = LineString(xy)
784
+ # vertex distances
785
+ vdst = np.array([line.project(Point(*v)) for v in xy])
786
+ d1 = line.project(p1)
787
+ d2 = line.project(p2)
788
+ # switch if needed
789
+ if d1 > d2:
790
+ d1, d2 = d2, d1
791
+ uni.begin, uni.end = uni.end, uni.begin
792
+ # get slice of points to keep
793
+ uni.used = slice(
794
+ np.flatnonzero(vdst >= d1)[0].item(),
795
+ np.flatnonzero(vdst <= d2)[-1].item() + 1,
796
+ )
797
+
798
+ # concatenate begin, keep, end
799
+ if uni.begin > 0:
800
+ x1, y1 = self.invpoints[uni.begin].x, self.invpoints[uni.begin].y
801
+ else:
802
+ x1, y1 = [], []
803
+ if uni.end > 0:
804
+ x2, y2 = self.invpoints[uni.end].x, self.invpoints[uni.end].y
805
+ else:
806
+ x2, y2 = [], []
807
+ if not uni.manual:
808
+ xx = uni._x[uni.used]
809
+ yy = uni._y[uni.used]
810
+ else:
811
+ xx, yy = [], []
812
+
813
+ # store trimmed
814
+ uni.x = np.hstack((x1, xx, x2))
815
+ uni.y = np.hstack((y1, yy, y2))
816
+
817
+ def create_shapes(self, tolerance=None):
818
+ def splitme(edges):
819
+ """Recursive boundary splitter"""
820
+ for idx, edge in enumerate(edges):
821
+ for _, l in lns:
822
+ if edge.intersects(l):
823
+ m = linemerge([edge, l])
824
+ if m.geom_type == "MultiLineString":
825
+ p = edge.intersection(l)
826
+ if (
827
+ p.geom_type == "LineString"
828
+ ): # seems to occur whe duplicate points are on line
829
+ p = MultiPoint(p.coords)
830
+ if p.geom_type == "MultiPoint":
831
+ pts = [l.interpolate(l.project(pt)) for pt in p.geoms]
832
+ pts = sorted(pts, key=lambda pt: edge.project(pt))
833
+ else:
834
+ pts = [l.interpolate(l.project(p))]
835
+ edges.pop(idx)
836
+ pts = (
837
+ [Point(edge.coords[0])] + pts + [Point(edge.coords[-1])]
838
+ )
839
+ for start, stop in zip(pts[:-1], pts[1:]):
840
+ p_seg = LineString([start, stop])
841
+ if p_seg.is_valid:
842
+ edges.append(p_seg)
843
+ return edges, True
844
+ return edges, False
845
+
846
+ # define bounds and area
847
+ edges, area = self.range_shapes
848
+ lns = []
849
+ log = []
850
+ # trim univariant lines
851
+ for uni in self.unilines.values():
852
+ ln = area.intersection(uni.shape(ratio=self.ratio, tolerance=tolerance))
853
+ if ln.geom_type == "LineString" and not ln.is_empty:
854
+ lns.append((uni.id, ln))
855
+ if ln.geom_type == "MultiLineString":
856
+ for ln_part in ln.geoms:
857
+ if ln_part.geom_type == "LineString" and not ln_part.is_empty:
858
+ lns.append((uni.id, ln_part))
859
+ # split boundaries
860
+ do = True
861
+ while do:
862
+ edges, do = splitme(edges)
863
+ # polygonize
864
+ polys = list(polygonize(edges + [l for _, l in lns]))
865
+ # create shapes
866
+ shapes = {}
867
+ unilists = {}
868
+ tosolve = []
869
+ for ix, poly in enumerate(polys):
870
+ unilist = []
871
+ for uni_id, ln in lns:
872
+ if ln.relate_pattern(poly, "*1*F*****"):
873
+ unilist.append(uni_id)
874
+ if unilist:
875
+ phases = set.intersection(*(self.unilines[id].phases for id in unilist))
876
+ vd = [
877
+ phases.symmetric_difference(self.unilines[id].phases)
878
+ == self.unilines[id].out
879
+ or not phases.symmetric_difference(self.unilines[id].phases)
880
+ or phases.symmetric_difference(self.unilines[id].phases).union(
881
+ self.unilines[id].out
882
+ )
883
+ in polymorphs
884
+ for id in unilist
885
+ ]
886
+ if all(vd):
887
+ if frozenset(phases) in shapes:
888
+ # multivariant field crossed just by single univariant line
889
+ if len(unilist) == 1:
890
+ if self.unilines[unilist[0]].out.issubset(phases):
891
+ phases = phases.difference(
892
+ self.unilines[unilist[0]].out
893
+ )
894
+ shapes[frozenset(phases)] = poly
895
+ unilists[frozenset(phases)] = unilist
896
+ elif len(unilists[frozenset(phases)]) == 1:
897
+ if self.unilines[
898
+ unilists[frozenset(phases)][0]
899
+ ].out.issubset(phases):
900
+ orig_unilist = unilists[frozenset(phases)]
901
+ shapes[frozenset(phases)] = poly
902
+ unilists[frozenset(phases)] = unilist
903
+ phases = phases.difference(
904
+ self.unilines[orig_unilist[0]].out
905
+ )
906
+ shapes[frozenset(phases)] = poly
907
+ unilists[frozenset(phases)] = orig_unilist
908
+ else:
909
+ shapes[frozenset(phases)] = (
910
+ shapes[frozenset(phases)].union(poly).buffer(0.00001)
911
+ )
912
+ log.append(
913
+ "Area defined by unilines {} is self-intersecting with {}.".format(
914
+ " ".join([str(id) for id in unilist]),
915
+ " ".join(
916
+ [str(id) for id in unilists[frozenset(phases)]]
917
+ ),
918
+ )
919
+ )
920
+ unilists[frozenset(phases)] = list(
921
+ set(unilists[frozenset(phases)] + unilist)
922
+ )
923
+ else:
924
+ if len(unilist) == 1:
925
+ tosolve.append((unilist, phases, poly))
926
+ else:
927
+ shapes[frozenset(phases)] = poly
928
+ unilists[frozenset(phases)] = unilist
929
+ else:
930
+ log.append(
931
+ "Area defined by unilines {} is not valid field.".format(
932
+ " ".join([str(id) for id in unilist])
933
+ )
934
+ )
935
+ for unilist, phases, poly in tosolve:
936
+ if len(unilist) == 1 and self.unilines[unilist[0]].out.issubset(
937
+ set.union(*polymorphs)
938
+ ):
939
+ phases = self.unilines[unilist[0]].phases.difference(
940
+ self.unilines[unilist[0]].out
941
+ )
942
+ if frozenset(phases) in shapes: # switch polymorph
943
+ for pp in polymorphs:
944
+ if self.unilines[unilist[0]].out.issubset(pp):
945
+ phases = self.unilines[unilist[0]].phases.difference(
946
+ pp.difference(self.unilines[unilist[0]].out)
947
+ )
948
+ break
949
+ shapes[frozenset(phases)] = poly
950
+ unilists[frozenset(phases)] = unilist
951
+ elif frozenset(phases) in shapes:
952
+ phases = frozenset(phases.difference(self.unilines[unilist[0]].out))
953
+ shapes[frozenset(phases)] = poly
954
+ unilists[frozenset(phases)] = unilist
955
+ else:
956
+ shapes[frozenset(phases)] = poly
957
+ unilists[frozenset(phases)] = unilist
958
+ return shapes, unilists, log
959
+
960
+ def show(self):
961
+ for ln in self.unilines.values():
962
+ plt.plot(ln.x, ln.y, "k-")
963
+
964
+ for ln in self.invpoints.values():
965
+ plt.plot(ln.x, ln.y, "ro")
966
+
967
+ plt.xlim(self.xrange)
968
+ plt.ylim(self.yrange)
969
+ plt.xlabel(self.x_var_label)
970
+ plt.xlabel(self.y_var_label)
971
+ plt.show()
972
+
973
+ @staticmethod
974
+ def read_file(projfile):
975
+ with gzip.open(str(projfile), "rb") as stream:
976
+ data = pickle.load(stream)
977
+ return data
978
+
979
+ @staticmethod
980
+ def from_file(projfile):
981
+ with gzip.open(str(projfile), "rb") as stream:
982
+ data = pickle.load(stream)
983
+ return data["section"]
984
+
985
+
986
+ class PTsection(SectionBase):
987
+ """P-T pseudosection class"""
988
+
989
+ def __init__(self, **kwargs):
990
+ self.xrange = kwargs.get("trange", (200.0, 1000.0))
991
+ self.yrange = kwargs.get("prange", (0.1, 20.0))
992
+ self.x_var = "T"
993
+ self.x_var_label = "Temperature [C]"
994
+ self.x_var_res = 0.01
995
+ self.y_var = "p"
996
+ self.y_var_label = "Pressure [kbar]"
997
+ self.y_var_res = 0.001
998
+ super(PTsection, self).__init__(**kwargs)
999
+
1000
+
1001
+ class TXsection(SectionBase):
1002
+ """T-X pseudosection class"""
1003
+
1004
+ def __init__(self, **kwargs):
1005
+ self.xrange = kwargs.get("trange", (200.0, 1000.0))
1006
+ self.yrange = (0.0, 1.0)
1007
+ self.x_var = "T"
1008
+ self.x_var_label = "Temperature [C]"
1009
+ self.x_var_res = 0.01
1010
+ self.y_var = "C"
1011
+ self.y_var_label = "Composition"
1012
+ self.y_var_res = 0.001
1013
+ super(TXsection, self).__init__(**kwargs)
1014
+
1015
+
1016
+ class PXsection(SectionBase):
1017
+ """P-X pseudosection class"""
1018
+
1019
+ def __init__(self, **kwargs):
1020
+ self.xrange = (0.0, 1.0)
1021
+ self.yrange = kwargs.get("prange", (0.1, 20.0))
1022
+ self.x_var = "C"
1023
+ self.x_var_label = "Composition"
1024
+ self.x_var_res = 0.001
1025
+ self.y_var = "p"
1026
+ self.y_var_label = "Pressure [kbar]"
1027
+ self.y_var_res = 0.001
1028
+ super(PXsection, self).__init__(**kwargs)
1029
+
1030
+
1031
+ # Descartes https://github.com/benjimin/descartes
1032
+
1033
+
1034
+ class Polygon(object):
1035
+ # Adapt Shapely or GeoJSON/geo_interface polygons to a common interface
1036
+ def __init__(self, context):
1037
+ if hasattr(context, "interiors"):
1038
+ self.context = context
1039
+ else:
1040
+ self.context = getattr(context, "__geo_interface__", context)
1041
+
1042
+ @property
1043
+ def geom_type(self):
1044
+ return getattr(self.context, "geom_type", None) or self.context["type"]
1045
+
1046
+ @property
1047
+ def exterior(self):
1048
+ return getattr(self.context, "exterior", None) or self.context["coordinates"][0]
1049
+
1050
+ @property
1051
+ def interiors(self):
1052
+ value = getattr(self.context, "interiors", None)
1053
+ if value is None:
1054
+ value = self.context["coordinates"][1:]
1055
+ return value
1056
+
1057
+
1058
+ def PolygonPath(polygon):
1059
+ """Constructs a compound matplotlib path from a Shapely or GeoJSON-like
1060
+ geometric object"""
1061
+ this = Polygon(polygon)
1062
+ assert this.geom_type == "Polygon"
1063
+
1064
+ def coding(ob):
1065
+ # The codes will be all "LINETO" commands, except for "MOVETO"s at the
1066
+ # beginning of each subpath
1067
+ n = len(getattr(ob, "coords", None) or ob)
1068
+ vals = np.ones(n, dtype=Path.code_type) * Path.LINETO
1069
+ vals[0] = Path.MOVETO
1070
+ return vals
1071
+
1072
+ vertices = np.concatenate(
1073
+ [np.asarray(this.exterior.coords)[:, :2]]
1074
+ + [np.asarray(r.coords)[:, :2] for r in this.interiors]
1075
+ )
1076
+ codes = np.concatenate(
1077
+ [coding(this.exterior)] + [coding(r) for r in this.interiors]
1078
+ )
1079
+ return Path(vertices, codes)
1080
+
1081
+
1082
+ def PolygonPatch(polygon, **kwargs):
1083
+ """Constructs a matplotlib patch from a geometric object
1084
+
1085
+ The `polygon` may be a Shapely or GeoJSON-like object with or without holes.
1086
+ The `kwargs` are those supported by the matplotlib.patches.Polygon class
1087
+ constructor. Returns an instance of matplotlib.patches.PathPatch.
1088
+ Example (using Shapely Point and a matplotlib axes):
1089
+ >>> b = Point(0, 0).buffer(1.0)
1090
+ >>> patch = PolygonPatch(b, fc='blue', ec='blue', alpha=0.5)
1091
+ >>> axis.add_patch(patch)
1092
+ """
1093
+ return PathPatch(PolygonPath(polygon), **kwargs)