calphy 1.3.13__py3-none-any.whl → 1.4.2__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.
calphy/phase_diagram.py CHANGED
@@ -4,18 +4,466 @@ import pandas as pd
4
4
  import matplotlib.pyplot as plt
5
5
  import warnings
6
6
  import itertools
7
+ from itertools import combinations
7
8
  import math
9
+ import copy
10
+ import os
11
+ from calphy.composition_transformation import CompositionTransformation
12
+ import yaml
13
+ import matplotlib.patches as mpatches
8
14
 
9
15
  from calphy.integrators import kb
10
16
 
11
17
  from scipy.spatial import ConvexHull
12
18
  from scipy.interpolate import splrep, splev
19
+ from scipy.optimize import curve_fit
20
+
13
21
 
14
22
  colors = ['#a6cee3','#1f78b4','#b2df8a',
15
23
  '#33a02c','#fb9a99','#e31a1c',
16
24
  '#fdbf6f','#ff7f00','#cab2d6',
17
25
  '#6a3d9a','#ffff99','#b15928']
18
26
 
27
+ matcolors = {
28
+ "amber": {
29
+ 50 : '#fff8e1',
30
+ 100 : '#ffecb3',
31
+ 200 : '#ffe082',
32
+ 300 : '#ffd54f',
33
+ 400 : '#ffca28',
34
+ 500 : '#ffc107',
35
+ 600 : '#ffb300',
36
+ 700 : '#ffa000',
37
+ 800 : '#ff8f00',
38
+ 900 : '#ff6f00',
39
+ },
40
+ "blue_grey": {
41
+ 50 : '#ECEFF1',
42
+ 100 : '#CFD8DC',
43
+ 200 : '#B0BEC5',
44
+ 300 : '#90A4AE',
45
+ 400 : '#78909C',
46
+ 500 : '#607D8B',
47
+ 600 : '#546E7A',
48
+ 700 : '#455A64',
49
+ 800 : '#37474F',
50
+ 900 : '#263238',
51
+ },
52
+ "blue": {
53
+ 50 : '#E3F2FD',
54
+ 100 : '#BBDEFB',
55
+ 200 : '#90CAF9',
56
+ 300 : '#64B5F6',
57
+ 400 : '#42A5F5',
58
+ 500 : '#2196F3',
59
+ 600 : '#1E88E5',
60
+ 700 : '#1976D2',
61
+ 800 : '#1565C0',
62
+ 900 : '#0D47A1',
63
+ },
64
+ "brown": {
65
+ 50 : '#EFEBE9',
66
+ 100 : '#D7CCC8',
67
+ 200 : '#BCAAA4',
68
+ 300 : '#A1887F',
69
+ 400 : '#8D6E63',
70
+ 500 : '#795548',
71
+ 600 : '#6D4C41',
72
+ 700 : '#5D4037',
73
+ 800 : '#4E342E',
74
+ 900 : '#3E2723',
75
+ },
76
+ "cyan": {
77
+ 50 : '#E0F7FA',
78
+ 100 : '#B2EBF2',
79
+ 200 : '#80DEEA',
80
+ 300 : '#4DD0E1',
81
+ 400 : '#26C6DA',
82
+ 500 : '#00BCD4',
83
+ 600 : '#00ACC1',
84
+ 700 : '#0097A7',
85
+ 800 : '#00838F',
86
+ 900 : '#006064',
87
+ },
88
+ "deep_orange": {
89
+ 50 : '#FBE9E7',
90
+ 100 : '#FFCCBC',
91
+ 200 : '#FFAB91',
92
+ 300 : '#FF8A65',
93
+ 400 : '#FF7043',
94
+ 500 : '#FF5722',
95
+ 600 : '#F4511E',
96
+ 700 : '#E64A19',
97
+ 800 : '#D84315',
98
+ 900 : '#BF360C',
99
+ },
100
+ "deep_purple": {
101
+ 50 : '#EDE7F6',
102
+ 100 : '#D1C4E9',
103
+ 200 : '#B39DDB',
104
+ 300 : '#9575CD',
105
+ 400 : '#7E57C2',
106
+ 500 : '#673AB7',
107
+ 600 : '#5E35B1',
108
+ 700 : '#512DA8',
109
+ 800 : '#4527A0',
110
+ 900 : '#311B92',
111
+ },
112
+ "green": {
113
+ 50 : '#E8F5E9',
114
+ 100 : '#C8E6C9',
115
+ 200 : '#A5D6A7',
116
+ 300 : '#81C784',
117
+ 400 : '#66BB6A',
118
+ 500 : '#4CAF50',
119
+ 600 : '#43A047',
120
+ 700 : '#388E3C',
121
+ 800 : '#2E7D32',
122
+ 900 : '#1B5E20',
123
+ },
124
+ "grey": {
125
+ 50 : '#FAFAFA',
126
+ 100 : '#F5F5F5',
127
+ 200 : '#EEEEEE',
128
+ 300 : '#E0E0E0',
129
+ 400 : '#BDBDBD',
130
+ 500 : '#9E9E9E',
131
+ 600 : '#757575',
132
+ 700 : '#616161',
133
+ 800 : '#424242',
134
+ 900 : '#212121',
135
+ },
136
+ "indigo": {
137
+ 50 : '#E8EAF6',
138
+ 100 : '#C5CAE9',
139
+ 200 : '#9FA8DA',
140
+ 300 : '#7986CB',
141
+ 400 : '#5C6BC0',
142
+ 500 : '#3F51B5',
143
+ 600 : '#3949AB',
144
+ 700 : '#303F9F',
145
+ 800 : '#283593',
146
+ 900 : '#1A237E',
147
+ },
148
+ "light_blue": {
149
+ 50 : '#E1F5FE',
150
+ 100 : '#B3E5FC',
151
+ 200 : '#81D4FA',
152
+ 300 : '#4FC3F7',
153
+ 400 : '#29B6F6',
154
+ 500 : '#03A9F4',
155
+ 600 : '#039BE5',
156
+ 700 : '#0288D1',
157
+ 800 : '#0277BD',
158
+ 900 : '#01579B',
159
+ },
160
+ "light_green": {
161
+ 50 : '#F1F8E9',
162
+ 100 : '#DCEDC8',
163
+ 200 : '#C5E1A5',
164
+ 300 : '#AED581',
165
+ 400 : '#9CCC65',
166
+ 500 : '#8BC34A',
167
+ 600 : '#7CB342',
168
+ 700 : '#689F38',
169
+ 800 : '#558B2F',
170
+ 900 : '#33691E',
171
+ },
172
+ "lime": {
173
+ 50 : '#F9FBE7',
174
+ 100 : '#F0F4C3',
175
+ 200 : '#E6EE9C',
176
+ 300 : '#DCE775',
177
+ 400 : '#D4E157',
178
+ 500 : '#CDDC39',
179
+ 600 : '#C0CA33',
180
+ 700 : '#AFB42B',
181
+ 800 : '#9E9D24',
182
+ 900 : '#827717',
183
+ },
184
+ "orange": {
185
+ 50 : '#FFF3E0',
186
+ 100 : '#FFE0B2',
187
+ 200 : '#FFCC80',
188
+ 300 : '#FFB74D',
189
+ 400 : '#FFA726',
190
+ 500 : '#FF9800',
191
+ 600 : '#FB8C00',
192
+ 700 : '#F57C00',
193
+ 800 : '#EF6C00',
194
+ 900 : '#E65100',
195
+ },
196
+ "pink": {
197
+ 50 : '#FCE4EC',
198
+ 100 : '#F8BBD0',
199
+ 200 : '#F48FB1',
200
+ 300 : '#F06292',
201
+ 400 : '#EC407A',
202
+ 500 : '#E91E63',
203
+ 600 : '#D81B60',
204
+ 700 : '#C2185B',
205
+ 800 : '#AD1457',
206
+ 900 : '#880E4F',
207
+ },
208
+ "purple": {
209
+ 50 : '#F3E5F5',
210
+ 100 : '#E1BEE7',
211
+ 200 : '#CE93D8',
212
+ 300 : '#BA68C8',
213
+ 400 : '#AB47BC',
214
+ 500 : '#9C27B0',
215
+ 600 : '#8E24AA',
216
+ 700 : '#7B1FA2',
217
+ 800 : '#6A1B9A',
218
+ 900 : '#4A148C',
219
+ },
220
+ "red": {
221
+ 50 : '#FFEBEE',
222
+ 100 : '#FFCDD2',
223
+ 200 : '#EF9A9A',
224
+ 300 : '#E57373',
225
+ 500 : '#F44336',
226
+ 600 : '#E53935',
227
+ 700 : '#D32F2F',
228
+ 800 : '#C62828',
229
+ 900 : '#B71C1C',
230
+ },
231
+ "teal": {
232
+ 50 : '#E0F2F1',
233
+ 100 : '#B2DFDB',
234
+ 200 : '#80CBC4',
235
+ 300 : '#4DB6AC',
236
+ 400 : '#26A69A',
237
+ 500 : '#009688',
238
+ 600 : '#00897B',
239
+ 700 : '#00796B',
240
+ 800 : '#00695C',
241
+ 900 : '#004D40',
242
+ },
243
+ "yellow": {
244
+ 50 : '#FFFDE7',
245
+ 100 : '#FFF9C4',
246
+ 200 : '#FFF59D',
247
+ 300 : '#FFF176',
248
+ 400 : '#FFEE58',
249
+ 500 : '#FFEB3B',
250
+ 600 : '#FDD835',
251
+ 700 : '#FBC02D',
252
+ 800 : '#F9A825',
253
+ 900 : '#F57F17',
254
+ }
255
+ }
256
+
257
+ def fix_data_file(datafile, nelements):
258
+ """
259
+ Change the atom types keyword in the structure file
260
+ """
261
+ lines = []
262
+ with open(datafile, 'r') as fin:
263
+ for line in fin:
264
+ if 'atom types' in line:
265
+ lines.append(f'{nelements} atom types\n')
266
+ else:
267
+ lines.append(line)
268
+ outfile = datafile + 'mod.data'
269
+ with open(outfile, 'w') as fout:
270
+ for line in lines:
271
+ fout.write(line)
272
+ return outfile
273
+
274
+ class CScale:
275
+ def __init__(self):
276
+ self._input_chemical_composition = None
277
+ self._output_chemical_composition = None
278
+ self.restrictions = []
279
+
280
+ @property
281
+ def input_chemical_composition(self):
282
+ return self._input_chemical_composition
283
+
284
+ @property
285
+ def output_chemical_composition(self):
286
+ return self._output_chemical_composition
287
+
288
+
289
+ class SimpleCalculation:
290
+ """
291
+ Simple calc class
292
+ """
293
+ def __init__(self, lattice,
294
+ element,
295
+ input_chemical_composition,
296
+ output_chemical_composition):
297
+ self.lattice = lattice
298
+ self.element = element
299
+ self.composition_scaling = CScale()
300
+ self.composition_scaling._input_chemical_composition = input_chemical_composition
301
+ self.composition_scaling._output_chemical_composition = output_chemical_composition
302
+
303
+
304
+ def prepare_inputs_for_phase_diagram(inputyamlfile, calculation_base_name=None):
305
+ with open(inputyamlfile, 'r') as fin:
306
+ data = yaml.safe_load(fin)
307
+
308
+ if calculation_base_name is None:
309
+ calculation_base_name = inputyamlfile
310
+
311
+ for phase in data['phases']:
312
+ phase_reference_state = phase['reference_phase']
313
+ phase_name = phase['phase_name']
314
+
315
+ comps = phase['composition']
316
+ reference_element = comps["reference_element"]
317
+ use_composition_scaling = bool(comps["use_composition_scaling"])
318
+ if str(phase_reference_state) == 'liquid':
319
+ use_composition_scaling = False
320
+
321
+ other_element_list = copy.deepcopy(phase['element'])
322
+ other_element_list.remove(reference_element)
323
+ other_element = other_element_list[0]
324
+
325
+ #convert to list if scalar
326
+ if not isinstance(comps['range'], list):
327
+ comps["range"] = [comps["range"]]
328
+ if len(comps["range"]) == 2:
329
+ comp_arr = np.arange(comps['range'][0], comps['range'][-1], comps['interval'])
330
+ last_val = comps['range'][-1]
331
+ if last_val not in comp_arr:
332
+ comp_arr = np.append(comp_arr, last_val)
333
+ ncomps = len(comp_arr)
334
+ is_reference = np.abs(comp_arr-comps['reference']) < 1E-5
335
+ elif len(comps["range"]) == 1:
336
+ ncomps = 1
337
+ comp_arr = [comps["range"][0]]
338
+ is_reference = [True]
339
+ else:
340
+ raise ValueError("Composition range should be scalar of list of two values!")
341
+
342
+ temps = phase["temperature"]
343
+ if not isinstance(temps['range'], list):
344
+ temps["range"] = [temps["range"]]
345
+ if len(temps["range"]) == 2:
346
+ ntemps = int((temps['range'][-1]-temps['range'][0])/temps['interval'])+1
347
+ temp_arr = np.linspace(temps['range'][0], temps['range'][-1], ntemps, endpoint=True)
348
+ elif len(temps["range"]) == 1:
349
+ ntemps = 1
350
+ temp_arr = [temps["range"][0]]
351
+ else:
352
+ raise ValueError("Temperature range should be scalar of list of two values!")
353
+
354
+ all_calculations = []
355
+
356
+ for count, comp in enumerate(comp_arr):
357
+ #check if ref comp equals given comp
358
+ if is_reference[count]:
359
+ #copy the dict
360
+ calc = copy.deepcopy(phase)
361
+
362
+ #pop extra keys which are not needed
363
+ #we dont kick out phase_name
364
+ extra_keys = ['composition', 'monte_carlo']
365
+ for key in extra_keys:
366
+ _ = calc.pop(key, None)
367
+
368
+ #update file if needed
369
+ outfile = fix_data_file(calc['lattice'], len(calc['element']))
370
+
371
+ #add ref phase, needed
372
+ calc['reference_phase'] = str(phase_reference_state)
373
+ calc['reference_composition'] = comps['reference']
374
+ calc['mode'] = str('fe')
375
+ calc['folder_prefix'] = f'{phase_name}-{comp:.2f}'
376
+ calc['lattice'] = str(outfile)
377
+
378
+ #now we need to run this for different temp
379
+ for temp in temp_arr:
380
+ calc_for_temp = copy.deepcopy(calc)
381
+ calc_for_temp['temperature'] = int(temp)
382
+ all_calculations.append(calc_for_temp)
383
+ else:
384
+ #off stoichiometric
385
+ #copy the dict
386
+ calc = copy.deepcopy(phase)
387
+
388
+ #first thing first, we need to calculate the number of atoms
389
+ #we follow the convention that composition is always given with the second species
390
+ n_atoms = np.sum(calc['composition']['number_of_atoms'])
391
+
392
+ #find number of atoms of second species
393
+ output_chemical_composition = {}
394
+ n_species_b = int(np.round(comp*n_atoms, decimals=0))
395
+ output_chemical_composition[reference_element] = n_species_b
396
+
397
+ n_species_a = int(n_atoms-n_species_b)
398
+ output_chemical_composition[other_element] = n_species_a
399
+
400
+ if n_species_a == 0:
401
+ raise ValueError("Please add pure phase as a new entry!")
402
+ #create input comp dict and output comp dict
403
+ input_chemical_composition = {element:number for element, number in zip(calc['element'],
404
+ calc['composition']['number_of_atoms'])}
405
+
406
+ #good, now we need to write such a structure out; likely better to use working directory for that
407
+ folder_prefix = f'{phase_name}-{comp:.2f}'
408
+ calc['reference_composition'] = comps['reference']
409
+ #if solid, its very easy; kinda
410
+ #if calc['reference_phase'] == 'solid':
411
+ if use_composition_scaling:
412
+ #this is solid , and comp scale is turned on
413
+ #pop extra keys which are not needed
414
+ #we dont kick out phase_name
415
+ extra_keys = ['composition', 'reference_phase']
416
+ for key in extra_keys:
417
+ _ = calc.pop(key, None)
418
+
419
+ #just submit comp scales
420
+ #add ref phase, needed
421
+ calc['mode'] = str('composition_scaling')
422
+ calc['folder_prefix'] = folder_prefix
423
+ calc['composition_scaling'] = {}
424
+ calc['composition_scaling']['output_chemical_composition'] = output_chemical_composition
425
+
426
+ else:
427
+ #manually create a mixed structure - not that the pair style is always ok :)
428
+
429
+ outfile = os.path.join(os.getcwd(), os.path.basename(calc['lattice'])+folder_prefix+'.comp.mod')
430
+ #print(f'finding comp trf from {input_chemical_composition} to {output_chemical_composition}')
431
+ #write_structure(calc['lattice'], input_chemical_composition, output_chemical_composition, outfile)
432
+
433
+ simplecalc = SimpleCalculation(calc['lattice'],
434
+ calc["element"],
435
+ input_chemical_composition,
436
+ output_chemical_composition)
437
+ compsc = CompositionTransformation(simplecalc)
438
+ compsc.write_structure(outfile)
439
+
440
+ #pop extra keys which are not needed
441
+ #we dont kick out phase name
442
+ extra_keys = ['composition']
443
+ for key in extra_keys:
444
+ _ = calc.pop(key, None)
445
+
446
+ #add ref phase, needed
447
+ calc['mode'] = str('fe')
448
+ calc['folder_prefix'] = folder_prefix
449
+ calc['lattice'] = str(outfile)
450
+
451
+ #now we need to run this for different temp
452
+ for temp in temp_arr:
453
+ calc_for_temp = copy.deepcopy(calc)
454
+ calc_for_temp['temperature'] = int(temp)
455
+ all_calculations.append(calc_for_temp)
456
+
457
+ #finish and write up the file
458
+ output_data = {"calculations": all_calculations}
459
+ for rep in ['.yml', '.yaml']:
460
+ calculation_base_name = calculation_base_name.replace(rep, '')
461
+
462
+ outfile_phase = phase_name + '_' + calculation_base_name + ".yaml"
463
+ with open(outfile_phase, 'w') as fout:
464
+ yaml.safe_dump(output_data, fout)
465
+ print(f'Total {len(all_calculations)} calculations found for phase {phase_name}, written to {outfile_phase}')
466
+
19
467
 
