-
Notifications
You must be signed in to change notification settings - Fork 160
feat(extxyz): add unit convert and tag synonym matching #678
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3a7171c
quipGapXYZ: add unit convert and synonym matching
SchrodingersCattt 974c1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] da807b8
style: add Exception after except
SchrodingersCattt 26c6156
style: add after
SchrodingersCattt 37b96b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] aebec7c
fix: removed redundant
SchrodingersCattt 81a27d8
style: add Exception after except
SchrodingersCattt 20a264d
Merge remote-tracking branch 'upstream/master' into pr-678
SchrodingersCattt 13941d2
feat(extxyz): add table-driven unit conversion for energy, force, and…
SchrodingersCattt 4210ff6
fix(extxyz): convert virial with energy-unit factor and add au/a.u. a…
SchrodingersCattt a52b3e9
feat(extxyz): log unit conversions when factor != 1.0
SchrodingersCattt 36c846d
fix(extxyz): add au/a.u. stress-unit aliases and Pa/kPa support
SchrodingersCattt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| """Unit conversion helpers for extended XYZ (extxyz) format. | ||
|
|
||
| This module provides a table-driven approach to convert energy, force, and | ||
| stress/pressure values from various unit systems commonly found in extxyz | ||
| files into dpdata's internal units: | ||
|
|
||
| - Energy: eV | ||
| - Force: eV/angstrom | ||
| - Stress/Pressure: eV/angstrom^3 (before volume multiplication to get virial) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dpdata.unit import EnergyConversion, ForceConversion, PressureConversion | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Unit alias mapping tables | ||
| # Keys are LOWERCASE; values are canonical names recognized by dpdata.unit | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| _ENERGY_UNIT_MAP: dict[str, str] = { | ||
| "ev": "eV", | ||
| "hartree": "hartree", | ||
| "ha": "hartree", | ||
| "au": "hartree", | ||
| "a.u.": "hartree", | ||
| "ry": "rydberg", | ||
| "rydberg": "rydberg", | ||
| "kcal/mol": "kcal_mol", | ||
| "kcal_mol": "kcal_mol", | ||
| "kj/mol": "kJ_mol", | ||
| "kj_mol": "kJ_mol", | ||
| } | ||
|
|
||
| _LENGTH_UNIT_MAP: dict[str, str] = { | ||
| "angstrom": "angstrom", | ||
| "ang": "angstrom", | ||
| "ang.": "angstrom", | ||
| "a": "angstrom", | ||
| "bohr": "bohr", | ||
| "nm": "nm", | ||
| } | ||
|
|
||
| _PRESSURE_UNIT_MAP: dict[str, str] = { | ||
| "gpa": "GPa", | ||
| "kpa": "kPa", | ||
| "pa": "Pa", | ||
| "kbar": "kbar", | ||
| "bar": "bar", | ||
| "ev/angstrom^3": "eV/angstrom^3", | ||
| "ev/ang^3": "eV/angstrom^3", | ||
| "ev/a^3": "eV/angstrom^3", | ||
| "ha/bohr^3": "hartree/bohr^3", | ||
| "hartree/bohr^3": "hartree/bohr^3", | ||
| "au/bohr^3": "hartree/bohr^3", | ||
| "a.u./bohr^3": "hartree/bohr^3", | ||
| } | ||
|
|
||
| # dpdata internal unit strings | ||
| _INTERNAL_ENERGY = "eV" | ||
| _INTERNAL_FORCE = "eV/angstrom" | ||
| _INTERNAL_PRESSURE = "eV/angstrom^3" | ||
|
|
||
|
|
||
| def _parse_force_unit(raw: str) -> tuple[str, str]: | ||
| """Split a composite force unit string into (energy_part, length_part). | ||
|
|
||
| Examples | ||
| -------- | ||
| >>> _parse_force_unit("kcal/mol/angstrom") | ||
| ('kcal/mol', 'angstrom') | ||
| >>> _parse_force_unit("hartree/bohr") | ||
| ('hartree', 'bohr') | ||
| >>> _parse_force_unit("ev/ang") | ||
| ('ev', 'ang') | ||
| """ | ||
| # Handle atomic-unit shorthand: "au" or "a.u." means hartree/bohr for force | ||
| _FORCE_UNIT_ALIASES = {"au": ("hartree", "bohr"), "a.u.": ("hartree", "bohr")} | ||
| if raw in _FORCE_UNIT_ALIASES: | ||
| return _FORCE_UNIT_ALIASES[raw] | ||
|
|
||
| # Try matching known energy prefixes (longest first) to handle | ||
| # composite names like "kcal/mol" that themselves contain "/". | ||
| for e_key in sorted(_ENERGY_UNIT_MAP.keys(), key=len, reverse=True): | ||
| prefix = e_key + "/" | ||
| if raw.startswith(prefix): | ||
| l_part = raw[len(prefix) :] | ||
| if l_part: | ||
| return e_key, l_part | ||
| # Fallback: split on last "/" | ||
| parts = raw.rsplit("/", 1) | ||
| if len(parts) == 2 and parts[0] and parts[1]: | ||
| return parts[0], parts[1] | ||
| raise ValueError(f"Cannot parse force unit string: '{raw}'") | ||
|
|
||
|
|
||
| def _get_unit_factor(unit_str: str | None, quantity: str) -> float: | ||
| """Return the multiplicative factor to convert from the given unit to dpdata internals. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| unit_str : str or None | ||
| The unit string read from the extxyz header (e.g. "hartree", "kcal/mol/angstrom"). | ||
| If None, returns 1.0 (assumes data is already in internal units). | ||
| quantity : str | ||
| One of "energy", "force", or "stress". | ||
|
|
||
| Returns | ||
| ------- | ||
| float | ||
| Conversion factor such that ``value_internal = value_file * factor``. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If the unit string is not recognized or the quantity type is invalid. | ||
| """ | ||
| if unit_str is None: | ||
| return 1.0 | ||
|
|
||
| key = unit_str.lower().strip() | ||
|
|
||
| if quantity == "energy": | ||
| canonical = _ENERGY_UNIT_MAP.get(key) | ||
| if canonical is None: | ||
| raise ValueError( | ||
| f"Unsupported energy unit: '{unit_str}'. " | ||
| f"Supported: {list(_ENERGY_UNIT_MAP.keys())}" | ||
| ) | ||
| return EnergyConversion(canonical, _INTERNAL_ENERGY).value() | ||
|
|
||
| elif quantity == "force": | ||
| e_part, l_part = _parse_force_unit(key) | ||
| e_canonical = _ENERGY_UNIT_MAP.get(e_part) | ||
| l_canonical = _LENGTH_UNIT_MAP.get(l_part) | ||
| if e_canonical is None: | ||
| raise ValueError( | ||
| f"Unsupported energy part in force unit: '{e_part}' " | ||
| f"(from '{unit_str}'). Supported: {list(_ENERGY_UNIT_MAP.keys())}" | ||
| ) | ||
| if l_canonical is None: | ||
| raise ValueError( | ||
| f"Unsupported length part in force unit: '{l_part}' " | ||
| f"(from '{unit_str}'). Supported: {list(_LENGTH_UNIT_MAP.keys())}" | ||
| ) | ||
| src_unit = f"{e_canonical}/{l_canonical}" | ||
| return ForceConversion(src_unit, _INTERNAL_FORCE).value() | ||
|
|
||
| elif quantity == "stress": | ||
| canonical = _PRESSURE_UNIT_MAP.get(key) | ||
| if canonical is None: | ||
| raise ValueError( | ||
| f"Unsupported stress/pressure unit: '{unit_str}'. " | ||
| f"Supported: {list(_PRESSURE_UNIT_MAP.keys())}" | ||
| ) | ||
| return PressureConversion(canonical, _INTERNAL_PRESSURE).value() | ||
|
|
||
| else: | ||
| raise ValueError( | ||
| f"Unknown quantity type: '{quantity}'. Must be 'energy', 'force', or 'stress'." | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep common atomic-unit aliases in this table as well.
au/a.u.are frequent spellings for Hartree in chemistry datasets, and this helper currently rejectsenergy-unit=aueven though the PR is meant to handle unit synonyms. Ifforce-unit=au/a.u.is intended to mean Hartree/Bohr, that should be handled explicitly too, with regression tests.