Skip to content

Implement mapping parser for phonopy - #264

Draft
ladinesa wants to merge 7 commits into
developfrom
phonopy-parser
Draft

ladinesa wants to merge 7 commits into
developfrom
phonopy-parser

Conversation

@ladinesa

@ladinesa ladinesa commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator
  • Write phonopy properties to custom phonopy schema.
  • Use mapping parser for phonopy
  • Make fhiaims use also mapping for phonon archive

@ladinesa
ladinesa requested a balanced review from Copilot October 8, 2026 09:21
@ladinesa
ladinesa marked this pull request as draft October 8, 2026 09:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Calculator-dependent unit errors and several parser failure paths can produce inaccurate results or abort valid partial parses.

8 open findings
What changed in this PR

Implements annotation-driven Phonopy parsing into the simulation schema.

Changes:

  • Adds mappings for systems, methods, force constants, bands, DOS, and thermodynamics.
  • Introduces a mapping-based archive writer.
  • Expands unit and integration coverage.
File Description
src/​nomad_simulation_parsers/​parsers/​phonopy/​parser.py Adds mapping transforms and output generation.
src/​nomad_simulation_parsers/​schema_packages/​phonopy.py Defines Phonopy schema mappings.
tests/​parsers/​phonopy/​test_mapping.py Tests mapping behavior.
tests/​parsers/​phonopy/​test_recognition.py Tests YAML loading and cached transforms.
tests/​parsers/​phonopy/​test_integration.py Verifies mapped archive outputs.
tests/​parsers/​phonopy/​test_archive_writer.py Extends legacy writer tests.
tests/​parsers/​phonopy/​conftest.py Routes fixtures through the public parser.
tests/​parsers/​phonopy/​README.md Updates the test matrix.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

if force_constants is None:
obj.produce_force_constants()
force_constants = obj.force_constants
return [{'value': force_constants * ureg.eV / ureg.angstrom**2}]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Comment on lines +158 to +160
'n_bands': segment['frequencies'].shape[-1],
'value': segment['frequencies'] * ureg.joule,
'endpoints_labels': [str(label) for label in segment['labels']],
Comment on lines +432 to +436
force_constants = phonopy_obj.force_constants
force_constants = (
(force_constants * ureg.eV / ureg.angstrom**2).to('J/(m**2)').magnitude
)
if force_constants is None:
phonopy_obj.produce_force_constants()
force_constants = phonopy_obj.force_constants
force_constants = force_constants * ureg.eV / ureg.angstrom**2
Comment on lines +44 to +45
_phonon_properties: PhononProperties | None = None
_thermodynamic_properties: dict[str, list[dict[str, Any]]] | None = None

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is ok since assigning a new data object will load the file again.

Comment on lines +143 to +147
force_constants = obj.force_constants
if force_constants is None:
obj.produce_force_constants()
force_constants = obj.force_constants
return [{'value': force_constants * ureg.eV / ureg.angstrom**2}]
Comment on lines +550 to +556
if self.archive.data.outputs:
self.archive.data.outputs[
0
].model_system_ref = self.archive.data.model_system[-1]
self.archive.data.outputs[
0
].model_method_ref = self.archive.data.model_method[0]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

swtiched to mapping

Comment on lines +547 to +549
# References are intentionally linked after conversion: the generic
# mapper cannot resolve object references between freshly-created list
# elements from a plain source dictionary.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

switched to mapping

@VonaCecilia VonaCecilia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The PR looks overall complete and well structure. I have owever a few clarification to ask:

  • is there a particular reason why some of these quantities and sections are defined specifically in the Phonopy plugin rather than in the general nomad-simulations schema? For example, properties like force constants or phonon band structures could also be relevant to other parsers. Is there a plan to move them to the general schema later, or to reuse existing components where possible?
  • Do we plan to account for the fact that the units of quantities in Phonopy output files can vary depending on the calculator used and read them from the output where they are specified? In general we should be carefull with the unit in this PR.
  • For what I see the tests do not cover some end-to-end quantities like frequency of the dos, temperrature, etc. In my local test they are not populated properly, is it possible to verify it?
  • I did not go deep into calculator.py since it is not part of the PR, however I have notice that we need to run some phonopy script to get properties like the thermal one, and there are some paramters to set that determine the accuracy of the results. When the file are not present, do we get them usining the Phonopy API, right? how do these parameters are considered?

Comment on lines 281 to 295

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not directly related to the PR, but I think we should verify the normalization of the thermodynamic properties.

According to the Phonopy documentation, free energy is reported in kJ/mol and heat capacity in J/K/mol, where one mole refers to Avogadro's number of primitive cells.

Since we want the total free energy in J and the total heat capacity in J/K, both quantities should be scaled accordingly. The conversion to eV and eV/K per primitive cell is already performed in get_thermodynamical_properties(), so the remaining conversions should be:

scale = n_atoms_supercell / n_atoms_primitive
free_energy = free_energy * scale * eV_to_J
heat_capacity = heat_capacity * scale * eV_to_J

