Skip to content

Commit

Permalink
125 molar amount missing in solution (#127)
Browse files Browse the repository at this point in the history
* Add amount_of_substance; normalization

* Update descriptions

* Update docs

* Fix normalization

* Add tests for amount of substance

* Remove todo

* Use g/ml as units for density
  • Loading branch information
ka-sarthak authored Dec 19, 2024
1 parent d8efe78 commit e6ed114
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 33 deletions.
40 changes: 26 additions & 14 deletions docs/explanation/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,21 @@ The `nomad_material_processing.vapor_deposition.pvd.mbe` module uses the thermal
source and also adds a plasma source.


### nomad_material_processing.solution.general
### Solutions

The main entry sections in this module are
`nomad_material_processing.solution.general` provides
[`Solution`](#nomad_material_processing.solution.general.Solution) and
[`SolutionPreparation`](#nomad_material_processing.solution.general.SolutionPreparation) which can
be used to create NOMAD
[`SolutionPreparation`](#nomad_material_processing.solution.general.SolutionPreparation)
entry sections which can be used to create NOMAD
[entries](https://nomad-lab.eu/prod/v1/docs/reference/glossary.html#entry).
There's a long list of other auxiliary sections supporting these entry section which
It also contains other auxiliary sections supporting these entry section which
can be accessed in the
[metainfo browser](https://nomad-lab.eu/prod/v1/oasis/gui/analyze/metainfo/nomad_material_processing)
by searching for: `"nomad_material_processing.solution.general"`

#### `nomad_material_processing.solution.general.Solution`
#### Solution

Describes liquid solutions by extending the
`nomad_material_processing.solution.general.Solution` describes liquid solutions by extending the
[`CompositeSystem`](https://nomad-lab.eu/prod/v1/docs/howto/customization/base_sections.html#system) with quantities: _pH_, _mass_,
_calculated_volume_, _measured_volume_, _density_, and sub-sections:
_solvents_, _solutes_, and _solution_storage_.
Expand Down Expand Up @@ -172,14 +172,24 @@ _calculated_volume_ of the solution. The component can either nest a
sub-section describing its composition, or can be another `Solution` entry connected
via reference.
These options are are handled by
`SolutionComponent` and `SolutionComponentReference` sections respectively. Let's take a closer look at each of them.
`SolutionComponent` and `SolutionComponentReference` sections respectively.

`SolutionComponent` extends `PureSubstanceComponent` with quantities:
_component_role_, _mass_, _volume_, _density_, and sub-section: _molar_concentration_.
Let's take a closer look at each of them.

__`SolutionComponent`__ extends `PureSubstanceComponent` with quantities:
_component_role_, _mass_, _volume_, _density_, _amount_of_substance_ (in moles),
and sub-section: _molar_concentration_.
The _pure_substance_ sub-section inherited from `PureSubstanceComponent` specifies the
chemical compound. This information along with the mass of the component and
chemical compound. This information along with the amount of the component and
total volume of the solution is used to automatically determine the molar concentration of
the component, populating the corresponding sub-section.

If not provided, _amount_of_substance_ can be determined from _mass_ and
_pure_substance.molecular_mass_.
On other hand, if _amount_of_substance_ is available, but _mass_ is missing, it can be
determined using _amount_of_substance_ and _pure_substance.molecular_mass_.
_mass_ can also be determined if _volume_ and _density_ are available.

Based on the _component_role_, the components are copied over to either
`Solution.solvents` or `Solution.solutes`.

Expand All @@ -189,10 +199,11 @@ class SolutionComponent(PureSubstanceComponent):
mass: float
volume: float
density: float
amount_of_substance: float
molar_concentration: MolarConcentration
```

`SolutionComponentReference` makes a reference to another `Solution` entry and specifies
__`SolutionComponentReference`__ makes a reference to another `Solution` entry and specifies
the amount used. Based on this, _solutes_ and _solvents_ of the referenced solution are
copied over to the first solution. Their mass and volume are adjusted based on the
amount of the referenced solution used.
Expand All @@ -212,9 +223,10 @@ combined into one.
The _solution_storage_ uses `SolutionStorage` section to describe storage conditions
, i.e., temperature and atmosphere, along with preparation and expiry dates.

#### `nomad_material_processing.solution.general.SolutionPreparation`
#### SolutionPreparation

Describes the steps of solution preparation by extending
`nomad_material_processing.solution.general.SolutionPreparation`
describes the steps of solution preparation by extending
[`Process`](https://nomad-lab.eu/prod/v1/docs/howto/customization/base_sections.html#process).
Based on the steps added, it also creates a `Solution` entry and references it under the
_solution_ sub-section.
Expand Down
64 changes: 45 additions & 19 deletions src/nomad_material_processing/solution/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,19 @@ class MolarConcentration(ArchiveSection):
)
calculated_concentration = Quantity(
type=float,
description=(
'The expected concentration calculated from the component moles and '
'total volume.'
),
description="""
The expected concentration calculated from the component moles and total volume.
""",
a_eln=ELNAnnotation(
defaultDisplayUnit='mol / liter',
),
unit='mol / liter',
)
measured_concentration = Quantity(
type=float,
description=(
"""The concentration observed or measured
with some characterization technique."""
),
description="""
The concentration observed or measured with some characterization technique.
""",
a_eln=ELNAnnotation(
component='NumberEditQuantity',
defaultDisplayUnit='mol / liter',
Expand Down Expand Up @@ -170,7 +168,6 @@ class SolutionComponent(PureSubstanceComponent, BaseSolutionComponent):
Section for a component added to the solution.
"""

# TODO get the density of the component automatically if not provided
m_def = Section(
description='A component added to the solution.',
a_eln=ELNAnnotation(
Expand Down Expand Up @@ -211,23 +208,41 @@ class SolutionComponent(PureSubstanceComponent, BaseSolutionComponent):
)
mass = Quantity(
type=float,
description='The mass of the component without the container.',
description="""
The mass of the component without the container. Can be calculated automatically
if `volume` and `density` are available or if `amount_of_substance`
and `pure_substance.molecular_mass` are available.
""",
a_eln=ELNAnnotation(
component='NumberEditQuantity',
defaultDisplayUnit='gram',
minValue=0,
),
unit='kilogram',
)
amount_of_substance = Quantity(
link='https://doi.org/10.1351/goldbook.A00297',
type=float,
description="""
The number of elementary entities of the given substance. Can be calculated
automatically if `mass` and `pure_substance.molecular_mass` are available.
""",
a_eln=ELNAnnotation(
component='NumberEditQuantity',
defaultDisplayUnit='mole',
minValue=0,
),
unit='mole',
)
density = Quantity(
type=float,
description='The density of the liquid component.',
a_eln=ELNAnnotation(
component='NumberEditQuantity',
defaultDisplayUnit='gram / liter',
defaultDisplayUnit='gram / milliliter',
minValue=0,
),
unit='kilogram / liter',
unit='gram / milliliter',
)
molar_concentration = SubSection(section_def=MolarConcentration)
pure_substance = SubSection(section_def=PubChemPureSubstanceSection)
Expand Down Expand Up @@ -267,8 +282,8 @@ def calculate_molar_concentration(
self, volume: Quantity, logger: 'BoundLogger' = None
) -> None:
"""
Calculate the molar concentration of the component
in a given volume of solution.
Calculate the molar concentration of the component in a given volume of
solution.
Args:
volume (Quantity): The volume of the solution.
Expand All @@ -283,9 +298,10 @@ def calculate_molar_concentration(
return
if not self.molar_concentration:
self.molar_concentration = MolarConcentration()
moles = self._calculate_moles(logger)
if moles:
self.molar_concentration.calculated_concentration = moles / volume
if self.amount_of_substance:
self.molar_concentration.calculated_concentration = (
self.amount_of_substance / volume
)

def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None:
"""
Expand All @@ -301,6 +317,12 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None:
self.pure_substance.normalize(archive, logger)
if self.volume and self.density:
self.mass = self.volume * self.density
if self.mass and self.pure_substance and not self.amount_of_substance:
self.amount_of_substance = self._calculate_moles(logger)
if self.amount_of_substance and self.pure_substance and not self.mass:
self.mass = self.amount_of_substance * (
self.pure_substance.molecular_mass * ureg.N_A
)
super().normalize(archive, logger)


Expand Down Expand Up @@ -348,7 +370,7 @@ class Solution(CompositeSystem, EntryData):
a_eln=ELNAnnotation(
defaultDisplayUnit='gram / milliliter',
),
unit='kilogram / liter',
unit='gram / milliliter',
)
mass = Quantity(
description='The mass of the solution.',
Expand Down Expand Up @@ -426,7 +448,7 @@ def combine_components(component_list, logger: 'BoundLogger' = None) -> None:
continue
comparison_key = component.pure_substance.pub_chem_cid
if comparison_key in combined_components:
for prop in ['mass', 'volume']:
for prop in ['mass', 'volume', 'amount_of_substance']:
val1 = getattr(combined_components[comparison_key], prop, None)
val2 = getattr(component, prop, None)
if val1 and val2:
Expand Down Expand Up @@ -517,13 +539,17 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: #
self.solvents[-1].volume *= scaler
if self.solvents[-1].mass:
self.solvents[-1].mass *= scaler
if self.solvents[-1].amount_of_substance:
self.solvents[-1].amount_of_substance *= scaler
if component.system.solutes:
for solute in component.system.solutes:
self.solutes.append(solute.m_copy(deep=True))
if self.solutes[-1].volume:
self.solutes[-1].volume *= scaler
if self.solutes[-1].mass:
self.solutes[-1].mass *= scaler
if self.solutes[-1].amount_of_substance:
self.solutes[-1].amount_of_substance *= scaler

self.solvents = self.combine_components(self.solvents, logger)
self.solutes = self.combine_components(self.solutes, logger)
Expand Down
25 changes: 25 additions & 0 deletions tests/solution/test_solution_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,16 @@ def test_solution():
starter_water_concentration = starter_solution_archive.data.solvents[
0
].molar_concentration.calculated_concentration
starter_water_amount = starter_solution_archive.data.solvents[0].amount_of_substance
starter_salt_concentration = starter_solution_archive.data.solutes[
0
].molar_concentration.calculated_concentration
starter_salt_mass = starter_solution_archive.data.solutes[0].mass

assert pytest.approx(starter_water_concentration, 1e-3) == 55.523 * ureg('mol/l')
assert pytest.approx(starter_water_amount, 1e-3) == 0.5 * ureg('l') * 55.523 * ureg(
'mol/l'
)
assert pytest.approx(starter_salt_concentration, 1e-3) == 0.345 * ureg('mol/l')
assert pytest.approx(starter_salt_mass, 1e-3) == 0.01 * ureg('kg')

Expand All @@ -90,6 +94,13 @@ def test_solution():
)
== starter_water_concentration
)
assert (
pytest.approx(
main_solution_archive.data.solvents[0].amount_of_substance,
1e-3,
)
== starter_water_amount / 5
)
assert (
pytest.approx(
main_solution_archive.data.solutes[
Expand Down Expand Up @@ -129,6 +140,13 @@ def test_solution():
)
== starter_water_concentration
)
assert (
pytest.approx(
main_solution_archive.data.solvents[0].amount_of_substance,
1e-3,
)
== (starter_water_amount / 5) * 2
)
assert (
pytest.approx(
main_solution_archive.data.solutes[
Expand All @@ -154,6 +172,13 @@ def test_solution():
)
== starter_water_concentration
)
assert (
pytest.approx(
starter_solution_archive.data.solvents[0].amount_of_substance,
1e-3,
)
== starter_water_amount
)
assert (
pytest.approx(
starter_solution_archive.data.solutes[
Expand Down

0 comments on commit e6ed114

Please sign in to comment.