20
468
  def _get_temp_arg(tarr, temp, threshold=1E-1):
21
469
  if tarr is None:
@@ -91,7 +539,9 @@ def get_phase_free_energy(df, phase, temp,
91
539
  composition_grid=10000,
92
540
  composition_cutoff=None,
93
541
  reset_value=1,
94
- plot=False):
542
+ plot=False,
543
+ end_weight=3,
544
+ end_indices=4):
95
545
  """
96
546
  Get the free energy of a phase as a function of composition.
97
547
 
@@ -165,7 +615,9 @@ def get_phase_free_energy(df, phase, temp,
165
615
  else:
166
616
  entropy_term = []
167
617
 
168
- fe_fit = _get_free_energy_fit(composition, fes, fit_order=fit_order)
618
+ fe_fit = _get_free_energy_fit(composition, fes, fit_order=fit_order,
619
+ end_weight=end_weight,
620
+ end_indices=end_indices)
169
621
  compfine = np.linspace(np.min(composition), np.max(composition), composition_grid)
170
622
 
171
623
  #now fit on the comp grid again
@@ -234,21 +686,25 @@ def get_free_energy_mixing(dict_list, threshold=1E-3):
234
686
  d["free_energy_mix"] = ref
235
687
  return dict_list
236
688
 
237
- def create_color_list(dict_list, color_list=None):
238
- if color_list is None:
239
- color_list = colors
240
-
241
- phase_list = [d["phase"] for d in dict_list]
242
- combinations = list(itertools.combinations_with_replacement(phase_list, 2))
243
-
244
- if len(combinations) > len(color_list):
245
- raise ValueError(f'need {len(combinations)} colors, please provide using color_list=')
246
-
689
+ def create_color_list(phases):
690
+ combinations_list = ['-'.join(pair) for pair in combinations(phases, 2)]
691
+ same_element_pairs = ['-'.join([item, item]) for item in phases]
692
+ final_combinations = same_element_pairs + combinations_list
693
+
247
694
  color_dict = {}
248
- for count, combo in enumerate(combinations):
249
- color_dict[f'{combo[0]}-{combo[1]}'] = color_list[count]
250
- #add reverse just in case
251
- color_dict[f'{combo[1]}-{combo[0]}'] = color_list[count]
695
+
696
+ color_keys = list(matcolors.keys())
697
+ int_keys = list(matcolors['red'].keys())
698
+
699
+ for count, combination in enumerate(final_combinations):
700
+ index = count%len(color_keys)
701
+ second_index = -1
702
+ color_hex = matcolors[color_keys[int(index)]][int_keys[second_index]]
703
+ color_dict[combination] = color_hex
704
+ raw = combination.split('-')
705
+ if raw[0] != raw[1]:
706
+ reversecombo = f'{raw[1]}-{raw[0]}'
707
+ color_dict[reversecombo] = color_hex
252
708
  return color_dict
253
709
 
254
710
  def get_tangent_type(dict_list, tangent, energy):
@@ -298,16 +754,15 @@ def get_tangent_type(dict_list, tangent, energy):
298
754
  def get_common_tangents(dict_list,
299
755
  peak_cutoff=0.01,
300
756
  plot=False,
301
- remove_self_tangents_for=[],
302
- color_dict=None):
757
+ remove_self_tangents_for=[]):
303
758
  """
