Skip to content

Commit c2087b2

Browse files
authored
Merge pull request #100 from lmseidler/main
Element composition dict with str and mixed keys
2 parents ad8548f + dbe82b3 commit c2087b2

File tree

4 files changed

+103
-14
lines changed

4 files changed

+103
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Changed
9+
- to set the elemental composition it is now possible to use dicts with not only int but also the element symbols (str)
10+
- dict keys for elemental compositions will now always be checked for validity
811

912
## [0.5.0] - 2024-12-16
1013
### Changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ def main():
133133
config.generate.max_num_atoms = 15
134134
config.generate.element_composition = "Ce:1-1"
135135
# alternatively as a dictionary: config.generate.element_composition = {39:(1,1)}
136+
# or: config.generate.element_composition = {"Ce":(1,1)"}
137+
# or as mixed-key dict, e.g. for Ce and O: {"Ce":(1,1), 7:(2,2)}
136138
config.generate.forbidden_elements = "21-30,39-48,57-80"
137139
# alternatively as a list: config.generate.forbidden_elements = [20,21,22,23] # 24,25,26...
138140

src/mindlessgen/prog/config.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import numpy as np
1313
import toml
1414

15+
from mindlessgen.molecules.molecule import PSE_SYMBOLS
16+
1517
from ..molecules import PSE_NUMBERS
1618

1719

@@ -290,18 +292,19 @@ def element_composition(self):
290292

