From e6ed11430e76f8964652b07b30f4a36bcd9eeb2f Mon Sep 17 00:00:00 2001 From: Sarthak Kapoor <57119427+ka-sarthak@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:53:46 +0100 Subject: [PATCH] 125 molar amount missing in solution (#127) * 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 --- docs/explanation/schemas.md | 40 ++++++++---- .../solution/general.py | 64 +++++++++++++------ tests/solution/test_solution_schema.py | 25 ++++++++ 3 files changed, 96 insertions(+), 33 deletions(-) diff --git a/docs/explanation/schemas.md b/docs/explanation/schemas.md index 607730dd..d67e8add 100644 --- a/docs/explanation/schemas.md +++ b/docs/explanation/schemas.md @@ -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_. @@ -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`. @@ -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. @@ -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. diff --git a/src/nomad_material_processing/solution/general.py b/src/nomad_material_processing/solution/general.py index ad19257e..a6e873f8 100644 --- a/src/nomad_material_processing/solution/general.py +++ b/src/nomad_material_processing/solution/general.py @@ -75,10 +75,9 @@ 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', ), @@ -86,10 +85,9 @@ class MolarConcentration(ArchiveSection): ) 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', @@ -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( @@ -211,7 +208,11 @@ 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', @@ -219,15 +220,29 @@ class SolutionComponent(PureSubstanceComponent, BaseSolutionComponent): ), 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) @@ -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. @@ -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: """ @@ -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) @@ -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.', @@ -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: @@ -517,6 +539,8 @@ 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)) @@ -524,6 +548,8 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: # 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) diff --git a/tests/solution/test_solution_schema.py b/tests/solution/test_solution_schema.py index 4f557274..b92b9385 100644 --- a/tests/solution/test_solution_schema.py +++ b/tests/solution/test_solution_schema.py @@ -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') @@ -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[ @@ -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[ @@ -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[