Skip to content

Imaging refactor part 2 - Atoms: add_atoms(), refine_atoms(), plot(kind='atoms') and tests#236

Open
darshan-mali wants to merge 4 commits into
electronmicroscopy:devfrom
darshan-mali:imaging_refactor
Open

Imaging refactor part 2 - Atoms: add_atoms(), refine_atoms(), plot(kind='atoms') and tests#236
darshan-mali wants to merge 4 commits into
electronmicroscopy:devfrom
darshan-mali:imaging_refactor

Conversation

@darshan-mali

@darshan-mali darshan-mali commented May 28, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Part 2 of Imaging refactor

Enables the Lattice class to store atoms and their data. This includes

  • add_atoms() function that checks if an atom is present at a given position and stores the atomic positions in both pixel and unit cell coordinates
  • refine_atoms() function to refine the atomic positions
  • plot() now includes kind='atoms'

Relevant references

Part 1 can be found at : #198

This PR involves the use of the Vector datastructure.
A tutorial for the same can be found at: https://github.com/electronmicroscopy/quantem-tutorials/blob/main/tutorials/core/vector.ipynb

API and logic

After defining the lattice vectors, atoms can be added to Lattice via the add_atoms() method.
The add_atoms() takes in the fractional/unit cell coordinates of all atoms in one unit cell $0 \leq u,v < 1$.
These are tiled across all unit cells and checked against the background to determine if an atom is present.

The refine_atoms() functions optimizes for the position of the atom via 2D Gaussian fitting.

The pixel and unit cell coordinates of the atoms are stored in Lattice.atoms which is a Vector datastructure, alongwith other fitting information.

Both add_atoms() and refine_atoms() set the default_plot='atoms' in Lattice.plot().

Files changed

Updated files

  • src/quantem/imaging/lattice.py: The add_atoms() and refine_atoms() functions were added.
  • src/quantem/imaging/lattice_visualization.py: Functionality for plot(kind='atoms') was added along with necessary helpers
  • tests/imaging/test_lattice.py: Basic pytests for add_atoms() and refine_atoms() based on expected user behaviour

Examples

A testing notebook with examples can be found here:
Lattice_refactor_2_atoms.ipynb

(Note: Some plotting calls have been commented out to reduce file size. Please uncomment them before running)

Example code block:

lat = Lattice.from_data(
    im
).define_lattice_vectors(
    origin = (1626, 1024),
    v = ( 25 ,  0),
    u = ( 0,   25),
    refine_lattice = True,  # Default = True 
    block_size = 5,
).add_atoms(
    positions_frac = [
        [0.0,0.0],
        [0.5,0.5],
    ],
    edge_min_dist_px=100,
    # numbers = [0, 1],
    # intenisty_min = 0.001,
    # intensity_radius = 3.5,
    # contrast_min = 0.001,
).refine_atoms(
    fit_radius = 7,
    max_nfev = 150,
    max_move_px = 2.5,
).plot()

Example output:
image

PR Checklist

  • This PR introduces a public-facing change (e.g., API, CLI input/output).
    • For functional and algorithmic changes, tests are written or updated.
    • Documentation (e.g., tutorials, examples, README) has been updated.

Reviewer checklist

  • The notebook provided runs as expected and no bugs were found
  • Tests pass and are appropriate for the changes
  • Documentation and examples are sufficient and clear
  • The implementation matches the stated intent of the contribution

@darshan-mali darshan-mali requested a review from wwmills May 28, 2026 23:24
relative to the lattice origin r0 and basis vectors (u, v), and are used to tile the
image with candidate atom centers at all visible integer translations.
numbers : array-like of int, shape (S,), optional
Identifier per site (e.g., species or label). If None, uses 1..S. Used only for plotting

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.

If None, it appears to use 0, 1, ..., S-1

# VALIDATION: Check that lattice vectors have been defined
if not hasattr(self, "_lat") or self._lat is None:
raise ValueError(
"Lattice vectors have not been fitted. Please call define_lattice() first."

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.

define_lattice() --> define_lattice_vectors()

r_px = float(intensity_radius) if intensity_radius is not None else _auto_radius_px()

# Annulus radii for background contrast measurement (in pixels)
rin, rout = (1.5 * r_px, 3.0 * r_px) if annulus_radii is None else annulus_radii

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 this what we want? It seems like this ring will include neighboring atoms... This could cause issues for atom position refinement.

from scipy.ndimage import distance_transform_edt

DT = distance_transform_edt(m)
except Exception:

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.

not sure why the distance_transform_edt would fail, but if it does, perhaps add an output message for this? otherwise this is a silent failure

# Compute mean intensity in the detection disk for all candidates
int_center = np.empty(xy.shape[0], dtype=float)
for i in range(xy.shape[0]):
int_center[i] = mean_disk(x[i], y[i])

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.

we compute mean intensity for all atoms, even though we have a validity mask (we could generate keep right before this) that should rule some out at this point. It probably doesn't cost too much in the way of time, but it is worth making this change.

estimate is invalid or non-positive, a robust fallback is used.
max_nfev : int, default 200
Maximum number of function evaluations for the non-linear least-squares solver.
max_move_px : float, optional

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.

max_move_px ends up not being the actual maximum movement, it is just the maximum movement in the x and y directions - both moving by max_move_px is not forbidden.

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.

Additional comment on this - I think that the maximum movement should not be greater than the window that the fit is over. The default is to make it equal, which seems okay, but user can set it greater; do we want to forbid that?

max(pmax - pmin, amp0 * 4.0),
max(2.0 * r_fit, 1.0),
pmax + (pmax - pmin),
]

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.

these values seem arbitrary - why 0.25 for sigma min? why amp0*4 for amplitude max? I don't really have a better idea but just want to be thoughtful about these hard-coded values.

)

assert result is simple_lattice

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.

these tests seem okay, and pass, but there is no test that validates the found positions of the atoms after refinement. That would be a good test.

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