Skip to content

Commit

Permalink
refact: add __repr_ to Element
Browse files Browse the repository at this point in the history
  • Loading branch information
dvp committed Nov 2, 2024
1 parent 4bdcdcc commit f7c9b45
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 47 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ exclude = [
"__pycache__",
"adhoc",
"docs/source/conf.py",
"extern",
"notebooks",
"wrk",
]
Expand Down
87 changes: 58 additions & 29 deletions src/mckit/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,6 @@ def __init__(
else:
raise ValueError("Incorrect set of parameters.")

self._hash = reduce(xor, map(hash, self._composition.keys()))

def copy(self) -> Composition:
"""Create full copy of self."""
return Composition(atomic=cast(TFractions, self._composition.items()), **self.options)
Expand Down Expand Up @@ -157,9 +155,7 @@ def __eq__(self, other) -> bool:
# return Approx(self, rel_tol=rel_tol, abs_tol=abs_tol)

def __hash__(self) -> int:
return reduce(
xor, map(hash, self._composition.keys())
) # TODO dvp: check why self._hash is not used?
return reduce(xor, map(hash, self._composition.keys()))

def mcnp_words(self, pretty: bool = False) -> list[str]:
words = [f"M{self.name()} "]
Expand Down Expand Up @@ -534,18 +530,10 @@ class Element:
Attributes:
_charge: Z of the element,
_mass_number: A of the element
_comment: Optional comment to the element.
_lib: Data library ID. Usually it is MCNP library, like '31b' for FENDL31b.
_isomer: Isomer level. Default 0. Usually may appear in FISPACT output.
_comment: Optional comment to the element.
_molar: molar mass of the element
Methods:
expand()
Expands natural composition of this element.
fispact_repr()
Gets FISPACT representation of the element.
mcnp_repr()
Gets MCNP representation of the element.
"""

def __init__(
Expand All @@ -555,12 +543,12 @@ def __init__(
Args:
_name: Name of isotope. It can be ZAID = Z * 1000 + A, where Z - charge,
A - the number of protons and neutrons. If A = 0, then natural abundance
is used. Also, it can be an atom_name optionally followed by '-' and A.
'-' can be omitted. If there is no A, then A is assumed to be 0.
comment: Optional comment to the element.
lib: Data library ID. Usually it is MCNP library, like '31b' for FENDL31b.
A - the number of protons and neutrons. If A = 0, then natural abundance
is used. Also, it can be an atom_name optionally followed by '-' and A.
'-' can be omitted. If there is no A, then A is assumed to be 0.
lib: Data library ID. Usually it is MCNP library, like '31c' for FENDL31c.
isomer: Isomer level. Default 0. Usually may appear in FISPACT output.
comment: Optional comment to the element.
"""
if isinstance(_name, int):
self._charge = _name // 1000
Expand Down Expand Up @@ -606,6 +594,17 @@ def __eq__(self, other) -> bool:
and self._isomer == other._isomer
)

def __lt__(self, other: Element) -> bool:
"""Compare Elements by Z, A and isomer level."""
return (
self._charge < other.charge
or self._charge == other.charge
and (
self._mass_number < other.mass_number
or (self._mass_number == other.mass_number and self._isomer < other._isomer)
)
)

def __str__(self) -> str:
_name = _CHARGE_TO_NAME[self.charge].capitalize()
if self._mass_number > 0:
Expand All @@ -616,6 +615,32 @@ def __str__(self) -> str:
_name += str(self._isomer - 1)
return _name

def __repr__(self) -> str:
"""Create str representation for debugging.
Examples:
>>> print(repr(Element("H")))
Element("H")
>>> print(repr(Element("Ta181", isomer=1)))
Element("Ta181", isomer=1)
>>> print(repr(Element("H", lib="31c")))
Element("H", lib="31c")
>>> print(repr(Element("H", lib="31c", comment="Plain hydrogen")))
Element("H", lib="31c", comment="Plain hydrogen")
"""
_buf = 'Element("' + _CHARGE_TO_NAME[self.charge].capitalize()
if self._mass_number > 0:
_buf += str(self._mass_number)
_buf += '"'
if self._isomer > 0:
_buf += f", isomer={self._isomer}"
if self._lib:
_buf += f', lib="{self._lib}"'
if self._comment:
_buf += f', comment="{self._comment}"'
_buf += ")"
return _buf

def mcnp_repr(self) -> str:
"""Gets MCNP representation of the element."""
_name = str(self.charge * 1000 + self.mass_number)
Expand All @@ -636,29 +661,33 @@ def fispact_repr(self) -> str:

@property
def charge(self) -> int:
"""Gets element's charge number."""
"""Gets element's charge number (Z)."""
return self._charge

@property
def mass_number(self):
"""Gets element's mass number."""
def mass_number(self) -> int:
"""Gets element's mass number (A)."""
return self._mass_number

