-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SMACT Metallicity Handling Enhancements #367
Merged
+1,364
−5
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
17d4ae6
new_intermetallics_screening_features
ryannduma d0e999d
run pre-commit
ryannduma 5dbe9d7
changes to intermetallics_readme
ryannduma e4f4d84
intermetallics_readme_update
ryannduma 1ff91b8
coder rabbit nitpick changes
ryannduma 56246d3
backward compatibility, & composition handling
ryannduma f5ec89a
added usage examples to docs,
ryannduma 92aeb6c
small nitpick changes to resolve coderrabbitai rev
ryannduma db78427
nitpick changes to references in readme
ryannduma 4727c9a
these fixes, correct the three failing tests:
ryannduma 96b4f30
potential fix for Regex pattern did not match
ryannduma b6dd89a
final changes
ryannduma 82b8f6d
move intermetallics classification example to docs
ryannduma 9b90f92
resetting latest commit
ryannduma 30b7acb
trying to test something
ryannduma ad8fe19
move intermetallics_classification.ipynb to
ryannduma 7dcf738
major changes to PR from intermetallics to
ryannduma 484df32
Add colab installation to notebook
AntObi cfe9e56
Update pyproject.toml with dependencies for notebook
AntObi 7669102
Update requirements.txt for binder compataibility
AntObi 272fd4a
Convert metallicity readme to an example notebook
AntObi a4980b4
Add metallicity to examples
AntObi 94aec86
Rename header in examples/metallicity
AntObi fdb2167
Fix order of execution
AntObi 7b278f3
Try to fix test
AntObi ca030ef
changes from get_d_electron_fraction to
ryannduma File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
smact.metallicity module | ||
==================== | ||
|
||
.. automodule:: smact.metallicity | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
"""Utility functions for handling intermetallic compounds in SMACT.""" | ||
|
||
from __future__ import annotations | ||
|
||
import numpy as np | ||
from pymatgen.core import Composition | ||
|
||
import smact | ||
from smact import Element | ||
from smact.properties import valence_electron_count | ||
|
||
|
||
def _ensure_composition(composition: str | Composition) -> Composition: | ||
"""Convert input to a pymatgen Composition if it isn't already. | ||
Args: | ||
composition: Chemical formula as string or pymatgen Composition | ||
Returns: | ||
Composition: A pymatgen Composition object | ||
Raises: | ||
ValueError: If the composition string is empty | ||
ValueError: If the formula is invalid and can't be parsed. | ||
""" | ||
if isinstance(composition, str): | ||
if not composition.strip(): | ||
raise ValueError("Empty composition") | ||
# Try to parse with pymatgen | ||
try: | ||
return Composition(composition) | ||
except ValueError as exc: | ||
# If pymatgen can't parse, re-raise with a message the test expects | ||
raise ValueError("Invalid formula") from exc | ||
return composition | ||
|
||
|
||
def get_element_fraction(composition: str | Composition, element_set: set[str]) -> float: | ||
"""Calculate the fraction of elements from a given set in a composition. | ||
This helper function is used to avoid code duplication in functions that | ||
calculate fractions of specific element types (e.g., metals, d-block elements). | ||
Args: | ||
composition: Chemical formula as string or pymatgen Composition | ||
element_set: Set of element symbols to check for | ||
Returns: | ||
float: Fraction of the composition that consists of elements from the set (0-1) | ||
""" | ||
comp = _ensure_composition(composition) | ||
total_amt = sum(comp.values()) | ||
target_amt = sum(amt for el, amt in comp.items() if el.symbol in element_set) | ||
return target_amt / total_amt | ||
|
||
|
||
def get_metal_fraction(composition: str | Composition) -> float: | ||
"""Calculate the fraction of metallic elements in a composition. | ||
Implemented using get_element_fraction helper with smact.metals set. | ||
""" | ||
return get_element_fraction(composition, smact.metals) | ||
|
||
|
||
def get_d_block_element_fraction(composition: str | Composition) -> float: | ||
"""Calculate the fraction of d-block elements in a composition. | ||
Implemented using get_element_fraction helper with smact.d_block set. | ||
""" | ||
return get_element_fraction(composition, smact.d_block) | ||
|
||
|
||
def get_distinct_metal_count(composition: str | Composition) -> int: | ||
"""Count the number of distinct metallic elements in a composition.""" | ||
comp = _ensure_composition(composition) | ||
return sum(1 for el in comp.elements if el.symbol in smact.metals) | ||
|
||
|
||
def get_pauling_test_mismatch(composition: str | Composition) -> float: | ||
"""Calculate a score for how much the composition deviates from ideal | ||
Pauling electronegativity ordering. | ||
Higher mismatch => more difference (ionic, e.g. NaCl). | ||
Lower mismatch => metal-metal bonds (e.g. Fe-Al). | ||
Returns: | ||
float: Mismatch score (0=perfect match, higher=more deviation, NaN=missing data) | ||
""" | ||
comp = _ensure_composition(composition) | ||
elements = [Element(el.symbol) for el in comp.elements] | ||
electronegativities = [el.pauling_eneg for el in elements] | ||
|
||
# If any element lacks a known electronegativity, return NaN | ||
if None in electronegativities: | ||
return float("nan") | ||
else: | ||
mismatches = [] | ||
for i, (_el1, eneg1) in enumerate(zip(elements, electronegativities, strict=False)): | ||
for _el2, eneg2 in zip(elements[i + 1 :], electronegativities[i + 1 :], strict=False): | ||
# Always use absolute difference | ||
mismatch = abs(eneg1 - eneg2) | ||
mismatches.append(mismatch) | ||
|
||
# Return average mismatch across all unique pairs | ||
return np.mean(mismatches) if mismatches else 0.0 | ||
|
||
|
||
def metallicity_score(composition: str | Composition) -> float: | ||
"""Calculate a score (0-1) indicating the degree of a compound's metallic/alloy nature. | ||
1. Fraction of metallic elements | ||
2. Number of distinct metals | ||
3. d-block element fraction | ||
4. Electronegativity difference (Pauling mismatch) | ||
5. Valence electron count proximity to 8 | ||
Args: | ||
composition: Chemical formula or pymatgen Composition | ||
""" | ||
comp = _ensure_composition(composition) | ||
|
||
# Basic metrics | ||
metal_fraction = get_metal_fraction(comp) | ||
d_block_element_fraction = get_d_block_element_fraction(comp) | ||
n_metals = get_distinct_metal_count(comp) | ||
|
||
# Valence electron count factor | ||
try: | ||
vec = valence_electron_count(comp.reduced_formula) | ||
vec_factor = 1.0 - abs(vec - 8.0) / 8.0 | ||
except ValueError: | ||
vec_factor = 0.5 | ||
|
||
# Pauling mismatch => large => penalize | ||
pauling_mismatch = get_pauling_test_mismatch(comp) | ||
if np.isnan(pauling_mismatch): | ||
pauling_term = 0.5 | ||
else: | ||
scale = 3.0 | ||
penalty = min(pauling_mismatch / scale, 1.0) | ||
pauling_term = 1.0 - penalty | ||
|
||
# Weighted sum | ||
weights = { | ||
"metal_fraction": 0.3, | ||
"d_block_element_fraction": 0.2, | ||
"n_metals": 0.2, | ||
"vec": 0.15, | ||
"pauling": 0.15, | ||
} | ||
score = ( | ||
weights["metal_fraction"] * metal_fraction | ||
+ weights["d_block_element_fraction"] * d_block_element_fraction | ||
+ weights["n_metals"] * min(n_metals / 3.0, 1.0) | ||
+ weights["vec"] * vec_factor | ||
+ weights["pauling"] * pauling_term | ||
) | ||
return max(0.0, min(1.0, score)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
"""Tests for the metallicity module.""" | ||
|
||
from __future__ import annotations | ||
|
||
import unittest | ||
|
||
import pytest | ||
from pymatgen.core import Composition | ||
|
||
import smact | ||
from smact.metallicity import ( | ||
get_d_block_element_fraction, | ||
get_distinct_metal_count, | ||
get_element_fraction, | ||
get_metal_fraction, | ||
get_pauling_test_mismatch, | ||
metallicity_score, | ||
) | ||
|
||
|
||
class TestMetallicity(unittest.TestCase): | ||
"""Test the metallicity module functionality.""" | ||
|
||
def setUp(self): | ||
"""Set up test cases.""" | ||
# Known metallic/alloy compounds | ||
self.metallic_compounds = [ | ||
"Fe3Al", # Classic intermetallic | ||
"Ni3Ti", # Superalloy component | ||
"Cu3Au", # Ordered alloy | ||
"Fe2Nb", # Laves phase | ||
] | ||
|
||
# Known non-metallic compounds | ||
self.non_metallic_compounds = [ | ||
"NaCl", # Ionic | ||
"SiO2", # Covalent | ||
"Fe2O3", # Metal oxide | ||
"CuSO4", # Complex ionic | ||
] | ||
|
||
def test_get_element_fraction(self): | ||
"""Test the helper function for element fraction calculations.""" | ||
# Test with metals set | ||
self.assertAlmostEqual( | ||
get_element_fraction(Composition("Fe3Al"), smact.metals), | ||
1.0, | ||
places=6, | ||
msg="Expected all elements in Fe3Al to be metals", | ||
) | ||
|
||
# Test with d-block set | ||
self.assertAlmostEqual( | ||
get_element_fraction(Composition("Fe2Nb"), smact.d_block), | ||
1.0, | ||
places=6, | ||
msg="Expected all elements in Fe2Nb to be d-block", | ||
) | ||
|
||
# Test with empty set | ||
self.assertAlmostEqual( | ||
get_element_fraction(Composition("Fe3Al"), set()), | ||
0.0, | ||
places=6, | ||
msg="Expected zero fraction for empty element set", | ||
) | ||
|
||
def test_get_metal_fraction(self): | ||
"""Test metal fraction calculation.""" | ||
# Should be 1.0 for pure metallic compounds | ||
self.assertAlmostEqual( | ||
get_metal_fraction(Composition("Fe3Al")), | ||
1.0, | ||
places=6, | ||
msg="Expected pure metallic composition for Fe3Al", | ||
) | ||
|
||
# Should be 0.0 for compounds with no metals | ||
self.assertAlmostEqual( | ||
get_metal_fraction(Composition("SiO2")), | ||
0.0, | ||
places=6, | ||
msg="Expected no metallic elements in SiO2", | ||
) | ||
|
||
# Should be fractional for mixed compounds | ||
fe2o3 = get_metal_fraction(Composition("Fe2O3")) | ||
self.assertTrue( | ||
0 < fe2o3 < 1, | ||
msg=f"Expected fractional metal content for Fe2O3, got {fe2o3:.2f}", | ||
) | ||
|
||
def test_get_d_block_element_fraction(self): | ||
"""Test d-block element fraction calculation.""" | ||
# Should be 1.0 for pure transition metal compounds | ||
self.assertAlmostEqual( | ||
get_d_block_element_fraction(Composition("Fe2Nb")), | ||
1.0, | ||
places=6, | ||
msg="Expected all d-block elements in Fe2Nb", | ||
) | ||
|
||
# Should be 0.0 for compounds with no d-block elements | ||
self.assertAlmostEqual( | ||
get_d_block_element_fraction(Composition("NaCl")), | ||
0.0, | ||
places=6, | ||
msg="Expected no d-block elements in NaCl", | ||
) | ||
|
||
def test_get_distinct_metal_count(self): | ||
"""Test counting of distinct metals.""" | ||
self.assertEqual( | ||
get_distinct_metal_count(Composition("Fe3Al")), | ||
2, | ||
msg="Expected 2 distinct metals in Fe3Al", | ||
) | ||
self.assertEqual( | ||
get_distinct_metal_count(Composition("NaCl")), | ||
1, | ||
msg="Expected 1 distinct metal in NaCl", | ||
) | ||
self.assertEqual( | ||
get_distinct_metal_count(Composition("SiO2")), | ||
0, | ||
msg="Expected no metals in SiO2", | ||
) | ||
|
||
def test_get_pauling_test_mismatch(self): | ||
"""Test Pauling electronegativity mismatch calculation.""" | ||
# Ionic compounds should have high mismatch | ||
nacl_mismatch = get_pauling_test_mismatch(Composition("NaCl")) | ||
|
||
# Metallic compounds should have lower mismatch | ||
fe3al_mismatch = get_pauling_test_mismatch(Composition("Fe3Al")) | ||
|
||
self.assertTrue( | ||
fe3al_mismatch < nacl_mismatch, | ||
msg=f"Expected lower Pauling mismatch for Fe3Al ({fe3al_mismatch:.2f}) than NaCl ({nacl_mismatch:.2f})", | ||
) | ||
|
||
def test_metallicity_score(self): | ||
"""Test the overall metallicity scoring function.""" | ||
# Known metallic compounds should score high | ||
for formula in self.metallic_compounds: | ||
score = metallicity_score(formula) | ||
self.assertTrue( | ||
score > 0.7, | ||
msg=f"Expected high metallicity score (>0.7) for {formula}, but got {score:.2f}", | ||
) | ||
|
||
# Non-metallic compounds should score low | ||
for formula in self.non_metallic_compounds: | ||
score = metallicity_score(formula) | ||
self.assertTrue( | ||
score < 0.5, | ||
msg=f"Expected low metallicity score (<0.5) for {formula}, but got {score:.2f}", | ||
) | ||
|
||
def test_edge_cases(self): | ||
"""Test edge cases and error handling.""" | ||
# Single element | ||
score = metallicity_score("Fe") | ||
self.assertTrue(0.0 <= score <= 1.0, msg=f"Expected score between 0 and 1 for Fe, got {score:.2f}") | ||
|
||
# Empty composition -> expect ValueError("Empty composition") | ||
with pytest.raises(ValueError, match="Empty composition"): | ||
metallicity_score("") | ||
|
||
# Invalid formula -> e.g. "NotAnElement" | ||
with pytest.raises(ValueError, match="Invalid formula"): | ||
metallicity_score("NotAnElement") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if this function is needed. Could you explain if it is?
I believe the
Composition
class is already robust enough with its error handling.In each function call in the rest of module, you could probably have
comp=Composition(composition)
even if composition is an instance of the Composition class.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think this might be a redundancy on my end, but I originally wrote this to ensure the composition was a pymatgen composition object even in contexts where they may enter a composition that's otherwise - ill do some testing on my end and possibly get rid of the function should this be the case and incorporate your suggested changes.