Skip to content

feat(extxyz): add unit convert and tag synonym matching#678

Open
SchrodingersCattt wants to merge 9 commits into
deepmodeling:masterfrom
SchrodingersCattt:devel
Open

feat(extxyz): add unit convert and tag synonym matching#678
SchrodingersCattt wants to merge 9 commits into
deepmodeling:masterfrom
SchrodingersCattt:devel

Conversation

@SchrodingersCattt

@SchrodingersCattt SchrodingersCattt commented Jul 3, 2024

Copy link
Copy Markdown
Contributor

Many datasets using the exyz format have different formats for units and tags. This PR aims to address the situation with multiple possible datasets.

Summary by CodeRabbit

  • New Features

    • Added unit-aware parsing for extended XYZ metadata, including energy, force, and stress/virial scaling based on declared header units.
  • Improvements

    • When stress is provided with a stress unit, derived virials are now converted consistently; unsupported or invalid units now raise clear errors instead of proceeding silently.
  • Tests

    • Expanded unit-conversion coverage with new fixtures (hartree, kcal/mol, GPa, kbar, unsupported units) and more granular conversion/error-handling assertions.

@coderabbitai

coderabbitai Bot commented Jul 3, 2024

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Extended XYZ parsing now reads optional unit metadata for energy, force, and stress, converts those values into dpdata internal units, and applies the factors during frame loading. New helper functions, fixtures, and tests cover supported units, default behavior, and unsupported-unit errors.

Changes

ExtXYZ unit conversion

