Skip to content

Commit

Permalink
Fixed Recipe.create_solution to accept multiple solutes. Added tests …
Browse files Browse the repository at this point in the history
…for same.
  • Loading branch information
JMarvi3 committed Jul 5, 2024
1 parent ef39dc2 commit f2047cc
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
author = 'Eugene Kwan and James Marvin'

# The full version, including alpha/beta/rc tags
release = '0.4.6'
release = '0.4.7'

# -- General configuration ---------------------------------------------------

Expand Down
60 changes: 45 additions & 15 deletions pyplate/pyplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,10 @@ def convert_one(substance: Substance, u: str) -> float:
concentration = [concentration] * len(solute)
elif not isinstance(concentration, Iterable):
raise TypeError("Concentration(s) must be a str.")

if len(concentration) != n:
raise ValueError("Number of concentrations must match number of solutes.")

bottom_arrays = {}
for i, (c, substance) in enumerate(zip(concentration, solute)):
if not isinstance(c, str):
Expand All @@ -1136,6 +1140,10 @@ def convert_one(substance: Substance, u: str) -> float:
quantity = [quantity] * len(solute)
elif not isinstance(quantity, Iterable):
raise TypeError("Quantity(s) must be a str.")

if len(quantity) != n:
raise ValueError("Number of quantities must match number of solutes.")

for i, (q, substance) in enumerate(zip(quantity, solute)):
if not isinstance(q, str):
raise TypeError("Quantity(s) must be a str.")
Expand Down Expand Up @@ -1979,24 +1987,39 @@ def create_solution(self, solute: Substance | Iterable[Substance], solvent: Subs
A new Container so that it may be used in later recipe steps.
"""

if not isinstance(solute, Substance):
raise TypeError("Solute must be a Substance.")
if not isinstance(solvent, Substance):
raise TypeError("Solvent must be a Substance.")
if name and not isinstance(name, str):
if not isinstance(solvent, (Substance, Container)):
raise TypeError("Solvent must be a Substance or a Container.")
if name is not None and not isinstance(name, str):
raise TypeError("Name must be a str.")

if 'concentration' in kwargs and not isinstance(kwargs['concentration'], str):
raise TypeError("Concentration must be a str.")
if 'quantity' in kwargs and not isinstance(kwargs['quantity'], str):
raise TypeError("Quantity must be a str.")
if not isinstance(solute, Substance):
if not isinstance(solute, Iterable):
raise TypeError("Solute must be a Substance or an iterable of Substances.")
elif any(not isinstance(substance, Substance) for substance in solute):
raise TypeError("Solute must be a Substance or an iterable of Substances.")

if 'concentration' in kwargs:
if not isinstance(kwargs['concentration'], str):
if not isinstance(kwargs['concentration'], Iterable):
raise TypeError("Concentration must be a str or an iterable of strs.")
elif any(not isinstance(concentration, str) for concentration in kwargs['concentration']):
raise TypeError("Concentration must be a str or an iterable of strs.")

if 'quantity' in kwargs:
if not isinstance(kwargs['quantity'], str):
if not isinstance(kwargs['quantity'], Iterable):
raise TypeError("Quantity must be a str or an iterable of strs.")
elif any(not isinstance(quantity, str) for quantity in kwargs['quantity']):
raise TypeError("Quantity must be a str or an iterable of strs.")

if 'total_quantity' in kwargs and not isinstance(kwargs['total_quantity'], str):
raise TypeError("Total quantity must be a str.")
if ('concentration' in kwargs) + ('total_quantity' in kwargs) + ('quantity' in kwargs) != 2:
raise ValueError("Must specify two values out of concentration, quantity, and total quantity.")

if not name:
name = f"solution of {solute.name} in {solvent.name}"
solute_names = ', '.join(substance.name for substance in solute) if isinstance(solute, Iterable) else solute.name
if name is None:
name = f"solution of {solute_names} in {solvent.name}"

new_container = Container(name)
self.uses(new_container)
Expand Down Expand Up @@ -2223,23 +2246,30 @@ def bake(self) -> dict[str, Container | Plate]:
dest_name = dest.name
step.frm.append(None)
solute, solvent, kwargs = step.operands

solute_names = ', '.join([solute.name for solute in solute]) if isinstance(solute, Iterable) else solute.name
# kwargs should have two out of concentration, quantity, and total_quantity
if 'concentration' in kwargs and 'total_quantity' in kwargs:
step.instructions = f"""Create a solution of '{solute.name}' in '{solvent.name
step.instructions = f"""Create a solution of '{solute_names}' in '{solvent.name
}' with a concentration of {kwargs['concentration']
} and a total quantity of {kwargs['total_quantity']}."""
elif 'concentration' in kwargs and 'quantity' in kwargs:
step.instructions = f"""Create a solution of '{solute.name}' in '{solvent.name
step.instructions = f"""Create a solution of '{solute_names}' in '{solvent.name
}' with a concentration of {kwargs['concentration']
} and a quantity of {kwargs['quantity']}."""
elif 'quantity' in kwargs and 'total_quantity' in kwargs:
step.instructions = f"""Create a solution of '{solute.name}' in '{solvent.name
step.instructions = f"""Create a solution of '{solute_names}' in '{solvent.name
}' with a total quantity of {kwargs['total_quantity']
} and a quantity of {kwargs['quantity']}."""

