calphy 1.3.13__py3-none-any.whl → 1.4.3__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,469 @@ 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
+ if "use_composition_scaling" in comps.keys():
318
+ use_composition_scaling = bool(comps["use_composition_scaling"])
319
+ else:
320
+ use_composition_scaling = True
321
+ if str(phase_reference_state) == 'liquid':
322
+ use_composition_scaling = False
323
+
324
+ other_element_list = copy.deepcopy(phase['element'])
325
+ other_element_list.remove(reference_element)
326
+ other_element = other_element_list[0]
327
+
328
+ #convert to list if scalar
329
+ if not isinstance(comps['range'], list):
330
+ comps["range"] = [comps["range"]]
331
+ if len(comps["range"]) == 2:
332
+ comp_arr = np.arange(comps['range'][0], comps['range'][-1], comps['interval'])
333
+ last_val = comps['range'][-1]
334
+ if last_val not in comp_arr:
335
+ comp_arr = np.append(comp_arr, last_val)
336
+ ncomps = len(comp_arr)
337
+ is_reference = np.abs(comp_arr-comps['reference']) < 1E-5
338
+ elif len(comps["range"]) == 1:
339
+ ncomps = 1
340
+ comp_arr = [comps["range"][0]]
341
+ is_reference = [True]
342
+ else:
343
+ raise ValueError("Composition range should be scalar of list of two values!")
344
+
345
+ temps = phase["temperature"]
346
+ if not isinstance(temps['range'], list):
347
+ temps["range"] = [temps["range"]]
348
+ if len(temps["range"]) == 2:
349
+ ntemps = int((temps['range'][-1]-temps['range'][0])/temps['interval'])+1
350
+ temp_arr = np.linspace(temps['range'][0], temps['range'][-1], ntemps, endpoint=True)
351
+ elif len(temps["range"]) == 1:
352
+ ntemps = 1
353
+ temp_arr = [temps["range"][0]]
354
+ else:
355
+ raise ValueError("Temperature range should be scalar of list of two values!")
356
+
357
+ all_calculations = []
358
+
359
+ for count, comp in enumerate(comp_arr):
360
+ #check if ref comp equals given comp
361
+ if is_reference[count]:
362
+ #copy the dict
363
+ calc = copy.deepcopy(phase)
364
+
365
+ #pop extra keys which are not needed
366
+ #we dont kick out phase_name
367
+ extra_keys = ['composition', 'monte_carlo']
368
+ for key in extra_keys:
369
+ _ = calc.pop(key, None)
370
+
371
+ #update file if needed
372
+ outfile = fix_data_file(calc['lattice'], len(calc['element']))
373
+
374
+ #add ref phase, needed
375
+ calc['reference_phase'] = str(phase_reference_state)
376
+ calc['reference_composition'] = comps['reference']
377
+ calc['mode'] = str('fe')
378
+ calc['folder_prefix'] = f'{phase_name}-{comp:.2f}'
379
+ calc['lattice'] = str(outfile)
380
+
381
+ #now we need to run this for different temp
382
+ for temp in temp_arr:
383
+ calc_for_temp = copy.deepcopy(calc)
384
+ calc_for_temp['temperature'] = int(temp)
385
+ all_calculations.append(calc_for_temp)
386
+ else:
387
+ #off stoichiometric
388
+ #copy the dict
389
+ calc = copy.deepcopy(phase)
390
+
391
+ #first thing first, we need to calculate the number of atoms
392
+ #we follow the convention that composition is always given with the second species
393
+ n_atoms = np.sum(calc['composition']['number_of_atoms'])
394
+
395
+ #find number of atoms of second species
396
+ output_chemical_composition = {}
397
+ n_species_b = int(np.round(comp*n_atoms, decimals=0))
398
+ output_chemical_composition[reference_element] = n_species_b
399
+
400
+ n_species_a = int(n_atoms-n_species_b)
401
+ output_chemical_composition[other_element] = n_species_a
402
+
403
+ if n_species_a == 0:
404
+ raise ValueError("Please add pure phase as a new entry!")
405
+ #create input comp dict and output comp dict
406
+ input_chemical_composition = {element:number for element, number in zip(calc['element'],
407
+ calc['composition']['number_of_atoms'])}
408
+
409
+ #good, now we need to write such a structure out; likely better to use working directory for that
410
+ folder_prefix = f'{phase_name}-{comp:.2f}'
411
+ calc['reference_composition'] = comps['reference']
412
+ #if solid, its very easy; kinda
413
+ #if calc['reference_phase'] == 'solid':
414
+ if use_composition_scaling:
415
+ #this is solid , and comp scale is turned on
416
+ #pop extra keys which are not needed
417
+ #we dont kick out phase_name
418
+ extra_keys = ['composition', 'reference_phase']
419
+ for key in extra_keys:
420
+ _ = calc.pop(key, None)
421
+
422
+ #just submit comp scales
423
+ #add ref phase, needed
424
+ calc['mode'] = str('composition_scaling')
425
+ calc['folder_prefix'] = folder_prefix
426
+ calc['composition_scaling'] = {}
427
+ calc['composition_scaling']['output_chemical_composition'] = output_chemical_composition
428
+
429
+ else:
430
+ #manually create a mixed structure - not that the pair style is always ok :)
431
+
432
+ outfile = os.path.join(os.getcwd(), os.path.basename(calc['lattice'])+folder_prefix+'.comp.mod')
433
+ #print(f'finding comp trf from {input_chemical_composition} to {output_chemical_composition}')
434
+ #write_structure(calc['lattice'], input_chemical_composition, output_chemical_composition, outfile)
435
+
436
+ simplecalc = SimpleCalculation(calc['lattice'],
437
+ calc["element"],
438
+ input_chemical_composition,
439
+ output_chemical_composition)
440
+ compsc = CompositionTransformation(simplecalc)
441
+ compsc.write_structure(outfile)
442
+
443
+ #pop extra keys which are not needed
444
+ #we dont kick out phase name
445
+ extra_keys = ['composition']
446
+ for key in extra_keys:
447
+ _ = calc.pop(key, None)
448
+
449
+ #add ref phase, needed
450
+ calc['mode'] = str('fe')
451
+ calc['folder_prefix'] = folder_prefix
452
+ calc['lattice'] = str(outfile)
453
+
454
+ #now we need to run this for different temp
455
+ for temp in temp_arr:
456
+ calc_for_temp = copy.deepcopy(calc)
457
+ calc_for_temp['temperature'] = int(temp)
458
+ all_calculations.append(calc_for_temp)
459
+
460
+ #finish and write up the file
461
+ output_data = {"calculations": all_calculations}
462
+ for rep in ['.yml', '.yaml']:
463
+ calculation_base_name = calculation_base_name.replace(rep, '')
464
+
465
+ outfile_phase = phase_name + '_' + calculation_base_name + ".yaml"
466
+ with open(outfile_phase, 'w') as fout:
467
+ yaml.safe_dump(output_data, fout)
468
+ print(f'Total {len(all_calculations)} calculations found for phase {phase_name}, written to {outfile_phase}')
469
+
19
470
 