Currently, different scaling factors are used for free energy and heat capacity.

Also, I would rename n_atoms to n_atoms_unitcell for clarity, since it refers to the input unit cell rather than necessarily the primitive cell.

],
'representations': [
{
'lattice_vectors': atoms.cell * ureg.angstrom,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

According to the Phonopy documentation, the length units depend on the calculator used to generate the input files. Therefore, the assumption that positions and lattice vectors are expressed in Ångström is only correct for certain calculators, such as VASP, CRYSTAL, and CP2K. Is the conversion to Ångström performed elsewhere?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The same for the rest of the Ångström units below.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we determine the units once when loading the file and reuse them throughout the parser, rather than hardcoding the units in each transformer? The YAML file already provides unit information, and this would make the parser more consistent across different calculators. We should also check which conversions are already performed by phonopy.load().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I always thought that phonopy is working on ase atoms objects, I will check,

return [
{
'name': 'harmonic lattice dynamics',
'type': 'finite displacement',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe we could rename this method to get_finite_displacement_method() to make its purpose more explicit, since it specifically creates the dictionary for a finite-displacement calculation.

if force_constants is None:
obj.produce_force_constants()
force_constants = obj.force_constants
return [{'value': force_constants * ureg.eV / ureg.angstrom**2}]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

return sec_system


def _populate_bandstructures(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we have both this and get_phonon_band_structure? Is it needed to maintain some older part of the code?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes it is the # 3 in TODO: fhiaims still using the old api

Comment on lines +119 to +126
class QPath(Variables):
"""Reciprocal-space path points in fractional coordinates."""

points = Quantity(type=np.float64, shape=['n_points', 3])
add_mapping_annotation(points, PHONOPY_KEY, '.points')


class PhononBandStructure(PhysicalProperty):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to align this schema more closely with the existing ElectronicBandStructure in nomad-simulations? Both represent band values along reciprocal-space paths and have similar requirements. Using a consistent structure for the paths, labels, and values could simplify plotting and make the data easier to work with across different parsers.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes absolutely, would it be possible for you to work on the phonon schema in nomad-simulations in parallel? The I can make the switch also in this pr.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it's best to discuss this in the next area C meeting!!

monkeypatch.setattr(
parser_module, 'PhononProperties', lambda *args, **kwargs: properties
)
monkeypatch.setattr(parser_module, 'get_bandstructures', lambda _: [])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it intentional that get_bandstructures() returns an empty list here?

assert len(archive.data.outputs) == 1
assert archive.data.outputs[0].model_method_ref is archive.data.model_method[0]
assert archive.data.outputs[0].force_constants[0].value.shape == (2, 2, 3, 3)
assert len(archive.data.outputs[0].phonon_dos) == 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would also add assertions for the DOS quantities to verify that the frequencies are correctly populated in the archive.

@pytest.mark.integration
def test_cp2k_fixture_band_segments_for_archive_writer(cp2k_phonopy_object):
properties = PhononProperties(cp2k_phonopy_object, get_logger(__name__), k_mesh=2)
def test_cp2k_fixture_band_segments_for_archive_writer(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test only verifies the output of get_bandstructure(), but not whether the band structure data is correctly populated in the NOMAD archive, right?

assert len(outputs.phonon_band_structures) == 9
assert len(outputs.phonon_dos) == 1
assert len(outputs.vibrational_free_energies) == 1
assert outputs.model_system_ref is simulation.model_system[1]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to also check the heat capacity, temperature points, and band structure and DOS quantities? This would help verify that the data is actually populated in the archive. At least, I would add an assertion for the heat capacity, since we already check the free energy.

@ladinesa

Copy link
Copy Markdown
Collaborator Author

The PR looks overall complete and well structure. I have owever a few clarification to ask:

* is there a particular reason why some of these quantities and sections are defined specifically in the Phonopy plugin rather than in the general nomad-simulations schema? For example, properties like force constants or phonon band structures could also be relevant to other parsers. Is there a plan to move them to the general schema later, or to reuse existing components where possible?

* Do we plan to account for the fact that the units of quantities in Phonopy output files can vary depending on the calculator used and read them from the output where they are specified? In general we should be carefull with the unit in this PR.

* For what I see the tests do not cover some end-to-end quantities like frequency of the dos, temperrature, etc. In my local test they are not populated properly, is it possible to verify it?

* I did not go deep into calculator.py since it is not part of the PR, however I have notice that we need to run some phonopy script to get properties like the thermal one, and there are some paramters to set that determine the accuracy of the results. When the file are not present, do we get them usining the Phonopy API, right? how do these parameters are considered?
  1. This is just a transient fix while waiting for the definition to be formalized in nomad-simulations.
  2. Yes, I agree will go through all units to verify this.
  3. I will add tests for these.
  4. The calculator only needs a phonopy obj, which for a phonopy yaml file is read via the phonopy reader together with the force constants file, and for fhiaims is constructed by reading the forces and parameters from the fhiaims outputs. There are no parameters injected by the parser.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants