Skip to content

Conversation

@curtischong
Copy link
Collaborator

@curtischong curtischong commented Oct 28, 2025

Summary

Updates torchsim to use the latest version of Vesin which supports mixed PBC neighborlist calculations.

Checklist

Before a pull request can be merged, the following items must be checked:

  • Doc strings have been added in the Google docstring format.
  • Run ruff on your code.
  • Tests have been added for any new functionality or bug fixes.

We highly recommended installing the prek hooks running in CI locally to speedup the development process. Simply run pip install prek && prek install to install the hooks which will check your code before each commit.

@curtischong curtischong changed the title Pbc Mixed Pbc Oct 28, 2025
@curtischong curtischong changed the title Mixed Pbc Mixed Pbc Support Oct 28, 2025
cell=cell,
pbc=self.pbc,
cutoff=self.cutoff,
sorti=False,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep. this is a spelling mistake


# Fix: Ensure pbc has the correct shape [n_systems, 3]
pbc_tensor = torch.tensor([[pbc] * 3] * len(atoms_list), dtype=torch.bool)
pbc_tensor = torch.tensor(pbc).repeat(state.n_systems, 1)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this repeat is needed since torch_nl_linked_cell accepts (num_systems, 3) for the pbc. it doens't just accept a tensor of shape (3,)

@curtischong curtischong force-pushed the pbc branch 2 times, most recently from 94065f9 to 88d8036 Compare November 1, 2025 20:55
assert trajectory.get_array("atomic_numbers").shape == (1, 10)
assert trajectory.get_array("cell").shape == (1, 3, 3)
assert trajectory.get_array("pbc").shape == (1,)
assert trajectory.get_array("pbc").shape == (1, 3)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not 100% sure this is the correct test. should it be shape (1,3) or just shape (3,)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think pbc should be system_attributes so I would argue for (1, 3)

Copy link
Collaborator Author

@curtischong curtischong Nov 7, 2025

Choose a reason for hiding this comment

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

yeah I saw that we're also probably going to change trajectory reporter soon as well to make it less confusing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think performance-wise making pbc a system attribute will do anything. Since I can see the case for torchsim taking in arbitrary states and manipulating them I am also leaning towards making pbc a system attribute

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

okay. yeah this is completely wrong. In this PR I'll keep pbc a global attribute and make it (3,). that should be the last one. Man I was quite tired when I first wrote this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree let's keep as (3,) for now

positions=system_positions,
types=system_atomic_numbers,
cell=system_cell,
pbc=system_pbc,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the simstate.pbc is a torch.Tensor by default now! so we don't need this

@curtischong curtischong force-pushed the pbc branch 3 times, most recently from 5aba8d7 to fcf1d1a Compare November 3, 2025 00:23
@curtischong curtischong added the breaking Breaking changes label Nov 3, 2025
@curtischong curtischong added the feature Entirely new features, not improvements to existing ones label Nov 3, 2025
@curtischong
Copy link
Collaborator Author

there is an annoying bug with torch.jit that I'm trying to solve in a test (our nl function takes too long bc we are timing the compilation and the invocation of the function I believe) but the main API for this is pretty much done. So you can review if you want

@curtischong curtischong force-pushed the pbc branch 2 times, most recently from 19029e5 to 4f77996 Compare November 5, 2025 00:19
@curtischong curtischong marked this pull request as ready for review November 5, 2025 00:19
wrap only the pbc axis for each atom

support init simstate with list of bools

better test that uses itertools

update vesin version

fix pbc check in integrators

fix more pytests

more fixing

more fixes

VesinNeighborListTorch is slow

fix metatensor for pbc

more fixes

wip fix pbc trajectory

trajectory  runs

bump metatomic version or vesin will complain

fix errors

wip fix more trajectory issues

make trajectory pass

fix pbc in diff_sim

fix ase atoms to state conversion for pbc

fix neighbors.py

assert the pymatgen pbc is valid

proper conversion between pbc for atoms, and pymatgen

do not pass in pbc to phononpy

rm warning and add doc to github

make consistent with prev implementation

fix io tests

lint

minor simplification

simplify test

more simplification changes

fix some tests

satisfy prek but ruff check errors :/ we'll fix this later

more cleanup

rename

renamove more diffs

more changes

wip fix

rm init in md

add type checking and fix pbc type in dataclass def

cleanup state

loosen test for nl
Copy link
Collaborator

@thomasloux thomasloux left a comment

Choose a reason for hiding this comment

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

Some modifications are needed in ts.transforms. Otherwise looks correct to me.
I would also suggest using this PR to port pbc to system_attributes

