diff --git a/pyproject.toml b/pyproject.toml index 78baa298..ed20dbdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -383,6 +383,7 @@ exclude = [ "__pycache__", "adhoc", "docs/source/conf.py", + "extern", "notebooks", "wrk", ] diff --git a/src/mckit/material.py b/src/mckit/material.py index 706bfcec..463d74fb 100644 --- a/src/mckit/material.py +++ b/src/mckit/material.py @@ -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) @@ -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()} "] @@ -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__( @@ -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 @@ -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: @@ -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) @@ -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. @@ -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:] diff --git a/tests/test_material.py b/tests/test_material.py index ba304f79..82f747ff 100644 --- a/tests/test_material.py +++ b/tests/test_material.py @@ -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], @@ -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( @@ -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"] @@ -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 @@ -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( @@ -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( @@ -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 = [ @@ -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])