20
471
  def _get_temp_arg(tarr, temp, threshold=1E-1):
21
472
  if tarr is None:
@@ -91,7 +542,9 @@ def get_phase_free_energy(df, phase, temp,
91
542
  composition_grid=10000,
92
543
  composition_cutoff=None,
93
544
  reset_value=1,
94
- plot=False):
545
+ plot=False,
546
+ end_weight=3,
547
+ end_indices=4):
95
548
  """
96
549
  Get the free energy of a phase as a function of composition.
97
550
 
@@ -165,7 +618,9 @@ def get_phase_free_energy(df, phase, temp,
165
618
  else:
166
619
  entropy_term = []
167
620
 
168
- fe_fit = _get_free_energy_fit(composition, fes, fit_order=fit_order)
621
+ fe_fit = _get_free_energy_fit(composition, fes, fit_order=fit_order,
622
+ end_weight=end_weight,
623
+ end_indices=end_indices)
169
624
  compfine = np.linspace(np.min(composition), np.max(composition), composition_grid)
170
625
 
171
626
  #now fit on the comp grid again
@@ -234,21 +689,25 @@ def get_free_energy_mixing(dict_list, threshold=1E-3):
234
689
  d["free_energy_mix"] = ref
235
690
  return dict_list
236
691
 
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
-
692
+ def create_color_list(phases):
693
+ combinations_list = ['-'.join(pair) for pair in combinations(phases, 2)]
694
+ same_element_pairs = ['-'.join([item, item]) for item in phases]
695
+ final_combinations = same_element_pairs + combinations_list
696
+
247
697
  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]
698
+
699
+ color_keys = list(matcolors.keys())
700
+ int_keys = list(matcolors['red'].keys())
701
+
702
+ for count, combination in enumerate(final_combinations):
703
+ index = count%len(color_keys)
704
+ second_index = -1
705
+ color_hex = matcolors[color_keys[int(index)]][int_keys[second_index]]
706
+ color_dict[combination] = color_hex
707
+ raw = combination.split('-')
708
+ if raw[0] != raw[1]:
709
+ reversecombo = f'{raw[1]}-{raw[0]}'
710
+ color_dict[reversecombo] = color_hex
252
711
  return color_dict
253
712
 
254
713
  def get_tangent_type(dict_list, tangent, energy):
@@ -298,16 +757,15 @@ def get_tangent_type(dict_list, tangent, energy):
298
757
  def get_common_tangents(dict_list,
299
758
  peak_cutoff=0.01,
300
759
  plot=False,
301
- remove_self_tangents_for=[],
302
- color_dict=None):
760
+ remove_self_tangents_for=[]):
303
761
  """