assert trajectory.get_array("atomic_numbers").shape == (1, 10)
assert trajectory.get_array("cell").shape == (1, 3, 3)
assert trajectory.get_array("pbc").shape == (1,)
assert trajectory.get_array("pbc").shape == (1, 3)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think pbc should be system_attributes so I would argue for (1, 3)

@curtischong
Copy link
Collaborator Author

curtischong commented Nov 7, 2025

@orionarcher or @abhijeetgangan do you recall why pbc is a global attribute? I'm actually fine with either option (it being a global/system attribute) because I think when ppl are doing simulations, they would choose to fix periodicity for all of the systems in their batch.

I do see the case for it being a system attribute. Just wanted your opinion!

@abhijeetgangan
Copy link
Collaborator

abhijeetgangan commented Nov 7, 2025

@curtischong In principle you could have different settings for each system in the batch, even mixed PBC for that matter. Only cases it would break would be where the simulations is meant for PBC systems only like phonons or elastic constants etc. We could certainly add error handling case by case but I think this could be future addition and we can keep and issue open for it.

IMO this PR is already a big feature compared to the latter as I see it having more applications than people running different PBC systems in a batch.

@thomasloux
Copy link
Collaborator

IMO this PR is already a big feature compared to the latter as I see it having more applications than people running different PBC systems in a batch.

I agree, we should do that separately. Thanks for this PR @curtischong

@curtischong
Copy link
Collaborator Author

curtischong commented Nov 7, 2025

