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.
pypsbuilder/tcapi.py ADDED
@@ -0,0 +1,1681 @@
1
+ """THERMOCALC API classes.
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
+ Todo:
7
+ * Implement own class for divariant fields
8
+
9
+ """
10
+
11
+ # author: Ondrej Lexa
12
+ # website: https://github.com/ondrolexa/pypsbuilder
13
+
14
+ import sys
15
+ import os
16
+ import subprocess
17
+
18
+ # import itertools
19
+ # import re
20
+ from pathlib import Path
21
+
22
+ # from collections import OrderedDict
23
+ import numpy as np
24
+ from .psclasses import TCResult, TCResultSet
25
+
26
+ popen_kw = dict(
27
+ stdout=subprocess.PIPE,
28
+ stdin=subprocess.PIPE,
29
+ stderr=subprocess.STDOUT,
30
+ universal_newlines=False,
31
+ )
32
+
33
+
34
+ def get_tcapi(workdir=".", TCenc="mac-roman"):
35
+ workdir = Path(workdir).resolve()
36
+ try:
37
+ errinfo = "Initialize project error!"
38
+ tcexe = None
39
+ drexe = None
40
+ # default exe
41
+ if sys.platform.startswith("win"):
42
+ tcpat = "tc3*.exe"
43
+ else:
44
+ tcpat = "tc3*"
45
+ # THERMOCALC exe
46
+ for p in workdir.glob(tcpat):
47
+ if p.is_file() and os.access(str(p), os.X_OK):
48
+ tcexe = p.resolve()
49
+ break
50
+ # default exe
51
+ if sys.platform.startswith("win"):
52
+ drpat = "dr1*.exe"
53
+ else:
54
+ drpat = "dr1*"
55
+ # DRAWPD exe
56
+ for p in workdir.glob(drpat):
57
+ if p.is_file() and os.access(str(p), os.X_OK):
58
+ drexe = p.resolve()
59
+ break
60
+ if not tcexe:
61
+ raise InitError("No THERMOCALC executable in {} directory.".format(workdir))
62
+ # TC version
63
+ errinfo = "THERMOCALC executable test."
64
+ if sys.platform.startswith("win"):
65
+ startupinfo = subprocess.STARTUPINFO()
66
+ startupinfo.dwFlags = 1
67
+ startupinfo.wShowWindow = 0
68
+ else:
69
+ startupinfo = None
70
+ p = subprocess.Popen(
71
+ str(tcexe), cwd=str(workdir), startupinfo=startupinfo, **popen_kw
72
+ )
73
+ out, err = p.communicate(input="kill\n\n".encode(TCenc))
74
+ if err is not None:
75
+ print(err.decode("utf-8"))
76
+ sys.stdout.flush()
77
+ output = out.decode(TCenc)
78
+ if float(output.split("\n")[0].split()[1]) < 3.5:
79
+ api = TC34API(workdir, tcexe, drexe)
80
+ if api.OK:
81
+ return api, True
82
+ else:
83
+ return api.status, False
84
+ else:
85
+ api = TC35API(workdir, tcexe, drexe)
86
+ if api.OK:
87
+ return api, True
88
+ else:
89
+ return api.status, False
90
+ except BaseException as e:
91
+ if (
92
+ isinstance(e, InitError)
93
+ or isinstance(e, ScriptfileError)
94
+ or isinstance(e, TCError)
95
+ ):
96
+ status = "{}: {}".format(type(e).__name__, str(e))
97
+ else:
98
+ status = "{}: {} {}".format(type(e).__name__, str(e), errinfo)
99
+ return status, False
100
+
101
+
102
+ class InitError(Exception):
103
+ pass
104
+
105
+
106
+ class ScriptfileError(Exception):
107
+ pass
108
+
109
+
110
+ class TCError(Exception):
111
+ pass
112
+
113
+
114
+ class TCAPI(object):
115
+ """THERMOCALC working directory API.
116
+
117
+ Attributes:
118
+ workdir (pathlib.Path): Path instance pointing to working directory.
119
+ tcexe (pathlib.Path): Path instance pointing to *THERMOCALC* executable.
120
+ drexe (pathlib.Path): Path instance pointing to *dawpd* executable
121
+ name (str): Basename of the project.
122
+ axname (str): Name of a-x file in use.
123
+ OK (bool): Boolean value. True when all settings are correct and
124
+ THERMOCALC is ready to be used by builders.
125
+ excess (set): Set of excess phases from scriptfile.
126
+ trange (tuple): Tuple of temperature window from setdefTwindow
127
+ prange (tuple): Tuple of pressure window from setdefPwindow
128
+ bulk (list): List of bulk composition(s).
129
+ ptx_steps (int): Number of compositional steps for T-X and P-X sections.
130
+ phases (list): List of names of available phases.
131
+ TCenc (str): Encoding used for THERMOCALC output text files.
132
+ Default 'mac-roman'.
133
+
134
+ Raises:
135
+ InitError: An error occurred during initialization of working dir.
136
+ ScriptfileError: Error or problem in scriptfile.
137
+ TCError: THERMOCALC bombed.
138
+
139
+ """
140
+
141
+ def __str__(self):
142
+ return str(self.workdir)
143
+
144
+ def __repr__(self):
145
+ if self.OK:
146
+ return "\n".join(
147
+ [
148
+ "{}".format(self.tcversion),
149
+ "Working directory: {}".format(self.workdir),
150
+ "Scriptfile: {}".format("tc-" + self.name + ".txt"),
151
+ "AX file: {}".format("tc-" + self.axname + ".txt"),
152
+ "Status: {}".format(self.status),
153
+ ]
154
+ )
155
+ else:
156
+ return "\n".join(
157
+ [
158
+ "Uninitialized working directory {}".format(self.workdir),
159
+ "Status: {}".format(self.status),
160
+ ]
161
+ )
162
+
163
+ @property
164
+ def scriptfile(self):
165
+ """pathlib.Path: Path to scriptfile."""
166
+ return self.workdir.joinpath("tc-" + self.name + ".txt")
167
+
168
+ def read_scriptfile(self):
169
+ with self.scriptfile.open("r", encoding=self.TCenc) as f:
170
+ r = f.read()
171
+ return r
172
+
173
+ @property
174
+ def drfile(self):
175
+ """pathlib.Path: Path to -dr output file."""
176
+ return self.workdir.joinpath("tc-" + self.name + "-dr.txt")
177
+
178
+ @property
179
+ def logfile(self):
180
+ """pathlib.Path: Path to THERMOCALC log file."""
181
+ return self.workdir.joinpath("tc-log.txt")
182
+
183
+ @property
184
+ def icfile(self):
185
+ """pathlib.Path: Path to ic file."""
186
+ return self.workdir.joinpath("tc-" + self.name + "-ic.txt")
187
+
188
+ @property
189
+ def itfile(self):
190
+ """pathlib.Path: Path to it file."""
191
+ return self.workdir.joinpath("tc-" + self.name + "-it.txt")
192
+
193
+ @property
194
+ def ofile(self):
195
+ """pathlib.Path: Path to project output file."""
196
+ return self.workdir.joinpath("tc-" + self.name + "-o.txt")
197
+
198
+ @property
199
+ def csvfile(self):
200
+ """pathlib.Path: Path to csv file."""
201
+ return self.workdir.joinpath("tc-" + self.name + "-csv.txt")
202
+
203
+ @property
204
+ def drawpdfile(self):
205
+ """pathlib.Path: Path to drawpd file."""
206
+ return self.workdir.joinpath("dr-" + self.name + ".txt")
207
+
208
+ @property
209
+ def axfile(self):
210
+ """pathlib.Path: Path to used a-x file."""
211
+ return self.workdir.joinpath("tc-" + self.axname + ".txt")
212
+
213
+ @property
214
+ def prefsfile(self):
215
+ """pathlib.Path: Path to THERMOCALC prefs file."""
216
+ return self.workdir.joinpath("tc-prefs.txt")
217
+
218
+ def read_prefsfile(self):
219
+ with self.prefsfile.open("r", encoding=self.TCenc) as f:
220
+ r = f.read()
221
+ return r
222
+
223
+ @property
224
+ def tcversion(self):
225
+ """str: THERMOCALC version string"""
226
+ return self.tcout.split("\n")[0]
227
+
228
+ @property
229
+ def tcnewversion(self):
230
+ """bool: False for THERMOCALC older than 3.5."""
231
+ return not float(self.tcversion.split()[1]) < 3.5
232
+
233
+ @property
234
+ def datasetfile(self):
235
+ """pathlib.Path: Path to dataset file."""
236
+ return self.workdir.joinpath(self.dataset.split(" produced")[0])
237
+
238
+ @property
239
+ def dataset(self):
240
+ """str: Version identification of thermodynamic dataset in use."""
241
+ return self.tcout.split("using ")[1].split("\n")[0]
242
+
243
+ def parse_dogmin(self):
244
+ """Dogmin parser."""
245
+ try:
246
+ with self.icfile.open("r", encoding=self.TCenc) as f:
247
+ resic = f.read()
248
+ except Exception:
249
+ resic = None
250
+ try:
251
+ with self.logfile.open("r", encoding=self.TCenc) as f:
252
+ output = f.read().split(
253
+ "##########################################################\n"
254
+ )[-1]
255
+ except Exception:
256
+ output = None
257
+ return output, resic
258
+
259
+ def runtc(self, instr="kill\n\n"):
260
+ """Low-level method to actually run THERMOCALC.
261
+
262
+ Args:
263
+ instr (str): String to be passed to standard input for session.
264
+
265
+ Returns:
266
+ str: THERMOCALC standard output
267
+ """
268
+ if sys.platform.startswith("win"):
269
+ startupinfo = subprocess.STARTUPINFO()
270
+ startupinfo.dwFlags = 1
271
+ startupinfo.wShowWindow = 0
272
+ else:
273
+ startupinfo = None
274
+ p = subprocess.Popen(
275
+ str(self.tcexe), cwd=str(self.workdir), startupinfo=startupinfo, **popen_kw
276
+ )
277
+ output, err = p.communicate(input=instr.encode(self.TCenc))
278
+ if err is not None:
279
+ print(err.decode("utf-8"))
280
+ sys.stdout.flush()
281
+ return output.decode(self.TCenc)
282
+
283
+ def rundr(self):
284
+ """Method to run drawpd."""
285
+ if self.drexe:
286
+ instr = "\n" + self.name + "\n"
287
+ if sys.platform.startswith("win"):
288
+ startupinfo = subprocess.STARTUPINFO()
289
+ startupinfo.dwFlags = 1
290
+ startupinfo.wShowWindow = 0
291
+ else:
292
+ startupinfo = None
293
+ p = subprocess.Popen(
294
+ str(self.drexe),
295
+ cwd=str(self.workdir),
296
+ startupinfo=startupinfo,
297
+ **popen_kw
298
+ )
299
+ p.communicate(input=instr.encode(self.TCenc))
300
+ sys.stdout.flush()
301
+ return True
302
+ else:
303
+ print("No drawpd executable identified in working directory.")
304
+ return False
305
+
306
+
307
+ class TC35API(TCAPI):
308
+ def __init__(self, workdir, tcexe, drexe, encoding="mac-roman"):
309
+ self.workdir = Path(workdir).resolve()
310
+ self.tcexe = tcexe
311
+ self.drexe = drexe
312
+ self.TCenc = encoding
313
+ try:
314
+ # TC version
315
+ errinfo = "THERMOCALC executable test."
316
+ self.tcout = self.runtc("\nkill\n\n")
317
+ # tc-prefs file
318
+ if not self.workdir.joinpath("tc-prefs.txt").exists():
319
+ raise InitError("No tc-prefs.txt file in working directory.")
320
+ errinfo = "tc-prefs.txt file in working directory cannot be accessed."
321
+ for line in self.workdir.joinpath("tc-prefs.txt").open(
322
+ "r", encoding=self.TCenc
323
+ ):
324
+ kw = line.split()
325
+ if kw != []:
326
+ if kw[0] == "scriptfile":
327
+ self.name = kw[1]
328
+ if not self.scriptfile.exists():
329
+ raise InitError(
330
+ "tc-prefs: scriptfile tc-"
331
+ + self.name
332
+ + ".txt does not exists in your working directory."
333
+ )
334
+ if len(self.name) > 15:
335
+ raise InitError(
336
+ "tc-prefs: scriptfile name is longer than 15 characters. It cause troubles for THERMOCALC. Please rename scriptfile to shorter name."
337
+ )
338
+ if kw[0] == "calcmode":
339
+ if kw[1] != "1":
340
+ raise InitError("tc-prefs: calcmode must be 1.")
341
+ if kw[0] == "dontwrap":
342
+ if kw[1] != "no":
343
+ raise InitError("tc-prefs: dontwrap must be no.")
344
+ # defaults
345
+ self.ptx_steps = 20 # IS IT NEEDED ????
346
+ # Checks run output
347
+ if "exit, and correct the scriptfile ?" in self.tcout:
348
+ raise ScriptfileError(
349
+ self.tcout.split("exit, and correct the scriptfile ?")[0].split(
350
+ "\n"
351
+ )[-4]
352
+ )
353
+ # Checks various settings
354
+ errinfo = "Scriptfile error!"
355
+ with self.scriptfile.open("r", encoding=self.TCenc) as f:
356
+ r = f.read()
357
+ lines = [ln.strip() for ln in r.splitlines() if ln.strip() != ""]
358
+ lines = lines[: lines.index("*")] # remove part not used by TC
359
+ # Check pypsbuilder blocks
360
+ if not ("%{PSBGUESS-BEGIN}" in lines and "%{PSBGUESS-END}" in lines):
361
+ raise ScriptfileError(
362
+ "There are not {PSBGUESS-BEGIN} and {PSBGUESS-END} tags in your scriptfile."
363
+ )
364
+ if not ("%{PSBCALC-BEGIN}" in lines and "%{PSBCALC-END}" in lines):
365
+ raise ScriptfileError(
366
+ "There are not {PSBCALC-BEGIN} and {PSBCALC-END} tags in your scriptfile."
367
+ )
368
+ if not ("%{PSBBULK-BEGIN}" in lines and "%{PSBBULK-END}" in lines):
369
+ raise ScriptfileError(
370
+ "There are not {PSBBULK-BEGIN} and {PSBBULK-END} tags in your scriptfile."
371
+ )
372
+ # Create scripts directory
373
+ scripts = {}
374
+ for ln in lines:
375
+ ln_clean = ln.split("%")[0].strip()
376
+ if ln_clean != "":
377
+ tokens = ln_clean.split(maxsplit=1)
378
+ if len(tokens) > 1:
379
+ if tokens[0] in scripts:
380
+ scripts[tokens[0]].append(tokens[1].strip())
381
+ else:
382
+ scripts[tokens[0]] = [tokens[1].strip()]
383
+ else:
384
+ scripts[tokens[0]] = []
385
+ # axfile
386
+ if "axfile" not in scripts:
387
+ raise ScriptfileError("No axfile script, axfile is mandatory script.")
388
+ errinfo = "Missing argument for axfile script in scriptfile."
389
+ self.axname = scripts["axfile"][0]
390
+ if not self.axfile.exists():
391
+ raise ScriptfileError(
392
+ "axfile "
393
+ + str(self.axfile)
394
+ + " does not exists in working directory"
395
+ )
396
+ # diagramPT
397
+ if "diagramPT" not in scripts:
398
+ raise ScriptfileError(
399
+ "No diagramPT script, diagramPT is mandatory script."
400
+ )
401
+ errinfo = "Wrong arguments for diagramPT script in scriptfile."
402
+ pmin, pmax, tmin, tmax = scripts["diagramPT"][0].split()
403
+ self.prange = float(pmin), float(pmax)
404
+ self.trange = float(tmin), float(tmax)
405
+ # bulk
406
+ errinfo = "Wrong bulk in scriptfile."
407
+ if "bulk" not in scripts:
408
+ raise ScriptfileError("No bulk script, bulk must be provided.")
409
+ if not (1 < len(scripts["bulk"]) < 4):
410
+ raise ScriptfileError("Bulk script must have 2 or 3 lines.")
411
+ self.bulk = []
412
+ self.bulk.append(scripts["bulk"][0].split())
413
+ self.bulk.append(scripts["bulk"][1].split())
414
+ if len(scripts["bulk"]) == 3:
415
+ self.bulk.append(
416
+ scripts["bulk"][2].split()[: len(self.bulk[0])]
417
+ ) # remove possible number of steps
418
+ # try to get actual bulk from output
419
+ self.usedbulk = None
420
+ if "specification of bulk composition" in self.tcout:
421
+ try:
422
+ part = self.tcout.split("specification of bulk composition")[
423
+ 1
424
+ ].split(
425
+ "<==========================================================>"
426
+ )[
427
+ 0
428
+ ]
429
+ lns = [ln for ln in part.split(os.linesep) if ln.startswith(" ")]
430
+ self.usedbulk = []
431
+ self.usedbulk.append(lns[0].split())
432
+ self.usedbulk.append(lns[1].split())
433
+ if len(lns) == 3:
434
+ self.usedbulk.append(lns[2].split()[: len(self.usedbulk[0])])
435
+ except BaseException as e:
436
+ self.usedbulk = None
437
+ # inexcess
438
+ errinfo = "Wrong inexcess in scriptfile."
439
+ if "setexcess" in scripts:
440
+ raise ScriptfileError(
441
+ "setexcess script depreceated, use inexcess instead."
442
+ )
443
+ if "inexcess" in scripts:
444
+ if scripts["inexcess"]:
445
+ self.excess = set(scripts["inexcess"][0].split()) - set(["no"])
446
+ else:
447
+ raise ScriptfileError(
448
+ "In case of no excess phases, use inexcess no or remove script"
449
+ )
450
+ else:
451
+ self.excess = set()
452
+ # omit
453
+ errinfo = "Wrong omit in scriptfile."
454
+ if "omit" in scripts:
455
+ self.omit = set(scripts["omit"][0].split())
456
+ else:
457
+ self.omit = set()
458
+ # autoexit
459
+ if "autoexit" not in scripts:
460
+ raise ScriptfileError("No autoexit script, autoexit must be provided.")
461
+ # acceptvar
462
+ if "acceptvar" in scripts:
463
+ if scripts["acceptvar"][0] == "no":
464
+ raise ScriptfileError("Acceptvar script must be yes or removed")
465
+ # with
466
+ if "with" in scripts:
467
+ if scripts["with"][0].split()[0] == "someof":
468
+ raise ScriptfileError(
469
+ "Pypsbuilder does not support with sameof <phase list>. Use omit with and omit."
470
+ )
471
+ # samecoding
472
+ if "samecoding" in scripts:
473
+ self.samecoding = [set(sc.split()) for sc in scripts["samecoding"]]
474
+ else:
475
+ self.samecoding = [set()]
476
+ # pseudosection
477
+ if "pseudosection" not in scripts:
478
+ raise ScriptfileError(
479
+ "No pseudosection script, pseudosection is mandatory script."
480
+ )
481
+ # dogmin
482
+ if "dogmin" in scripts:
483
+ raise ScriptfileError(
484
+ "Dogmin script should be removed from scriptfile."
485
+ )
486
+ # union ax phases and samecoding and diff omit
487
+ if "BOMBED" in self.tcout:
488
+ raise TCError(self.tcout.split("BOMBED")[1].split(os.linesep)[0])
489
+ else:
490
+ ax_phases = set(
491
+ self.tcout.split("reading ax:")[1].split(2 * os.linesep)[0].split()
492
+ )
493
+ self.phases = ax_phases.union(*self.samecoding) - self.omit
494
+ # OK
495
+ self.status = "Initial check done."
496
+ self.OK = True
497
+ except BaseException as e:
498
+ if (
499
+ isinstance(e, InitError)
500
+ or isinstance(e, ScriptfileError)
501
+ or isinstance(e, TCError)
502
+ ):
503
+ self.status = "{}: {}".format(type(e).__name__, str(e))
504
+ else:
505
+ exc_type, exc_obj, exc_tb = sys.exc_info()
506
+ self.status = "{}: {} on line {}".format(
507
+ type(e).__name__, str(e), exc_tb.tb_lineno
508
+ )
509
+ self.OK = False
510
+
511
+ def parse_logfile(self, **kwargs):
512
+ """Parser for THERMOCALC 3.50beta output.
513
+
514
+ It parses the outputs of THERMOCALC after calculation.
515
+
516
+ Args:
517
+ output (str): When not None, used as content of logfile. Default None.
518
+ resic (str): When not None, used as content of icfile. Default None.
519
+ get_phases (bool): When true returns also tuple (phases, out, calcs). Default False
520
+
521
+ Returns:
522
+ status (str): Result of parsing. 'ok', 'nir' (nothing in range) or 'bombed'.
523
+ results (TCResultSet): Results of TC calculation.
524
+ output (str): Full nonparsed THERMOCALC output.
525
+
526
+ Example:
527
+ Parse output after univariant line calculation in P-T pseudosection::
528
+
529
+ >>> tc = TCAPI('pat/to/dir')
530
+ >>> status, result, output = tc.parse_logfile()
531
+ """
532
+ output = kwargs.get("output", None)
533
+ resic = kwargs.get("resic", None)
534
+ get_phases = kwargs.get("get_phases", False)
535
+ try:
536
+ if output is None:
537
+ with self.logfile.open("r", encoding=self.TCenc) as f:
538
+ output = f.read().split(
539
+ "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n"
540
+ )[1]
541
+ lines = [ln for ln in output.splitlines() if ln != ""]
542
+ results = None
543
+ do_parse = True
544
+ if resic is None:
545
+ if not self.icfile.exists():
546
+ if [ix for ix, ln in enumerate(lines) if "BOMBED" in ln]:
547
+ status = "bombed"
548
+ else:
549
+ status = "nir"
550
+ do_parse = False
551
+ else:
552
+ with self.icfile.open("r", encoding=self.TCenc) as f:
553
+ resic = f.read()
554
+ if do_parse:
555
+ lines = [ln for ln in output.splitlines() if ln != ""]
556
+ # parse ptguesses
557
+ bstarts = [
558
+ ix
559
+ for ix, ln in enumerate(lines)
560
+ if ln.startswith(
561
+ "------------------------------------------------------------"
562
+ )
563
+ ]
564
+ bstarts.append(len(lines))
565
+ ptguesses = []
566
+ corrects = []
567
+ for bs, be in zip(bstarts[:-1], bstarts[1:]):
568
+ block = lines[bs:be]
569
+ if block[2].startswith("#"):
570
+ corrects.append(False)
571
+ else:
572
+ corrects.append(True)
573
+ xyz = [
574
+ ix for ix, ln in enumerate(block) if ln.startswith("xyzguess")
575
+ ]
576
+ gixs = [
577
+ ix for ix, ln in enumerate(block) if ln.startswith("ptguess")
578
+ ][0] - 3
579
+ gixe = xyz[-1] + 2
580
+ ptguesses.append(block[gixs:gixe])
581
+ # parse icfile
582
+ blocks = resic.split(
583
+ "\n===========================================================\n\n"
584
+ )[1:]
585
+ # done
586
+ if len(blocks) > 0:
587
+ rlist = [
588
+ TCResult.from_block(block, ptguess)
589
+ for block, ptguess, correct in zip(blocks, ptguesses, corrects)
590
+ if correct
591
+ ]
592
+ if len(rlist) > 0:
593
+ status = "ok"
594
+ results = TCResultSet(rlist)
595
+ else:
596
+ status = "nir"
597
+ else:
598
+ status = "nir"
599
+ if get_phases:
600
+ phases, out = None, None
601
+ with self.scriptfile.open("r", encoding=self.TCenc) as f:
602
+ scf = f.read()
603
+ _, rem = scf.split("%{PSBCALC-BEGIN}")
604
+ calc, _ = rem.split("%{PSBCALC-END}")
605
+ calcs = calc.splitlines()
606
+ for ln in calcs:
607
+ script = ln.split("%")[0].strip()
608
+ if script.startswith("with"):
609
+ phases = set(script.split("with", 1)[1].split()).union(
610
+ self.excess
611
+ )
612
+ if script.startswith("zeromodeisopleth"):
613
+ out = set(script.split("zeromodeisopleth", 1)[1].split())
614
+ return status, results, output, (phases, out, calcs)
615
+ else:
616
+ return status, results, output
617
+ except Exception:
618
+ if get_phases:
619
+ return "bombed", None, None, (None, None, [])
620
+ else:
621
+ return "bombed", None, None
622
+
623
+ def parse_logfile_backup(self, **kwargs):
624
+ output = kwargs.get("output", None)
625
+ resic = kwargs.get("resic", None)
626
+ if output is None:
627
+ with self.logfile.open("r", encoding=self.TCenc) as f:
628
+ output = f.read()
629
+ lines = [ln for ln in output.splitlines() if ln != ""]
630
+ results = None
631
+ do_parse = True
632
+ if resic is None:
633
+ if not self.icfile.exists():
634
+ if [ix for ix, ln in enumerate(lines) if "BOMBED" in ln]:
635
+ status = "bombed"
636
+ else:
637
+ status = "nir"
638
+ do_parse = False
639
+ else:
640
+ with self.icfile.open("r", encoding=self.TCenc) as f:
641
+ resic = f.read()
642
+ if do_parse:
643
+ lines = [ln for ln in output.splitlines() if ln != ""]
644
+ # parse ptguesses
645
+ bstarts = [
646
+ ix
647
+ for ix, ln in enumerate(lines)
648
+ if ln.startswith(
649
+ "--------------------------------------------------------------------"
650
+ )
651
+ ]
652
+ bstarts.append(len(lines))
653
+ ptguesses = []
654
+ corrects = []
655
+ for bs, be in zip(bstarts[:-1], bstarts[1:]):
656
+ block = lines[bs:be]
657
+ if block[2].startswith("#"):
658
+ corrects.append(False)
659
+ else:
660
+ corrects.append(True)
661
+ xyz = [ix for ix, ln in enumerate(block) if ln.startswith("xyzguess")]
662
+ gixs = [ix for ix, ln in enumerate(block) if ln.startswith("ptguess")][
663
+ 0
664
+ ] - 3
665
+ gixe = xyz[-1] + 2
666
+ ptguesses.append(block[gixs:gixe])
667
+ # parse icfile
668
+ blocks = resic.split(
669
+ "\n===========================================================\n\n"
670
+ )[1:]
671
+ # done
672
+ if len(blocks) > 0:
673
+ rlist = [
674
+ TCResult.from_block(block, ptguess)
675
+ for block, ptguess, correct in zip(blocks, ptguesses, corrects)
676
+ if correct
677
+ ]
678
+ if len(rlist) > 0:
679
+ status = "ok"
680
+ results = TCResultSet(rlist)
681
+ else:
682
+ status = "nir"
683
+ else:
684
+ status = "nir"
685
+ return status, results, output
686
+
687
+ def update_scriptfile(self, **kwargs):
688
+ """Method to update scriptfile.
689
+
690
+ This method is used to programatically edit scriptfile.
691
+
692
+ Kwargs:
693
+ calcs: List of lines defining fully hands-off calculations. Default None.
694
+ get_old_calcs: When True method returns existing calcs lines
695
+ before possible modification. Default False.
696
+ guesses: List of lines defining ptguesses. If None guesses
697
+ are not modified. Default None.
698
+ get_old_guesses: When True method returns existing ptguess lines
699
+ before possible modification. Default False.
700
+ bulk: List of lines defining bulk composition. Default None.
701
+ xsteps: Number of compositional steps between two bulks.
702
+ Default 20.
703
+ """
704
+ calcs = kwargs.get("calcs", None)
705
+ get_old_calcs = kwargs.get("get_old_calcs", False)
706
+ guesses = kwargs.get("guesses", None)
707
+ get_old_guesses = kwargs.get("get_old_guesses", False)
708
+ bulk = kwargs.get("bulk", None)
709
+ xsteps = kwargs.get("xsteps", None)
710
+ with self.scriptfile.open("r", encoding=self.TCenc) as f:
711
+ scf = f.read()
712
+ changed = False
713
+ scf_1, rem = scf.split("%{PSBCALC-BEGIN}")
714
+ old, scf_2 = rem.split("%{PSBCALC-END}")
715
+ old_calcs = old.strip().splitlines()
716
+ if calcs is not None:
717
+ scf = (
718
+ scf_1
719
+ + "%{PSBCALC-BEGIN}\n"
720
+ + "\n".join(calcs)
721
+ + "\n%{PSBCALC-END}"
722
+ + scf_2
723
+ )
724
+ changed = True
725
+ scf_1, rem = scf.split("%{PSBGUESS-BEGIN}")
726
+ old, scf_2 = rem.split("%{PSBGUESS-END}")
727
+ old_guesses = old.strip().splitlines()
728
+ if guesses is not None:
729
+ scf = (
730
+ scf_1
731
+ + "%{PSBGUESS-BEGIN}\n"
732
+ + "\n".join(guesses)
733
+ + "\n%{PSBGUESS-END}"
734
+ + scf_2
735
+ )
736
+ changed = True
737
+ if bulk is not None:
738
+ scf_1, rem = scf.split("%{PSBBULK-BEGIN}")
739
+ old, scf_2 = rem.split("%{PSBBULK-END}")
740
+ bulk_lines = []
741
+ if len(bulk) == 2:
742
+ bulk_lines.append("bulk {}".format(" ".join(bulk[0])))
743
+ bulk_lines.append("bulk {}".format(" ".join(bulk[1])))
744
+ else:
745
+ bulk_lines.append("bulk {}".format(" ".join(bulk[0])))
746
+ bulk_lines.append("bulk {}".format(" ".join(bulk[1])))
747
+ bulk_lines.append("bulk {} {}".format(" ".join(bulk[2]), xsteps))
748
+ scf = (
749
+ scf_1
750
+ + "%{PSBBULK-BEGIN}\n"
751
+ + "\n".join(bulk_lines)
752
+ + "\n%{PSBBULK-END}"
753
+ + scf_2
754
+ )
755
+ changed = True
756
+ if xsteps is not None:
757
+ bulk_lines = []
758
+ scf_1, rem = scf.split("%{PSBBULK-BEGIN}")
759
+ old, scf_2 = rem.split("%{PSBBULK-END}")
760
+ if len(self.bulk) == 3:
761
+ bulk_lines.append("bulk {}".format(" ".join(self.bulk[0])))
762
+ bulk_lines.append("bulk {}".format(" ".join(self.bulk[1])))
763
+ bulk_lines.append("bulk {} {}".format(" ".join(self.bulk[2]), xsteps))
764
+ scf = (
765
+ scf_1
766
+ + "%{PSBBULK-BEGIN}\n"
767
+ + "\n".join(bulk_lines)
768
+ + "\n%{PSBBULK-END}"
769
+ + scf_2
770
+ )
771
+ changed = True
772
+ if changed:
773
+ with self.scriptfile.open("w", encoding=self.TCenc) as f:
774
+ f.write(scf)
775
+ if get_old_calcs and get_old_guesses:
776
+ return old_calcs, old_guesses
777
+ elif get_old_calcs:
778
+ return old_calcs
779
+ elif get_old_guesses:
780
+ return old_guesses
781
+ else:
782
+ return None
783
+
784
+ def calc_t(self, phases, out, **kwargs):
785
+ """Method to run THERMOCALC to find univariant line using Calc T at P strategy.
786
+
787
+ Args:
788
+ phases (set): Set of present phases
789
+ out (set): Set of single zero mode phase
790
+ prange (tuple): Temperature range for calculation
791
+ trange (tuple): Pressure range for calculation
792
+ steps (int): Number of steps
793
+
794
+ Returns:
795
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
796
+ Input ans could be used to reproduce calculation.
797
+ """
798
+ prange = kwargs.get("prange", self.prange)
799
+ trange = kwargs.get("trange", self.trange)
800
+ steps = kwargs.get("steps", 50)
801
+ step = (prange[1] - prange[0]) / steps
802
+ calcs = [
803
+ "calcP {:g} {:g} {:g}".format(*prange, step),
804
+ "calcT {:g} {:g}".format(*trange),
805
+ "calctatp yes",
806
+ "with {}".format(" ".join(phases - self.excess)),
807
+ "zeromodeisopleth {}".format(" ".join(out)),
808
+ ]
809
+ self.update_scriptfile(calcs=calcs)
810
+ tcout = self.runtc()
811
+ return tcout, calcs
812
+
813
+ def calc_p(self, phases, out, **kwargs):
814
+ """Method to run THERMOCALC to find univariant line using Calc P at T strategy.
815
+
816
+ Args:
817
+ phases (set): Set of present phases
818
+ out (set): Set of single zero mode phase
819
+ prange (tuple): Temperature range for calculation
820
+ trange (tuple): Pressure range for calculation
821
+ steps (int): Number of steps
822
+
823
+ Returns:
824
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
825
+ Input ans could be used to reproduce calculation.
826
+ """
827
+ prange = kwargs.get("prange", self.prange)
828
+ trange = kwargs.get("trange", self.trange)
829
+ steps = kwargs.get("steps", 50)
830
+ step = (trange[1] - trange[0]) / steps
831
+ calcs = [
832
+ "calcP {:g} {:g}".format(*prange),
833
+ "calcT {:g} {:g} {:g}".format(*trange, step),
834
+ "calctatp no",
835
+ "with {}".format(" ".join(phases - self.excess)),
836
+ "zeromodeisopleth {}".format(" ".join(out)),
837
+ ]
838
+ self.update_scriptfile(calcs=calcs)
839
+ tcout = self.runtc()
840
+ return tcout, calcs
841
+
842
+ def calc_pt(self, phases, out, **kwargs):
843
+ """Method to run THERMOCALC to find invariant point.
844
+
845
+ Args:
846
+ phases (set): Set of present phases
847
+ out (set): Set of two zero mode phases
848
+ prange (tuple): Temperature range for calculation
849
+ trange (tuple): Pressure range for calculation
850
+
851
+ Returns:
852
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
853
+ Input ans could be used to reproduce calculation.
854
+ """
855
+ prange = kwargs.get("prange", self.prange)
856
+ trange = kwargs.get("trange", self.trange)
857
+ calcs = [
858
+ "calcP {:g} {:g}".format(*prange),
859
+ "calcT {:g} {:g}".format(*trange),
860
+ "with {}".format(" ".join(phases - self.excess)),
861
+ "zeromodeisopleth {}".format(" ".join(out)),
862
+ ]
863
+ self.update_scriptfile(calcs=calcs)
864
+ tcout = self.runtc()
865
+ return tcout, calcs
866
+
867
+ def calc_tx(self, phases, out, **kwargs):
868
+ """Method to run THERMOCALC for T-X pseudosection calculations.
869
+
870
+ Args:
871
+ phases (set): Set of present phases
872
+ out (set): Set of zero mode phases
873
+ prange (tuple): Temperature range for calculation
874
+ trange (tuple): Pressure range for calculation
875
+ xvals (tuple): range for X variable
876
+ steps (int): Number of steps
877
+
878
+ Returns:
879
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
880
+ Input ans could be used to reproduce calculation.
881
+ """
882
+ prange = kwargs.get("prange", self.prange)
883
+ trange = kwargs.get("trange", self.trange)
884
+ xvals = kwargs.get("xvals", (0, 1))
885
+ steps = kwargs.get("steps", 20)
886
+ step = (prange[1] - prange[0]) / steps
887
+ if prange[0] == prange[1]:
888
+ calcs = [
889
+ "calcP {:g} {:g}".format(*prange),
890
+ "calcT {:g} {:g}".format(*trange),
891
+ "calctatp yes",
892
+ "with {}".format(" ".join(phases - self.excess)),
893
+ "zeromodeisopleth {}".format(" ".join(out)),
894
+ "bulksubrange {:g} {:g}".format(*xvals),
895
+ ]
896
+ else:
897
+ calcs = [
898
+ "calcP {:g} {:g} {:g}".format(*prange, step),
899
+ "calcT {:g} {:g}".format(*trange),
900
+ "calctatp yes",
901
+ "with {}".format(" ".join(phases - self.excess)),
902
+ "zeromodeisopleth {}".format(" ".join(out)),
903
+ "bulksubrange {:g} {:g}".format(*xvals),
904
+ ]
905
+ self.update_scriptfile(calcs=calcs, xsteps=steps)
906
+ tcout = self.runtc()
907
+ return tcout, calcs
908
+
909
+ def calc_px(self, phases, out, **kwargs):
910
+ """Method to run THERMOCALC for p-X pseudosection calculations.
911
+
912
+ Args:
913
+ phases (set): Set of present phases
914
+ out (set): Set of zero mode phases
915
+ prange (tuple): Temperature range for calculation
916
+ trange (tuple): Pressure range for calculation
917
+ xvals (tuple): range for X variable
918
+ steps (int): Number of steps
919
+
920
+ Returns:
921
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
922
+ Input ans could be used to reproduce calculation.
923
+ """
924
+ prange = kwargs.get("prange", self.prange)
925
+ trange = kwargs.get("trange", self.trange)
926
+ xvals = kwargs.get("xvals", (0, 1))
927
+ steps = kwargs.get("steps", 20)
928
+ step = (trange[1] - trange[0]) / steps
929
+ if trange[0] == trange[1]:
930
+ calcs = [
931
+ "calcP {:g} {:g}".format(*prange),
932
+ "calcT {:g} {:g}".format(*trange),
933
+ "calctatp no",
934
+ "with {}".format(" ".join(phases - self.excess)),
935
+ "zeromodeisopleth {}".format(" ".join(out)),
936
+ "bulksubrange {:g} {:g}".format(*xvals),
937
+ ]
938
+ else:
939
+ calcs = [
940
+ "calcP {:g} {:g}".format(*prange),
941
+ "calcT {:g} {:g} {:g}".format(*trange, step),
942
+ "calctatp no",
943
+ "with {}".format(" ".join(phases - self.excess)),
944
+ "zeromodeisopleth {}".format(" ".join(out)),
945
+ "bulksubrange {:g} {:g}".format(*xvals),
946
+ ]
947
+ self.update_scriptfile(calcs=calcs, xsteps=steps)
948
+ tcout = self.runtc()
949
+ return tcout, calcs
950
+
951
+ def calc_assemblage(self, phases, p, t, onebulk=None):
952
+ """Method to run THERMOCALC to calculate compositions of stable assemblage.
953
+
954
+ Args:
955
+ phases (set): Set of present phases
956
+ p (float): Temperature for calculation
957
+ t (float): Pressure for calculation
958
+
959
+ Returns:
960
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
961
+ Input ans could be used to reproduce calculation.
962
+ """
963
+ calcs = [
964
+ "calcP {}".format(p),
965
+ "calcT {}".format(t),
966
+ "with {}".format(" ".join(phases - self.excess)),
967
+ ]
968
+ if onebulk is not None:
969
+ calcs.append("onebulk {}".format(onebulk))
970
+ self.update_scriptfile(calcs=calcs)
971
+ tcout = self.runtc("\nkill\n\n")
972
+ return tcout, calcs
973
+
974
+ def dogmin(self, phases, p, t, variance, doglevel=1, onebulk=None):
975
+ """Run THERMOCALC dogmin session.
976
+
977
+ Args:
978
+ variance (int): Maximum variance to be considered
979
+
980
+ Returns:
981
+ str: THERMOCALC standard output
982
+ """
983
+ calcs = [
984
+ "calcP {}".format(p),
985
+ "calcT {}".format(t),
986
+ "dogmin yes {}".format(doglevel),
987
+ "with {}".format(" ".join(phases - self.excess)),
988
+ "maxvar {}".format(variance),
989
+ ]
990
+ if onebulk is not None:
991
+ calcs.append("onebulk {}".format(onebulk))
992
+ old_calcs = self.update_scriptfile(get_old_calcs=True, calcs=calcs)
993
+ tcout = self.runtc("\nkill\n\n")
994
+ self.update_scriptfile(calcs=old_calcs)
995
+ return tcout
996
+
997
+ def calc_variance(self, phases):
998
+ """Get variance of assemblage.
999
+
1000
+ Args:
1001
+ phases (set): Set of present phases
1002
+
1003
+ Returns:
1004
+ int: variance
1005
+ """
1006
+ variance = None
1007
+ calcs = [
1008
+ "calcP {} {}".format(*self.prange),
1009
+ "calcT {} {}".format(*self.trange),
1010
+ "with {}".format(" ".join(phases - self.excess)),
1011
+ "acceptvar no",
1012
+ ]
1013
+ old_calcs = self.update_scriptfile(get_old_calcs=True, calcs=calcs)
1014
+ tcout = self.runtc("kill\n\n")
1015
+ self.update_scriptfile(calcs=old_calcs)
1016
+ for ln in tcout.splitlines():
1017
+ if "variance of required equilibrium" in ln:
1018
+ variance = int(ln[ln.index("(") + 1 : ln.index("?")])
1019
+ break
1020
+ return variance
1021
+
1022
+
1023
+ class TC34API(TCAPI):
1024
+ def __init__(self, workdir, tcexe, drexe, encoding="mac-roman"):
1025
+ self.workdir = Path(workdir).resolve()
1026
+ self.tcexe = tcexe
1027
+ self.drexe = drexe
1028
+ self.TCenc = encoding
1029
+ try:
1030
+ # TC version
1031
+ errinfo = "THERMOCALC executable test."
1032
+ self.tcout = self.runtc()
1033
+ # tc-prefs file
1034
+ if not self.workdir.joinpath("tc-prefs.txt").exists():
1035
+ raise InitError("No tc-prefs.txt file in working directory.")
1036
+ errinfo = "tc-prefs.txt file in working directory cannot be accessed."
1037
+ for line in self.workdir.joinpath("tc-prefs.txt").open(
1038
+ "r", encoding=self.TCenc
1039
+ ):
1040
+ kw = line.split()
1041
+ if kw != []:
1042
+ if kw[0] == "scriptfile":
1043
+ self.name = kw[1]
1044
+ if not self.scriptfile.exists():
1045
+ raise InitError(
1046
+ "tc-prefs: scriptfile tc-"
1047
+ + self.name
1048
+ + ".txt does not exists in your working directory."
1049
+ )
1050
+ if kw[0] == "calcmode":
1051
+ if kw[1] != "1":
1052
+ raise InitError("tc-prefs: calcmode must be 1.")
1053
+
1054
+ errinfo = "Scriptfile error!"
1055
+ self.excess = set()
1056
+ self.trange = (200.0, 1000.0)
1057
+ self.prange = (0.1, 20.0)
1058
+ self.bulk = []
1059
+ self.ptx_steps = 20
1060
+ check = {
1061
+ "axfile": False,
1062
+ "setbulk": False,
1063
+ "printbulkinfo": False,
1064
+ "setexcess": False,
1065
+ "printxyz": False,
1066
+ }
1067
+ errinfo = "Check your scriptfile."
1068
+ with self.scriptfile.open("r", encoding=self.TCenc) as f:
1069
+ lines = f.readlines()
1070
+ gsb, gse = False, False
1071
+ dgb, dge = False, False
1072
+ bub, bue = False, False
1073
+ for line in lines:
1074
+ kw = line.split("%")[0].split()
1075
+ if "{PSBGUESS-BEGIN}" in line:
1076
+ gsb = True
1077
+ if "{PSBGUESS-END}" in line:
1078
+ gse = True
1079
+ if "{PSBDOGMIN-BEGIN}" in line:
1080
+ dgb = True
1081
+ if "{PSBDOGMIN-END}" in line:
1082
+ dge = True
1083
+ if "{PSBBULK-BEGIN}" in line:
1084
+ bub = True
1085
+ if "{PSBBULK-END}" in line:
1086
+ bue = True
1087
+ if kw == ["*"]:
1088
+ break
1089
+ if kw:
1090
+ if kw[0] == "axfile":
1091
+ errinfo = "Wrong argument for axfile keyword in scriptfile."
1092
+ self.axname = kw[1]
1093
+ if not self.axfile.exists():
1094
+ raise ScriptfileError(
1095
+ "Axfile "
1096
+ + str(self.axfile)
1097
+ + " does not exists in working directory"
1098
+ )
1099
+ check["axfile"] = True
1100
+ elif kw[0] == "setdefTwindow":
1101
+ errinfo = (
1102
+ "Wrong arguments for setdefTwindow keyword in scriptfile."
1103
+ )
1104
+ self.trange = (float(kw[-2]), float(kw[-1]))
1105
+ elif kw[0] == "setdefPwindow":
1106
+ errinfo = (
1107
+ "Wrong arguments for setdefPwindow keyword in scriptfile."
1108
+ )
1109
+ self.prange = (float(kw[-2]), float(kw[-1]))
1110
+ elif kw[0] == "setbulk":
1111
+ errinfo = "Wrong arguments for setbulk keyword in scriptfile."
1112
+ bulk = kw[1:]
1113
+ if "ask" in bulk:
1114
+ raise ScriptfileError("Setbulk must not be set to ask.")
1115
+ if "yes" in bulk:
1116
+ bulk.remove("yes")
1117
+ if "no" not in bulk:
1118
+ if len(self.bulk) == 1:
1119
+ if len(self.bulk[0]) < len(bulk):
1120
+ self.ptx_steps = int(bulk[-1])
1121
+ bulk = bulk[:-1]
1122
+ self.bulk.append(bulk)
1123
+ check["setbulk"] = True
1124
+
1125
+ elif kw[0] == "setexcess":
1126
+ errinfo = "Wrong argument for setexcess keyword in scriptfile."
1127
+ self.excess = set(kw[1:])
1128
+ if "yes" in self.excess:
1129
+ self.excess.remove("yes")
1130
+ if "no" in self.excess:
1131
+ self.excess = set()
1132
+ if "ask" in self.excess:
1133
+ raise ScriptfileError("Setexcess must not be set to ask.")
1134
+ check["setexcess"] = True
1135
+ elif kw[0] == "calctatp":
1136
+ errinfo = "Wrong argument for calctatp keyword in scriptfile."
1137
+ if not kw[1] == "ask":
1138
+ raise ScriptfileError("Calctatp must be set to ask.")
1139
+ # elif kw[0] == 'drawpd':
1140
+ # errinfo = 'Wrong argument for drawpd keyword in scriptfile.'
1141
+ # if kw[1] == 'no':
1142
+ # raise ScriptfileError('Drawpd must be set to yes.')
1143
+ # check['drawpd'] = True
1144
+ elif kw[0] == "printbulkinfo":
1145
+ errinfo = (
1146
+ "Wrong argument for printbulkinfo keyword in scriptfile."
1147
+ )
1148
+ if kw[1] == "no":
1149
+ raise ScriptfileError("Printbulkinfo must be set to yes.")
1150
+ check["printbulkinfo"] = True
1151
+ elif kw[0] == "printxyz":
1152
+ errinfo = "Wrong argument for printxyz keyword in scriptfile."
1153
+ if kw[1] == "no":
1154
+ raise ScriptfileError("Printxyz must be set to yes.")
1155
+ check["printxyz"] = True
1156
+ elif kw[0] == "dogmin":
1157
+ errinfo = "Wrong argument for dogmin keyword in scriptfile."
1158
+ if not kw[1] == "no":
1159
+ raise ScriptfileError("Dogmin must be set to no.")
1160
+ elif kw[0] == "fluidpresent":
1161
+ raise ScriptfileError(
1162
+ "Fluidpresent must be deleted from scriptfile."
1163
+ )
1164
+ elif kw[0] == "seta":
1165
+ errinfo = "Wrong argument for seta keyword in scriptfile."
1166
+ if not kw[1] == "no":
1167
+ raise ScriptfileError("Seta must be set to no.")
1168
+ elif kw[0] == "setmu":
1169
+ errinfo = "Wrong argument for setmu keyword in scriptfile."
1170
+ if not kw[1] == "no":
1171
+ raise ScriptfileError("Setmu must be set to no.")
1172
+ elif kw[0] == "usecalcq":
1173
+ errinfo = "Wrong argument for usecalcq keyword in scriptfile."
1174
+ if kw[1] == "ask":
1175
+ raise ScriptfileError("Usecalcq must be yes or no.")
1176
+ elif kw[0] == "pseudosection":
1177
+ errinfo = (
1178
+ "Wrong argument for pseudosection keyword in scriptfile."
1179
+ )
1180
+ if kw[1] == "ask":
1181
+ raise ScriptfileError("Pseudosection must be yes or no.")
1182
+ elif kw[0] == "zeromodeiso":
1183
+ errinfo = (
1184
+ "Wrong argument for zeromodeiso keyword in scriptfile."
1185
+ )
1186
+ if not kw[1] == "yes":
1187
+ raise ScriptfileError("Zeromodeiso must be set to yes.")
1188
+ elif kw[0] == "setmodeiso":
1189
+ errinfo = "Wrong argument for setmodeiso keyword in scriptfile."
1190
+ if not kw[1] == "yes":
1191
+ raise ScriptfileError("Setmodeiso must be set to yes.")
1192
+ elif kw[0] == "convliq":
1193
+ raise ScriptfileError("Convliq not yet supported.")
1194
+ elif kw[0] == "setiso":
1195
+ errinfo = "Wrong argument for setiso keyword in scriptfile."
1196
+ if kw[1] != "no":
1197
+ raise ScriptfileError("Setiso must be set to no.")
1198
+
1199
+ if not check["axfile"]:
1200
+ raise ScriptfileError("Axfile name must be provided in scriptfile.")
1201
+ if not check["setbulk"]:
1202
+ raise ScriptfileError("Setbulk must be provided in scriptfile.")
1203
+ if not check["setexcess"]:
1204
+ raise ScriptfileError(
1205
+ "Setexcess must not be set to ask. To suppress this error put empty setexcess keyword to your scriptfile."
1206
+ )
1207
+ # if not check['drawpd']:
1208
+ # raise ScriptfileError('Drawpd must be set to yes. To suppress this error put drawpd yes keyword to your scriptfile.')
1209
+ if not check["printbulkinfo"]:
1210
+ raise ScriptfileError(
1211
+ "Printbulkinfo must be set to yes. To suppress this error put printbulkinfo yes keyword to your scriptfile."
1212
+ )
1213
+ if not check["printxyz"]:
1214
+ raise ScriptfileError(
1215
+ "Printxyz must be set to yes. To suppress this error put printxyz yes keyword to your scriptfile."
1216
+ )
1217
+ if not (gsb and gse):
1218
+ raise ScriptfileError(
1219
+ "There are not {PSBGUESS-BEGIN} and {PSBGUESS-END} tags in your scriptfile."
1220
+ )
1221
+ if not (dgb and dge):
1222
+ raise ScriptfileError(
1223
+ "There are not {PSBDOGMIN-BEGIN} and {PSBDOGMIN-END} tags in your scriptfile."
1224
+ )
1225
+ if not (bub and bue):
1226
+ raise ScriptfileError(
1227
+ "There are not {PSBBULK-BEGIN} and {PSBBULK-END} tags in your scriptfile."
1228
+ )
1229
+
1230
+ # TC
1231
+ if "BOMBED" in self.tcout:
1232
+ raise TCError(self.tcout.split("BOMBED")[1].split("\n")[0])
1233
+ else:
1234
+ self.phases = set(
1235
+ self.tcout.split("choose from:")[1].split("\n")[0].split()
1236
+ )
1237
+ # OK
1238
+ self.status = "Initial check done."
1239
+ self.OK = True
1240
+ except BaseException as e:
1241
+ if (
1242
+ isinstance(e, InitError)
1243
+ or isinstance(e, ScriptfileError)
1244
+ or isinstance(e, TCError)
1245
+ ):
1246
+ self.status = "{}: {}".format(type(e).__name__, str(e))
1247
+ else:
1248
+ self.status = "{}: {} {}".format(type(e).__name__, str(e), errinfo)
1249
+ self.OK = False
1250
+
1251
+ def parse_logfile(self, **kwargs):
1252
+ """Parser for THERMOCALC 3.4x output.
1253
+
1254
+ It parses the outputs of THERMOCALC after calculation.
1255
+
1256
+ Args:
1257
+ output (str): When not None, used as content of logfile. Default None.
1258
+ resic (str): When not None, used as content of icfile. Default None.
1259
+ get_phases (bool): When true returns also tuple (phases, out, calcs). Default False
1260
+
1261
+ Returns:
1262
+ status (str): Result of parsing. 'ok', 'nir' (nothing in range) or 'bombed'.
1263
+ results (TCResultSet): Results of TC calculation.
1264
+ output (str): Full nonparsed THERMOCALC output.
1265
+
1266
+ Example:
1267
+ Parse output after univariant line calculation in P-T pseudosection::
1268
+
1269
+ >>> tc = TCAPI('pat/to/dir')
1270
+ >>> status, result, output = tc.parse_logfile()
1271
+ """
1272
+ output = kwargs.get("output", None)
1273
+ get_phases = kwargs.get("get_phases", False)
1274
+ try:
1275
+ if output is None:
1276
+ with self.logfile.open("r", encoding=self.TCenc) as f:
1277
+ output = f.read()
1278
+ lines = [
1279
+ "".join([c for c in ln if ord(c) < 128])
1280
+ for ln in output.splitlines()
1281
+ if ln != ""
1282
+ ]
1283
+ pts = []
1284
+ res = []
1285
+ variance = -1
1286
+ if [ix for ix, ln in enumerate(lines) if "BOMBED" in ln]:
1287
+ status = "bombed"
1288
+ else:
1289
+ for ln in lines:
1290
+ if "variance of required equilibrium" in ln:
1291
+ variance = int(ln[ln.index("(") + 1 : ln.index("?")])
1292
+ break
1293
+ bstarts = [
1294
+ ix for ix, ln in enumerate(lines) if ln.startswith(" P(kbar)")
1295
+ ]
1296
+ bstarts.append(len(lines))
1297
+ for bs, be in zip(bstarts[:-1], bstarts[1:]):
1298
+ block = lines[bs:be]
1299
+ pts.append([float(n) for n in block[1].split()[:2]])
1300
+ xyz = [
1301
+ ix for ix, ln in enumerate(block) if ln.startswith("xyzguess")
1302
+ ]
1303
+ gixs = [
1304
+ ix for ix, ln in enumerate(block) if ln.startswith("ptguess")
1305
+ ][0] - 3
1306
+ gixe = xyz[-1] + 2
1307
+ ptguess = block[gixs:gixe]
1308
+ data = {}
1309
+ rbix = [
1310
+ ix for ix, ln in enumerate(block) if ln.startswith("rbi yes")
1311
+ ][0]
1312
+ phases = block[rbix - 1].split()[1:]
1313
+ for phase, val in zip(phases, block[rbix].split()[2:]):
1314
+ data[phase] = dict(mode=float(val))
1315
+ for ix in xyz:
1316
+ lbl = block[ix].split()[1]
1317
+ phase, comp = (
1318
+ lbl[lbl.find("(") + 1 : lbl.find(")")],
1319
+ lbl[: lbl.find("(")],
1320
+ )
1321
+ if phase not in data:
1322
+ raise Exception(
1323
+ "Check model {} in your ax file. Commonly liq coded as L for starting guesses.".format(
1324
+ phase
1325
+ )
1326
+ )
1327
+ data[phase][comp] = float(block[ix].split()[2])
1328
+ rbiox = block[rbix + 1].split()[2:]
1329
+ for delta in range(len(phases)):
1330
+ rbi = {
1331
+ c: float(v)
1332
+ for c, v in zip(
1333
+ rbiox, block[rbix + 2 + delta].split()[2:-2]
1334
+ )
1335
+ }
1336
+ rbi["H2O"] = float(block[rbix + 2 + delta].split()[1])
1337
+ # data[phases[delta]]['rbi'] = comp
1338
+ data[phases[delta]].update(rbi)
1339
+ res.append(dict(data=data, ptguess=ptguess))
1340
+ if res:
1341
+ status = "ok"
1342
+ pp, TT = np.array(pts).T
1343
+ results = TCResultSet(
1344
+ [
1345
+ TCResult(
1346
+ T,
1347
+ p,
1348
+ variance=variance,
1349
+ c=0.0,
1350
+ data=r["data"],
1351
+ ptguess=r["ptguess"],
1352
+ )
1353
+ for (r, p, T) in zip(res, pp, TT)
1354
+ ]
1355
+ )
1356
+ else:
1357
+ status = "nir"
1358
+ results = None
1359
+ if get_phases:
1360
+ phases, out = None, None
1361
+ for ln in output.splitlines():
1362
+ if ln.startswith("which phases"):
1363
+ phases = set(ln.split(":", 1)[1].split())
1364
+ if ln.startswith("which to set"):
1365
+ out = set(ln.split(":", 1)[1].split())
1366
+ return status, results, output, (phases, out, [])
1367
+ else:
1368
+ return status, results, output
1369
+ except Exception:
1370
+ if get_phases:
1371
+ return "bombed", None, None, (None, None, "")
1372
+ else:
1373
+ return "bombed", None, None
1374
+
1375
+ def update_scriptfile(self, **kwargs):
1376
+ """Method to update scriptfile.
1377
+
1378
+ This methodcould be used to read or update ptguess or dogmin settings.
1379
+
1380
+ Args:
1381
+ guesses (list): List of lines defining ptguesses. If None guesses
1382
+ are not modified. Default None.
1383
+ get_old_guesses (bool): When True method returns existing ptguess
1384
+ before possible modification. Default False.
1385
+ dogmin (str): Argument of dogmin script. Could be 'no' or 'yes' or
1386
+ 'yes X', where X is log level. When None no modification is
1387
+ done. Default None.
1388
+ which (set): Set of phases used for dogmin.
1389
+ bulk (list): Bulk composition. Default None.
1390
+ xvals (tuple): x values for compositions. Default (0, 1)
1391
+ xsteps (int): Number of compositional steps between two bulks.
1392
+ Default 20.
1393
+ p (float): Pressure for dogmin calculation
1394
+ T (float): Temperature for dogmin calculation
1395
+ """
1396
+ guesses = kwargs.get("guesses", None)
1397
+ get_old_guesses = kwargs.get("get_old_guesses", False)
1398
+ dogmin = kwargs.get("dogmin", None) # None or 'no' or 'yes 1'
1399
+ which = kwargs.get("which", None)
1400
+ bulk = kwargs.get("bulk", None)
1401
+ xvals = kwargs.get("xvals", (0, 1))
1402
+ xsteps = kwargs.get("xsteps", 20)
1403
+ p = kwargs.get("p", None)
1404
+ T = kwargs.get("T", None)
1405
+ with self.scriptfile.open("r", encoding=self.TCenc) as f:
1406
+ sc = f.readlines()
1407
+ changed = False
1408
+ gsb = [ix for ix, ln in enumerate(sc) if ln.startswith("%{PSBGUESS-BEGIN}")]
1409
+ gse = [ix for ix, ln in enumerate(sc) if ln.startswith("%{PSBGUESS-END}")]
1410
+ if get_old_guesses:
1411
+ if gsb and gse:
1412
+ old_guesses = [ln.strip() for ln in sc[gsb[0] + 1 : gse[0]]]
1413
+ if guesses is not None:
1414
+ if gsb and gse:
1415
+ sc = sc[: gsb[0] + 1] + [gln + "\n" for gln in guesses] + sc[gse[0] :]
1416
+ changed = True
1417
+ if dogmin is not None:
1418
+ dgb = [
1419
+ ix for ix, ln in enumerate(sc) if ln.startswith("%{PSBDOGMIN-BEGIN}")
1420
+ ]
1421
+ dge = [ix for ix, ln in enumerate(sc) if ln.startswith("%{PSBDOGMIN-END}")]
1422
+ dglines = []
1423
+ dglines.append("dogmin {}\n".format(dogmin))
1424
+ if which is not None:
1425
+ dglines.append("which {}\n".format(" ".join(which)))
1426
+ dglines.append("setPwindow {} {}\n".format(p, p))
1427
+ dglines.append("setTwindow {} {}\n".format(T, T))
1428
+ if dgb and dge:
1429
+ sc = sc[: dgb[0] + 1] + dglines + sc[dge[0] :]
1430
+ changed = True
1431
+ if bulk is not None:
1432
+ bub = [ix for ix, ln in enumerate(sc) if ln.startswith("%{PSBBULK-BEGIN}")]
1433
+ bue = [ix for ix, ln in enumerate(sc) if ln.startswith("%{PSBBULK-END}")]
1434
+ bulines = []
1435
+ if len(bulk) == 2:
1436
+ bulines.append(
1437
+ "setbulk yes {} % x={:g}\n".format(" ".join(bulk[0]), xvals[0])
1438
+ )
1439
+ bulines.append(
1440
+ "setbulk yes {} {:d} % x={:g}\n".format(
1441
+ " ".join(bulk[1]), xsteps, xvals[1]
1442
+ )
1443
+ )
1444
+ else:
1445
+ bulines.append("setbulk yes {}\n".format(" ".join(bulk[0])))
1446
+ if bub and bue:
1447
+ sc = sc[: bub[0] + 1] + bulines + sc[bue[0] :]
1448
+ changed = True
1449
+ if changed:
1450
+ with self.scriptfile.open("w", encoding=self.TCenc) as f:
1451
+ for ln in sc:
1452
+ f.write(ln)
1453
+ if get_old_guesses:
1454
+ return old_guesses
1455
+ else:
1456
+ return None
1457
+
1458
+ def interpolate_bulk(self, x):
1459
+ if len(self.bulk) == 2:
1460
+ new_bulk = []
1461
+ try:
1462
+ _ = (e for e in x)
1463
+ except TypeError:
1464
+ b1 = np.array([float(v) for v in self.bulk[0]])
1465
+ b2 = np.array([float(v) for v in self.bulk[1]])
1466
+ db = b2 - b1
1467
+ bi = b1 + x * db
1468
+ new_bulk.append(["{:g}".format(v) for v in bi])
1469
+ else:
1470
+ for x_val in x:
1471
+ b1 = np.array([float(v) for v in self.bulk[0]])
1472
+ b2 = np.array([float(v) for v in self.bulk[1]])
1473
+ db = b2 - b1
1474
+ bi = b1 + x_val * db
1475
+ new_bulk.append(["{:g}".format(v) for v in bi])
1476
+ else:
1477
+ new_bulk = self.bulk[0]
1478
+ return new_bulk
1479
+
1480
+ def parse_kwargs(self, **kwargs):
1481
+ prange = kwargs.get("prange", self.prange)
1482
+ trange = kwargs.get("trange", self.trange)
1483
+ steps = kwargs.get("steps", 50)
1484
+ if np.diff(prange)[0] < 0.001:
1485
+ prec = kwargs.get(
1486
+ "prec", max(int(2 - np.floor(np.log10(np.diff(trange)[0]))), 0) + 1
1487
+ )
1488
+ elif np.diff(trange)[0] < 0.001:
1489
+ prec = kwargs.get(
1490
+ "prec", max(int(2 - np.floor(np.log10(np.diff(prange)[0]))), 0) + 1
1491
+ )
1492
+ else:
1493
+ prec = kwargs.get(
1494
+ "prec",
1495
+ max(
1496
+ int(
1497
+ 2
1498
+ - np.floor(
1499
+ np.log10(min(np.diff(trange)[0], np.diff(prange)[0]))
1500
+ )
1501
+ ),
1502
+ 0,
1503
+ )
1504
+ + 1,
1505
+ )
1506
+ return prange, trange, steps, prec
1507
+
1508
+ def calc_t(self, phases, out, **kwargs):
1509
+ """Method to run THERMOCALC to find univariant line using Calc T at P strategy.
1510
+
1511
+ Args:
1512
+ phases (set): Set of present phases
1513
+ out (set): Set of single zero mode phase
1514
+ prange (tuple): Temperature range for calculation
1515
+ trange (tuple): Pressure range for calculation
1516
+ steps (int): Number of steps
1517
+
1518
+ Returns:
1519
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
1520
+ Input ans could be used to reproduce calculation.
1521
+ """
1522
+ prange, trange, steps, prec = self.parse_kwargs(**kwargs)
1523
+ step = (prange[1] - prange[0]) / steps
1524
+ tmpl = "{}\n\n{}\ny\n{:.{prec}f} {:.{prec}f}\n{:.{prec}f} {:.{prec}f}\n{:g}\nn\n\nkill\n\n"
1525
+ ans = tmpl.format(
1526
+ " ".join(phases), " ".join(out), *prange, *trange, step, prec=prec
1527
+ )
1528
+ tcout = self.runtc(ans)
1529
+ return tcout, ans
1530
+
1531
+ def calc_p(self, phases, out, **kwargs):
1532
+ """Method to run THERMOCALC to find univariant line using Calc P at T strategy.
1533
+
1534
+ Args:
1535
+ phases (set): Set of present phases
1536
+ out (set): Set of single zero mode phase
1537
+ prange (tuple): Temperature range for calculation
1538
+ trange (tuple): Pressure range for calculation
1539
+ steps (int): Number of steps
1540
+
1541
+ Returns:
1542
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
1543
+ Input ans could be used to reproduce calculation.
1544
+ """
1545
+ prange, trange, steps, prec = self.parse_kwargs(**kwargs)
1546
+ step = (trange[1] - trange[0]) / steps
1547
+ tmpl = "{}\n\n{}\nn\n{:.{prec}f} {:.{prec}f}\n{:.{prec}f} {:.{prec}f}\n{:g}\nn\n\nkill\n\n"
1548
+ ans = tmpl.format(
1549
+ " ".join(phases), " ".join(out), *trange, *prange, step, prec=prec
1550
+ )
1551
+ tcout = self.runtc(ans)
1552
+ return tcout, ans
1553
+
1554
+ def calc_pt(self, phases, out, **kwargs):
1555
+ """Method to run THERMOCALC to find invariant point.
1556
+
1557
+ Args:
1558
+ phases (set): Set of present phases
1559
+ out (set): Set of two zero mode phases
1560
+ prange (tuple): Temperature range for calculation
1561
+ trange (tuple): Pressure range for calculation
1562
+ steps (int): Number of steps
1563
+
1564
+ Returns:
1565
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
1566
+ Input ans could be used to reproduce calculation.
1567
+ """
1568
+ prange, trange, steps, prec = self.parse_kwargs(**kwargs)
1569
+ tmpl = (
1570
+ "{}\n\n{}\n{:.{prec}f} {:.{prec}f} {:.{prec}f} {:.{prec}f}\nn\n\nkill\n\n"
1571
+ )
1572
+ ans = tmpl.format(" ".join(phases), " ".join(out), *trange, *prange, prec=prec)
1573
+ tcout = self.runtc(ans)
1574
+ return tcout, ans
1575
+
1576
+ def calc_tx(self, phases, out, **kwargs):
1577
+ """Method to run THERMOCALC for T-X pseudosection calculations.
1578
+
1579
+ Args:
1580
+ phases (set): Set of present phases
1581
+ out (set): Set of zero mode phases
1582
+ prange (tuple): Temperature range for calculation
1583
+ trange (tuple): Pressure range for calculation
1584
+ steps (int): Number of steps
1585
+
1586
+ Returns:
1587
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
1588
+ Input ans could be used to reproduce calculation.
1589
+ """
1590
+ prange, trange, steps, prec = self.parse_kwargs(**kwargs)
1591
+ if len(out) > 1:
1592
+ tmpl = "{}\n\n{}\n{:.{prec}f} {:.{prec}f} {:.{prec}f} {:.{prec}f}\nn\n\nkill\n\n"
1593
+ ans = tmpl.format(
1594
+ " ".join(phases), " ".join(out), *trange, *prange, prec=prec
1595
+ )
1596
+ else:
1597
+ tmpl = "{}\n\n{}\ny\n\n{:.{prec}f} {:.{prec}f}\nn\nkill\n\n"
1598
+ ans = tmpl.format(" ".join(phases), " ".join(out), *trange, prec=prec)
1599
+ tcout = self.runtc(ans)
1600
+ return tcout, ans
1601
+
1602
+ def calc_px(self, phases, out, **kwargs):
1603
+ """Method to run THERMOCALC for P-X pseudosection calculations.
1604
+
1605
+ Args:
1606
+ phases (set): Set of present phases
1607
+ out (set): Set of zero mode phases
1608
+ prange (tuple): Temperature range for calculation
1609
+ trange (tuple): Pressure range for calculation
1610
+ steps (int): Number of steps
1611
+
1612
+ Returns:
1613
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
1614
+ Input ans could be used to reproduce calculation.
1615
+ """
1616
+ prange, trange, steps, prec = self.parse_kwargs(**kwargs)
1617
+ if len(out) > 1:
1618
+ tmpl = "{}\n\n{}\n{:.{prec}f} {:.{prec}f} {:.{prec}f} {:.{prec}f}\nn\n\nkill\n\n"
1619
+ ans = tmpl.format(
1620
+ " ".join(phases), " ".join(out), *trange, *prange, prec=prec
1621
+ )
1622
+ else:
1623
+ tmpl = "{}\n\n{}\nn\n\n{:.{prec}f} {:.{prec}f}\nn\nkill\n\n"
1624
+ ans = tmpl.format(" ".join(phases), " ".join(out), *prange, prec=prec)
1625
+ tcout = self.runtc(ans)
1626
+ return tcout, ans
1627
+
1628
+ def calc_assemblage(self, phases, p, t):
1629
+ """Method to run THERMOCALC to calculate compositions of stable assemblage.
1630
+
1631
+ Args:
1632
+ phases (set): Set of present phases
1633
+ p (float): Temperature for calculation
1634
+ t (float): Pressure for calculation
1635
+
1636
+ Returns:
1637
+ tuple: (tcout, ans) standard output and input for THERMOCALC run.
1638
+ Input ans could be used to reproduce calculation.
1639
+ """
1640
+ tmpl = "{}\n\n\n{}\n{}\nkill\n\n"
1641
+ ans = tmpl.format(" ".join(phases), p, t)
1642
+ tcout = self.runtc(ans)
1643
+ return tcout, ans
1644
+
1645
+ def dogmin(self, phases, p, t, variance, doglevel=1, onebulk=None):
1646
+ """Run THERMOCALC dogmin session.
1647
+
1648
+ Args:
1649
+ variance (int): Maximum variance to be considered
1650
+
1651
+ Returns:
1652
+ str: THERMOCALC standard output
1653
+ """
1654
+ self.update_scriptfile(
1655
+ dogmin="yes {}".format(doglevel),
1656
+ which=phases,
1657
+ T="{:.3f}".format(t),
1658
+ p="{:.3f}".format(p),
1659
+ )
1660
+ tmpl = "{}\n\nn\n\n"
1661
+ ans = tmpl.format(variance)
1662
+ tcout = self.runtc(ans)
1663
+ self.update_scriptfile(dogmin="no")
1664
+ return tcout
1665
+
1666
+ def calc_variance(self, phases):
1667
+ """Get variance of assemblage.
1668
+
1669
+ Args:
1670
+ phases (set): Set of present phases
1671
+
1672
+ Returns:
1673
+ int: variance
1674
+ """
1675
+ variance = None
1676
+ tcout = self.runtc("{}\nkill\n\n".format(" ".join(phases)))
1677
+ for ln in tcout.splitlines():
1678
+ if "variance of required equilibrium" in ln:
1679
+ variance = int(ln[ln.index("(") + 1 : ln.index("?")])
1680
+ break
1681
+ return variance