291293
@element_composition.setter
292294
def element_composition(
293-
self, composition: None | str | dict[int, tuple[int | None, int | None]]
295+
self, composition: None | str | dict[int | str, tuple[int | None, int | None]]
294296
) -> None:
295297
"""
296-
If composition_str: str, it should be a string with the format:
298+
If composition: str:
297299
Parses the element_composition string and stores the parsed data
298300
in the _element_composition dictionary.
299301
Format: "C:2-10, H:10-20, O:1-5, N:1-*"
300-
If composition_str: dict, it should be a dictionary with integer keys and tuple values. Will be stored as is.
302+
If composition: dict:
303+
Should be a dictionary with integer/string keys and tuple values. Will be stored as is.
301304
302305
Arguments:
303306
composition_str (str): String with the element composition
304-
composition_str (dict): Dictionary with integer keys and tuple values
307+
composition_str (dict): Dictionary with integer/str keys and tuple values
305308
Raises:
306309
TypeError: If composition_str is not a string or a dictionary
307310
AttributeError: If the element is not found in the periodic table
@@ -312,25 +315,50 @@ def element_composition(
312315

313316
if not composition:
314317
return
318+
319+
# Will return if composition dict does not contain either int or str keys and tuple[int | None, int | None] values
320+
# Will also return if dict is valid after setting property
315321
if isinstance(composition, dict):
322+
tmp = {}
323+
324+
# Check validity and also convert str keys into atomic numbers
316325
for key, value in composition.items():
317326
if (
318-
not isinstance(key, int)
327+
not (isinstance(key, int) or isinstance(key, str))
319328
or not isinstance(value, tuple)
320329
or len(value) != 2
321330
or not all(isinstance(val, int) or val is None for val in value)
322331
):
323332
raise TypeError(
324-
"Element composition dictionary should be a dictionary with integer keys and tuple values (int, int)."
333+
"Element composition dictionary should be a dictionary with either integer or string keys and tuple values (int, int)."
325334
)
326-
self._element_composition = composition
335+
336+
# Convert str keys
337+
if isinstance(key, str):
338+
element_number = PSE_NUMBERS.get(key.lower(), None)
339+
if element_number is None:
340+
raise KeyError(
341+
f"Element {key} not found in the periodic table."
342+
)
343+
tmp[element_number - 1] = composition[key]
344+
# Check int keys
345+
else:
346+
if key + 1 in PSE_SYMBOLS:
347+
tmp[key] = composition[key]
348+
else:
349+
raise KeyError(
350+
f"Element with atomic number {key+1} (provided key: {key}) not found in the periodic table."
351+
)
352+
self._element_composition = tmp
327353
return
354+
328355
if not isinstance(composition, str):
329356
raise TypeError(
330357
"Element composition should be a string (will be parsed) or "
331-
+ "a dictionary with integer keys and tuple values."
358+
+ "a dictionary with integer/string keys and tuple values."
332359
)
333360

361+
# Parsing composition string
334362
element_dict: dict[int, tuple[int | None, int | None]] = {}
335363
elements = composition.split(",")
336364
# remove leading and trailing whitespaces
@@ -537,9 +565,11 @@ def check_config(self, verbosity: int = 1) -> None:
537565
if (
538566
np.sum(
539567
[
540-
self.element_composition.get(i, (0, 0))[0]
541-
if self.element_composition.get(i, (0, 0))[0] is not None
542-
else 0
568+
(
569+
self.element_composition.get(i, (0, 0))[0]
570+
if self.element_composition.get(i, (0, 0))[0] is not None
571+
else 0
572+
)
543573
for i in self.element_composition
544574
]
545575
)

test/test_generate/test_generate_molecule.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def test_generate_atom_list(min_atoms, max_atoms, default_generate_config):
4141
assert np.sum(atom_list) <= max_atoms
4242

4343

44-
# Test the element composition property of the GenerateConfig class
45-
def test_generate_config_element_composition(default_generate_config):
46-
"""Test the element composition property of the GenerateConfig class."""
44+
# Test the element composition property of the GenerateConfig class with a composition string
45+
def test_generate_config_element_composition_string(default_generate_config):
46+
"""Test the element composition property of the GenerateConfig class with a composition string."""
4747
default_generate_config.min_num_atoms = 10
4848
default_generate_config.max_num_atoms = 15
4949
default_generate_config.element_composition = "C:2-2, N:3-3, O:1-1"
@@ -55,6 +55,60 @@ def test_generate_config_element_composition(default_generate_config):
5555
assert atom_list[7] == 1
5656

5757

58+
# Test the element composition property of the GenerateConfig class with an int key composition dict
59+
def test_generate_config_element_composition_dict_int(default_generate_config):
60+
"""Test the element composition property of the GenerateConfig class with an int key composition dict."""
61+
62+
# Pure int keys
63+
default_generate_config.min_num_atoms = 10
64+
default_generate_config.max_num_atoms = 15
65+
default_generate_config.element_composition = {
66+
5: (2, 2),
67+
6: (3, 3),
68+
7: (1, 1),
69+
} # NOTE: mind 0-based indexing for atomic numbers
70+
atom_list = generate_atom_list(default_generate_config, verbosity=1)
71+
72+
# Check that the atom list contains the correct number of atoms for each element
73+
assert atom_list[5] == 2
74+
assert atom_list[6] == 3
75+
assert atom_list[7] == 1
76+
77+
78+
# Test the element composition property of the GenerateConfig class with an int key composition dict
79+
def test_generate_config_element_composition_dict_string(default_generate_config):
80+
"""Test the element composition property of the GenerateConfig class with a str key composition dict."""
81+
82+
default_generate_config.min_num_atoms = 10
83+
default_generate_config.max_num_atoms = 15
84+
default_generate_config.element_composition = {
85+
"C": (2, 2),
86+
"N": (3, 3),
87+
"O": (1, 1),
88+
}
89+
atom_list = generate_atom_list(default_generate_config, verbosity=1)
90+
91+
# Check that the atom list contains the correct number of atoms for each element
92+
assert atom_list[5] == 2
93+
assert atom_list[6] == 3
94+
assert atom_list[7] == 1
95+
96+
97+
# Test the element composition property of the GenerateConfig class with an int key composition dict
98+
def test_generate_config_element_composition_dict_mixed(default_generate_config):
99+
"""Test the element composition property of the GenerateConfig class with a str key composition dict."""
100+
101+
default_generate_config.min_num_atoms = 10
102+
default_generate_config.max_num_atoms = 15
103+
default_generate_config.element_composition = {5: (2, 2), "N": (3, 3), "O": (1, 1)}
104+
atom_list = generate_atom_list(default_generate_config, verbosity=1)
105+
106+
# Check that the atom list contains the correct number of atoms for each element
107+
assert atom_list[5] == 2
108+
assert atom_list[6] == 3
109+
assert atom_list[7] == 1
110+
111+
58112
# Test the forbidden_elements property of the GenerateConfig class
59113
def test_generate_config_forbidden_elements(default_generate_config):
60114
"""Test the forbidden_elements property of the GenerateConfig class."""

0 commit comments

Comments
 (0)