304
762
  Get common tangent constructions using convex hull method
305
763
  """
306
764
  points = np.vstack([np.column_stack((d["composition"],
307
765
  d["free_energy_mix"])) for d in dict_list])
308
766
 
309
- if color_dict is None:
310
- color_dict = create_color_list(dict_list)
767
+ #if color_dict is None:
768
+ # color_dict = create_color_list(dict_list)
311
769
 
312
770
  #make common tangent constructions
313
771
  #term checks if two different phases are stable at the end points, then common tangent is needed
@@ -328,7 +786,7 @@ def get_common_tangents(dict_list,
328
786
 
329
787
  tangents = []
330
788
  energies = []
331
- tangent_colors = []
789
+ tangent_types = []
332
790
  phases = []
333
791
 
334
792
  for d in dist:
@@ -336,10 +794,16 @@ def get_common_tangents(dict_list,
336
794
  e = [convex_points[sargs][d], convex_points[sargs][d+1]]
337
795
  phase_str = get_tangent_type(dict_list, t, e)
338
796
 
339
- if phase_str not in remove_self_tangents_for:
797
+ remove = False
798
+ ps = phase_str.split('-')
799
+ if ps[0] == ps[1]:
800
+ if ps[0] in remove_self_tangents_for:
801
+ remove = True
802
+
803
+ if not remove:
340
804
  tangents.append(t)
341
805
  energies.append(e)
342
- tangent_colors.append(color_dict[phase_str])
806
+ tangent_types.append(phase_str)
343
807
  phases.append(phase_str.split("-"))
344
808
 
345
809
  if plot:
@@ -349,15 +813,27 @@ def get_common_tangents(dict_list,
349
813
  plt.plot(t, e, color="black", ls="dashed")
350
814
  plt.ylim(top=0.0)
351
815
 
352
- return np.array(tangents), np.array(energies), np.array(tangent_colors), color_dict, np.array(phases)
816
+ return np.array(tangents), np.array(energies), np.array(tangent_types), np.array(phases)
353
817
 
354
818
 
355
819
  def plot_phase_diagram(tangents, temperature,
356
- colors,
820
+ tangent_types,
821
+ phases,
357
822
  edgecolor="#37474f",
358
823
  linewidth=1,
359
824
  linestyle='-'):
360
825
 
826
+ #get a phase list
827
+ color_dict = create_color_list(phases)
828
+ minimal_color_dict = {}
829
+ color_list = []
830
+ for key, val in color_dict.items():
831
+ if val not in color_list:
832
+ color_list.append(val)
833
+ minimal_color_dict[key] = val
834
+
835
+ legend_patches = [mpatches.Patch(color=color, label=label) for label, color in minimal_color_dict.items()]
836
+
361
837
  fig, ax = plt.subplots(edgecolor=edgecolor)
362
838
 
363
839
  for count, x in enumerate(tangents):
@@ -366,7 +842,8 @@ def plot_phase_diagram(tangents, temperature,
366
842
  [temperature[count], temperature[count]],
367
843
  linestyle,
368
844
  lw=linewidth,
369
- c=colors[count][c],
845
+ c=color_dict[tangent_types[count][c]],
370
846
  )
847
+ ax.legend(handles=legend_patches, loc='center left', bbox_to_anchor=(1, 0.5))
371
848
  return fig
372
849