Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Utility functions to load and save the new database via the pyMBE API, `pmb.save_database` and `pmb.load_database`. (#147)
- Added functions to define particle states: `pmb.define_particle_states` and `pmb.define_monoprototic_particle_states`. (#147)
- Added utility functions in `lib/handy_functions` to define residue and particle templates for aminoacids en peptides and residues: `define_protein_AA_particles`, `define_protein_AA_residues` and `define_peptide_AA_residues`. (#147)
- Added support for nanoparticle templates and instances in the canonical storage layer via `NanoparticleTemplate` and `NanoparticleInstance`. (#148)
- Added new API methods `pmb.define_nanoparticle` and `pmb.create_nanoparticle` to define and build nanoparticles with configurable core particles and surface site composition. (#148)
- Added nanoparticle site-construction utilities in `pyMBE.lib.nanoparticle_tools` for spherical site distribution, patch construction, and overlap checks. (#148)
- Added sample script `samples/nanoparticles_grxmc.py` to demonstrate nanoparticle setup and simulation workflows. (#148)
- Added dedicated nanoparticle unit tests (`testsuite/nanoparticle_unit_tests.py`) and coverage for nanoparticle-related code paths. (#148)

## Changed
- Create methods (`create_particle`, `create_residue`, `create_molecule`, `create_protein`, `create_hydrogel`) now raise a ValueError if no template is found for an input `name` instead than a warning. (#147)
Expand All @@ -22,10 +27,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Pka values are now stored as part of chemical reactions and no longer an attribute of particle templates. (#147)
- Amino acid residue templates are no longer defined internally in `define_peptide` and `define_protein`. Those definitions are now exposed to the user. (#147)
- Molecule templates now need to be defined to be used as templates for hydrogel chains in hydrogels. (#147)
- Rigid-body setup is now integrated into nanoparticle creation, allowing nanoparticles to be initialized as rigid objects directly from `pmb.create_nanoparticle`. (#148)
- Nanoparticle construction now supports primary/secondary site partitioning and multi-patch layouts driven by template parameters. (#148)

## Fixed
- Wrong handling of units in `get_radius_map` when the `dimensionless` argument was triggered. (#147)
- Utility methods `get_particle_id_map`, `calculate_HH`, `calculate_net_charge`, `center_object_in_simulation_box` now support all template types in pyMBE, including hydrogels. Some of these methods have been renamed to expose directly in the API this change in behavior. (#147)
- Fixed edge cases in rigid-body initialization used by nanoparticle creation to improve robustness of newly created nanoparticle objects. (#148)


### Removed
Expand Down
86 changes: 85 additions & 1 deletion pyMBE/lib/handy_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,90 @@ def get_number_of_particles(espresso_system, ptype):
kwargs = {"type": ptype}
return espresso_system.number_of_particles(*args, **kwargs)

def generate_lattice_positions(lattice_type, number_of_sites, lattice_constant=1.0, box_length=None, origin=None):
"""
Generate lattice positions for a requested lattice type and number of sites.

Args:
lattice_type ('str'):
Lattice type identifier. Supported values are:
- ``"sc"`` (simple cubic)
- ``"bcc"`` (body-centered cubic)
- ``"fcc"`` (face-centered cubic)

number_of_sites ('int'):
Number of lattice positions to generate.

lattice_constant ('float', optional):
Lattice constant. Used when ``box_length`` is not provided.
Must be positive.

box_length ('float', optional):
If provided, lattice positions are fitted into a cubic box of side
``box_length`` by choosing the cell spacing automatically.

origin ('list[float]', optional):
Origin shift applied to all generated coordinates.
Defaults to ``[0.0, 0.0, 0.0]``.

Returns:
('list[list[float]]'):
List of 3D lattice positions.

Raises:
ValueError:
If ``lattice_type`` is unsupported, ``number_of_sites`` is negative,
or geometric inputs are invalid.
"""
lattice_key = lattice_type.lower()
basis_map = {
"sc": np.array([[0.0, 0.0, 0.0]]),
"bcc": np.array([[0.0, 0.0, 0.0],
[0.5, 0.5, 0.5]]),
"fcc": np.array([[0.0, 0.0, 0.0],
[0.0, 0.5, 0.5],
[0.5, 0.0, 0.5],
[0.5, 0.5, 0.0]]),
}
if lattice_key not in basis_map:
raise ValueError(f"Unsupported lattice_type '{lattice_type}'. Supported values are {list(basis_map.keys())}.")
if number_of_sites < 0:
raise ValueError("number_of_sites must be a non-negative integer.")
if number_of_sites == 0:
return []
if origin is None:
origin = np.zeros(3)
else:
origin = np.array(origin, dtype=float)
if origin.shape != (3,):
raise ValueError("origin must be a 3D coordinate [x, y, z].")

points_per_cell = len(basis_map[lattice_key])
n_cells = int(np.ceil((number_of_sites / points_per_cell) ** (1.0 / 3.0)))
if n_cells <= 0:
n_cells = 1

if box_length is not None:
if box_length <= 0:
raise ValueError("box_length must be positive.")
spacing = float(box_length) / n_cells
else:
if lattice_constant <= 0:
raise ValueError("lattice_constant must be positive.")
spacing = float(lattice_constant)

basis = basis_map[lattice_key]
positions = []
for i in range(n_cells):
for j in range(n_cells):
for k in range(n_cells):
cell_origin = np.array([i, j, k], dtype=float) * spacing
for site in basis:
positions.append((cell_origin + site * spacing + origin).tolist())
if len(positions) == number_of_sites:
return positions
return positions

def get_residues_from_topology_dict(topology_dict, model):
"""
Groups beads from a topology dictionary into residues and assigns residue names.
Expand Down Expand Up @@ -751,4 +835,4 @@ def setup_electrostatic_interactions(units, espresso_system, kT, c_salt=None, so
espresso_system.actors.add(coulomb)
else:
espresso_system.electrostatics.solver = coulomb
logging.debug("*** Electrostatics successfully added to the system ***")
logging.debug("*** Electrostatics successfully added to the system ***")
Loading
Loading