Layer / File(s) Summary
Unit helper functions
dpdata/formats/xyz/_unit_convert.py
Adds unit alias tables, force-unit parsing, and conversion-factor lookup for energy, force, and stress quantities.
Header unit parsing and scaling
dpdata/formats/xyz/quip_gap_xyz.py
Reads energy, force, and stress unit fields from extxyz headers, applies the computed factors to energies and forces, and scales stress-derived virials.
Tests and XYZ fixtures
tests/test_quip_gap_xyz.py, tests/xyz/*
Adds helper coverage, conversion assertions, unsupported-unit failure coverage, and fixture files for the supported unit cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • deepmodeling/dpdata#983: Shares the same quip_gap_xyz.py stress-to-virial parsing path and adds related stress handling logic.

Suggested labels: enhancement, dpdata, size:L

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding extxyz unit conversion support, though it also mentions tag synonym matching that is not evident in the diff summary.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 3, 2024

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 2 untouched benchmarks


Comparing SchrodingersCattt:devel (13941d2) with master (5de0229)

Open in CodSpeed

@codecov

codecov Bot commented Jul 3, 2024

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.36364% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.93%. Comparing base (5de0229) to head (13941d2).

Files with missing lines Patch % Lines
dpdata/formats/xyz/_unit_convert.py 95.45% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #678      +/-   ##
==========================================
+ Coverage   86.87%   86.93%   +0.05%     
==========================================
  Files          89       90       +1     
  Lines        8268     8319      +51     
==========================================
+ Hits         7183     7232      +49     
- Misses       1085     1087       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment on lines +273 to +275
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_au2eVperang

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove redundant conditional block for kcal/mol/bohr.

The conditional block for kcal/mol/bohr is redundant and should be removed.

-  elif f_units == "kcal/mol/bohr":
-    info_dict["forces"] = (
-      np.array([force_array]).astype("float32") * f_conv_au2eVperang
-    )

Committable suggestion was skipped due to low confidence.

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
else:
raise ValueError("No valid energy field found in field_dict.")
except KeyError:
raise ValueError("Error while accessing energy fields in field_dict.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Raise exceptions with raise ... from None.

Within an except clause, raise exceptions with raise ... from None to distinguish them from errors in exception handling.

-  raise ValueError("Error while accessing energy fields in field_dict.")
+  raise ValueError("Error while accessing energy fields in field_dict.") from None
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raise ValueError("Error while accessing energy fields in field_dict.")
raise ValueError("Error while accessing energy fields in field_dict.") from None
Tools
Ruff

252-252: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
info_dict["forces"] = np.array([force_array]).astype("float32")
else:
info_dict["forces"] = np.array([force_array]).astype("float32")
except:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Avoid using bare except statements.

Using bare except statements can catch unexpected errors and make debugging difficult. Specify the exception type or use except Exception as e to handle specific exceptions.

-  except:
+  except KeyError:

Committable suggestion was skipped due to low confidence.

Tools
Ruff

283-283: Do not use bare except

(E722)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment on lines +254 to +299
try:
if f_units == "kcal/mol/angstrom":
<<<<<<< HEAD
info_dict["forces"] = np.array([force_array]).astype("float32") * f_conv_kcalpermolperang2eVperang
elif f_units == "hartree/angstrom" or f_units == "hartree/ang" or f_units == "hartree/ang.":
info_dict["forces"] = np.array([force_array]).astype("float32") * f_conv_auperang2eVperang
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = np.array([force_array]).astype("float32") * f_conv_kcalpermolperbohr2eVperang
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = np.array([force_array]).astype("float32") * f_conv_au2eVperang
elif f_units == "ev/angstrom" or f_units == "ev/ang" or f_units == "ev/ang.":
info_dict["forces"] = np.array([force_array]).astype("float32")
else: info_dict["forces"] = np.array([force_array]).astype("float32")
except Exception:
info_dict["forces"] = np.array([force_array]).astype("float32")
=======
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperang2eVperang
)
elif (
f_units == "hartree/angstrom"
or f_units == "hartree/ang"
or f_units == "hartree/ang."
):
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_auperang2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperbohr2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_au2eVperang
)
elif (
f_units == "ev/angstrom" or f_units == "ev/ang" or f_units == "ev/ang."
):
info_dict["forces"] = np.array([force_array]).astype("float32")
else:
info_dict["forces"] = np.array([force_array]).astype("float32")
except:
info_dict["forces"] = np.array([force_array]).astype("float32")
>>>>>>> origin/devel

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unit conversion for forces and redundant conditional block.

The new logic for force unit conversion is correctly implemented but contains a redundant conditional block for kcal/mol/bohr and uses a bare except statement.

-  elif f_units == "kcal/mol/bohr":
-    info_dict["forces"] = (
-      np.array([force_array]).astype("float32") * f_conv_au2eVperang
-    )
-  except:
-    info_dict["forces"] = np.array([force_array]).astype("float32")
+  except KeyError as e:
+    raise ValueError("Error while accessing force fields in field_dict.") from e

Committable suggestion was skipped due to low confidence.

Tools
Ruff

255-256: SyntaxError: Expected an indented block after if statement


256-256: SyntaxError: Expected except or finally after try block


256-256: SyntaxError: Expected a statement


256-256: SyntaxError: Expected a statement


256-256: SyntaxError: Expected a statement


257-257: SyntaxError: Unexpected indentation


258-258: SyntaxError: unindent does not match any outer indentation level


258-258: SyntaxError: Expected a statement


258-258: SyntaxError: Invalid annotated assignment target


258-259: SyntaxError: Expected an expression


259-259: SyntaxError: Unexpected indentation


260-260: SyntaxError: unindent does not match any outer indentation level


260-260: SyntaxError: Expected a statement


260-260: SyntaxError: Invalid annotated assignment target


260-261: SyntaxError: Expected an expression


261-261: SyntaxError: Unexpected indentation


262-262: SyntaxError: unindent does not match any outer indentation level


262-262: SyntaxError: Expected a statement


262-262: SyntaxError: Invalid annotated assignment target


262-263: SyntaxError: Expected an expression


263-263: SyntaxError: Unexpected indentation


264-264: SyntaxError: unindent does not match any outer indentation level


264-264: SyntaxError: Expected a statement


264-264: SyntaxError: Invalid annotated assignment target


264-265: SyntaxError: Expected an expression


265-265: SyntaxError: Unexpected indentation


266-266: SyntaxError: unindent does not match any outer indentation level


266-266: SyntaxError: Expected a statement


266-266: SyntaxError: Expected a statement


267-267: SyntaxError: Unexpected indentation


267-267: SyntaxError: Expected a statement


267-268: SyntaxError: Expected an expression


268-268: SyntaxError: Unexpected indentation


269-269: SyntaxError: Expected a statement


269-269: SyntaxError: Expected a statement


269-269: SyntaxError: Expected a statement


269-269: SyntaxError: Expected a statement


269-270: SyntaxError: Expected a statement


270-270: SyntaxError: Unexpected indentation


274-274: SyntaxError: unindent does not match any outer indentation level


274-274: SyntaxError: Expected a statement


275-275: SyntaxError: Invalid annotated assignment target


278-279: SyntaxError: Expected an expression


279-279: SyntaxError: Unexpected indentation


282-282: SyntaxError: unindent does not match any outer indentation level


282-282: SyntaxError: Expected a statement


282-282: SyntaxError: Invalid annotated assignment target


282-283: SyntaxError: Expected an expression


283-283: SyntaxError: Unexpected indentation


287-287: SyntaxError: unindent does not match any outer indentation level


287-287: SyntaxError: Expected a statement


287-287: SyntaxError: Invalid annotated assignment target


287-288: SyntaxError: Expected an expression


288-288: SyntaxError: Unexpected indentation


291-291: SyntaxError: unindent does not match any outer indentation level


291-291: SyntaxError: Expected a statement


292-292: SyntaxError: Invalid annotated assignment target


293-294: SyntaxError: Expected an expression


294-294: SyntaxError: Unexpected indentation


295-295: SyntaxError: unindent does not match any outer indentation level


295-295: SyntaxError: Expected a statement


295-295: SyntaxError: Expected a statement


295-296: SyntaxError: Expected a statement


296-296: SyntaxError: Unexpected indentation


297-297: SyntaxError: unindent does not match any outer indentation level


297-297: SyntaxError: Expected a statement


297-297: SyntaxError: Expected a statement


297-298: SyntaxError: Expected a statement


298-298: SyntaxError: Unexpected indentation


299-299: SyntaxError: Expected a statement


299-299: SyntaxError: Expected a statement


299-299: SyntaxError: Expected a statement


299-299: SyntaxError: Expected a statement

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment on lines +197 to +203
try:
e_units = np.array([field_dict["energy-unit"].lower()])
f_units = np.array([field_dict["force-unit"].lower()])
except Exception:
pass
# print('No units information contained.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Proper handling of missing unit information.

The try-except block for handling missing unit information should raise an exception or log a warning if units are not found, rather than passing silently.

-  except Exception:
-    pass
-    # print('No units information contained.')
+  except KeyError as e:
+    raise ValueError("No units information contained.") from e
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
e_units = np.array([field_dict["energy-unit"].lower()])
f_units = np.array([field_dict["force-unit"].lower()])
except Exception:
pass
# print('No units information contained.')
try:
e_units = np.array([field_dict["energy-unit"].lower()])
f_units = np.array([field_dict["force-unit"].lower()])
except KeyError as e:
raise ValueError("No units information contained.") from e

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment on lines +235 to +253
try:
possible_fields = [
"energy",
"energies",
"Energies",
"potential-energy.energy",
"Energy",
]
for key in possible_fields:
if key in field_dict:
info_dict["energies"] = np.array(
[field_dict[key]], dtype="float32"
)
break
else:
raise ValueError("No valid energy field found in field_dict.")
except KeyError:
raise ValueError("Error while accessing energy fields in field_dict.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Improved handling of possible energy fields and raising exceptions.

The changes improve handling of possible energy fields and raising exceptions. Use raise ... from None to distinguish exceptions from errors in exception handling.

-  raise ValueError("Error while accessing energy fields in field_dict.")
+  raise ValueError("Error while accessing energy fields in field_dict.") from None
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
possible_fields = [
"energy",
"energies",
"Energies",
"potential-energy.energy",
"Energy",
]
for key in possible_fields:
if key in field_dict:
info_dict["energies"] = np.array(
[field_dict[key]], dtype="float32"
)
break
else:
raise ValueError("No valid energy field found in field_dict.")
except KeyError:
raise ValueError("Error while accessing energy fields in field_dict.")
try:
possible_fields = [
"energy",
"energies",
"Energies",
"potential-energy.energy",
"Energy",
]
for key in possible_fields:
if key in field_dict:
info_dict["energies"] = np.array(
[field_dict[key]], dtype="float32"
)
break
else:
raise ValueError("No valid energy field found in field_dict.")
except KeyError:
raise ValueError("Error while accessing energy fields in field_dict.") from None
Tools
Ruff

252-252: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment on lines +254 to +283
try:
if f_units == "kcal/mol/angstrom":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperang2eVperang
)
elif (
f_units == "hartree/angstrom"
or f_units == "hartree/ang"
or f_units == "hartree/ang."
):
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_auperang2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperbohr2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_au2eVperang
)
elif (
f_units == "ev/angstrom" or f_units == "ev/ang" or f_units == "ev/ang."
):
info_dict["forces"] = np.array([force_array]).astype("float32")
else:
info_dict["forces"] = np.array([force_array]).astype("float32")
except Exception:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unit conversion for forces and redundant conditional block.

The new logic for force unit conversion is correctly implemented but contains a redundant conditional block for kcal/mol/bohr and uses a bare except statement.

-  elif f_units == "kcal/mol/bohr":
-    info_dict["forces"] = (
-      np.array([force_array]).astype("float32") * f_conv_au2eVperang
-    )
-  except:
-    info_dict["forces"] = np.array([force_array]).astype("float32")
+  except KeyError as e:
+    raise ValueError("Error while accessing force fields in field_dict.") from e
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
if f_units == "kcal/mol/angstrom":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperang2eVperang
)
elif (
f_units == "hartree/angstrom"
or f_units == "hartree/ang"
or f_units == "hartree/ang."
):
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_auperang2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperbohr2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_au2eVperang
)
elif (
f_units == "ev/angstrom" or f_units == "ev/ang" or f_units == "ev/ang."
):
info_dict["forces"] = np.array([force_array]).astype("float32")
else:
info_dict["forces"] = np.array([force_array]).astype("float32")
except Exception:
try:
if f_units == "kcal/mol/angstrom":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperang2eVperang
)
elif (
f_units == "hartree/angstrom"
or f_units == "hartree/ang"
or f_units == "hartree/ang."
):
info_dict["forces"] = (
np.array([force_array]).astype("float32") * f_conv_auperang2eVperang
)
elif f_units == "kcal/mol/bohr":
info_dict["forces"] = (
np.array([force_array]).astype("float32")
* f_conv_kcalpermolperbohr2eVperang
)
elif (
f_units == "ev/angstrom" or f_units == "ev/ang" or f_units == "ev/ang."
):
info_dict["forces"] = np.array([force_array]).astype("float32")
else:
info_dict["forces"] = np.array([force_array]).astype("float32")
except KeyError as e:
raise ValueError("Error while accessing force fields in field_dict.") from e

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
used_colomn += field_length
continue
except Exception as e:
print("unknown field {}".format(kv_dict["key"]), e)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the reason not to raise an error?

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
]
used_colomn += field_length
continue
except Exception as e:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the error it is expected to catch?

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
Comment on lines +211 to +214
"""
info_dict["energies"] = np.array([field_dict["energy"]]).astype("float32")
info_dict["forces"] = np.array([force_array]).astype("float32")
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If it is not used anymore, it should be removed.

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
info_dict["energies"] = np.array([field_dict["energy"]]).astype(
"float32"
)
except Exception:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the expected error to catch here? It's hard to read.

Comment thread dpdata/xyz/quip_gap_xyz.py Outdated
)
elif e_units == "ev":
info_dict["energies"] = np.array([field_dict["energy"]]).astype(
"float32"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is it converted to float32?

@njzjz njzjz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The unit test should be added to cover the cases in this PR.

@SchrodingersCattt SchrodingersCattt marked this pull request as draft June 30, 2026 01:40
@SchrodingersCattt SchrodingersCattt changed the title quipGapXYZ: add unit convert and tag synonym matching feat(extxyz): add unit convert and tag synonym matching Jul 3, 2026
SchrodingersCattt added a commit to SchrodingersCattt/dpdata that referenced this pull request Jul 3, 2026
… stress

- New module dpdata/formats/xyz/_unit_convert.py with unit alias maps
  and _get_unit_factor() helper that delegates to dpdata.unit converters
- Support energy-unit, force-unit, stress-unit headers in extxyz files
- Convert to dpdata internal units (eV, eV/angstrom, eV/angstrom^3)
- Raise ValueError for unsupported units instead of silent pass
- Use float64 throughout instead of float32
- Add _parse_force_unit() for composite unit strings (e.g. kcal/mol/angstrom)
- Add stress_factor parameter to _parse_stress_to_virials()
- Add comprehensive tests for unit conversion module and end-to-end

Resolves reviewer feedback from PR deepmodeling#678:
- No more bare except / silent error swallowing
- No more dead commented-out code
- No more float32 precision truncation
- Full test coverage for unit conversion paths
# Conflicts:
#	dpdata/xyz/quip_gap_xyz.py
… stress

- New module dpdata/formats/xyz/_unit_convert.py with unit alias maps
  and _get_unit_factor() helper that delegates to dpdata.unit converters
- Support energy-unit, force-unit, stress-unit headers in extxyz files
- Convert to dpdata internal units (eV, eV/angstrom, eV/angstrom^3)
- Raise ValueError for unsupported units instead of silent pass
- Use float64 throughout instead of float32
- Add _parse_force_unit() for composite unit strings (e.g. kcal/mol/angstrom)
- Add stress_factor parameter to _parse_stress_to_virials()
- Add comprehensive tests for unit conversion module and end-to-end

Resolves reviewer feedback from PR deepmodeling#678:
- No more bare except / silent error swallowing
- No more dead commented-out code
- No more float32 precision truncation
- Full test coverage for unit conversion paths
@SchrodingersCattt SchrodingersCattt marked this pull request as ready for review July 3, 2026 01:49
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. dpdata enhancement New feature or request labels Jul 3, 2026
@SchrodingersCattt SchrodingersCattt requested a review from njzjz July 3, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dpdata enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants