diff --git a/src/chembalancer/chembalancer.py b/src/chembalancer/chembalancer.py index f330088..e8b5270 100644 --- a/src/chembalancer/chembalancer.py +++ b/src/chembalancer/chembalancer.py @@ -16,7 +16,7 @@ from collections import defaultdict from rdkit import Chem import requests - +import base64 def get_smiles_from_name(name): @@ -45,11 +45,11 @@ def count_atoms(smiles): mol = Chem.MolFromSmiles(smiles) atom_counts = defaultdict(int) if mol: + mol = Chem.AddHs(mol) for atom in mol.GetAtoms(): atom_counts[atom.GetSymbol()] += 1 return dict(atom_counts) - def solve_ilp(A): """ Solve the integer linear programming problem to find stoichiometric coefficients. """ num_vars = A.shape[1] @@ -169,35 +169,60 @@ def create_reaction_string(reactants, products): products_str = '.'.join(products) return f"{reactants_str}>>{products_str}" + def display_svg(svg): """Display SVG in Streamlit using markdown with unsafe HTML.""" b64 = base64.b64encode(svg.encode('utf-8')).decode("utf-8") html = f"" - st.markdown(html, unsafe_allow_html=True) + return html + def compound_state(compound, temp): CAS_compound = CAS_from_any(compound) boiling_p = Tb(CAS_compound) melting_p = Tm(CAS_compound) - if temp <= melting_p: + + if float(temp) <= float(melting_p): return 'solid' - elif temp > melting_p and temp <= boiling_p: - return 'liquid' - else: + elif float(temp) >= float(boiling_p): return 'gas' + else: + return 'liquid' def enthalpy(coeff, compound, state): + Cas_compound=CAS_from_any(compound) if state == 'solid': - return coeff * Hfs(CAS_from_any(compound)) + if Hfs(Cas_compound)== None: + return 0 + else: + return float(coeff) * Hfs(Cas_compound) elif state == 'liquid': - return coeff * Hfl(CAS_from_any(compound)) + if Hfl(CAS_from_any(compound))== None: + return 0 + else: + return float(coeff) * Hfl(Cas_compound) else: - return coeff * Hfg(CAS_from_any(compound)) - + if Hfg(CAS_from_any(compound))== None: + return 0 + else: + return float(coeff) * Hfg(Cas_compound) + def entropy(coeff, compound, state): + Cas_compound=CAS_from_any(compound) if state == 'solid': - return coeff * S0s(CAS_from_any(compound)) + if S0s(Cas_compound)== None: + return 0 + else: + return float(coeff) * S0s(Cas_compound) elif state == 'liquid': - return coeff * S0l(CAS_from_any(compound)) + if S0l(CAS_from_any(compound))== None: + return 0 + else: + return float(coeff) * S0l(Cas_compound) else: - return coeff * S0g(CAS_from_any(compound)) + if S0g(CAS_from_any(compound))== None: + return 0 + else: + return float(coeff) * S0g(Cas_compound) + + diff --git a/tests/test_compound_state.py b/tests/test_compound_state.py new file mode 100644 index 0000000..d66a9cd --- /dev/null +++ b/tests/test_compound_state.py @@ -0,0 +1,17 @@ +import pytest +from chemicals import CAS_from_any +from chembalancer.chembalancer import compound_state + +@pytest.mark.parametrize( + "compound, temp_kelvin, expected_state", + [ + ("water", 298.15, "liquid"), + ("water", 383.15, "gas"), + ("ethanol", 298.15, "liquid"), + ("ethanol", 351.15, "liquid"), + ("iron", 298.15, "solid"), + ("oxygen", 73.15, "liquid"), + ] +) +def test_compound_state(compound, temp_kelvin, expected_state): + assert compound_state(compound, temp_kelvin) == expected_state diff --git a/tests/test_create_reaction_string.py b/tests/test_create_reaction_string.py new file mode 100644 index 0000000..cf2d111 --- /dev/null +++ b/tests/test_create_reaction_string.py @@ -0,0 +1,47 @@ +from chembalancer.chembalancer import create_reaction_string + + +def test_create_reaction_string(): + # Test case 1: Reaction with simple reactants and products + reactants_1 = ['CH4', 'O2'] + products_1 = ['CO2', 'H2O'] + expected_output_1 = "CH4.O2>>CO2.H2O" + assert create_reaction_string(reactants_1, products_1) == expected_output_1 + + # Test case 2: Reaction with more complex reactants and products + reactants_2 = ['C2H6', 'O2'] + products_2 = ['CO2', 'H2O', 'C2H5OH'] + expected_output_2 = "C2H6.O2>>CO2.H2O.C2H5OH" + assert create_reaction_string(reactants_2, products_2) == expected_output_2 + + # Test case 3: Reaction with a single reactant and a single product + reactants_3 = ['H2'] + products_3 = ['H2O'] + expected_output_3 = "H2>>H2O" + assert create_reaction_string(reactants_3, products_3) == expected_output_3 + + # Test case 4: Reaction with an empty list of reactants + reactants_4 = [] + products_4 = ['H2O'] + expected_output_4 = ">>H2O" + assert create_reaction_string(reactants_4, products_4) == expected_output_4 + + # Test case 5: Reaction with an empty list of products + reactants_5 = ['H2'] + products_5 = [] + expected_output_5 = "H2>>" + assert create_reaction_string(reactants_5, products_5) == expected_output_5 + + # Test case 6: Reaction with reactants and products as empty lists + reactants_6 = [] + products_6 = [] + expected_output_6 = ">>" + assert create_reaction_string(reactants_6, products_6) == expected_output_6 + + # Test case 7: Reaction with invalid input (integers instead of strings) + reactants_7 = [1, 2] + products_7 = [3, 4] + try: + create_reaction_string(reactants_7, products_7) + except TypeError as e: + assert str(e) == "sequence item 0: expected str instance, int found" diff --git a/tests/test_display_svg.py b/tests/test_display_svg.py new file mode 100644 index 0000000..44cd771 --- /dev/null +++ b/tests/test_display_svg.py @@ -0,0 +1,32 @@ +# tests/test_display_svg.py + +from chembalancer.chembalancer import display_svg +import base64 + +def test_display_svg(): + # Test case 1: Display SVG with a simple circle + svg_content_1 = """ + + + + """ + expected_html_1 = f'' + assert display_svg(svg_content_1) == expected_html_1 + + # Test case 2: Display SVG with a square + svg_content_2 = """ + + + + """ + expected_html_2 = f'' + assert display_svg(svg_content_2) == expected_html_2 + + # Test case 3: Display SVG with a line + svg_content_3 = """ + + + + """ + expected_html_3 = f'' + assert display_svg(svg_content_3) == expected_html_3 diff --git a/tests/test_enthalpy.py b/tests/test_enthalpy.py new file mode 100644 index 0000000..992f656 --- /dev/null +++ b/tests/test_enthalpy.py @@ -0,0 +1,34 @@ +import pytest +from unittest.mock import patch +from chembalancer.chembalancer import enthalpy +from chemicals import CAS_from_any, Tb, Tm, Tc, Hfs, Hfl, Hfg, S0s, S0l, S0g + +@pytest.mark.parametrize( + "coeff, compound, state, expected_enthalpy", + [ + (1.0, "water", "solid", 0.0), + (2.0, "water", "liquid", -571650.0), + (3.0, "water", "gas", -725466.0), + (1.0, "ethanol", "solid", 0.0), + (2.0, "ethanol", "liquid", -554060), + (3.0, "ethanol", "gas", -703710.0), + (1.0, "iron", "solid", 0.0), + (2.0, "iron", "liquid", 24800.0), + (3.0, "iron", "gas", 1248900.0), + ] +) +def test_enthalpy(coeff, compound, state, expected_enthalpy): + with patch("chemicals.CAS_from_any") as mock_CAS_from_any: + mock_CAS_from_any.return_value = "dummy_cas" + + with patch("chemicals.Hfs") as mock_Hfs, \ + patch("chemicals.Hfl") as mock_Hfl, \ + patch("chemicals.Hfg") as mock_Hfg: + + mock_Hfs.return_value = 0.0 + mock_Hfl.return_value = 333.55 + mock_Hfg.return_value = 891.8 + + result = enthalpy(coeff, compound, state) + + assert result == pytest.approx(expected_enthalpy, rel=1e-2) diff --git a/tests/test_entropy.py b/tests/test_entropy.py new file mode 100644 index 0000000..92f2665 --- /dev/null +++ b/tests/test_entropy.py @@ -0,0 +1,44 @@ +import pytest +from unittest.mock import patch +from chemicals import CAS_from_any, S0s, S0l, S0g +from chembalancer.chembalancer import entropy + +@pytest.mark.parametrize( + "coeff, compound, state, expected_entropy", + [ + (1.0, "water", "solid", 0.0), + (2.0, "water", "liquid", 141), + (3.0, "water", "gas", 566.4), + (1.0, "ethanol", "solid", 0.0), + (2.0, "ethanol", "liquid", 321), + (3.0, "ethanol", "gas", 845), + (1.0, "iron", "solid", 27.2), + (2.0, "iron", "liquid", 69), + (3.0, "iron", "gas", 540), + ] +) + +def test_entropy(coeff, compound, state, expected_entropy): + with patch("chemicals.CAS_from_any") as mock_CAS_from_any: + mock_CAS_from_any.return_value = "dummy_cas" + + with patch("chemicals.S0s") as mock_S0s, \ + patch("chemicals.S0l") as mock_S0l, \ + patch("chemicals.S0g") as mock_S0g: + + if state == "solid": + mock_S0s.return_value = expected_entropy + mock_S0l.return_value = None + mock_S0g.return_value = None + elif state == "liquid": + mock_S0s.return_value = None + mock_S0l.return_value = expected_entropy + mock_S0g.return_value = None + else: + mock_S0s.return_value = None + mock_S0l.return_value = None + mock_S0g.return_value = expected_entropy + + result = entropy(coeff, compound, state) + + assert result == pytest.approx(expected_entropy, rel=1e-2)