From 4a708c09617277b727352231abbb1b5766d94759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Wed, 25 Sep 2024 17:03:31 +0200 Subject: [PATCH 01/22] Added first version of PerovskiteComposition schemas --- pyproject.toml | 3 +- schema.archive.yaml | 171 ++++++++++++ schema.py | 197 +++++++++++++ .../composition.py | 261 ++++++++++++++++++ 4 files changed, 631 insertions(+), 1 deletion(-) create mode 100644 schema.archive.yaml create mode 100644 schema.py create mode 100644 src/perovskite_solar_cell_database/composition.py diff --git a/pyproject.toml b/pyproject.toml index 5ac88f3..a640176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,8 @@ dependencies = [ "nomad-schema-plugin-simulation-workflow>=1.0.1", "rdkit", "openpyxl", - "lxml_html_clean" + "lxml_html_clean", + "nomad-lab>=1.3.4", ] license = { file = "LICENSE" } diff --git a/schema.archive.yaml b/schema.archive.yaml new file mode 100644 index 0000000..43ac78b --- /dev/null +++ b/schema.archive.yaml @@ -0,0 +1,171 @@ +definitions: + name: 'Perovskite composition' + sections: + perovskite_composition: + quantities: + # To be generated in the back end + short_form: + type: str + + long_form: + type: str + + # Parameters to be given by the user + composition_estimate: + type: + type_kind: Enum + type_data: + - 'Estimated from precursor solutions' + - 'Literature value' + - 'Estimated from XRD data' + - 'Estimated from spectroscopic data' + - 'Theoretical simulation' + - 'Hypothetical compound' + - 'Other' + m_annotations: + eln: + component: EnumEditQuantity + + sample_type: + type: + type_kind: Enum + type_data: + - 'Polycrystalline film' + - 'Single crystal' + - 'Quantum dots' + - 'Nano rods' + - 'Colloidal solution' + - 'Amorphous' + - 'Other' + m_annotations: + eln: + component: EnumEditQuantity + + dimensionality: + type: + type_kind: Enum + type_data: + - '0D' + - '1D' + - '2D' + - '2D/3D' + - '3D' + - 'Other' + description: The dimensionality of the perovskite, i.e. 3D, 2D, 1D (nanorods), quantum dots (0D), etc. + m_annotations: + eln: + component: EnumEditQuantity + + band_gap: + type: float + unit: eV + shape: [] + description: Band gap of photoabsorber in eV. + m_annotations: + eln: + component: NumberEditQuantity + defaultDisplayUnit: eV + + # Sub sections + sub_sections: + a_ions: + section: '#/Ions' + repeats: true + + b_ions: + section: '#/Ions' + repeats: true + + c_ions: + section: '#/Ions' + repeats: true + + secondary_phases_impurities_and_dopants: + section: '#/Materials_in_layer' + repeats: true + Ions: + quantities: + abbreviation: + type: str + shape: [] + description: The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically + m_annotations: + eln: + component: StringEditQuantity + + coefficient: + type: float + shape: [] + description: The stoichiometric coefficient + m_annotations: + eln: + component: NumberEditQuantity + + + # Here we could have one boolean filed asking if teh remainder of the ion data should be read for online source + + # Here we could give a text field asking for teh source of the ion data, but with a predetermined default answer + + common_name: + type: str + shape: [] + description: The common trade name of the ion + m_annotations: + eln: + component: StringEditQuantity + + molecular_formula: + type: str + shape: [] + description: The molecular formula + m_annotations: + eln: + component: StringEditQuantity + + smile: + type: str + shape: [] + description: The canonical SMILE string + m_annotations: + eln: + component: StringEditQuantity + + iupac_name: + type: str + shape: [] + description: The standard IUPAC name + m_annotations: + eln: + component: StringEditQuantity + + cas_number: + type: str + shape: [] + description: The CAS number if available + m_annotations: + eln: + component: StringEditQuantity + + source_compound_smile: + type: str + shape: [] + description: The canonical SMILE string + m_annotations: + eln: + component: StringEditQuantity + + source_compound_iupac_name: + type: str + shape: [] + description: The standard IUPAC name + m_annotations: + eln: + component: StringEditQuantity + + source_compound_cas_number: + type: str + shape: [] + description: The CAS number if available + m_annotations: + eln: + component: StringEditQuantity \ No newline at end of file diff --git a/schema.py b/schema.py new file mode 100644 index 0000000..e4b281e --- /dev/null +++ b/schema.py @@ -0,0 +1,197 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from typing import ( + TYPE_CHECKING, +) +from nomad.metainfo import ( + Package, + Quantity, + SubSection, + MEnum, + Section, +) +from nomad.datamodel.data import ( + ArchiveSection, +) +if TYPE_CHECKING: + pass + +m_package = Package(name='Perovskite composition') + + +class perovskite_composition(ArchiveSection): + ''' + Class autogenerated from yaml schema. + ''' + m_def = Section() + short_form = Quantity( + type=str, + ) + long_form = Quantity( + type=str, + ) + composition_estimate = Quantity( + type=MEnum( + [ + 'Estimated from precursor solutions', + 'Literature value', + 'Estimated from XRD data', + 'Estimated from spectroscopic data', + 'Theoretical simulation', + 'Hypothetical compound', + 'Other']), + a_eln={ + "component": "EnumEditQuantity"}, + ) + sample_type = Quantity( + type=MEnum( + [ + 'Polycrystalline film', + 'Single crystal', + 'Quantum dots', + 'Nano rods', + 'Colloidal solution', + 'Amorphous', + 'Other']), + a_eln={ + "component": "EnumEditQuantity"}, + ) + dimensionality = Quantity( + type=MEnum(['0D', '1D', '2D', '2D/3D', '3D', 'Other']), + description='The dimensionality of the perovskite, i.e. 3D, 2D, 1D (nanorods), quantum dots (0D), etc.', + a_eln={ + "component": "EnumEditQuantity" + }, + ) + band_gap = Quantity( + type=float, + description='Band gap of photoabsorber in eV.', + a_eln={ + "component": "NumberEditQuantity", + "defaultDisplayUnit": "eV" + }, + unit="eV", + shape=[], + ) + a_ions = SubSection( + section_def=Ions, + repeats=True, + ) + b_ions = SubSection( + section_def=Ions, + repeats=True, + ) + c_ions = SubSection( + section_def=Ions, + repeats=True, + ) + secondary_phases_impurities_and_dopants = SubSection( + section_def=Materials_in_layer, + repeats=True, + ) + + +class Ions(ArchiveSection): + ''' + Class autogenerated from yaml schema. + ''' + m_def = Section() + abbreviation = Quantity( + type=str, + description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + coefficient = Quantity( + type=float, + description='The stoichiometric coefficient', + a_eln={ + "component": "NumberEditQuantity" + }, + shape=[], + ) + common_name = Quantity( + type=str, + description='The common trade name of the ion', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + molecular_formula = Quantity( + type=str, + description='The molecular formula', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + smile = Quantity( + type=str, + description='The canonical SMILE string', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + iupac_name = Quantity( + type=str, + description='The standard IUPAC name', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + cas_number = Quantity( + type=str, + description='The CAS number if available', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + source_compound_smile = Quantity( + type=str, + description='The canonical SMILE string', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + source_compound_iupac_name = Quantity( + type=str, + description='The standard IUPAC name', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + source_compound_cas_number = Quantity( + type=str, + description='The CAS number if available', + a_eln={ + "component": "StringEditQuantity" + }, + shape=[], + ) + + +m_package.__init_metainfo__() diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py new file mode 100644 index 0000000..be1e728 --- /dev/null +++ b/src/perovskite_solar_cell_database/composition.py @@ -0,0 +1,261 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from typing import TYPE_CHECKING + +from nomad.datamodel.data import EntryData, EntryDataCategory +from nomad.datamodel.datamodel import EntryArchive +from nomad.datamodel.metainfo.annotations import ELNAnnotation, ELNComponentEnum +from nomad.datamodel.metainfo.basesections import ( + CompositeSystem, + PubChemPureSubstanceSection, + PureSubstance, + SystemComponent, + Component, +) +from nomad.metainfo.metainfo import ( + Category, + MEnum, + Package, + Quantity, + Reference, + Section, + SubSection, +) +from nomad.units import ureg +from structlog.stdlib import BoundLogger + +if TYPE_CHECKING: + from nomad.datamodel.datamodel import EntryArchive + from structlog.stdlib import BoundLogger + +m_package = Package() + + +class PerovskiteCompositionCategory(EntryDataCategory): + m_def = Category(label='Perovskite Composition', categories=[EntryDataCategory]) + + +class PerovskiteIon(PureSubstance, EntryData): + """ + Class autogenerated from yaml schema. + """ + + m_def = Section() + abbreviation = Quantity( + type=str, + description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + pure_substance = SubSection( + section_def=PubChemPureSubstanceSection, + description=""" + Section with properties describing the substance. + """, + ) + source_compound = SubSection( + section_def=PubChemPureSubstanceSection, + description=""" + Section with properties describing the substance. + """, + ) + + +class IonComponent(SystemComponent): + abbreviation = Quantity( + type=str, + description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + coefficient = Quantity( + type=float, + description='The stoichiometric coefficient', + a_eln=ELNAnnotation(component=ELNComponentEnum.NumberEditQuantity), + shape=[], + ) + common_name = Quantity( + type=str, + description='The common trade name of the ion', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + molecular_formula = Quantity( + type=str, + description='The molecular formula', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + smile = Quantity( + type=str, + description='The canonical SMILE string', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + iupac_name = Quantity( + type=str, + description='The standard IUPAC name', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + cas_number = Quantity( + type=str, + description='The CAS number if available', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_smile = Quantity( + type=str, + description='The canonical SMILE string', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_iupac_name = Quantity( + type=str, + description='The standard IUPAC name', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_cas_number = Quantity( + type=str, + description='The CAS number if available', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + system = Quantity( + type=Reference(PerovskiteIon.m_def), + description='A reference to the component system.', + a_eln=dict(component='ReferenceEditQuantity'), + ) + + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: + """ + The normalizer for the `IonComponent` class. + + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + """ + super().normalize(archive, logger) + if not isinstance(self.system, PerovskiteIon): + return + if self.abbreviation is None: + self.abbreviation = self.system.abbreviation + if isinstance(self.system.pure_substance, PubChemPureSubstanceSection): + if self.molecular_formula is None: + self.molecular_formula = self.system.pure_substance.molecular_formula + if self.smile is None: + self.smile = self.system.pure_substance.smile + if self.iupac_name is None: + self.iupac_name = self.system.pure_substance.iupac_name + if self.cas_number is None: + self.cas_number = self.system.pure_substance.cas_number + if isinstance(self.system.source_compound, PubChemPureSubstanceSection): + if self.source_compound_smile is None: + self.source_compound_smile = self.system.source_compound.smile + if self.source_compound_iupac_name is None: + self.source_compound_iupac_name = self.system.source_compound.iupac_name + if self.source_compound_cas_number is None: + self.source_compound_cas_number = self.system.source_compound.cas_number + + + +class PerovskiteComposition(CompositeSystem, EntryData): + """ + Schema for describing a perovskite composition. + """ + + m_def = Section() + short_form = Quantity( + type=str, + ) + long_form = Quantity( + type=str, + ) + composition_estimate = Quantity( + type=MEnum( + [ + 'Estimated from precursor solutions', + 'Literature value', + 'Estimated from XRD data', + 'Estimated from spectroscopic data', + 'Theoretical simulation', + 'Hypothetical compound', + 'Other', + ] + ), + a_eln=ELNAnnotation(component=ELNComponentEnum.EnumEditQuantity), + ) + sample_type = Quantity( + type=MEnum( + [ + 'Polycrystalline film', + 'Single crystal', + 'Quantum dots', + 'Nano rods', + 'Colloidal solution', + 'Amorphous', + 'Other', + ] + ), + a_eln=ELNAnnotation(component=ELNComponentEnum.EnumEditQuantity), + ) + dimensionality = Quantity( + type=MEnum(['0D', '1D', '2D', '2D/3D', '3D', 'Other']), + description='The dimensionality of the perovskite, i.e. 3D, 2D, 1D (nanorods), quantum dots (0D), etc.', + a_eln=ELNAnnotation(component=ELNComponentEnum.EnumEditQuantity), + ) + band_gap = Quantity( + type=float, + description='Band gap of photoabsorber in eV.', + a_eln=ELNAnnotation(component=ELNComponentEnum.NumberEditQuantity, defaultDisplayUnit='eV'), + unit='eV', + shape=[], + ) + a_ions = SubSection( + section_def=PerovskiteIon, + repeats=True, + ) + b_ions = SubSection( + section_def=PerovskiteIon, + repeats=True, + ) + c_ions = SubSection( + section_def=PerovskiteIon, + repeats=True, + ) + impurities = SubSection( + section_def=Component, + repeats=True, + ) + + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: + """ + The normalizer for the `PerovskiteComposition` class. + + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + """ + super().normalize(archive, logger) + + +m_package.__init_metainfo__() From a414140d2aacb8b46318143f3e4883db78cac93b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 26 Sep 2024 17:09:34 +0200 Subject: [PATCH 02/22] Added A,B, and C ions --- .../composition.py | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index be1e728..c510cd1 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -22,11 +22,11 @@ from nomad.datamodel.datamodel import EntryArchive from nomad.datamodel.metainfo.annotations import ELNAnnotation, ELNComponentEnum from nomad.datamodel.metainfo.basesections import ( + Component, CompositeSystem, PubChemPureSubstanceSection, PureSubstance, SystemComponent, - Component, ) from nomad.metainfo.metainfo import ( Category, @@ -51,9 +51,9 @@ class PerovskiteCompositionCategory(EntryDataCategory): m_def = Category(label='Perovskite Composition', categories=[EntryDataCategory]) -class PerovskiteIon(PureSubstance, EntryData): +class PerovskiteIon(PureSubstance): """ - Class autogenerated from yaml schema. + Abstract class for describing a general perovskite ion. """ m_def = Section() @@ -77,7 +77,28 @@ class PerovskiteIon(PureSubstance, EntryData): ) -class IonComponent(SystemComponent): +class PerovsktieAIon(PerovskiteIon, EntryData): + m_def = Section( + categories=[PerovskiteCompositionCategory], + label='Perovskite A Ion', + ) + + +class PerovsktieBIon(PerovskiteIon, EntryData): + m_def = Section( + categories=[PerovskiteCompositionCategory], + label='Perovskite B Ion', + ) + + +class PerovsktieCIon(PerovskiteIon, EntryData): + m_def = Section( + categories=[PerovskiteCompositionCategory], + label='Perovskite C Ion', + ) + + +class PerovskiteIonComponent(SystemComponent): abbreviation = Quantity( type=str, description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', @@ -176,13 +197,39 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.source_compound_cas_number = self.system.source_compound.cas_number +class PerovskiteAIonComponent(PerovskiteIonComponent): + system = Quantity( + type=Reference(PerovsktieAIon.m_def), + description='A reference to the component system.', + a_eln=dict(component='ReferenceEditQuantity'), + ) + + +class PerovskiteBIonComponent(PerovskiteIonComponent): + system = Quantity( + type=Reference(PerovsktieBIon.m_def), + description='A reference to the component system.', + a_eln=dict(component='ReferenceEditQuantity'), + ) + + +class PerovskiteCIonComponent(PerovskiteIonComponent): + system = Quantity( + type=Reference(PerovsktieCIon.m_def), + description='A reference to the component system.', + a_eln=dict(component='ReferenceEditQuantity'), + ) + class PerovskiteComposition(CompositeSystem, EntryData): """ Schema for describing a perovskite composition. """ - m_def = Section() + m_def = Section( + categories=[PerovskiteCompositionCategory], + label='Perovskite Composition', + ) short_form = Quantity( type=str, ) @@ -225,20 +272,22 @@ class PerovskiteComposition(CompositeSystem, EntryData): band_gap = Quantity( type=float, description='Band gap of photoabsorber in eV.', - a_eln=ELNAnnotation(component=ELNComponentEnum.NumberEditQuantity, defaultDisplayUnit='eV'), + a_eln=ELNAnnotation( + component=ELNComponentEnum.NumberEditQuantity, defaultDisplayUnit='eV' + ), unit='eV', shape=[], ) a_ions = SubSection( - section_def=PerovskiteIon, + section_def=PerovskiteAIonComponent, repeats=True, ) b_ions = SubSection( - section_def=PerovskiteIon, + section_def=PerovskiteBIonComponent, repeats=True, ) c_ions = SubSection( - section_def=PerovskiteIon, + section_def=PerovskiteCIonComponent, repeats=True, ) impurities = SubSection( @@ -256,6 +305,13 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: logger (BoundLogger): A structlog logger. """ super().normalize(archive, logger) + ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions + self.components = ions + self.short_form = '' + self.long_form = '' + for ion in ions: + if ion.abbreviation is not None and ion.coefficient is not None: + self.short_form += f"{ion.abbreviation}{ion.coefficient:.2f}" m_package.__init_metainfo__() From a5b0a41ef6214c3ef68f11d399cf278e863d21fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Wed, 2 Oct 2024 16:55:39 +0200 Subject: [PATCH 03/22] Added entry point, formula, and mass fraction --- pyproject.toml | 3 +- .../__init__.py | 15 +++++++ .../composition.py | 36 +++++++++++++-- src/perovskite_solar_cell_database/utils.py | 44 +++++++++++++++++++ 4 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 src/perovskite_solar_cell_database/utils.py diff --git a/pyproject.toml b/pyproject.toml index a640176..913f62f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -132,4 +132,5 @@ where = ["src"] [project.entry-points.'nomad.plugin'] perovskite_solar_cell = "perovskite_solar_cell_database:perovskite_solar_cell" -solar_cell_app = "perovskite_solar_cell_database.apps:solar_cells" \ No newline at end of file +solar_cell_app = "perovskite_solar_cell_database.apps:solar_cells" +perovskite_composition = "perovskite_solar_cell_database:perovskite_composition" \ No newline at end of file diff --git a/src/perovskite_solar_cell_database/__init__.py b/src/perovskite_solar_cell_database/__init__.py index 4ee935c..ea7b785 100644 --- a/src/perovskite_solar_cell_database/__init__.py +++ b/src/perovskite_solar_cell_database/__init__.py @@ -17,3 +17,18 @@ def load(self): name='PerovskiteSolarCell', description='Schema package defined for the perovskite solar cells database.', ) + + +class PerovskiteCompositionEntryPoint(SchemaPackageEntryPoint): + def load(self): + from perovskite_solar_cell_database.composition import ( + m_package, + ) + + return m_package + + +perovskite_composition = PerovskiteCompositionEntryPoint( + name='PerovskiteComposition', + description='Schema package defined for the perovskite compositions.', +) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index c510cd1..93099ee 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -38,7 +38,8 @@ SubSection, ) from nomad.units import ureg -from structlog.stdlib import BoundLogger + +from perovskite_solar_cell_database.utils import create_archive if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive @@ -304,14 +305,41 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: normalized. logger (BoundLogger): A structlog logger. """ - super().normalize(archive, logger) ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions self.components = ions + if not any(ion.coefficient is None for ion in ions): + coefficient_sum = sum([ion.coefficient for ion in ions]) + for component in self.components: + if ( + not isinstance(component, PerovskiteIonComponent) + or not isinstance(component.system, PerovskiteIon) + or not isinstance( + component.system.pure_substance, PubChemPureSubstanceSection + ) + or component.system.pure_substance.molecular_mass is None + ): + continue + component.mass_fraction = ( + component.system.pure_substance.molecular_mass + * component.coefficient + / coefficient_sum + ) self.short_form = '' self.long_form = '' for ion in ions: - if ion.abbreviation is not None and ion.coefficient is not None: - self.short_form += f"{ion.abbreviation}{ion.coefficient:.2f}" + if ion.abbreviation is None: + continue + self.short_form += ion.abbreviation + if ion.coefficient is None: + continue + if ion.coefficient == 1: + coefficient_str = '' + elif ion.coefficient == int(ion.coefficient): + coefficient_str = str(int(ion.coefficient)) + else: + coefficient_str = f'{ion.coefficient:.2}' + self.long_form += f'{ion.abbreviation}{coefficient_str}' + super().normalize(archive, logger) m_package.__init_metainfo__() diff --git a/src/perovskite_solar_cell_database/utils.py b/src/perovskite_solar_cell_database/utils.py new file mode 100644 index 0000000..a843ad1 --- /dev/null +++ b/src/perovskite_solar_cell_database/utils.py @@ -0,0 +1,44 @@ +# +# Copyright The NOMAD Authors. +# +# This file is part of NOMAD. See https://nomad-lab.eu for further info. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +def get_reference(upload_id, entry_id): + return f'../uploads/{upload_id}/archive/{entry_id}#data' + + +def get_entry_id_from_file_name(file_name, archive): + from nomad.utils import hash + + return hash(archive.metadata.upload_id, file_name) + + +def create_archive(entity, archive, file_name) -> str: + import json + + from nomad.datamodel.context import ClientContext + + if isinstance(archive.m_context, ClientContext): + return None + if not archive.m_context.raw_path_exists(file_name): + entity_entry = entity.m_to_dict(with_root_def=True) + with archive.m_context.raw_file(file_name, 'w') as outfile: + json.dump({'data': entity_entry}, outfile) + archive.m_context.process_updated_raw_file(file_name) + return get_reference( + archive.metadata.upload_id, get_entry_id_from_file_name(file_name, archive) + ) \ No newline at end of file From c162cc7383a4475b58644eb7949acda9baad1c47 Mon Sep 17 00:00:00 2001 From: Pepe Marquez Date: Fri, 4 Oct 2024 18:11:01 +0200 Subject: [PATCH 04/22] Removed schemas from root --- schema.archive.yaml | 171 -------------------------------------- schema.py | 197 -------------------------------------------- 2 files changed, 368 deletions(-) delete mode 100644 schema.archive.yaml delete mode 100644 schema.py diff --git a/schema.archive.yaml b/schema.archive.yaml deleted file mode 100644 index 43ac78b..0000000 --- a/schema.archive.yaml +++ /dev/null @@ -1,171 +0,0 @@ -definitions: - name: 'Perovskite composition' - sections: - perovskite_composition: - quantities: - # To be generated in the back end - short_form: - type: str - - long_form: - type: str - - # Parameters to be given by the user - composition_estimate: - type: - type_kind: Enum - type_data: - - 'Estimated from precursor solutions' - - 'Literature value' - - 'Estimated from XRD data' - - 'Estimated from spectroscopic data' - - 'Theoretical simulation' - - 'Hypothetical compound' - - 'Other' - m_annotations: - eln: - component: EnumEditQuantity - - sample_type: - type: - type_kind: Enum - type_data: - - 'Polycrystalline film' - - 'Single crystal' - - 'Quantum dots' - - 'Nano rods' - - 'Colloidal solution' - - 'Amorphous' - - 'Other' - m_annotations: - eln: - component: EnumEditQuantity - - dimensionality: - type: - type_kind: Enum - type_data: - - '0D' - - '1D' - - '2D' - - '2D/3D' - - '3D' - - 'Other' - description: The dimensionality of the perovskite, i.e. 3D, 2D, 1D (nanorods), quantum dots (0D), etc. - m_annotations: - eln: - component: EnumEditQuantity - - band_gap: - type: float - unit: eV - shape: [] - description: Band gap of photoabsorber in eV. - m_annotations: - eln: - component: NumberEditQuantity - defaultDisplayUnit: eV - - # Sub sections - sub_sections: - a_ions: - section: '#/Ions' - repeats: true - - b_ions: - section: '#/Ions' - repeats: true - - c_ions: - section: '#/Ions' - repeats: true - - secondary_phases_impurities_and_dopants: - section: '#/Materials_in_layer' - repeats: true - Ions: - quantities: - abbreviation: - type: str - shape: [] - description: The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically - m_annotations: - eln: - component: StringEditQuantity - - coefficient: - type: float - shape: [] - description: The stoichiometric coefficient - m_annotations: - eln: - component: NumberEditQuantity - - - # Here we could have one boolean filed asking if teh remainder of the ion data should be read for online source - - # Here we could give a text field asking for teh source of the ion data, but with a predetermined default answer - - common_name: - type: str - shape: [] - description: The common trade name of the ion - m_annotations: - eln: - component: StringEditQuantity - - molecular_formula: - type: str - shape: [] - description: The molecular formula - m_annotations: - eln: - component: StringEditQuantity - - smile: - type: str - shape: [] - description: The canonical SMILE string - m_annotations: - eln: - component: StringEditQuantity - - iupac_name: - type: str - shape: [] - description: The standard IUPAC name - m_annotations: - eln: - component: StringEditQuantity - - cas_number: - type: str - shape: [] - description: The CAS number if available - m_annotations: - eln: - component: StringEditQuantity - - source_compound_smile: - type: str - shape: [] - description: The canonical SMILE string - m_annotations: - eln: - component: StringEditQuantity - - source_compound_iupac_name: - type: str - shape: [] - description: The standard IUPAC name - m_annotations: - eln: - component: StringEditQuantity - - source_compound_cas_number: - type: str - shape: [] - description: The CAS number if available - m_annotations: - eln: - component: StringEditQuantity \ No newline at end of file diff --git a/schema.py b/schema.py deleted file mode 100644 index e4b281e..0000000 --- a/schema.py +++ /dev/null @@ -1,197 +0,0 @@ -# -# Copyright The NOMAD Authors. -# -# This file is part of NOMAD. See https://nomad-lab.eu for further info. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -from typing import ( - TYPE_CHECKING, -) -from nomad.metainfo import ( - Package, - Quantity, - SubSection, - MEnum, - Section, -) -from nomad.datamodel.data import ( - ArchiveSection, -) -if TYPE_CHECKING: - pass - -m_package = Package(name='Perovskite composition') - - -class perovskite_composition(ArchiveSection): - ''' - Class autogenerated from yaml schema. - ''' - m_def = Section() - short_form = Quantity( - type=str, - ) - long_form = Quantity( - type=str, - ) - composition_estimate = Quantity( - type=MEnum( - [ - 'Estimated from precursor solutions', - 'Literature value', - 'Estimated from XRD data', - 'Estimated from spectroscopic data', - 'Theoretical simulation', - 'Hypothetical compound', - 'Other']), - a_eln={ - "component": "EnumEditQuantity"}, - ) - sample_type = Quantity( - type=MEnum( - [ - 'Polycrystalline film', - 'Single crystal', - 'Quantum dots', - 'Nano rods', - 'Colloidal solution', - 'Amorphous', - 'Other']), - a_eln={ - "component": "EnumEditQuantity"}, - ) - dimensionality = Quantity( - type=MEnum(['0D', '1D', '2D', '2D/3D', '3D', 'Other']), - description='The dimensionality of the perovskite, i.e. 3D, 2D, 1D (nanorods), quantum dots (0D), etc.', - a_eln={ - "component": "EnumEditQuantity" - }, - ) - band_gap = Quantity( - type=float, - description='Band gap of photoabsorber in eV.', - a_eln={ - "component": "NumberEditQuantity", - "defaultDisplayUnit": "eV" - }, - unit="eV", - shape=[], - ) - a_ions = SubSection( - section_def=Ions, - repeats=True, - ) - b_ions = SubSection( - section_def=Ions, - repeats=True, - ) - c_ions = SubSection( - section_def=Ions, - repeats=True, - ) - secondary_phases_impurities_and_dopants = SubSection( - section_def=Materials_in_layer, - repeats=True, - ) - - -class Ions(ArchiveSection): - ''' - Class autogenerated from yaml schema. - ''' - m_def = Section() - abbreviation = Quantity( - type=str, - description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - coefficient = Quantity( - type=float, - description='The stoichiometric coefficient', - a_eln={ - "component": "NumberEditQuantity" - }, - shape=[], - ) - common_name = Quantity( - type=str, - description='The common trade name of the ion', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - molecular_formula = Quantity( - type=str, - description='The molecular formula', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - smile = Quantity( - type=str, - description='The canonical SMILE string', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - iupac_name = Quantity( - type=str, - description='The standard IUPAC name', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - cas_number = Quantity( - type=str, - description='The CAS number if available', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - source_compound_smile = Quantity( - type=str, - description='The canonical SMILE string', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - source_compound_iupac_name = Quantity( - type=str, - description='The standard IUPAC name', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - source_compound_cas_number = Quantity( - type=str, - description='The CAS number if available', - a_eln={ - "component": "StringEditQuantity" - }, - shape=[], - ) - - -m_package.__init_metainfo__() From ce6e1289f7f964776a5abc12a7f492e04efafd8b Mon Sep 17 00:00:00 2001 From: Pepe Marquez Date: Fri, 4 Oct 2024 18:24:06 +0200 Subject: [PATCH 05/22] Updated actions. --- .github/workflows/actions.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index dc87590..1cfb621 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -73,10 +73,10 @@ jobs: args: "check ." # to enable auto-formatting check, uncomment the following lines below - ruff-formatting: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 - with: - args: "format . --check" + # ruff-formatting: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: chartboost/ruff-action@v1 + # with: + # args: "format . --check" From 348ad9a833cf40ff464aa87535cb181a170fac56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 31 Oct 2024 17:19:31 +0100 Subject: [PATCH 06/22] Added impurity class --- .../composition.py | 105 ++++++++++++++++-- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 93099ee..ed01bc9 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -22,10 +22,10 @@ from nomad.datamodel.datamodel import EntryArchive from nomad.datamodel.metainfo.annotations import ELNAnnotation, ELNComponentEnum from nomad.datamodel.metainfo.basesections import ( - Component, CompositeSystem, PubChemPureSubstanceSection, PureSubstance, + PureSubstanceComponent, SystemComponent, ) from nomad.metainfo.metainfo import ( @@ -124,7 +124,7 @@ class PerovskiteIonComponent(SystemComponent): a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), shape=[], ) - smile = Quantity( + smiles = Quantity( type=str, description='The canonical SMILE string', a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), @@ -142,7 +142,7 @@ class PerovskiteIonComponent(SystemComponent): a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), shape=[], ) - source_compound_smile = Quantity( + source_compound_smiles = Quantity( type=str, description='The canonical SMILE string', a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), @@ -183,15 +183,15 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if isinstance(self.system.pure_substance, PubChemPureSubstanceSection): if self.molecular_formula is None: self.molecular_formula = self.system.pure_substance.molecular_formula - if self.smile is None: - self.smile = self.system.pure_substance.smile + if self.smiles is None: + self.smiles = self.system.pure_substance.smile if self.iupac_name is None: self.iupac_name = self.system.pure_substance.iupac_name if self.cas_number is None: self.cas_number = self.system.pure_substance.cas_number if isinstance(self.system.source_compound, PubChemPureSubstanceSection): - if self.source_compound_smile is None: - self.source_compound_smile = self.system.source_compound.smile + if self.source_compound_smiles is None: + self.source_compound_smiles = self.system.source_compound.smile if self.source_compound_iupac_name is None: self.source_compound_iupac_name = self.system.source_compound.iupac_name if self.source_compound_cas_number is None: @@ -222,6 +222,91 @@ class PerovskiteCIonComponent(PerovskiteIonComponent): ) +class Impurity(PureSubstanceComponent): + abbreviation = Quantity( + type=str, + description='The abbreviation used for the additive or impurity.', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + concentration = Quantity( + type=float, + description='The concentration of the additive or impurity.', + a_eln=ELNAnnotation( + component=ELNComponentEnum.NumberEditQuantity, defaultDisplayUnit='mol%' + ), + unit='cm^-3', + shape=[], + ) + common_name = Quantity( + type=str, + description='The common or trivial name of the additive or impurity.', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + molecular_formula = Quantity( + type=str, + description='The Molecular formula which indicates the numbers of each type of atom in a molecule, with no information about the structure.', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + smiles = Quantity( + type=str, + description='The canonical SMILES string of the additive or impurity.', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + iupac_name = Quantity( + type=str, + description='The preferred systematic IUPAC name of the additive or impurity.', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + cas_number = Quantity( + type=str, + description='The CAS number for the additive or impurity.', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + pure_substance = SubSection( + section_def=PubChemPureSubstanceSection, + description=""" + Section describing the pure substance that is the component. + """, + ) + + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: + """ + The normalizer for the `Impurity` class. + + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + """ + super().normalize(archive, logger) + if isinstance(self.pure_substance, PubChemPureSubstanceSection): + if self.molecular_formula is None: + self.molecular_formula = self.pure_substance.molecular_formula + if self.smiles is None: + self.smiles = self.pure_substance.smile + if self.iupac_name is None: + self.iupac_name = self.pure_substance.iupac_name + if self.cas_number is None: + self.cas_number = self.pure_substance.cas_number + if self.common_name is None: + self.common_name = self.pure_substance.name + else: + pure_substance = PubChemPureSubstanceSection() + pure_substance.molecular_formula = self.molecular_formula + pure_substance.smile = self.smiles + pure_substance.iupac_name = self.iupac_name + pure_substance.cas_number = self.cas_number + pure_substance.name = self.common_name + pure_substance.normalize(archive, logger) + self.pure_substance = pure_substance + + class PerovskiteComposition(CompositeSystem, EntryData): """ Schema for describing a perovskite composition. @@ -292,7 +377,11 @@ class PerovskiteComposition(CompositeSystem, EntryData): repeats=True, ) impurities = SubSection( - section_def=Component, + section_def=Impurity, + repeats=True, + ) + additives = SubSection( + section_def=Impurity, repeats=True, ) From ba3f3c20ec468edc857dfdade6c258bff82c1d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Tue, 5 Nov 2024 11:09:05 +0100 Subject: [PATCH 07/22] Aligned ion schema with json schema --- .../composition.py | 364 ++++++++++++------ 1 file changed, 239 insertions(+), 125 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index ed01bc9..ca1cd9c 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -19,8 +19,16 @@ from typing import TYPE_CHECKING from nomad.datamodel.data import EntryData, EntryDataCategory -from nomad.datamodel.datamodel import EntryArchive -from nomad.datamodel.metainfo.annotations import ELNAnnotation, ELNComponentEnum +from nomad.datamodel.datamodel import ( + EntryArchive, + ArchiveSection, +) +from nomad.datamodel.metainfo.annotations import ( + ELNAnnotation, + ELNComponentEnum, + SectionDisplayAnnotation, + Filter, +) from nomad.datamodel.metainfo.basesections import ( CompositeSystem, PubChemPureSubstanceSection, @@ -52,12 +60,100 @@ class PerovskiteCompositionCategory(EntryDataCategory): m_def = Category(label='Perovskite Composition', categories=[EntryDataCategory]) -class PerovskiteIon(PureSubstance): +class PerovskiteChemicalSection(ArchiveSection): + common_name = Quantity( + type=str, + description='The common trade name', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + molecular_formula = Quantity( + type=str, + description='The molecular formula', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + smiles = Quantity( + type=str, + description='The canonical SMILE string', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + iupac_name = Quantity( + type=str, + description='The standard IUPAC name', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + cas_number = Quantity( + type=str, + description='The CAS number if available', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + + +class PerovskiteIonSection(PerovskiteChemicalSection): + abbreviation = Quantity( + type=str, + description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_molecular_formula = Quantity( + type=str, + description='The molecular formula of the source compound', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_smiles = Quantity( + type=str, + description='The canonical SMILE string of the source compound', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_iupac_name = Quantity( + type=str, + description='The standard IUPAC name of the source compound', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + source_compound_cas_number = Quantity( + type=str, + description='The CAS number if available of the source compound', + a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), + shape=[], + ) + + +class PerovskiteIon(PureSubstance, PerovskiteIonSection): """ Abstract class for describing a general perovskite ion. """ - m_def = Section() + m_def = Section( + a_display=SectionDisplayAnnotation( + visible=Filter( + exclude=[ + 'description', + 'name', + 'lab_id', + ] + ), + order=[ + 'common_name', + 'abbreviation', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) abbreviation = Quantity( type=str, description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', @@ -77,89 +173,135 @@ class PerovskiteIon(PureSubstance): """, ) + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: + """ + The normalizer for the `PerovskiteIon` class. -class PerovsktieAIon(PerovskiteIon, EntryData): + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + """ + super().normalize(archive, logger) + if isinstance(self.pure_substance, PubChemPureSubstanceSection): + if self.molecular_formula is None: + self.molecular_formula = self.pure_substance.molecular_formula + if self.smiles is None: + self.smiles = self.pure_substance.smile + if self.iupac_name is None: + self.iupac_name = self.pure_substance.iupac_name + if self.cas_number is None: + self.cas_number = self.pure_substance.cas_number + else: + pure_substance = PubChemPureSubstanceSection( + molecular_formula=self.molecular_formula, + smile=self.smiles, + iupac_name=self.iupac_name, + cas_number=self.cas_number, + name=self.common_name, + ) + pure_substance.normalize(archive, logger) + self.pure_substance = pure_substance + self.normalize(archive, logger) + if isinstance(self.source_compound, PubChemPureSubstanceSection): + if self.source_compound_molecular_formula is None: + self.source_compound_molecular_formula = self.source_compound.molecular_formula + if self.source_compound_smiles is None: + self.source_compound_smiles = self.source_compound.smile + if self.source_compound_iupac_name is None: + self.source_compound_iupac_name = self.source_compound.iupac_name + if self.source_compound_cas_number is None: + self.source_compound_cas_number = self.source_compound.cas_number + else: + source_compound = PubChemPureSubstanceSection( + molecular_formula=self.source_compound_molecular_formula, + smile=self.source_compound_smiles, + iupac_name=self.source_compound_iupac_name, + cas_number=self.source_compound_cas_number, + ) + source_compound.normalize(archive, logger) + self.source_compound = source_compound + self.normalize(archive, logger) + + +class PerovskiteAIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite A Ion', - ) - - -class PerovsktieBIon(PerovskiteIon, EntryData): + a_display={ + "visible":{ + "exclude":[ + 'description', + 'name', + 'lab_id', + 'datetime', + 'elemental_composition', + ] + }, + "order":[ + 'common_name', + 'abbreviation', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + 'pure_substance', + 'source_compound', + ] + } + # a_display=SectionDisplayAnnotation( + # visible=Filter( + # exclude=[ + # 'description', + # 'name', + # 'lab_id', + # 'datetime', + # 'elemental_composition', + # ] + # ), + # order=[ + # 'common_name', + # 'abbreviation', + # 'molecular_formula', + # 'smiles', + # 'iupac_name', + # 'cas_number', + # 'source_compound_molecular_formula', + # 'source_compound_smiles', + # 'source_compound_iupac_name', + # 'source_compound_cas_number', + # 'pure_substance', + # 'source_compound', + # ] + # ) + ) + + +class PerovskiteBIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite B Ion', ) -class PerovsktieCIon(PerovskiteIon, EntryData): +class PerovskiteXIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite C Ion', ) -class PerovskiteIonComponent(SystemComponent): - abbreviation = Quantity( - type=str, - description='The standard abbreviation of the ion. If the abbreviation is in the archive, additional data is complemented automatically', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) +class PerovskiteIonComponent(SystemComponent, PerovskiteIonSection): coefficient = Quantity( type=float, description='The stoichiometric coefficient', a_eln=ELNAnnotation(component=ELNComponentEnum.NumberEditQuantity), shape=[], ) - common_name = Quantity( - type=str, - description='The common trade name of the ion', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - molecular_formula = Quantity( - type=str, - description='The molecular formula', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - smiles = Quantity( - type=str, - description='The canonical SMILE string', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - iupac_name = Quantity( - type=str, - description='The standard IUPAC name', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - cas_number = Quantity( - type=str, - description='The CAS number if available', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - source_compound_smiles = Quantity( - type=str, - description='The canonical SMILE string', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - source_compound_iupac_name = Quantity( - type=str, - description='The standard IUPAC name', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - source_compound_cas_number = Quantity( - type=str, - description='The CAS number if available', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) system = Quantity( type=Reference(PerovskiteIon.m_def), description='A reference to the component system.', @@ -180,27 +322,27 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: return if self.abbreviation is None: self.abbreviation = self.system.abbreviation - if isinstance(self.system.pure_substance, PubChemPureSubstanceSection): - if self.molecular_formula is None: - self.molecular_formula = self.system.pure_substance.molecular_formula - if self.smiles is None: - self.smiles = self.system.pure_substance.smile - if self.iupac_name is None: - self.iupac_name = self.system.pure_substance.iupac_name - if self.cas_number is None: - self.cas_number = self.system.pure_substance.cas_number - if isinstance(self.system.source_compound, PubChemPureSubstanceSection): - if self.source_compound_smiles is None: - self.source_compound_smiles = self.system.source_compound.smile - if self.source_compound_iupac_name is None: - self.source_compound_iupac_name = self.system.source_compound.iupac_name - if self.source_compound_cas_number is None: - self.source_compound_cas_number = self.system.source_compound.cas_number + if self.molecular_formula is None: + self.molecular_formula = self.system.molecular_formula + if self.smiles is None: + self.smiles = self.system.smiles + if self.iupac_name is None: + self.iupac_name = self.system.iupac_name + if self.cas_number is None: + self.cas_number = self.system.cas_number + if self.source_compound_molecular_formula is None: + self.source_compound_molecular_formula = self.system.source_compound_molecular_formula + if self.source_compound_smiles is None: + self.source_compound_smiles = self.system.source_compound_smiles + if self.source_compound_iupac_name is None: + self.source_compound_iupac_name = self.system.source_compound_iupac_name + if self.source_compound_cas_number is None: + self.source_compound_cas_number = self.system.source_compound_cas_number class PerovskiteAIonComponent(PerovskiteIonComponent): system = Quantity( - type=Reference(PerovsktieAIon.m_def), + type=Reference(PerovskiteAIon.m_def), description='A reference to the component system.', a_eln=dict(component='ReferenceEditQuantity'), ) @@ -208,21 +350,21 @@ class PerovskiteAIonComponent(PerovskiteIonComponent): class PerovskiteBIonComponent(PerovskiteIonComponent): system = Quantity( - type=Reference(PerovsktieBIon.m_def), + type=Reference(PerovskiteBIon.m_def), description='A reference to the component system.', a_eln=dict(component='ReferenceEditQuantity'), ) -class PerovskiteCIonComponent(PerovskiteIonComponent): +class PerovskiteXIonComponent(PerovskiteIonComponent): system = Quantity( - type=Reference(PerovsktieCIon.m_def), + type=Reference(PerovskiteXIon.m_def), description='A reference to the component system.', a_eln=dict(component='ReferenceEditQuantity'), ) -class Impurity(PureSubstanceComponent): +class Impurity(PureSubstanceComponent, PerovskiteChemicalSection): abbreviation = Quantity( type=str, description='The abbreviation used for the additive or impurity.', @@ -238,36 +380,6 @@ class Impurity(PureSubstanceComponent): unit='cm^-3', shape=[], ) - common_name = Quantity( - type=str, - description='The common or trivial name of the additive or impurity.', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - molecular_formula = Quantity( - type=str, - description='The Molecular formula which indicates the numbers of each type of atom in a molecule, with no information about the structure.', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - smiles = Quantity( - type=str, - description='The canonical SMILES string of the additive or impurity.', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - iupac_name = Quantity( - type=str, - description='The preferred systematic IUPAC name of the additive or impurity.', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) - cas_number = Quantity( - type=str, - description='The CAS number for the additive or impurity.', - a_eln=ELNAnnotation(component=ELNComponentEnum.StringEditQuantity), - shape=[], - ) pure_substance = SubSection( section_def=PubChemPureSubstanceSection, description=""" @@ -297,14 +409,16 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if self.common_name is None: self.common_name = self.pure_substance.name else: - pure_substance = PubChemPureSubstanceSection() - pure_substance.molecular_formula = self.molecular_formula - pure_substance.smile = self.smiles - pure_substance.iupac_name = self.iupac_name - pure_substance.cas_number = self.cas_number - pure_substance.name = self.common_name + pure_substance = PubChemPureSubstanceSection( + name=self.common_name, + molecular_formula=self.molecular_formula, + smile=self.smiles, + iupac_name=self.iupac_name, + cas_number=self.cas_number, + ) pure_substance.normalize(archive, logger) self.pure_substance = pure_substance + self.normalize(archive, logger) class PerovskiteComposition(CompositeSystem, EntryData): @@ -372,8 +486,8 @@ class PerovskiteComposition(CompositeSystem, EntryData): section_def=PerovskiteBIonComponent, repeats=True, ) - c_ions = SubSection( - section_def=PerovskiteCIonComponent, + x_ions = SubSection( + section_def=PerovskiteXIonComponent, repeats=True, ) impurities = SubSection( @@ -394,7 +508,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: normalized. logger (BoundLogger): A structlog logger. """ - ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions + ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.x_ions self.components = ions if not any(ion.coefficient is None for ion in ions): coefficient_sum = sum([ion.coefficient for ion in ions]) From 964feb20a881d24103d85c2d2a9a892fa8caad83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Wed, 6 Nov 2024 17:06:28 +0100 Subject: [PATCH 08/22] Fixed normalize function and added annotations --- .../composition.py | 435 ++++++++++++------ 1 file changed, 301 insertions(+), 134 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index ca1cd9c..06b1607 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -20,14 +20,14 @@ from nomad.datamodel.data import EntryData, EntryDataCategory from nomad.datamodel.datamodel import ( - EntryArchive, ArchiveSection, + EntryArchive, ) from nomad.datamodel.metainfo.annotations import ( ELNAnnotation, ELNComponentEnum, - SectionDisplayAnnotation, Filter, + SectionProperties, ) from nomad.datamodel.metainfo.basesections import ( CompositeSystem, @@ -45,9 +45,6 @@ Section, SubSection, ) -from nomad.units import ureg - -from perovskite_solar_cell_database.utils import create_archive if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive @@ -132,26 +129,31 @@ class PerovskiteIon(PureSubstance, PerovskiteIonSection): """ m_def = Section( - a_display=SectionDisplayAnnotation( - visible=Filter( - exclude=[ - 'description', - 'name', - 'lab_id', + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'description', + 'name', + 'lab_id', + 'pure_substance', + 'source_compound', + 'datetime', + ], + ), + order=[ + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', ] - ), - order=[ - 'common_name', - 'abbreviation', - 'molecular_formula', - 'smiles', - 'iupac_name', - 'cas_number', - 'source_compound_molecular_formula', - 'source_compound_smiles', - 'source_compound_iupac_name', - 'source_compound_cas_number', - ] + ) ) ) abbreviation = Quantity( @@ -182,102 +184,79 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: normalized. logger (BoundLogger): A structlog logger. """ + pure_substance = PubChemPureSubstanceSection( + molecular_formula=self.molecular_formula, + smile=self.smiles, + iupac_name=self.iupac_name, + cas_number=self.cas_number, + name=self.common_name, + ) + pure_substance.normalize(archive, logger) + if self.molecular_formula is None: + self.molecular_formula = pure_substance.molecular_formula + if self.smiles is None: + self.smiles = pure_substance.smile + if self.iupac_name is None: + self.iupac_name = pure_substance.iupac_name + if self.cas_number is None: + self.cas_number = pure_substance.cas_number + if self.common_name is None: + self.common_name = pure_substance.name + self.pure_substance = pure_substance + source_compound = PubChemPureSubstanceSection( + molecular_formula=self.source_compound_molecular_formula, + smile=self.source_compound_smiles, + iupac_name=self.source_compound_iupac_name, + cas_number=self.source_compound_cas_number, + ) + source_compound.normalize(archive, logger) + if self.source_compound_molecular_formula is None: + self.source_compound_molecular_formula = source_compound.molecular_formula + if self.source_compound_smiles is None: + self.source_compound_smiles = source_compound.smile + if self.source_compound_iupac_name is None: + self.source_compound_iupac_name = source_compound.iupac_name + if self.source_compound_cas_number is None: + self.source_compound_cas_number = source_compound.cas_number + self.source_compound = source_compound super().normalize(archive, logger) - if isinstance(self.pure_substance, PubChemPureSubstanceSection): - if self.molecular_formula is None: - self.molecular_formula = self.pure_substance.molecular_formula - if self.smiles is None: - self.smiles = self.pure_substance.smile - if self.iupac_name is None: - self.iupac_name = self.pure_substance.iupac_name - if self.cas_number is None: - self.cas_number = self.pure_substance.cas_number - else: - pure_substance = PubChemPureSubstanceSection( - molecular_formula=self.molecular_formula, - smile=self.smiles, - iupac_name=self.iupac_name, - cas_number=self.cas_number, - name=self.common_name, - ) - pure_substance.normalize(archive, logger) - self.pure_substance = pure_substance - self.normalize(archive, logger) - if isinstance(self.source_compound, PubChemPureSubstanceSection): - if self.source_compound_molecular_formula is None: - self.source_compound_molecular_formula = self.source_compound.molecular_formula - if self.source_compound_smiles is None: - self.source_compound_smiles = self.source_compound.smile - if self.source_compound_iupac_name is None: - self.source_compound_iupac_name = self.source_compound.iupac_name - if self.source_compound_cas_number is None: - self.source_compound_cas_number = self.source_compound.cas_number - else: - source_compound = PubChemPureSubstanceSection( - molecular_formula=self.source_compound_molecular_formula, - smile=self.source_compound_smiles, - iupac_name=self.source_compound_iupac_name, - cas_number=self.source_compound_cas_number, - ) - source_compound.normalize(archive, logger) - self.source_compound = source_compound - self.normalize(archive, logger) class PerovskiteAIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite A Ion', - a_display={ - "visible":{ - "exclude":[ - 'description', - 'name', - 'lab_id', - 'datetime', - 'elemental_composition', + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'description', + 'name', + 'lab_id', + 'datetime', + ], + ), + editable=Filter( + exclude=[ + 'pure_substance', + 'source_compound', + 'elemental_composition', + ] + ), + order=[ + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', ] - }, - "order":[ - 'common_name', - 'abbreviation', - 'molecular_formula', - 'smiles', - 'iupac_name', - 'cas_number', - 'source_compound_molecular_formula', - 'source_compound_smiles', - 'source_compound_iupac_name', - 'source_compound_cas_number', - 'pure_substance', - 'source_compound', - ] - } - # a_display=SectionDisplayAnnotation( - # visible=Filter( - # exclude=[ - # 'description', - # 'name', - # 'lab_id', - # 'datetime', - # 'elemental_composition', - # ] - # ), - # order=[ - # 'common_name', - # 'abbreviation', - # 'molecular_formula', - # 'smiles', - # 'iupac_name', - # 'cas_number', - # 'source_compound_molecular_formula', - # 'source_compound_smiles', - # 'source_compound_iupac_name', - # 'source_compound_cas_number', - # 'pure_substance', - # 'source_compound', - # ] - # ) + ) + ) ) @@ -285,6 +264,37 @@ class PerovskiteBIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite B Ion', + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'description', + 'name', + 'lab_id', + 'datetime', + ], + ), + editable=Filter( + exclude=[ + 'pure_substance', + 'source_compound', + 'elemental_composition', + ] + ), + order=[ + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) ) @@ -292,10 +302,68 @@ class PerovskiteXIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite C Ion', + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'description', + 'name', + 'lab_id', + 'datetime', + ], + ), + editable=Filter( + exclude=[ + 'pure_substance', + 'source_compound', + 'elemental_composition', + ] + ), + order=[ + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) ) class PerovskiteIonComponent(SystemComponent, PerovskiteIonSection): + m_def = Section( + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'name', + 'mass', + 'mass_fraction', + ], + ), + order=[ + 'system', + 'coefficient', + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) + ) coefficient = Quantity( type=float, description='The stoichiometric coefficient', @@ -341,6 +409,33 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: class PerovskiteAIonComponent(PerovskiteIonComponent): + m_def = Section( + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'name', + 'mass', + 'mass_fraction', + ], + ), + order=[ + 'system', + 'coefficient', + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) + ) system = Quantity( type=Reference(PerovskiteAIon.m_def), description='A reference to the component system.', @@ -349,6 +444,33 @@ class PerovskiteAIonComponent(PerovskiteIonComponent): class PerovskiteBIonComponent(PerovskiteIonComponent): + m_def = Section( + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'name', + 'mass', + 'mass_fraction', + ], + ), + order=[ + 'system', + 'coefficient', + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) + ) system = Quantity( type=Reference(PerovskiteBIon.m_def), description='A reference to the component system.', @@ -357,6 +479,33 @@ class PerovskiteBIonComponent(PerovskiteIonComponent): class PerovskiteXIonComponent(PerovskiteIonComponent): + m_def = Section( + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'name', + 'mass', + 'mass_fraction', + ], + ), + order=[ + 'system', + 'coefficient', + 'abbreviation', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + 'source_compound_molecular_formula', + 'source_compound_smiles', + 'source_compound_iupac_name', + 'source_compound_cas_number', + ] + ) + ) + ) system = Quantity( type=Reference(PerovskiteXIon.m_def), description='A reference to the component system.', @@ -365,6 +514,28 @@ class PerovskiteXIonComponent(PerovskiteIonComponent): class Impurity(PureSubstanceComponent, PerovskiteChemicalSection): + m_def = Section( + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'name', + 'mass', + ], + ), + order=[ + 'abbreviation', + 'concentration', + 'mass_fraction', + 'common_name', + 'molecular_formula', + 'smiles', + 'iupac_name', + 'cas_number', + ] + ) + ) + ) abbreviation = Quantity( type=str, description='The abbreviation used for the additive or impurity.', @@ -396,29 +567,25 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: normalized. logger (BoundLogger): A structlog logger. """ + pure_substance = PubChemPureSubstanceSection( + name=self.common_name, + molecular_formula=self.molecular_formula, + smile=self.smiles, + iupac_name=self.iupac_name, + cas_number=self.cas_number, + ) + pure_substance.normalize(archive, logger) + if self.molecular_formula is None: + self.molecular_formula = pure_substance.molecular_formula + if self.smiles is None: + self.smiles = pure_substance.smile + if self.iupac_name is None: + self.iupac_name = pure_substance.iupac_name + if self.cas_number is None: + self.cas_number = pure_substance.cas_number + if self.common_name is None: + self.common_name = pure_substance.name super().normalize(archive, logger) - if isinstance(self.pure_substance, PubChemPureSubstanceSection): - if self.molecular_formula is None: - self.molecular_formula = self.pure_substance.molecular_formula - if self.smiles is None: - self.smiles = self.pure_substance.smile - if self.iupac_name is None: - self.iupac_name = self.pure_substance.iupac_name - if self.cas_number is None: - self.cas_number = self.pure_substance.cas_number - if self.common_name is None: - self.common_name = self.pure_substance.name - else: - pure_substance = PubChemPureSubstanceSection( - name=self.common_name, - molecular_formula=self.molecular_formula, - smile=self.smiles, - iupac_name=self.iupac_name, - cas_number=self.cas_number, - ) - pure_substance.normalize(archive, logger) - self.pure_substance = pure_substance - self.normalize(archive, logger) class PerovskiteComposition(CompositeSystem, EntryData): From e084cdacc2aa300580c765ca35b9c96fc5de267f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Wed, 6 Nov 2024 20:32:03 +0100 Subject: [PATCH 09/22] Added first attempt at topology normalization --- .../composition.py | 159 +++++++++++++++++- 1 file changed, 152 insertions(+), 7 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 06b1607..5b9a625 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -18,7 +18,11 @@ from typing import TYPE_CHECKING -from nomad.datamodel.data import EntryData, EntryDataCategory +from ase import Atoms +from nomad.datamodel.data import ( + EntryData, + EntryDataCategory, +) from nomad.datamodel.datamodel import ( ArchiveSection, EntryArchive, @@ -36,6 +40,13 @@ PureSubstanceComponent, SystemComponent, ) +from nomad.datamodel.results import ( + Material, + Properties, + Relation, + Results, + System, +) from nomad.metainfo.metainfo import ( Category, MEnum, @@ -45,6 +56,8 @@ Section, SubSection, ) +from nomad.normalizing.common import nomad_atoms_from_ase_atoms +from nomad.normalizing.topology import add_system, add_system_info if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive @@ -53,6 +66,42 @@ m_package = Package() +def convert_rdkit_mol_to_ase_atoms(rdkit_mol): + """ + Convert an RDKit molecule to an ASE atoms object. + + Args: + rdkit_mol (rdkit.Chem.Mol): RDKit molecule object. + + Returns: + ase.Atoms: ASE atoms object. + """ + positions = rdkit_mol.GetConformer().GetPositions() + atomic_numbers = [atom.GetAtomicNum() for atom in rdkit_mol.GetAtoms()] + ase_atoms = Atoms(numbers=atomic_numbers, positions=positions) + return ase_atoms + + +def optimize_molecule(smiles): + from rdkit import Chem + from rdkit.Chem import AllChem + + try: + m = Chem.MolFromSmiles(smiles) + m = Chem.AddHs(m) + + AllChem.EmbedMolecule(m) + AllChem.MMFFOptimizeMolecule(m) + + ase_atoms = convert_rdkit_mol_to_ase_atoms(m) + + # Further processing + # ... + return ase_atoms + except Exception as e: + print(f'An error occurred: {e}') + + class PerovskiteCompositionCategory(EntryDataCategory): m_def = Category(label='Perovskite Composition', categories=[EntryDataCategory]) @@ -89,6 +138,24 @@ class PerovskiteChemicalSection(ArchiveSection): shape=[], ) + def to_topology_system(self) -> System: + """ + Convert the section to a system. + + Returns: + System: The system object. + """ + atoms=optimize_molecule(self.smiles) + structural_type = 'molecule' + if len(atoms) == 1: + structural_type = 'atom' + return System( + label=self.common_name, + method='parser', + atoms=atoms, + structural_type=structural_type, + ) + class PerovskiteIonSection(PerovskiteChemicalSection): abbreviation = Quantity( @@ -220,6 +287,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.source_compound_cas_number = source_compound.cas_number self.source_compound = source_compound super().normalize(archive, logger) + # if self.smiles is not None: + # system = self.to_topology_system() + class PerovskiteAIon(PerovskiteIon, EntryData): @@ -298,7 +368,7 @@ class PerovskiteBIon(PerovskiteIon, EntryData): ) -class PerovskiteXIon(PerovskiteIon, EntryData): +class PerovskiteCIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite C Ion', @@ -478,7 +548,7 @@ class PerovskiteBIonComponent(PerovskiteIonComponent): ) -class PerovskiteXIonComponent(PerovskiteIonComponent): +class PerovskiteCIonComponent(PerovskiteIonComponent): m_def = Section( a_eln=ELNAnnotation( properties=SectionProperties( @@ -507,7 +577,7 @@ class PerovskiteXIonComponent(PerovskiteIonComponent): ) ) system = Quantity( - type=Reference(PerovskiteXIon.m_def), + type=Reference(PerovskiteCIon.m_def), description='A reference to the component system.', a_eln=dict(component='ReferenceEditQuantity'), ) @@ -596,6 +666,31 @@ class PerovskiteComposition(CompositeSystem, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], label='Perovskite Composition', + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'datetime', + 'description', + 'name', + 'lab_id', + ], + ), + order=[ + 'short_form', + 'long_form', + 'composition_estimate', + 'sample_type', + 'dimensionality', + 'band_gap', + 'a_ions', + 'b_ions', + 'c_ions', + 'impurities', + 'additives', + ] + ) + ) ) short_form = Quantity( type=str, @@ -653,8 +748,8 @@ class PerovskiteComposition(CompositeSystem, EntryData): section_def=PerovskiteBIonComponent, repeats=True, ) - x_ions = SubSection( - section_def=PerovskiteXIonComponent, + c_ions = SubSection( + section_def=PerovskiteCIonComponent, repeats=True, ) impurities = SubSection( @@ -675,7 +770,13 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: normalized. logger (BoundLogger): A structlog logger. """ - ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.x_ions + if not archive.results: + archive.results = Results() + if not archive.results.material: + archive.results.material = Material() + if not archive.results.properties: + archive.results.properties = Properties() + ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions self.components = ions if not any(ion.coefficient is None for ion in ions): coefficient_sum = sum([ion.coefficient for ion in ions]) @@ -709,7 +810,51 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: else: coefficient_str = f'{ion.coefficient:.2}' self.long_form += f'{ion.abbreviation}{coefficient_str}' + + if self.dimensionality in ['0D', '1D', '2D', '3D']: + archive.results.properties.dimensionality = self.dimensionality + if self.dimensionality == '3D': + archive.results.material.structural_type = 'bulk' + elif self.dimensionality != '0D': + archive.results.material.structural_type = self.dimensionality super().normalize(archive, logger) + topology = {} + # Add original system + parent_system = System( + label='Perovskite Composition', + description='A system describing the chemistry and components of the perovskite.', + system_relation=Relation(type='root'), + ) + + parent_system.structural_type = archive.results.material.structural_type + parent_system.chemical_formula_hill = ( + archive.results.material.chemical_formula_hill + ) + parent_system.elements = archive.results.material.elements + parent_system.chemical_formula_iupac = ( + archive.results.material.chemical_formula_iupac + ) + + add_system(parent_system, topology) + add_system_info(parent_system, topology) + + sub_systems: list[PerovskiteChemicalSection] = ions + self.impurities + self.additives + for sub_system in sub_systems: + child_system = sub_system.to_topology_system() + add_system(child_system, topology, parent_system) + add_system_info(child_system, topology) + + material = archive.m_setdefault('results.material') + for system in topology.values(): + material.m_add_sub_section(Material.topology, system) + + # topology contains an extra parent + if len(sub_systems) == len(material.topology) - 1: + for i in range(len(self.ions)): + material.topology[i + 1].chemical_formula_descriptive = self.ions[ + i + ].name + m_package.__init_metainfo__() From 9940683ebabd9ed8a5fa2813bc8075a525bb0cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Wed, 6 Nov 2024 20:51:57 +0100 Subject: [PATCH 10/22] Added conversion to nomad atoms --- src/perovskite_solar_cell_database/composition.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 5b9a625..abeb82e 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -145,7 +145,8 @@ def to_topology_system(self) -> System: Returns: System: The system object. """ - atoms=optimize_molecule(self.smiles) + ase_atoms = optimize_molecule(self.smiles) + atoms = nomad_atoms_from_ase_atoms(ase_atoms) structural_type = 'molecule' if len(atoms) == 1: structural_type = 'atom' @@ -851,7 +852,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: # topology contains an extra parent if len(sub_systems) == len(material.topology) - 1: - for i in range(len(self.ions)): + for i in range(len(sub_systems)): material.topology[i + 1].chemical_formula_descriptive = self.ions[ i ].name From b8beab5a6b9f5154e31070c35a6376a0252d68d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 08:29:50 +0100 Subject: [PATCH 11/22] Finished adding formula and topology --- .../composition.py | 88 ++++++++++++------- 1 file changed, 58 insertions(+), 30 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index abeb82e..0802e9a 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -19,6 +19,9 @@ from typing import TYPE_CHECKING from ase import Atoms +from nomad.atomutils import ( + Formula, +) from nomad.datamodel.data import ( EntryData, EntryDataCategory, @@ -39,6 +42,7 @@ PureSubstance, PureSubstanceComponent, SystemComponent, + elemental_composition_from_formula, ) from nomad.datamodel.results import ( Material, @@ -145,10 +149,12 @@ def to_topology_system(self) -> System: Returns: System: The system object. """ + if self.smiles is None: + return System(label=self.common_name) ase_atoms = optimize_molecule(self.smiles) atoms = nomad_atoms_from_ase_atoms(ase_atoms) structural_type = 'molecule' - if len(atoms) == 1: + if len(ase_atoms) == 1: structural_type = 'atom' return System( label=self.common_name, @@ -271,6 +277,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if self.common_name is None: self.common_name = pure_substance.name self.pure_substance = pure_substance + formula = self.pure_substance.molecular_formula + if isinstance(formula, str) and formula.endswith('-'): + self.pure_substance.molecular_formula = formula[:-1] source_compound = PubChemPureSubstanceSection( molecular_formula=self.source_compound_molecular_formula, smile=self.source_compound_smiles, @@ -287,9 +296,17 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if self.source_compound_cas_number is None: self.source_compound_cas_number = source_compound.cas_number self.source_compound = source_compound + super().normalize(archive, logger) - # if self.smiles is not None: - # system = self.to_topology_system() + + system = self.to_topology_system() + system.system_relation = Relation(type='root') + topology = {} + add_system(system, topology) + add_system_info(system, topology) + material = archive.m_setdefault('results.material') + for system in topology.values(): + material.m_add_sub_section(Material.topology, system) @@ -461,6 +478,8 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: return if self.abbreviation is None: self.abbreviation = self.system.abbreviation + if self.common_name is None: + self.common_name = self.system.common_name if self.molecular_formula is None: self.molecular_formula = self.system.molecular_formula if self.smiles is None: @@ -513,6 +532,11 @@ class PerovskiteAIonComponent(PerovskiteIonComponent): a_eln=dict(component='ReferenceEditQuantity'), ) + def to_topology_system(self) -> System: + system = super().to_topology_system() + system.label = 'Perovskite A Ion: ' + self.abbreviation + return system + class PerovskiteBIonComponent(PerovskiteIonComponent): m_def = Section( @@ -548,6 +572,11 @@ class PerovskiteBIonComponent(PerovskiteIonComponent): a_eln=dict(component='ReferenceEditQuantity'), ) + def to_topology_system(self) -> System: + system = super().to_topology_system() + system.label = 'Perovskite B Ion: ' + self.abbreviation + return system + class PerovskiteCIonComponent(PerovskiteIonComponent): m_def = Section( @@ -583,6 +612,11 @@ class PerovskiteCIonComponent(PerovskiteIonComponent): a_eln=dict(component='ReferenceEditQuantity'), ) + def to_topology_system(self) -> System: + system = super().to_topology_system() + system.label = 'Perovskite C Ion: ' + self.abbreviation + return system + class Impurity(PureSubstanceComponent, PerovskiteChemicalSection): m_def = Section( @@ -779,25 +813,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: archive.results.properties = Properties() ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions self.components = ions - if not any(ion.coefficient is None for ion in ions): - coefficient_sum = sum([ion.coefficient for ion in ions]) - for component in self.components: - if ( - not isinstance(component, PerovskiteIonComponent) - or not isinstance(component.system, PerovskiteIon) - or not isinstance( - component.system.pure_substance, PubChemPureSubstanceSection - ) - or component.system.pure_substance.molecular_mass is None - ): - continue - component.mass_fraction = ( - component.system.pure_substance.molecular_mass - * component.coefficient - / coefficient_sum - ) self.short_form = '' self.long_form = '' + formula_str = '' for ion in ions: if ion.abbreviation is None: continue @@ -811,6 +829,16 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: else: coefficient_str = f'{ion.coefficient:.2}' self.long_form += f'{ion.abbreviation}{coefficient_str}' + if not isinstance(ion.molecular_formula, str): + continue + cleaned_formula = ion.molecular_formula.replace('+', '').replace('-', '') + formula_str += f'({cleaned_formula}){coefficient_str}' + try: + formula = Formula(formula_str) + formula.populate(archive.results.material, overwrite=True) + self.elemental_composition = elemental_composition_from_formula(formula) + except Exception as e: + logger.warn('Could not analyse chemical formula.', exc_info=e) if self.dimensionality in ['0D', '1D', '2D', '3D']: archive.results.properties.dimensionality = self.dimensionality @@ -818,8 +846,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: archive.results.material.structural_type = 'bulk' elif self.dimensionality != '0D': archive.results.material.structural_type = self.dimensionality - super().normalize(archive, logger) + super().normalize(archive, logger) + topology = {} # Add original system parent_system = System( @@ -840,9 +869,8 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: add_system(parent_system, topology) add_system_info(parent_system, topology) - sub_systems: list[PerovskiteChemicalSection] = ions + self.impurities + self.additives - for sub_system in sub_systems: - child_system = sub_system.to_topology_system() + for ion in ions: + child_system = ion.to_topology_system() add_system(child_system, topology, parent_system) add_system_info(child_system, topology) @@ -850,12 +878,12 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: for system in topology.values(): material.m_add_sub_section(Material.topology, system) - # topology contains an extra parent - if len(sub_systems) == len(material.topology) - 1: - for i in range(len(sub_systems)): - material.topology[i + 1].chemical_formula_descriptive = self.ions[ - i - ].name + # topology contains an extra parent TODO: Check if this is necessary + # if len(ions) == len(material.topology) - 1: + # for i in range(len(ions)): + # material.topology[i + 1].chemical_formula_descriptive = ions[ + # i + # ].common_name m_package.__init_metainfo__() From dc1468a9857f32a26cf8b17c0685e4c09d39af23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 09:30:13 +0100 Subject: [PATCH 12/22] Added ion parser --- pyproject.toml | 3 +- .../__init__.py | 23 +++++++++- src/perovskite_solar_cell_database/parser.py | 41 ++++++++++++++++++ tests/data/perovskite_ions.xlsx | Bin 0 -> 44713 bytes tests/data/perovskite_ions_reduced.xlsx | Bin 0 -> 9809 bytes 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 src/perovskite_solar_cell_database/parser.py create mode 100644 tests/data/perovskite_ions.xlsx create mode 100644 tests/data/perovskite_ions_reduced.xlsx diff --git a/pyproject.toml b/pyproject.toml index 913f62f..9643058 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,4 +133,5 @@ where = ["src"] [project.entry-points.'nomad.plugin'] perovskite_solar_cell = "perovskite_solar_cell_database:perovskite_solar_cell" solar_cell_app = "perovskite_solar_cell_database.apps:solar_cells" -perovskite_composition = "perovskite_solar_cell_database:perovskite_composition" \ No newline at end of file +perovskite_composition = "perovskite_solar_cell_database:perovskite_composition" +ion_parser = "perovskite_solar_cell_database:ion_parser" \ No newline at end of file diff --git a/src/perovskite_solar_cell_database/__init__.py b/src/perovskite_solar_cell_database/__init__.py index ea7b785..168af13 100644 --- a/src/perovskite_solar_cell_database/__init__.py +++ b/src/perovskite_solar_cell_database/__init__.py @@ -1,4 +1,7 @@ -from nomad.config.models.plugins import SchemaPackageEntryPoint +from nomad.config.models.plugins import ( + ParserEntryPoint, + SchemaPackageEntryPoint, +) from pydantic import Field @@ -32,3 +35,21 @@ def load(self): name='PerovskiteComposition', description='Schema package defined for the perovskite compositions.', ) + + +class IonParserEntryPoint(ParserEntryPoint): + def load(self): + from perovskite_solar_cell_database.parser import IonParser + + return IonParser(**self.dict()) + + +ion_parser = IonParserEntryPoint( + name='PerovskiteIonParser', + description='Parse excel files containing perovskite ions.', + mainfile_name_re=r'.+\.xlsx', + mainfile_mime_re='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + mainfile_contents_dict={ + 'Sheet1': {'__has_all_keys': ['perovskite_site', 'abbreviation', 'molecular_formula', 'smiles', 'common_name', 'iupac_name', 'cas_number', 'source_compound_iupac_name', 'source_compound_smiles', 'source_compound_cas_number']}, + }, +) \ No newline at end of file diff --git a/src/perovskite_solar_cell_database/parser.py b/src/perovskite_solar_cell_database/parser.py new file mode 100644 index 0000000..23ee73c --- /dev/null +++ b/src/perovskite_solar_cell_database/parser.py @@ -0,0 +1,41 @@ +import numpy as np +import pandas as pd +from nomad.datamodel import EntryArchive +from nomad.parsing import MatchingParser + +from perovskite_solar_cell_database.composition import ( + PerovskiteAIon, + PerovskiteBIon, + PerovskiteCIon, +) +from perovskite_solar_cell_database.utils import create_archive + + +class IonParser(MatchingParser): + def parse( + self, + mainfile: str, + archive: EntryArchive, + logger=None, + child_archives: dict[str, EntryArchive] = None, + ) -> None: + df = pd.read_excel(mainfile).replace(np.nan, None) + for index, row in df.iterrows(): + if row['perovskite_site'] == 'A': + ion = PerovskiteAIon() + elif row['perovskite_site'] == 'B': + ion = PerovskiteBIon() + elif row['perovskite_site'] == 'C': + ion = PerovskiteCIon() + else: + raise ValueError(f'Unknown ion type {row["perovskite_site"]}') + ion.abbreviation = row['abbreviation'] + ion.molecular_formula = row['molecular_formula'] + ion.smiles = row['smiles'] + ion.common_name = row['common_name'] + ion.iupac_name = row['iupac_name'] + ion.cas_number = row['cas_number'] + ion.source_compound_iupac_name = row['source_compound_iupac_name'] + ion.source_compound_smiles = row['source_compound_smiles'] + ion.source_compound_cas_number = row['source_compound_cas_number'] + create_archive(ion, archive, f'{row['abbreviation']}_perovskite_ion.archive.json') diff --git a/tests/data/perovskite_ions.xlsx b/tests/data/perovskite_ions.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4f407a4978eebf5c4184eaa3b428d32b973d35f8 GIT binary patch literal 44713 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 literal 0 HcmV?d00001 diff --git a/tests/data/perovskite_ions_reduced.xlsx b/tests/data/perovskite_ions_reduced.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..738ffadc9134ec935c6bfa1b6e21af2024ae00f8 GIT binary patch literal 9809 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 literal 0 HcmV?d00001 From 19e7484380fac7f8fd8d0aeb76c044753019de84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 12:05:50 +0100 Subject: [PATCH 13/22] Separated section and entry classes and added search for abbreviation --- .../composition.py | 160 ++++++++++++------ 1 file changed, 109 insertions(+), 51 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 0802e9a..16e421c 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -16,6 +16,7 @@ # limitations under the License. # +import re from typing import TYPE_CHECKING from ase import Atoms @@ -44,7 +45,12 @@ SystemComponent, elemental_composition_from_formula, ) +from nomad.datamodel.metainfo.common import ( + ProvenanceTracker, +) from nomad.datamodel.results import ( + BandGap, + ElectronicProperties, Material, Properties, Relation, @@ -61,7 +67,15 @@ SubSection, ) from nomad.normalizing.common import nomad_atoms_from_ase_atoms -from nomad.normalizing.topology import add_system, add_system_info +from nomad.normalizing.topology import ( + add_system, + add_system_info, +) +from nomad.search import ( + MetadataPagination, + search, +) +from structlog.stdlib import BoundLogger if TYPE_CHECKING: from nomad.datamodel.datamodel import EntryArchive @@ -99,8 +113,6 @@ def optimize_molecule(smiles): ase_atoms = convert_rdkit_mol_to_ase_atoms(m) - # Further processing - # ... return ase_atoms except Exception as e: print(f'An error occurred: {e}') @@ -258,6 +270,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: normalized. logger (BoundLogger): A structlog logger. """ + self.lab_id = 'perovskite_ion_' + self.abbreviation pure_substance = PubChemPureSubstanceSection( molecular_formula=self.molecular_formula, smile=self.smiles, @@ -278,8 +291,8 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.common_name = pure_substance.name self.pure_substance = pure_substance formula = self.pure_substance.molecular_formula - if isinstance(formula, str) and formula.endswith('-'): - self.pure_substance.molecular_formula = formula[:-1] + if isinstance(formula, str): + self.pure_substance.molecular_formula = re.sub(r'(?<=[A-Za-z])\d*[+-]', '', formula) source_compound = PubChemPureSubstanceSection( molecular_formula=self.source_compound_molecular_formula, smile=self.source_compound_smiles, @@ -475,7 +488,31 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: """ super().normalize(archive, logger) if not isinstance(self.system, PerovskiteIon): - return + if self.abbreviation is None: + return + query = { + 'section_defs.definition_qualified_name:all': [ + 'perovskite_solar_cell_database.composition.PerovskiteIon' + ], + 'results.eln.lab_ids': 'perovskite_ion_' + self.abbreviation + } # TODO: Search also for smiles and molecular_formula + search_result = search( + owner='all', + query=query, + pagination=MetadataPagination( + page_size=1, + order_by='entry_create_time', + order='asc', + ), + user_id=archive.metadata.main_author.user_id, + ) + if search_result.pagination.total > 0: + entry_id = search_result.data[0]['entry_id'] + upload_id = search_result.data[0]['upload_id'] + self.system = f'../uploads/{upload_id}/archive/{entry_id}#data' + else: + logger.warn(f'Could not find system for ion {self.abbreviation}.') + return # TODO: Create ion if self.abbreviation is None: self.abbreviation = self.system.abbreviation if self.common_name is None: @@ -693,40 +730,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: super().normalize(archive, logger) -class PerovskiteComposition(CompositeSystem, EntryData): - """ - Schema for describing a perovskite composition. - """ - - m_def = Section( - categories=[PerovskiteCompositionCategory], - label='Perovskite Composition', - a_eln=ELNAnnotation( - properties=SectionProperties( - visible=Filter( - exclude=[ - 'datetime', - 'description', - 'name', - 'lab_id', - ], - ), - order=[ - 'short_form', - 'long_form', - 'composition_estimate', - 'sample_type', - 'dimensionality', - 'band_gap', - 'a_ions', - 'b_ions', - 'c_ions', - 'impurities', - 'additives', - ] - ) - ) - ) +class PerovskiteCompositionSection(ArchiveSection): short_form = Quantity( type=str, ) @@ -798,7 +802,7 @@ class PerovskiteComposition(CompositeSystem, EntryData): def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: """ - The normalizer for the `PerovskiteComposition` class. + The normalizer for the `PerovskiteCompositionSection` class. Args: archive (EntryArchive): The archive containing the section that is being @@ -812,7 +816,6 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if not archive.results.properties: archive.results.properties = Properties() ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions - self.components = ions self.short_form = '' self.long_form = '' formula_str = '' @@ -831,12 +834,11 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.long_form += f'{ion.abbreviation}{coefficient_str}' if not isinstance(ion.molecular_formula, str): continue - cleaned_formula = ion.molecular_formula.replace('+', '').replace('-', '') + cleaned_formula = re.sub(r'(?<=[A-Za-z])\d*[+-]', '', ion.molecular_formula) formula_str += f'({cleaned_formula}){coefficient_str}' try: formula = Formula(formula_str) formula.populate(archive.results.material, overwrite=True) - self.elemental_composition = elemental_composition_from_formula(formula) except Exception as e: logger.warn('Could not analyse chemical formula.', exc_info=e) @@ -850,7 +852,6 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: super().normalize(archive, logger) topology = {} - # Add original system parent_system = System( label='Perovskite Composition', description='A system describing the chemistry and components of the perovskite.', @@ -878,12 +879,69 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: for system in topology.values(): material.m_add_sub_section(Material.topology, system) - # topology contains an extra parent TODO: Check if this is necessary - # if len(ions) == len(material.topology) - 1: - # for i in range(len(ions)): - # material.topology[i + 1].chemical_formula_descriptive = ions[ - # i - # ].common_name + if self.band_gap is not None: + archive.results.properties.electronic = ElectronicProperties( + band_gap=[ + BandGap( + value=self.band_gap, + provenance=ProvenanceTracker(label='perovskite_composition'), + ) + ] + ) + + +class PerovskiteComposition(PerovskiteCompositionSection, CompositeSystem, EntryData): + """ + Schema for describing a perovskite composition. + """ + + m_def = Section( + categories=[PerovskiteCompositionCategory], + label='Perovskite Composition', + a_eln=ELNAnnotation( + properties=SectionProperties( + visible=Filter( + exclude=[ + 'datetime', + 'description', + 'name', + 'lab_id', + ], + ), + order=[ + 'short_form', + 'long_form', + 'composition_estimate', + 'sample_type', + 'dimensionality', + 'band_gap', + 'a_ions', + 'b_ions', + 'c_ions', + 'impurities', + 'additives', + ] + ) + ) + ) + + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: + """ + The normalizer for the `PerovskiteComposition` class. + + Args: + archive (EntryArchive): The archive containing the section that is being + normalized. + logger (BoundLogger): A structlog logger. + """ + super().normalize(archive, logger) + self.components = self.a_ions + self.b_ions + self.c_ions + results: Results = archive.results + material: Material = results.material + if material.chemical_formula_iupac is not None: + self.elemental_composition = elemental_composition_from_formula( + Formula(material.chemical_formula_iupac) + ) m_package.__init_metainfo__() From 743116e35d2f1d4aed3dce177843fcbf9c1f8dfb Mon Sep 17 00:00:00 2001 From: Pepe Marquez <64071335+Pepe-Marquez@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:22:26 +0100 Subject: [PATCH 14/22] Update install_this_plugin.md --- docs/how_to/install_this_plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to/install_this_plugin.md b/docs/how_to/install_this_plugin.md index 2b45e0f..6a2dec6 100644 --- a/docs/how_to/install_this_plugin.md +++ b/docs/how_to/install_this_plugin.md @@ -1,4 +1,4 @@ # Install This Plugin !!! note "Attention" - TODO + TODO From ac0a4018138ad3849dec1638bf90ccaca90d48a3 Mon Sep 17 00:00:00 2001 From: Pepe Marquez <64071335+Pepe-Marquez@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:28:16 +0100 Subject: [PATCH 15/22] Update install_this_plugin.md --- docs/how_to/install_this_plugin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how_to/install_this_plugin.md b/docs/how_to/install_this_plugin.md index 6a2dec6..2b45e0f 100644 --- a/docs/how_to/install_this_plugin.md +++ b/docs/how_to/install_this_plugin.md @@ -1,4 +1,4 @@ # Install This Plugin !!! note "Attention" - TODO + TODO From 7b3b89368df10109c2d2b3a747affc893484d7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 14:17:57 +0100 Subject: [PATCH 16/22] Changed C back to X ion and parser bug fix --- .../composition.py | 20 +++++++++---------- src/perovskite_solar_cell_database/parser.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 16e421c..2784053 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -399,10 +399,10 @@ class PerovskiteBIon(PerovskiteIon, EntryData): ) -class PerovskiteCIon(PerovskiteIon, EntryData): +class PerovskiteXIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], - label='Perovskite C Ion', + label='Perovskite X Ion', a_eln=ELNAnnotation( properties=SectionProperties( visible=Filter( @@ -615,7 +615,7 @@ def to_topology_system(self) -> System: return system -class PerovskiteCIonComponent(PerovskiteIonComponent): +class PerovskiteXIonComponent(PerovskiteIonComponent): m_def = Section( a_eln=ELNAnnotation( properties=SectionProperties( @@ -644,7 +644,7 @@ class PerovskiteCIonComponent(PerovskiteIonComponent): ) ) system = Quantity( - type=Reference(PerovskiteCIon.m_def), + type=Reference(PerovskiteXIon.m_def), description='A reference to the component system.', a_eln=dict(component='ReferenceEditQuantity'), ) @@ -779,16 +779,16 @@ class PerovskiteCompositionSection(ArchiveSection): unit='eV', shape=[], ) - a_ions = SubSection( + ions_a_site = SubSection( section_def=PerovskiteAIonComponent, repeats=True, ) - b_ions = SubSection( + ions_b_site = SubSection( section_def=PerovskiteBIonComponent, repeats=True, ) - c_ions = SubSection( - section_def=PerovskiteCIonComponent, + ions_x_site = SubSection( + section_def=PerovskiteXIonComponent, repeats=True, ) impurities = SubSection( @@ -815,7 +815,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: archive.results.material = Material() if not archive.results.properties: archive.results.properties = Properties() - ions: list[PerovskiteIonComponent] = self.a_ions + self.b_ions + self.c_ions + ions: list[PerovskiteIonComponent] = self.ions_a_site + self.ions_b_site + self.ions_x_site self.short_form = '' self.long_form = '' formula_str = '' @@ -935,7 +935,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: logger (BoundLogger): A structlog logger. """ super().normalize(archive, logger) - self.components = self.a_ions + self.b_ions + self.c_ions + self.components = self.ions_a_site + self.ions_b_site + self.ions_x_site results: Results = archive.results material: Material = results.material if material.chemical_formula_iupac is not None: diff --git a/src/perovskite_solar_cell_database/parser.py b/src/perovskite_solar_cell_database/parser.py index 23ee73c..e51241c 100644 --- a/src/perovskite_solar_cell_database/parser.py +++ b/src/perovskite_solar_cell_database/parser.py @@ -6,7 +6,7 @@ from perovskite_solar_cell_database.composition import ( PerovskiteAIon, PerovskiteBIon, - PerovskiteCIon, + PerovskiteXIon, ) from perovskite_solar_cell_database.utils import create_archive @@ -25,8 +25,8 @@ def parse( ion = PerovskiteAIon() elif row['perovskite_site'] == 'B': ion = PerovskiteBIon() - elif row['perovskite_site'] == 'C': - ion = PerovskiteCIon() + elif row['perovskite_site'] == 'X': + ion = PerovskiteXIon() else: raise ValueError(f'Unknown ion type {row["perovskite_site"]}') ion.abbreviation = row['abbreviation'] @@ -38,4 +38,4 @@ def parse( ion.source_compound_iupac_name = row['source_compound_iupac_name'] ion.source_compound_smiles = row['source_compound_smiles'] ion.source_compound_cas_number = row['source_compound_cas_number'] - create_archive(ion, archive, f'{row['abbreviation']}_perovskite_ion.archive.json') + create_archive(ion, archive, f'{row["abbreviation"]}_perovskite_ion.archive.json') From ab36a2d947b0988068fd6cf386de3e4f923a886e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 14:31:30 +0100 Subject: [PATCH 17/22] Updated toml --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9643058..abb8b8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ authors = [ ] maintainers = [ {name = "Jose Marquez", email = "jose.marquez@physik.hu-berlin.de"}, - {name = "Yaru Wang", email = "wangyaru@physik.hu-berlin.de"}, + {name = "Hampus Näsström", email = "hampus.naesstroem@physik.hu-berlin.de"}, ] requires-python = ">=3.9" dependencies = [ @@ -36,7 +36,6 @@ dependencies = [ "rdkit", "openpyxl", "lxml_html_clean", - "nomad-lab>=1.3.4", ] license = { file = "LICENSE" } From 64b6b757c6b806f630091a626db90558b23a688b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 14:36:46 +0100 Subject: [PATCH 18/22] Removing trailing comma --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index abb8b8b..1a10909 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "nomad-schema-plugin-simulation-workflow>=1.0.1", "rdkit", "openpyxl", - "lxml_html_clean", + "lxml_html_clean" ] license = { file = "LICENSE" } From f001e0a4fa2071eea79aa00cf20cb8a15b73d2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 14:43:36 +0100 Subject: [PATCH 19/22] Revert action changes --- .github/workflows/actions.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 1cfb621..dc87590 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -73,10 +73,10 @@ jobs: args: "check ." # to enable auto-formatting check, uncomment the following lines below - # ruff-formatting: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: chartboost/ruff-action@v1 - # with: - # args: "format . --check" + ruff-formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 + with: + args: "format . --check" From a010cf248d10c770f25aabb91d606396c4e56bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 14:50:26 +0100 Subject: [PATCH 20/22] Added author --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1a10909..e08493f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ readme = "README.md" authors = [ {name = "Jose Marquez", email = "jose.marquez@physik.hu-berlin.de"}, {name = "Yaru Wang", email = "wangyaru@physik.hu-berlin.de"}, + {name = "Hampus Näsström", email = "hampus.naesstroem@physik.hu-berlin.de"}, ] maintainers = [ {name = "Jose Marquez", email = "jose.marquez@physik.hu-berlin.de"}, From b9709a9a0573395809e1a8c88056f0718237b04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 15:28:31 +0100 Subject: [PATCH 21/22] Moved search imports --- src/perovskite_solar_cell_database/composition.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 2784053..0ecd7d0 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -71,10 +71,6 @@ add_system, add_system_info, ) -from nomad.search import ( - MetadataPagination, - search, -) from structlog.stdlib import BoundLogger if TYPE_CHECKING: @@ -490,6 +486,10 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if not isinstance(self.system, PerovskiteIon): if self.abbreviation is None: return + from nomad.search import ( + MetadataPagination, + search, + ) query = { 'section_defs.definition_qualified_name:all': [ 'perovskite_solar_cell_database.composition.PerovskiteIon' From 47318383c46c11ced267df6f3c282315cef76473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hampus=20N=C3=A4sstr=C3=B6m?= Date: Thu, 7 Nov 2024 15:44:34 +0100 Subject: [PATCH 22/22] Ruff formatting --- .../__init__.py | 19 +++++-- .../composition.py | 54 ++++++++++--------- src/perovskite_solar_cell_database/parser.py | 4 +- src/perovskite_solar_cell_database/utils.py | 2 +- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/perovskite_solar_cell_database/__init__.py b/src/perovskite_solar_cell_database/__init__.py index 168af13..f8e759f 100644 --- a/src/perovskite_solar_cell_database/__init__.py +++ b/src/perovskite_solar_cell_database/__init__.py @@ -29,7 +29,7 @@ def load(self): ) return m_package - + perovskite_composition = PerovskiteCompositionEntryPoint( name='PerovskiteComposition', @@ -50,6 +50,19 @@ def load(self): mainfile_name_re=r'.+\.xlsx', mainfile_mime_re='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', mainfile_contents_dict={ - 'Sheet1': {'__has_all_keys': ['perovskite_site', 'abbreviation', 'molecular_formula', 'smiles', 'common_name', 'iupac_name', 'cas_number', 'source_compound_iupac_name', 'source_compound_smiles', 'source_compound_cas_number']}, + 'Sheet1': { + '__has_all_keys': [ + 'perovskite_site', + 'abbreviation', + 'molecular_formula', + 'smiles', + 'common_name', + 'iupac_name', + 'cas_number', + 'source_compound_iupac_name', + 'source_compound_smiles', + 'source_compound_cas_number', + ] + }, }, -) \ No newline at end of file +) diff --git a/src/perovskite_solar_cell_database/composition.py b/src/perovskite_solar_cell_database/composition.py index 0ecd7d0..2be94c0 100644 --- a/src/perovskite_solar_cell_database/composition.py +++ b/src/perovskite_solar_cell_database/composition.py @@ -234,7 +234,7 @@ class PerovskiteIon(PureSubstance, PerovskiteIonSection): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) ) ) @@ -288,7 +288,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.pure_substance = pure_substance formula = self.pure_substance.molecular_formula if isinstance(formula, str): - self.pure_substance.molecular_formula = re.sub(r'(?<=[A-Za-z])\d*[+-]', '', formula) + self.pure_substance.molecular_formula = re.sub( + r'(?<=[A-Za-z])\d*[+-]', '', formula + ) source_compound = PubChemPureSubstanceSection( molecular_formula=self.source_compound_molecular_formula, smile=self.source_compound_smiles, @@ -307,7 +309,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: self.source_compound = source_compound super().normalize(archive, logger) - + system = self.to_topology_system() system.system_relation = Relation(type='root') topology = {} @@ -318,7 +320,6 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: material.m_add_sub_section(Material.topology, system) - class PerovskiteAIon(PerovskiteIon, EntryData): m_def = Section( categories=[PerovskiteCompositionCategory], @@ -351,9 +352,9 @@ class PerovskiteAIon(PerovskiteIon, EntryData): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) - ) + ), ) @@ -389,9 +390,9 @@ class PerovskiteBIon(PerovskiteIon, EntryData): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) - ) + ), ) @@ -427,9 +428,9 @@ class PerovskiteXIon(PerovskiteIon, EntryData): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) - ) + ), ) @@ -457,7 +458,7 @@ class PerovskiteIonComponent(SystemComponent, PerovskiteIonSection): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) ) ) @@ -490,16 +491,17 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: MetadataPagination, search, ) + query = { 'section_defs.definition_qualified_name:all': [ 'perovskite_solar_cell_database.composition.PerovskiteIon' ], - 'results.eln.lab_ids': 'perovskite_ion_' + self.abbreviation + 'results.eln.lab_ids': 'perovskite_ion_' + self.abbreviation, } # TODO: Search also for smiles and molecular_formula search_result = search( owner='all', query=query, - pagination=MetadataPagination( + pagination=MetadataPagination( page_size=1, order_by='entry_create_time', order='asc', @@ -526,7 +528,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: if self.cas_number is None: self.cas_number = self.system.cas_number if self.source_compound_molecular_formula is None: - self.source_compound_molecular_formula = self.system.source_compound_molecular_formula + self.source_compound_molecular_formula = ( + self.system.source_compound_molecular_formula + ) if self.source_compound_smiles is None: self.source_compound_smiles = self.system.source_compound_smiles if self.source_compound_iupac_name is None: @@ -559,7 +563,7 @@ class PerovskiteAIonComponent(PerovskiteIonComponent): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) ) ) @@ -599,7 +603,7 @@ class PerovskiteBIonComponent(PerovskiteIonComponent): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) ) ) @@ -639,7 +643,7 @@ class PerovskiteXIonComponent(PerovskiteIonComponent): 'source_compound_smiles', 'source_compound_iupac_name', 'source_compound_cas_number', - ] + ], ) ) ) @@ -674,7 +678,7 @@ class Impurity(PureSubstanceComponent, PerovskiteChemicalSection): 'smiles', 'iupac_name', 'cas_number', - ] + ], ) ) ) @@ -699,7 +703,7 @@ class Impurity(PureSubstanceComponent, PerovskiteChemicalSection): Section describing the pure substance that is the component. """, ) - + def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: """ The normalizer for the `Impurity` class. @@ -815,7 +819,9 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: archive.results.material = Material() if not archive.results.properties: archive.results.properties = Properties() - ions: list[PerovskiteIonComponent] = self.ions_a_site + self.ions_b_site + self.ions_x_site + ions: list[PerovskiteIonComponent] = ( + self.ions_a_site + self.ions_b_site + self.ions_x_site + ) self.short_form = '' self.long_form = '' formula_str = '' @@ -850,7 +856,7 @@ def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: archive.results.material.structural_type = self.dimensionality super().normalize(archive, logger) - + topology = {} parent_system = System( label='Perovskite Composition', @@ -905,7 +911,7 @@ class PerovskiteComposition(PerovskiteCompositionSection, CompositeSystem, Entry 'datetime', 'description', 'name', - 'lab_id', + 'lab_id', ], ), order=[ @@ -920,9 +926,9 @@ class PerovskiteComposition(PerovskiteCompositionSection, CompositeSystem, Entry 'c_ions', 'impurities', 'additives', - ] + ], ) - ) + ), ) def normalize(self, archive: 'EntryArchive', logger: 'BoundLogger') -> None: diff --git a/src/perovskite_solar_cell_database/parser.py b/src/perovskite_solar_cell_database/parser.py index e51241c..3e5743a 100644 --- a/src/perovskite_solar_cell_database/parser.py +++ b/src/perovskite_solar_cell_database/parser.py @@ -38,4 +38,6 @@ def parse( ion.source_compound_iupac_name = row['source_compound_iupac_name'] ion.source_compound_smiles = row['source_compound_smiles'] ion.source_compound_cas_number = row['source_compound_cas_number'] - create_archive(ion, archive, f'{row["abbreviation"]}_perovskite_ion.archive.json') + create_archive( + ion, archive, f'{row["abbreviation"]}_perovskite_ion.archive.json' + ) diff --git a/src/perovskite_solar_cell_database/utils.py b/src/perovskite_solar_cell_database/utils.py index a843ad1..9bdfb7e 100644 --- a/src/perovskite_solar_cell_database/utils.py +++ b/src/perovskite_solar_cell_database/utils.py @@ -41,4 +41,4 @@ def create_archive(entity, archive, file_name) -> str: archive.m_context.process_updated_raw_file(file_name) return get_reference( archive.metadata.upload_id, get_entry_id_from_file_name(file_name, archive) - ) \ No newline at end of file + )