304
759
  Get common tangent constructions using convex hull method
305
760
  """
306
761
  points = np.vstack([np.column_stack((d["composition"],
307
762
  d["free_energy_mix"])) for d in dict_list])
308
763
 
309
- if color_dict is None:
310
- color_dict = create_color_list(dict_list)
764
+ #if color_dict is None:
765
+ # color_dict = create_color_list(dict_list)
311
766
 
312
767
  #make common tangent constructions
313
768
  #term checks if two different phases are stable at the end points, then common tangent is needed
@@ -328,7 +783,7 @@ def get_common_tangents(dict_list,
328
783
 
329
784
  tangents = []
330
785
  energies = []
331
- tangent_colors = []
786
+ tangent_types = []
332
787
  phases = []
333
788
 
334
789
  for d in dist:
@@ -336,10 +791,16 @@ def get_common_tangents(dict_list,
336
791
  e = [convex_points[sargs][d], convex_points[sargs][d+1]]
337
792
  phase_str = get_tangent_type(dict_list, t, e)
338
793
 
339
- if phase_str not in remove_self_tangents_for:
794
+ remove = False
795
+ ps = phase_str.split('-')
796
+ if ps[0] == ps[1]:
797
+ if ps[0] in remove_self_tangents_for:
798
+ remove = True
799
+
800
+ if not remove:
340
801
  tangents.append(t)
341
802
  energies.append(e)
342
- tangent_colors.append(color_dict[phase_str])
803
+ tangent_types.append(phase_str)
343
804
  phases.append(phase_str.split("-"))
344
805
 
345
806
  if plot:
@@ -349,15 +810,27 @@ def get_common_tangents(dict_list,
349
810
  plt.plot(t, e, color="black", ls="dashed")
350
811
  plt.ylim(top=0.0)
351
812
 
352
- return np.array(tangents), np.array(energies), np.array(tangent_colors), color_dict, np.array(phases)
813
+ return np.array(tangents), np.array(energies), np.array(tangent_types), np.array(phases)
353
814
 
354
815
 
355
816
  def plot_phase_diagram(tangents, temperature,
356
- colors,
817
+ tangent_types,
818
+ phases,
357
819
  edgecolor="#37474f",
358
820
  linewidth=1,
359
821
  linestyle='-'):
360
822
 
823
+ #get a phase list
824
+ color_dict = create_color_list(phases)
825
+ minimal_color_dict = {}
826
+ color_list = []
827
+ for key, val in color_dict.items():
828
+ if val not in color_list:
829
+ color_list.append(val)
830
+ minimal_color_dict[key] = val
831
+
832
+ legend_patches = [mpatches.Patch(color=color, label=label) for label, color in minimal_color_dict.items()]
833
+
361
834
  fig, ax = plt.subplots(edgecolor=edgecolor)
362
835
 
363
836
  for count, x in enumerate(tangents):
@@ -366,7 +839,8 @@ def plot_phase_diagram(tangents, temperature,
366
839
  [temperature[count], temperature[count]],
367
840
  linestyle,
368
841
  lw=linewidth,
369
- c=colors[count][c],
842
+ c=color_dict[tangent_types[count][c]],
370
843
  )
844
+ ax.legend(handles=legend_patches, loc='center left', bbox_to_anchor=(1, 0.5))
371
845
  return fig
372
846