step.to[0] = self.results[dest_name]
self.used.add(dest_name)
self.results[dest_name] = Container.create_solution(solute, solvent, dest_name, **kwargs)
results = Container.create_solution(solute, solvent, dest_name, **kwargs)
if isinstance(solvent, Container):
self.used.add(solvent.name)
self.results[solvent.name], self.results[dest_name] = results
else:
self.results[dest_name] = results
step.substances_used = self.results[dest_name].get_substances()
step.to.append(self.results[dest_name])
elif operator == 'solution_from':
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pyplate-hte"
version = "0.4.6"
version = "0.4.7"
description = "A Python tool for designing chemistry experiments in plate format"
readme = "README.md"
license = {file = "LICENSE.txt"}
Expand Down
2 changes: 0 additions & 2 deletions tests/test_Container.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,6 @@ def test_create_solution(water, salt, sodium_sulfate):
assert (pytest.approx(Unit.convert_from(water, 100, 'mL', config.volume_storage_unit)) ==
qaunt_total_quant_solution.volume)


# TODO: Update with actual error
with pytest.raises(ValueError, match="Solution is impossible to create."):
# create solution with solute in solvent container
invalid_container_solution = Container.create_solution([salt, sodium_sulfate], invalid_solvent_container,
Expand Down
101 changes: 101 additions & 0 deletions tests/test_Recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,104 @@ def test_create_container(water, salt):
container3 = results[container3.name]
assert 10 + Unit.convert(salt, '5 mmol', 'mL') == container3.get_volume(unit='mL')
assert container3.contents.get(salt, None) == Unit.convert_to_storage(5, 'mmol')


def test_create_solution(water, salt, sodium_sulfate):
water_container = Container('water', initial_contents=[(water, '100 mL')])

recipe = Recipe()

# create solution with just one solute
recipe.create_solution(salt, water, concentration='1 M', total_quantity='100 mL', name='simple_solution')

# create solution with multiple solutes
recipe.create_solution(solute=[salt, sodium_sulfate], solvent=water,
concentration=['1 M', '1 M'],
quantity=['5.84428 g', '14.204 g'],
name='conc_quant_solution')
recipe.create_solution([salt, sodium_sulfate], water,
concentration=['1 M', '1 M'],
total_quantity='100 mL', name='conc_total_quant_solution')

recipe.create_solution([salt, sodium_sulfate], water,
quantity=['5.84428 g', '14.204 g'],
total_quantity='100 mL', name='qaunt_total_quant_solution')

results = recipe.bake()
simple_solution = results['simple_solution']
conc_quant_solution = results['conc_quant_solution']
conc_total_quant_solution = results['conc_total_quant_solution']
qaunt_total_quant_solution = results['qaunt_total_quant_solution']

# create solvent container
water_container = Container('water', initial_contents=[(water, '100 mL')])

# create solvent container with solute in it
invalid_solvent_container = Container.create_solution(salt, water, concentration='1 M', total_quantity='100 mL')

## verify simple solution
# verify solute amount
assert (pytest.approx(Unit.convert_from(salt, 100, 'mmol', config.moles_storage_unit)) ==
simple_solution.contents[salt])
# verify concentration
assert simple_solution.get_concentration(salt) == pytest.approx(1)
# verify total volume
assert (pytest.approx(Unit.convert_from(water, 100, 'mL', config.volume_storage_unit)) ==
simple_solution.volume)

## verify conc_quant_solution
# verify solute amounts
assert (pytest.approx(Unit.convert_from(salt, 5.84428, 'g', config.moles_storage_unit)) ==
conc_quant_solution.contents[salt])
assert (pytest.approx(Unit.convert_from(sodium_sulfate, 14.204, 'g', config.moles_storage_unit)) ==
conc_quant_solution.contents[sodium_sulfate])
# verify concentration
assert conc_quant_solution.get_concentration(salt) == pytest.approx(1)
assert conc_quant_solution.get_concentration(sodium_sulfate) == pytest.approx(1)
# verify total volume
assert (pytest.approx(Unit.convert_from(water, 100, 'mL', config.volume_storage_unit)) ==
conc_quant_solution.volume)

## verify conc_total_quant_solution
# verify solute amounts
assert (pytest.approx(Unit.convert_from(salt, 5.84428, 'g', config.moles_storage_unit)) ==
conc_total_quant_solution.contents[salt])
assert (pytest.approx(Unit.convert_from(sodium_sulfate, 14.204, 'g', config.moles_storage_unit)) ==
conc_total_quant_solution.contents[sodium_sulfate])
# verify concentration
assert conc_total_quant_solution.get_concentration(salt) == pytest.approx(1)
assert conc_total_quant_solution.get_concentration(sodium_sulfate) == pytest.approx(1)
# verify total volume
assert (pytest.approx(Unit.convert_from(water, 100, 'mL', config.volume_storage_unit)) ==
conc_total_quant_solution.volume)

## verify qaunt_total_quant_solution
# verify solute amounts
assert (pytest.approx(Unit.convert_from(salt, 5.84428, 'g', config.moles_storage_unit)) ==
qaunt_total_quant_solution.contents[salt])
assert (pytest.approx(Unit.convert_from(sodium_sulfate, 14.204, 'g', config.moles_storage_unit)) ==
qaunt_total_quant_solution.contents[sodium_sulfate])
# verify concentration
assert qaunt_total_quant_solution.get_concentration(salt) == pytest.approx(1)
assert qaunt_total_quant_solution.get_concentration(sodium_sulfate) == pytest.approx(1)
# verify total volume
assert (pytest.approx(Unit.convert_from(water, 100, 'mL', config.volume_storage_unit)) ==
qaunt_total_quant_solution.volume)

with pytest.raises(ValueError, match="Solution is impossible to create."):
# create solution with solute in solvent container
recipe2 = Recipe()
recipe2.uses(invalid_solvent_container)
recipe2.create_solution([salt, sodium_sulfate], invalid_solvent_container,
concentration='1 M', quantity=['1 g', '0.5 g'], name='invalid_container_solution')
results = recipe2.bake()
print(results)

with pytest.raises(ValueError, match="Solution is impossible to create."):
# invalid quantity of solute
recipe2 = Recipe()
recipe2.uses(water_container)
recipe2.create_solution([salt, sodium_sulfate], water_container,
concentration=['1 M', '1 M'],
quantity=['1 g', '0.5 g'], name='container_solution')
recipe2.bake()

0 comments on commit f2047cc

Please sign in to comment.