@thomasloux I have to rewrite how trajectory stores pbc tensors later tonight (it doesn't understand global attributes that are not constants). but other than that, we should be good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The tests pass, but I had a hard time following this logic. This file is overdue for an overhaul.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to see trajectory aligned with the H5MD standard at some point, thats the next overhaul I have planned

@curtischong curtischong force-pushed the pbc branch 2 times, most recently from 4dd71ad to 7866f65 Compare November 8, 2025 01:37
Copy link
Collaborator

@orionarcher orionarcher left a comment

Choose a reason for hiding this comment

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

Broadly looking good to me, a few comments

Comment on lines +96 to +99
@property
def pbc(self) -> torch.Tensor:
"""A getter for pbc that tells type checkers it's always defined."""
return self.pbc
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have mixed feelings about adding nonfunctional code just to satisfy type checkers. Why aren't they picking up on the attribute?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is because we define the attribute as torch.Tensor | bool so when the dataclass is initialized, users know they can pass those types in. This attribute is converted to a torch.Tensor inside the post_init. This typehint here is to let users know that when they access this attribute it's always a torch.Tensor.

I tried many different things to make this type accurate. One idea I had was to make pbc just a torch.Tensor, and then manually define a custom init for the dataclass. Unfortunately, child classes will have to manually call this init - which is just really messy and bloats up all of the code for children childclasses. I believe that this is the best way to use the nice "auto init" functions that dataclasses provides while having accurate types

Copy link
Collaborator

Choose a reason for hiding this comment

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

I actually still think, we could do the custom init only for SimState, and assume, as it is the case at the moment, that any Subclass of SimState (MDState...) will be created from a init_fn and another SimState, so that pbc will indeed be a torch.Tensor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nope. Child dataclasses do not call the inits of parent dataclasses (because they don't know which params to pass in - annoying I know). See #335.

This issue is reported here https://bugs.python.org/issue43835

I should add a comment in the code pointing to that issue

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree, but my point is that it's not a problem. You can assume it will be a torch.Tensor because it will be initiated from a SimState

Copy link
Collaborator

Choose a reason for hiding this comment

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

If I take a NVT Langevin, the init function is

def nvt_langevin_init(
    state: SimState | StateDict,
    model: ModelInterface,
    *,
    kT: float | torch.Tensor,
    seed: int | None = None,
    **_kwargs: Any,
) -> MDState:
    """Docs..."""
    if not isinstance(state, SimState):
        state = SimState(**state)

    model_output = model(state)

    momenta = getattr(
        state,
        "momenta",
        calculate_momenta(state.positions, state.masses, state.system_idx, kT, seed),
    )

    return MDState(
        positions=state.positions,
        momenta=momenta,
        energy=model_output["energy"],
        forces=model_output["forces"],
        masses=state.masses,
        cell=state.cell,
        pbc=state.pbc, # So pbc is taken from a SimState, so pbc *is* torch.tensor
        system_idx=state.system_idx,
        atomic_numbers=state.atomic_numbers,
        constraints=state.constraints,
    )

Copy link
Collaborator

Choose a reason for hiding this comment

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

My code design assumes that no user should create directly a MDState, which can be called into question. But it has the advantage of being a simpler code

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ahhh I was confusing nvt_langevin_init with the init of MDState. Can you put an issue up? so we can discuss it further (bc it'll prob be in a diff PR). Personally I'm fine with either pattern (using the overrides like I was doing in this PR vs the nvt_langevin_init function)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to see trajectory aligned with the H5MD standard at some point, thats the next overhaul I have planned

Comment on lines 831 to 841
return self.get_array(prop, start=start, stop=stop)[0]
return self.get_array(prop, start=start, stop=stop)

arrays["cell"] = np.expand_dims(return_prop(self, "cell", frame), axis=0)
arrays["atomic_numbers"] = return_prop(self, "atomic_numbers", frame)
arrays["masses"] = return_prop(self, "masses", frame)
arrays["cell"] = np.expand_dims(return_prop(self, "cell", frame), axis=0)[0]
arrays["atomic_numbers"] = return_prop(self, "atomic_numbers", frame)[0]
arrays["masses"] = return_prop(self, "masses", frame)[0]
Copy link
Collaborator

Choose a reason for hiding this comment

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

why move the index here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I updated the return_prop function (line 831) to not auto return the [0] index. This was done because return_prop(self, "pbc", frame) is now a global attribute that is a tensor (so we don't want to just return the first index, but the entire tensor).

So for all of the attributes, the functionality is the exact same - it's actually just a change to make PBC to work. (so we can return a tensor for global attribuets - not just constants - which was what it was before)

Copy link
Collaborator

@thomasloux thomasloux Nov 10, 2025

Choose a reason for hiding this comment

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

are you sure about that? I think it's just because get_array(name, n_frame, n_frame+1) normally return (1, size). You modify it to squeeze for pbc, so indeed you need to modify here. But I think you shouldn't squeeze pbc

@orionarcher
Copy link
Collaborator

IMO this PR is already a big feature compared to the latter as I see it having more applications than people running different PBC systems in a batch.

Given that we may soon have batched neighbor list calculations it's unclear to me if we want to support mixed-PBC. It's nice in principle but I would bet it's very rarely needed in practice and may complicate neighbor list calculations.

@curtischong
Copy link
Collaborator Author

curtischong commented Nov 10, 2025

I think catalytic applications would benefit from mixed PBC (without having to manually create a vacuum).

Mixed PBC does complicate the writing of kernels. Here is an example of what Vesin had to do when implementing mixed PBC https://github.com/Luthaf/vesin/pull/76/files#diff-38d490d3ab0a91479a981cd188ef933c68d7cbb24bc90ab3d66595f7da4e0da6R133
I do think that this PR does seem larger than it actually is because I think mic displacement vectors were changed to match ASE's.

Here was my implementation of mixed PBC in vesin (albeit with the wrong mic displacement vectors) and it's quite small
https://github.com/Luthaf/vesin/pull/75/files#diff-38d490d3ab0a91479a981cd188ef933c68d7cbb24bc90ab3d66595f7da4e0da6R74

We should ask the nvidia ppl to see how hard it might be to add mixed PBC (can they use the same algorithms?). It might be a really small change like the diff in my PR

Comment on lines 831 to 841
return self.get_array(prop, start=start, stop=stop)[0]
return self.get_array(prop, start=start, stop=stop)

arrays["cell"] = np.expand_dims(return_prop(self, "cell", frame), axis=0)
arrays["atomic_numbers"] = return_prop(self, "atomic_numbers", frame)
arrays["masses"] = return_prop(self, "masses", frame)
arrays["cell"] = np.expand_dims(return_prop(self, "cell", frame), axis=0)[0]
arrays["atomic_numbers"] = return_prop(self, "atomic_numbers", frame)[0]
arrays["masses"] = return_prop(self, "masses", frame)[0]
Copy link
Collaborator

@thomasloux thomasloux Nov 10, 2025

Choose a reason for hiding this comment

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

are you sure about that? I think it's just because get_array(name, n_frame, n_frame+1) normally return (1, size). You modify it to squeeze for pbc, so indeed you need to modify here. But I think you shouldn't squeeze pbc

@curtischong curtischong force-pushed the pbc branch 2 times, most recently from 4375c2e to 3a11f02 Compare November 11, 2025 04:18

self.flush()

def write_global_array(self, name: str, array: np.ndarray | torch.Tensor) -> None:
Copy link
Collaborator Author

@curtischong curtischong Nov 11, 2025

Choose a reason for hiding this comment

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

I needed to write this function just for pbc. because otherwise the trajectory format will add an extra dimension (bc write_arrays thinks there is a system_idx dimension)

arrays["positions"] = self.get_array("positions", start=frame, stop=frame + 1)[0]

def return_prop(self: Self, prop: str, frame: int) -> np.ndarray:
if prop == "pbc":
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes I hate this extra if statement as well. this trajectory file format also doesn't egneralize well to global attributes from child simstates. we need to settle on a good simstate definition for attributes soon

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

Labels

breaking Breaking changes feature Entirely new features, not improvements to existing ones

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants