@@ -903,7 +903,6 @@ def get_decomp_and_phase_separation_energy(
903
903
}
904
904
905
905
# NOTE calling PhaseDiagram is only reasonable if the composition has fewer than 5 elements
906
- # TODO can we call PatchedPhaseDiagram here?
907
906
inner_hull = PhaseDiagram (reduced_space )
908
907
909
908
competing_entries = inner_hull .stable_entries | {* self ._get_stable_entries_in_space (entry_elems )}
@@ -1036,8 +1035,6 @@ def get_critical_compositions(self, comp1, comp2):
1036
1035
if np .all (c1 == c2 ):
1037
1036
return [comp1 .copy (), comp2 .copy ()]
1038
1037
1039
- # NOTE made into method to facilitate inheritance of this method
1040
- # in PatchedPhaseDiagram if approximate solution can be found.
1041
1038
intersections = self ._get_simplex_intersections (c1 , c2 )
1042
1039
1043
1040
# find position along line
@@ -1619,29 +1616,21 @@ def __init__(
1619
1616
# Add the elemental references
1620
1617
inds .extend ([min_entries .index (el ) for el in el_refs .values ()])
1621
1618
1622
- self . qhull_entries = tuple (min_entries [idx ] for idx in inds )
1619
+ qhull_entries = tuple (min_entries [idx ] for idx in inds )
1623
1620
# make qhull spaces frozensets since they become keys to self.pds dict and frozensets are hashable
1624
1621
# prevent repeating elements in chemical space and avoid the ordering problem (i.e. Fe-O == O-Fe automatically)
1625
- self . _qhull_spaces = tuple (frozenset (entry .elements ) for entry in self . qhull_entries )
1622
+ qhull_spaces = tuple (frozenset (entry .elements ) for entry in qhull_entries )
1626
1623
1627
1624
# Get all unique chemical spaces
1628
- spaces = {s for s in self . _qhull_spaces if len (s ) > 1 }
1625
+ spaces = {s for s in qhull_spaces if len (s ) > 1 }
1629
1626
1630
1627
# Remove redundant chemical spaces
1631
- if not keep_all_spaces and len (spaces ) > 1 :
1632
- max_size = max (len (s ) for s in spaces )
1633
-
1634
- systems = set ()
1635
- # NOTE reduce the number of comparisons by only comparing to larger sets
1636
- for idx in range (2 , max_size + 1 ):
1637
- test = (s for s in spaces if len (s ) == idx )
1638
- refer = (s for s in spaces if len (s ) > idx )
1639
- systems |= {t for t in test if not any (t .issubset (r ) for r in refer )}
1640
-
1641
- spaces = systems
1628
+ spaces = self .remove_redundant_spaces (spaces , keep_all_spaces )
1642
1629
1643
1630
# TODO comprhys: refactor to have self._compute method to allow serialization
1644
- self .spaces = sorted (spaces , key = len , reverse = False ) # Calculate pds for smaller dimension spaces first
1631
+ self .spaces = sorted (spaces , key = len , reverse = True ) # Calculate pds for smaller dimension spaces last
1632
+ self .qhull_entries = qhull_entries
1633
+ self ._qhull_spaces = qhull_spaces
1645
1634
self .pds = dict (self ._get_pd_patch_for_space (s ) for s in tqdm (self .spaces , disable = not verbose ))
1646
1635
self .all_entries = all_entries
1647
1636
self .el_refs = el_refs
@@ -1675,7 +1664,19 @@ def __contains__(self, item: frozenset[Element]) -> bool:
1675
1664
return item in self .pds
1676
1665
1677
1666
def as_dict (self ) -> dict [str , Any ]:
1678
- """
1667
+ """Write the entries and elements used to construct the PatchedPhaseDiagram
1668
+ to a dictionary.
1669
+
1670
+ NOTE unlike PhaseDiagram the computation involved in constructing the
1671
+ PatchedPhaseDiagram is not saved on serialisation. This is done because
1672
+ hierarchically calling the `PhaseDiagram.as_dict()` method would break the
1673
+ link in memory between entries in overlapping patches leading to a
1674
+ ballooning of the amount of memory used.
1675
+
1676
+ NOTE For memory efficiency the best way to store patched phase diagrams is
1677
+ via pickling. As this allows all the entries in overlapping patches to share
1678
+ the same id in memory when unpickling.
1679
+
1679
1680
Returns:
1680
1681
dict[str, Any]: MSONable dictionary representation of PatchedPhaseDiagram.
1681
1682
"""
@@ -1688,7 +1689,18 @@ def as_dict(self) -> dict[str, Any]:
1688
1689
1689
1690
@classmethod
1690
1691
def from_dict (cls , dct : dict ) -> Self :
1691
- """
1692
+ """Reconstruct PatchedPhaseDiagram from dictionary serialisation.
1693
+
1694
+ NOTE unlike PhaseDiagram the computation involved in constructing the
1695
+ PatchedPhaseDiagram is not saved on serialisation. This is done because
1696
+ hierarchically calling the `PhaseDiagram.as_dict()` method would break the
1697
+ link in memory between entries in overlapping patches leading to a
1698
+ ballooning of the amount of memory used.
1699
+
1700
+ NOTE For memory efficiency the best way to store patched phase diagrams is
1701
+ via pickling. As this allows all the entries in overlapping patches to share
1702
+ the same id in memory when unpickling.
1703
+
1692
1704
Args:
1693
1705
dct (dict): dictionary representation of PatchedPhaseDiagram.
1694
1706
@@ -1699,9 +1711,23 @@ def from_dict(cls, dct: dict) -> Self:
1699
1711
elements = [Element .from_dict (elem ) for elem in dct ["elements" ]]
1700
1712
return cls (entries , elements )
1701
1713
1714
+ @staticmethod
1715
+ def remove_redundant_spaces (spaces , keep_all_spaces = False ):
1716
+ if keep_all_spaces or len (spaces ) <= 1 :
1717
+ return spaces
1718
+
1719
+ # Sort spaces by size in descending order and pre-compute lengths
1720
+ sorted_spaces = sorted (spaces , key = len , reverse = True )
1721
+
1722
+ result = []
1723
+ for i , space_i in enumerate (sorted_spaces ):
1724
+ if not any (space_i .issubset (larger_space ) for larger_space in sorted_spaces [:i ]):
1725
+ result .append (space_i )
1726
+
1727
+ return result
1728
+
1702
1729
# NOTE following methods are inherited unchanged from PhaseDiagram:
1703
1730
# __repr__,
1704
- # as_dict,
1705
1731
# all_entries_hulldata,
1706
1732
# unstable_entries,
1707
1733
# stable_entries,
@@ -1771,8 +1797,6 @@ def get_equilibrium_reaction_energy(self, entry: Entry) -> float:
1771
1797
"""
1772
1798
return self .get_phase_separation_energy (entry , stable_only = True )
1773
1799
1774
- # NOTE the following functions are not implemented for PatchedPhaseDiagram
1775
-
1776
1800
def get_decomp_and_e_above_hull (
1777
1801
self ,
1778
1802
entry : PDEntry ,
@@ -1787,6 +1811,20 @@ def get_decomp_and_e_above_hull(
1787
1811
entry = entry , allow_negative = allow_negative , check_stable = check_stable , on_error = on_error
1788
1812
)
1789
1813
1814
+ def _get_pd_patch_for_space (self , space : frozenset [Element ]) -> tuple [frozenset [Element ], PhaseDiagram ]:
1815
+ """
1816
+ Args:
1817
+ space (frozenset[Element]): chemical space of the form A-B-X.
1818
+
1819
+ Returns:
1820
+ space, PhaseDiagram for the given chemical space
1821
+ """
1822
+ space_entries = [e for e , s in zip (self .qhull_entries , self ._qhull_spaces ) if space .issuperset (s )]
1823
+
1824
+ return space , PhaseDiagram (space_entries )
1825
+
1826
+ # NOTE the following functions are not implemented for PatchedPhaseDiagram
1827
+
1790
1828
def _get_facet_and_simplex (self ):
1791
1829
"""Not Implemented - See PhaseDiagram."""
1792
1830
raise NotImplementedError ("_get_facet_and_simplex() not implemented for PatchedPhaseDiagram" )
@@ -1835,18 +1873,6 @@ def get_chempot_range_stability_phase(self):
1835
1873
"""Not Implemented - See PhaseDiagram."""
1836
1874
raise NotImplementedError ("get_chempot_range_stability_phase() not implemented for PatchedPhaseDiagram" )
1837
1875
1838
- def _get_pd_patch_for_space (self , space : frozenset [Element ]) -> tuple [frozenset [Element ], PhaseDiagram ]:
1839
- """
1840
- Args:
1841
- space (frozenset[Element]): chemical space of the form A-B-X.
1842
-
1843
- Returns:
1844
- space, PhaseDiagram for the given chemical space
1845
- """
1846
- space_entries = [e for e , s in zip (self .qhull_entries , self ._qhull_spaces ) if space .issuperset (s )]
1847
-
1848
- return space , PhaseDiagram (space_entries )
1849
-
1850
1876
1851
1877
class ReactionDiagram :
1852
1878
"""
0 commit comments