From 9add0c6654f0f8f4e366184877653667c114d3cd Mon Sep 17 00:00:00 2001 From: glaciapag Date: Sun, 8 Jan 2023 14:40:03 +0800 Subject: [PATCH] Separated stoichiometric functions from Compound class and placed in the stoich module --- src/pychemkit/exceptions/handlers.py | 51 +++++++++++++++++++++++ src/pychemkit/foundations/compound.py | 14 ------- src/pychemkit/stoich/converter.py | 36 +++++++++++++++++ test/test_compound.py | 8 ---- test/test_stoich.py | 58 +++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 src/pychemkit/exceptions/handlers.py create mode 100644 src/pychemkit/stoich/converter.py create mode 100644 test/test_stoich.py diff --git a/src/pychemkit/exceptions/handlers.py b/src/pychemkit/exceptions/handlers.py new file mode 100644 index 0000000..3be1a75 --- /dev/null +++ b/src/pychemkit/exceptions/handlers.py @@ -0,0 +1,51 @@ +from typing import List, Union + +from pychemkit.foundations.element import Element +from pychemkit.foundations.compound import Compound +from pychemkit.exceptions.invalids import NotAValidCompoundException, NotAValidElementException +from pychemkit.utils.utils import is_number + + +def entity_handler(entity: Union[str, Element, Compound]) -> Union[str, Element, Compound]: + """ + Handler for string inputs (not an instance of element, or a compound) + :param entity: string input of an element or a compound + :return: if valid, an instance of element of a compound, raises an exception if not + """ + if isinstance(entity, str): + try: + entity = Element(entity) + except NotAValidElementException: + try: + entity = Compound(entity) + except NotAValidCompoundException: + raise ValueError( + f'{entity} is not a valid Element of Compound' + ) + + return (entity, type(entity)) + + +def amount_handler(amount: float) -> float: + """ + Handler for amount inputs + :param amount: float or int input representing valid amounts in grams, moles, etc + :return: raises a TypeError if not a valid amount + """ + if is_number(amount): + return amount + else: + raise TypeError('Enter a valid amount') + + +def selection_handler(input: str, selection: List[str]): + """ + Handler for selections + :param input: input string + :param selection: list of valid selections + :return: raises a ValueError if the selection is not in the list + """ + if input not in selection: + raise ValueError( + f'Invalid input: {input}. Valud options are {selection}' + ) diff --git a/src/pychemkit/foundations/compound.py b/src/pychemkit/foundations/compound.py index 5db1b2d..9f613e6 100644 --- a/src/pychemkit/foundations/compound.py +++ b/src/pychemkit/foundations/compound.py @@ -26,20 +26,6 @@ def composition(self): def molecular_mass(self): return self._get_molecular_mass() - def mass_to_moles(self, mass): - if is_number(mass): - return mass / self._get_molecular_mass() - else: - return 'Enter a valid mass in grams' - - def mass_to_atoms(self, mass): - if is_number(mass): - moles = self._get_moles(mass) - atoms = self.moles_to_atoms(moles) - return atoms - else: - return 'Enter a valid mass in grams' - def moles_to_mass(self, moles): if is_number(moles): return moles * self._get_molecular_mass() diff --git a/src/pychemkit/stoich/converter.py b/src/pychemkit/stoich/converter.py new file mode 100644 index 0000000..dc46580 --- /dev/null +++ b/src/pychemkit/stoich/converter.py @@ -0,0 +1,36 @@ +from typing import Union, List + +import numpy as np +from pychemkit.exceptions.invalids import NotAValidCompoundException, NotAValidElementException +from pychemkit.foundations.element import Element +from pychemkit.foundations.compound import Compound +from pychemkit.foundations.constants import AVOGADRO_NUM +from pychemkit.utils.utils import is_number +from pychemkit.exceptions.handlers import entity_handler, amount_handler, selection_handler + + +class StoichConverter: + + def convert_mass(self, entity: Union[str, Element, Compound], mass: float, to: str) -> float: + """ + Converts mass in grams to mole, given a compound + :param Compound: An instance of a compound + :param mass: mass in grams + """ + + selection_handler(to, ['moles', 'atoms']) + entity, ent_type = entity_handler(entity) + + mass = amount_handler(mass) + + if ent_type == Element: + ent_mass = entity.atomic_mass + elif ent_type == Compound: + ent_mass = entity.molecular_mass + + moles = mass / ent_mass + + if to == 'moles': + return moles + elif to == 'atoms': + return np.round(moles * AVOGADRO_NUM, 1) diff --git a/test/test_compound.py b/test/test_compound.py index 35b2ec8..00d2871 100644 --- a/test/test_compound.py +++ b/test/test_compound.py @@ -53,14 +53,6 @@ def test_invalid_compounds(self): self.assertRaises(NotAValidCompoundException, Compound, 'HGA') self.assertRaises(NotAValidCompoundException, Compound, 'AL') - def test_mass_to_moles(self): - mass_test_grams = [128, 150, 400, 0.4, 10.9] - expected_moles = [x / z for x, z in zip(mass_test_grams, self.mmass)] - - for index, tc in enumerate(self.test_compounds): - moles = tc.mass_to_moles(mass_test_grams[index]) - self.assertAlmostEqual(moles, expected_moles[index], 3) - def test_get_element_percentage(self): mass_c = [24.02, 36.03, 72.06, 144.12, 12.01] diff --git a/test/test_stoich.py b/test/test_stoich.py new file mode 100644 index 0000000..ecaa423 --- /dev/null +++ b/test/test_stoich.py @@ -0,0 +1,58 @@ +import unittest + +from pychemkit.foundations.element import Element +from pychemkit.foundations.compound import Compound +from pychemkit.foundations.constants import AVOGADRO_NUM +from pychemkit.stoich.converter import StoichConverter + + +class TestStoich(unittest.TestCase): + + def setUp(self) -> None: + + el1 = Element('C') + el2 = Element('Hg') + el3 = 'Po' + + cpd1 = Compound('CH3COOH') + cpd2 = Compound('NaCl') + cpd3 = 'MgSO4' + + self.test_elements = [el1, el2, el3] + self.test_compounds = [cpd1, cpd2, cpd3] + + self.test_elem_masses = [12.01, 200.59, 208.98243] + self.test_cpd_masses = [60.025, 58.44, 120.366] + + self.converter = StoichConverter() + + def test_mass_to_moles(self): + + convert_to_list = ['moles'] + + expected_moles = { + 'moles': [1, 1, 1], + 'atoms': [AVOGADRO_NUM] * 3 + } + + for to in convert_to_list: + for i, (el, cpd) in enumerate(zip(self.test_elements, self.test_compounds)): + expected = expected_moles.get(to)[i] + actual_el = self.converter.convert_mass( + entity=el, + mass=self.test_elem_masses[i], + to=to + ) + + actual_cpd = self.converter.convert_mass( + entity=cpd, + mass=self.test_cpd_masses[i], + to=to + ) + + self.assertAlmostEqual(actual_el, expected, 1) + self.assertAlmostEqual(actual_cpd, expected, 1) + + +if __name__ == '__main__': + unittest.main()