Skip to content

feat: add diffpy.structure.Structure to pyobjcryst.Crystal converter - #13

Merged
sbillinge merged 3 commits into
diffpy:mainfrom
cadenmyers13:structure-to-crystal
Nov 3, 2026
Merged

sbillinge merged 3 commits into
diffpy:mainfrom
cadenmyers13:structure-to-crystal

Conversation

@cadenmyers13

Copy link
Copy Markdown
Contributor

What problem does this PR address?

There was no way to turn a diffpy.structure Structure into a pyobjcryst Crystal. srreal only converts the other way, and pyobjcryst only builds crystals from CIF files. This blocks anyone who has a Structure from a PDF fit and wants to use it in an ObjCryst calculation, such as a Rietveld fit.

This PR adds create_crystal_from_structure(structure, space_group) in a new diffpy.cmistructure.converters module. The name follows pyobjcryst's create_crystal_from_cif.

  • The space group is required. A Structure usually holds the full cell in P1 and doesn't record its space group. Building the Crystal in P1 would silently make every lattice parameter and atom position refinable.
  • The atoms are reduced to the asymmetric unit with the Crystal's own symmetry operations. diffpy.structure and ObjCryst pick different default origin choices: "Fd-3m" is origin choice 1 in diffpy and origin choice 2 in ObjCryst.
  • It raises a two-part ValueError instead of guessing, when:
    • the symbol or number is unknown;
    • the lattice doesn't fit the space group (ObjCryst would otherwise silently change the cell);
    • the atoms don't fit it (wrong setting, wrong origin choice, or only the asymmetric unit given).
  • The lattice, elements, positions, occupancies, labels and isotropic and anisotropic ADPs are copied.
  • The space group can be given as a Hermann-Mauguin symbol ("P b n m", "Pbnm", "F d -3 m:1") or as a number (62 or "62").

Example 1: convert the full LaMnO3 cell

from diffpy.cmistructure.converters import create_crystal_from_structure
from diffpy.structure import Structure

structure = Structure()
structure.read("tests/testdata/LaMnO3.stru")  # full cell, 20 atoms
crystal = create_crystal_from_structure(structure, "Pbnm")
print(crystal.GetSpaceGroup().GetName())
for i in range(crystal.GetNbScatterer()):
    atom = crystal.GetScatterer(i)
    print(atom.GetName(), round(atom.X, 4), round(atom.Y, 4), round(atom.Z, 4))
P b n m
La1 0.9961 0.0321 0.25
Mn1 0.0 0.5 0.0
O1 0.0596 0.4962 0.25
O2 0.7201 0.2894 0.0311

Example 2: a space group that doesn't fit

create_crystal_from_structure(structure, 62)  # 62 is Pnma, not Pbnm
ValueError: Space group P n m a does not fit the atoms of the structure. Please give the full unit cell of the structure and its space group, with the setting and origin choice, or 'P1' to keep every atom.

Tests: 41 pass locally, both with pyobjcryst main and with released pyobjcryst 2025.1.0 and diffpy.structure 3.5.0. The new tests are in tests/test_converters.py, and the silicon and asymmetric-unit fixtures are in tests/conftest.py.

What should the reviewer(s) do?

Please review the code and give feedback on these choices:

  1. A structure holding only the asymmetric unit raises an error. The alternative is to accept it as it is when no two of its atoms are symmetry-equivalent.
  2. Atoms must match the symmetry to diffpy's default tolerance (1e-5). A cell refined in P1 that has drifted off the symmetry will raise. Should there be a tolerance argument?
  3. The function isn't exported from diffpy.cmistructure. Exporting it would make importing the package also import pyobjcryst.

cadenmyers13 and others added 2 commits November 3, 2026 10:49
… to a pyobjcryst Crystal

New module diffpy.cmistructure.converters. The atoms are reduced to the
asymmetric unit with the Crystal's own space group, since ObjCryst and
diffpy.structure pick different default origin choices (e.g. Fd-3m).
Raises a ValueError if the symbol is unknown or the lattice or atoms
don't fit the space group. Tests in tests/test_converters.py, fixtures
in tests/conftest.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An int space group made pyobjcryst raise a Boost ArgumentError, so
converters.py passes str(space_group) to Crystal. The unknown-space-group
error now mentions numbers too. Tests in tests/test_converters.py cover
227 given as an int and as text, and 231.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.08%. Comparing base (0615cca) to head (72b57d2).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #13      +/-   ##
==========================================
+ Coverage   95.45%   96.08%   +0.63%     
==========================================
  Files           5        6       +1     
  Lines         638      741     +103     
==========================================
+ Hits          609      712     +103     
  Misses         29       29              
Files with missing lines Coverage Δ
tests/conftest.py 92.17% <100.00%> (+1.45%) ⬆️
tests/test_converters.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cadenmyers13

cadenmyers13 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

@sbillinge ready for review, please see "What should reviewers do?" section

@cadenmyers13

Copy link
Copy Markdown
Contributor Author

I know we talked about ase to diffpy.structure converts in the past. This could be a place to put that too

@sbillinge
sbillinge merged commit 91b34a0 into diffpy:main Nov 3, 2026
7 checks passed
@cadenmyers13
cadenmyers13 deleted the structure-to-crystal branch November 3, 2026 18:41
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.

2 participants