Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Outputs base class #29

Merged
merged 13 commits into from
Apr 2, 2024
163 changes: 139 additions & 24 deletions src/nomad_simulations/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,153 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
# 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.
#

import re
import numpy as np
from typing import Optional
from structlog.stdlib import BoundLogger

from nomad.units import ureg
from nomad.datamodel.data import ArchiveSection
from nomad.metainfo import Quantity, SubSection, SectionProxy, MEnum
from nomad.datamodel.metainfo.annotations import ELNAnnotation
from nomad.metainfo import Quantity, SubSection, SectionProxy, Reference

from .atoms_state import AtomsState, OrbitalsState
from .model_system import ModelSystem
from .numerical_settings import SelfConsistency


class Outputs(ArchiveSection):
""" """
class BaseOutputs(ArchiveSection):
"""
Base section to define the output properties of a simulation. This is used as a placeholder for both
final `Outputs` properties and the self-consistent (SCF) steps, see `Outputs` base section definition.
"""

# TODO add time quantities

normalizer_level = 2

name = Quantity(
type=str,
description="""
Name of the output property. This is used for easier identification of the property and is conneceted
with the class name of each output property class, e.g., `'ElectronicBandGap'`, `'ElectronicBandStructure'`, etc.
""",
a_eln=ELNAnnotation(component='StringEditQuantity'),
)

orbitals_state_ref = Quantity(
type=OrbitalsState,
description="""
Reference to the `OrbitalsState` section on which the simulation is performed and the
output property is calculated.
""",
)

atoms_state_ref = Quantity(
type=AtomsState,
description="""
Reference to the `AtomsState` section on which the simulation is performed and the
output property is calculated.
""",
)

model_system_ref = Quantity(
type=ModelSystem,
description="""
Reference to the `ModelSystem` section on which the simulation is performed and the
output property is calculated.
""",
)

is_derived = Quantity(
type=bool,
default=False,
description="""
Flag indicating whether the output property is derived from other output properties. We make
the distinction between directly parsed, derived, and post-processing output properties:
- Directly parsed: the output property is directly parsed from the simulation output files.
- Derived: the output property is derived from other output properties. No extra numerical settings
are required to calculate the output property.
- Post-processing: the output property is derived from other output properties. Extra numerical settings
are required to calculate the output property.
""",
)
ndaelman-hu marked this conversation as resolved.
Show resolved Hide resolved

outputs_ref = Quantity(
type=Reference(SectionProxy('BaseOutputs')),
description="""
Reference to the `BaseOutputs` section from which the output property was derived. This is only
relevant if `is_derived` is set to True.
""",
)

def check_is_derived(self, is_derived: bool, outputs_ref) -> Optional[bool]:
if not is_derived:
if outputs_ref is not None:
return True
return False
elif is_derived and outputs_ref is not None:
return True
return None

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I follow the logic here (maybe also add a small pydocs):

This function is used to set is_derived, correct?
If is_derived is already set (i.e. is not None), only then it should trigger.

Moreover, if outputs_ref fully covers is_derived, then you don't need the latter.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more point, I'm going to ask Area D for "write-protected" quantities, i.e. if the value has been set, it can't be updated. This would help make the normalization leaner.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because it might be missing outputs_ref, hence the None return describes the "missing refs" case. I added a better description after you reviewed it, sorry.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, but let's run over the possible scenarios:

  1. both outputs_ref and is_derived are set, no issue
  2. only outputs_ref is set: you can set is_derived = True, but no additional information is conveyed
  3. is_derived = True and outputs_ref = None: when would this happen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For number 3., maybe some wrong setting of is_derived or some missing outputs_ref when normalizing? In this case, return None is happening

def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)
self.logger = logger

check_derived = self.check_is_derived(self.is_derived, self.outputs_ref)
if check_derived is not None:
self.is_derived = check_derived
else:
logger.error(
'A derived output property must have a reference to another `Outputs` section.'
)
return


class Outputs(BaseOutputs):
"""
Output properties of a simulation.
JosePizarro3 marked this conversation as resolved.
Show resolved Hide resolved

# ! add more description once we defined the output properties
"""

n_scf_steps = Quantity(
JosePizarro3 marked this conversation as resolved.
Show resolved Hide resolved
type=np.int32,
description="""
Number of self-consistent steps to converge the output property.
""",
)

scf_step = SubSection(
sub_section=BaseOutputs.m_def,
repeats=True,
description="""
Self-consistent (SCF) steps performed for converging a given output property.
""",
)

is_converged = Quantity(
type=bool,
description="""
Flag indicating whether the output property is converged or not. This quantity is connected
with `SelfConsistency` defined in the `numerical_settings.py` module.
""",
)

self_consistency_ref = Quantity(
type=SelfConsistency,
description="""
Reference to the `SelfConsistency` section that defines the numerical settings to converge the
output property.
""",
)
JosePizarro3 marked this conversation as resolved.
Show resolved Hide resolved

# ? Can we add more functionality to automatically check convergence from `self_consistency_ref` and the last `scf_step[-1]`
JosePizarro3 marked this conversation as resolved.
Show resolved Hide resolved
def check_is_converged(self, is_converged: bool, logger: BoundLogger) -> bool:
if not is_converged:
logger.info('The output property is not converged.')
JosePizarro3 marked this conversation as resolved.
Show resolved Hide resolved
return False
return True

def normalize(self, archive, logger) -> None:
super().normalize(archive, logger)

self.is_converged = self.check_is_converged(self.is_converged, logger)
22 changes: 22 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#
# 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 nomad import utils


logger = utils.get_logger(__name__)
73 changes: 73 additions & 0 deletions tests/test_outputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#
# 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.
#

import pytest

from . import logger

from nomad_simulations.outputs import BaseOutputs, Outputs


class TestBaseOutputs:
"""
Test the `BaseOutputs` class defined in `outputs.py`.
"""

@pytest.mark.parametrize(
'is_derived, outputs_ref, result',
[
(False, BaseOutputs(), True),
(False, None, False),
(True, BaseOutputs(), True),
(True, None, None),
],
)
def test_normalize(self, is_derived, outputs_ref, result):
"""
Test the `normalize` and `check_is_derived` methods.
"""
outputs = BaseOutputs()
assert outputs.check_is_derived(is_derived, outputs_ref) == result
outputs.is_derived = is_derived
outputs.outputs_ref = outputs_ref
outputs.normalize(None, logger)
if result is not None:
assert outputs.is_derived == result


class TestOutputs:
"""
Test the `Outputs` class defined in `outputs.py`.
"""

@pytest.mark.parametrize(
'is_converged, result',
[
(False, False),
(True, True),
],
)
def test_normalize(self, is_converged, result):
"""
Test the `normalize` method.
"""
outputs = Outputs()
assert outputs.check_is_converged(is_converged, logger) == result
outputs.is_converged = is_converged
outputs.normalize(None, logger)
assert outputs.is_converged == result
ndaelman-hu marked this conversation as resolved.
Show resolved Hide resolved
Loading