@property
def molar_mass(self):
def molar_mass(self) -> float:
"""Gets element's molar mass."""
return self._molar

@property
def lib(self):
def lib(self) -> str | None:
"""Gets library name."""
return self._lib

@property
def isomer(self):
def isomer(self) -> int:
"""Gets isomer level."""
return self._isomer

@property
def comment(self) -> str | None:
return self._comment

def expand(self) -> dict[Element, float]:
"""Expands natural element into individual isotopes.
Expand All @@ -683,10 +712,10 @@ def _split_name(_name: str) -> tuple[str, str]:
('1', '001')
>>> Element._split_name("H")
('H', '0')
>>> Element._split_name("H002")
('H', '002')
>>> Element._split_name("H-002")
('H', '002')
>>> Element._split_name("H2")
('H', '2')
>>> Element._split_name("H-2")
('H', '2')
"""
if _name.isnumeric():
return _name[:-3], _name[-3:]
Expand Down
44 changes: 26 additions & 18 deletions tests/test_material.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class TestElement:
(4009, {}),
]

elements: Final[list[Element]] = [Element(name, **options) for name, options in cases]

hash_equality: Final = [
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
Expand Down Expand Up @@ -68,13 +70,26 @@ class TestElement:
@pytest.mark.parametrize("arg1", range(len(cases)))
@pytest.mark.parametrize("arg2", range(len(cases)))
def test_hash(self, arg1: int, arg2: int):
name1, options1 = self.cases[arg1]
name2, options2 = self.cases[arg2]
elem1 = Element(name1, **options1)
elem2 = Element(name2, **options2)
elem1 = TestElement.elements[arg1]
elem2 = TestElement.elements[arg2]
test_result = hash(elem1) == hash(elem2)
assert test_result == bool(self.hash_equality[arg1][arg2])

@pytest.mark.parametrize(
"arg1, arg2, expected",
[
(0, 1, False),
(0, 2, True),
(2, 3, True),
(3, 4, False),
],
)
def test_le(self, arg1: int, arg2: int, expected: bool) -> None:
elem1 = TestElement.elements[arg1]
elem2 = TestElement.elements[arg2]
assert (elem1 < elem2) == expected, "Element comparison failed"
assert elem1 == elem2 or (elem2 < elem1) != expected, "Inverted Element comparison failed"

@pytest.mark.parametrize(
"case_no, expected",
enumerate(
Expand Down Expand Up @@ -275,8 +290,7 @@ def test_hash(self, arg1: int, arg2: int):
),
)
def test_creation(self, case_no, expected):
name, options = self.cases[case_no]
elem = Element(name, **options)
elem = TestElement.elements[case_no]
assert elem.charge == expected["charge"]
assert elem.mass_number == expected["mass_number"]
assert elem.lib == expected["lib"]
Expand Down Expand Up @@ -349,8 +363,7 @@ def test_creation(self, case_no, expected):
),
)
def test_expand(self, case_no, expected):
name, options = self.cases[case_no]
elem = Element(name, **options)
elem = TestElement.elements[case_no]
expanded_ans = {
Element(name, **opt): pytest.approx(fraction, rel=1.0e-5)
for name, opt, fraction in expected
Expand Down Expand Up @@ -390,8 +403,7 @@ def test_expand(self, case_no, expected):
),
)
def test_str(self, case_no, expected):
name, options = self.cases[case_no]
elem = Element(name, **options)
elem = TestElement.elements[case_no]
assert expected == str(elem)

@pytest.mark.parametrize(
Expand Down Expand Up @@ -426,8 +438,7 @@ def test_str(self, case_no, expected):
),
)
def test_mcnp_repr(self, case_no, expected):
name, options = self.cases[case_no]
elem = Element(name, **options)
elem = TestElement.elements[case_no]
assert expected == elem.mcnp_repr()

@pytest.mark.parametrize(
Expand Down Expand Up @@ -462,8 +473,7 @@ def test_mcnp_repr(self, case_no, expected):
),
)
def test_fispact_repr(self, case_no, expected):
name, options = self.cases[case_no]
elem = Element(name, **options)
elem = TestElement.elements[case_no]
assert expected == elem.fispact_repr()

equality: Final = [
Expand Down Expand Up @@ -496,10 +506,8 @@ def test_fispact_repr(self, case_no, expected):
@pytest.mark.parametrize("arg1", range(len(cases)))
@pytest.mark.parametrize("arg2", range(len(cases)))
def test_eq(self, arg1: int, arg2: int):
name1, options1 = self.cases[arg1]
name2, options2 = self.cases[arg2]
elem1 = Element(name1, **options1)
elem2 = Element(name2, **options2)
elem1 = TestElement.elements[arg1]
elem2 = TestElement.elements[arg2]
test_result = elem1 == elem2
assert test_result == bool(self.equality[arg1][arg2])

Expand Down

0 comments on commit f7c9b45

Please sign in to comment.