pyMBE.pyMBE
1# 2# Copyright (C) 2023-2026 pyMBE-dev team 3# 4# This file is part of pyMBE. 5# 6# pyMBE is free software: you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation, either version 3 of the License, or 9# (at your option) any later version. 10# 11# pyMBE is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18 19import espressomd 20import re 21import json 22import pint 23import numpy as np 24import pandas as pd 25import scipy.constants 26import scipy.optimize 27import logging 28import importlib.resources 29 30# Database 31from pyMBE.storage.manager import Manager 32from pyMBE.storage.pint_quantity import PintQuantity 33## Templates 34from pyMBE.storage.templates.particle import ParticleTemplate, ParticleStateTemplate 35from pyMBE.storage.templates.residue import ResidueTemplate 36from pyMBE.storage.templates.molecule import MoleculeTemplate 37from pyMBE.storage.templates.peptide import PeptideTemplate 38from pyMBE.storage.templates.protein import ProteinTemplate 39from pyMBE.storage.templates.hydrogel import HydrogelTemplate, HydrogelNode, HydrogelChain 40from pyMBE.storage.templates.bond import BondTemplate 41from pyMBE.storage.templates.angle import AngleTemplate 42## Instances 43from pyMBE.storage.instances.particle import ParticleInstance 44from pyMBE.storage.instances.residue import ResidueInstance 45from pyMBE.storage.instances.molecule import MoleculeInstance 46from pyMBE.storage.instances.peptide import PeptideInstance 47from pyMBE.storage.instances.protein import ProteinInstance 48from pyMBE.storage.instances.bond import BondInstance 49from pyMBE.storage.instances.angle import AngleInstance 50from pyMBE.storage.instances.hydrogel import HydrogelInstance 51 52from pyMBE.simulation_builder.espresso_engine import EspressoSimulation 53from pyMBE.simulation_builder.lammps_engine import LammpsSimulation 54from pyMBE.simulation_builder.base_engine import DummyEngine 55from pyMBE.simulation_builder.engine_protocol import LammpsProtocol 56## Reactions 57from pyMBE.storage.reactions.reaction import Reaction, ReactionParticipant 58# Utilities 59import pyMBE.lib.handy_functions as hf 60import pyMBE.storage.io as io 61 62class pymbe_library(): 63 """ 64 Core library of the Molecular Builder for ESPResSo (pyMBE). 65 66 Attributes: 67 N_A ('pint.Quantity'): 68 Avogadro number. 69 70 kB ('pint.Quantity'): 71 Boltzmann constant. 72 73 e ('pint.Quantity'): 74 Elementary charge. 75 76 kT ('pint.Quantity'): 77 Thermal energy corresponding to the set temperature. 78 79 Kw ('pint.Quantity'): 80 Ionic product of water, used in G-RxMC and Donnan-related calculations. 81 82 db ('Manager'): 83 Database manager holding all pyMBE templates, instances and reactions. 84 85 rng ('numpy.random.Generator'): 86 Random number generator initialized with the provided seed. 87 88 units ('pint.UnitRegistry'): 89 Pint unit registry used for unit-aware calculations. 90 91 lattice_builder ('pyMBE.lib.lattice.LatticeBuilder'): 92 Optional lattice builder object (initialized as ''None''). 93 94 root ('importlib.resources.abc.Traversable'): 95 Root path to the pyMBE package resources. 96 """ 97 98 def __init__(self, seed, temperature=None, unit_length=None, unit_charge=None, Kw=None): 99 """ 100 Initializes the pyMBE library. 101 102 Args: 103 seed ('int'): 104 Seed for the random number generator. 105 106 temperature ('pint.Quantity', optional): 107 Simulation temperature. If ''None'', defaults to 298.15 K. 108 109 unit_length ('pint.Quantity', optional): 110 Reference length for reduced units. If ''None'', defaults to 111 0.355 nm. 112 113 unit_charge ('pint.Quantity', optional): 114 Reference charge for reduced units. If ''None'', defaults to 115 one elementary charge. 116 117 Kw ('pint.Quantity', optional): 118 Ionic product of water (typically in mol²/L²). If ''None'', 119 defaults to 1e-14 mol²/L². 120 """ 121 # Seed and RNG 122 self.seed=seed 123 self.rng = np.random.default_rng(seed) 124 self.units=pint.UnitRegistry() 125 self.N_A=scipy.constants.N_A / self.units.mol 126 self.kB=scipy.constants.k * self.units.J / self.units.K 127 self.e=scipy.constants.e * self.units.C 128 self.set_reduced_units(unit_length=unit_length, 129 unit_charge=unit_charge, 130 temperature=temperature, 131 Kw=Kw) 132 133 self.db = Manager(units=self.units) 134 self.simulation_engine = DummyEngine() 135 self.lattice_builder = None 136 self.root = importlib.resources.files(__package__) 137 138 def _check_bond_inputs(self, bond_type, bond_parameters): 139 """ 140 Checks that the input bond parameters are valid within the current pyMBE implementation. 141 142 Args: 143 bond_type ('str'): 144 label to identify the potential to model the bond. 145 146 bond_parameters ('dict'): 147 parameters of the potential of the bond. 148 """ 149 valid_bond_types = ["harmonic", "FENE"] 150 if bond_type not in valid_bond_types: 151 raise NotImplementedError(f"Bond type '{bond_type}' currently not implemented in pyMBE, accepted types are {valid_bond_types}") 152 required_parameters = {"harmonic": ["r_0","k"], 153 "FENE": ["r_0","k","d_r_max"]} 154 for required_parameter in required_parameters[bond_type]: 155 if required_parameter not in bond_parameters.keys(): 156 raise ValueError(f"Missing required parameter {required_parameter} for {bond_type} bond") 157 158 def _check_dimensionality(self, variable, expected_dimensionality): 159 """ 160 Checks if the dimensionality of 'variable' matches 'expected_dimensionality'. 161 162 Args: 163 variable ('pint.Quantity'): 164 Quantity to be checked. 165 166 expected_dimensionality ('str'): 167 Expected dimension of the variable. 168 169 Returns: 170 ('bool'): 171 'True' if the variable if of the expected dimensionality, 'False' otherwise. 172 173 Notes: 174 - 'expected_dimensionality' takes dimensionality following the Pint standards [docs](https://pint.readthedocs.io/en/0.10.1/wrapping.html?highlight=dimensionality#checking-dimensionality). 175 - For example, to check for a variable corresponding to a velocity 'expected_dimensionality = "[length]/[time]"' 176 """ 177 correct_dimensionality=variable.check(f"{expected_dimensionality}") 178 if not correct_dimensionality: 179 raise ValueError(f"The variable {variable} should have a dimensionality of {expected_dimensionality}, instead the variable has a dimensionality of {variable.dimensionality}") 180 return correct_dimensionality 181 182 def _check_pka_set(self, pka_set): 183 """ 184 Checks that 'pka_set' has the formatting expected by pyMBE. 185 186 Args: 187 pka_set ('dict'): 188 {"name" : {"pka_value": pka, "acidity": acidity}} 189 """ 190 required_keys=['pka_value','acidity'] 191 for required_key in required_keys: 192 for pka_name, pka_entry in pka_set.items(): 193 if required_key not in pka_entry: 194 raise ValueError(f'missing a required key "{required_key}" in entry "{pka_name}" of pka_set ("{pka_entry}")') 195 196 def _create_hydrogel_chain(self, hydrogel_chain, nodes,box_l, use_default_bond=False, gen_angle=False): 197 """ 198 Creates a chain between two nodes of a hydrogel. 199 200 Args: 201 hydrogel_chain ('HydrogelChain'): 202 template of a hydrogel chain 203 nodes ('dict'): 204 {node_index: {"name": node_particle_name, "pos": node_position, "id": node_particle_instance_id}} 205 box_l('list[float,float,float]'): side length of the simulation box for x,y and z coordinates. 206 use_default_bond ('bool', optional): 207 If True, use a default bond template if no specific template exists. Defaults to False. 208 209 gen_angle ('bool', optional): 210 If True, generate the angle potentials internal to the created 211 chain molecule. Junction angles near the hydrogel crosslinkers 212 are handled separately at the hydrogel level. 213 214 Return: 215 ('int'): 216 molecule_id of the created hydrogel chian. 217 218 Notes: 219 - If the chain is defined between node_start = ''[0 0 0]'' and node_end = ''[1 1 1]'', the chain will be placed between these two nodes. 220 - The chain will be placed in the direction of the vector between 'node_start' and 'node_end'. 221 """ 222 if self.lattice_builder is None: 223 raise ValueError("LatticeBuilder is not initialized. Use 'initialize_lattice_builder' first.") 224 molecule_tpl = self.db.get_template(pmb_type="molecule", 225 name=hydrogel_chain.molecule_name) 226 residue_list = molecule_tpl.residue_list 227 molecule_name = molecule_tpl.name 228 node_start = hydrogel_chain.node_start 229 node_end = hydrogel_chain.node_end 230 node_start_label = self.lattice_builder._create_node_label(node_start) 231 node_end_label = self.lattice_builder._create_node_label(node_end) 232 _, reverse = self.lattice_builder._get_node_vector_pair(node_start, node_end) 233 if node_start == node_end and residue_list != residue_list[::-1]: 234 raise ValueError(f"Aborted creation of hydrogel chain between '{node_start}' and '{node_end}' because pyMBE could not resolve a unique topology for that chain") 235 if reverse: 236 reverse_residue_order=True 237 else: 238 reverse_residue_order=False 239 start_node_id = nodes[node_start_label]["id"] 240 end_node_id = nodes[node_end_label]["id"] 241 # Finding a backbone vector between node_start and node_end 242 vec_between_nodes = np.array(nodes[node_end_label]["pos"]) - np.array(nodes[node_start_label]["pos"]) 243 vec_between_nodes = vec_between_nodes - self.lattice_builder.box_l * np.round(vec_between_nodes/self.lattice_builder.box_l) 244 backbone_vector = vec_between_nodes / (self.lattice_builder.mpc+1) 245 if reverse_residue_order: 246 vec_between_nodes *= -1.0 247 # Calculate the start position of the chain 248 chain_residues = self.db.get_template(pmb_type="molecule", 249 name=molecule_name).residue_list 250 part_start_chain_name = self.db.get_template(pmb_type="residue", 251 name=chain_residues[0]).central_bead 252 lj_parameters = self.get_lj_parameters(particle_name1=nodes[node_start_label]["name"], 253 particle_name2=part_start_chain_name) 254 bond_tpl = self.get_bond_template(particle_name1=nodes[node_start_label]["name"], 255 particle_name2=part_start_chain_name, 256 use_default_bond=use_default_bond) 257 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 258 bond_type=bond_tpl.bond_type, 259 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 260 first_bead_pos = np.array((nodes[node_start_label]["pos"])) + np.array(backbone_vector)*l0 261 mol_id = self.create_molecule(name=molecule_name, # Use the name defined earlier 262 number_of_molecules=1, # Creating one chain 263 box_l=box_l, ### Add lattice_builder box length size, this should be box_l=[self.lattice_builder.box_l]*3 264 list_of_first_residue_positions=[first_bead_pos.tolist()], #Start at the first node 265 backbone_vector=np.array(backbone_vector)/l0, 266 use_default_bond=use_default_bond, 267 reverse_residue_order=reverse_residue_order, 268 gen_angle=gen_angle)[0] 269 chain_pids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 270 attribute="molecule_id", 271 value=mol_id) 272 self.create_bond(particle_id1=start_node_id,particle_id2=chain_pids[0],use_default_bond=use_default_bond) 273 self.create_bond(particle_id1=chain_pids[-1],particle_id2=end_node_id,use_default_bond=use_default_bond) 274 return mol_id 275 276 def _generate_hydrogel_crosslinker_angles(self, central_particle_ids): 277 """ 278 Generate hydrogel angles centered on crosslinkers and adjacent terminal beads. 279 280 If the user defines any explicit angle template for such junction 281 triplets, then all required junction triplets must be defined. If none 282 are defined, hydrogel construction proceeds without crosslinker-adjacent 283 angles. 284 """ 285 particle_instances = self.db.get_instances(pmb_type="particle") 286 bonded_neighbors = {} 287 for bond in self.db.get_instances(pmb_type="bond").values(): 288 bonded_neighbors.setdefault(bond.particle_id1, set()).add(bond.particle_id2) 289 bonded_neighbors.setdefault(bond.particle_id2, set()).add(bond.particle_id1) 290 291 triplets = [] 292 for central_particle_id in sorted(set(central_particle_ids)): 293 neighbors = sorted(bonded_neighbors.get(central_particle_id, set())) 294 central_name = particle_instances[central_particle_id].name 295 for idx_i in range(len(neighbors)): 296 for idx_k in range(idx_i + 1, len(neighbors)): 297 side_particle_id1 = neighbors[idx_i] 298 side_particle_id3 = neighbors[idx_k] 299 side_name1 = particle_instances[side_particle_id1].name 300 side_name3 = particle_instances[side_particle_id3].name 301 angle_key = AngleTemplate.make_angle_key(side1=side_name1, 302 central=central_name, 303 side2=side_name3) 304 triplets.append((side_particle_id1, 305 central_particle_id, 306 side_particle_id3, 307 angle_key)) 308 309 defined_angle_templates = self.db.get_templates(pmb_type="angle") 310 defined_angle_keys = {angle_key for _, _, _, angle_key in triplets if angle_key in defined_angle_templates} 311 if not defined_angle_keys: 312 logging.warning("No angle templates defined for hydrogel crosslinkers") 313 return 314 missing_angle_keys = sorted({angle_key for _, _, _, angle_key in triplets if angle_key not in defined_angle_keys}) 315 if missing_angle_keys: 316 raise ValueError("Hydrogel crosslinker-adjacent angle templates must be defined for all required triplets. " 317 f"Missing definitions for: {missing_angle_keys}") 318 for side_particle_id1, central_particle_id, side_particle_id3, _ in triplets: 319 self.create_angular_potential(particle_id1=side_particle_id1, 320 particle_id2=central_particle_id, 321 particle_id3=side_particle_id3, 322 use_default_angle=False) 323 324 def _create_hydrogel_node(self, node_index, node_name,box_l): 325 """ 326 Set a node residue type. 327 328 Args: 329 node_index ('str'): 330 Lattice node index in the form of a string, e.g. "[0 0 0]". 331 332 node_name ('str'): 333 name of the node particle defined in pyMBE. 334 335 box_l('list[float,float,float]'): list of floats with the dimensions of the box 336 337 Returns: 338 ('tuple(list,int)'): 339 ('list'): Position of the node in the lattice. 340 ('int'): Particle ID of the node. 341 """ 342 if self.lattice_builder is None: 343 raise ValueError("LatticeBuilder is not initialized. Use 'initialize_lattice_builder' first.") 344 node_position = np.array(node_index)*0.25*self.lattice_builder.box_l 345 p_id = self.create_particle(name = node_name, 346 box_l=box_l, 347 number_of_particles=1, 348 position = [node_position]) 349 key = self.lattice_builder._get_node_by_label(f"[{node_index[0]} {node_index[1]} {node_index[2]}]") 350 self.lattice_builder.nodes[key] = node_name 351 return node_position.tolist(), p_id[0] 352 353 def _get_residue_list_from_sequence(self, sequence): 354 """ 355 Convenience function to get a 'residue_list' from a protein or peptide 'sequence'. 356 357 Args: 358 sequence ('lst'): 359 Sequence of the peptide or protein. 360 361 Returns: 362 residue_list ('list' of 'str'): 363 List of the 'name's of the 'residue's in the sequence of the 'molecule'. 364 """ 365 residue_list = [] 366 for item in sequence: 367 residue_name='AA-'+item 368 residue_list.append(residue_name) 369 return residue_list 370 371 def _get_template_type(self, name, allowed_types): 372 """ 373 Validate that a template name resolves unambiguously to exactly one 374 allowed pmb_type in the pyMBE database and return it. 375 376 Args: 377 name ('str'): 378 Name of the template to validate. 379 380 allowed_types ('set[str]'): 381 Set of allowed pmb_type values (e.g. {"molecule", "peptide"}). 382 383 Returns: 384 ('str'): 385 Resolved pmb_type. 386 387 Notes: 388 - This method does *not* return the template itself, only the validated pmb_type. 389 """ 390 registered_pmb_types_with_name = self.db._find_template_types(name=name) 391 filtered_types = allowed_types.intersection(registered_pmb_types_with_name) 392 if len(filtered_types) > 1: 393 raise ValueError(f"Ambiguous template name '{name}': found {len(filtered_types)} templates in the pyMBE database. Molecule creation aborted.") 394 if len(filtered_types) == 0: 395 raise ValueError(f"No {allowed_types} template found with name '{name}'. Found templates of types: {filtered_types}.") 396 return next(iter(filtered_types)) 397 398 def _delete_particles_from_engine(self, particle_ids): 399 """ 400 Remove a list of particles from an ESPResSo simulation system. 401 402 Args: 403 particle_ids ('Iterable[int]'): 404 A list (or other iterable) of ESPResSo particle IDs to remove. 405 406 Notes: 407 - This method removes particles only from the ESPResSo simulation, 408 **not** from the pyMBE database. Database cleanup must be handled 409 separately by the caller. 410 - Attempting to remove a non-existent particle ID will raise 411 an ESPResSo error. 412 """ 413 self.simulation_engine._delete_particles(particle_ids) 414 415 def add_instances_to_engine(self): 416 self.simulation_engine.add_instances_to_engine() 417 418 def calculate_center_of_mass(self, instance_id, pmb_type): 419 """ 420 Calculates the center of mass of a pyMBE object instance in an ESPResSo system. 421 422 Args: 423 instance_id ('int'): 424 pyMBE instance ID of the object whose center of mass is calculated. 425 426 pmb_type ('str'): 427 Type of the pyMBE object. Must correspond to a particle-aggregating 428 template type (e.g. '"molecule"', '"residue"', '"peptide"', '"protein"'). 429 430 Returns: 431 ('numpy.ndarray'): 432 Array of shape '(3,)' containing the Cartesian coordinates of the 433 center of mass. 434 435 Notes: 436 - This method assumes equal mass for all particles. 437 - Periodic boundary conditions are *not* unfolded; positions are taken 438 directly from ESPResSo particle coordinates. 439 """ 440 return self.simulation_engine.calculate_center_of_mass(instance_id=instance_id, 441 pmb_type=pmb_type) 442 443 def calculate_HH(self, template_name, pH_list=None, pka_set=None): 444 """ 445 Calculates the charge in the template object according to the ideal Henderson–Hasselbalch titration curve. 446 447 Args: 448 template_name ('str'): 449 Name of the template. 450 451 pH_list ('list[float]', optional): 452 pH values at which the charge is evaluated. 453 Defaults to 50 values between 2 and 12. 454 455 pka_set ('dict', optional): 456 Mapping: {particle_name: {"pka_value": 'float', "acidity": "acidic"|"basic"}} 457 458 Returns: 459 'list[float]': 460 Net molecular charge at each pH value. 461 """ 462 if pH_list is None: 463 pH_list = np.linspace(2, 12, 50) 464 if pka_set is None: 465 pka_set = self.get_pka_set() 466 self._check_pka_set(pka_set=pka_set) 467 particle_counts = self.db.get_particle_templates_under(template_name=template_name, 468 return_counts=True) 469 if not particle_counts: 470 return [None] * len(pH_list) 471 charge_number_map = self.get_charge_number_map() 472 def formal_charge(particle_name): 473 tpl = self.db.get_template(name=particle_name, 474 pmb_type="particle") 475 state = self.db.get_template(name=tpl.initial_state, 476 pmb_type="particle_state") 477 return charge_number_map[state.es_type] 478 Z_HH = [] 479 for pH in pH_list: 480 Z = 0.0 481 for particle, multiplicity in particle_counts.items(): 482 if particle in pka_set: 483 pka = pka_set[particle]["pka_value"] 484 acidity = pka_set[particle]["acidity"] 485 if acidity == "acidic": 486 psi = -1 487 elif acidity == "basic": 488 psi = +1 489 else: 490 raise ValueError(f"Unknown acidity '{acidity}' for particle '{particle}'") 491 charge = psi / (1.0 + 10.0 ** (psi * (pH - pka))) 492 Z += multiplicity * charge 493 else: 494 Z += multiplicity * formal_charge(particle) 495 Z_HH.append(Z) 496 return Z_HH 497 498 def calculate_HH_Donnan(self, c_macro, c_salt, pH_list=None, pka_set=None): 499 """ 500 Computes macromolecular charges using the Henderson–Hasselbalch equation 501 coupled to ideal Donnan partitioning. 502 503 Args: 504 c_macro ('dict'): 505 Mapping of macromolecular species names to their concentrations 506 in the system: 507 '{molecule_name: concentration}'. 508 509 c_salt ('float' or 'pint.Quantity'): 510 Salt concentration in the reservoir. 511 512 pH_list ('list[float]', optional): 513 List of pH values in the reservoir at which the calculation is 514 performed. If 'None', 50 equally spaced values between 2 and 12 515 are used. 516 517 pka_set ('dict', optional): 518 Dictionary defining the acid–base properties of titratable particle 519 types: 520 '{particle_name: {"pka_value": float, "acidity": "acidic" | "basic"}}'. 521 If 'None', the pKa set is taken from the pyMBE database. 522 523 Returns: 524 'dict': 525 Dictionary containing: 526 - '"charges_dict"' ('dict'): 527 Mapping '{molecule_name: list}' of Henderson–Hasselbalch–Donnan 528 charges evaluated at each pH value. 529 - '"pH_system_list"' ('list[float]'): 530 Effective pH values inside the system phase after Donnan 531 partitioning. 532 - '"partition_coefficients"' ('list[float]'): 533 Partition coefficients of monovalent cations at each pH value. 534 535 Notes: 536 - This method assumes **ideal Donnan equilibrium** and **monovalent salt**. 537 - The ionic strength of the reservoir includes both salt and 538 pH-dependent H⁺/OH⁻ contributions. 539 - All charged macromolecular species present in the system must be 540 included in 'c_macro'; missing species will lead to incorrect results. 541 - The nonlinear Donnan equilibrium equation is solved using a scalar 542 root finder ('brentq') in logarithmic form for numerical stability. 543 - This method is intended for **two-phase systems**; for single-phase 544 systems use 'calculate_HH' instead. 545 """ 546 if pH_list is None: 547 pH_list=np.linspace(2,12,50) 548 if pka_set is None: 549 pka_set=self.get_pka_set() 550 self._check_pka_set(pka_set=pka_set) 551 partition_coefficients_list = [] 552 pH_system_list = [] 553 Z_HH_Donnan={} 554 for key in c_macro: 555 Z_HH_Donnan[key] = [] 556 def calc_charges(c_macro, pH): 557 """ 558 Calculates the charges of the different kinds of molecules according to the Henderson-Hasselbalch equation. 559 560 Args: 561 c_macro ('dict'): 562 {"name": concentration} - A dict containing the concentrations of all charged macromolecular species in the system. 563 564 pH ('float'): 565 pH-value that is used in the HH equation. 566 567 Returns: 568 ('dict'): 569 {"molecule_name": charge} 570 """ 571 charge = {} 572 for name in c_macro: 573 charge[name] = self.calculate_HH(name, [pH], pka_set)[0] 574 return charge 575 576 def calc_partition_coefficient(charge, c_macro): 577 """ 578 Calculates the partition coefficients of positive ions according to the ideal Donnan theory. 579 580 Args: 581 charge ('dict'): 582 {"molecule_name": charge} 583 584 c_macro ('dict'): 585 {"name": concentration} - A dict containing the concentrations of all charged macromolecular species in the system. 586 """ 587 nonlocal ionic_strength_res 588 charge_density = 0.0 589 for key in charge: 590 charge_density += charge[key] * c_macro[key] 591 return (-charge_density / (2 * ionic_strength_res) + np.sqrt((charge_density / (2 * ionic_strength_res))**2 + 1)).magnitude 592 for pH_value in pH_list: 593 # calculate the ionic strength of the reservoir 594 if pH_value <= 7.0: 595 ionic_strength_res = 10 ** (-pH_value) * self.units.mol/self.units.l + c_salt 596 elif pH_value > 7.0: 597 ionic_strength_res = 10 ** (-(14-pH_value)) * self.units.mol/self.units.l + c_salt 598 #Determine the partition coefficient of positive ions by solving the system of nonlinear, coupled equations 599 #consisting of the partition coefficient given by the ideal Donnan theory and the Henderson-Hasselbalch equation. 600 #The nonlinear equation is formulated for log(xi) since log-operations are not supported for RootResult objects. 601 equation = lambda logxi: logxi - np.log10(calc_partition_coefficient(calc_charges(c_macro, pH_value - logxi), c_macro)) 602 logxi = scipy.optimize.root_scalar(equation, bracket=[-1e2, 1e2], method="brentq") 603 partition_coefficient = 10**logxi.root 604 charges_temp = calc_charges(c_macro, pH_value-np.log10(partition_coefficient)) 605 for key in c_macro: 606 Z_HH_Donnan[key].append(charges_temp[key]) 607 pH_system_list.append(pH_value - np.log10(partition_coefficient)) 608 partition_coefficients_list.append(partition_coefficient) 609 return {"charges_dict": Z_HH_Donnan, "pH_system_list": pH_system_list, "partition_coefficients": partition_coefficients_list} 610 611 def calculate_net_charge(self,object_name,pmb_type,dimensionless=False): 612 """ 613 Calculates the net charge per instance of a given pmb object type. 614 615 Args: 616 object_name (str): 617 Name of the object (e.g. molecule, residue, peptide, protein). 618 pmb_type (str): 619 Type of object to analyze. Must be molecule-like. 620 dimensionless (bool, optional): 621 If True, return charge as a pure number. 622 If False, return a quantity with reduced_charge units. 623 624 Returns: 625 dict: 626 {"mean": mean_net_charge, "instances": {instance_id: net_charge}} 627 """ 628 return self.simulation_engine.calculate_net_charge(object_name, 629 pmb_type, 630 dimensionless) 631 632 def center_object_in_simulation_box(self, instance_id, box_l,pmb_type): 633 """ 634 Centers a pyMBE object instance in the simulation box of an ESPResSo system. 635 The object is translated such that its center of mass coincides with the 636 geometric center of the ESPResSo simulation box. 637 638 Args: 639 instance_id ('int'): 640 ID of the pyMBE object instance to be centered. 641 642 box_l('list[float,float,float]'): list of floats with the dimensions of the box 643 644 pmb_type ('str'): 645 Type of the pyMBE object. 646 647 Notes: 648 - Works for both cubic and non-cubic simulation boxes. 649 """ 650 inst = self.db.get_instance(instance_id=instance_id, 651 pmb_type=pmb_type) 652 center_of_mass = self.calculate_center_of_mass(instance_id=instance_id, 653 pmb_type=pmb_type) 654 box_center = [box_l[0]/2.0, 655 box_l[1]/2.0, 656 box_l[2]/2.0] 657 particle_id_list = self.get_particle_id_map(object_name=inst.name)["all"] 658 for pid in particle_id_list: 659 es_pos=self.db.get_instance(instance_id=pid, 660 pmb_type='particle').position 661 centered_position=es_pos - center_of_mass + box_center 662 663 self.db._update_instance(instance_id=pid, 664 pmb_type='particle', 665 attribute='position', 666 value=centered_position) 667 if isinstance(self.simulation_engine, EspressoSimulation): 668 self.simulation_engine._update_particle_position( 669 particle_id=pid, position=centered_position) 670 671 def create_added_salt(self, box_l, cation_name, anion_name, c_salt): 672 """ 673 Creates a 'c_salt' concentration of 'cation_name' and 'anion_name' ions into the 'espresso_system'. 674 675 Args: 676 cation_name('str'): 'name' of a particle with a positive charge. 677 anion_name('str'): 'name' of a particle with a negative charge. 678 c_salt('float'): Salt concentration. 679 680 Returns: 681 c_salt_calculated('float'): Calculated salt concentration added to 'espresso_system'. 682 """ 683 cation_tpl = self.db.get_template(pmb_type="particle", 684 name=cation_name) 685 cation_state = self.db.get_template(pmb_type="particle_state", 686 name=cation_tpl.initial_state) 687 cation_charge = cation_state.z 688 anion_tpl = self.db.get_template(pmb_type="particle", 689 name=anion_name) 690 anion_state = self.db.get_template(pmb_type="particle_state", 691 name=anion_tpl.initial_state) 692 anion_charge = anion_state.z 693 if cation_charge <= 0: 694 raise ValueError(f'ERROR cation charge must be positive, charge {cation_charge}') 695 if anion_charge >= 0: 696 raise ValueError(f'ERROR anion charge must be negative, charge {anion_charge}') 697 # Calculate the number of ions in the simulation box 698 volume=self.units.Quantity(np.prod(box_l), 'reduced_length**3') 699 if c_salt.check('[substance] [length]**-3'): 700 N_ions= int((volume*c_salt.to('mol/reduced_length**3')*self.N_A).magnitude) 701 c_salt_calculated=N_ions/(volume*self.N_A) 702 elif c_salt.check('[length]**-3'): 703 N_ions= int((volume*c_salt.to('reduced_length**-3')).magnitude) 704 c_salt_calculated=N_ions/volume 705 else: 706 raise ValueError('Unknown units for c_salt, please provided it in [mol / volume] or [particle / volume]', c_salt) 707 N_cation = N_ions*abs(anion_charge) 708 N_anion = N_ions*abs(cation_charge) 709 self.create_particle(box_l=box_l, 710 name=cation_name, 711 number_of_particles=N_cation) 712 self.create_particle(box_l=box_l, 713 name=anion_name, 714 number_of_particles=N_anion) 715 if c_salt_calculated.check('[substance] [length]**-3'): 716 logging.info(f"added salt concentration of {c_salt_calculated.to('mol/L')} given by {N_cation} cations and {N_anion} anions") 717 elif c_salt_calculated.check('[length]**-3'): 718 logging.info(f"added salt concentration of {c_salt_calculated.to('reduced_length**-3')} given by {N_cation} cations and {N_anion} anions") 719 return c_salt_calculated 720 721 def create_bond(self, particle_id1, particle_id2, use_default_bond=False): 722 """ 723 Creates a bond between two particle instances in an ESPResSo system and registers it in the pyMBE database. 724 725 This method performs the following steps: 726 1. Retrieves the particle instances corresponding to 'particle_id1' and 'particle_id2' from the database. 727 2. Retrieves or creates the corresponding ESPResSo bond instance using the bond template. 728 3. Adds the ESPResSo bond instance to the ESPResSo system if it was newly created. 729 4. Adds the bond to the first particle's bond list in ESPResSo. 730 5. Creates a 'BondInstance' in the database and registers it. 731 732 Args: 733 particle_id1 ('int'): 734 pyMBE and ESPResSo ID of the first particle. 735 736 particle_id2 ('int'): 737 pyMBE and ESPResSo ID of the second particle. 738 739 use_default_bond ('bool', optional): 740 If True, use a default bond template if no specific template exists. Defaults to False. 741 742 Returns: 743 ('int'): 744 bond_id of the bond instance created in the pyMBE database. 745 """ 746 particle_inst_1 = self.db.get_instance(pmb_type="particle", 747 instance_id=particle_id1) 748 particle_inst_2 = self.db.get_instance(pmb_type="particle", 749 instance_id=particle_id2) 750 bond_tpl = self.get_bond_template(particle_name1=particle_inst_1.name, 751 particle_name2=particle_inst_2.name, 752 use_default_bond=use_default_bond) 753 bond_id = self.db._propose_instance_id(pmb_type="bond") 754 pmb_bond_instance = BondInstance(bond_id=bond_id, 755 name=bond_tpl.name, 756 particle_id1=particle_id1, 757 particle_id2=particle_id2) 758 self.db._register_instance(instance=pmb_bond_instance) 759 760 def create_counterions(self, object_name, cation_name, anion_name, box_l): 761 """ 762 Creates particles of 'cation_name' and 'anion_name' in 'espresso_system' to counter the net charge of 'object_name'. 763 764 Args: 765 object_name ('str'): 766 'name' of a pyMBE object. 767 768 cation_name ('str'): 769 'name' of a particle with a positive charge. 770 771 anion_name ('str'): 772 'name' of a particle with a negative charge. 773 774 box_l('list[float,float,float]'): list of floats with the dimensions of the box 775 776 Returns: 777 ('dict'): 778 {"name": number} 779 780 Notes: 781 This function currently does not support the creation of counterions for hydrogels. 782 """ 783 cation_tpl = self.db.get_template(pmb_type="particle", 784 name=cation_name) 785 cation_state = self.db.get_template(pmb_type="particle_state", 786 name=cation_tpl.initial_state) 787 cation_charge = cation_state.z 788 anion_tpl = self.db.get_template(pmb_type="particle", 789 name=anion_name) 790 anion_state = self.db.get_template(pmb_type="particle_state", 791 name=anion_tpl.initial_state) 792 anion_charge = anion_state.z 793 object_ids = self.get_particle_id_map(object_name=object_name)["all"] 794 counterion_number={} 795 object_charge={} 796 for name in ['positive', 'negative']: 797 object_charge[name]=0 798 for id in object_ids: 799 object_name = self.db.get_instance(pmb_type="particle", 800 instance_id=id).name 801 object_tpl = self.db.get_template(pmb_type="particle", 802 name=object_name) 803 object_state = self.db.get_template(pmb_type="particle_state", 804 name=object_tpl.initial_state) 805 object_z = object_state.z 806 if object_z > 0: 807 object_charge['positive']+=1*(np.abs(object_z )) 808 elif object_z < 0: 809 object_charge['negative']+=1*(np.abs(object_z )) 810 if object_charge['positive'] % abs(anion_charge) == 0: 811 counterion_number[anion_name]=int(object_charge['positive']/abs(anion_charge)) 812 else: 813 raise ValueError('The number of positive charges in the pmb_object must be divisible by the charge of the anion') 814 if object_charge['negative'] % abs(cation_charge) == 0: 815 counterion_number[cation_name]=int(object_charge['negative']/cation_charge) 816 else: 817 raise ValueError('The number of negative charges in the pmb_object must be divisible by the charge of the cation') 818 if counterion_number[cation_name] > 0: 819 self.create_particle(box_l=box_l, 820 name=cation_name, 821 number_of_particles=counterion_number[cation_name]) 822 else: 823 counterion_number[cation_name]=0 824 if counterion_number[anion_name] > 0: 825 self.create_particle(box_l=box_l, 826 name=anion_name, 827 number_of_particles=counterion_number[anion_name]) 828 else: 829 counterion_number[anion_name] = 0 830 logging.info('the following counter-ions have been created: ') 831 for name in counterion_number.keys(): 832 logging.info(f'Ion type: {name} created number: {counterion_number[name]}') 833 return counterion_number 834 835 836 def create_hydrogel(self, name, box_l, use_default_bond=False, gen_angle=False): 837 """ 838 Creates a hydrogel in espresso_system using a pyMBE hydrogel template given by 'name' 839 840 Args: 841 box_l('list[float,float,float]'): list of floats with the dimensions of the box 842 843 name ('str'): 844 name of the hydrogel template in the pyMBE database. 845 846 use_default_bond ('bool', optional): 847 If True, use a default bond template if no specific template exists. Defaults to False. 848 849 gen_angle ('bool', optional): 850 If True, generate angle potentials for the internal hydrogel 851 chains and, when explicitly defined, for all crosslinker-adjacent 852 triplets. Defaults to False. 853 854 Returns: 855 ('int'): id of the hydrogel instance created. 856 """ 857 if not self.db._has_template(name=name, pmb_type="hydrogel"): 858 raise ValueError(f"Hydrogel template with name '{name}' is not defined in the pyMBE database.") 859 hydrogel_tpl = self.db.get_template(pmb_type="hydrogel", 860 name=name) 861 assembly_id = self.db._propose_instance_id(pmb_type="hydrogel") 862 # Create the nodes 863 nodes = {} 864 hydrogel_angle_centers = set() 865 node_topology = hydrogel_tpl.node_map 866 for node in node_topology: 867 node_index = node.lattice_index 868 node_name = node.particle_name 869 node_pos, node_id = self._create_hydrogel_node(node_index=node_index, 870 node_name=node_name, 871 box_l=box_l) 872 node_label = self.lattice_builder._create_node_label(node_index=node_index) 873 nodes[node_label] = {"name": node_name, "id": node_id, "pos": node_pos} 874 self.db._update_instance(instance_id=node_id, 875 pmb_type="particle", 876 attribute="assembly_id", 877 value=assembly_id) 878 for hydrogel_chain in hydrogel_tpl.chain_map: 879 molecule_id = self._create_hydrogel_chain(hydrogel_chain=hydrogel_chain, 880 nodes=nodes, 881 box_l=box_l, 882 use_default_bond=use_default_bond, 883 gen_angle=gen_angle, 884 ) 885 self.db._update_instance(instance_id=molecule_id, 886 pmb_type="molecule", 887 attribute="assembly_id", 888 value=assembly_id) 889 if gen_angle: 890 residue_ids = self.db._find_instance_ids_by_attribute(pmb_type="residue", 891 attribute="molecule_id", 892 value=molecule_id) 893 first_residue_id = min(residue_ids) 894 last_residue_id = max(residue_ids) 895 first_residue = self.db.get_instance(pmb_type="residue", 896 instance_id=first_residue_id) 897 last_residue = self.db.get_instance(pmb_type="residue", 898 instance_id=last_residue_id) 899 first_central_bead_name = self.db.get_template(pmb_type="residue", 900 name=first_residue.name).central_bead 901 last_central_bead_name = self.db.get_template(pmb_type="residue", 902 name=last_residue.name).central_bead 903 particle_instances = self.db.get_instances(pmb_type="particle") 904 first_residue_particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 905 attribute="residue_id", 906 value=first_residue_id) 907 last_residue_particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 908 attribute="residue_id", 909 value=last_residue_id) 910 first_bead_id = None 911 for particle_id in first_residue_particle_ids: 912 if particle_instances[particle_id].name == first_central_bead_name: 913 first_bead_id = particle_id 914 break 915 916 last_bead_id = None 917 for particle_id in last_residue_particle_ids: 918 if particle_instances[particle_id].name == last_central_bead_name: 919 last_bead_id = particle_id 920 break 921 node_start_label = self.lattice_builder._create_node_label(hydrogel_chain.node_start) 922 node_end_label = self.lattice_builder._create_node_label(hydrogel_chain.node_end) 923 hydrogel_angle_centers.update({ 924 nodes[node_start_label]["id"], 925 nodes[node_end_label]["id"], 926 first_bead_id, 927 last_bead_id, 928 }) 929 self.db._propagate_id(root_type="hydrogel", 930 root_id=assembly_id, 931 attribute="assembly_id", 932 value=assembly_id) 933 if gen_angle: 934 self._generate_hydrogel_crosslinker_angles( 935 central_particle_ids=hydrogel_angle_centers) 936 # Register an hydrogel instance in the pyMBE databasegit 937 self.db._register_instance(HydrogelInstance(name=name, 938 assembly_id=assembly_id)) 939 return assembly_id 940 941 942 def create_molecule(self, name, number_of_molecules, box_l, list_of_first_residue_positions=None, backbone_vector=None, use_default_bond=False, reverse_residue_order = False, gen_angle=False): 943 """ 944 Creates instances of a given molecule template name into ESPResSo. 945 946 Args: 947 name ('str'): 948 Label of the molecule type to be created. 'name'. 949 950 box_l('list[float,float,float]'): list of floats with the dimensions of the box 951 952 number_of_molecules ('int'): 953 Number of molecules or peptides of type 'name' to be created. 954 955 list_of_first_residue_positions ('list', optional): 956 List of coordinates where the central bead of the first_residue_position will be created, random by default. 957 958 backbone_vector ('list' of 'float'): 959 Backbone vector of the molecule, random by default. Central beads of the residues in the 'residue_list' are placed along this vector. 960 961 use_default_bond('bool', optional): 962 Controls if a bond of type 'default' is used to bond particles with undefined bonds in the pyMBE database. 963 964 reverse_residue_order('bool', optional): 965 Creates residues in reverse sequential order than the one defined in the molecule template. Defaults to False. 966 967 Returns: 968 ('list' of 'int'): 969 List with the 'molecule_id' of the pyMBE molecule instances created into 'espresso_system'. 970 971 Notes: 972 - This function can be used to create both molecules and peptides. 973 """ 974 pmb_type = self._get_template_type(name=name, 975 allowed_types={"molecule", "peptide"}) 976 if number_of_molecules <= 0: 977 return {} 978 if list_of_first_residue_positions is not None: 979 for item in list_of_first_residue_positions: 980 if not isinstance(item, list): 981 raise ValueError("The provided input position is not a nested list. Should be a nested list with elements of 3D lists, corresponding to xyz coord.") 982 elif len(item) != 3: 983 raise ValueError("The provided input position is formatted wrong. The elements in the provided list does not have 3 coordinates, corresponding to xyz coord.") 984 985 if len(list_of_first_residue_positions) != number_of_molecules: 986 raise ValueError(f"Number of positions provided in {list_of_first_residue_positions} does not match number of molecules desired, {number_of_molecules}") 987 # Generate an arbitrary random unit vector 988 if backbone_vector is None: 989 backbone_vector = self.generate_random_points_in_a_sphere(center=[0,0,0], 990 radius=1, 991 n_samples=1, 992 on_surface=True)[0] 993 else: 994 backbone_vector = np.array(backbone_vector) 995 first_residue = True 996 molecule_tpl = self.db.get_template(pmb_type=pmb_type, 997 name=name) 998 if reverse_residue_order: 999 residue_list = molecule_tpl.residue_list[::-1] 1000 else: 1001 residue_list = molecule_tpl.residue_list 1002 pos_index = 0 1003 molecule_ids = [] 1004 for n_mol in range(number_of_molecules): 1005 molecule_id = self.db._propose_instance_id(pmb_type=pmb_type) 1006 for residue in residue_list: 1007 if first_residue: 1008 if list_of_first_residue_positions is None: 1009 central_bead_pos = None 1010 else: 1011 central_bead_pos = [np.array(list_of_first_residue_positions[n_mol])] 1012 1013 residue_id = self.create_residue(name=residue, 1014 box_l=box_l, 1015 central_bead_position=central_bead_pos, 1016 use_default_bond= use_default_bond, 1017 backbone_vector=backbone_vector) 1018 1019 # Add molecule_id to the residue instance and all particles associated 1020 self.db._propagate_id(root_type="residue", 1021 root_id=residue_id, 1022 attribute="molecule_id", 1023 value=molecule_id) 1024 particle_ids_in_residue = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1025 attribute="residue_id", 1026 value=residue_id) 1027 prev_central_bead_id = particle_ids_in_residue[0] 1028 prev_central_bead_name = self.db.get_instance(pmb_type="particle", 1029 instance_id=prev_central_bead_id).name 1030 prev_central_bead_pos = self.db.get_instance(pmb_type="particle", 1031 instance_id=prev_central_bead_id).position 1032 # prev_central_bead_pos = espresso_system.part.by_id(prev_central_bead_id).pos 1033 first_residue = False 1034 else: 1035 1036 # Calculate the starting position of the new residue 1037 residue_tpl = self.db.get_template(pmb_type="residue", 1038 name=residue) 1039 lj_parameters = self.get_lj_parameters(particle_name1=prev_central_bead_name, 1040 particle_name2=residue_tpl.central_bead) 1041 bond_tpl = self.get_bond_template(particle_name1=prev_central_bead_name, 1042 particle_name2=residue_tpl.central_bead, 1043 use_default_bond=use_default_bond) 1044 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1045 bond_type=bond_tpl.bond_type, 1046 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1047 central_bead_pos = prev_central_bead_pos+backbone_vector*l0 1048 # Create the residue 1049 residue_id = self.create_residue(name=residue, 1050 box_l=box_l, 1051 central_bead_position=[central_bead_pos], 1052 use_default_bond= use_default_bond, 1053 backbone_vector=backbone_vector) 1054 # Add molecule_id to the residue instance and all particles associated 1055 self.db._propagate_id(root_type="residue", 1056 root_id=residue_id, 1057 attribute="molecule_id", 1058 value=molecule_id) 1059 particle_ids_in_residue = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1060 attribute="residue_id", 1061 value=residue_id) 1062 central_bead_id = particle_ids_in_residue[0] 1063 1064 # Bond the central beads of the new and previous residues 1065 self.create_bond(particle_id1=prev_central_bead_id, 1066 particle_id2=central_bead_id, 1067 use_default_bond=use_default_bond) 1068 1069 prev_central_bead_id = central_bead_id 1070 prev_central_bead_name = self.db.get_instance(pmb_type="particle", instance_id=central_bead_id).name 1071 prev_central_bead_pos =central_bead_pos 1072 # Create a Peptide or Molecule instance and register it on the pyMBE database 1073 if pmb_type == "molecule": 1074 inst = MoleculeInstance(molecule_id=molecule_id, 1075 name=name) 1076 elif pmb_type == "peptide": 1077 inst = PeptideInstance(name=name, 1078 molecule_id=molecule_id) 1079 self.db._register_instance(inst) 1080 if gen_angle: 1081 self._generate_angles_for_entity( 1082 entity_id=molecule_id, 1083 entity_id_col='molecule_id') 1084 first_residue = True 1085 pos_index+=1 1086 molecule_ids.append(molecule_id) 1087 return molecule_ids 1088 1089 def create_particle(self, name, box_l, number_of_particles, position=None, fix=False): 1090 """ 1091 Creates one or more particles in an ESPResSo system based on the particle template in the pyMBE database. 1092 1093 Args: 1094 name ('str'): 1095 Label of the particle template in the pyMBE database. 1096 1097 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1098 1099 number_of_particles ('int'): 1100 Number of particles to be created. 1101 1102 position (list of ['float','float','float'], optional): 1103 Initial positions of the particles. If not given, particles are created in random positions. Defaults to None. 1104 1105 fix ('bool', optional): 1106 Controls if the particle motion is frozen in the integrator, it is used to create rigid objects. Defaults to False. 1107 1108 Returns: 1109 ('list' of 'int'): 1110 List with the ids of the particles created into 'espresso_system'. 1111 """ 1112 if number_of_particles <=0: 1113 return [] 1114 if not self.db._has_template(name=name, pmb_type="particle"): 1115 raise ValueError(f"Particle template with name '{name}' is not defined in the pyMBE database.") 1116 1117 part_tpl = self.db.get_template(pmb_type="particle", 1118 name=name) 1119 part_state = self.db.get_template(pmb_type="particle_state", 1120 name=part_tpl.initial_state) 1121 name_state=part_state.name 1122 1123 if fix is False: 1124 fix=[fix]*3 1125 1126 created_pid_list=[] 1127 for index in range(number_of_particles): 1128 if position is None: 1129 particle_position = self.rng.random((1, 3))[0] *np.copy(box_l) 1130 else: 1131 particle_position = np.array(position[index]) 1132 1133 particle_id = self.db._propose_instance_id(pmb_type="particle") 1134 created_pid_list.append(particle_id) 1135 part_inst = ParticleInstance(name=name, 1136 particle_id=particle_id, 1137 initial_state=name_state, 1138 position=particle_position, 1139 fix=fix) 1140 self.db._register_instance(part_inst) 1141 1142 return created_pid_list 1143 1144 def create_protein(self, name, number_of_proteins, box_l, topology_dict): 1145 """ 1146 Creates one or more protein molecules in an ESPResSo system based on the 1147 protein template in the pyMBE database and a provided topology. 1148 1149 Args: 1150 name (str): 1151 Name of the protein template stored in the pyMBE database. 1152 1153 number_of_proteins (int): 1154 Number of protein molecules to generate. 1155 1156 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1157 1158 topology_dict (dict): 1159 Dictionary defining the internal structure of the protein. Expected format: 1160 {"ResidueName1": {"initial_pos": np.ndarray, 1161 "chain_id": int, 1162 "radius": float}, 1163 "ResidueName2": { ... }, 1164 ... 1165 } 1166 The '"initial_pos"' entry is required and represents the residue’s 1167 reference coordinates before shifting to the protein's center-of-mass. 1168 1169 Returns: 1170 ('list' of 'int'): 1171 List of the molecule_id of the Protein instances created into ESPResSo. 1172 1173 Notes: 1174 - Particles are created using 'create_particle()' with 'fix=True', 1175 meaning they are initially immobilized. 1176 - The function assumes all residues in 'topology_dict' correspond to 1177 particle templates already defined in the pyMBE database. 1178 - Bonds between residues are not created here; it assumes a rigid body representation of the protein. 1179 """ 1180 if number_of_proteins <= 0: 1181 return 1182 if not self.db._has_template(name=name, pmb_type="protein"): 1183 raise ValueError(f"Protein template with name '{name}' is not defined in the pyMBE database.") 1184 protein_tpl = self.db.get_template(pmb_type="protein", name=name) 1185 box_half = box_l[0] / 2.0 1186 # Create protein 1187 mol_ids = [] 1188 for _ in range(number_of_proteins): 1189 # create a molecule identifier in pyMBE 1190 molecule_id = self.db._propose_instance_id(pmb_type="protein") 1191 # place protein COM randomly 1192 protein_center = self.generate_coordinates_outside_sphere(radius=1, 1193 max_dist=box_half, 1194 n_samples=1, 1195 center=[box_half]*3)[0] 1196 residues = hf.get_residues_from_topology_dict(topology_dict=topology_dict, 1197 model=protein_tpl.model) 1198 # CREATE RESIDUES + PARTICLES 1199 for _, rdata in residues.items(): 1200 base_resname = rdata["resname"] 1201 residue_name = f"AA-{base_resname}" 1202 # residue instance ID 1203 residue_id = self.db._propose_instance_id("residue") 1204 # register ResidueInstance 1205 self.db._register_instance(ResidueInstance(name=residue_name, 1206 residue_id=residue_id, 1207 molecule_id=molecule_id)) 1208 # PARTICLE CREATION 1209 for bead_id in rdata["beads"]: 1210 bead_type = re.split(r'\d+', bead_id)[0] 1211 relative_pos = topology_dict[bead_id]["initial_pos"] 1212 absolute_pos = relative_pos + protein_center 1213 particle_id = self.create_particle(name=bead_type, 1214 box_l=box_l, 1215 number_of_particles=1, 1216 position=[absolute_pos], 1217 fix=[True,True,True])[0] 1218 # update metadata 1219 self.db._update_instance(instance_id=particle_id, 1220 pmb_type="particle", 1221 attribute="molecule_id", 1222 value=molecule_id) 1223 self.db._update_instance(instance_id=particle_id, 1224 pmb_type="particle", 1225 attribute="residue_id", 1226 value=residue_id) 1227 protein_inst = ProteinInstance(name=name, 1228 molecule_id=molecule_id) 1229 self.db._register_instance(protein_inst) 1230 mol_ids.append(molecule_id) 1231 return mol_ids 1232 1233 def create_residue(self, name, box_l, central_bead_position=None,use_default_bond=False, backbone_vector=None, gen_angle=False): 1234 """ 1235 Creates a residue into ESPResSo. 1236 1237 Args: 1238 name ('str'): 1239 Label of the residue type to be created. 1240 1241 central_bead_position ('list' of 'float'): 1242 Position of the central bead. 1243 1244 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1245 1246 use_default_bond ('bool'): 1247 Switch to control if a bond of type 'default' is used to bond a particle whose bonds types are not defined in the pyMBE database. 1248 1249 backbone_vector ('list' of 'float'): 1250 Backbone vector of the molecule. All side chains are created perpendicularly to 'backbone_vector'. 1251 1252 Returns: 1253 (int): 1254 residue_id of the residue created. 1255 """ 1256 if not self.db._has_template(name=name, pmb_type="residue"): 1257 raise ValueError(f"Residue template with name '{name}' is not defined in the pyMBE database.") 1258 res_tpl = self.db.get_template(pmb_type="residue", 1259 name=name) 1260 # Assign a residue_id 1261 residue_id = self.db._propose_instance_id(pmb_type="residue") 1262 res_inst = ResidueInstance(name=name, 1263 residue_id=residue_id) 1264 self.db._register_instance(res_inst) 1265 # create the principal bead 1266 central_bead_name = res_tpl.central_bead 1267 central_bead_id = self.create_particle(name=central_bead_name, 1268 box_l=box_l, 1269 position=central_bead_position, 1270 number_of_particles = 1)[0] 1271 1272 central_bead_position = self.db.get_instance(pmb_type="particle", 1273 instance_id=central_bead_id).position 1274 # # central_bead_position=espresso_system.part.by_id(central_bead_id).pos 1275 1276 # Assigns residue_id to the central_bead particle created. 1277 self.db._update_instance(pmb_type="particle", 1278 instance_id=central_bead_id, 1279 attribute="residue_id", 1280 value=residue_id) 1281 1282 # create the lateral beads 1283 side_chain_list = res_tpl.side_chains 1284 side_chain_beads_ids = [] 1285 for side_chain_name in side_chain_list: 1286 pmb_type = self._get_template_type(name=side_chain_name, 1287 allowed_types={"particle", "residue"}) 1288 if pmb_type == 'particle': 1289 lj_parameters = self.get_lj_parameters(particle_name1=central_bead_name, 1290 particle_name2=side_chain_name) 1291 bond_tpl = self.get_bond_template(particle_name1=central_bead_name, 1292 particle_name2=side_chain_name, 1293 use_default_bond=use_default_bond) 1294 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1295 bond_type=bond_tpl.bond_type, 1296 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1297 if backbone_vector is None: 1298 bead_position=self.generate_random_points_in_a_sphere(center=central_bead_position, 1299 radius=l0, 1300 n_samples=1, 1301 on_surface=True)[0] 1302 else: 1303 bead_position=central_bead_position+self.generate_trial_perpendicular_vector(vector=np.array(backbone_vector), 1304 magnitude=l0) 1305 1306 side_bead_id = self.create_particle(name=side_chain_name, 1307 box_l=box_l, 1308 position=[bead_position], 1309 number_of_particles=1)[0] 1310 side_chain_beads_ids.append(side_bead_id) 1311 self.db._update_instance(pmb_type="particle", 1312 instance_id=side_bead_id, 1313 attribute="residue_id", 1314 value=residue_id) 1315 self.create_bond(particle_id1=central_bead_id, 1316 particle_id2=side_bead_id, 1317 use_default_bond=use_default_bond) 1318 1319 elif pmb_type == 'residue': 1320 1321 side_residue_tpl = self.db.get_template(name=side_chain_name, 1322 pmb_type=pmb_type) 1323 central_bead_side_chain = side_residue_tpl.central_bead 1324 lj_parameters = self.get_lj_parameters(particle_name1=central_bead_name, 1325 particle_name2=central_bead_side_chain) 1326 bond_tpl = self.get_bond_template(particle_name1=central_bead_name, 1327 particle_name2=central_bead_side_chain, 1328 use_default_bond=use_default_bond) 1329 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1330 bond_type=bond_tpl.bond_type, 1331 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1332 if backbone_vector is None: 1333 residue_position=self.generate_random_points_in_a_sphere(center=central_bead_position, 1334 radius=l0, 1335 n_samples=1, 1336 on_surface=True)[0] 1337 else: 1338 residue_position=central_bead_position+self.generate_trial_perpendicular_vector(vector=backbone_vector, 1339 magnitude=l0) 1340 side_residue_id = self.create_residue(name=side_chain_name, 1341 box_l=box_l, 1342 central_bead_position=[residue_position], 1343 use_default_bond=use_default_bond) 1344 # Find particle ids of the inner residue 1345 side_chain_beads_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1346 attribute="residue_id", 1347 value=side_residue_id) 1348 # Change the residue_id of the residue in the side chain to the one of the outer residue 1349 for particle_id in side_chain_beads_ids: 1350 self.db._update_instance(instance_id=particle_id, 1351 pmb_type="particle", 1352 attribute="residue_id", 1353 value=residue_id) 1354 # Remove the instance of the inner residue 1355 self.db.delete_instance(pmb_type="residue", 1356 instance_id=side_residue_id) 1357 self.create_bond(particle_id1=central_bead_id, 1358 particle_id2=side_chain_beads_ids[0], 1359 use_default_bond=use_default_bond) 1360 if gen_angle: 1361 self._generate_angles_for_entity( 1362 entity_id=residue_id, 1363 entity_id_col="residue_id") 1364 return residue_id 1365 1366 def define_bond(self, bond_type, bond_parameters, particle_pairs): 1367 """ 1368 Defines bond templates for each particle pair in 'particle_pairs' in the pyMBE database. 1369 1370 Args: 1371 bond_type ('str'): 1372 label to identify the potential to model the bond. 1373 1374 bond_parameters ('dict'): 1375 parameters of the potential of the bond. 1376 1377 particle_pairs ('lst'): 1378 list of the 'names' of the 'particles' to be bonded. 1379 1380 Notes: 1381 -Currently, only HARMONIC and FENE bonds are supported. 1382 - For a HARMONIC bond the dictionary must contain the following parameters: 1383 - k ('pint.Quantity') : Magnitude of the bond. It should have units of energy/length**2 1384 using the 'pmb.units' UnitRegistry. 1385 - r_0 ('pint.Quantity') : Equilibrium bond length. It should have units of length using 1386 the 'pmb.units' UnitRegistry. 1387 - For a FENE bond the dictionary must contain the same parameters as for a HARMONIC bond and: 1388 - d_r_max ('pint.Quantity'): Maximal stretching length for FENE. It should have 1389 units of length using the 'pmb.units' UnitRegistry. Default 'None'. 1390 """ 1391 self._check_bond_inputs(bond_parameters=bond_parameters, 1392 bond_type=bond_type) 1393 parameters_expected_dimensions={"r_0": "length", 1394 "k": "energy/length**2", 1395 "d_r_max": "length"} 1396 1397 parameters_tpl = {} 1398 for key in bond_parameters.keys(): 1399 parameters_tpl[key]= PintQuantity.from_quantity(q=bond_parameters[key], 1400 expected_dimension=parameters_expected_dimensions[key], 1401 ureg=self.units) 1402 1403 bond_names=[] 1404 for particle_name1, particle_name2 in particle_pairs: 1405 1406 tpl = BondTemplate(particle_name1=particle_name1, 1407 particle_name2=particle_name2, 1408 parameters=parameters_tpl, 1409 bond_type=bond_type) 1410 tpl._make_name() 1411 if tpl.name in bond_names: 1412 raise RuntimeError(f"Bond {tpl.name} has already been defined, please check the list of particle pairs") 1413 bond_names.append(tpl.name) 1414 self.db._register_template(tpl) 1415 1416 1417 def define_default_bond(self, bond_type, bond_parameters): 1418 """ 1419 Defines a bond template as a "default" template in the pyMBE database. 1420 1421 Args: 1422 bond_type ('str'): 1423 label to identify the potential to model the bond. 1424 1425 bond_parameters ('dict'): 1426 parameters of the potential of the bond. 1427 1428 Notes: 1429 - Currently, only harmonic and FENE bonds are supported. 1430 """ 1431 self._check_bond_inputs(bond_parameters=bond_parameters, 1432 bond_type=bond_type) 1433 parameters_expected_dimensions={"r_0": "length", 1434 "k": "energy/length**2", 1435 "d_r_max": "length"} 1436 parameters_tpl = {} 1437 for key in bond_parameters.keys(): 1438 parameters_tpl[key]= PintQuantity.from_quantity(q=bond_parameters[key], 1439 expected_dimension=parameters_expected_dimensions[key], 1440 ureg=self.units) 1441 tpl = BondTemplate(parameters=parameters_tpl, 1442 bond_type=bond_type) 1443 tpl.name = "default" 1444 self.db._register_template(tpl) 1445 1446 def define_angular_potential(self, angle_type, angle_parameters, particle_triplets): 1447 """ 1448 Defines angle potential templates for each particle triplet in `particle_triplets`. 1449 1450 Args: 1451 angle_type ('str'): 1452 Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine". 1453 1454 angle_parameters ('dict'): 1455 Parameters of the angle potential. Must contain: 1456 - "k" ('pint.Quantity'): Bending stiffness with dimensions of energy. 1457 - "phi_0" ('float'): Equilibrium angle in radians. 1458 1459 particle_triplets ('list[tuple[str,str,str]]'): 1460 List of (side_particle1, central_particle, side_particle2) triplets. 1461 """ 1462 valid_angle_types = ["harmonic", "cosine", "harmonic_cosine"] 1463 if angle_type not in valid_angle_types: 1464 raise NotImplementedError(f"Angle potential type '{angle_type}' currently not implemented in pyMBE, accepted types are {valid_angle_types}") 1465 1466 if "k" not in angle_parameters: 1467 raise ValueError("Magnitude of the angle potential (k) is missing") 1468 if "phi_0" not in angle_parameters: 1469 raise ValueError("Equilibrium angle (phi_0) is missing") 1470 1471 parameters_tpl = {"k": PintQuantity.from_quantity(q=angle_parameters["k"], 1472 expected_dimension="energy", 1473 ureg=self.units), 1474 "phi_0": PintQuantity.from_quantity(q=angle_parameters["phi_0"], 1475 expected_dimension="dimensionless", 1476 ureg=self.units),} 1477 angle_names = [] 1478 for side1, central, side2 in particle_triplets: 1479 tpl = AngleTemplate(side_particle1=side1, 1480 central_particle=central, 1481 side_particle2=side2, 1482 parameters=parameters_tpl, 1483 angle_type=angle_type) 1484 tpl._make_name() 1485 if tpl.name in angle_names: 1486 raise RuntimeError(f"Angle {tpl.name} has already been defined, please check the list of particle triplets") 1487 angle_names.append(tpl.name) 1488 self.db._register_template(tpl) 1489 1490 def define_default_angular_potential(self, angle_type, angle_parameters): 1491 """ 1492 Defines an angle template as a "default" template in the pyMBE database. 1493 1494 Args: 1495 angle_type ('str'): 1496 Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine". 1497 1498 angle_parameters ('dict'): 1499 Parameters of the angle potential (k, phi_0). 1500 """ 1501 valid_angle_types = ["harmonic", "cosine", "harmonic_cosine"] 1502 if angle_type not in valid_angle_types: 1503 raise NotImplementedError(f"Angle potential type '{angle_type}' currently not implemented in pyMBE, accepted types are {valid_angle_types}") 1504 if "k" not in angle_parameters: 1505 raise ValueError("Magnitude of the angle potential (k) is missing") 1506 if "phi_0" not in angle_parameters: 1507 raise ValueError("Equilibrium angle (phi_0) is missing") 1508 parameters_tpl = {"k": PintQuantity.from_quantity(q=angle_parameters["k"], 1509 expected_dimension="energy", 1510 ureg=self.units), 1511 "phi_0": PintQuantity.from_quantity(q=angle_parameters["phi_0"], 1512 expected_dimension="dimensionless", 1513 ureg=self.units),} 1514 tpl = AngleTemplate(parameters=parameters_tpl, 1515 angle_type=angle_type) 1516 tpl.name = "default" 1517 self.db._register_template(tpl) 1518 1519 def create_angular_potential(self, particle_id1, particle_id2, particle_id3, use_default_angle=False): 1520 """ 1521 Creates an angle between three particle instances in an ESPResSo system 1522 and registers it in the pyMBE database. 1523 1524 Args: 1525 particle_id1 ('int'): ID of the first side particle. 1526 particle_id2 ('int'): ID of the central particle. 1527 particle_id3 ('int'): ID of the second side particle. 1528 use_default_angle ('bool', optional): If True, use the default angle if no specific one is found. 1529 """ 1530 particle_inst_1 = self.db.get_instance(pmb_type="particle", instance_id=particle_id1) 1531 particle_inst_2 = self.db.get_instance(pmb_type="particle", instance_id=particle_id2) 1532 particle_inst_3 = self.db.get_instance(pmb_type="particle", instance_id=particle_id3) 1533 1534 # Verify that bonds exist between side particles and central particle 1535 bond_instances = self.db.get_instances(pmb_type="bond") 1536 bonded_pairs = set() 1537 for bond in bond_instances.values(): 1538 pair = frozenset([bond.particle_id1, bond.particle_id2]) 1539 bonded_pairs.add(pair) 1540 if frozenset([particle_id1, particle_id2]) not in bonded_pairs: 1541 raise ValueError(f"Cannot create angle: no bond exists between particle {particle_id1} and central particle {particle_id2}.") 1542 if frozenset([particle_id3, particle_id2]) not in bonded_pairs: 1543 raise ValueError(f"Cannot create angle: no bond exists between particle {particle_id3} and central particle {particle_id2}.") 1544 1545 angle_tpl = self.get_angle_template(side_name1=particle_inst_1.name, 1546 central_name=particle_inst_2.name, 1547 side_name2=particle_inst_3.name, 1548 use_default_angle=use_default_angle) 1549 angle_id = self.db._propose_instance_id(pmb_type="angle") 1550 pmb_angle_instance = AngleInstance(angle_id=angle_id, 1551 name=angle_tpl.name, 1552 particle_id1=particle_id1, 1553 particle_id2=particle_id2, 1554 particle_id3=particle_id3) 1555 self.db._register_instance(instance=pmb_angle_instance) 1556 1557 def get_angle_template(self, side_name1, central_name, side_name2, use_default_angle=False): 1558 """ 1559 Retrieves an angle template connecting three particle templates. 1560 1561 Args: 1562 side_name1 ('str'): Name of the first side particle. 1563 central_name ('str'): Name of the central particle. 1564 side_name2 ('str'): Name of the second side particle. 1565 use_default_angle ('bool', optional): If True, fall back to the default angle template. 1566 1567 Returns: 1568 ('AngleTemplate'): The matching angle template. 1569 """ 1570 angle_key = AngleTemplate.make_angle_key(side1=side_name1, central=central_name, side2=side_name2) 1571 try: 1572 return self.db.get_template(name=angle_key, pmb_type="angle") 1573 except ValueError: 1574 pass 1575 1576 if use_default_angle: 1577 return self.db.get_template(name="default", pmb_type="angle") 1578 1579 raise ValueError(f"No angle template found for '{side_name1}-{central_name}-{side_name2}', and default angles are deactivated.") 1580 1581 def _generate_angles_for_entity(self, entity_id, entity_id_col): 1582 """ 1583 Auto-generates angles from bond topology for an entity (molecule or residue). 1584 1585 For each particle in the entity that has two or more bonded neighbors, 1586 this method finds all neighbor pairs and applies any matching angle potential. 1587 1588 Args: 1589 entity_id ('int'): The molecule_id or residue_id to generate angles for. 1590 entity_id_col ('str'): Either "molecule_id" or "residue_id". 1591 """ 1592 # Get all particle IDs for this entity 1593 particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1594 attribute=entity_id_col, 1595 value=entity_id) 1596 if not particle_ids: 1597 return 1598 1599 # Build neighbor map from bond instances 1600 neighbors = {pid: set() for pid in particle_ids} 1601 pid_set = set(particle_ids) 1602 bond_instances = self.db.get_instances(pmb_type="bond") 1603 for bond in bond_instances.values(): 1604 i, j = bond.particle_id1, bond.particle_id2 1605 if i in pid_set and j in pid_set: 1606 neighbors[i].add(j) 1607 neighbors[j].add(i) 1608 1609 # For each particle with 2+ neighbors, generate angles 1610 for j in particle_ids: 1611 nbs = sorted(neighbors[j]) 1612 if len(nbs) < 2: 1613 continue 1614 1615 for idx_i in range(len(nbs)): 1616 for idx_k in range(idx_i + 1, len(nbs)): 1617 i = nbs[idx_i] 1618 k = nbs[idx_k] 1619 try: 1620 self.create_angular_potential(particle_id1=i, 1621 particle_id2=j, 1622 particle_id3=k, 1623 use_default_angle=True) 1624 except ValueError: 1625 # No angle template defined for this triplet — skip 1626 continue 1627 1628 def define_hydrogel(self, name, node_map, chain_map): 1629 """ 1630 Defines a hydrogel template in the pyMBE database. 1631 1632 Args: 1633 name ('str'): 1634 Unique label that identifies the 'hydrogel'. 1635 1636 node_map ('list of dict'): 1637 [{"particle_name": , "lattice_index": }, ... ] 1638 1639 chain_map ('list of dict'): 1640 [{"node_start": , "node_end": , "residue_list": , ... ] 1641 """ 1642 # Sanity tests 1643 node_indices = {tuple(entry['lattice_index']) for entry in node_map} 1644 chain_map_connectivity = set() 1645 for entry in chain_map: 1646 start = self.lattice_builder.node_labels[entry['node_start']] 1647 end = self.lattice_builder.node_labels[entry['node_end']] 1648 chain_map_connectivity.add((start,end)) 1649 if self.lattice_builder.lattice.connectivity != chain_map_connectivity: 1650 raise ValueError("Incomplete hydrogel: A diamond lattice must contain correct 16 lattice index pairs") 1651 diamond_indices = {tuple(row) for row in self.lattice_builder.lattice.indices} 1652 if node_indices != diamond_indices: 1653 raise ValueError(f"Incomplete hydrogel: A diamond lattice must contain exactly 8 lattice indices, {diamond_indices} ") 1654 # Register information in the pyMBE database 1655 nodes=[] 1656 for entry in node_map: 1657 nodes.append(HydrogelNode(particle_name=entry["particle_name"], 1658 lattice_index=entry["lattice_index"])) 1659 chains=[] 1660 for chain in chain_map: 1661 chains.append(HydrogelChain(node_start=chain["node_start"], 1662 node_end=chain["node_end"], 1663 molecule_name=chain["molecule_name"])) 1664 tpl = HydrogelTemplate(name=name, 1665 node_map=nodes, 1666 chain_map=chains) 1667 self.db._register_template(tpl) 1668 1669 def define_molecule(self, name, residue_list): 1670 """ 1671 Defines a molecule template in the pyMBE database. 1672 1673 Args: 1674 name('str'): 1675 Unique label that identifies the 'molecule'. 1676 1677 residue_list ('list' of 'str'): 1678 List of the 'name's of the 'residue's in the sequence of the 'molecule'. 1679 """ 1680 tpl = MoleculeTemplate(name=name, 1681 residue_list=residue_list) 1682 self.db._register_template(tpl) 1683 1684 def define_monoprototic_acidbase_reaction(self, particle_name, pka, acidity, metadata=None): 1685 """ 1686 Defines an acid-base reaction for a monoprototic particle in the pyMBE database. 1687 1688 Args: 1689 particle_name ('str'): 1690 Unique label that identifies the particle template. 1691 1692 pka ('float'): 1693 pka-value of the acid or base. 1694 1695 acidity ('str'): 1696 Identifies whether if the particle is 'acidic' or 'basic'. 1697 1698 metadata ('dict', optional): 1699 Additional information to be stored in the reaction. Defaults to None. 1700 """ 1701 supported_acidities = ["acidic", "basic"] 1702 if acidity not in supported_acidities: 1703 raise ValueError(f"Unsupported acidity '{acidity}' for particle '{particle_name}'. Supported acidities are {supported_acidities}.") 1704 reaction_type = "monoprotic" 1705 if acidity == "basic": 1706 reaction_type += "_base" 1707 else: 1708 reaction_type += "_acid" 1709 reaction = Reaction(participants=[ReactionParticipant(particle_name=particle_name, 1710 state_name=f"{particle_name}H", 1711 coefficient=-1), 1712 ReactionParticipant(particle_name=particle_name, 1713 state_name=f"{particle_name}", 1714 coefficient=1)], 1715 reaction_type=reaction_type, 1716 pK=pka, 1717 metadata=metadata) 1718 self.db._register_reaction(reaction) 1719 1720 def define_monoprototic_particle_states(self, particle_name, acidity): 1721 """ 1722 Defines particle states for a monoprotonic particle template including the charges in each of its possible states. 1723 1724 Args: 1725 particle_name ('str'): 1726 Unique label that identifies the particle template. 1727 1728 acidity ('str'): 1729 Identifies whether the particle is 'acidic' or 'basic'. 1730 """ 1731 acidity_valid_keys = ['acidic', 'basic'] 1732 if not pd.isna(acidity): 1733 if acidity not in acidity_valid_keys: 1734 raise ValueError(f"Acidity {acidity} provided for particle name {particle_name} is not supported. Valid keys are: {acidity_valid_keys}") 1735 if acidity == "acidic": 1736 states = [{"name": f"{particle_name}H", "z": 0}, 1737 {"name": f"{particle_name}", "z": -1}] 1738 1739 elif acidity == "basic": 1740 states = [{"name": f"{particle_name}H", "z": 1}, 1741 {"name": f"{particle_name}", "z": 0}] 1742 self.define_particle_states(particle_name=particle_name, 1743 states=states) 1744 1745 def define_particle(self, name, sigma, epsilon, z=0, acidity=pd.NA, pka=pd.NA, cutoff=pd.NA, offset=pd.NA): 1746 """ 1747 Defines a particle template in the pyMBE database. 1748 1749 Args: 1750 name('str'): 1751 Unique label that identifies this particle type. 1752 1753 sigma('pint.Quantity'): 1754 Sigma parameter used to set up Lennard-Jones interactions for this particle type. 1755 1756 epsilon('pint.Quantity'): 1757 Epsilon parameter used to setup Lennard-Jones interactions for this particle tipe. 1758 1759 z('int', optional): 1760 Permanent charge number of this particle type. Defaults to 0. 1761 1762 acidity('str', optional): 1763 Identifies whether if the particle is 'acidic' or 'basic', used to setup constant pH simulations. Defaults to pd.NA. 1764 1765 pka('float', optional): 1766 If 'particle' is an acid or a base, it defines its pka-value. Defaults to pd.NA. 1767 1768 cutoff('pint.Quantity', optional): 1769 Cutoff parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA. 1770 1771 offset('pint.Quantity', optional): 1772 Offset parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA. 1773 1774 Notes: 1775 - 'sigma', 'cutoff' and 'offset' must have a dimensitonality of '[length]' and should be defined using pmb.units. 1776 - 'epsilon' must have a dimensitonality of '[energy]' and should be defined using pmb.units. 1777 - 'cutoff' defaults to '2**(1./6.) reduced_length'. 1778 - 'offset' defaults to 0. 1779 - For more information on 'sigma', 'epsilon', 'cutoff' and 'offset' check 'pmb.setup_lj_interactions()'. 1780 """ 1781 # If 'cutoff' and 'offset' are not defined, default them to the following values 1782 if pd.isna(cutoff): 1783 cutoff=self.units.Quantity(2**(1./6.), "reduced_length") 1784 if pd.isna(offset): 1785 offset=self.units.Quantity(0, "reduced_length") 1786 # Define particle states 1787 if acidity is pd.NA: 1788 states = [{"name": f"{name}", "z": z}] 1789 self.define_particle_states(particle_name=name, 1790 states=states) 1791 initial_state = name 1792 else: 1793 self.define_monoprototic_particle_states(particle_name=name, 1794 acidity=acidity) 1795 initial_state = f"{name}H" 1796 if pka is not pd.NA: 1797 self.define_monoprototic_acidbase_reaction(particle_name=name, 1798 acidity=acidity, 1799 pka=pka) 1800 tpl = ParticleTemplate(name=name, 1801 sigma=PintQuantity.from_quantity(q=sigma, expected_dimension="length", ureg=self.units), 1802 epsilon=PintQuantity.from_quantity(q=epsilon, expected_dimension="energy", ureg=self.units), 1803 cutoff=PintQuantity.from_quantity(q=cutoff, expected_dimension="length", ureg=self.units), 1804 offset=PintQuantity.from_quantity(q=offset, expected_dimension="length", ureg=self.units), 1805 initial_state=initial_state) 1806 self.db._register_template(tpl) 1807 1808 def define_particle_states(self, particle_name, states): 1809 """ 1810 Define the chemical states of an existing particle template. 1811 1812 Args: 1813 particle_name ('str'): 1814 Name of a particle template. 1815 1816 states ('list' of 'dict'): 1817 List of dictionaries defining the particle states. Each dictionary 1818 must contain: 1819 - 'name' ('str'): Name of the particle state (e.g. '"H"', '"-"', 1820 '"neutral"'). 1821 - 'z' ('int'): Charge number of the particle in this state. 1822 Example: 1823 states = [{"name": "AH", "z": 0}, # protonated 1824 {"name": "A-", "z": -1}] # deprotonated 1825 Notes: 1826 - Each state is assigned a unique Espresso 'es_type' automatically. 1827 - Chemical reactions (e.g. acid–base equilibria) are **not** created by 1828 this method and must be defined separately (e.g. via 1829 'set_particle_acidity()' or custom reaction definitions). 1830 - Particles without explicitly defined states are assumed to have a 1831 single, implicit state with their default charge. 1832 """ 1833 for s in states: 1834 state = ParticleStateTemplate(particle_name=particle_name, 1835 name=s["name"], 1836 z=s["z"], 1837 es_type=self.propose_unused_type()) 1838 self.db._register_template(state) 1839 1840 def define_peptide(self, name, sequence, model): 1841 """ 1842 Defines a peptide template in the pyMBE database. 1843 1844 Args: 1845 name ('str'): 1846 Unique label that identifies the peptide. 1847 1848 sequence ('str'): 1849 Sequence of the peptide. 1850 1851 model ('str'): 1852 Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported. 1853 """ 1854 valid_keys = ['1beadAA','2beadAA'] 1855 if model not in valid_keys: 1856 raise ValueError('Invalid label for the peptide model, please choose between 1beadAA or 2beadAA') 1857 clean_sequence = hf.protein_sequence_parser(sequence=sequence) 1858 residue_list = self._get_residue_list_from_sequence(sequence=clean_sequence) 1859 tpl = PeptideTemplate(name=name, 1860 residue_list=residue_list, 1861 model=model, 1862 sequence=sequence) 1863 self.db._register_template(tpl) 1864 1865 def define_protein(self, name, sequence, model): 1866 """ 1867 Defines a protein template in the pyMBE database. 1868 1869 Args: 1870 name ('str'): 1871 Unique label that identifies the protein. 1872 1873 sequence ('str'): 1874 Sequence of the protein. 1875 1876 model ('string'): 1877 Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported. 1878 1879 Notes: 1880 - Currently, only 'lj_setup_mode="wca"' is supported. This corresponds to setting up the WCA potential. 1881 """ 1882 valid_model_keys = ['1beadAA','2beadAA'] 1883 if model not in valid_model_keys: 1884 raise ValueError('Invalid key for the protein model, supported models are {valid_model_keys}') 1885 1886 residue_list = self._get_residue_list_from_sequence(sequence=sequence) 1887 tpl = ProteinTemplate(name=name, 1888 model=model, 1889 residue_list=residue_list, 1890 sequence=sequence) 1891 self.db._register_template(tpl) 1892 1893 def define_residue(self, name, central_bead, side_chains): 1894 """ 1895 Defines a residue template in the pyMBE database. 1896 1897 Args: 1898 name ('str'): 1899 Unique label that identifies the residue. 1900 1901 central_bead ('str'): 1902 'name' of the 'particle' to be placed as central_bead of the residue. 1903 1904 side_chains('list' of 'str'): 1905 List of 'name's of the pmb_objects to be placed as side_chains of the residue. Currently, only pyMBE objects of type 'particle' or 'residue' are supported. 1906 """ 1907 tpl = ResidueTemplate(name=name, 1908 central_bead=central_bead, 1909 side_chains=side_chains) 1910 self.db._register_template(tpl) 1911 1912 1913 def delete_instances_in_system(self, instance_id, pmb_type): 1914 """ 1915 Deletes the instance with instance_id from the ESPResSo system. 1916 Related assembly, molecule, residue, particles and bond instances will also be deleted from the pyMBE dataframe. 1917 1918 Args: 1919 instance_id ('int'): 1920 id of the assembly to be deleted. 1921 1922 pmb_type ('str'): 1923 the instance type to be deleted. 1924 1925 espresso_system ('espressomd.system.System'): 1926 Instance of a system class from espressomd library. 1927 """ 1928 if pmb_type == "particle": 1929 instance_identifier = "particle_id" 1930 elif pmb_type == "residue": 1931 instance_identifier = "residue_id" 1932 elif pmb_type in self.db._molecule_like_types: 1933 instance_identifier = "molecule_id" 1934 elif pmb_type in self.db._assembly_like_types: 1935 instance_identifier = "assembly_id" 1936 particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1937 attribute=instance_identifier, 1938 value=instance_id) 1939 self._delete_particles_from_engine(particle_ids=particle_ids) 1940 self.db.delete_instance(pmb_type=pmb_type, 1941 instance_id=instance_id) 1942 1943 def determine_reservoir_concentrations(self, pH_res, c_salt_res, activity_coefficient_monovalent_pair, max_number_sc_runs=200): 1944 """ 1945 Determines ionic concentrations in the reservoir at fixed pH and salt concentration. 1946 1947 Args: 1948 pH_res ('float'): 1949 Target pH value in the reservoir. 1950 1951 c_salt_res ('pint.Quantity'): 1952 Concentration of monovalent salt (e.g., NaCl) in the reservoir. 1953 1954 activity_coefficient_monovalent_pair ('callable'): 1955 Function returning the activity coefficient of a monovalent ion pair 1956 as a function of ionic strength: 1957 'gamma = activity_coefficient_monovalent_pair(I)'. 1958 1959 max_number_sc_runs ('int', optional): 1960 Maximum number of self-consistent iterations allowed before 1961 convergence is enforced. Defaults to 200. 1962 1963 Returns: 1964 tuple: 1965 (cH_res, cOH_res, cNa_res, cCl_res) 1966 - cH_res ('pint.Quantity'): Concentration of H⁺ ions. 1967 - cOH_res ('pint.Quantity'): Concentration of OH⁻ ions. 1968 - cNa_res ('pint.Quantity'): Concentration of Na⁺ ions. 1969 - cCl_res ('pint.Quantity'): Concentration of Cl⁻ ions. 1970 1971 Notess: 1972 - The algorithm enforces electroneutrality in the reservoir. 1973 - Water autodissociation is included via the equilibrium constant 'Kw'. 1974 - Non-ideal effects enter through activity coefficients depending on 1975 ionic strength. 1976 - The implementation follows the self-consistent scheme described in 1977 Landsgesell (PhD thesis, Sec. 5.3, doi:10.18419/opus-10831), adapted 1978 from the original code (doi:10.18419/darus-2237). 1979 """ 1980 cH_res, cOH_res, cNa_res, cCl_res = self.simulation_engine.determine_reservoir_concentrations( pH_res, c_salt_res, activity_coefficient_monovalent_pair, max_number_sc_runs) 1981 return cH_res, cOH_res, cNa_res, cCl_res 1982 1983 def enable_motion_of_rigid_object(self, instance_id, pmb_type): 1984 """ 1985 Enables translational and rotational motion of a rigid pyMBE object instance 1986 in an ESPResSo system.This method creates a rigid-body center particle at the center of mass of 1987 the specified pyMBE object and attaches all constituent particles to it 1988 using ESPResSo virtual sites. The resulting rigid object can translate and 1989 rotate as a single body. 1990 1991 Args: 1992 instance_id ('int'): 1993 Instance ID of the pyMBE object whose rigid-body motion is enabled. 1994 1995 pmb_type ('str'): 1996 pyMBE object type of the instance (e.g. '"molecule"', '"peptide"', 1997 '"protein"', or any assembly-like type). 1998 1999 Notess: 2000 - This method requires ESPResSo to be compiled with the following 2001 features enabled: 2002 - '"VIRTUAL_SITES_RELATIVE"' 2003 - '"MASS"' 2004 - A new ESPResSo particle is created to represent the rigid-body center. 2005 - The mass of the rigid-body center is set to the number of particles 2006 belonging to the object. 2007 - The rotational inertia tensor is approximated from the squared 2008 distances of the particles to the center of mass. 2009 """ 2010 self.simulation_engine.enable_motion_of_rigid_object(instance_id, pmb_type) 2011 2012 def generate_coordinates_outside_sphere(self, center, radius, max_dist, n_samples): 2013 """ 2014 Generates random coordinates outside a sphere and inside a larger bounding sphere. 2015 2016 Args: 2017 center ('array-like'): 2018 Coordinates of the center of the spheres. 2019 2020 radius ('float'): 2021 Radius of the inner exclusion sphere. Must be positive. 2022 2023 max_dist ('float'): 2024 Radius of the outer sampling sphere. Must be larger than 'radius'. 2025 2026 n_samples ('int'): 2027 Number of coordinates to generate. 2028 2029 Returns: 2030 'list' of 'numpy.ndarray': 2031 List of coordinates lying outside the inner sphere and inside the 2032 outer sphere. 2033 2034 Notess: 2035 - Points are uniformly sampled inside a sphere of radius 'max_dist' centered at 'center' 2036 and only those with a distance greater than or equal to 'radius' from the center are retained. 2037 """ 2038 if not radius > 0: 2039 raise ValueError (f'The value of {radius} must be a positive value') 2040 if not radius < max_dist: 2041 raise ValueError(f'The min_dist ({radius} must be lower than the max_dist ({max_dist}))') 2042 coord_list = [] 2043 counter = 0 2044 while counter<n_samples: 2045 coord = self.generate_random_points_in_a_sphere(center=center, 2046 radius=max_dist, 2047 n_samples=1)[0] 2048 if np.linalg.norm(coord-np.asarray(center))>=radius: 2049 coord_list.append (coord) 2050 counter += 1 2051 return coord_list 2052 2053 def generate_random_points_in_a_sphere(self, center, radius, n_samples, on_surface=False): 2054 """ 2055 Generates uniformly distributed random points inside or on the surface of a sphere. 2056 2057 Args: 2058 center ('array-like'): 2059 Coordinates of the center of the sphere. 2060 2061 radius ('float'): 2062 Radius of the sphere. 2063 2064 n_samples ('int'): 2065 Number of sample points to generate. 2066 2067 on_surface ('bool', optional): 2068 If True, points are uniformly sampled on the surface of the sphere. 2069 If False, points are uniformly sampled within the sphere volume. 2070 Defaults to False. 2071 2072 Returns: 2073 'numpy.ndarray': 2074 Array of shape '(n_samples, d)' containing the generated coordinates, 2075 where 'd' is the dimensionality of 'center'. 2076 Notes: 2077 - Points are sampled in a space whose dimensionality is inferred 2078 from the length of 'center'. 2079 """ 2080 # initial values 2081 center=np.array(center) 2082 d = center.shape[0] 2083 # sample n_samples points in d dimensions from a standard normal distribution 2084 samples = self.rng.normal(size=(n_samples, d)) 2085 # make the samples lie on the surface of the unit hypersphere 2086 normalize_radii = np.linalg.norm(samples, axis=1)[:, np.newaxis] 2087 samples /= normalize_radii 2088 if not on_surface: 2089 # make the samples lie inside the hypersphere with the correct density 2090 uniform_points = self.rng.uniform(size=n_samples)[:, np.newaxis] 2091 new_radii = np.power(uniform_points, 1/d) 2092 samples *= new_radii 2093 # scale the points to have the correct radius and center 2094 samples = samples * radius + center 2095 return samples 2096 2097 def generate_trial_perpendicular_vector(self,vector,magnitude): 2098 """ 2099 Generates a random vector perpendicular to a given vector. 2100 2101 Args: 2102 vector ('array-like'): 2103 Reference vector to which the generated vector will be perpendicular. 2104 2105 magnitude ('float'): 2106 Desired magnitude of the perpendicular vector. 2107 2108 Returns: 2109 'numpy.ndarray': 2110 Vector orthogonal to 'vector' with norm equal to 'magnitude'. 2111 """ 2112 np_vec = np.array(vector) 2113 if np.all(np_vec == 0): 2114 raise ValueError('Zero vector') 2115 np_vec /= np.linalg.norm(np_vec) 2116 # Generate a random vector 2117 random_vector = self.generate_random_points_in_a_sphere(radius=1, 2118 center=[0,0,0], 2119 n_samples=1, 2120 on_surface=True)[0] 2121 # Project the random vector onto the input vector and subtract the projection 2122 projection = np.dot(random_vector, np_vec) * np_vec 2123 perpendicular_vector = random_vector - projection 2124 # Normalize the perpendicular vector to have the same magnitude as the input vector 2125 perpendicular_vector /= np.linalg.norm(perpendicular_vector) 2126 return perpendicular_vector*magnitude 2127 2128 def get_bond_template(self, particle_name1, particle_name2, use_default_bond=False) : 2129 """ 2130 Retrieves a bond template connecting two particle templates. 2131 2132 Args: 2133 particle_name1 ('str'): 2134 Name of the first particle template. 2135 2136 particle_name2 ('str'): 2137 Name of the second particle template. 2138 2139 use_default_bond ('bool', optional): 2140 If True, returns the default bond template when no specific bond 2141 template is found. Defaults to False. 2142 2143 Returns: 2144 'BondTemplate': 2145 Bond template object retrieved from the pyMBE database. 2146 2147 Notes: 2148 - This method searches the pyMBE database for a bond template defined between particle templates with names 'particle_name1' and 'particle_name2'. 2149 - If no specific bond template is found and 'use_default_bond' is enabled, a default bond template is returned instead. 2150 """ 2151 # Try to find a specific bond template 2152 bond_key = BondTemplate.make_bond_key(pn1=particle_name1, 2153 pn2=particle_name2) 2154 try: 2155 return self.db.get_template(name=bond_key, 2156 pmb_type="bond") 2157 except ValueError: 2158 pass 2159 2160 # Fallback to default bond if allowed 2161 if use_default_bond: 2162 return self.db.get_template(name="default", 2163 pmb_type="bond") 2164 2165 # No bond template found 2166 raise ValueError(f"No bond template found between '{particle_name1}' and '{particle_name2}', and default bonds are deactivated.") 2167 2168 def get_charge_number_map(self): 2169 """ 2170 Construct a mapping from ESPResSo particle types to their charge numbers. 2171 2172 Returns: 2173 'dict[int, float]': 2174 Dictionary mapping ESPResSo particle types to charge numbers, 2175 ''{es_type: z}''. 2176 2177 Notess: 2178 - The mapping is built from particle *states*, not instances. 2179 - If multiple templates define states with the same ''es_type'', 2180 the last encountered definition will overwrite previous ones. 2181 This behavior is intentional and assumes database consistency. 2182 - Neutral particles (''z = 0'') are included in the map. 2183 """ 2184 charge_number_map = {} 2185 particle_templates = self.db.get_templates("particle") 2186 for tpl in particle_templates.values(): 2187 for state in self.db.get_particle_states_templates(particle_name=tpl.name).values(): 2188 charge_number_map[state.es_type] = state.z 2189 return charge_number_map 2190 2191 def get_instances_df(self, pmb_type): 2192 """ 2193 Returns a dataframe with all instances of type 'pmb_type' in the pyMBE database. 2194 2195 Args: 2196 pmb_type ('str'): 2197 pmb type to search instances in the pyMBE database. 2198 2199 Returns: 2200 ('Pandas.Dataframe'): 2201 Dataframe with all instances of type 'pmb_type'. 2202 """ 2203 return self.db._get_instances_df(pmb_type=pmb_type) 2204 2205 def get_lj_parameters(self, particle_name1, particle_name2, combining_rule='Lorentz-Berthelot'): 2206 """ 2207 Returns the Lennard-Jones parameters for the interaction between the particle types given by 2208 'particle_name1' and 'particle_name2' in the pyMBE database, calculated according to the provided combining rule. 2209 2210 Args: 2211 particle_name1 ('str'): 2212 label of the type of the first particle type 2213 2214 particle_name2 ('str'): 2215 label of the type of the second particle type 2216 2217 combining_rule ('string', optional): 2218 combining rule used to calculate 'sigma' and 'epsilon' for the potential betwen a pair of particles. Defaults to 'Lorentz-Berthelot'. 2219 2220 Returns: 2221 ('dict'): 2222 {"epsilon": epsilon_value, "sigma": sigma_value, "offset": offset_value, "cutoff": cutoff_value} 2223 2224 Notes: 2225 - Currently, the only 'combining_rule' supported is Lorentz-Berthelot. 2226 - If the sigma value of 'particle_name1' or 'particle_name2' is 0, the function will return an empty dictionary. No LJ interactions are set up for particles with sigma = 0. 2227 """ 2228 lj_parameters=self.db.get_lj_parameters(particle_name1=particle_name1,particle_name2=particle_name2,combining_rule=combining_rule) 2229 return lj_parameters 2230 2231 def get_particle_id_map(self, object_name): 2232 """ 2233 Collect all particle IDs associated with an object of given name in the 2234 pyMBE database. 2235 2236 Args: 2237 object_name ('str'): 2238 Name of the object. 2239 2240 Returns: 2241 ('dict'): 2242 {"all": [particle_ids], 2243 "residue_map": {residue_id: [particle_ids]}, 2244 "molecule_map": {molecule_id: [particle_ids]}, 2245 "assembly_map": {assembly_id: [particle_ids]},} 2246 2247 Notess: 2248 - Works for all supported pyMBE templates. 2249 - Relies in the internal method Manager.get_particle_id_map, see method for the detailed code. 2250 """ 2251 return self.db.get_particle_id_map(object_name=object_name) 2252 2253 def get_pka_set(self): 2254 """ 2255 Retrieve the pKa set for all titratable particles in the pyMBE database. 2256 2257 Returns: 2258 ('dict'): 2259 Dictionary of the form: 2260 {"particle_name": {"pka_value": float, 2261 "acidity": "acidic" | "basic"}} 2262 Notes: 2263 - If a particle participates in multiple acid/base reactions, an error is raised. 2264 """ 2265 pka_set = {} 2266 supported_reactions = ["monoprotic_acid", 2267 "monoprotic_base"] 2268 for reaction in self.db._reactions.values(): 2269 if reaction.reaction_type not in supported_reactions: 2270 continue 2271 # Identify involved particle(s) 2272 particle_names = {participant.particle_name for participant in reaction.participants} 2273 particle_name = particle_names.pop() 2274 if particle_name in pka_set: 2275 raise ValueError(f"Multiple acid/base reactions found for particle '{particle_name}'.") 2276 pka_set[particle_name] = {"pka_value": reaction.pK} 2277 if reaction.reaction_type == "monoprotic_acid": 2278 acidity = "acidic" 2279 elif reaction.reaction_type == "monoprotic_base": 2280 acidity = "basic" 2281 pka_set[particle_name]["acidity"] = acidity 2282 return pka_set 2283 2284 def get_radius_map(self, dimensionless=True): 2285 """ 2286 Gets the effective radius of each particle defined in the pyMBE database. 2287 2288 Args: 2289 dimensionless ('bool'): 2290 If ``True``, return magnitudes expressed in ``reduced_length``. 2291 If ``False``, return Pint quantities with units. 2292 2293 Returns: 2294 ('dict'): 2295 {espresso_type: radius}. 2296 2297 Notes: 2298 - The radius corresponds to (sigma+offset)/2 2299 """ 2300 return self.db.get_radius_map(dimensionless) 2301 2302 def get_reactions_df(self): 2303 """ 2304 Returns a dataframe with all reaction templates in the pyMBE database. 2305 2306 Returns: 2307 (Pandas.Dataframe): 2308 Dataframe with all reaction templates. 2309 """ 2310 return self.db._get_reactions_df() 2311 2312 def get_reduced_units(self): 2313 """ 2314 Returns the current set of reduced units defined in pyMBE. 2315 2316 Returns: 2317 reduced_units_text ('str'): 2318 text with information about the current set of reduced units. 2319 2320 """ 2321 unit_length=self.units.Quantity(1,'reduced_length') 2322 unit_energy=self.units.Quantity(1,'reduced_energy') 2323 unit_charge=self.units.Quantity(1,'reduced_charge') 2324 reduced_units_text = "\n".join(["Current set of reduced units:", 2325 f"{unit_length.to('nm'):.5g} = {unit_length}", 2326 f"{unit_energy.to('J'):.5g} = {unit_energy}", 2327 f"{unit_charge.to('C'):.5g} = {unit_charge}", 2328 f"Temperature: {(self.kT/self.kB).to('K'):.5g}"]) 2329 return reduced_units_text 2330 2331 def get_templates_df(self, pmb_type): 2332 """ 2333 Returns a dataframe with all templates of type 'pmb_type' in the pyMBE database. 2334 2335 Args: 2336 pmb_type ('str'): 2337 pmb type to search templates in the pyMBE database. 2338 2339 Returns: 2340 ('Pandas.Dataframe'): 2341 Dataframe with all templates of type given by 'pmb_type'. 2342 """ 2343 return self.db._get_templates_df(pmb_type=pmb_type) 2344 2345 def get_type_map(self): 2346 """ 2347 Return the mapping of ESPResSo types for all particle states defined in the pyMBE database. 2348 2349 Returns: 2350 'dict[str, int]': 2351 A dictionary mapping each particle state to its corresponding ESPResSo type: 2352 {state_name: es_type, ...} 2353 """ 2354 2355 return self.db.get_es_types_map() 2356 2357 def initialize_lattice_builder(self, diamond_lattice): 2358 """ 2359 Initialize the lattice builder with the DiamondLattice object. 2360 2361 Args: 2362 diamond_lattice ('DiamondLattice'): 2363 DiamondLattice object from the 'lib/lattice' module to be used in the LatticeBuilder. 2364 """ 2365 from .lib.lattice import LatticeBuilder, DiamondLattice 2366 if not isinstance(diamond_lattice, DiamondLattice): 2367 raise TypeError("Currently only DiamondLattice objects are supported.") 2368 self.lattice_builder = LatticeBuilder(lattice=diamond_lattice) 2369 logging.info(f"LatticeBuilder initialized with mpc={diamond_lattice.mpc} and box_l={diamond_lattice.box_l}") 2370 return self.lattice_builder 2371 2372 def load_database(self, folder, format='csv'): 2373 """ 2374 Loads a pyMBE database stored in 'folder'. 2375 2376 Args: 2377 folder ('str' or 'Path'): 2378 Path to the folder where the pyMBE database was stored. 2379 2380 format ('str', optional): 2381 Format of the database to be loaded. Defaults to 'csv'. 2382 2383 Return: 2384 ('dict'): 2385 metadata with additional information about the source of the information in the database. 2386 2387 Notes: 2388 - The folder must contain the files generated by 'pmb.save_database()'. 2389 - Currently, only 'csv' format is supported. 2390 """ 2391 supported_formats = ['csv'] 2392 if format not in supported_formats: 2393 raise ValueError(f"Format {format} not supported. Supported formats are {supported_formats}") 2394 if format == 'csv': 2395 metadata =io._load_database_csv(self.db, 2396 folder=folder) 2397 return metadata 2398 2399 def load_pka_set(self, filename): 2400 """ 2401 Load a pKa set and attach chemical states and acid–base reactions 2402 to existing particle templates. 2403 2404 Args: 2405 filename ('str'): 2406 Path to a JSON file containing the pKa set. Expected format: 2407 {"metadata": {...}, 2408 "data": {"A": {"acidity": "acidic", "pka_value": 4.5}, 2409 "B": {"acidity": "basic", "pka_value": 9.8}}} 2410 2411 Returns: 2412 ('dict'): 2413 Dictionary with bibliographic metadata about the original work were the pKa set was determined. 2414 2415 Notes: 2416 - This method is designed for monoprotic acids and bases only. 2417 """ 2418 with open(filename, "r") as f: 2419 pka_data = json.load(f) 2420 pka_set = pka_data["data"] 2421 metadata = pka_data.get("metadata", {}) 2422 self._check_pka_set(pka_set) 2423 for particle_name, entry in pka_set.items(): 2424 acidity = entry["acidity"] 2425 pka = entry["pka_value"] 2426 self.define_monoprototic_acidbase_reaction(particle_name=particle_name, 2427 pka=pka, 2428 acidity=acidity, 2429 metadata=metadata) 2430 return metadata 2431 2432 def propose_unused_type(self): 2433 """ 2434 Propose an unused ESPResSo particle type. 2435 2436 Returns: 2437 ('int'): 2438 The next available integer ESPResSo type. Returns ''0'' if no integer types are currently defined. 2439 """ 2440 return self.db.propose_unused_type() 2441 2442 def read_protein_vtf(self, filename, unit_length=None): 2443 """ 2444 Loads a coarse-grained protein model from a VTF file. 2445 2446 Args: 2447 filename ('str'): 2448 Path to the VTF file. 2449 2450 unit_length ('Pint.Quantity'): 2451 Unit of length for coordinates (pyMBE UnitRegistry). Defaults to Angstrom. 2452 2453 Returns: 2454 ('tuple'): 2455 ('dict'): Particle topology. 2456 ('str'): One-letter amino-acid sequence (including n/c ends). 2457 """ 2458 logging.info(f"Loading protein coarse-grain model file: {filename}") 2459 if unit_length is None: 2460 unit_length = 1 * self.units.angstrom 2461 atoms = {} # atom_id -> atom info 2462 coords = [] # ordered coordinates 2463 residues = {} # resid -> resname (first occurrence) 2464 has_n_term = False 2465 has_c_term = False 2466 aa_3to1 = {"ALA": "A", "ARG": "R", "ASN": "N", "ASP": "D", 2467 "CYS": "C", "GLU": "E", "GLN": "Q", "GLY": "G", 2468 "HIS": "H", "ILE": "I", "LEU": "L", "LYS": "K", 2469 "MET": "M", "PHE": "F", "PRO": "P", "SER": "S", 2470 "THR": "T", "TRP": "W", "TYR": "Y", "VAL": "V", 2471 "n": "n", "c": "c"} 2472 # --- parse VTF --- 2473 with open(filename, "r") as f: 2474 for line in f: 2475 fields = line.split() 2476 if not fields: 2477 continue 2478 if fields[0] == "atom": 2479 atom_id = int(fields[1]) 2480 atom_name = fields[3] 2481 resname = fields[5] 2482 resid = int(fields[7]) 2483 chain_id = fields[9] 2484 radius = float(fields[11]) * unit_length 2485 atoms[atom_id] = {"name": atom_name, 2486 "resname": resname, 2487 "resid": resid, 2488 "chain_id": chain_id, 2489 "radius": radius} 2490 if resname == "n": 2491 has_n_term = True 2492 elif resname == "c": 2493 has_c_term = True 2494 # register residue 2495 if resid not in residues: 2496 residues[resid] = resname 2497 elif fields[0].isnumeric(): 2498 xyz = [(float(x) * unit_length).to("reduced_length").magnitude 2499 for x in fields[1:4]] 2500 coords.append(xyz) 2501 sequence = "" 2502 # N-terminus 2503 if has_n_term: 2504 sequence += "n" 2505 # protein residues only 2506 protein_resids = sorted(resid for resid, resname in residues.items() if resname not in ("n", "c", "Ca")) 2507 for resid in protein_resids: 2508 resname = residues[resid] 2509 try: 2510 sequence += aa_3to1[resname] 2511 except KeyError: 2512 raise ValueError(f"Unknown residue name '{resname}' in VTF file") 2513 # C-terminus 2514 if has_c_term: 2515 sequence += "c" 2516 last_resid = max(protein_resids) 2517 # --- build topology --- 2518 topology_dict = {} 2519 for atom_id in sorted(atoms.keys()): 2520 atom = atoms[atom_id] 2521 resname = atom["resname"] 2522 resid = atom["resid"] 2523 # apply labeling rules 2524 if resname == "n": 2525 label_resid = 0 2526 elif resname == "c": 2527 label_resid = last_resid + 1 2528 elif resname == "Ca": 2529 label_resid = last_resid + 2 2530 else: 2531 label_resid = resid # preserve original resid 2532 label = f"{atom['name']}{label_resid}" 2533 if label in topology_dict: 2534 raise ValueError(f"Duplicate particle label '{label}'. Check VTF residue definitions.") 2535 topology_dict[label] = {"initial_pos": coords[atom_id - 1], "chain_id": atom["chain_id"], "radius": atom["radius"],} 2536 return topology_dict, sequence 2537 2538 2539 def save_database(self, folder, format='csv'): 2540 """ 2541 Saves the current pyMBE database into a file 'filename'. 2542 2543 Args: 2544 folder ('str' or 'Path'): 2545 Path to the folder where the database files will be saved. 2546 2547 """ 2548 supported_formats = ['csv'] 2549 if format not in supported_formats: 2550 raise ValueError(f"Format {format} not supported. Supported formats are: {supported_formats}") 2551 if format == 'csv': 2552 io._save_database_csv(self.db, 2553 folder=folder) 2554 2555 def set_particle_initial_state(self, particle_name, state_name): 2556 """ 2557 Sets the default initial state of a particle template defined in the pyMBE database. 2558 2559 Args: 2560 particle_name ('str'): 2561 Unique label that identifies the particle template. 2562 2563 state_name ('str'): 2564 Name of the state to be set as default initial state. 2565 """ 2566 part_tpl = self.db.get_template(name=particle_name, 2567 2568 pmb_type="particle") 2569 part_tpl.initial_state = state_name 2570 logging.info(f"Default initial state of particle {particle_name} set to {state_name}.") 2571 2572 def set_reduced_units(self, unit_length=None, unit_charge=None, temperature=None, Kw=None): 2573 """ 2574 Sets the set of reduced units used by pyMBE.units and it prints it. 2575 2576 Args: 2577 unit_length ('pint.Quantity', optional): 2578 Reduced unit of length defined using the 'pmb.units' UnitRegistry. Defaults to None. 2579 2580 unit_charge ('pint.Quantity', optional): 2581 Reduced unit of charge defined using the 'pmb.units' UnitRegistry. Defaults to None. 2582 2583 temperature ('pint.Quantity', optional): 2584 Temperature of the system, defined using the 'pmb.units' UnitRegistry. Defaults to None. 2585 2586 Kw ('pint.Quantity', optional): 2587 Ionic product of water in mol^2/l^2. Defaults to None. 2588 2589 Notes: 2590 - If no 'temperature' is given, a value of 298.15 K is assumed by default. 2591 - If no 'unit_length' is given, a value of 0.355 nm is assumed by default. 2592 - If no 'unit_charge' is given, a value of 1 elementary charge is assumed by default. 2593 - If no 'Kw' is given, a value of 10^(-14) * mol^2 / l^2 is assumed by default. 2594 """ 2595 if unit_length is None: 2596 unit_length= 0.355*self.units.nm 2597 if temperature is None: 2598 temperature = 298.15 * self.units.K 2599 if unit_charge is None: 2600 unit_charge = scipy.constants.e * self.units.C 2601 if Kw is None: 2602 Kw = 1e-14 2603 # Sanity check 2604 variables=[unit_length,temperature,unit_charge] 2605 dimensionalities=["[length]","[temperature]","[charge]"] 2606 for variable,dimensionality in zip(variables,dimensionalities): 2607 self._check_dimensionality(variable,dimensionality) 2608 self.Kw=Kw*self.units.mol**2 / (self.units.l**2) 2609 self.kT=temperature*self.kB 2610 self.units._build_cache() 2611 self.units.define(f'reduced_energy = {self.kT} ') 2612 self.units.define(f'reduced_length = {unit_length}') 2613 self.units.define(f'reduced_charge = {unit_charge}') 2614 logging.info(self.get_reduced_units()) 2615 2616 def set_simulation_engine(self,simulation_engine,box_l=None): 2617 """ 2618 Sets the instance attribute simulation_engine to an instance of a class of type SimulationEngine. 2619 2620 Args: 2621 simulation_engine (Any): object which contains the methods to setup molecular dynamics and montecarlo simulations 2622 box_l('list[float,float,float]'): list of floats with the dimensions of the box 2623 """ 2624 2625 if isinstance(simulation_engine, espressomd.System): 2626 self.simulation_engine=EspressoSimulation(box_l=simulation_engine.box_l, 2627 db=self.db, 2628 espresso_system=simulation_engine, 2629 units=self.units, 2630 kT=self.kT, 2631 Kw=self.Kw, 2632 seed=self.seed) 2633 elif isinstance(simulation_engine,LammpsProtocol): 2634 self.simulation_engine=LammpsSimulation(box_l=box_l, 2635 db=self.db, 2636 lammps=simulation_engine, 2637 units=self.units, 2638 kT=self.kT, 2639 Kw=self.Kw, 2640 seed=self.seed) 2641 else: 2642 raise ValueError('The specified simulation engine is not implemented yet') 2643 2644 def setup_cpH (self, counter_ion, constant_pH, exclusion_range=None, use_exclusion_radius_per_type = False): 2645 """ 2646 Sets up the Acid/Base reactions for acidic/basic particles defined in the pyMBE database 2647 to be sampled in the constant pH ensemble. 2648 2649 Args: 2650 counter_ion ('str'): 2651 'name' of the counter_ion 'particle'. 2652 2653 constant_pH ('float'): 2654 pH-value. 2655 2656 exclusion_range ('pint.Quantity', optional): 2657 Below this value, no particles will be inserted. 2658 2659 use_exclusion_radius_per_type ('bool', optional): 2660 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2661 2662 Returns: 2663 ('reaction_methods.ConstantpHEnsemble'): 2664 Instance of a reaction_methods.ConstantpHEnsemble object from the espressomd library. 2665 """ 2666 2667 RE = self.simulation_engine.setup_cpH(counter_ion=counter_ion, 2668 constant_pH=constant_pH, 2669 exclusion_range=exclusion_range, 2670 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2671 return RE 2672 2673 def setup_gcmc(self, c_salt_res, salt_cation_name, salt_anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2674 """ 2675 Sets up grand-canonical coupling to a reservoir of salt. 2676 For reactive systems coupled to a reservoir, the grand-reaction method has to be used instead. 2677 2678 Args: 2679 c_salt_res ('pint.Quantity'): 2680 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2681 2682 salt_cation_name ('str'): 2683 Name of the salt cation (e.g. Na+) particle. 2684 2685 salt_anion_name ('str'): 2686 Name of the salt anion (e.g. Cl-) particle. 2687 2688 activity_coefficient ('callable'): 2689 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2690 2691 exclusion_range('pint.Quantity', optional): 2692 For distances shorter than this value, no particles will be inserted. 2693 2694 use_exclusion_radius_per_type('bool',optional): 2695 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2696 2697 Returns: 2698 ('reaction_methods.ReactionEnsemble'): 2699 Instance of a reaction_methods.ReactionEnsemble object from the espressomd library. 2700 """ 2701 RE = self.simulation_engine.setup_gcmc(c_salt_res=c_salt_res, 2702 salt_anion_name=salt_anion_name, 2703 salt_cation_name=salt_cation_name, 2704 activity_coefficient=activity_coefficient, 2705 exclusion_range=exclusion_range, 2706 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2707 return RE 2708 2709 def setup_grxmc_reactions(self, pH_res, c_salt_res, proton_name, hydroxide_name, salt_cation_name, salt_anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2710 """ 2711 Sets up acid/base reactions for acidic/basic monoprotic particles defined in the pyMBE database, 2712 as well as a grand-canonical coupling to a reservoir of small ions. 2713 2714 2715 Args: 2716 pH_res ('float'): 2717 pH-value in the reservoir. 2718 2719 c_salt_res ('pint.Quantity'): 2720 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2721 2722 proton_name ('str'): 2723 Name of the proton (H+) particle. 2724 2725 hydroxide_name ('str'): 2726 Name of the hydroxide (OH-) particle. 2727 2728 salt_cation_name ('str'): 2729 Name of the salt cation (e.g. Na+) particle. 2730 2731 salt_anion_name ('str'): 2732 Name of the salt anion (e.g. Cl-) particle. 2733 2734 activity_coefficient ('callable'): 2735 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2736 2737 exclusion_range('pint.Quantity', optional): 2738 For distances shorter than this value, no particles will be inserted. 2739 2740 use_exclusion_radius_per_type('bool', optional): 2741 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2742 2743 Returns: 2744 For the Espresso system class: 2745 Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'): 2746 2747 'reaction_methods.ReactionEnsemble': 2748 espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 2749 2750 'pint.Quantity': 2751 Ionic strength of the reservoir (useful for calculating partition coefficients). 2752 2753 Notess: 2754 - This implementation uses the original formulation of the grand-reaction method by Landsgesell et al. [1]. 2755 2756 [1] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020. 2757 """ 2758 output=self.simulation_engine.setup_grxmc_reactions(pH_res=pH_res, 2759 c_salt_res=c_salt_res, 2760 proton_name=proton_name, 2761 hydroxide_name=hydroxide_name, 2762 salt_cation_name=salt_cation_name, 2763 salt_anion_name=salt_anion_name, 2764 activity_coefficient=activity_coefficient, 2765 exclusion_range=exclusion_range, 2766 use_exclusion_radius_per_type=use_exclusion_radius_per_type) 2767 2768 return output 2769 2770 def setup_grxmc_unified(self, pH_res, c_salt_res, cation_name, anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2771 """ 2772 Sets up acid/base reactions for acidic/basic 'particles' defined in the pyMBE database, as well as a grand-canonical coupling to a 2773 reservoir of small ions using a unified formulation for small ions. 2774 2775 Args: 2776 pH_res ('float'): 2777 pH-value in the reservoir. 2778 2779 c_salt_res ('pint.Quantity'): 2780 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2781 2782 cation_name ('str'): 2783 Name of the cationic particle. 2784 2785 anion_name ('str'): 2786 Name of the anionic particle. 2787 2788 activity_coefficient ('callable'): 2789 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2790 2791 exclusion_range('pint.Quantity', optional): 2792 Below this value, no particles will be inserted. 2793 2794 use_exclusion_radius_per_type('bool', optional): 2795 Controls if one exclusion_radius per each espresso_type. Defaults to 'False'. 2796 2797 Returns: 2798 For the Espresso system class: 2799 Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'): 2800 2801 'reaction_methods.ReactionEnsemble': 2802 espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 2803 2804 'pint.Quantity': 2805 Ionic strength of the reservoir (useful for calculating partition coefficients). 2806 2807 Notes: 2808 - This implementation uses the formulation of the grand-reaction method by Curk et al. [1], which relies on "unified" ion types X+ = {H+, Na+} and X- = {OH-, Cl-}. 2809 - A function that implements the original version of the grand-reaction method by Landsgesell et al. [2] is also available under the name 'setup_grxmc_reactions'. 2810 2811 [1] Curk, T., Yuan, J., & Luijten, E. (2022). Accelerated simulation method for charge regulation effects. The Journal of Chemical Physics, 156(4). 2812 [2] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020. 2813 """ 2814 output=self.simulation_engine.setup_grxmc_unified(pH_res=pH_res, 2815 c_salt_res=c_salt_res, 2816 cation_name=cation_name, 2817 anion_name=anion_name, 2818 activity_coefficient=activity_coefficient, 2819 exclusion_range=exclusion_range, 2820 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2821 return output 2822 2823 def setup_lj_interactions(self, shift_potential=True, combining_rule='Lorentz-Berthelot'): 2824 """ 2825 Sets up the Lennard-Jones (LJ) potential between all pairs of particle states defined in the pyMBE database. 2826 2827 Args: 2828 2829 shift_potential('bool', optional): 2830 If True, a shift will be automatically computed such that the potential is continuous at the cutoff radius. Otherwise, no shift will be applied. Defaults to True. 2831 2832 combining_rule('string', optional): 2833 combining rule used to calculate 'sigma' and 'epsilon' for the potential between a pair of particles. Defaults to 'Lorentz-Berthelot'. 2834 2835 warning('bool', optional): 2836 switch to activate/deactivate warning messages. Defaults to True. 2837 2838 Notes: 2839 - Currently, the only 'combining_rule' supported is Lorentz-Berthelot. 2840 - Check the documentation of ESPResSo for more info about the potential https://espressomd.github.io/doc4.2.0/inter_non-bonded.html 2841 2842 """ 2843 self.simulation_engine.setup_lj_interactions(shift_potential=shift_potential, 2844 combining_rule=combining_rule)
63class pymbe_library(): 64 """ 65 Core library of the Molecular Builder for ESPResSo (pyMBE). 66 67 Attributes: 68 N_A ('pint.Quantity'): 69 Avogadro number. 70 71 kB ('pint.Quantity'): 72 Boltzmann constant. 73 74 e ('pint.Quantity'): 75 Elementary charge. 76 77 kT ('pint.Quantity'): 78 Thermal energy corresponding to the set temperature. 79 80 Kw ('pint.Quantity'): 81 Ionic product of water, used in G-RxMC and Donnan-related calculations. 82 83 db ('Manager'): 84 Database manager holding all pyMBE templates, instances and reactions. 85 86 rng ('numpy.random.Generator'): 87 Random number generator initialized with the provided seed. 88 89 units ('pint.UnitRegistry'): 90 Pint unit registry used for unit-aware calculations. 91 92 lattice_builder ('pyMBE.lib.lattice.LatticeBuilder'): 93 Optional lattice builder object (initialized as ''None''). 94 95 root ('importlib.resources.abc.Traversable'): 96 Root path to the pyMBE package resources. 97 """ 98 99 def __init__(self, seed, temperature=None, unit_length=None, unit_charge=None, Kw=None): 100 """ 101 Initializes the pyMBE library. 102 103 Args: 104 seed ('int'): 105 Seed for the random number generator. 106 107 temperature ('pint.Quantity', optional): 108 Simulation temperature. If ''None'', defaults to 298.15 K. 109 110 unit_length ('pint.Quantity', optional): 111 Reference length for reduced units. If ''None'', defaults to 112 0.355 nm. 113 114 unit_charge ('pint.Quantity', optional): 115 Reference charge for reduced units. If ''None'', defaults to 116 one elementary charge. 117 118 Kw ('pint.Quantity', optional): 119 Ionic product of water (typically in mol²/L²). If ''None'', 120 defaults to 1e-14 mol²/L². 121 """ 122 # Seed and RNG 123 self.seed=seed 124 self.rng = np.random.default_rng(seed) 125 self.units=pint.UnitRegistry() 126 self.N_A=scipy.constants.N_A / self.units.mol 127 self.kB=scipy.constants.k * self.units.J / self.units.K 128 self.e=scipy.constants.e * self.units.C 129 self.set_reduced_units(unit_length=unit_length, 130 unit_charge=unit_charge, 131 temperature=temperature, 132 Kw=Kw) 133 134 self.db = Manager(units=self.units) 135 self.simulation_engine = DummyEngine() 136 self.lattice_builder = None 137 self.root = importlib.resources.files(__package__) 138 139 def _check_bond_inputs(self, bond_type, bond_parameters): 140 """ 141 Checks that the input bond parameters are valid within the current pyMBE implementation. 142 143 Args: 144 bond_type ('str'): 145 label to identify the potential to model the bond. 146 147 bond_parameters ('dict'): 148 parameters of the potential of the bond. 149 """ 150 valid_bond_types = ["harmonic", "FENE"] 151 if bond_type not in valid_bond_types: 152 raise NotImplementedError(f"Bond type '{bond_type}' currently not implemented in pyMBE, accepted types are {valid_bond_types}") 153 required_parameters = {"harmonic": ["r_0","k"], 154 "FENE": ["r_0","k","d_r_max"]} 155 for required_parameter in required_parameters[bond_type]: 156 if required_parameter not in bond_parameters.keys(): 157 raise ValueError(f"Missing required parameter {required_parameter} for {bond_type} bond") 158 159 def _check_dimensionality(self, variable, expected_dimensionality): 160 """ 161 Checks if the dimensionality of 'variable' matches 'expected_dimensionality'. 162 163 Args: 164 variable ('pint.Quantity'): 165 Quantity to be checked. 166 167 expected_dimensionality ('str'): 168 Expected dimension of the variable. 169 170 Returns: 171 ('bool'): 172 'True' if the variable if of the expected dimensionality, 'False' otherwise. 173 174 Notes: 175 - 'expected_dimensionality' takes dimensionality following the Pint standards [docs](https://pint.readthedocs.io/en/0.10.1/wrapping.html?highlight=dimensionality#checking-dimensionality). 176 - For example, to check for a variable corresponding to a velocity 'expected_dimensionality = "[length]/[time]"' 177 """ 178 correct_dimensionality=variable.check(f"{expected_dimensionality}") 179 if not correct_dimensionality: 180 raise ValueError(f"The variable {variable} should have a dimensionality of {expected_dimensionality}, instead the variable has a dimensionality of {variable.dimensionality}") 181 return correct_dimensionality 182 183 def _check_pka_set(self, pka_set): 184 """ 185 Checks that 'pka_set' has the formatting expected by pyMBE. 186 187 Args: 188 pka_set ('dict'): 189 {"name" : {"pka_value": pka, "acidity": acidity}} 190 """ 191 required_keys=['pka_value','acidity'] 192 for required_key in required_keys: 193 for pka_name, pka_entry in pka_set.items(): 194 if required_key not in pka_entry: 195 raise ValueError(f'missing a required key "{required_key}" in entry "{pka_name}" of pka_set ("{pka_entry}")') 196 197 def _create_hydrogel_chain(self, hydrogel_chain, nodes,box_l, use_default_bond=False, gen_angle=False): 198 """ 199 Creates a chain between two nodes of a hydrogel. 200 201 Args: 202 hydrogel_chain ('HydrogelChain'): 203 template of a hydrogel chain 204 nodes ('dict'): 205 {node_index: {"name": node_particle_name, "pos": node_position, "id": node_particle_instance_id}} 206 box_l('list[float,float,float]'): side length of the simulation box for x,y and z coordinates. 207 use_default_bond ('bool', optional): 208 If True, use a default bond template if no specific template exists. Defaults to False. 209 210 gen_angle ('bool', optional): 211 If True, generate the angle potentials internal to the created 212 chain molecule. Junction angles near the hydrogel crosslinkers 213 are handled separately at the hydrogel level. 214 215 Return: 216 ('int'): 217 molecule_id of the created hydrogel chian. 218 219 Notes: 220 - If the chain is defined between node_start = ''[0 0 0]'' and node_end = ''[1 1 1]'', the chain will be placed between these two nodes. 221 - The chain will be placed in the direction of the vector between 'node_start' and 'node_end'. 222 """ 223 if self.lattice_builder is None: 224 raise ValueError("LatticeBuilder is not initialized. Use 'initialize_lattice_builder' first.") 225 molecule_tpl = self.db.get_template(pmb_type="molecule", 226 name=hydrogel_chain.molecule_name) 227 residue_list = molecule_tpl.residue_list 228 molecule_name = molecule_tpl.name 229 node_start = hydrogel_chain.node_start 230 node_end = hydrogel_chain.node_end 231 node_start_label = self.lattice_builder._create_node_label(node_start) 232 node_end_label = self.lattice_builder._create_node_label(node_end) 233 _, reverse = self.lattice_builder._get_node_vector_pair(node_start, node_end) 234 if node_start == node_end and residue_list != residue_list[::-1]: 235 raise ValueError(f"Aborted creation of hydrogel chain between '{node_start}' and '{node_end}' because pyMBE could not resolve a unique topology for that chain") 236 if reverse: 237 reverse_residue_order=True 238 else: 239 reverse_residue_order=False 240 start_node_id = nodes[node_start_label]["id"] 241 end_node_id = nodes[node_end_label]["id"] 242 # Finding a backbone vector between node_start and node_end 243 vec_between_nodes = np.array(nodes[node_end_label]["pos"]) - np.array(nodes[node_start_label]["pos"]) 244 vec_between_nodes = vec_between_nodes - self.lattice_builder.box_l * np.round(vec_between_nodes/self.lattice_builder.box_l) 245 backbone_vector = vec_between_nodes / (self.lattice_builder.mpc+1) 246 if reverse_residue_order: 247 vec_between_nodes *= -1.0 248 # Calculate the start position of the chain 249 chain_residues = self.db.get_template(pmb_type="molecule", 250 name=molecule_name).residue_list 251 part_start_chain_name = self.db.get_template(pmb_type="residue", 252 name=chain_residues[0]).central_bead 253 lj_parameters = self.get_lj_parameters(particle_name1=nodes[node_start_label]["name"], 254 particle_name2=part_start_chain_name) 255 bond_tpl = self.get_bond_template(particle_name1=nodes[node_start_label]["name"], 256 particle_name2=part_start_chain_name, 257 use_default_bond=use_default_bond) 258 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 259 bond_type=bond_tpl.bond_type, 260 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 261 first_bead_pos = np.array((nodes[node_start_label]["pos"])) + np.array(backbone_vector)*l0 262 mol_id = self.create_molecule(name=molecule_name, # Use the name defined earlier 263 number_of_molecules=1, # Creating one chain 264 box_l=box_l, ### Add lattice_builder box length size, this should be box_l=[self.lattice_builder.box_l]*3 265 list_of_first_residue_positions=[first_bead_pos.tolist()], #Start at the first node 266 backbone_vector=np.array(backbone_vector)/l0, 267 use_default_bond=use_default_bond, 268 reverse_residue_order=reverse_residue_order, 269 gen_angle=gen_angle)[0] 270 chain_pids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 271 attribute="molecule_id", 272 value=mol_id) 273 self.create_bond(particle_id1=start_node_id,particle_id2=chain_pids[0],use_default_bond=use_default_bond) 274 self.create_bond(particle_id1=chain_pids[-1],particle_id2=end_node_id,use_default_bond=use_default_bond) 275 return mol_id 276 277 def _generate_hydrogel_crosslinker_angles(self, central_particle_ids): 278 """ 279 Generate hydrogel angles centered on crosslinkers and adjacent terminal beads. 280 281 If the user defines any explicit angle template for such junction 282 triplets, then all required junction triplets must be defined. If none 283 are defined, hydrogel construction proceeds without crosslinker-adjacent 284 angles. 285 """ 286 particle_instances = self.db.get_instances(pmb_type="particle") 287 bonded_neighbors = {} 288 for bond in self.db.get_instances(pmb_type="bond").values(): 289 bonded_neighbors.setdefault(bond.particle_id1, set()).add(bond.particle_id2) 290 bonded_neighbors.setdefault(bond.particle_id2, set()).add(bond.particle_id1) 291 292 triplets = [] 293 for central_particle_id in sorted(set(central_particle_ids)): 294 neighbors = sorted(bonded_neighbors.get(central_particle_id, set())) 295 central_name = particle_instances[central_particle_id].name 296 for idx_i in range(len(neighbors)): 297 for idx_k in range(idx_i + 1, len(neighbors)): 298 side_particle_id1 = neighbors[idx_i] 299 side_particle_id3 = neighbors[idx_k] 300 side_name1 = particle_instances[side_particle_id1].name 301 side_name3 = particle_instances[side_particle_id3].name 302 angle_key = AngleTemplate.make_angle_key(side1=side_name1, 303 central=central_name, 304 side2=side_name3) 305 triplets.append((side_particle_id1, 306 central_particle_id, 307 side_particle_id3, 308 angle_key)) 309 310 defined_angle_templates = self.db.get_templates(pmb_type="angle") 311 defined_angle_keys = {angle_key for _, _, _, angle_key in triplets if angle_key in defined_angle_templates} 312 if not defined_angle_keys: 313 logging.warning("No angle templates defined for hydrogel crosslinkers") 314 return 315 missing_angle_keys = sorted({angle_key for _, _, _, angle_key in triplets if angle_key not in defined_angle_keys}) 316 if missing_angle_keys: 317 raise ValueError("Hydrogel crosslinker-adjacent angle templates must be defined for all required triplets. " 318 f"Missing definitions for: {missing_angle_keys}") 319 for side_particle_id1, central_particle_id, side_particle_id3, _ in triplets: 320 self.create_angular_potential(particle_id1=side_particle_id1, 321 particle_id2=central_particle_id, 322 particle_id3=side_particle_id3, 323 use_default_angle=False) 324 325 def _create_hydrogel_node(self, node_index, node_name,box_l): 326 """ 327 Set a node residue type. 328 329 Args: 330 node_index ('str'): 331 Lattice node index in the form of a string, e.g. "[0 0 0]". 332 333 node_name ('str'): 334 name of the node particle defined in pyMBE. 335 336 box_l('list[float,float,float]'): list of floats with the dimensions of the box 337 338 Returns: 339 ('tuple(list,int)'): 340 ('list'): Position of the node in the lattice. 341 ('int'): Particle ID of the node. 342 """ 343 if self.lattice_builder is None: 344 raise ValueError("LatticeBuilder is not initialized. Use 'initialize_lattice_builder' first.") 345 node_position = np.array(node_index)*0.25*self.lattice_builder.box_l 346 p_id = self.create_particle(name = node_name, 347 box_l=box_l, 348 number_of_particles=1, 349 position = [node_position]) 350 key = self.lattice_builder._get_node_by_label(f"[{node_index[0]} {node_index[1]} {node_index[2]}]") 351 self.lattice_builder.nodes[key] = node_name 352 return node_position.tolist(), p_id[0] 353 354 def _get_residue_list_from_sequence(self, sequence): 355 """ 356 Convenience function to get a 'residue_list' from a protein or peptide 'sequence'. 357 358 Args: 359 sequence ('lst'): 360 Sequence of the peptide or protein. 361 362 Returns: 363 residue_list ('list' of 'str'): 364 List of the 'name's of the 'residue's in the sequence of the 'molecule'. 365 """ 366 residue_list = [] 367 for item in sequence: 368 residue_name='AA-'+item 369 residue_list.append(residue_name) 370 return residue_list 371 372 def _get_template_type(self, name, allowed_types): 373 """ 374 Validate that a template name resolves unambiguously to exactly one 375 allowed pmb_type in the pyMBE database and return it. 376 377 Args: 378 name ('str'): 379 Name of the template to validate. 380 381 allowed_types ('set[str]'): 382 Set of allowed pmb_type values (e.g. {"molecule", "peptide"}). 383 384 Returns: 385 ('str'): 386 Resolved pmb_type. 387 388 Notes: 389 - This method does *not* return the template itself, only the validated pmb_type. 390 """ 391 registered_pmb_types_with_name = self.db._find_template_types(name=name) 392 filtered_types = allowed_types.intersection(registered_pmb_types_with_name) 393 if len(filtered_types) > 1: 394 raise ValueError(f"Ambiguous template name '{name}': found {len(filtered_types)} templates in the pyMBE database. Molecule creation aborted.") 395 if len(filtered_types) == 0: 396 raise ValueError(f"No {allowed_types} template found with name '{name}'. Found templates of types: {filtered_types}.") 397 return next(iter(filtered_types)) 398 399 def _delete_particles_from_engine(self, particle_ids): 400 """ 401 Remove a list of particles from an ESPResSo simulation system. 402 403 Args: 404 particle_ids ('Iterable[int]'): 405 A list (or other iterable) of ESPResSo particle IDs to remove. 406 407 Notes: 408 - This method removes particles only from the ESPResSo simulation, 409 **not** from the pyMBE database. Database cleanup must be handled 410 separately by the caller. 411 - Attempting to remove a non-existent particle ID will raise 412 an ESPResSo error. 413 """ 414 self.simulation_engine._delete_particles(particle_ids) 415 416 def add_instances_to_engine(self): 417 self.simulation_engine.add_instances_to_engine() 418 419 def calculate_center_of_mass(self, instance_id, pmb_type): 420 """ 421 Calculates the center of mass of a pyMBE object instance in an ESPResSo system. 422 423 Args: 424 instance_id ('int'): 425 pyMBE instance ID of the object whose center of mass is calculated. 426 427 pmb_type ('str'): 428 Type of the pyMBE object. Must correspond to a particle-aggregating 429 template type (e.g. '"molecule"', '"residue"', '"peptide"', '"protein"'). 430 431 Returns: 432 ('numpy.ndarray'): 433 Array of shape '(3,)' containing the Cartesian coordinates of the 434 center of mass. 435 436 Notes: 437 - This method assumes equal mass for all particles. 438 - Periodic boundary conditions are *not* unfolded; positions are taken 439 directly from ESPResSo particle coordinates. 440 """ 441 return self.simulation_engine.calculate_center_of_mass(instance_id=instance_id, 442 pmb_type=pmb_type) 443 444 def calculate_HH(self, template_name, pH_list=None, pka_set=None): 445 """ 446 Calculates the charge in the template object according to the ideal Henderson–Hasselbalch titration curve. 447 448 Args: 449 template_name ('str'): 450 Name of the template. 451 452 pH_list ('list[float]', optional): 453 pH values at which the charge is evaluated. 454 Defaults to 50 values between 2 and 12. 455 456 pka_set ('dict', optional): 457 Mapping: {particle_name: {"pka_value": 'float', "acidity": "acidic"|"basic"}} 458 459 Returns: 460 'list[float]': 461 Net molecular charge at each pH value. 462 """ 463 if pH_list is None: 464 pH_list = np.linspace(2, 12, 50) 465 if pka_set is None: 466 pka_set = self.get_pka_set() 467 self._check_pka_set(pka_set=pka_set) 468 particle_counts = self.db.get_particle_templates_under(template_name=template_name, 469 return_counts=True) 470 if not particle_counts: 471 return [None] * len(pH_list) 472 charge_number_map = self.get_charge_number_map() 473 def formal_charge(particle_name): 474 tpl = self.db.get_template(name=particle_name, 475 pmb_type="particle") 476 state = self.db.get_template(name=tpl.initial_state, 477 pmb_type="particle_state") 478 return charge_number_map[state.es_type] 479 Z_HH = [] 480 for pH in pH_list: 481 Z = 0.0 482 for particle, multiplicity in particle_counts.items(): 483 if particle in pka_set: 484 pka = pka_set[particle]["pka_value"] 485 acidity = pka_set[particle]["acidity"] 486 if acidity == "acidic": 487 psi = -1 488 elif acidity == "basic": 489 psi = +1 490 else: 491 raise ValueError(f"Unknown acidity '{acidity}' for particle '{particle}'") 492 charge = psi / (1.0 + 10.0 ** (psi * (pH - pka))) 493 Z += multiplicity * charge 494 else: 495 Z += multiplicity * formal_charge(particle) 496 Z_HH.append(Z) 497 return Z_HH 498 499 def calculate_HH_Donnan(self, c_macro, c_salt, pH_list=None, pka_set=None): 500 """ 501 Computes macromolecular charges using the Henderson–Hasselbalch equation 502 coupled to ideal Donnan partitioning. 503 504 Args: 505 c_macro ('dict'): 506 Mapping of macromolecular species names to their concentrations 507 in the system: 508 '{molecule_name: concentration}'. 509 510 c_salt ('float' or 'pint.Quantity'): 511 Salt concentration in the reservoir. 512 513 pH_list ('list[float]', optional): 514 List of pH values in the reservoir at which the calculation is 515 performed. If 'None', 50 equally spaced values between 2 and 12 516 are used. 517 518 pka_set ('dict', optional): 519 Dictionary defining the acid–base properties of titratable particle 520 types: 521 '{particle_name: {"pka_value": float, "acidity": "acidic" | "basic"}}'. 522 If 'None', the pKa set is taken from the pyMBE database. 523 524 Returns: 525 'dict': 526 Dictionary containing: 527 - '"charges_dict"' ('dict'): 528 Mapping '{molecule_name: list}' of Henderson–Hasselbalch–Donnan 529 charges evaluated at each pH value. 530 - '"pH_system_list"' ('list[float]'): 531 Effective pH values inside the system phase after Donnan 532 partitioning. 533 - '"partition_coefficients"' ('list[float]'): 534 Partition coefficients of monovalent cations at each pH value. 535 536 Notes: 537 - This method assumes **ideal Donnan equilibrium** and **monovalent salt**. 538 - The ionic strength of the reservoir includes both salt and 539 pH-dependent H⁺/OH⁻ contributions. 540 - All charged macromolecular species present in the system must be 541 included in 'c_macro'; missing species will lead to incorrect results. 542 - The nonlinear Donnan equilibrium equation is solved using a scalar 543 root finder ('brentq') in logarithmic form for numerical stability. 544 - This method is intended for **two-phase systems**; for single-phase 545 systems use 'calculate_HH' instead. 546 """ 547 if pH_list is None: 548 pH_list=np.linspace(2,12,50) 549 if pka_set is None: 550 pka_set=self.get_pka_set() 551 self._check_pka_set(pka_set=pka_set) 552 partition_coefficients_list = [] 553 pH_system_list = [] 554 Z_HH_Donnan={} 555 for key in c_macro: 556 Z_HH_Donnan[key] = [] 557 def calc_charges(c_macro, pH): 558 """ 559 Calculates the charges of the different kinds of molecules according to the Henderson-Hasselbalch equation. 560 561 Args: 562 c_macro ('dict'): 563 {"name": concentration} - A dict containing the concentrations of all charged macromolecular species in the system. 564 565 pH ('float'): 566 pH-value that is used in the HH equation. 567 568 Returns: 569 ('dict'): 570 {"molecule_name": charge} 571 """ 572 charge = {} 573 for name in c_macro: 574 charge[name] = self.calculate_HH(name, [pH], pka_set)[0] 575 return charge 576 577 def calc_partition_coefficient(charge, c_macro): 578 """ 579 Calculates the partition coefficients of positive ions according to the ideal Donnan theory. 580 581 Args: 582 charge ('dict'): 583 {"molecule_name": charge} 584 585 c_macro ('dict'): 586 {"name": concentration} - A dict containing the concentrations of all charged macromolecular species in the system. 587 """ 588 nonlocal ionic_strength_res 589 charge_density = 0.0 590 for key in charge: 591 charge_density += charge[key] * c_macro[key] 592 return (-charge_density / (2 * ionic_strength_res) + np.sqrt((charge_density / (2 * ionic_strength_res))**2 + 1)).magnitude 593 for pH_value in pH_list: 594 # calculate the ionic strength of the reservoir 595 if pH_value <= 7.0: 596 ionic_strength_res = 10 ** (-pH_value) * self.units.mol/self.units.l + c_salt 597 elif pH_value > 7.0: 598 ionic_strength_res = 10 ** (-(14-pH_value)) * self.units.mol/self.units.l + c_salt 599 #Determine the partition coefficient of positive ions by solving the system of nonlinear, coupled equations 600 #consisting of the partition coefficient given by the ideal Donnan theory and the Henderson-Hasselbalch equation. 601 #The nonlinear equation is formulated for log(xi) since log-operations are not supported for RootResult objects. 602 equation = lambda logxi: logxi - np.log10(calc_partition_coefficient(calc_charges(c_macro, pH_value - logxi), c_macro)) 603 logxi = scipy.optimize.root_scalar(equation, bracket=[-1e2, 1e2], method="brentq") 604 partition_coefficient = 10**logxi.root 605 charges_temp = calc_charges(c_macro, pH_value-np.log10(partition_coefficient)) 606 for key in c_macro: 607 Z_HH_Donnan[key].append(charges_temp[key]) 608 pH_system_list.append(pH_value - np.log10(partition_coefficient)) 609 partition_coefficients_list.append(partition_coefficient) 610 return {"charges_dict": Z_HH_Donnan, "pH_system_list": pH_system_list, "partition_coefficients": partition_coefficients_list} 611 612 def calculate_net_charge(self,object_name,pmb_type,dimensionless=False): 613 """ 614 Calculates the net charge per instance of a given pmb object type. 615 616 Args: 617 object_name (str): 618 Name of the object (e.g. molecule, residue, peptide, protein). 619 pmb_type (str): 620 Type of object to analyze. Must be molecule-like. 621 dimensionless (bool, optional): 622 If True, return charge as a pure number. 623 If False, return a quantity with reduced_charge units. 624 625 Returns: 626 dict: 627 {"mean": mean_net_charge, "instances": {instance_id: net_charge}} 628 """ 629 return self.simulation_engine.calculate_net_charge(object_name, 630 pmb_type, 631 dimensionless) 632 633 def center_object_in_simulation_box(self, instance_id, box_l,pmb_type): 634 """ 635 Centers a pyMBE object instance in the simulation box of an ESPResSo system. 636 The object is translated such that its center of mass coincides with the 637 geometric center of the ESPResSo simulation box. 638 639 Args: 640 instance_id ('int'): 641 ID of the pyMBE object instance to be centered. 642 643 box_l('list[float,float,float]'): list of floats with the dimensions of the box 644 645 pmb_type ('str'): 646 Type of the pyMBE object. 647 648 Notes: 649 - Works for both cubic and non-cubic simulation boxes. 650 """ 651 inst = self.db.get_instance(instance_id=instance_id, 652 pmb_type=pmb_type) 653 center_of_mass = self.calculate_center_of_mass(instance_id=instance_id, 654 pmb_type=pmb_type) 655 box_center = [box_l[0]/2.0, 656 box_l[1]/2.0, 657 box_l[2]/2.0] 658 particle_id_list = self.get_particle_id_map(object_name=inst.name)["all"] 659 for pid in particle_id_list: 660 es_pos=self.db.get_instance(instance_id=pid, 661 pmb_type='particle').position 662 centered_position=es_pos - center_of_mass + box_center 663 664 self.db._update_instance(instance_id=pid, 665 pmb_type='particle', 666 attribute='position', 667 value=centered_position) 668 if isinstance(self.simulation_engine, EspressoSimulation): 669 self.simulation_engine._update_particle_position( 670 particle_id=pid, position=centered_position) 671 672 def create_added_salt(self, box_l, cation_name, anion_name, c_salt): 673 """ 674 Creates a 'c_salt' concentration of 'cation_name' and 'anion_name' ions into the 'espresso_system'. 675 676 Args: 677 cation_name('str'): 'name' of a particle with a positive charge. 678 anion_name('str'): 'name' of a particle with a negative charge. 679 c_salt('float'): Salt concentration. 680 681 Returns: 682 c_salt_calculated('float'): Calculated salt concentration added to 'espresso_system'. 683 """ 684 cation_tpl = self.db.get_template(pmb_type="particle", 685 name=cation_name) 686 cation_state = self.db.get_template(pmb_type="particle_state", 687 name=cation_tpl.initial_state) 688 cation_charge = cation_state.z 689 anion_tpl = self.db.get_template(pmb_type="particle", 690 name=anion_name) 691 anion_state = self.db.get_template(pmb_type="particle_state", 692 name=anion_tpl.initial_state) 693 anion_charge = anion_state.z 694 if cation_charge <= 0: 695 raise ValueError(f'ERROR cation charge must be positive, charge {cation_charge}') 696 if anion_charge >= 0: 697 raise ValueError(f'ERROR anion charge must be negative, charge {anion_charge}') 698 # Calculate the number of ions in the simulation box 699 volume=self.units.Quantity(np.prod(box_l), 'reduced_length**3') 700 if c_salt.check('[substance] [length]**-3'): 701 N_ions= int((volume*c_salt.to('mol/reduced_length**3')*self.N_A).magnitude) 702 c_salt_calculated=N_ions/(volume*self.N_A) 703 elif c_salt.check('[length]**-3'): 704 N_ions= int((volume*c_salt.to('reduced_length**-3')).magnitude) 705 c_salt_calculated=N_ions/volume 706 else: 707 raise ValueError('Unknown units for c_salt, please provided it in [mol / volume] or [particle / volume]', c_salt) 708 N_cation = N_ions*abs(anion_charge) 709 N_anion = N_ions*abs(cation_charge) 710 self.create_particle(box_l=box_l, 711 name=cation_name, 712 number_of_particles=N_cation) 713 self.create_particle(box_l=box_l, 714 name=anion_name, 715 number_of_particles=N_anion) 716 if c_salt_calculated.check('[substance] [length]**-3'): 717 logging.info(f"added salt concentration of {c_salt_calculated.to('mol/L')} given by {N_cation} cations and {N_anion} anions") 718 elif c_salt_calculated.check('[length]**-3'): 719 logging.info(f"added salt concentration of {c_salt_calculated.to('reduced_length**-3')} given by {N_cation} cations and {N_anion} anions") 720 return c_salt_calculated 721 722 def create_bond(self, particle_id1, particle_id2, use_default_bond=False): 723 """ 724 Creates a bond between two particle instances in an ESPResSo system and registers it in the pyMBE database. 725 726 This method performs the following steps: 727 1. Retrieves the particle instances corresponding to 'particle_id1' and 'particle_id2' from the database. 728 2. Retrieves or creates the corresponding ESPResSo bond instance using the bond template. 729 3. Adds the ESPResSo bond instance to the ESPResSo system if it was newly created. 730 4. Adds the bond to the first particle's bond list in ESPResSo. 731 5. Creates a 'BondInstance' in the database and registers it. 732 733 Args: 734 particle_id1 ('int'): 735 pyMBE and ESPResSo ID of the first particle. 736 737 particle_id2 ('int'): 738 pyMBE and ESPResSo ID of the second particle. 739 740 use_default_bond ('bool', optional): 741 If True, use a default bond template if no specific template exists. Defaults to False. 742 743 Returns: 744 ('int'): 745 bond_id of the bond instance created in the pyMBE database. 746 """ 747 particle_inst_1 = self.db.get_instance(pmb_type="particle", 748 instance_id=particle_id1) 749 particle_inst_2 = self.db.get_instance(pmb_type="particle", 750 instance_id=particle_id2) 751 bond_tpl = self.get_bond_template(particle_name1=particle_inst_1.name, 752 particle_name2=particle_inst_2.name, 753 use_default_bond=use_default_bond) 754 bond_id = self.db._propose_instance_id(pmb_type="bond") 755 pmb_bond_instance = BondInstance(bond_id=bond_id, 756 name=bond_tpl.name, 757 particle_id1=particle_id1, 758 particle_id2=particle_id2) 759 self.db._register_instance(instance=pmb_bond_instance) 760 761 def create_counterions(self, object_name, cation_name, anion_name, box_l): 762 """ 763 Creates particles of 'cation_name' and 'anion_name' in 'espresso_system' to counter the net charge of 'object_name'. 764 765 Args: 766 object_name ('str'): 767 'name' of a pyMBE object. 768 769 cation_name ('str'): 770 'name' of a particle with a positive charge. 771 772 anion_name ('str'): 773 'name' of a particle with a negative charge. 774 775 box_l('list[float,float,float]'): list of floats with the dimensions of the box 776 777 Returns: 778 ('dict'): 779 {"name": number} 780 781 Notes: 782 This function currently does not support the creation of counterions for hydrogels. 783 """ 784 cation_tpl = self.db.get_template(pmb_type="particle", 785 name=cation_name) 786 cation_state = self.db.get_template(pmb_type="particle_state", 787 name=cation_tpl.initial_state) 788 cation_charge = cation_state.z 789 anion_tpl = self.db.get_template(pmb_type="particle", 790 name=anion_name) 791 anion_state = self.db.get_template(pmb_type="particle_state", 792 name=anion_tpl.initial_state) 793 anion_charge = anion_state.z 794 object_ids = self.get_particle_id_map(object_name=object_name)["all"] 795 counterion_number={} 796 object_charge={} 797 for name in ['positive', 'negative']: 798 object_charge[name]=0 799 for id in object_ids: 800 object_name = self.db.get_instance(pmb_type="particle", 801 instance_id=id).name 802 object_tpl = self.db.get_template(pmb_type="particle", 803 name=object_name) 804 object_state = self.db.get_template(pmb_type="particle_state", 805 name=object_tpl.initial_state) 806 object_z = object_state.z 807 if object_z > 0: 808 object_charge['positive']+=1*(np.abs(object_z )) 809 elif object_z < 0: 810 object_charge['negative']+=1*(np.abs(object_z )) 811 if object_charge['positive'] % abs(anion_charge) == 0: 812 counterion_number[anion_name]=int(object_charge['positive']/abs(anion_charge)) 813 else: 814 raise ValueError('The number of positive charges in the pmb_object must be divisible by the charge of the anion') 815 if object_charge['negative'] % abs(cation_charge) == 0: 816 counterion_number[cation_name]=int(object_charge['negative']/cation_charge) 817 else: 818 raise ValueError('The number of negative charges in the pmb_object must be divisible by the charge of the cation') 819 if counterion_number[cation_name] > 0: 820 self.create_particle(box_l=box_l, 821 name=cation_name, 822 number_of_particles=counterion_number[cation_name]) 823 else: 824 counterion_number[cation_name]=0 825 if counterion_number[anion_name] > 0: 826 self.create_particle(box_l=box_l, 827 name=anion_name, 828 number_of_particles=counterion_number[anion_name]) 829 else: 830 counterion_number[anion_name] = 0 831 logging.info('the following counter-ions have been created: ') 832 for name in counterion_number.keys(): 833 logging.info(f'Ion type: {name} created number: {counterion_number[name]}') 834 return counterion_number 835 836 837 def create_hydrogel(self, name, box_l, use_default_bond=False, gen_angle=False): 838 """ 839 Creates a hydrogel in espresso_system using a pyMBE hydrogel template given by 'name' 840 841 Args: 842 box_l('list[float,float,float]'): list of floats with the dimensions of the box 843 844 name ('str'): 845 name of the hydrogel template in the pyMBE database. 846 847 use_default_bond ('bool', optional): 848 If True, use a default bond template if no specific template exists. Defaults to False. 849 850 gen_angle ('bool', optional): 851 If True, generate angle potentials for the internal hydrogel 852 chains and, when explicitly defined, for all crosslinker-adjacent 853 triplets. Defaults to False. 854 855 Returns: 856 ('int'): id of the hydrogel instance created. 857 """ 858 if not self.db._has_template(name=name, pmb_type="hydrogel"): 859 raise ValueError(f"Hydrogel template with name '{name}' is not defined in the pyMBE database.") 860 hydrogel_tpl = self.db.get_template(pmb_type="hydrogel", 861 name=name) 862 assembly_id = self.db._propose_instance_id(pmb_type="hydrogel") 863 # Create the nodes 864 nodes = {} 865 hydrogel_angle_centers = set() 866 node_topology = hydrogel_tpl.node_map 867 for node in node_topology: 868 node_index = node.lattice_index 869 node_name = node.particle_name 870 node_pos, node_id = self._create_hydrogel_node(node_index=node_index, 871 node_name=node_name, 872 box_l=box_l) 873 node_label = self.lattice_builder._create_node_label(node_index=node_index) 874 nodes[node_label] = {"name": node_name, "id": node_id, "pos": node_pos} 875 self.db._update_instance(instance_id=node_id, 876 pmb_type="particle", 877 attribute="assembly_id", 878 value=assembly_id) 879 for hydrogel_chain in hydrogel_tpl.chain_map: 880 molecule_id = self._create_hydrogel_chain(hydrogel_chain=hydrogel_chain, 881 nodes=nodes, 882 box_l=box_l, 883 use_default_bond=use_default_bond, 884 gen_angle=gen_angle, 885 ) 886 self.db._update_instance(instance_id=molecule_id, 887 pmb_type="molecule", 888 attribute="assembly_id", 889 value=assembly_id) 890 if gen_angle: 891 residue_ids = self.db._find_instance_ids_by_attribute(pmb_type="residue", 892 attribute="molecule_id", 893 value=molecule_id) 894 first_residue_id = min(residue_ids) 895 last_residue_id = max(residue_ids) 896 first_residue = self.db.get_instance(pmb_type="residue", 897 instance_id=first_residue_id) 898 last_residue = self.db.get_instance(pmb_type="residue", 899 instance_id=last_residue_id) 900 first_central_bead_name = self.db.get_template(pmb_type="residue", 901 name=first_residue.name).central_bead 902 last_central_bead_name = self.db.get_template(pmb_type="residue", 903 name=last_residue.name).central_bead 904 particle_instances = self.db.get_instances(pmb_type="particle") 905 first_residue_particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 906 attribute="residue_id", 907 value=first_residue_id) 908 last_residue_particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 909 attribute="residue_id", 910 value=last_residue_id) 911 first_bead_id = None 912 for particle_id in first_residue_particle_ids: 913 if particle_instances[particle_id].name == first_central_bead_name: 914 first_bead_id = particle_id 915 break 916 917 last_bead_id = None 918 for particle_id in last_residue_particle_ids: 919 if particle_instances[particle_id].name == last_central_bead_name: 920 last_bead_id = particle_id 921 break 922 node_start_label = self.lattice_builder._create_node_label(hydrogel_chain.node_start) 923 node_end_label = self.lattice_builder._create_node_label(hydrogel_chain.node_end) 924 hydrogel_angle_centers.update({ 925 nodes[node_start_label]["id"], 926 nodes[node_end_label]["id"], 927 first_bead_id, 928 last_bead_id, 929 }) 930 self.db._propagate_id(root_type="hydrogel", 931 root_id=assembly_id, 932 attribute="assembly_id", 933 value=assembly_id) 934 if gen_angle: 935 self._generate_hydrogel_crosslinker_angles( 936 central_particle_ids=hydrogel_angle_centers) 937 # Register an hydrogel instance in the pyMBE databasegit 938 self.db._register_instance(HydrogelInstance(name=name, 939 assembly_id=assembly_id)) 940 return assembly_id 941 942 943 def create_molecule(self, name, number_of_molecules, box_l, list_of_first_residue_positions=None, backbone_vector=None, use_default_bond=False, reverse_residue_order = False, gen_angle=False): 944 """ 945 Creates instances of a given molecule template name into ESPResSo. 946 947 Args: 948 name ('str'): 949 Label of the molecule type to be created. 'name'. 950 951 box_l('list[float,float,float]'): list of floats with the dimensions of the box 952 953 number_of_molecules ('int'): 954 Number of molecules or peptides of type 'name' to be created. 955 956 list_of_first_residue_positions ('list', optional): 957 List of coordinates where the central bead of the first_residue_position will be created, random by default. 958 959 backbone_vector ('list' of 'float'): 960 Backbone vector of the molecule, random by default. Central beads of the residues in the 'residue_list' are placed along this vector. 961 962 use_default_bond('bool', optional): 963 Controls if a bond of type 'default' is used to bond particles with undefined bonds in the pyMBE database. 964 965 reverse_residue_order('bool', optional): 966 Creates residues in reverse sequential order than the one defined in the molecule template. Defaults to False. 967 968 Returns: 969 ('list' of 'int'): 970 List with the 'molecule_id' of the pyMBE molecule instances created into 'espresso_system'. 971 972 Notes: 973 - This function can be used to create both molecules and peptides. 974 """ 975 pmb_type = self._get_template_type(name=name, 976 allowed_types={"molecule", "peptide"}) 977 if number_of_molecules <= 0: 978 return {} 979 if list_of_first_residue_positions is not None: 980 for item in list_of_first_residue_positions: 981 if not isinstance(item, list): 982 raise ValueError("The provided input position is not a nested list. Should be a nested list with elements of 3D lists, corresponding to xyz coord.") 983 elif len(item) != 3: 984 raise ValueError("The provided input position is formatted wrong. The elements in the provided list does not have 3 coordinates, corresponding to xyz coord.") 985 986 if len(list_of_first_residue_positions) != number_of_molecules: 987 raise ValueError(f"Number of positions provided in {list_of_first_residue_positions} does not match number of molecules desired, {number_of_molecules}") 988 # Generate an arbitrary random unit vector 989 if backbone_vector is None: 990 backbone_vector = self.generate_random_points_in_a_sphere(center=[0,0,0], 991 radius=1, 992 n_samples=1, 993 on_surface=True)[0] 994 else: 995 backbone_vector = np.array(backbone_vector) 996 first_residue = True 997 molecule_tpl = self.db.get_template(pmb_type=pmb_type, 998 name=name) 999 if reverse_residue_order: 1000 residue_list = molecule_tpl.residue_list[::-1] 1001 else: 1002 residue_list = molecule_tpl.residue_list 1003 pos_index = 0 1004 molecule_ids = [] 1005 for n_mol in range(number_of_molecules): 1006 molecule_id = self.db._propose_instance_id(pmb_type=pmb_type) 1007 for residue in residue_list: 1008 if first_residue: 1009 if list_of_first_residue_positions is None: 1010 central_bead_pos = None 1011 else: 1012 central_bead_pos = [np.array(list_of_first_residue_positions[n_mol])] 1013 1014 residue_id = self.create_residue(name=residue, 1015 box_l=box_l, 1016 central_bead_position=central_bead_pos, 1017 use_default_bond= use_default_bond, 1018 backbone_vector=backbone_vector) 1019 1020 # Add molecule_id to the residue instance and all particles associated 1021 self.db._propagate_id(root_type="residue", 1022 root_id=residue_id, 1023 attribute="molecule_id", 1024 value=molecule_id) 1025 particle_ids_in_residue = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1026 attribute="residue_id", 1027 value=residue_id) 1028 prev_central_bead_id = particle_ids_in_residue[0] 1029 prev_central_bead_name = self.db.get_instance(pmb_type="particle", 1030 instance_id=prev_central_bead_id).name 1031 prev_central_bead_pos = self.db.get_instance(pmb_type="particle", 1032 instance_id=prev_central_bead_id).position 1033 # prev_central_bead_pos = espresso_system.part.by_id(prev_central_bead_id).pos 1034 first_residue = False 1035 else: 1036 1037 # Calculate the starting position of the new residue 1038 residue_tpl = self.db.get_template(pmb_type="residue", 1039 name=residue) 1040 lj_parameters = self.get_lj_parameters(particle_name1=prev_central_bead_name, 1041 particle_name2=residue_tpl.central_bead) 1042 bond_tpl = self.get_bond_template(particle_name1=prev_central_bead_name, 1043 particle_name2=residue_tpl.central_bead, 1044 use_default_bond=use_default_bond) 1045 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1046 bond_type=bond_tpl.bond_type, 1047 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1048 central_bead_pos = prev_central_bead_pos+backbone_vector*l0 1049 # Create the residue 1050 residue_id = self.create_residue(name=residue, 1051 box_l=box_l, 1052 central_bead_position=[central_bead_pos], 1053 use_default_bond= use_default_bond, 1054 backbone_vector=backbone_vector) 1055 # Add molecule_id to the residue instance and all particles associated 1056 self.db._propagate_id(root_type="residue", 1057 root_id=residue_id, 1058 attribute="molecule_id", 1059 value=molecule_id) 1060 particle_ids_in_residue = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1061 attribute="residue_id", 1062 value=residue_id) 1063 central_bead_id = particle_ids_in_residue[0] 1064 1065 # Bond the central beads of the new and previous residues 1066 self.create_bond(particle_id1=prev_central_bead_id, 1067 particle_id2=central_bead_id, 1068 use_default_bond=use_default_bond) 1069 1070 prev_central_bead_id = central_bead_id 1071 prev_central_bead_name = self.db.get_instance(pmb_type="particle", instance_id=central_bead_id).name 1072 prev_central_bead_pos =central_bead_pos 1073 # Create a Peptide or Molecule instance and register it on the pyMBE database 1074 if pmb_type == "molecule": 1075 inst = MoleculeInstance(molecule_id=molecule_id, 1076 name=name) 1077 elif pmb_type == "peptide": 1078 inst = PeptideInstance(name=name, 1079 molecule_id=molecule_id) 1080 self.db._register_instance(inst) 1081 if gen_angle: 1082 self._generate_angles_for_entity( 1083 entity_id=molecule_id, 1084 entity_id_col='molecule_id') 1085 first_residue = True 1086 pos_index+=1 1087 molecule_ids.append(molecule_id) 1088 return molecule_ids 1089 1090 def create_particle(self, name, box_l, number_of_particles, position=None, fix=False): 1091 """ 1092 Creates one or more particles in an ESPResSo system based on the particle template in the pyMBE database. 1093 1094 Args: 1095 name ('str'): 1096 Label of the particle template in the pyMBE database. 1097 1098 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1099 1100 number_of_particles ('int'): 1101 Number of particles to be created. 1102 1103 position (list of ['float','float','float'], optional): 1104 Initial positions of the particles. If not given, particles are created in random positions. Defaults to None. 1105 1106 fix ('bool', optional): 1107 Controls if the particle motion is frozen in the integrator, it is used to create rigid objects. Defaults to False. 1108 1109 Returns: 1110 ('list' of 'int'): 1111 List with the ids of the particles created into 'espresso_system'. 1112 """ 1113 if number_of_particles <=0: 1114 return [] 1115 if not self.db._has_template(name=name, pmb_type="particle"): 1116 raise ValueError(f"Particle template with name '{name}' is not defined in the pyMBE database.") 1117 1118 part_tpl = self.db.get_template(pmb_type="particle", 1119 name=name) 1120 part_state = self.db.get_template(pmb_type="particle_state", 1121 name=part_tpl.initial_state) 1122 name_state=part_state.name 1123 1124 if fix is False: 1125 fix=[fix]*3 1126 1127 created_pid_list=[] 1128 for index in range(number_of_particles): 1129 if position is None: 1130 particle_position = self.rng.random((1, 3))[0] *np.copy(box_l) 1131 else: 1132 particle_position = np.array(position[index]) 1133 1134 particle_id = self.db._propose_instance_id(pmb_type="particle") 1135 created_pid_list.append(particle_id) 1136 part_inst = ParticleInstance(name=name, 1137 particle_id=particle_id, 1138 initial_state=name_state, 1139 position=particle_position, 1140 fix=fix) 1141 self.db._register_instance(part_inst) 1142 1143 return created_pid_list 1144 1145 def create_protein(self, name, number_of_proteins, box_l, topology_dict): 1146 """ 1147 Creates one or more protein molecules in an ESPResSo system based on the 1148 protein template in the pyMBE database and a provided topology. 1149 1150 Args: 1151 name (str): 1152 Name of the protein template stored in the pyMBE database. 1153 1154 number_of_proteins (int): 1155 Number of protein molecules to generate. 1156 1157 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1158 1159 topology_dict (dict): 1160 Dictionary defining the internal structure of the protein. Expected format: 1161 {"ResidueName1": {"initial_pos": np.ndarray, 1162 "chain_id": int, 1163 "radius": float}, 1164 "ResidueName2": { ... }, 1165 ... 1166 } 1167 The '"initial_pos"' entry is required and represents the residue’s 1168 reference coordinates before shifting to the protein's center-of-mass. 1169 1170 Returns: 1171 ('list' of 'int'): 1172 List of the molecule_id of the Protein instances created into ESPResSo. 1173 1174 Notes: 1175 - Particles are created using 'create_particle()' with 'fix=True', 1176 meaning they are initially immobilized. 1177 - The function assumes all residues in 'topology_dict' correspond to 1178 particle templates already defined in the pyMBE database. 1179 - Bonds between residues are not created here; it assumes a rigid body representation of the protein. 1180 """ 1181 if number_of_proteins <= 0: 1182 return 1183 if not self.db._has_template(name=name, pmb_type="protein"): 1184 raise ValueError(f"Protein template with name '{name}' is not defined in the pyMBE database.") 1185 protein_tpl = self.db.get_template(pmb_type="protein", name=name) 1186 box_half = box_l[0] / 2.0 1187 # Create protein 1188 mol_ids = [] 1189 for _ in range(number_of_proteins): 1190 # create a molecule identifier in pyMBE 1191 molecule_id = self.db._propose_instance_id(pmb_type="protein") 1192 # place protein COM randomly 1193 protein_center = self.generate_coordinates_outside_sphere(radius=1, 1194 max_dist=box_half, 1195 n_samples=1, 1196 center=[box_half]*3)[0] 1197 residues = hf.get_residues_from_topology_dict(topology_dict=topology_dict, 1198 model=protein_tpl.model) 1199 # CREATE RESIDUES + PARTICLES 1200 for _, rdata in residues.items(): 1201 base_resname = rdata["resname"] 1202 residue_name = f"AA-{base_resname}" 1203 # residue instance ID 1204 residue_id = self.db._propose_instance_id("residue") 1205 # register ResidueInstance 1206 self.db._register_instance(ResidueInstance(name=residue_name, 1207 residue_id=residue_id, 1208 molecule_id=molecule_id)) 1209 # PARTICLE CREATION 1210 for bead_id in rdata["beads"]: 1211 bead_type = re.split(r'\d+', bead_id)[0] 1212 relative_pos = topology_dict[bead_id]["initial_pos"] 1213 absolute_pos = relative_pos + protein_center 1214 particle_id = self.create_particle(name=bead_type, 1215 box_l=box_l, 1216 number_of_particles=1, 1217 position=[absolute_pos], 1218 fix=[True,True,True])[0] 1219 # update metadata 1220 self.db._update_instance(instance_id=particle_id, 1221 pmb_type="particle", 1222 attribute="molecule_id", 1223 value=molecule_id) 1224 self.db._update_instance(instance_id=particle_id, 1225 pmb_type="particle", 1226 attribute="residue_id", 1227 value=residue_id) 1228 protein_inst = ProteinInstance(name=name, 1229 molecule_id=molecule_id) 1230 self.db._register_instance(protein_inst) 1231 mol_ids.append(molecule_id) 1232 return mol_ids 1233 1234 def create_residue(self, name, box_l, central_bead_position=None,use_default_bond=False, backbone_vector=None, gen_angle=False): 1235 """ 1236 Creates a residue into ESPResSo. 1237 1238 Args: 1239 name ('str'): 1240 Label of the residue type to be created. 1241 1242 central_bead_position ('list' of 'float'): 1243 Position of the central bead. 1244 1245 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1246 1247 use_default_bond ('bool'): 1248 Switch to control if a bond of type 'default' is used to bond a particle whose bonds types are not defined in the pyMBE database. 1249 1250 backbone_vector ('list' of 'float'): 1251 Backbone vector of the molecule. All side chains are created perpendicularly to 'backbone_vector'. 1252 1253 Returns: 1254 (int): 1255 residue_id of the residue created. 1256 """ 1257 if not self.db._has_template(name=name, pmb_type="residue"): 1258 raise ValueError(f"Residue template with name '{name}' is not defined in the pyMBE database.") 1259 res_tpl = self.db.get_template(pmb_type="residue", 1260 name=name) 1261 # Assign a residue_id 1262 residue_id = self.db._propose_instance_id(pmb_type="residue") 1263 res_inst = ResidueInstance(name=name, 1264 residue_id=residue_id) 1265 self.db._register_instance(res_inst) 1266 # create the principal bead 1267 central_bead_name = res_tpl.central_bead 1268 central_bead_id = self.create_particle(name=central_bead_name, 1269 box_l=box_l, 1270 position=central_bead_position, 1271 number_of_particles = 1)[0] 1272 1273 central_bead_position = self.db.get_instance(pmb_type="particle", 1274 instance_id=central_bead_id).position 1275 # # central_bead_position=espresso_system.part.by_id(central_bead_id).pos 1276 1277 # Assigns residue_id to the central_bead particle created. 1278 self.db._update_instance(pmb_type="particle", 1279 instance_id=central_bead_id, 1280 attribute="residue_id", 1281 value=residue_id) 1282 1283 # create the lateral beads 1284 side_chain_list = res_tpl.side_chains 1285 side_chain_beads_ids = [] 1286 for side_chain_name in side_chain_list: 1287 pmb_type = self._get_template_type(name=side_chain_name, 1288 allowed_types={"particle", "residue"}) 1289 if pmb_type == 'particle': 1290 lj_parameters = self.get_lj_parameters(particle_name1=central_bead_name, 1291 particle_name2=side_chain_name) 1292 bond_tpl = self.get_bond_template(particle_name1=central_bead_name, 1293 particle_name2=side_chain_name, 1294 use_default_bond=use_default_bond) 1295 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1296 bond_type=bond_tpl.bond_type, 1297 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1298 if backbone_vector is None: 1299 bead_position=self.generate_random_points_in_a_sphere(center=central_bead_position, 1300 radius=l0, 1301 n_samples=1, 1302 on_surface=True)[0] 1303 else: 1304 bead_position=central_bead_position+self.generate_trial_perpendicular_vector(vector=np.array(backbone_vector), 1305 magnitude=l0) 1306 1307 side_bead_id = self.create_particle(name=side_chain_name, 1308 box_l=box_l, 1309 position=[bead_position], 1310 number_of_particles=1)[0] 1311 side_chain_beads_ids.append(side_bead_id) 1312 self.db._update_instance(pmb_type="particle", 1313 instance_id=side_bead_id, 1314 attribute="residue_id", 1315 value=residue_id) 1316 self.create_bond(particle_id1=central_bead_id, 1317 particle_id2=side_bead_id, 1318 use_default_bond=use_default_bond) 1319 1320 elif pmb_type == 'residue': 1321 1322 side_residue_tpl = self.db.get_template(name=side_chain_name, 1323 pmb_type=pmb_type) 1324 central_bead_side_chain = side_residue_tpl.central_bead 1325 lj_parameters = self.get_lj_parameters(particle_name1=central_bead_name, 1326 particle_name2=central_bead_side_chain) 1327 bond_tpl = self.get_bond_template(particle_name1=central_bead_name, 1328 particle_name2=central_bead_side_chain, 1329 use_default_bond=use_default_bond) 1330 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1331 bond_type=bond_tpl.bond_type, 1332 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1333 if backbone_vector is None: 1334 residue_position=self.generate_random_points_in_a_sphere(center=central_bead_position, 1335 radius=l0, 1336 n_samples=1, 1337 on_surface=True)[0] 1338 else: 1339 residue_position=central_bead_position+self.generate_trial_perpendicular_vector(vector=backbone_vector, 1340 magnitude=l0) 1341 side_residue_id = self.create_residue(name=side_chain_name, 1342 box_l=box_l, 1343 central_bead_position=[residue_position], 1344 use_default_bond=use_default_bond) 1345 # Find particle ids of the inner residue 1346 side_chain_beads_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1347 attribute="residue_id", 1348 value=side_residue_id) 1349 # Change the residue_id of the residue in the side chain to the one of the outer residue 1350 for particle_id in side_chain_beads_ids: 1351 self.db._update_instance(instance_id=particle_id, 1352 pmb_type="particle", 1353 attribute="residue_id", 1354 value=residue_id) 1355 # Remove the instance of the inner residue 1356 self.db.delete_instance(pmb_type="residue", 1357 instance_id=side_residue_id) 1358 self.create_bond(particle_id1=central_bead_id, 1359 particle_id2=side_chain_beads_ids[0], 1360 use_default_bond=use_default_bond) 1361 if gen_angle: 1362 self._generate_angles_for_entity( 1363 entity_id=residue_id, 1364 entity_id_col="residue_id") 1365 return residue_id 1366 1367 def define_bond(self, bond_type, bond_parameters, particle_pairs): 1368 """ 1369 Defines bond templates for each particle pair in 'particle_pairs' in the pyMBE database. 1370 1371 Args: 1372 bond_type ('str'): 1373 label to identify the potential to model the bond. 1374 1375 bond_parameters ('dict'): 1376 parameters of the potential of the bond. 1377 1378 particle_pairs ('lst'): 1379 list of the 'names' of the 'particles' to be bonded. 1380 1381 Notes: 1382 -Currently, only HARMONIC and FENE bonds are supported. 1383 - For a HARMONIC bond the dictionary must contain the following parameters: 1384 - k ('pint.Quantity') : Magnitude of the bond. It should have units of energy/length**2 1385 using the 'pmb.units' UnitRegistry. 1386 - r_0 ('pint.Quantity') : Equilibrium bond length. It should have units of length using 1387 the 'pmb.units' UnitRegistry. 1388 - For a FENE bond the dictionary must contain the same parameters as for a HARMONIC bond and: 1389 - d_r_max ('pint.Quantity'): Maximal stretching length for FENE. It should have 1390 units of length using the 'pmb.units' UnitRegistry. Default 'None'. 1391 """ 1392 self._check_bond_inputs(bond_parameters=bond_parameters, 1393 bond_type=bond_type) 1394 parameters_expected_dimensions={"r_0": "length", 1395 "k": "energy/length**2", 1396 "d_r_max": "length"} 1397 1398 parameters_tpl = {} 1399 for key in bond_parameters.keys(): 1400 parameters_tpl[key]= PintQuantity.from_quantity(q=bond_parameters[key], 1401 expected_dimension=parameters_expected_dimensions[key], 1402 ureg=self.units) 1403 1404 bond_names=[] 1405 for particle_name1, particle_name2 in particle_pairs: 1406 1407 tpl = BondTemplate(particle_name1=particle_name1, 1408 particle_name2=particle_name2, 1409 parameters=parameters_tpl, 1410 bond_type=bond_type) 1411 tpl._make_name() 1412 if tpl.name in bond_names: 1413 raise RuntimeError(f"Bond {tpl.name} has already been defined, please check the list of particle pairs") 1414 bond_names.append(tpl.name) 1415 self.db._register_template(tpl) 1416 1417 1418 def define_default_bond(self, bond_type, bond_parameters): 1419 """ 1420 Defines a bond template as a "default" template in the pyMBE database. 1421 1422 Args: 1423 bond_type ('str'): 1424 label to identify the potential to model the bond. 1425 1426 bond_parameters ('dict'): 1427 parameters of the potential of the bond. 1428 1429 Notes: 1430 - Currently, only harmonic and FENE bonds are supported. 1431 """ 1432 self._check_bond_inputs(bond_parameters=bond_parameters, 1433 bond_type=bond_type) 1434 parameters_expected_dimensions={"r_0": "length", 1435 "k": "energy/length**2", 1436 "d_r_max": "length"} 1437 parameters_tpl = {} 1438 for key in bond_parameters.keys(): 1439 parameters_tpl[key]= PintQuantity.from_quantity(q=bond_parameters[key], 1440 expected_dimension=parameters_expected_dimensions[key], 1441 ureg=self.units) 1442 tpl = BondTemplate(parameters=parameters_tpl, 1443 bond_type=bond_type) 1444 tpl.name = "default" 1445 self.db._register_template(tpl) 1446 1447 def define_angular_potential(self, angle_type, angle_parameters, particle_triplets): 1448 """ 1449 Defines angle potential templates for each particle triplet in `particle_triplets`. 1450 1451 Args: 1452 angle_type ('str'): 1453 Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine". 1454 1455 angle_parameters ('dict'): 1456 Parameters of the angle potential. Must contain: 1457 - "k" ('pint.Quantity'): Bending stiffness with dimensions of energy. 1458 - "phi_0" ('float'): Equilibrium angle in radians. 1459 1460 particle_triplets ('list[tuple[str,str,str]]'): 1461 List of (side_particle1, central_particle, side_particle2) triplets. 1462 """ 1463 valid_angle_types = ["harmonic", "cosine", "harmonic_cosine"] 1464 if angle_type not in valid_angle_types: 1465 raise NotImplementedError(f"Angle potential type '{angle_type}' currently not implemented in pyMBE, accepted types are {valid_angle_types}") 1466 1467 if "k" not in angle_parameters: 1468 raise ValueError("Magnitude of the angle potential (k) is missing") 1469 if "phi_0" not in angle_parameters: 1470 raise ValueError("Equilibrium angle (phi_0) is missing") 1471 1472 parameters_tpl = {"k": PintQuantity.from_quantity(q=angle_parameters["k"], 1473 expected_dimension="energy", 1474 ureg=self.units), 1475 "phi_0": PintQuantity.from_quantity(q=angle_parameters["phi_0"], 1476 expected_dimension="dimensionless", 1477 ureg=self.units),} 1478 angle_names = [] 1479 for side1, central, side2 in particle_triplets: 1480 tpl = AngleTemplate(side_particle1=side1, 1481 central_particle=central, 1482 side_particle2=side2, 1483 parameters=parameters_tpl, 1484 angle_type=angle_type) 1485 tpl._make_name() 1486 if tpl.name in angle_names: 1487 raise RuntimeError(f"Angle {tpl.name} has already been defined, please check the list of particle triplets") 1488 angle_names.append(tpl.name) 1489 self.db._register_template(tpl) 1490 1491 def define_default_angular_potential(self, angle_type, angle_parameters): 1492 """ 1493 Defines an angle template as a "default" template in the pyMBE database. 1494 1495 Args: 1496 angle_type ('str'): 1497 Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine". 1498 1499 angle_parameters ('dict'): 1500 Parameters of the angle potential (k, phi_0). 1501 """ 1502 valid_angle_types = ["harmonic", "cosine", "harmonic_cosine"] 1503 if angle_type not in valid_angle_types: 1504 raise NotImplementedError(f"Angle potential type '{angle_type}' currently not implemented in pyMBE, accepted types are {valid_angle_types}") 1505 if "k" not in angle_parameters: 1506 raise ValueError("Magnitude of the angle potential (k) is missing") 1507 if "phi_0" not in angle_parameters: 1508 raise ValueError("Equilibrium angle (phi_0) is missing") 1509 parameters_tpl = {"k": PintQuantity.from_quantity(q=angle_parameters["k"], 1510 expected_dimension="energy", 1511 ureg=self.units), 1512 "phi_0": PintQuantity.from_quantity(q=angle_parameters["phi_0"], 1513 expected_dimension="dimensionless", 1514 ureg=self.units),} 1515 tpl = AngleTemplate(parameters=parameters_tpl, 1516 angle_type=angle_type) 1517 tpl.name = "default" 1518 self.db._register_template(tpl) 1519 1520 def create_angular_potential(self, particle_id1, particle_id2, particle_id3, use_default_angle=False): 1521 """ 1522 Creates an angle between three particle instances in an ESPResSo system 1523 and registers it in the pyMBE database. 1524 1525 Args: 1526 particle_id1 ('int'): ID of the first side particle. 1527 particle_id2 ('int'): ID of the central particle. 1528 particle_id3 ('int'): ID of the second side particle. 1529 use_default_angle ('bool', optional): If True, use the default angle if no specific one is found. 1530 """ 1531 particle_inst_1 = self.db.get_instance(pmb_type="particle", instance_id=particle_id1) 1532 particle_inst_2 = self.db.get_instance(pmb_type="particle", instance_id=particle_id2) 1533 particle_inst_3 = self.db.get_instance(pmb_type="particle", instance_id=particle_id3) 1534 1535 # Verify that bonds exist between side particles and central particle 1536 bond_instances = self.db.get_instances(pmb_type="bond") 1537 bonded_pairs = set() 1538 for bond in bond_instances.values(): 1539 pair = frozenset([bond.particle_id1, bond.particle_id2]) 1540 bonded_pairs.add(pair) 1541 if frozenset([particle_id1, particle_id2]) not in bonded_pairs: 1542 raise ValueError(f"Cannot create angle: no bond exists between particle {particle_id1} and central particle {particle_id2}.") 1543 if frozenset([particle_id3, particle_id2]) not in bonded_pairs: 1544 raise ValueError(f"Cannot create angle: no bond exists between particle {particle_id3} and central particle {particle_id2}.") 1545 1546 angle_tpl = self.get_angle_template(side_name1=particle_inst_1.name, 1547 central_name=particle_inst_2.name, 1548 side_name2=particle_inst_3.name, 1549 use_default_angle=use_default_angle) 1550 angle_id = self.db._propose_instance_id(pmb_type="angle") 1551 pmb_angle_instance = AngleInstance(angle_id=angle_id, 1552 name=angle_tpl.name, 1553 particle_id1=particle_id1, 1554 particle_id2=particle_id2, 1555 particle_id3=particle_id3) 1556 self.db._register_instance(instance=pmb_angle_instance) 1557 1558 def get_angle_template(self, side_name1, central_name, side_name2, use_default_angle=False): 1559 """ 1560 Retrieves an angle template connecting three particle templates. 1561 1562 Args: 1563 side_name1 ('str'): Name of the first side particle. 1564 central_name ('str'): Name of the central particle. 1565 side_name2 ('str'): Name of the second side particle. 1566 use_default_angle ('bool', optional): If True, fall back to the default angle template. 1567 1568 Returns: 1569 ('AngleTemplate'): The matching angle template. 1570 """ 1571 angle_key = AngleTemplate.make_angle_key(side1=side_name1, central=central_name, side2=side_name2) 1572 try: 1573 return self.db.get_template(name=angle_key, pmb_type="angle") 1574 except ValueError: 1575 pass 1576 1577 if use_default_angle: 1578 return self.db.get_template(name="default", pmb_type="angle") 1579 1580 raise ValueError(f"No angle template found for '{side_name1}-{central_name}-{side_name2}', and default angles are deactivated.") 1581 1582 def _generate_angles_for_entity(self, entity_id, entity_id_col): 1583 """ 1584 Auto-generates angles from bond topology for an entity (molecule or residue). 1585 1586 For each particle in the entity that has two or more bonded neighbors, 1587 this method finds all neighbor pairs and applies any matching angle potential. 1588 1589 Args: 1590 entity_id ('int'): The molecule_id or residue_id to generate angles for. 1591 entity_id_col ('str'): Either "molecule_id" or "residue_id". 1592 """ 1593 # Get all particle IDs for this entity 1594 particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1595 attribute=entity_id_col, 1596 value=entity_id) 1597 if not particle_ids: 1598 return 1599 1600 # Build neighbor map from bond instances 1601 neighbors = {pid: set() for pid in particle_ids} 1602 pid_set = set(particle_ids) 1603 bond_instances = self.db.get_instances(pmb_type="bond") 1604 for bond in bond_instances.values(): 1605 i, j = bond.particle_id1, bond.particle_id2 1606 if i in pid_set and j in pid_set: 1607 neighbors[i].add(j) 1608 neighbors[j].add(i) 1609 1610 # For each particle with 2+ neighbors, generate angles 1611 for j in particle_ids: 1612 nbs = sorted(neighbors[j]) 1613 if len(nbs) < 2: 1614 continue 1615 1616 for idx_i in range(len(nbs)): 1617 for idx_k in range(idx_i + 1, len(nbs)): 1618 i = nbs[idx_i] 1619 k = nbs[idx_k] 1620 try: 1621 self.create_angular_potential(particle_id1=i, 1622 particle_id2=j, 1623 particle_id3=k, 1624 use_default_angle=True) 1625 except ValueError: 1626 # No angle template defined for this triplet — skip 1627 continue 1628 1629 def define_hydrogel(self, name, node_map, chain_map): 1630 """ 1631 Defines a hydrogel template in the pyMBE database. 1632 1633 Args: 1634 name ('str'): 1635 Unique label that identifies the 'hydrogel'. 1636 1637 node_map ('list of dict'): 1638 [{"particle_name": , "lattice_index": }, ... ] 1639 1640 chain_map ('list of dict'): 1641 [{"node_start": , "node_end": , "residue_list": , ... ] 1642 """ 1643 # Sanity tests 1644 node_indices = {tuple(entry['lattice_index']) for entry in node_map} 1645 chain_map_connectivity = set() 1646 for entry in chain_map: 1647 start = self.lattice_builder.node_labels[entry['node_start']] 1648 end = self.lattice_builder.node_labels[entry['node_end']] 1649 chain_map_connectivity.add((start,end)) 1650 if self.lattice_builder.lattice.connectivity != chain_map_connectivity: 1651 raise ValueError("Incomplete hydrogel: A diamond lattice must contain correct 16 lattice index pairs") 1652 diamond_indices = {tuple(row) for row in self.lattice_builder.lattice.indices} 1653 if node_indices != diamond_indices: 1654 raise ValueError(f"Incomplete hydrogel: A diamond lattice must contain exactly 8 lattice indices, {diamond_indices} ") 1655 # Register information in the pyMBE database 1656 nodes=[] 1657 for entry in node_map: 1658 nodes.append(HydrogelNode(particle_name=entry["particle_name"], 1659 lattice_index=entry["lattice_index"])) 1660 chains=[] 1661 for chain in chain_map: 1662 chains.append(HydrogelChain(node_start=chain["node_start"], 1663 node_end=chain["node_end"], 1664 molecule_name=chain["molecule_name"])) 1665 tpl = HydrogelTemplate(name=name, 1666 node_map=nodes, 1667 chain_map=chains) 1668 self.db._register_template(tpl) 1669 1670 def define_molecule(self, name, residue_list): 1671 """ 1672 Defines a molecule template in the pyMBE database. 1673 1674 Args: 1675 name('str'): 1676 Unique label that identifies the 'molecule'. 1677 1678 residue_list ('list' of 'str'): 1679 List of the 'name's of the 'residue's in the sequence of the 'molecule'. 1680 """ 1681 tpl = MoleculeTemplate(name=name, 1682 residue_list=residue_list) 1683 self.db._register_template(tpl) 1684 1685 def define_monoprototic_acidbase_reaction(self, particle_name, pka, acidity, metadata=None): 1686 """ 1687 Defines an acid-base reaction for a monoprototic particle in the pyMBE database. 1688 1689 Args: 1690 particle_name ('str'): 1691 Unique label that identifies the particle template. 1692 1693 pka ('float'): 1694 pka-value of the acid or base. 1695 1696 acidity ('str'): 1697 Identifies whether if the particle is 'acidic' or 'basic'. 1698 1699 metadata ('dict', optional): 1700 Additional information to be stored in the reaction. Defaults to None. 1701 """ 1702 supported_acidities = ["acidic", "basic"] 1703 if acidity not in supported_acidities: 1704 raise ValueError(f"Unsupported acidity '{acidity}' for particle '{particle_name}'. Supported acidities are {supported_acidities}.") 1705 reaction_type = "monoprotic" 1706 if acidity == "basic": 1707 reaction_type += "_base" 1708 else: 1709 reaction_type += "_acid" 1710 reaction = Reaction(participants=[ReactionParticipant(particle_name=particle_name, 1711 state_name=f"{particle_name}H", 1712 coefficient=-1), 1713 ReactionParticipant(particle_name=particle_name, 1714 state_name=f"{particle_name}", 1715 coefficient=1)], 1716 reaction_type=reaction_type, 1717 pK=pka, 1718 metadata=metadata) 1719 self.db._register_reaction(reaction) 1720 1721 def define_monoprototic_particle_states(self, particle_name, acidity): 1722 """ 1723 Defines particle states for a monoprotonic particle template including the charges in each of its possible states. 1724 1725 Args: 1726 particle_name ('str'): 1727 Unique label that identifies the particle template. 1728 1729 acidity ('str'): 1730 Identifies whether the particle is 'acidic' or 'basic'. 1731 """ 1732 acidity_valid_keys = ['acidic', 'basic'] 1733 if not pd.isna(acidity): 1734 if acidity not in acidity_valid_keys: 1735 raise ValueError(f"Acidity {acidity} provided for particle name {particle_name} is not supported. Valid keys are: {acidity_valid_keys}") 1736 if acidity == "acidic": 1737 states = [{"name": f"{particle_name}H", "z": 0}, 1738 {"name": f"{particle_name}", "z": -1}] 1739 1740 elif acidity == "basic": 1741 states = [{"name": f"{particle_name}H", "z": 1}, 1742 {"name": f"{particle_name}", "z": 0}] 1743 self.define_particle_states(particle_name=particle_name, 1744 states=states) 1745 1746 def define_particle(self, name, sigma, epsilon, z=0, acidity=pd.NA, pka=pd.NA, cutoff=pd.NA, offset=pd.NA): 1747 """ 1748 Defines a particle template in the pyMBE database. 1749 1750 Args: 1751 name('str'): 1752 Unique label that identifies this particle type. 1753 1754 sigma('pint.Quantity'): 1755 Sigma parameter used to set up Lennard-Jones interactions for this particle type. 1756 1757 epsilon('pint.Quantity'): 1758 Epsilon parameter used to setup Lennard-Jones interactions for this particle tipe. 1759 1760 z('int', optional): 1761 Permanent charge number of this particle type. Defaults to 0. 1762 1763 acidity('str', optional): 1764 Identifies whether if the particle is 'acidic' or 'basic', used to setup constant pH simulations. Defaults to pd.NA. 1765 1766 pka('float', optional): 1767 If 'particle' is an acid or a base, it defines its pka-value. Defaults to pd.NA. 1768 1769 cutoff('pint.Quantity', optional): 1770 Cutoff parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA. 1771 1772 offset('pint.Quantity', optional): 1773 Offset parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA. 1774 1775 Notes: 1776 - 'sigma', 'cutoff' and 'offset' must have a dimensitonality of '[length]' and should be defined using pmb.units. 1777 - 'epsilon' must have a dimensitonality of '[energy]' and should be defined using pmb.units. 1778 - 'cutoff' defaults to '2**(1./6.) reduced_length'. 1779 - 'offset' defaults to 0. 1780 - For more information on 'sigma', 'epsilon', 'cutoff' and 'offset' check 'pmb.setup_lj_interactions()'. 1781 """ 1782 # If 'cutoff' and 'offset' are not defined, default them to the following values 1783 if pd.isna(cutoff): 1784 cutoff=self.units.Quantity(2**(1./6.), "reduced_length") 1785 if pd.isna(offset): 1786 offset=self.units.Quantity(0, "reduced_length") 1787 # Define particle states 1788 if acidity is pd.NA: 1789 states = [{"name": f"{name}", "z": z}] 1790 self.define_particle_states(particle_name=name, 1791 states=states) 1792 initial_state = name 1793 else: 1794 self.define_monoprototic_particle_states(particle_name=name, 1795 acidity=acidity) 1796 initial_state = f"{name}H" 1797 if pka is not pd.NA: 1798 self.define_monoprototic_acidbase_reaction(particle_name=name, 1799 acidity=acidity, 1800 pka=pka) 1801 tpl = ParticleTemplate(name=name, 1802 sigma=PintQuantity.from_quantity(q=sigma, expected_dimension="length", ureg=self.units), 1803 epsilon=PintQuantity.from_quantity(q=epsilon, expected_dimension="energy", ureg=self.units), 1804 cutoff=PintQuantity.from_quantity(q=cutoff, expected_dimension="length", ureg=self.units), 1805 offset=PintQuantity.from_quantity(q=offset, expected_dimension="length", ureg=self.units), 1806 initial_state=initial_state) 1807 self.db._register_template(tpl) 1808 1809 def define_particle_states(self, particle_name, states): 1810 """ 1811 Define the chemical states of an existing particle template. 1812 1813 Args: 1814 particle_name ('str'): 1815 Name of a particle template. 1816 1817 states ('list' of 'dict'): 1818 List of dictionaries defining the particle states. Each dictionary 1819 must contain: 1820 - 'name' ('str'): Name of the particle state (e.g. '"H"', '"-"', 1821 '"neutral"'). 1822 - 'z' ('int'): Charge number of the particle in this state. 1823 Example: 1824 states = [{"name": "AH", "z": 0}, # protonated 1825 {"name": "A-", "z": -1}] # deprotonated 1826 Notes: 1827 - Each state is assigned a unique Espresso 'es_type' automatically. 1828 - Chemical reactions (e.g. acid–base equilibria) are **not** created by 1829 this method and must be defined separately (e.g. via 1830 'set_particle_acidity()' or custom reaction definitions). 1831 - Particles without explicitly defined states are assumed to have a 1832 single, implicit state with their default charge. 1833 """ 1834 for s in states: 1835 state = ParticleStateTemplate(particle_name=particle_name, 1836 name=s["name"], 1837 z=s["z"], 1838 es_type=self.propose_unused_type()) 1839 self.db._register_template(state) 1840 1841 def define_peptide(self, name, sequence, model): 1842 """ 1843 Defines a peptide template in the pyMBE database. 1844 1845 Args: 1846 name ('str'): 1847 Unique label that identifies the peptide. 1848 1849 sequence ('str'): 1850 Sequence of the peptide. 1851 1852 model ('str'): 1853 Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported. 1854 """ 1855 valid_keys = ['1beadAA','2beadAA'] 1856 if model not in valid_keys: 1857 raise ValueError('Invalid label for the peptide model, please choose between 1beadAA or 2beadAA') 1858 clean_sequence = hf.protein_sequence_parser(sequence=sequence) 1859 residue_list = self._get_residue_list_from_sequence(sequence=clean_sequence) 1860 tpl = PeptideTemplate(name=name, 1861 residue_list=residue_list, 1862 model=model, 1863 sequence=sequence) 1864 self.db._register_template(tpl) 1865 1866 def define_protein(self, name, sequence, model): 1867 """ 1868 Defines a protein template in the pyMBE database. 1869 1870 Args: 1871 name ('str'): 1872 Unique label that identifies the protein. 1873 1874 sequence ('str'): 1875 Sequence of the protein. 1876 1877 model ('string'): 1878 Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported. 1879 1880 Notes: 1881 - Currently, only 'lj_setup_mode="wca"' is supported. This corresponds to setting up the WCA potential. 1882 """ 1883 valid_model_keys = ['1beadAA','2beadAA'] 1884 if model not in valid_model_keys: 1885 raise ValueError('Invalid key for the protein model, supported models are {valid_model_keys}') 1886 1887 residue_list = self._get_residue_list_from_sequence(sequence=sequence) 1888 tpl = ProteinTemplate(name=name, 1889 model=model, 1890 residue_list=residue_list, 1891 sequence=sequence) 1892 self.db._register_template(tpl) 1893 1894 def define_residue(self, name, central_bead, side_chains): 1895 """ 1896 Defines a residue template in the pyMBE database. 1897 1898 Args: 1899 name ('str'): 1900 Unique label that identifies the residue. 1901 1902 central_bead ('str'): 1903 'name' of the 'particle' to be placed as central_bead of the residue. 1904 1905 side_chains('list' of 'str'): 1906 List of 'name's of the pmb_objects to be placed as side_chains of the residue. Currently, only pyMBE objects of type 'particle' or 'residue' are supported. 1907 """ 1908 tpl = ResidueTemplate(name=name, 1909 central_bead=central_bead, 1910 side_chains=side_chains) 1911 self.db._register_template(tpl) 1912 1913 1914 def delete_instances_in_system(self, instance_id, pmb_type): 1915 """ 1916 Deletes the instance with instance_id from the ESPResSo system. 1917 Related assembly, molecule, residue, particles and bond instances will also be deleted from the pyMBE dataframe. 1918 1919 Args: 1920 instance_id ('int'): 1921 id of the assembly to be deleted. 1922 1923 pmb_type ('str'): 1924 the instance type to be deleted. 1925 1926 espresso_system ('espressomd.system.System'): 1927 Instance of a system class from espressomd library. 1928 """ 1929 if pmb_type == "particle": 1930 instance_identifier = "particle_id" 1931 elif pmb_type == "residue": 1932 instance_identifier = "residue_id" 1933 elif pmb_type in self.db._molecule_like_types: 1934 instance_identifier = "molecule_id" 1935 elif pmb_type in self.db._assembly_like_types: 1936 instance_identifier = "assembly_id" 1937 particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1938 attribute=instance_identifier, 1939 value=instance_id) 1940 self._delete_particles_from_engine(particle_ids=particle_ids) 1941 self.db.delete_instance(pmb_type=pmb_type, 1942 instance_id=instance_id) 1943 1944 def determine_reservoir_concentrations(self, pH_res, c_salt_res, activity_coefficient_monovalent_pair, max_number_sc_runs=200): 1945 """ 1946 Determines ionic concentrations in the reservoir at fixed pH and salt concentration. 1947 1948 Args: 1949 pH_res ('float'): 1950 Target pH value in the reservoir. 1951 1952 c_salt_res ('pint.Quantity'): 1953 Concentration of monovalent salt (e.g., NaCl) in the reservoir. 1954 1955 activity_coefficient_monovalent_pair ('callable'): 1956 Function returning the activity coefficient of a monovalent ion pair 1957 as a function of ionic strength: 1958 'gamma = activity_coefficient_monovalent_pair(I)'. 1959 1960 max_number_sc_runs ('int', optional): 1961 Maximum number of self-consistent iterations allowed before 1962 convergence is enforced. Defaults to 200. 1963 1964 Returns: 1965 tuple: 1966 (cH_res, cOH_res, cNa_res, cCl_res) 1967 - cH_res ('pint.Quantity'): Concentration of H⁺ ions. 1968 - cOH_res ('pint.Quantity'): Concentration of OH⁻ ions. 1969 - cNa_res ('pint.Quantity'): Concentration of Na⁺ ions. 1970 - cCl_res ('pint.Quantity'): Concentration of Cl⁻ ions. 1971 1972 Notess: 1973 - The algorithm enforces electroneutrality in the reservoir. 1974 - Water autodissociation is included via the equilibrium constant 'Kw'. 1975 - Non-ideal effects enter through activity coefficients depending on 1976 ionic strength. 1977 - The implementation follows the self-consistent scheme described in 1978 Landsgesell (PhD thesis, Sec. 5.3, doi:10.18419/opus-10831), adapted 1979 from the original code (doi:10.18419/darus-2237). 1980 """ 1981 cH_res, cOH_res, cNa_res, cCl_res = self.simulation_engine.determine_reservoir_concentrations( pH_res, c_salt_res, activity_coefficient_monovalent_pair, max_number_sc_runs) 1982 return cH_res, cOH_res, cNa_res, cCl_res 1983 1984 def enable_motion_of_rigid_object(self, instance_id, pmb_type): 1985 """ 1986 Enables translational and rotational motion of a rigid pyMBE object instance 1987 in an ESPResSo system.This method creates a rigid-body center particle at the center of mass of 1988 the specified pyMBE object and attaches all constituent particles to it 1989 using ESPResSo virtual sites. The resulting rigid object can translate and 1990 rotate as a single body. 1991 1992 Args: 1993 instance_id ('int'): 1994 Instance ID of the pyMBE object whose rigid-body motion is enabled. 1995 1996 pmb_type ('str'): 1997 pyMBE object type of the instance (e.g. '"molecule"', '"peptide"', 1998 '"protein"', or any assembly-like type). 1999 2000 Notess: 2001 - This method requires ESPResSo to be compiled with the following 2002 features enabled: 2003 - '"VIRTUAL_SITES_RELATIVE"' 2004 - '"MASS"' 2005 - A new ESPResSo particle is created to represent the rigid-body center. 2006 - The mass of the rigid-body center is set to the number of particles 2007 belonging to the object. 2008 - The rotational inertia tensor is approximated from the squared 2009 distances of the particles to the center of mass. 2010 """ 2011 self.simulation_engine.enable_motion_of_rigid_object(instance_id, pmb_type) 2012 2013 def generate_coordinates_outside_sphere(self, center, radius, max_dist, n_samples): 2014 """ 2015 Generates random coordinates outside a sphere and inside a larger bounding sphere. 2016 2017 Args: 2018 center ('array-like'): 2019 Coordinates of the center of the spheres. 2020 2021 radius ('float'): 2022 Radius of the inner exclusion sphere. Must be positive. 2023 2024 max_dist ('float'): 2025 Radius of the outer sampling sphere. Must be larger than 'radius'. 2026 2027 n_samples ('int'): 2028 Number of coordinates to generate. 2029 2030 Returns: 2031 'list' of 'numpy.ndarray': 2032 List of coordinates lying outside the inner sphere and inside the 2033 outer sphere. 2034 2035 Notess: 2036 - Points are uniformly sampled inside a sphere of radius 'max_dist' centered at 'center' 2037 and only those with a distance greater than or equal to 'radius' from the center are retained. 2038 """ 2039 if not radius > 0: 2040 raise ValueError (f'The value of {radius} must be a positive value') 2041 if not radius < max_dist: 2042 raise ValueError(f'The min_dist ({radius} must be lower than the max_dist ({max_dist}))') 2043 coord_list = [] 2044 counter = 0 2045 while counter<n_samples: 2046 coord = self.generate_random_points_in_a_sphere(center=center, 2047 radius=max_dist, 2048 n_samples=1)[0] 2049 if np.linalg.norm(coord-np.asarray(center))>=radius: 2050 coord_list.append (coord) 2051 counter += 1 2052 return coord_list 2053 2054 def generate_random_points_in_a_sphere(self, center, radius, n_samples, on_surface=False): 2055 """ 2056 Generates uniformly distributed random points inside or on the surface of a sphere. 2057 2058 Args: 2059 center ('array-like'): 2060 Coordinates of the center of the sphere. 2061 2062 radius ('float'): 2063 Radius of the sphere. 2064 2065 n_samples ('int'): 2066 Number of sample points to generate. 2067 2068 on_surface ('bool', optional): 2069 If True, points are uniformly sampled on the surface of the sphere. 2070 If False, points are uniformly sampled within the sphere volume. 2071 Defaults to False. 2072 2073 Returns: 2074 'numpy.ndarray': 2075 Array of shape '(n_samples, d)' containing the generated coordinates, 2076 where 'd' is the dimensionality of 'center'. 2077 Notes: 2078 - Points are sampled in a space whose dimensionality is inferred 2079 from the length of 'center'. 2080 """ 2081 # initial values 2082 center=np.array(center) 2083 d = center.shape[0] 2084 # sample n_samples points in d dimensions from a standard normal distribution 2085 samples = self.rng.normal(size=(n_samples, d)) 2086 # make the samples lie on the surface of the unit hypersphere 2087 normalize_radii = np.linalg.norm(samples, axis=1)[:, np.newaxis] 2088 samples /= normalize_radii 2089 if not on_surface: 2090 # make the samples lie inside the hypersphere with the correct density 2091 uniform_points = self.rng.uniform(size=n_samples)[:, np.newaxis] 2092 new_radii = np.power(uniform_points, 1/d) 2093 samples *= new_radii 2094 # scale the points to have the correct radius and center 2095 samples = samples * radius + center 2096 return samples 2097 2098 def generate_trial_perpendicular_vector(self,vector,magnitude): 2099 """ 2100 Generates a random vector perpendicular to a given vector. 2101 2102 Args: 2103 vector ('array-like'): 2104 Reference vector to which the generated vector will be perpendicular. 2105 2106 magnitude ('float'): 2107 Desired magnitude of the perpendicular vector. 2108 2109 Returns: 2110 'numpy.ndarray': 2111 Vector orthogonal to 'vector' with norm equal to 'magnitude'. 2112 """ 2113 np_vec = np.array(vector) 2114 if np.all(np_vec == 0): 2115 raise ValueError('Zero vector') 2116 np_vec /= np.linalg.norm(np_vec) 2117 # Generate a random vector 2118 random_vector = self.generate_random_points_in_a_sphere(radius=1, 2119 center=[0,0,0], 2120 n_samples=1, 2121 on_surface=True)[0] 2122 # Project the random vector onto the input vector and subtract the projection 2123 projection = np.dot(random_vector, np_vec) * np_vec 2124 perpendicular_vector = random_vector - projection 2125 # Normalize the perpendicular vector to have the same magnitude as the input vector 2126 perpendicular_vector /= np.linalg.norm(perpendicular_vector) 2127 return perpendicular_vector*magnitude 2128 2129 def get_bond_template(self, particle_name1, particle_name2, use_default_bond=False) : 2130 """ 2131 Retrieves a bond template connecting two particle templates. 2132 2133 Args: 2134 particle_name1 ('str'): 2135 Name of the first particle template. 2136 2137 particle_name2 ('str'): 2138 Name of the second particle template. 2139 2140 use_default_bond ('bool', optional): 2141 If True, returns the default bond template when no specific bond 2142 template is found. Defaults to False. 2143 2144 Returns: 2145 'BondTemplate': 2146 Bond template object retrieved from the pyMBE database. 2147 2148 Notes: 2149 - This method searches the pyMBE database for a bond template defined between particle templates with names 'particle_name1' and 'particle_name2'. 2150 - If no specific bond template is found and 'use_default_bond' is enabled, a default bond template is returned instead. 2151 """ 2152 # Try to find a specific bond template 2153 bond_key = BondTemplate.make_bond_key(pn1=particle_name1, 2154 pn2=particle_name2) 2155 try: 2156 return self.db.get_template(name=bond_key, 2157 pmb_type="bond") 2158 except ValueError: 2159 pass 2160 2161 # Fallback to default bond if allowed 2162 if use_default_bond: 2163 return self.db.get_template(name="default", 2164 pmb_type="bond") 2165 2166 # No bond template found 2167 raise ValueError(f"No bond template found between '{particle_name1}' and '{particle_name2}', and default bonds are deactivated.") 2168 2169 def get_charge_number_map(self): 2170 """ 2171 Construct a mapping from ESPResSo particle types to their charge numbers. 2172 2173 Returns: 2174 'dict[int, float]': 2175 Dictionary mapping ESPResSo particle types to charge numbers, 2176 ''{es_type: z}''. 2177 2178 Notess: 2179 - The mapping is built from particle *states*, not instances. 2180 - If multiple templates define states with the same ''es_type'', 2181 the last encountered definition will overwrite previous ones. 2182 This behavior is intentional and assumes database consistency. 2183 - Neutral particles (''z = 0'') are included in the map. 2184 """ 2185 charge_number_map = {} 2186 particle_templates = self.db.get_templates("particle") 2187 for tpl in particle_templates.values(): 2188 for state in self.db.get_particle_states_templates(particle_name=tpl.name).values(): 2189 charge_number_map[state.es_type] = state.z 2190 return charge_number_map 2191 2192 def get_instances_df(self, pmb_type): 2193 """ 2194 Returns a dataframe with all instances of type 'pmb_type' in the pyMBE database. 2195 2196 Args: 2197 pmb_type ('str'): 2198 pmb type to search instances in the pyMBE database. 2199 2200 Returns: 2201 ('Pandas.Dataframe'): 2202 Dataframe with all instances of type 'pmb_type'. 2203 """ 2204 return self.db._get_instances_df(pmb_type=pmb_type) 2205 2206 def get_lj_parameters(self, particle_name1, particle_name2, combining_rule='Lorentz-Berthelot'): 2207 """ 2208 Returns the Lennard-Jones parameters for the interaction between the particle types given by 2209 'particle_name1' and 'particle_name2' in the pyMBE database, calculated according to the provided combining rule. 2210 2211 Args: 2212 particle_name1 ('str'): 2213 label of the type of the first particle type 2214 2215 particle_name2 ('str'): 2216 label of the type of the second particle type 2217 2218 combining_rule ('string', optional): 2219 combining rule used to calculate 'sigma' and 'epsilon' for the potential betwen a pair of particles. Defaults to 'Lorentz-Berthelot'. 2220 2221 Returns: 2222 ('dict'): 2223 {"epsilon": epsilon_value, "sigma": sigma_value, "offset": offset_value, "cutoff": cutoff_value} 2224 2225 Notes: 2226 - Currently, the only 'combining_rule' supported is Lorentz-Berthelot. 2227 - If the sigma value of 'particle_name1' or 'particle_name2' is 0, the function will return an empty dictionary. No LJ interactions are set up for particles with sigma = 0. 2228 """ 2229 lj_parameters=self.db.get_lj_parameters(particle_name1=particle_name1,particle_name2=particle_name2,combining_rule=combining_rule) 2230 return lj_parameters 2231 2232 def get_particle_id_map(self, object_name): 2233 """ 2234 Collect all particle IDs associated with an object of given name in the 2235 pyMBE database. 2236 2237 Args: 2238 object_name ('str'): 2239 Name of the object. 2240 2241 Returns: 2242 ('dict'): 2243 {"all": [particle_ids], 2244 "residue_map": {residue_id: [particle_ids]}, 2245 "molecule_map": {molecule_id: [particle_ids]}, 2246 "assembly_map": {assembly_id: [particle_ids]},} 2247 2248 Notess: 2249 - Works for all supported pyMBE templates. 2250 - Relies in the internal method Manager.get_particle_id_map, see method for the detailed code. 2251 """ 2252 return self.db.get_particle_id_map(object_name=object_name) 2253 2254 def get_pka_set(self): 2255 """ 2256 Retrieve the pKa set for all titratable particles in the pyMBE database. 2257 2258 Returns: 2259 ('dict'): 2260 Dictionary of the form: 2261 {"particle_name": {"pka_value": float, 2262 "acidity": "acidic" | "basic"}} 2263 Notes: 2264 - If a particle participates in multiple acid/base reactions, an error is raised. 2265 """ 2266 pka_set = {} 2267 supported_reactions = ["monoprotic_acid", 2268 "monoprotic_base"] 2269 for reaction in self.db._reactions.values(): 2270 if reaction.reaction_type not in supported_reactions: 2271 continue 2272 # Identify involved particle(s) 2273 particle_names = {participant.particle_name for participant in reaction.participants} 2274 particle_name = particle_names.pop() 2275 if particle_name in pka_set: 2276 raise ValueError(f"Multiple acid/base reactions found for particle '{particle_name}'.") 2277 pka_set[particle_name] = {"pka_value": reaction.pK} 2278 if reaction.reaction_type == "monoprotic_acid": 2279 acidity = "acidic" 2280 elif reaction.reaction_type == "monoprotic_base": 2281 acidity = "basic" 2282 pka_set[particle_name]["acidity"] = acidity 2283 return pka_set 2284 2285 def get_radius_map(self, dimensionless=True): 2286 """ 2287 Gets the effective radius of each particle defined in the pyMBE database. 2288 2289 Args: 2290 dimensionless ('bool'): 2291 If ``True``, return magnitudes expressed in ``reduced_length``. 2292 If ``False``, return Pint quantities with units. 2293 2294 Returns: 2295 ('dict'): 2296 {espresso_type: radius}. 2297 2298 Notes: 2299 - The radius corresponds to (sigma+offset)/2 2300 """ 2301 return self.db.get_radius_map(dimensionless) 2302 2303 def get_reactions_df(self): 2304 """ 2305 Returns a dataframe with all reaction templates in the pyMBE database. 2306 2307 Returns: 2308 (Pandas.Dataframe): 2309 Dataframe with all reaction templates. 2310 """ 2311 return self.db._get_reactions_df() 2312 2313 def get_reduced_units(self): 2314 """ 2315 Returns the current set of reduced units defined in pyMBE. 2316 2317 Returns: 2318 reduced_units_text ('str'): 2319 text with information about the current set of reduced units. 2320 2321 """ 2322 unit_length=self.units.Quantity(1,'reduced_length') 2323 unit_energy=self.units.Quantity(1,'reduced_energy') 2324 unit_charge=self.units.Quantity(1,'reduced_charge') 2325 reduced_units_text = "\n".join(["Current set of reduced units:", 2326 f"{unit_length.to('nm'):.5g} = {unit_length}", 2327 f"{unit_energy.to('J'):.5g} = {unit_energy}", 2328 f"{unit_charge.to('C'):.5g} = {unit_charge}", 2329 f"Temperature: {(self.kT/self.kB).to('K'):.5g}"]) 2330 return reduced_units_text 2331 2332 def get_templates_df(self, pmb_type): 2333 """ 2334 Returns a dataframe with all templates of type 'pmb_type' in the pyMBE database. 2335 2336 Args: 2337 pmb_type ('str'): 2338 pmb type to search templates in the pyMBE database. 2339 2340 Returns: 2341 ('Pandas.Dataframe'): 2342 Dataframe with all templates of type given by 'pmb_type'. 2343 """ 2344 return self.db._get_templates_df(pmb_type=pmb_type) 2345 2346 def get_type_map(self): 2347 """ 2348 Return the mapping of ESPResSo types for all particle states defined in the pyMBE database. 2349 2350 Returns: 2351 'dict[str, int]': 2352 A dictionary mapping each particle state to its corresponding ESPResSo type: 2353 {state_name: es_type, ...} 2354 """ 2355 2356 return self.db.get_es_types_map() 2357 2358 def initialize_lattice_builder(self, diamond_lattice): 2359 """ 2360 Initialize the lattice builder with the DiamondLattice object. 2361 2362 Args: 2363 diamond_lattice ('DiamondLattice'): 2364 DiamondLattice object from the 'lib/lattice' module to be used in the LatticeBuilder. 2365 """ 2366 from .lib.lattice import LatticeBuilder, DiamondLattice 2367 if not isinstance(diamond_lattice, DiamondLattice): 2368 raise TypeError("Currently only DiamondLattice objects are supported.") 2369 self.lattice_builder = LatticeBuilder(lattice=diamond_lattice) 2370 logging.info(f"LatticeBuilder initialized with mpc={diamond_lattice.mpc} and box_l={diamond_lattice.box_l}") 2371 return self.lattice_builder 2372 2373 def load_database(self, folder, format='csv'): 2374 """ 2375 Loads a pyMBE database stored in 'folder'. 2376 2377 Args: 2378 folder ('str' or 'Path'): 2379 Path to the folder where the pyMBE database was stored. 2380 2381 format ('str', optional): 2382 Format of the database to be loaded. Defaults to 'csv'. 2383 2384 Return: 2385 ('dict'): 2386 metadata with additional information about the source of the information in the database. 2387 2388 Notes: 2389 - The folder must contain the files generated by 'pmb.save_database()'. 2390 - Currently, only 'csv' format is supported. 2391 """ 2392 supported_formats = ['csv'] 2393 if format not in supported_formats: 2394 raise ValueError(f"Format {format} not supported. Supported formats are {supported_formats}") 2395 if format == 'csv': 2396 metadata =io._load_database_csv(self.db, 2397 folder=folder) 2398 return metadata 2399 2400 def load_pka_set(self, filename): 2401 """ 2402 Load a pKa set and attach chemical states and acid–base reactions 2403 to existing particle templates. 2404 2405 Args: 2406 filename ('str'): 2407 Path to a JSON file containing the pKa set. Expected format: 2408 {"metadata": {...}, 2409 "data": {"A": {"acidity": "acidic", "pka_value": 4.5}, 2410 "B": {"acidity": "basic", "pka_value": 9.8}}} 2411 2412 Returns: 2413 ('dict'): 2414 Dictionary with bibliographic metadata about the original work were the pKa set was determined. 2415 2416 Notes: 2417 - This method is designed for monoprotic acids and bases only. 2418 """ 2419 with open(filename, "r") as f: 2420 pka_data = json.load(f) 2421 pka_set = pka_data["data"] 2422 metadata = pka_data.get("metadata", {}) 2423 self._check_pka_set(pka_set) 2424 for particle_name, entry in pka_set.items(): 2425 acidity = entry["acidity"] 2426 pka = entry["pka_value"] 2427 self.define_monoprototic_acidbase_reaction(particle_name=particle_name, 2428 pka=pka, 2429 acidity=acidity, 2430 metadata=metadata) 2431 return metadata 2432 2433 def propose_unused_type(self): 2434 """ 2435 Propose an unused ESPResSo particle type. 2436 2437 Returns: 2438 ('int'): 2439 The next available integer ESPResSo type. Returns ''0'' if no integer types are currently defined. 2440 """ 2441 return self.db.propose_unused_type() 2442 2443 def read_protein_vtf(self, filename, unit_length=None): 2444 """ 2445 Loads a coarse-grained protein model from a VTF file. 2446 2447 Args: 2448 filename ('str'): 2449 Path to the VTF file. 2450 2451 unit_length ('Pint.Quantity'): 2452 Unit of length for coordinates (pyMBE UnitRegistry). Defaults to Angstrom. 2453 2454 Returns: 2455 ('tuple'): 2456 ('dict'): Particle topology. 2457 ('str'): One-letter amino-acid sequence (including n/c ends). 2458 """ 2459 logging.info(f"Loading protein coarse-grain model file: {filename}") 2460 if unit_length is None: 2461 unit_length = 1 * self.units.angstrom 2462 atoms = {} # atom_id -> atom info 2463 coords = [] # ordered coordinates 2464 residues = {} # resid -> resname (first occurrence) 2465 has_n_term = False 2466 has_c_term = False 2467 aa_3to1 = {"ALA": "A", "ARG": "R", "ASN": "N", "ASP": "D", 2468 "CYS": "C", "GLU": "E", "GLN": "Q", "GLY": "G", 2469 "HIS": "H", "ILE": "I", "LEU": "L", "LYS": "K", 2470 "MET": "M", "PHE": "F", "PRO": "P", "SER": "S", 2471 "THR": "T", "TRP": "W", "TYR": "Y", "VAL": "V", 2472 "n": "n", "c": "c"} 2473 # --- parse VTF --- 2474 with open(filename, "r") as f: 2475 for line in f: 2476 fields = line.split() 2477 if not fields: 2478 continue 2479 if fields[0] == "atom": 2480 atom_id = int(fields[1]) 2481 atom_name = fields[3] 2482 resname = fields[5] 2483 resid = int(fields[7]) 2484 chain_id = fields[9] 2485 radius = float(fields[11]) * unit_length 2486 atoms[atom_id] = {"name": atom_name, 2487 "resname": resname, 2488 "resid": resid, 2489 "chain_id": chain_id, 2490 "radius": radius} 2491 if resname == "n": 2492 has_n_term = True 2493 elif resname == "c": 2494 has_c_term = True 2495 # register residue 2496 if resid not in residues: 2497 residues[resid] = resname 2498 elif fields[0].isnumeric(): 2499 xyz = [(float(x) * unit_length).to("reduced_length").magnitude 2500 for x in fields[1:4]] 2501 coords.append(xyz) 2502 sequence = "" 2503 # N-terminus 2504 if has_n_term: 2505 sequence += "n" 2506 # protein residues only 2507 protein_resids = sorted(resid for resid, resname in residues.items() if resname not in ("n", "c", "Ca")) 2508 for resid in protein_resids: 2509 resname = residues[resid] 2510 try: 2511 sequence += aa_3to1[resname] 2512 except KeyError: 2513 raise ValueError(f"Unknown residue name '{resname}' in VTF file") 2514 # C-terminus 2515 if has_c_term: 2516 sequence += "c" 2517 last_resid = max(protein_resids) 2518 # --- build topology --- 2519 topology_dict = {} 2520 for atom_id in sorted(atoms.keys()): 2521 atom = atoms[atom_id] 2522 resname = atom["resname"] 2523 resid = atom["resid"] 2524 # apply labeling rules 2525 if resname == "n": 2526 label_resid = 0 2527 elif resname == "c": 2528 label_resid = last_resid + 1 2529 elif resname == "Ca": 2530 label_resid = last_resid + 2 2531 else: 2532 label_resid = resid # preserve original resid 2533 label = f"{atom['name']}{label_resid}" 2534 if label in topology_dict: 2535 raise ValueError(f"Duplicate particle label '{label}'. Check VTF residue definitions.") 2536 topology_dict[label] = {"initial_pos": coords[atom_id - 1], "chain_id": atom["chain_id"], "radius": atom["radius"],} 2537 return topology_dict, sequence 2538 2539 2540 def save_database(self, folder, format='csv'): 2541 """ 2542 Saves the current pyMBE database into a file 'filename'. 2543 2544 Args: 2545 folder ('str' or 'Path'): 2546 Path to the folder where the database files will be saved. 2547 2548 """ 2549 supported_formats = ['csv'] 2550 if format not in supported_formats: 2551 raise ValueError(f"Format {format} not supported. Supported formats are: {supported_formats}") 2552 if format == 'csv': 2553 io._save_database_csv(self.db, 2554 folder=folder) 2555 2556 def set_particle_initial_state(self, particle_name, state_name): 2557 """ 2558 Sets the default initial state of a particle template defined in the pyMBE database. 2559 2560 Args: 2561 particle_name ('str'): 2562 Unique label that identifies the particle template. 2563 2564 state_name ('str'): 2565 Name of the state to be set as default initial state. 2566 """ 2567 part_tpl = self.db.get_template(name=particle_name, 2568 2569 pmb_type="particle") 2570 part_tpl.initial_state = state_name 2571 logging.info(f"Default initial state of particle {particle_name} set to {state_name}.") 2572 2573 def set_reduced_units(self, unit_length=None, unit_charge=None, temperature=None, Kw=None): 2574 """ 2575 Sets the set of reduced units used by pyMBE.units and it prints it. 2576 2577 Args: 2578 unit_length ('pint.Quantity', optional): 2579 Reduced unit of length defined using the 'pmb.units' UnitRegistry. Defaults to None. 2580 2581 unit_charge ('pint.Quantity', optional): 2582 Reduced unit of charge defined using the 'pmb.units' UnitRegistry. Defaults to None. 2583 2584 temperature ('pint.Quantity', optional): 2585 Temperature of the system, defined using the 'pmb.units' UnitRegistry. Defaults to None. 2586 2587 Kw ('pint.Quantity', optional): 2588 Ionic product of water in mol^2/l^2. Defaults to None. 2589 2590 Notes: 2591 - If no 'temperature' is given, a value of 298.15 K is assumed by default. 2592 - If no 'unit_length' is given, a value of 0.355 nm is assumed by default. 2593 - If no 'unit_charge' is given, a value of 1 elementary charge is assumed by default. 2594 - If no 'Kw' is given, a value of 10^(-14) * mol^2 / l^2 is assumed by default. 2595 """ 2596 if unit_length is None: 2597 unit_length= 0.355*self.units.nm 2598 if temperature is None: 2599 temperature = 298.15 * self.units.K 2600 if unit_charge is None: 2601 unit_charge = scipy.constants.e * self.units.C 2602 if Kw is None: 2603 Kw = 1e-14 2604 # Sanity check 2605 variables=[unit_length,temperature,unit_charge] 2606 dimensionalities=["[length]","[temperature]","[charge]"] 2607 for variable,dimensionality in zip(variables,dimensionalities): 2608 self._check_dimensionality(variable,dimensionality) 2609 self.Kw=Kw*self.units.mol**2 / (self.units.l**2) 2610 self.kT=temperature*self.kB 2611 self.units._build_cache() 2612 self.units.define(f'reduced_energy = {self.kT} ') 2613 self.units.define(f'reduced_length = {unit_length}') 2614 self.units.define(f'reduced_charge = {unit_charge}') 2615 logging.info(self.get_reduced_units()) 2616 2617 def set_simulation_engine(self,simulation_engine,box_l=None): 2618 """ 2619 Sets the instance attribute simulation_engine to an instance of a class of type SimulationEngine. 2620 2621 Args: 2622 simulation_engine (Any): object which contains the methods to setup molecular dynamics and montecarlo simulations 2623 box_l('list[float,float,float]'): list of floats with the dimensions of the box 2624 """ 2625 2626 if isinstance(simulation_engine, espressomd.System): 2627 self.simulation_engine=EspressoSimulation(box_l=simulation_engine.box_l, 2628 db=self.db, 2629 espresso_system=simulation_engine, 2630 units=self.units, 2631 kT=self.kT, 2632 Kw=self.Kw, 2633 seed=self.seed) 2634 elif isinstance(simulation_engine,LammpsProtocol): 2635 self.simulation_engine=LammpsSimulation(box_l=box_l, 2636 db=self.db, 2637 lammps=simulation_engine, 2638 units=self.units, 2639 kT=self.kT, 2640 Kw=self.Kw, 2641 seed=self.seed) 2642 else: 2643 raise ValueError('The specified simulation engine is not implemented yet') 2644 2645 def setup_cpH (self, counter_ion, constant_pH, exclusion_range=None, use_exclusion_radius_per_type = False): 2646 """ 2647 Sets up the Acid/Base reactions for acidic/basic particles defined in the pyMBE database 2648 to be sampled in the constant pH ensemble. 2649 2650 Args: 2651 counter_ion ('str'): 2652 'name' of the counter_ion 'particle'. 2653 2654 constant_pH ('float'): 2655 pH-value. 2656 2657 exclusion_range ('pint.Quantity', optional): 2658 Below this value, no particles will be inserted. 2659 2660 use_exclusion_radius_per_type ('bool', optional): 2661 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2662 2663 Returns: 2664 ('reaction_methods.ConstantpHEnsemble'): 2665 Instance of a reaction_methods.ConstantpHEnsemble object from the espressomd library. 2666 """ 2667 2668 RE = self.simulation_engine.setup_cpH(counter_ion=counter_ion, 2669 constant_pH=constant_pH, 2670 exclusion_range=exclusion_range, 2671 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2672 return RE 2673 2674 def setup_gcmc(self, c_salt_res, salt_cation_name, salt_anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2675 """ 2676 Sets up grand-canonical coupling to a reservoir of salt. 2677 For reactive systems coupled to a reservoir, the grand-reaction method has to be used instead. 2678 2679 Args: 2680 c_salt_res ('pint.Quantity'): 2681 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2682 2683 salt_cation_name ('str'): 2684 Name of the salt cation (e.g. Na+) particle. 2685 2686 salt_anion_name ('str'): 2687 Name of the salt anion (e.g. Cl-) particle. 2688 2689 activity_coefficient ('callable'): 2690 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2691 2692 exclusion_range('pint.Quantity', optional): 2693 For distances shorter than this value, no particles will be inserted. 2694 2695 use_exclusion_radius_per_type('bool',optional): 2696 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2697 2698 Returns: 2699 ('reaction_methods.ReactionEnsemble'): 2700 Instance of a reaction_methods.ReactionEnsemble object from the espressomd library. 2701 """ 2702 RE = self.simulation_engine.setup_gcmc(c_salt_res=c_salt_res, 2703 salt_anion_name=salt_anion_name, 2704 salt_cation_name=salt_cation_name, 2705 activity_coefficient=activity_coefficient, 2706 exclusion_range=exclusion_range, 2707 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2708 return RE 2709 2710 def setup_grxmc_reactions(self, pH_res, c_salt_res, proton_name, hydroxide_name, salt_cation_name, salt_anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2711 """ 2712 Sets up acid/base reactions for acidic/basic monoprotic particles defined in the pyMBE database, 2713 as well as a grand-canonical coupling to a reservoir of small ions. 2714 2715 2716 Args: 2717 pH_res ('float'): 2718 pH-value in the reservoir. 2719 2720 c_salt_res ('pint.Quantity'): 2721 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2722 2723 proton_name ('str'): 2724 Name of the proton (H+) particle. 2725 2726 hydroxide_name ('str'): 2727 Name of the hydroxide (OH-) particle. 2728 2729 salt_cation_name ('str'): 2730 Name of the salt cation (e.g. Na+) particle. 2731 2732 salt_anion_name ('str'): 2733 Name of the salt anion (e.g. Cl-) particle. 2734 2735 activity_coefficient ('callable'): 2736 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2737 2738 exclusion_range('pint.Quantity', optional): 2739 For distances shorter than this value, no particles will be inserted. 2740 2741 use_exclusion_radius_per_type('bool', optional): 2742 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2743 2744 Returns: 2745 For the Espresso system class: 2746 Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'): 2747 2748 'reaction_methods.ReactionEnsemble': 2749 espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 2750 2751 'pint.Quantity': 2752 Ionic strength of the reservoir (useful for calculating partition coefficients). 2753 2754 Notess: 2755 - This implementation uses the original formulation of the grand-reaction method by Landsgesell et al. [1]. 2756 2757 [1] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020. 2758 """ 2759 output=self.simulation_engine.setup_grxmc_reactions(pH_res=pH_res, 2760 c_salt_res=c_salt_res, 2761 proton_name=proton_name, 2762 hydroxide_name=hydroxide_name, 2763 salt_cation_name=salt_cation_name, 2764 salt_anion_name=salt_anion_name, 2765 activity_coefficient=activity_coefficient, 2766 exclusion_range=exclusion_range, 2767 use_exclusion_radius_per_type=use_exclusion_radius_per_type) 2768 2769 return output 2770 2771 def setup_grxmc_unified(self, pH_res, c_salt_res, cation_name, anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2772 """ 2773 Sets up acid/base reactions for acidic/basic 'particles' defined in the pyMBE database, as well as a grand-canonical coupling to a 2774 reservoir of small ions using a unified formulation for small ions. 2775 2776 Args: 2777 pH_res ('float'): 2778 pH-value in the reservoir. 2779 2780 c_salt_res ('pint.Quantity'): 2781 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2782 2783 cation_name ('str'): 2784 Name of the cationic particle. 2785 2786 anion_name ('str'): 2787 Name of the anionic particle. 2788 2789 activity_coefficient ('callable'): 2790 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2791 2792 exclusion_range('pint.Quantity', optional): 2793 Below this value, no particles will be inserted. 2794 2795 use_exclusion_radius_per_type('bool', optional): 2796 Controls if one exclusion_radius per each espresso_type. Defaults to 'False'. 2797 2798 Returns: 2799 For the Espresso system class: 2800 Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'): 2801 2802 'reaction_methods.ReactionEnsemble': 2803 espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 2804 2805 'pint.Quantity': 2806 Ionic strength of the reservoir (useful for calculating partition coefficients). 2807 2808 Notes: 2809 - This implementation uses the formulation of the grand-reaction method by Curk et al. [1], which relies on "unified" ion types X+ = {H+, Na+} and X- = {OH-, Cl-}. 2810 - A function that implements the original version of the grand-reaction method by Landsgesell et al. [2] is also available under the name 'setup_grxmc_reactions'. 2811 2812 [1] Curk, T., Yuan, J., & Luijten, E. (2022). Accelerated simulation method for charge regulation effects. The Journal of Chemical Physics, 156(4). 2813 [2] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020. 2814 """ 2815 output=self.simulation_engine.setup_grxmc_unified(pH_res=pH_res, 2816 c_salt_res=c_salt_res, 2817 cation_name=cation_name, 2818 anion_name=anion_name, 2819 activity_coefficient=activity_coefficient, 2820 exclusion_range=exclusion_range, 2821 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2822 return output 2823 2824 def setup_lj_interactions(self, shift_potential=True, combining_rule='Lorentz-Berthelot'): 2825 """ 2826 Sets up the Lennard-Jones (LJ) potential between all pairs of particle states defined in the pyMBE database. 2827 2828 Args: 2829 2830 shift_potential('bool', optional): 2831 If True, a shift will be automatically computed such that the potential is continuous at the cutoff radius. Otherwise, no shift will be applied. Defaults to True. 2832 2833 combining_rule('string', optional): 2834 combining rule used to calculate 'sigma' and 'epsilon' for the potential between a pair of particles. Defaults to 'Lorentz-Berthelot'. 2835 2836 warning('bool', optional): 2837 switch to activate/deactivate warning messages. Defaults to True. 2838 2839 Notes: 2840 - Currently, the only 'combining_rule' supported is Lorentz-Berthelot. 2841 - Check the documentation of ESPResSo for more info about the potential https://espressomd.github.io/doc4.2.0/inter_non-bonded.html 2842 2843 """ 2844 self.simulation_engine.setup_lj_interactions(shift_potential=shift_potential, 2845 combining_rule=combining_rule)
Core library of the Molecular Builder for ESPResSo (pyMBE).
Attributes:
- N_A ('pint.Quantity'): Avogadro number.
- kB ('pint.Quantity'): Boltzmann constant.
- e ('pint.Quantity'): Elementary charge.
- kT ('pint.Quantity'): Thermal energy corresponding to the set temperature.
- Kw ('pint.Quantity'): Ionic product of water, used in G-RxMC and Donnan-related calculations.
- db ('Manager'): Database manager holding all pyMBE templates, instances and reactions.
- rng ('numpy.random.Generator'): Random number generator initialized with the provided seed.
- units ('pint.UnitRegistry'): Pint unit registry used for unit-aware calculations.
- lattice_builder ('pyMBE.lib.lattice.LatticeBuilder'): Optional lattice builder object (initialized as ''None'').
- root ('importlib.resources.abc.Traversable'): Root path to the pyMBE package resources.
99 def __init__(self, seed, temperature=None, unit_length=None, unit_charge=None, Kw=None): 100 """ 101 Initializes the pyMBE library. 102 103 Args: 104 seed ('int'): 105 Seed for the random number generator. 106 107 temperature ('pint.Quantity', optional): 108 Simulation temperature. If ''None'', defaults to 298.15 K. 109 110 unit_length ('pint.Quantity', optional): 111 Reference length for reduced units. If ''None'', defaults to 112 0.355 nm. 113 114 unit_charge ('pint.Quantity', optional): 115 Reference charge for reduced units. If ''None'', defaults to 116 one elementary charge. 117 118 Kw ('pint.Quantity', optional): 119 Ionic product of water (typically in mol²/L²). If ''None'', 120 defaults to 1e-14 mol²/L². 121 """ 122 # Seed and RNG 123 self.seed=seed 124 self.rng = np.random.default_rng(seed) 125 self.units=pint.UnitRegistry() 126 self.N_A=scipy.constants.N_A / self.units.mol 127 self.kB=scipy.constants.k * self.units.J / self.units.K 128 self.e=scipy.constants.e * self.units.C 129 self.set_reduced_units(unit_length=unit_length, 130 unit_charge=unit_charge, 131 temperature=temperature, 132 Kw=Kw) 133 134 self.db = Manager(units=self.units) 135 self.simulation_engine = DummyEngine() 136 self.lattice_builder = None 137 self.root = importlib.resources.files(__package__)
Initializes the pyMBE library.
Arguments:
- seed ('int'): Seed for the random number generator.
- temperature ('pint.Quantity', optional): Simulation temperature. If ''None'', defaults to 298.15 K.
- unit_length ('pint.Quantity', optional): Reference length for reduced units. If ''None'', defaults to 0.355 nm.
- unit_charge ('pint.Quantity', optional): Reference charge for reduced units. If ''None'', defaults to one elementary charge.
- Kw ('pint.Quantity', optional): Ionic product of water (typically in mol²/L²). If ''None'', defaults to 1e-14 mol²/L².
419 def calculate_center_of_mass(self, instance_id, pmb_type): 420 """ 421 Calculates the center of mass of a pyMBE object instance in an ESPResSo system. 422 423 Args: 424 instance_id ('int'): 425 pyMBE instance ID of the object whose center of mass is calculated. 426 427 pmb_type ('str'): 428 Type of the pyMBE object. Must correspond to a particle-aggregating 429 template type (e.g. '"molecule"', '"residue"', '"peptide"', '"protein"'). 430 431 Returns: 432 ('numpy.ndarray'): 433 Array of shape '(3,)' containing the Cartesian coordinates of the 434 center of mass. 435 436 Notes: 437 - This method assumes equal mass for all particles. 438 - Periodic boundary conditions are *not* unfolded; positions are taken 439 directly from ESPResSo particle coordinates. 440 """ 441 return self.simulation_engine.calculate_center_of_mass(instance_id=instance_id, 442 pmb_type=pmb_type)
Calculates the center of mass of a pyMBE object instance in an ESPResSo system.
Arguments:
- instance_id ('int'): pyMBE instance ID of the object whose center of mass is calculated.
- pmb_type ('str'): Type of the pyMBE object. Must correspond to a particle-aggregating template type (e.g. '"molecule"', '"residue"', '"peptide"', '"protein"').
Returns:
('numpy.ndarray'): Array of shape '(3,)' containing the Cartesian coordinates of the center of mass.
Notes:
- This method assumes equal mass for all particles.
- Periodic boundary conditions are not unfolded; positions are taken directly from ESPResSo particle coordinates.
444 def calculate_HH(self, template_name, pH_list=None, pka_set=None): 445 """ 446 Calculates the charge in the template object according to the ideal Henderson–Hasselbalch titration curve. 447 448 Args: 449 template_name ('str'): 450 Name of the template. 451 452 pH_list ('list[float]', optional): 453 pH values at which the charge is evaluated. 454 Defaults to 50 values between 2 and 12. 455 456 pka_set ('dict', optional): 457 Mapping: {particle_name: {"pka_value": 'float', "acidity": "acidic"|"basic"}} 458 459 Returns: 460 'list[float]': 461 Net molecular charge at each pH value. 462 """ 463 if pH_list is None: 464 pH_list = np.linspace(2, 12, 50) 465 if pka_set is None: 466 pka_set = self.get_pka_set() 467 self._check_pka_set(pka_set=pka_set) 468 particle_counts = self.db.get_particle_templates_under(template_name=template_name, 469 return_counts=True) 470 if not particle_counts: 471 return [None] * len(pH_list) 472 charge_number_map = self.get_charge_number_map() 473 def formal_charge(particle_name): 474 tpl = self.db.get_template(name=particle_name, 475 pmb_type="particle") 476 state = self.db.get_template(name=tpl.initial_state, 477 pmb_type="particle_state") 478 return charge_number_map[state.es_type] 479 Z_HH = [] 480 for pH in pH_list: 481 Z = 0.0 482 for particle, multiplicity in particle_counts.items(): 483 if particle in pka_set: 484 pka = pka_set[particle]["pka_value"] 485 acidity = pka_set[particle]["acidity"] 486 if acidity == "acidic": 487 psi = -1 488 elif acidity == "basic": 489 psi = +1 490 else: 491 raise ValueError(f"Unknown acidity '{acidity}' for particle '{particle}'") 492 charge = psi / (1.0 + 10.0 ** (psi * (pH - pka))) 493 Z += multiplicity * charge 494 else: 495 Z += multiplicity * formal_charge(particle) 496 Z_HH.append(Z) 497 return Z_HH
Calculates the charge in the template object according to the ideal Henderson–Hasselbalch titration curve.
Arguments:
- template_name ('str'): Name of the template.
- pH_list ('list[float]', optional): pH values at which the charge is evaluated. Defaults to 50 values between 2 and 12.
- pka_set ('dict', optional): Mapping: {particle_name: {"pka_value": 'float', "acidity": "acidic"|"basic"}}
Returns:
'list[float]': Net molecular charge at each pH value.
499 def calculate_HH_Donnan(self, c_macro, c_salt, pH_list=None, pka_set=None): 500 """ 501 Computes macromolecular charges using the Henderson–Hasselbalch equation 502 coupled to ideal Donnan partitioning. 503 504 Args: 505 c_macro ('dict'): 506 Mapping of macromolecular species names to their concentrations 507 in the system: 508 '{molecule_name: concentration}'. 509 510 c_salt ('float' or 'pint.Quantity'): 511 Salt concentration in the reservoir. 512 513 pH_list ('list[float]', optional): 514 List of pH values in the reservoir at which the calculation is 515 performed. If 'None', 50 equally spaced values between 2 and 12 516 are used. 517 518 pka_set ('dict', optional): 519 Dictionary defining the acid–base properties of titratable particle 520 types: 521 '{particle_name: {"pka_value": float, "acidity": "acidic" | "basic"}}'. 522 If 'None', the pKa set is taken from the pyMBE database. 523 524 Returns: 525 'dict': 526 Dictionary containing: 527 - '"charges_dict"' ('dict'): 528 Mapping '{molecule_name: list}' of Henderson–Hasselbalch–Donnan 529 charges evaluated at each pH value. 530 - '"pH_system_list"' ('list[float]'): 531 Effective pH values inside the system phase after Donnan 532 partitioning. 533 - '"partition_coefficients"' ('list[float]'): 534 Partition coefficients of monovalent cations at each pH value. 535 536 Notes: 537 - This method assumes **ideal Donnan equilibrium** and **monovalent salt**. 538 - The ionic strength of the reservoir includes both salt and 539 pH-dependent H⁺/OH⁻ contributions. 540 - All charged macromolecular species present in the system must be 541 included in 'c_macro'; missing species will lead to incorrect results. 542 - The nonlinear Donnan equilibrium equation is solved using a scalar 543 root finder ('brentq') in logarithmic form for numerical stability. 544 - This method is intended for **two-phase systems**; for single-phase 545 systems use 'calculate_HH' instead. 546 """ 547 if pH_list is None: 548 pH_list=np.linspace(2,12,50) 549 if pka_set is None: 550 pka_set=self.get_pka_set() 551 self._check_pka_set(pka_set=pka_set) 552 partition_coefficients_list = [] 553 pH_system_list = [] 554 Z_HH_Donnan={} 555 for key in c_macro: 556 Z_HH_Donnan[key] = [] 557 def calc_charges(c_macro, pH): 558 """ 559 Calculates the charges of the different kinds of molecules according to the Henderson-Hasselbalch equation. 560 561 Args: 562 c_macro ('dict'): 563 {"name": concentration} - A dict containing the concentrations of all charged macromolecular species in the system. 564 565 pH ('float'): 566 pH-value that is used in the HH equation. 567 568 Returns: 569 ('dict'): 570 {"molecule_name": charge} 571 """ 572 charge = {} 573 for name in c_macro: 574 charge[name] = self.calculate_HH(name, [pH], pka_set)[0] 575 return charge 576 577 def calc_partition_coefficient(charge, c_macro): 578 """ 579 Calculates the partition coefficients of positive ions according to the ideal Donnan theory. 580 581 Args: 582 charge ('dict'): 583 {"molecule_name": charge} 584 585 c_macro ('dict'): 586 {"name": concentration} - A dict containing the concentrations of all charged macromolecular species in the system. 587 """ 588 nonlocal ionic_strength_res 589 charge_density = 0.0 590 for key in charge: 591 charge_density += charge[key] * c_macro[key] 592 return (-charge_density / (2 * ionic_strength_res) + np.sqrt((charge_density / (2 * ionic_strength_res))**2 + 1)).magnitude 593 for pH_value in pH_list: 594 # calculate the ionic strength of the reservoir 595 if pH_value <= 7.0: 596 ionic_strength_res = 10 ** (-pH_value) * self.units.mol/self.units.l + c_salt 597 elif pH_value > 7.0: 598 ionic_strength_res = 10 ** (-(14-pH_value)) * self.units.mol/self.units.l + c_salt 599 #Determine the partition coefficient of positive ions by solving the system of nonlinear, coupled equations 600 #consisting of the partition coefficient given by the ideal Donnan theory and the Henderson-Hasselbalch equation. 601 #The nonlinear equation is formulated for log(xi) since log-operations are not supported for RootResult objects. 602 equation = lambda logxi: logxi - np.log10(calc_partition_coefficient(calc_charges(c_macro, pH_value - logxi), c_macro)) 603 logxi = scipy.optimize.root_scalar(equation, bracket=[-1e2, 1e2], method="brentq") 604 partition_coefficient = 10**logxi.root 605 charges_temp = calc_charges(c_macro, pH_value-np.log10(partition_coefficient)) 606 for key in c_macro: 607 Z_HH_Donnan[key].append(charges_temp[key]) 608 pH_system_list.append(pH_value - np.log10(partition_coefficient)) 609 partition_coefficients_list.append(partition_coefficient) 610 return {"charges_dict": Z_HH_Donnan, "pH_system_list": pH_system_list, "partition_coefficients": partition_coefficients_list}
Computes macromolecular charges using the Henderson–Hasselbalch equation coupled to ideal Donnan partitioning.
Arguments:
- c_macro ('dict'): Mapping of macromolecular species names to their concentrations in the system: '{molecule_name: concentration}'.
- c_salt ('float' or 'pint.Quantity'): Salt concentration in the reservoir.
- pH_list ('list[float]', optional): List of pH values in the reservoir at which the calculation is performed. If 'None', 50 equally spaced values between 2 and 12 are used.
- pka_set ('dict', optional): Dictionary defining the acid–base properties of titratable particle types: '{particle_name: {"pka_value": float, "acidity": "acidic" | "basic"}}'. If 'None', the pKa set is taken from the pyMBE database.
Returns:
'dict': Dictionary containing: - '"charges_dict"' ('dict'): Mapping '{molecule_name: list}' of Henderson–Hasselbalch–Donnan charges evaluated at each pH value. - '"pH_system_list"' ('list[float]'): Effective pH values inside the system phase after Donnan partitioning. - '"partition_coefficients"' ('list[float]'): Partition coefficients of monovalent cations at each pH value.
Notes:
- This method assumes ideal Donnan equilibrium and monovalent salt.
- The ionic strength of the reservoir includes both salt and pH-dependent H⁺/OH⁻ contributions.
- All charged macromolecular species present in the system must be included in 'c_macro'; missing species will lead to incorrect results.
- The nonlinear Donnan equilibrium equation is solved using a scalar root finder ('brentq') in logarithmic form for numerical stability.
- This method is intended for two-phase systems; for single-phase systems use 'calculate_HH' instead.
612 def calculate_net_charge(self,object_name,pmb_type,dimensionless=False): 613 """ 614 Calculates the net charge per instance of a given pmb object type. 615 616 Args: 617 object_name (str): 618 Name of the object (e.g. molecule, residue, peptide, protein). 619 pmb_type (str): 620 Type of object to analyze. Must be molecule-like. 621 dimensionless (bool, optional): 622 If True, return charge as a pure number. 623 If False, return a quantity with reduced_charge units. 624 625 Returns: 626 dict: 627 {"mean": mean_net_charge, "instances": {instance_id: net_charge}} 628 """ 629 return self.simulation_engine.calculate_net_charge(object_name, 630 pmb_type, 631 dimensionless)
Calculates the net charge per instance of a given pmb object type.
Arguments:
- object_name (str): Name of the object (e.g. molecule, residue, peptide, protein).
- pmb_type (str): Type of object to analyze. Must be molecule-like.
- dimensionless (bool, optional): If True, return charge as a pure number. If False, return a quantity with reduced_charge units.
Returns:
dict: {"mean": mean_net_charge, "instances": {instance_id: net_charge}}
633 def center_object_in_simulation_box(self, instance_id, box_l,pmb_type): 634 """ 635 Centers a pyMBE object instance in the simulation box of an ESPResSo system. 636 The object is translated such that its center of mass coincides with the 637 geometric center of the ESPResSo simulation box. 638 639 Args: 640 instance_id ('int'): 641 ID of the pyMBE object instance to be centered. 642 643 box_l('list[float,float,float]'): list of floats with the dimensions of the box 644 645 pmb_type ('str'): 646 Type of the pyMBE object. 647 648 Notes: 649 - Works for both cubic and non-cubic simulation boxes. 650 """ 651 inst = self.db.get_instance(instance_id=instance_id, 652 pmb_type=pmb_type) 653 center_of_mass = self.calculate_center_of_mass(instance_id=instance_id, 654 pmb_type=pmb_type) 655 box_center = [box_l[0]/2.0, 656 box_l[1]/2.0, 657 box_l[2]/2.0] 658 particle_id_list = self.get_particle_id_map(object_name=inst.name)["all"] 659 for pid in particle_id_list: 660 es_pos=self.db.get_instance(instance_id=pid, 661 pmb_type='particle').position 662 centered_position=es_pos - center_of_mass + box_center 663 664 self.db._update_instance(instance_id=pid, 665 pmb_type='particle', 666 attribute='position', 667 value=centered_position) 668 if isinstance(self.simulation_engine, EspressoSimulation): 669 self.simulation_engine._update_particle_position( 670 particle_id=pid, position=centered_position)
Centers a pyMBE object instance in the simulation box of an ESPResSo system. The object is translated such that its center of mass coincides with the geometric center of the ESPResSo simulation box.
Arguments:
- instance_id ('int'): ID of the pyMBE object instance to be centered.
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
- pmb_type ('str'): Type of the pyMBE object.
Notes:
- Works for both cubic and non-cubic simulation boxes.
672 def create_added_salt(self, box_l, cation_name, anion_name, c_salt): 673 """ 674 Creates a 'c_salt' concentration of 'cation_name' and 'anion_name' ions into the 'espresso_system'. 675 676 Args: 677 cation_name('str'): 'name' of a particle with a positive charge. 678 anion_name('str'): 'name' of a particle with a negative charge. 679 c_salt('float'): Salt concentration. 680 681 Returns: 682 c_salt_calculated('float'): Calculated salt concentration added to 'espresso_system'. 683 """ 684 cation_tpl = self.db.get_template(pmb_type="particle", 685 name=cation_name) 686 cation_state = self.db.get_template(pmb_type="particle_state", 687 name=cation_tpl.initial_state) 688 cation_charge = cation_state.z 689 anion_tpl = self.db.get_template(pmb_type="particle", 690 name=anion_name) 691 anion_state = self.db.get_template(pmb_type="particle_state", 692 name=anion_tpl.initial_state) 693 anion_charge = anion_state.z 694 if cation_charge <= 0: 695 raise ValueError(f'ERROR cation charge must be positive, charge {cation_charge}') 696 if anion_charge >= 0: 697 raise ValueError(f'ERROR anion charge must be negative, charge {anion_charge}') 698 # Calculate the number of ions in the simulation box 699 volume=self.units.Quantity(np.prod(box_l), 'reduced_length**3') 700 if c_salt.check('[substance] [length]**-3'): 701 N_ions= int((volume*c_salt.to('mol/reduced_length**3')*self.N_A).magnitude) 702 c_salt_calculated=N_ions/(volume*self.N_A) 703 elif c_salt.check('[length]**-3'): 704 N_ions= int((volume*c_salt.to('reduced_length**-3')).magnitude) 705 c_salt_calculated=N_ions/volume 706 else: 707 raise ValueError('Unknown units for c_salt, please provided it in [mol / volume] or [particle / volume]', c_salt) 708 N_cation = N_ions*abs(anion_charge) 709 N_anion = N_ions*abs(cation_charge) 710 self.create_particle(box_l=box_l, 711 name=cation_name, 712 number_of_particles=N_cation) 713 self.create_particle(box_l=box_l, 714 name=anion_name, 715 number_of_particles=N_anion) 716 if c_salt_calculated.check('[substance] [length]**-3'): 717 logging.info(f"added salt concentration of {c_salt_calculated.to('mol/L')} given by {N_cation} cations and {N_anion} anions") 718 elif c_salt_calculated.check('[length]**-3'): 719 logging.info(f"added salt concentration of {c_salt_calculated.to('reduced_length**-3')} given by {N_cation} cations and {N_anion} anions") 720 return c_salt_calculated
Creates a 'c_salt' concentration of 'cation_name' and 'anion_name' ions into the 'espresso_system'.
Arguments:
- cation_name('str'): 'name' of a particle with a positive charge.
- anion_name('str'): 'name' of a particle with a negative charge.
- c_salt('float'): Salt concentration.
Returns:
c_salt_calculated('float'): Calculated salt concentration added to 'espresso_system'.
722 def create_bond(self, particle_id1, particle_id2, use_default_bond=False): 723 """ 724 Creates a bond between two particle instances in an ESPResSo system and registers it in the pyMBE database. 725 726 This method performs the following steps: 727 1. Retrieves the particle instances corresponding to 'particle_id1' and 'particle_id2' from the database. 728 2. Retrieves or creates the corresponding ESPResSo bond instance using the bond template. 729 3. Adds the ESPResSo bond instance to the ESPResSo system if it was newly created. 730 4. Adds the bond to the first particle's bond list in ESPResSo. 731 5. Creates a 'BondInstance' in the database and registers it. 732 733 Args: 734 particle_id1 ('int'): 735 pyMBE and ESPResSo ID of the first particle. 736 737 particle_id2 ('int'): 738 pyMBE and ESPResSo ID of the second particle. 739 740 use_default_bond ('bool', optional): 741 If True, use a default bond template if no specific template exists. Defaults to False. 742 743 Returns: 744 ('int'): 745 bond_id of the bond instance created in the pyMBE database. 746 """ 747 particle_inst_1 = self.db.get_instance(pmb_type="particle", 748 instance_id=particle_id1) 749 particle_inst_2 = self.db.get_instance(pmb_type="particle", 750 instance_id=particle_id2) 751 bond_tpl = self.get_bond_template(particle_name1=particle_inst_1.name, 752 particle_name2=particle_inst_2.name, 753 use_default_bond=use_default_bond) 754 bond_id = self.db._propose_instance_id(pmb_type="bond") 755 pmb_bond_instance = BondInstance(bond_id=bond_id, 756 name=bond_tpl.name, 757 particle_id1=particle_id1, 758 particle_id2=particle_id2) 759 self.db._register_instance(instance=pmb_bond_instance)
Creates a bond between two particle instances in an ESPResSo system and registers it in the pyMBE database.
This method performs the following steps:
- Retrieves the particle instances corresponding to 'particle_id1' and 'particle_id2' from the database.
- Retrieves or creates the corresponding ESPResSo bond instance using the bond template.
- Adds the ESPResSo bond instance to the ESPResSo system if it was newly created.
- Adds the bond to the first particle's bond list in ESPResSo.
- Creates a 'BondInstance' in the database and registers it.
Arguments:
- particle_id1 ('int'): pyMBE and ESPResSo ID of the first particle.
- particle_id2 ('int'): pyMBE and ESPResSo ID of the second particle.
- use_default_bond ('bool', optional): If True, use a default bond template if no specific template exists. Defaults to False.
Returns:
('int'): bond_id of the bond instance created in the pyMBE database.
761 def create_counterions(self, object_name, cation_name, anion_name, box_l): 762 """ 763 Creates particles of 'cation_name' and 'anion_name' in 'espresso_system' to counter the net charge of 'object_name'. 764 765 Args: 766 object_name ('str'): 767 'name' of a pyMBE object. 768 769 cation_name ('str'): 770 'name' of a particle with a positive charge. 771 772 anion_name ('str'): 773 'name' of a particle with a negative charge. 774 775 box_l('list[float,float,float]'): list of floats with the dimensions of the box 776 777 Returns: 778 ('dict'): 779 {"name": number} 780 781 Notes: 782 This function currently does not support the creation of counterions for hydrogels. 783 """ 784 cation_tpl = self.db.get_template(pmb_type="particle", 785 name=cation_name) 786 cation_state = self.db.get_template(pmb_type="particle_state", 787 name=cation_tpl.initial_state) 788 cation_charge = cation_state.z 789 anion_tpl = self.db.get_template(pmb_type="particle", 790 name=anion_name) 791 anion_state = self.db.get_template(pmb_type="particle_state", 792 name=anion_tpl.initial_state) 793 anion_charge = anion_state.z 794 object_ids = self.get_particle_id_map(object_name=object_name)["all"] 795 counterion_number={} 796 object_charge={} 797 for name in ['positive', 'negative']: 798 object_charge[name]=0 799 for id in object_ids: 800 object_name = self.db.get_instance(pmb_type="particle", 801 instance_id=id).name 802 object_tpl = self.db.get_template(pmb_type="particle", 803 name=object_name) 804 object_state = self.db.get_template(pmb_type="particle_state", 805 name=object_tpl.initial_state) 806 object_z = object_state.z 807 if object_z > 0: 808 object_charge['positive']+=1*(np.abs(object_z )) 809 elif object_z < 0: 810 object_charge['negative']+=1*(np.abs(object_z )) 811 if object_charge['positive'] % abs(anion_charge) == 0: 812 counterion_number[anion_name]=int(object_charge['positive']/abs(anion_charge)) 813 else: 814 raise ValueError('The number of positive charges in the pmb_object must be divisible by the charge of the anion') 815 if object_charge['negative'] % abs(cation_charge) == 0: 816 counterion_number[cation_name]=int(object_charge['negative']/cation_charge) 817 else: 818 raise ValueError('The number of negative charges in the pmb_object must be divisible by the charge of the cation') 819 if counterion_number[cation_name] > 0: 820 self.create_particle(box_l=box_l, 821 name=cation_name, 822 number_of_particles=counterion_number[cation_name]) 823 else: 824 counterion_number[cation_name]=0 825 if counterion_number[anion_name] > 0: 826 self.create_particle(box_l=box_l, 827 name=anion_name, 828 number_of_particles=counterion_number[anion_name]) 829 else: 830 counterion_number[anion_name] = 0 831 logging.info('the following counter-ions have been created: ') 832 for name in counterion_number.keys(): 833 logging.info(f'Ion type: {name} created number: {counterion_number[name]}') 834 return counterion_number
Creates particles of 'cation_name' and 'anion_name' in 'espresso_system' to counter the net charge of 'object_name'.
Arguments:
- object_name ('str'): 'name' of a pyMBE object.
- cation_name ('str'): 'name' of a particle with a positive charge.
- anion_name ('str'): 'name' of a particle with a negative charge.
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
Returns: ('dict'): {"name": number}
Notes:
This function currently does not support the creation of counterions for hydrogels.
837 def create_hydrogel(self, name, box_l, use_default_bond=False, gen_angle=False): 838 """ 839 Creates a hydrogel in espresso_system using a pyMBE hydrogel template given by 'name' 840 841 Args: 842 box_l('list[float,float,float]'): list of floats with the dimensions of the box 843 844 name ('str'): 845 name of the hydrogel template in the pyMBE database. 846 847 use_default_bond ('bool', optional): 848 If True, use a default bond template if no specific template exists. Defaults to False. 849 850 gen_angle ('bool', optional): 851 If True, generate angle potentials for the internal hydrogel 852 chains and, when explicitly defined, for all crosslinker-adjacent 853 triplets. Defaults to False. 854 855 Returns: 856 ('int'): id of the hydrogel instance created. 857 """ 858 if not self.db._has_template(name=name, pmb_type="hydrogel"): 859 raise ValueError(f"Hydrogel template with name '{name}' is not defined in the pyMBE database.") 860 hydrogel_tpl = self.db.get_template(pmb_type="hydrogel", 861 name=name) 862 assembly_id = self.db._propose_instance_id(pmb_type="hydrogel") 863 # Create the nodes 864 nodes = {} 865 hydrogel_angle_centers = set() 866 node_topology = hydrogel_tpl.node_map 867 for node in node_topology: 868 node_index = node.lattice_index 869 node_name = node.particle_name 870 node_pos, node_id = self._create_hydrogel_node(node_index=node_index, 871 node_name=node_name, 872 box_l=box_l) 873 node_label = self.lattice_builder._create_node_label(node_index=node_index) 874 nodes[node_label] = {"name": node_name, "id": node_id, "pos": node_pos} 875 self.db._update_instance(instance_id=node_id, 876 pmb_type="particle", 877 attribute="assembly_id", 878 value=assembly_id) 879 for hydrogel_chain in hydrogel_tpl.chain_map: 880 molecule_id = self._create_hydrogel_chain(hydrogel_chain=hydrogel_chain, 881 nodes=nodes, 882 box_l=box_l, 883 use_default_bond=use_default_bond, 884 gen_angle=gen_angle, 885 ) 886 self.db._update_instance(instance_id=molecule_id, 887 pmb_type="molecule", 888 attribute="assembly_id", 889 value=assembly_id) 890 if gen_angle: 891 residue_ids = self.db._find_instance_ids_by_attribute(pmb_type="residue", 892 attribute="molecule_id", 893 value=molecule_id) 894 first_residue_id = min(residue_ids) 895 last_residue_id = max(residue_ids) 896 first_residue = self.db.get_instance(pmb_type="residue", 897 instance_id=first_residue_id) 898 last_residue = self.db.get_instance(pmb_type="residue", 899 instance_id=last_residue_id) 900 first_central_bead_name = self.db.get_template(pmb_type="residue", 901 name=first_residue.name).central_bead 902 last_central_bead_name = self.db.get_template(pmb_type="residue", 903 name=last_residue.name).central_bead 904 particle_instances = self.db.get_instances(pmb_type="particle") 905 first_residue_particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 906 attribute="residue_id", 907 value=first_residue_id) 908 last_residue_particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 909 attribute="residue_id", 910 value=last_residue_id) 911 first_bead_id = None 912 for particle_id in first_residue_particle_ids: 913 if particle_instances[particle_id].name == first_central_bead_name: 914 first_bead_id = particle_id 915 break 916 917 last_bead_id = None 918 for particle_id in last_residue_particle_ids: 919 if particle_instances[particle_id].name == last_central_bead_name: 920 last_bead_id = particle_id 921 break 922 node_start_label = self.lattice_builder._create_node_label(hydrogel_chain.node_start) 923 node_end_label = self.lattice_builder._create_node_label(hydrogel_chain.node_end) 924 hydrogel_angle_centers.update({ 925 nodes[node_start_label]["id"], 926 nodes[node_end_label]["id"], 927 first_bead_id, 928 last_bead_id, 929 }) 930 self.db._propagate_id(root_type="hydrogel", 931 root_id=assembly_id, 932 attribute="assembly_id", 933 value=assembly_id) 934 if gen_angle: 935 self._generate_hydrogel_crosslinker_angles( 936 central_particle_ids=hydrogel_angle_centers) 937 # Register an hydrogel instance in the pyMBE databasegit 938 self.db._register_instance(HydrogelInstance(name=name, 939 assembly_id=assembly_id)) 940 return assembly_id
Creates a hydrogel in espresso_system using a pyMBE hydrogel template given by 'name'
Arguments:
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
- name ('str'): name of the hydrogel template in the pyMBE database.
- use_default_bond ('bool', optional): If True, use a default bond template if no specific template exists. Defaults to False.
- gen_angle ('bool', optional): If True, generate angle potentials for the internal hydrogel chains and, when explicitly defined, for all crosslinker-adjacent triplets. Defaults to False.
Returns:
('int'): id of the hydrogel instance created.
943 def create_molecule(self, name, number_of_molecules, box_l, list_of_first_residue_positions=None, backbone_vector=None, use_default_bond=False, reverse_residue_order = False, gen_angle=False): 944 """ 945 Creates instances of a given molecule template name into ESPResSo. 946 947 Args: 948 name ('str'): 949 Label of the molecule type to be created. 'name'. 950 951 box_l('list[float,float,float]'): list of floats with the dimensions of the box 952 953 number_of_molecules ('int'): 954 Number of molecules or peptides of type 'name' to be created. 955 956 list_of_first_residue_positions ('list', optional): 957 List of coordinates where the central bead of the first_residue_position will be created, random by default. 958 959 backbone_vector ('list' of 'float'): 960 Backbone vector of the molecule, random by default. Central beads of the residues in the 'residue_list' are placed along this vector. 961 962 use_default_bond('bool', optional): 963 Controls if a bond of type 'default' is used to bond particles with undefined bonds in the pyMBE database. 964 965 reverse_residue_order('bool', optional): 966 Creates residues in reverse sequential order than the one defined in the molecule template. Defaults to False. 967 968 Returns: 969 ('list' of 'int'): 970 List with the 'molecule_id' of the pyMBE molecule instances created into 'espresso_system'. 971 972 Notes: 973 - This function can be used to create both molecules and peptides. 974 """ 975 pmb_type = self._get_template_type(name=name, 976 allowed_types={"molecule", "peptide"}) 977 if number_of_molecules <= 0: 978 return {} 979 if list_of_first_residue_positions is not None: 980 for item in list_of_first_residue_positions: 981 if not isinstance(item, list): 982 raise ValueError("The provided input position is not a nested list. Should be a nested list with elements of 3D lists, corresponding to xyz coord.") 983 elif len(item) != 3: 984 raise ValueError("The provided input position is formatted wrong. The elements in the provided list does not have 3 coordinates, corresponding to xyz coord.") 985 986 if len(list_of_first_residue_positions) != number_of_molecules: 987 raise ValueError(f"Number of positions provided in {list_of_first_residue_positions} does not match number of molecules desired, {number_of_molecules}") 988 # Generate an arbitrary random unit vector 989 if backbone_vector is None: 990 backbone_vector = self.generate_random_points_in_a_sphere(center=[0,0,0], 991 radius=1, 992 n_samples=1, 993 on_surface=True)[0] 994 else: 995 backbone_vector = np.array(backbone_vector) 996 first_residue = True 997 molecule_tpl = self.db.get_template(pmb_type=pmb_type, 998 name=name) 999 if reverse_residue_order: 1000 residue_list = molecule_tpl.residue_list[::-1] 1001 else: 1002 residue_list = molecule_tpl.residue_list 1003 pos_index = 0 1004 molecule_ids = [] 1005 for n_mol in range(number_of_molecules): 1006 molecule_id = self.db._propose_instance_id(pmb_type=pmb_type) 1007 for residue in residue_list: 1008 if first_residue: 1009 if list_of_first_residue_positions is None: 1010 central_bead_pos = None 1011 else: 1012 central_bead_pos = [np.array(list_of_first_residue_positions[n_mol])] 1013 1014 residue_id = self.create_residue(name=residue, 1015 box_l=box_l, 1016 central_bead_position=central_bead_pos, 1017 use_default_bond= use_default_bond, 1018 backbone_vector=backbone_vector) 1019 1020 # Add molecule_id to the residue instance and all particles associated 1021 self.db._propagate_id(root_type="residue", 1022 root_id=residue_id, 1023 attribute="molecule_id", 1024 value=molecule_id) 1025 particle_ids_in_residue = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1026 attribute="residue_id", 1027 value=residue_id) 1028 prev_central_bead_id = particle_ids_in_residue[0] 1029 prev_central_bead_name = self.db.get_instance(pmb_type="particle", 1030 instance_id=prev_central_bead_id).name 1031 prev_central_bead_pos = self.db.get_instance(pmb_type="particle", 1032 instance_id=prev_central_bead_id).position 1033 # prev_central_bead_pos = espresso_system.part.by_id(prev_central_bead_id).pos 1034 first_residue = False 1035 else: 1036 1037 # Calculate the starting position of the new residue 1038 residue_tpl = self.db.get_template(pmb_type="residue", 1039 name=residue) 1040 lj_parameters = self.get_lj_parameters(particle_name1=prev_central_bead_name, 1041 particle_name2=residue_tpl.central_bead) 1042 bond_tpl = self.get_bond_template(particle_name1=prev_central_bead_name, 1043 particle_name2=residue_tpl.central_bead, 1044 use_default_bond=use_default_bond) 1045 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1046 bond_type=bond_tpl.bond_type, 1047 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1048 central_bead_pos = prev_central_bead_pos+backbone_vector*l0 1049 # Create the residue 1050 residue_id = self.create_residue(name=residue, 1051 box_l=box_l, 1052 central_bead_position=[central_bead_pos], 1053 use_default_bond= use_default_bond, 1054 backbone_vector=backbone_vector) 1055 # Add molecule_id to the residue instance and all particles associated 1056 self.db._propagate_id(root_type="residue", 1057 root_id=residue_id, 1058 attribute="molecule_id", 1059 value=molecule_id) 1060 particle_ids_in_residue = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1061 attribute="residue_id", 1062 value=residue_id) 1063 central_bead_id = particle_ids_in_residue[0] 1064 1065 # Bond the central beads of the new and previous residues 1066 self.create_bond(particle_id1=prev_central_bead_id, 1067 particle_id2=central_bead_id, 1068 use_default_bond=use_default_bond) 1069 1070 prev_central_bead_id = central_bead_id 1071 prev_central_bead_name = self.db.get_instance(pmb_type="particle", instance_id=central_bead_id).name 1072 prev_central_bead_pos =central_bead_pos 1073 # Create a Peptide or Molecule instance and register it on the pyMBE database 1074 if pmb_type == "molecule": 1075 inst = MoleculeInstance(molecule_id=molecule_id, 1076 name=name) 1077 elif pmb_type == "peptide": 1078 inst = PeptideInstance(name=name, 1079 molecule_id=molecule_id) 1080 self.db._register_instance(inst) 1081 if gen_angle: 1082 self._generate_angles_for_entity( 1083 entity_id=molecule_id, 1084 entity_id_col='molecule_id') 1085 first_residue = True 1086 pos_index+=1 1087 molecule_ids.append(molecule_id) 1088 return molecule_ids
Creates instances of a given molecule template name into ESPResSo.
Arguments:
- name ('str'): Label of the molecule type to be created. 'name'.
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
- number_of_molecules ('int'): Number of molecules or peptides of type 'name' to be created.
- list_of_first_residue_positions ('list', optional): List of coordinates where the central bead of the first_residue_position will be created, random by default.
- backbone_vector ('list' of 'float'): Backbone vector of the molecule, random by default. Central beads of the residues in the 'residue_list' are placed along this vector.
- use_default_bond('bool', optional): Controls if a bond of type 'default' is used to bond particles with undefined bonds in the pyMBE database.
- reverse_residue_order('bool', optional): Creates residues in reverse sequential order than the one defined in the molecule template. Defaults to False.
Returns:
('list' of 'int'): List with the 'molecule_id' of the pyMBE molecule instances created into 'espresso_system'.
Notes:
- This function can be used to create both molecules and peptides.
1090 def create_particle(self, name, box_l, number_of_particles, position=None, fix=False): 1091 """ 1092 Creates one or more particles in an ESPResSo system based on the particle template in the pyMBE database. 1093 1094 Args: 1095 name ('str'): 1096 Label of the particle template in the pyMBE database. 1097 1098 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1099 1100 number_of_particles ('int'): 1101 Number of particles to be created. 1102 1103 position (list of ['float','float','float'], optional): 1104 Initial positions of the particles. If not given, particles are created in random positions. Defaults to None. 1105 1106 fix ('bool', optional): 1107 Controls if the particle motion is frozen in the integrator, it is used to create rigid objects. Defaults to False. 1108 1109 Returns: 1110 ('list' of 'int'): 1111 List with the ids of the particles created into 'espresso_system'. 1112 """ 1113 if number_of_particles <=0: 1114 return [] 1115 if not self.db._has_template(name=name, pmb_type="particle"): 1116 raise ValueError(f"Particle template with name '{name}' is not defined in the pyMBE database.") 1117 1118 part_tpl = self.db.get_template(pmb_type="particle", 1119 name=name) 1120 part_state = self.db.get_template(pmb_type="particle_state", 1121 name=part_tpl.initial_state) 1122 name_state=part_state.name 1123 1124 if fix is False: 1125 fix=[fix]*3 1126 1127 created_pid_list=[] 1128 for index in range(number_of_particles): 1129 if position is None: 1130 particle_position = self.rng.random((1, 3))[0] *np.copy(box_l) 1131 else: 1132 particle_position = np.array(position[index]) 1133 1134 particle_id = self.db._propose_instance_id(pmb_type="particle") 1135 created_pid_list.append(particle_id) 1136 part_inst = ParticleInstance(name=name, 1137 particle_id=particle_id, 1138 initial_state=name_state, 1139 position=particle_position, 1140 fix=fix) 1141 self.db._register_instance(part_inst) 1142 1143 return created_pid_list
Creates one or more particles in an ESPResSo system based on the particle template in the pyMBE database.
Arguments:
- name ('str'): Label of the particle template in the pyMBE database.
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
- number_of_particles ('int'): Number of particles to be created.
- position (list of ['float','float','float'], optional): Initial positions of the particles. If not given, particles are created in random positions. Defaults to None.
- fix ('bool', optional): Controls if the particle motion is frozen in the integrator, it is used to create rigid objects. Defaults to False.
Returns:
('list' of 'int'): List with the ids of the particles created into 'espresso_system'.
1145 def create_protein(self, name, number_of_proteins, box_l, topology_dict): 1146 """ 1147 Creates one or more protein molecules in an ESPResSo system based on the 1148 protein template in the pyMBE database and a provided topology. 1149 1150 Args: 1151 name (str): 1152 Name of the protein template stored in the pyMBE database. 1153 1154 number_of_proteins (int): 1155 Number of protein molecules to generate. 1156 1157 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1158 1159 topology_dict (dict): 1160 Dictionary defining the internal structure of the protein. Expected format: 1161 {"ResidueName1": {"initial_pos": np.ndarray, 1162 "chain_id": int, 1163 "radius": float}, 1164 "ResidueName2": { ... }, 1165 ... 1166 } 1167 The '"initial_pos"' entry is required and represents the residue’s 1168 reference coordinates before shifting to the protein's center-of-mass. 1169 1170 Returns: 1171 ('list' of 'int'): 1172 List of the molecule_id of the Protein instances created into ESPResSo. 1173 1174 Notes: 1175 - Particles are created using 'create_particle()' with 'fix=True', 1176 meaning they are initially immobilized. 1177 - The function assumes all residues in 'topology_dict' correspond to 1178 particle templates already defined in the pyMBE database. 1179 - Bonds between residues are not created here; it assumes a rigid body representation of the protein. 1180 """ 1181 if number_of_proteins <= 0: 1182 return 1183 if not self.db._has_template(name=name, pmb_type="protein"): 1184 raise ValueError(f"Protein template with name '{name}' is not defined in the pyMBE database.") 1185 protein_tpl = self.db.get_template(pmb_type="protein", name=name) 1186 box_half = box_l[0] / 2.0 1187 # Create protein 1188 mol_ids = [] 1189 for _ in range(number_of_proteins): 1190 # create a molecule identifier in pyMBE 1191 molecule_id = self.db._propose_instance_id(pmb_type="protein") 1192 # place protein COM randomly 1193 protein_center = self.generate_coordinates_outside_sphere(radius=1, 1194 max_dist=box_half, 1195 n_samples=1, 1196 center=[box_half]*3)[0] 1197 residues = hf.get_residues_from_topology_dict(topology_dict=topology_dict, 1198 model=protein_tpl.model) 1199 # CREATE RESIDUES + PARTICLES 1200 for _, rdata in residues.items(): 1201 base_resname = rdata["resname"] 1202 residue_name = f"AA-{base_resname}" 1203 # residue instance ID 1204 residue_id = self.db._propose_instance_id("residue") 1205 # register ResidueInstance 1206 self.db._register_instance(ResidueInstance(name=residue_name, 1207 residue_id=residue_id, 1208 molecule_id=molecule_id)) 1209 # PARTICLE CREATION 1210 for bead_id in rdata["beads"]: 1211 bead_type = re.split(r'\d+', bead_id)[0] 1212 relative_pos = topology_dict[bead_id]["initial_pos"] 1213 absolute_pos = relative_pos + protein_center 1214 particle_id = self.create_particle(name=bead_type, 1215 box_l=box_l, 1216 number_of_particles=1, 1217 position=[absolute_pos], 1218 fix=[True,True,True])[0] 1219 # update metadata 1220 self.db._update_instance(instance_id=particle_id, 1221 pmb_type="particle", 1222 attribute="molecule_id", 1223 value=molecule_id) 1224 self.db._update_instance(instance_id=particle_id, 1225 pmb_type="particle", 1226 attribute="residue_id", 1227 value=residue_id) 1228 protein_inst = ProteinInstance(name=name, 1229 molecule_id=molecule_id) 1230 self.db._register_instance(protein_inst) 1231 mol_ids.append(molecule_id) 1232 return mol_ids
Creates one or more protein molecules in an ESPResSo system based on the protein template in the pyMBE database and a provided topology.
Arguments:
- name (str): Name of the protein template stored in the pyMBE database.
- number_of_proteins (int): Number of protein molecules to generate.
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
- topology_dict (dict): Dictionary defining the internal structure of the protein. Expected format: {"ResidueName1": {"initial_pos": np.ndarray, "chain_id": int, "radius": float}, "ResidueName2": { ... }, ... } The '"initial_pos"' entry is required and represents the residue’s reference coordinates before shifting to the protein's center-of-mass.
Returns:
('list' of 'int'): List of the molecule_id of the Protein instances created into ESPResSo.
Notes:
- Particles are created using 'create_particle()' with 'fix=True', meaning they are initially immobilized.
- The function assumes all residues in 'topology_dict' correspond to particle templates already defined in the pyMBE database.
- Bonds between residues are not created here; it assumes a rigid body representation of the protein.
1234 def create_residue(self, name, box_l, central_bead_position=None,use_default_bond=False, backbone_vector=None, gen_angle=False): 1235 """ 1236 Creates a residue into ESPResSo. 1237 1238 Args: 1239 name ('str'): 1240 Label of the residue type to be created. 1241 1242 central_bead_position ('list' of 'float'): 1243 Position of the central bead. 1244 1245 box_l('list[float,float,float]'): list of floats with the dimensions of the box 1246 1247 use_default_bond ('bool'): 1248 Switch to control if a bond of type 'default' is used to bond a particle whose bonds types are not defined in the pyMBE database. 1249 1250 backbone_vector ('list' of 'float'): 1251 Backbone vector of the molecule. All side chains are created perpendicularly to 'backbone_vector'. 1252 1253 Returns: 1254 (int): 1255 residue_id of the residue created. 1256 """ 1257 if not self.db._has_template(name=name, pmb_type="residue"): 1258 raise ValueError(f"Residue template with name '{name}' is not defined in the pyMBE database.") 1259 res_tpl = self.db.get_template(pmb_type="residue", 1260 name=name) 1261 # Assign a residue_id 1262 residue_id = self.db._propose_instance_id(pmb_type="residue") 1263 res_inst = ResidueInstance(name=name, 1264 residue_id=residue_id) 1265 self.db._register_instance(res_inst) 1266 # create the principal bead 1267 central_bead_name = res_tpl.central_bead 1268 central_bead_id = self.create_particle(name=central_bead_name, 1269 box_l=box_l, 1270 position=central_bead_position, 1271 number_of_particles = 1)[0] 1272 1273 central_bead_position = self.db.get_instance(pmb_type="particle", 1274 instance_id=central_bead_id).position 1275 # # central_bead_position=espresso_system.part.by_id(central_bead_id).pos 1276 1277 # Assigns residue_id to the central_bead particle created. 1278 self.db._update_instance(pmb_type="particle", 1279 instance_id=central_bead_id, 1280 attribute="residue_id", 1281 value=residue_id) 1282 1283 # create the lateral beads 1284 side_chain_list = res_tpl.side_chains 1285 side_chain_beads_ids = [] 1286 for side_chain_name in side_chain_list: 1287 pmb_type = self._get_template_type(name=side_chain_name, 1288 allowed_types={"particle", "residue"}) 1289 if pmb_type == 'particle': 1290 lj_parameters = self.get_lj_parameters(particle_name1=central_bead_name, 1291 particle_name2=side_chain_name) 1292 bond_tpl = self.get_bond_template(particle_name1=central_bead_name, 1293 particle_name2=side_chain_name, 1294 use_default_bond=use_default_bond) 1295 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1296 bond_type=bond_tpl.bond_type, 1297 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1298 if backbone_vector is None: 1299 bead_position=self.generate_random_points_in_a_sphere(center=central_bead_position, 1300 radius=l0, 1301 n_samples=1, 1302 on_surface=True)[0] 1303 else: 1304 bead_position=central_bead_position+self.generate_trial_perpendicular_vector(vector=np.array(backbone_vector), 1305 magnitude=l0) 1306 1307 side_bead_id = self.create_particle(name=side_chain_name, 1308 box_l=box_l, 1309 position=[bead_position], 1310 number_of_particles=1)[0] 1311 side_chain_beads_ids.append(side_bead_id) 1312 self.db._update_instance(pmb_type="particle", 1313 instance_id=side_bead_id, 1314 attribute="residue_id", 1315 value=residue_id) 1316 self.create_bond(particle_id1=central_bead_id, 1317 particle_id2=side_bead_id, 1318 use_default_bond=use_default_bond) 1319 1320 elif pmb_type == 'residue': 1321 1322 side_residue_tpl = self.db.get_template(name=side_chain_name, 1323 pmb_type=pmb_type) 1324 central_bead_side_chain = side_residue_tpl.central_bead 1325 lj_parameters = self.get_lj_parameters(particle_name1=central_bead_name, 1326 particle_name2=central_bead_side_chain) 1327 bond_tpl = self.get_bond_template(particle_name1=central_bead_name, 1328 particle_name2=central_bead_side_chain, 1329 use_default_bond=use_default_bond) 1330 l0 = hf.calculate_initial_bond_length(lj_parameters=lj_parameters, 1331 bond_type=bond_tpl.bond_type, 1332 bond_parameters=bond_tpl.get_parameters(ureg=self.units)) 1333 if backbone_vector is None: 1334 residue_position=self.generate_random_points_in_a_sphere(center=central_bead_position, 1335 radius=l0, 1336 n_samples=1, 1337 on_surface=True)[0] 1338 else: 1339 residue_position=central_bead_position+self.generate_trial_perpendicular_vector(vector=backbone_vector, 1340 magnitude=l0) 1341 side_residue_id = self.create_residue(name=side_chain_name, 1342 box_l=box_l, 1343 central_bead_position=[residue_position], 1344 use_default_bond=use_default_bond) 1345 # Find particle ids of the inner residue 1346 side_chain_beads_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1347 attribute="residue_id", 1348 value=side_residue_id) 1349 # Change the residue_id of the residue in the side chain to the one of the outer residue 1350 for particle_id in side_chain_beads_ids: 1351 self.db._update_instance(instance_id=particle_id, 1352 pmb_type="particle", 1353 attribute="residue_id", 1354 value=residue_id) 1355 # Remove the instance of the inner residue 1356 self.db.delete_instance(pmb_type="residue", 1357 instance_id=side_residue_id) 1358 self.create_bond(particle_id1=central_bead_id, 1359 particle_id2=side_chain_beads_ids[0], 1360 use_default_bond=use_default_bond) 1361 if gen_angle: 1362 self._generate_angles_for_entity( 1363 entity_id=residue_id, 1364 entity_id_col="residue_id") 1365 return residue_id
Creates a residue into ESPResSo.
Arguments:
- name ('str'): Label of the residue type to be created.
- central_bead_position ('list' of 'float'): Position of the central bead.
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
- use_default_bond ('bool'): Switch to control if a bond of type 'default' is used to bond a particle whose bonds types are not defined in the pyMBE database.
- backbone_vector ('list' of 'float'): Backbone vector of the molecule. All side chains are created perpendicularly to 'backbone_vector'.
Returns:
(int): residue_id of the residue created.
1367 def define_bond(self, bond_type, bond_parameters, particle_pairs): 1368 """ 1369 Defines bond templates for each particle pair in 'particle_pairs' in the pyMBE database. 1370 1371 Args: 1372 bond_type ('str'): 1373 label to identify the potential to model the bond. 1374 1375 bond_parameters ('dict'): 1376 parameters of the potential of the bond. 1377 1378 particle_pairs ('lst'): 1379 list of the 'names' of the 'particles' to be bonded. 1380 1381 Notes: 1382 -Currently, only HARMONIC and FENE bonds are supported. 1383 - For a HARMONIC bond the dictionary must contain the following parameters: 1384 - k ('pint.Quantity') : Magnitude of the bond. It should have units of energy/length**2 1385 using the 'pmb.units' UnitRegistry. 1386 - r_0 ('pint.Quantity') : Equilibrium bond length. It should have units of length using 1387 the 'pmb.units' UnitRegistry. 1388 - For a FENE bond the dictionary must contain the same parameters as for a HARMONIC bond and: 1389 - d_r_max ('pint.Quantity'): Maximal stretching length for FENE. It should have 1390 units of length using the 'pmb.units' UnitRegistry. Default 'None'. 1391 """ 1392 self._check_bond_inputs(bond_parameters=bond_parameters, 1393 bond_type=bond_type) 1394 parameters_expected_dimensions={"r_0": "length", 1395 "k": "energy/length**2", 1396 "d_r_max": "length"} 1397 1398 parameters_tpl = {} 1399 for key in bond_parameters.keys(): 1400 parameters_tpl[key]= PintQuantity.from_quantity(q=bond_parameters[key], 1401 expected_dimension=parameters_expected_dimensions[key], 1402 ureg=self.units) 1403 1404 bond_names=[] 1405 for particle_name1, particle_name2 in particle_pairs: 1406 1407 tpl = BondTemplate(particle_name1=particle_name1, 1408 particle_name2=particle_name2, 1409 parameters=parameters_tpl, 1410 bond_type=bond_type) 1411 tpl._make_name() 1412 if tpl.name in bond_names: 1413 raise RuntimeError(f"Bond {tpl.name} has already been defined, please check the list of particle pairs") 1414 bond_names.append(tpl.name) 1415 self.db._register_template(tpl)
Defines bond templates for each particle pair in 'particle_pairs' in the pyMBE database.
Arguments:
- bond_type ('str'): label to identify the potential to model the bond.
- bond_parameters ('dict'): parameters of the potential of the bond.
- particle_pairs ('lst'): list of the 'names' of the 'particles' to be bonded.
Notes:
-Currently, only HARMONIC and FENE bonds are supported.
- For a HARMONIC bond the dictionary must contain the following parameters:
- k ('pint.Quantity') : Magnitude of the bond. It should have units of energy/length**2 using the 'pmb.units' UnitRegistry.
- r_0 ('pint.Quantity') : Equilibrium bond length. It should have units of length using the 'pmb.units' UnitRegistry.
- For a FENE bond the dictionary must contain the same parameters as for a HARMONIC bond and:
- d_r_max ('pint.Quantity'): Maximal stretching length for FENE. It should have units of length using the 'pmb.units' UnitRegistry. Default 'None'.
1418 def define_default_bond(self, bond_type, bond_parameters): 1419 """ 1420 Defines a bond template as a "default" template in the pyMBE database. 1421 1422 Args: 1423 bond_type ('str'): 1424 label to identify the potential to model the bond. 1425 1426 bond_parameters ('dict'): 1427 parameters of the potential of the bond. 1428 1429 Notes: 1430 - Currently, only harmonic and FENE bonds are supported. 1431 """ 1432 self._check_bond_inputs(bond_parameters=bond_parameters, 1433 bond_type=bond_type) 1434 parameters_expected_dimensions={"r_0": "length", 1435 "k": "energy/length**2", 1436 "d_r_max": "length"} 1437 parameters_tpl = {} 1438 for key in bond_parameters.keys(): 1439 parameters_tpl[key]= PintQuantity.from_quantity(q=bond_parameters[key], 1440 expected_dimension=parameters_expected_dimensions[key], 1441 ureg=self.units) 1442 tpl = BondTemplate(parameters=parameters_tpl, 1443 bond_type=bond_type) 1444 tpl.name = "default" 1445 self.db._register_template(tpl)
Defines a bond template as a "default" template in the pyMBE database.
Arguments:
- bond_type ('str'): label to identify the potential to model the bond.
- bond_parameters ('dict'): parameters of the potential of the bond.
Notes:
- Currently, only harmonic and FENE bonds are supported.
1447 def define_angular_potential(self, angle_type, angle_parameters, particle_triplets): 1448 """ 1449 Defines angle potential templates for each particle triplet in `particle_triplets`. 1450 1451 Args: 1452 angle_type ('str'): 1453 Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine". 1454 1455 angle_parameters ('dict'): 1456 Parameters of the angle potential. Must contain: 1457 - "k" ('pint.Quantity'): Bending stiffness with dimensions of energy. 1458 - "phi_0" ('float'): Equilibrium angle in radians. 1459 1460 particle_triplets ('list[tuple[str,str,str]]'): 1461 List of (side_particle1, central_particle, side_particle2) triplets. 1462 """ 1463 valid_angle_types = ["harmonic", "cosine", "harmonic_cosine"] 1464 if angle_type not in valid_angle_types: 1465 raise NotImplementedError(f"Angle potential type '{angle_type}' currently not implemented in pyMBE, accepted types are {valid_angle_types}") 1466 1467 if "k" not in angle_parameters: 1468 raise ValueError("Magnitude of the angle potential (k) is missing") 1469 if "phi_0" not in angle_parameters: 1470 raise ValueError("Equilibrium angle (phi_0) is missing") 1471 1472 parameters_tpl = {"k": PintQuantity.from_quantity(q=angle_parameters["k"], 1473 expected_dimension="energy", 1474 ureg=self.units), 1475 "phi_0": PintQuantity.from_quantity(q=angle_parameters["phi_0"], 1476 expected_dimension="dimensionless", 1477 ureg=self.units),} 1478 angle_names = [] 1479 for side1, central, side2 in particle_triplets: 1480 tpl = AngleTemplate(side_particle1=side1, 1481 central_particle=central, 1482 side_particle2=side2, 1483 parameters=parameters_tpl, 1484 angle_type=angle_type) 1485 tpl._make_name() 1486 if tpl.name in angle_names: 1487 raise RuntimeError(f"Angle {tpl.name} has already been defined, please check the list of particle triplets") 1488 angle_names.append(tpl.name) 1489 self.db._register_template(tpl)
Defines angle potential templates for each particle triplet in particle_triplets.
Arguments:
- angle_type ('str'): Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine".
- angle_parameters ('dict'): Parameters of the angle potential. Must contain:
- "k" ('pint.Quantity'): Bending stiffness with dimensions of energy.
- "phi_0" ('float'): Equilibrium angle in radians.
- particle_triplets ('list[tuple[str,str,str]]'): List of (side_particle1, central_particle, side_particle2) triplets.
1491 def define_default_angular_potential(self, angle_type, angle_parameters): 1492 """ 1493 Defines an angle template as a "default" template in the pyMBE database. 1494 1495 Args: 1496 angle_type ('str'): 1497 Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine". 1498 1499 angle_parameters ('dict'): 1500 Parameters of the angle potential (k, phi_0). 1501 """ 1502 valid_angle_types = ["harmonic", "cosine", "harmonic_cosine"] 1503 if angle_type not in valid_angle_types: 1504 raise NotImplementedError(f"Angle potential type '{angle_type}' currently not implemented in pyMBE, accepted types are {valid_angle_types}") 1505 if "k" not in angle_parameters: 1506 raise ValueError("Magnitude of the angle potential (k) is missing") 1507 if "phi_0" not in angle_parameters: 1508 raise ValueError("Equilibrium angle (phi_0) is missing") 1509 parameters_tpl = {"k": PintQuantity.from_quantity(q=angle_parameters["k"], 1510 expected_dimension="energy", 1511 ureg=self.units), 1512 "phi_0": PintQuantity.from_quantity(q=angle_parameters["phi_0"], 1513 expected_dimension="dimensionless", 1514 ureg=self.units),} 1515 tpl = AngleTemplate(parameters=parameters_tpl, 1516 angle_type=angle_type) 1517 tpl.name = "default" 1518 self.db._register_template(tpl)
Defines an angle template as a "default" template in the pyMBE database.
Arguments:
- angle_type ('str'): Type of angle potential. Supported: "harmonic", "cosine", "harmonic_cosine".
- angle_parameters ('dict'): Parameters of the angle potential (k, phi_0).
1520 def create_angular_potential(self, particle_id1, particle_id2, particle_id3, use_default_angle=False): 1521 """ 1522 Creates an angle between three particle instances in an ESPResSo system 1523 and registers it in the pyMBE database. 1524 1525 Args: 1526 particle_id1 ('int'): ID of the first side particle. 1527 particle_id2 ('int'): ID of the central particle. 1528 particle_id3 ('int'): ID of the second side particle. 1529 use_default_angle ('bool', optional): If True, use the default angle if no specific one is found. 1530 """ 1531 particle_inst_1 = self.db.get_instance(pmb_type="particle", instance_id=particle_id1) 1532 particle_inst_2 = self.db.get_instance(pmb_type="particle", instance_id=particle_id2) 1533 particle_inst_3 = self.db.get_instance(pmb_type="particle", instance_id=particle_id3) 1534 1535 # Verify that bonds exist between side particles and central particle 1536 bond_instances = self.db.get_instances(pmb_type="bond") 1537 bonded_pairs = set() 1538 for bond in bond_instances.values(): 1539 pair = frozenset([bond.particle_id1, bond.particle_id2]) 1540 bonded_pairs.add(pair) 1541 if frozenset([particle_id1, particle_id2]) not in bonded_pairs: 1542 raise ValueError(f"Cannot create angle: no bond exists between particle {particle_id1} and central particle {particle_id2}.") 1543 if frozenset([particle_id3, particle_id2]) not in bonded_pairs: 1544 raise ValueError(f"Cannot create angle: no bond exists between particle {particle_id3} and central particle {particle_id2}.") 1545 1546 angle_tpl = self.get_angle_template(side_name1=particle_inst_1.name, 1547 central_name=particle_inst_2.name, 1548 side_name2=particle_inst_3.name, 1549 use_default_angle=use_default_angle) 1550 angle_id = self.db._propose_instance_id(pmb_type="angle") 1551 pmb_angle_instance = AngleInstance(angle_id=angle_id, 1552 name=angle_tpl.name, 1553 particle_id1=particle_id1, 1554 particle_id2=particle_id2, 1555 particle_id3=particle_id3) 1556 self.db._register_instance(instance=pmb_angle_instance)
Creates an angle between three particle instances in an ESPResSo system and registers it in the pyMBE database.
Arguments:
- particle_id1 ('int'): ID of the first side particle.
- particle_id2 ('int'): ID of the central particle.
- particle_id3 ('int'): ID of the second side particle.
- use_default_angle ('bool', optional): If True, use the default angle if no specific one is found.
1558 def get_angle_template(self, side_name1, central_name, side_name2, use_default_angle=False): 1559 """ 1560 Retrieves an angle template connecting three particle templates. 1561 1562 Args: 1563 side_name1 ('str'): Name of the first side particle. 1564 central_name ('str'): Name of the central particle. 1565 side_name2 ('str'): Name of the second side particle. 1566 use_default_angle ('bool', optional): If True, fall back to the default angle template. 1567 1568 Returns: 1569 ('AngleTemplate'): The matching angle template. 1570 """ 1571 angle_key = AngleTemplate.make_angle_key(side1=side_name1, central=central_name, side2=side_name2) 1572 try: 1573 return self.db.get_template(name=angle_key, pmb_type="angle") 1574 except ValueError: 1575 pass 1576 1577 if use_default_angle: 1578 return self.db.get_template(name="default", pmb_type="angle") 1579 1580 raise ValueError(f"No angle template found for '{side_name1}-{central_name}-{side_name2}', and default angles are deactivated.")
Retrieves an angle template connecting three particle templates.
Arguments:
- side_name1 ('str'): Name of the first side particle.
- central_name ('str'): Name of the central particle.
- side_name2 ('str'): Name of the second side particle.
- use_default_angle ('bool', optional): If True, fall back to the default angle template.
Returns:
('AngleTemplate'): The matching angle template.
1629 def define_hydrogel(self, name, node_map, chain_map): 1630 """ 1631 Defines a hydrogel template in the pyMBE database. 1632 1633 Args: 1634 name ('str'): 1635 Unique label that identifies the 'hydrogel'. 1636 1637 node_map ('list of dict'): 1638 [{"particle_name": , "lattice_index": }, ... ] 1639 1640 chain_map ('list of dict'): 1641 [{"node_start": , "node_end": , "residue_list": , ... ] 1642 """ 1643 # Sanity tests 1644 node_indices = {tuple(entry['lattice_index']) for entry in node_map} 1645 chain_map_connectivity = set() 1646 for entry in chain_map: 1647 start = self.lattice_builder.node_labels[entry['node_start']] 1648 end = self.lattice_builder.node_labels[entry['node_end']] 1649 chain_map_connectivity.add((start,end)) 1650 if self.lattice_builder.lattice.connectivity != chain_map_connectivity: 1651 raise ValueError("Incomplete hydrogel: A diamond lattice must contain correct 16 lattice index pairs") 1652 diamond_indices = {tuple(row) for row in self.lattice_builder.lattice.indices} 1653 if node_indices != diamond_indices: 1654 raise ValueError(f"Incomplete hydrogel: A diamond lattice must contain exactly 8 lattice indices, {diamond_indices} ") 1655 # Register information in the pyMBE database 1656 nodes=[] 1657 for entry in node_map: 1658 nodes.append(HydrogelNode(particle_name=entry["particle_name"], 1659 lattice_index=entry["lattice_index"])) 1660 chains=[] 1661 for chain in chain_map: 1662 chains.append(HydrogelChain(node_start=chain["node_start"], 1663 node_end=chain["node_end"], 1664 molecule_name=chain["molecule_name"])) 1665 tpl = HydrogelTemplate(name=name, 1666 node_map=nodes, 1667 chain_map=chains) 1668 self.db._register_template(tpl)
Defines a hydrogel template in the pyMBE database.
Arguments:
- name ('str'): Unique label that identifies the 'hydrogel'.
- node_map ('list of dict'): [{"particle_name": , "lattice_index": }, ... ]
- chain_map ('list of dict'): [{"node_start": , "node_end": , "residue_list": , ... ]
1670 def define_molecule(self, name, residue_list): 1671 """ 1672 Defines a molecule template in the pyMBE database. 1673 1674 Args: 1675 name('str'): 1676 Unique label that identifies the 'molecule'. 1677 1678 residue_list ('list' of 'str'): 1679 List of the 'name's of the 'residue's in the sequence of the 'molecule'. 1680 """ 1681 tpl = MoleculeTemplate(name=name, 1682 residue_list=residue_list) 1683 self.db._register_template(tpl)
Defines a molecule template in the pyMBE database.
Arguments:
- name('str'): Unique label that identifies the 'molecule'.
- residue_list ('list' of 'str'): List of the 'name's of the 'residue's in the sequence of the 'molecule'.
1685 def define_monoprototic_acidbase_reaction(self, particle_name, pka, acidity, metadata=None): 1686 """ 1687 Defines an acid-base reaction for a monoprototic particle in the pyMBE database. 1688 1689 Args: 1690 particle_name ('str'): 1691 Unique label that identifies the particle template. 1692 1693 pka ('float'): 1694 pka-value of the acid or base. 1695 1696 acidity ('str'): 1697 Identifies whether if the particle is 'acidic' or 'basic'. 1698 1699 metadata ('dict', optional): 1700 Additional information to be stored in the reaction. Defaults to None. 1701 """ 1702 supported_acidities = ["acidic", "basic"] 1703 if acidity not in supported_acidities: 1704 raise ValueError(f"Unsupported acidity '{acidity}' for particle '{particle_name}'. Supported acidities are {supported_acidities}.") 1705 reaction_type = "monoprotic" 1706 if acidity == "basic": 1707 reaction_type += "_base" 1708 else: 1709 reaction_type += "_acid" 1710 reaction = Reaction(participants=[ReactionParticipant(particle_name=particle_name, 1711 state_name=f"{particle_name}H", 1712 coefficient=-1), 1713 ReactionParticipant(particle_name=particle_name, 1714 state_name=f"{particle_name}", 1715 coefficient=1)], 1716 reaction_type=reaction_type, 1717 pK=pka, 1718 metadata=metadata) 1719 self.db._register_reaction(reaction)
Defines an acid-base reaction for a monoprototic particle in the pyMBE database.
Arguments:
- particle_name ('str'): Unique label that identifies the particle template.
- pka ('float'): pka-value of the acid or base.
- acidity ('str'): Identifies whether if the particle is 'acidic' or 'basic'.
- metadata ('dict', optional): Additional information to be stored in the reaction. Defaults to None.
1721 def define_monoprototic_particle_states(self, particle_name, acidity): 1722 """ 1723 Defines particle states for a monoprotonic particle template including the charges in each of its possible states. 1724 1725 Args: 1726 particle_name ('str'): 1727 Unique label that identifies the particle template. 1728 1729 acidity ('str'): 1730 Identifies whether the particle is 'acidic' or 'basic'. 1731 """ 1732 acidity_valid_keys = ['acidic', 'basic'] 1733 if not pd.isna(acidity): 1734 if acidity not in acidity_valid_keys: 1735 raise ValueError(f"Acidity {acidity} provided for particle name {particle_name} is not supported. Valid keys are: {acidity_valid_keys}") 1736 if acidity == "acidic": 1737 states = [{"name": f"{particle_name}H", "z": 0}, 1738 {"name": f"{particle_name}", "z": -1}] 1739 1740 elif acidity == "basic": 1741 states = [{"name": f"{particle_name}H", "z": 1}, 1742 {"name": f"{particle_name}", "z": 0}] 1743 self.define_particle_states(particle_name=particle_name, 1744 states=states)
Defines particle states for a monoprotonic particle template including the charges in each of its possible states.
Arguments:
- particle_name ('str'): Unique label that identifies the particle template.
- acidity ('str'): Identifies whether the particle is 'acidic' or 'basic'.
1746 def define_particle(self, name, sigma, epsilon, z=0, acidity=pd.NA, pka=pd.NA, cutoff=pd.NA, offset=pd.NA): 1747 """ 1748 Defines a particle template in the pyMBE database. 1749 1750 Args: 1751 name('str'): 1752 Unique label that identifies this particle type. 1753 1754 sigma('pint.Quantity'): 1755 Sigma parameter used to set up Lennard-Jones interactions for this particle type. 1756 1757 epsilon('pint.Quantity'): 1758 Epsilon parameter used to setup Lennard-Jones interactions for this particle tipe. 1759 1760 z('int', optional): 1761 Permanent charge number of this particle type. Defaults to 0. 1762 1763 acidity('str', optional): 1764 Identifies whether if the particle is 'acidic' or 'basic', used to setup constant pH simulations. Defaults to pd.NA. 1765 1766 pka('float', optional): 1767 If 'particle' is an acid or a base, it defines its pka-value. Defaults to pd.NA. 1768 1769 cutoff('pint.Quantity', optional): 1770 Cutoff parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA. 1771 1772 offset('pint.Quantity', optional): 1773 Offset parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA. 1774 1775 Notes: 1776 - 'sigma', 'cutoff' and 'offset' must have a dimensitonality of '[length]' and should be defined using pmb.units. 1777 - 'epsilon' must have a dimensitonality of '[energy]' and should be defined using pmb.units. 1778 - 'cutoff' defaults to '2**(1./6.) reduced_length'. 1779 - 'offset' defaults to 0. 1780 - For more information on 'sigma', 'epsilon', 'cutoff' and 'offset' check 'pmb.setup_lj_interactions()'. 1781 """ 1782 # If 'cutoff' and 'offset' are not defined, default them to the following values 1783 if pd.isna(cutoff): 1784 cutoff=self.units.Quantity(2**(1./6.), "reduced_length") 1785 if pd.isna(offset): 1786 offset=self.units.Quantity(0, "reduced_length") 1787 # Define particle states 1788 if acidity is pd.NA: 1789 states = [{"name": f"{name}", "z": z}] 1790 self.define_particle_states(particle_name=name, 1791 states=states) 1792 initial_state = name 1793 else: 1794 self.define_monoprototic_particle_states(particle_name=name, 1795 acidity=acidity) 1796 initial_state = f"{name}H" 1797 if pka is not pd.NA: 1798 self.define_monoprototic_acidbase_reaction(particle_name=name, 1799 acidity=acidity, 1800 pka=pka) 1801 tpl = ParticleTemplate(name=name, 1802 sigma=PintQuantity.from_quantity(q=sigma, expected_dimension="length", ureg=self.units), 1803 epsilon=PintQuantity.from_quantity(q=epsilon, expected_dimension="energy", ureg=self.units), 1804 cutoff=PintQuantity.from_quantity(q=cutoff, expected_dimension="length", ureg=self.units), 1805 offset=PintQuantity.from_quantity(q=offset, expected_dimension="length", ureg=self.units), 1806 initial_state=initial_state) 1807 self.db._register_template(tpl)
Defines a particle template in the pyMBE database.
Arguments:
- name('str'): Unique label that identifies this particle type.
- sigma('pint.Quantity'): Sigma parameter used to set up Lennard-Jones interactions for this particle type.
- epsilon('pint.Quantity'): Epsilon parameter used to setup Lennard-Jones interactions for this particle tipe.
- z('int', optional): Permanent charge number of this particle type. Defaults to 0.
- acidity('str', optional): Identifies whether if the particle is 'acidic' or 'basic', used to setup constant pH simulations. Defaults to pd.NA.
- pka('float', optional): If 'particle' is an acid or a base, it defines its pka-value. Defaults to pd.NA.
- cutoff('pint.Quantity', optional): Cutoff parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA.
- offset('pint.Quantity', optional): Offset parameter used to set up Lennard-Jones interactions for this particle type. Defaults to pd.NA.
Notes:
- 'sigma', 'cutoff' and 'offset' must have a dimensitonality of '[length]' and should be defined using pmb.units.
- 'epsilon' must have a dimensitonality of '[energy]' and should be defined using pmb.units.
- 'cutoff' defaults to '2**(1./6.) reduced_length'.
- 'offset' defaults to 0.
- For more information on 'sigma', 'epsilon', 'cutoff' and 'offset' check 'pmb.setup_lj_interactions()'.
1809 def define_particle_states(self, particle_name, states): 1810 """ 1811 Define the chemical states of an existing particle template. 1812 1813 Args: 1814 particle_name ('str'): 1815 Name of a particle template. 1816 1817 states ('list' of 'dict'): 1818 List of dictionaries defining the particle states. Each dictionary 1819 must contain: 1820 - 'name' ('str'): Name of the particle state (e.g. '"H"', '"-"', 1821 '"neutral"'). 1822 - 'z' ('int'): Charge number of the particle in this state. 1823 Example: 1824 states = [{"name": "AH", "z": 0}, # protonated 1825 {"name": "A-", "z": -1}] # deprotonated 1826 Notes: 1827 - Each state is assigned a unique Espresso 'es_type' automatically. 1828 - Chemical reactions (e.g. acid–base equilibria) are **not** created by 1829 this method and must be defined separately (e.g. via 1830 'set_particle_acidity()' or custom reaction definitions). 1831 - Particles without explicitly defined states are assumed to have a 1832 single, implicit state with their default charge. 1833 """ 1834 for s in states: 1835 state = ParticleStateTemplate(particle_name=particle_name, 1836 name=s["name"], 1837 z=s["z"], 1838 es_type=self.propose_unused_type()) 1839 self.db._register_template(state)
Define the chemical states of an existing particle template.
Arguments:
- particle_name ('str'): Name of a particle template.
- states ('list' of 'dict'): List of dictionaries defining the particle states. Each dictionary
must contain:
- 'name' ('str'): Name of the particle state (e.g. '"H"', '"-"', '"neutral"').
- 'z' ('int'): Charge number of the particle in this state. Example: states = [{"name": "AH", "z": 0}, # protonated {"name": "A-", "z": -1}] # deprotonated
Notes:
- Each state is assigned a unique Espresso 'es_type' automatically.
- Chemical reactions (e.g. acid–base equilibria) are not created by this method and must be defined separately (e.g. via 'set_particle_acidity()' or custom reaction definitions).
- Particles without explicitly defined states are assumed to have a single, implicit state with their default charge.
1841 def define_peptide(self, name, sequence, model): 1842 """ 1843 Defines a peptide template in the pyMBE database. 1844 1845 Args: 1846 name ('str'): 1847 Unique label that identifies the peptide. 1848 1849 sequence ('str'): 1850 Sequence of the peptide. 1851 1852 model ('str'): 1853 Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported. 1854 """ 1855 valid_keys = ['1beadAA','2beadAA'] 1856 if model not in valid_keys: 1857 raise ValueError('Invalid label for the peptide model, please choose between 1beadAA or 2beadAA') 1858 clean_sequence = hf.protein_sequence_parser(sequence=sequence) 1859 residue_list = self._get_residue_list_from_sequence(sequence=clean_sequence) 1860 tpl = PeptideTemplate(name=name, 1861 residue_list=residue_list, 1862 model=model, 1863 sequence=sequence) 1864 self.db._register_template(tpl)
Defines a peptide template in the pyMBE database.
Arguments:
- name ('str'): Unique label that identifies the peptide.
- sequence ('str'): Sequence of the peptide.
- model ('str'): Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported.
1866 def define_protein(self, name, sequence, model): 1867 """ 1868 Defines a protein template in the pyMBE database. 1869 1870 Args: 1871 name ('str'): 1872 Unique label that identifies the protein. 1873 1874 sequence ('str'): 1875 Sequence of the protein. 1876 1877 model ('string'): 1878 Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported. 1879 1880 Notes: 1881 - Currently, only 'lj_setup_mode="wca"' is supported. This corresponds to setting up the WCA potential. 1882 """ 1883 valid_model_keys = ['1beadAA','2beadAA'] 1884 if model not in valid_model_keys: 1885 raise ValueError('Invalid key for the protein model, supported models are {valid_model_keys}') 1886 1887 residue_list = self._get_residue_list_from_sequence(sequence=sequence) 1888 tpl = ProteinTemplate(name=name, 1889 model=model, 1890 residue_list=residue_list, 1891 sequence=sequence) 1892 self.db._register_template(tpl)
Defines a protein template in the pyMBE database.
Arguments:
- name ('str'): Unique label that identifies the protein.
- sequence ('str'): Sequence of the protein.
- model ('string'): Model name. Currently only models with 1 bead '1beadAA' or with 2 beads '2beadAA' per amino acid are supported.
Notes:
- Currently, only 'lj_setup_mode="wca"' is supported. This corresponds to setting up the WCA potential.
1894 def define_residue(self, name, central_bead, side_chains): 1895 """ 1896 Defines a residue template in the pyMBE database. 1897 1898 Args: 1899 name ('str'): 1900 Unique label that identifies the residue. 1901 1902 central_bead ('str'): 1903 'name' of the 'particle' to be placed as central_bead of the residue. 1904 1905 side_chains('list' of 'str'): 1906 List of 'name's of the pmb_objects to be placed as side_chains of the residue. Currently, only pyMBE objects of type 'particle' or 'residue' are supported. 1907 """ 1908 tpl = ResidueTemplate(name=name, 1909 central_bead=central_bead, 1910 side_chains=side_chains) 1911 self.db._register_template(tpl)
Defines a residue template in the pyMBE database.
Arguments:
- name ('str'): Unique label that identifies the residue.
- central_bead ('str'): 'name' of the 'particle' to be placed as central_bead of the residue.
- side_chains('list' of 'str'): List of 'name's of the pmb_objects to be placed as side_chains of the residue. Currently, only pyMBE objects of type 'particle' or 'residue' are supported.
1914 def delete_instances_in_system(self, instance_id, pmb_type): 1915 """ 1916 Deletes the instance with instance_id from the ESPResSo system. 1917 Related assembly, molecule, residue, particles and bond instances will also be deleted from the pyMBE dataframe. 1918 1919 Args: 1920 instance_id ('int'): 1921 id of the assembly to be deleted. 1922 1923 pmb_type ('str'): 1924 the instance type to be deleted. 1925 1926 espresso_system ('espressomd.system.System'): 1927 Instance of a system class from espressomd library. 1928 """ 1929 if pmb_type == "particle": 1930 instance_identifier = "particle_id" 1931 elif pmb_type == "residue": 1932 instance_identifier = "residue_id" 1933 elif pmb_type in self.db._molecule_like_types: 1934 instance_identifier = "molecule_id" 1935 elif pmb_type in self.db._assembly_like_types: 1936 instance_identifier = "assembly_id" 1937 particle_ids = self.db._find_instance_ids_by_attribute(pmb_type="particle", 1938 attribute=instance_identifier, 1939 value=instance_id) 1940 self._delete_particles_from_engine(particle_ids=particle_ids) 1941 self.db.delete_instance(pmb_type=pmb_type, 1942 instance_id=instance_id)
Deletes the instance with instance_id from the ESPResSo system. Related assembly, molecule, residue, particles and bond instances will also be deleted from the pyMBE dataframe.
Arguments:
- instance_id ('int'): id of the assembly to be deleted.
- pmb_type ('str'): the instance type to be deleted.
- espresso_system ('espressomd.system.System'): Instance of a system class from espressomd library.
1944 def determine_reservoir_concentrations(self, pH_res, c_salt_res, activity_coefficient_monovalent_pair, max_number_sc_runs=200): 1945 """ 1946 Determines ionic concentrations in the reservoir at fixed pH and salt concentration. 1947 1948 Args: 1949 pH_res ('float'): 1950 Target pH value in the reservoir. 1951 1952 c_salt_res ('pint.Quantity'): 1953 Concentration of monovalent salt (e.g., NaCl) in the reservoir. 1954 1955 activity_coefficient_monovalent_pair ('callable'): 1956 Function returning the activity coefficient of a monovalent ion pair 1957 as a function of ionic strength: 1958 'gamma = activity_coefficient_monovalent_pair(I)'. 1959 1960 max_number_sc_runs ('int', optional): 1961 Maximum number of self-consistent iterations allowed before 1962 convergence is enforced. Defaults to 200. 1963 1964 Returns: 1965 tuple: 1966 (cH_res, cOH_res, cNa_res, cCl_res) 1967 - cH_res ('pint.Quantity'): Concentration of H⁺ ions. 1968 - cOH_res ('pint.Quantity'): Concentration of OH⁻ ions. 1969 - cNa_res ('pint.Quantity'): Concentration of Na⁺ ions. 1970 - cCl_res ('pint.Quantity'): Concentration of Cl⁻ ions. 1971 1972 Notess: 1973 - The algorithm enforces electroneutrality in the reservoir. 1974 - Water autodissociation is included via the equilibrium constant 'Kw'. 1975 - Non-ideal effects enter through activity coefficients depending on 1976 ionic strength. 1977 - The implementation follows the self-consistent scheme described in 1978 Landsgesell (PhD thesis, Sec. 5.3, doi:10.18419/opus-10831), adapted 1979 from the original code (doi:10.18419/darus-2237). 1980 """ 1981 cH_res, cOH_res, cNa_res, cCl_res = self.simulation_engine.determine_reservoir_concentrations( pH_res, c_salt_res, activity_coefficient_monovalent_pair, max_number_sc_runs) 1982 return cH_res, cOH_res, cNa_res, cCl_res
Determines ionic concentrations in the reservoir at fixed pH and salt concentration.
Arguments:
- pH_res ('float'): Target pH value in the reservoir.
- c_salt_res ('pint.Quantity'): Concentration of monovalent salt (e.g., NaCl) in the reservoir.
- activity_coefficient_monovalent_pair ('callable'): Function returning the activity coefficient of a monovalent ion pair as a function of ionic strength: 'gamma = activity_coefficient_monovalent_pair(I)'.
- max_number_sc_runs ('int', optional): Maximum number of self-consistent iterations allowed before convergence is enforced. Defaults to 200.
Returns:
tuple: (cH_res, cOH_res, cNa_res, cCl_res) - cH_res ('pint.Quantity'): Concentration of H⁺ ions. - cOH_res ('pint.Quantity'): Concentration of OH⁻ ions. - cNa_res ('pint.Quantity'): Concentration of Na⁺ ions. - cCl_res ('pint.Quantity'): Concentration of Cl⁻ ions.
Notess:
- The algorithm enforces electroneutrality in the reservoir.
- Water autodissociation is included via the equilibrium constant 'Kw'.
- Non-ideal effects enter through activity coefficients depending on ionic strength.
- The implementation follows the self-consistent scheme described in Landsgesell (PhD thesis, Sec. 5.3, doi:10.18419/opus-10831), adapted from the original code (doi:10.18419/darus-2237).
1984 def enable_motion_of_rigid_object(self, instance_id, pmb_type): 1985 """ 1986 Enables translational and rotational motion of a rigid pyMBE object instance 1987 in an ESPResSo system.This method creates a rigid-body center particle at the center of mass of 1988 the specified pyMBE object and attaches all constituent particles to it 1989 using ESPResSo virtual sites. The resulting rigid object can translate and 1990 rotate as a single body. 1991 1992 Args: 1993 instance_id ('int'): 1994 Instance ID of the pyMBE object whose rigid-body motion is enabled. 1995 1996 pmb_type ('str'): 1997 pyMBE object type of the instance (e.g. '"molecule"', '"peptide"', 1998 '"protein"', or any assembly-like type). 1999 2000 Notess: 2001 - This method requires ESPResSo to be compiled with the following 2002 features enabled: 2003 - '"VIRTUAL_SITES_RELATIVE"' 2004 - '"MASS"' 2005 - A new ESPResSo particle is created to represent the rigid-body center. 2006 - The mass of the rigid-body center is set to the number of particles 2007 belonging to the object. 2008 - The rotational inertia tensor is approximated from the squared 2009 distances of the particles to the center of mass. 2010 """ 2011 self.simulation_engine.enable_motion_of_rigid_object(instance_id, pmb_type)
Enables translational and rotational motion of a rigid pyMBE object instance in an ESPResSo system.This method creates a rigid-body center particle at the center of mass of the specified pyMBE object and attaches all constituent particles to it using ESPResSo virtual sites. The resulting rigid object can translate and rotate as a single body.
Arguments:
- instance_id ('int'): Instance ID of the pyMBE object whose rigid-body motion is enabled.
- pmb_type ('str'): pyMBE object type of the instance (e.g. '"molecule"', '"peptide"', '"protein"', or any assembly-like type).
Notess:
- This method requires ESPResSo to be compiled with the following features enabled:
- '"VIRTUAL_SITES_RELATIVE"'
- '"MASS"'
- A new ESPResSo particle is created to represent the rigid-body center.
- The mass of the rigid-body center is set to the number of particles belonging to the object.
- The rotational inertia tensor is approximated from the squared distances of the particles to the center of mass.
2013 def generate_coordinates_outside_sphere(self, center, radius, max_dist, n_samples): 2014 """ 2015 Generates random coordinates outside a sphere and inside a larger bounding sphere. 2016 2017 Args: 2018 center ('array-like'): 2019 Coordinates of the center of the spheres. 2020 2021 radius ('float'): 2022 Radius of the inner exclusion sphere. Must be positive. 2023 2024 max_dist ('float'): 2025 Radius of the outer sampling sphere. Must be larger than 'radius'. 2026 2027 n_samples ('int'): 2028 Number of coordinates to generate. 2029 2030 Returns: 2031 'list' of 'numpy.ndarray': 2032 List of coordinates lying outside the inner sphere and inside the 2033 outer sphere. 2034 2035 Notess: 2036 - Points are uniformly sampled inside a sphere of radius 'max_dist' centered at 'center' 2037 and only those with a distance greater than or equal to 'radius' from the center are retained. 2038 """ 2039 if not radius > 0: 2040 raise ValueError (f'The value of {radius} must be a positive value') 2041 if not radius < max_dist: 2042 raise ValueError(f'The min_dist ({radius} must be lower than the max_dist ({max_dist}))') 2043 coord_list = [] 2044 counter = 0 2045 while counter<n_samples: 2046 coord = self.generate_random_points_in_a_sphere(center=center, 2047 radius=max_dist, 2048 n_samples=1)[0] 2049 if np.linalg.norm(coord-np.asarray(center))>=radius: 2050 coord_list.append (coord) 2051 counter += 1 2052 return coord_list
Generates random coordinates outside a sphere and inside a larger bounding sphere.
Arguments:
- center ('array-like'): Coordinates of the center of the spheres.
- radius ('float'): Radius of the inner exclusion sphere. Must be positive.
- max_dist ('float'): Radius of the outer sampling sphere. Must be larger than 'radius'.
- n_samples ('int'): Number of coordinates to generate.
Returns:
'list' of 'numpy.ndarray': List of coordinates lying outside the inner sphere and inside the outer sphere.
Notess:
- Points are uniformly sampled inside a sphere of radius 'max_dist' centered at 'center' and only those with a distance greater than or equal to 'radius' from the center are retained.
2054 def generate_random_points_in_a_sphere(self, center, radius, n_samples, on_surface=False): 2055 """ 2056 Generates uniformly distributed random points inside or on the surface of a sphere. 2057 2058 Args: 2059 center ('array-like'): 2060 Coordinates of the center of the sphere. 2061 2062 radius ('float'): 2063 Radius of the sphere. 2064 2065 n_samples ('int'): 2066 Number of sample points to generate. 2067 2068 on_surface ('bool', optional): 2069 If True, points are uniformly sampled on the surface of the sphere. 2070 If False, points are uniformly sampled within the sphere volume. 2071 Defaults to False. 2072 2073 Returns: 2074 'numpy.ndarray': 2075 Array of shape '(n_samples, d)' containing the generated coordinates, 2076 where 'd' is the dimensionality of 'center'. 2077 Notes: 2078 - Points are sampled in a space whose dimensionality is inferred 2079 from the length of 'center'. 2080 """ 2081 # initial values 2082 center=np.array(center) 2083 d = center.shape[0] 2084 # sample n_samples points in d dimensions from a standard normal distribution 2085 samples = self.rng.normal(size=(n_samples, d)) 2086 # make the samples lie on the surface of the unit hypersphere 2087 normalize_radii = np.linalg.norm(samples, axis=1)[:, np.newaxis] 2088 samples /= normalize_radii 2089 if not on_surface: 2090 # make the samples lie inside the hypersphere with the correct density 2091 uniform_points = self.rng.uniform(size=n_samples)[:, np.newaxis] 2092 new_radii = np.power(uniform_points, 1/d) 2093 samples *= new_radii 2094 # scale the points to have the correct radius and center 2095 samples = samples * radius + center 2096 return samples
Generates uniformly distributed random points inside or on the surface of a sphere.
Arguments:
- center ('array-like'): Coordinates of the center of the sphere.
- radius ('float'): Radius of the sphere.
- n_samples ('int'): Number of sample points to generate.
- on_surface ('bool', optional): If True, points are uniformly sampled on the surface of the sphere. If False, points are uniformly sampled within the sphere volume. Defaults to False.
Returns:
'numpy.ndarray': Array of shape '(n_samples, d)' containing the generated coordinates, where 'd' is the dimensionality of 'center'.
Notes:
- Points are sampled in a space whose dimensionality is inferred from the length of 'center'.
2098 def generate_trial_perpendicular_vector(self,vector,magnitude): 2099 """ 2100 Generates a random vector perpendicular to a given vector. 2101 2102 Args: 2103 vector ('array-like'): 2104 Reference vector to which the generated vector will be perpendicular. 2105 2106 magnitude ('float'): 2107 Desired magnitude of the perpendicular vector. 2108 2109 Returns: 2110 'numpy.ndarray': 2111 Vector orthogonal to 'vector' with norm equal to 'magnitude'. 2112 """ 2113 np_vec = np.array(vector) 2114 if np.all(np_vec == 0): 2115 raise ValueError('Zero vector') 2116 np_vec /= np.linalg.norm(np_vec) 2117 # Generate a random vector 2118 random_vector = self.generate_random_points_in_a_sphere(radius=1, 2119 center=[0,0,0], 2120 n_samples=1, 2121 on_surface=True)[0] 2122 # Project the random vector onto the input vector and subtract the projection 2123 projection = np.dot(random_vector, np_vec) * np_vec 2124 perpendicular_vector = random_vector - projection 2125 # Normalize the perpendicular vector to have the same magnitude as the input vector 2126 perpendicular_vector /= np.linalg.norm(perpendicular_vector) 2127 return perpendicular_vector*magnitude
Generates a random vector perpendicular to a given vector.
Arguments:
- vector ('array-like'): Reference vector to which the generated vector will be perpendicular.
- magnitude ('float'): Desired magnitude of the perpendicular vector.
Returns:
'numpy.ndarray': Vector orthogonal to 'vector' with norm equal to 'magnitude'.
2129 def get_bond_template(self, particle_name1, particle_name2, use_default_bond=False) : 2130 """ 2131 Retrieves a bond template connecting two particle templates. 2132 2133 Args: 2134 particle_name1 ('str'): 2135 Name of the first particle template. 2136 2137 particle_name2 ('str'): 2138 Name of the second particle template. 2139 2140 use_default_bond ('bool', optional): 2141 If True, returns the default bond template when no specific bond 2142 template is found. Defaults to False. 2143 2144 Returns: 2145 'BondTemplate': 2146 Bond template object retrieved from the pyMBE database. 2147 2148 Notes: 2149 - This method searches the pyMBE database for a bond template defined between particle templates with names 'particle_name1' and 'particle_name2'. 2150 - If no specific bond template is found and 'use_default_bond' is enabled, a default bond template is returned instead. 2151 """ 2152 # Try to find a specific bond template 2153 bond_key = BondTemplate.make_bond_key(pn1=particle_name1, 2154 pn2=particle_name2) 2155 try: 2156 return self.db.get_template(name=bond_key, 2157 pmb_type="bond") 2158 except ValueError: 2159 pass 2160 2161 # Fallback to default bond if allowed 2162 if use_default_bond: 2163 return self.db.get_template(name="default", 2164 pmb_type="bond") 2165 2166 # No bond template found 2167 raise ValueError(f"No bond template found between '{particle_name1}' and '{particle_name2}', and default bonds are deactivated.")
Retrieves a bond template connecting two particle templates.
Arguments:
- particle_name1 ('str'): Name of the first particle template.
- particle_name2 ('str'): Name of the second particle template.
- use_default_bond ('bool', optional): If True, returns the default bond template when no specific bond template is found. Defaults to False.
Returns:
'BondTemplate': Bond template object retrieved from the pyMBE database.
Notes:
- This method searches the pyMBE database for a bond template defined between particle templates with names 'particle_name1' and 'particle_name2'.
- If no specific bond template is found and 'use_default_bond' is enabled, a default bond template is returned instead.
2169 def get_charge_number_map(self): 2170 """ 2171 Construct a mapping from ESPResSo particle types to their charge numbers. 2172 2173 Returns: 2174 'dict[int, float]': 2175 Dictionary mapping ESPResSo particle types to charge numbers, 2176 ''{es_type: z}''. 2177 2178 Notess: 2179 - The mapping is built from particle *states*, not instances. 2180 - If multiple templates define states with the same ''es_type'', 2181 the last encountered definition will overwrite previous ones. 2182 This behavior is intentional and assumes database consistency. 2183 - Neutral particles (''z = 0'') are included in the map. 2184 """ 2185 charge_number_map = {} 2186 particle_templates = self.db.get_templates("particle") 2187 for tpl in particle_templates.values(): 2188 for state in self.db.get_particle_states_templates(particle_name=tpl.name).values(): 2189 charge_number_map[state.es_type] = state.z 2190 return charge_number_map
Construct a mapping from ESPResSo particle types to their charge numbers.
Returns:
'dict[int, float]': Dictionary mapping ESPResSo particle types to charge numbers, ''{es_type: z}''.
Notess:
- The mapping is built from particle states, not instances.
- If multiple templates define states with the same ''es_type'', the last encountered definition will overwrite previous ones. This behavior is intentional and assumes database consistency.
- Neutral particles (''z = 0'') are included in the map.
2192 def get_instances_df(self, pmb_type): 2193 """ 2194 Returns a dataframe with all instances of type 'pmb_type' in the pyMBE database. 2195 2196 Args: 2197 pmb_type ('str'): 2198 pmb type to search instances in the pyMBE database. 2199 2200 Returns: 2201 ('Pandas.Dataframe'): 2202 Dataframe with all instances of type 'pmb_type'. 2203 """ 2204 return self.db._get_instances_df(pmb_type=pmb_type)
Returns a dataframe with all instances of type 'pmb_type' in the pyMBE database.
Arguments:
- pmb_type ('str'): pmb type to search instances in the pyMBE database.
Returns:
('Pandas.Dataframe'): Dataframe with all instances of type 'pmb_type'.
2206 def get_lj_parameters(self, particle_name1, particle_name2, combining_rule='Lorentz-Berthelot'): 2207 """ 2208 Returns the Lennard-Jones parameters for the interaction between the particle types given by 2209 'particle_name1' and 'particle_name2' in the pyMBE database, calculated according to the provided combining rule. 2210 2211 Args: 2212 particle_name1 ('str'): 2213 label of the type of the first particle type 2214 2215 particle_name2 ('str'): 2216 label of the type of the second particle type 2217 2218 combining_rule ('string', optional): 2219 combining rule used to calculate 'sigma' and 'epsilon' for the potential betwen a pair of particles. Defaults to 'Lorentz-Berthelot'. 2220 2221 Returns: 2222 ('dict'): 2223 {"epsilon": epsilon_value, "sigma": sigma_value, "offset": offset_value, "cutoff": cutoff_value} 2224 2225 Notes: 2226 - Currently, the only 'combining_rule' supported is Lorentz-Berthelot. 2227 - If the sigma value of 'particle_name1' or 'particle_name2' is 0, the function will return an empty dictionary. No LJ interactions are set up for particles with sigma = 0. 2228 """ 2229 lj_parameters=self.db.get_lj_parameters(particle_name1=particle_name1,particle_name2=particle_name2,combining_rule=combining_rule) 2230 return lj_parameters
Returns the Lennard-Jones parameters for the interaction between the particle types given by 'particle_name1' and 'particle_name2' in the pyMBE database, calculated according to the provided combining rule.
Arguments:
- particle_name1 ('str'): label of the type of the first particle type
- particle_name2 ('str'): label of the type of the second particle type
- combining_rule ('string', optional): combining rule used to calculate 'sigma' and 'epsilon' for the potential betwen a pair of particles. Defaults to 'Lorentz-Berthelot'.
Returns:
('dict'): {"epsilon": epsilon_value, "sigma": sigma_value, "offset": offset_value, "cutoff": cutoff_value}
Notes:
- Currently, the only 'combining_rule' supported is Lorentz-Berthelot.
- If the sigma value of 'particle_name1' or 'particle_name2' is 0, the function will return an empty dictionary. No LJ interactions are set up for particles with sigma = 0.
2232 def get_particle_id_map(self, object_name): 2233 """ 2234 Collect all particle IDs associated with an object of given name in the 2235 pyMBE database. 2236 2237 Args: 2238 object_name ('str'): 2239 Name of the object. 2240 2241 Returns: 2242 ('dict'): 2243 {"all": [particle_ids], 2244 "residue_map": {residue_id: [particle_ids]}, 2245 "molecule_map": {molecule_id: [particle_ids]}, 2246 "assembly_map": {assembly_id: [particle_ids]},} 2247 2248 Notess: 2249 - Works for all supported pyMBE templates. 2250 - Relies in the internal method Manager.get_particle_id_map, see method for the detailed code. 2251 """ 2252 return self.db.get_particle_id_map(object_name=object_name)
Collect all particle IDs associated with an object of given name in the pyMBE database.
Arguments:
- object_name ('str'): Name of the object.
Returns:
('dict'): {"all": [particle_ids], "residue_map": {residue_id: [particle_ids]}, "molecule_map": {molecule_id: [particle_ids]}, "assembly_map": {assembly_id: [particle_ids]},}
Notess:
- Works for all supported pyMBE templates.
- Relies in the internal method Manager.get_particle_id_map, see method for the detailed code.
2254 def get_pka_set(self): 2255 """ 2256 Retrieve the pKa set for all titratable particles in the pyMBE database. 2257 2258 Returns: 2259 ('dict'): 2260 Dictionary of the form: 2261 {"particle_name": {"pka_value": float, 2262 "acidity": "acidic" | "basic"}} 2263 Notes: 2264 - If a particle participates in multiple acid/base reactions, an error is raised. 2265 """ 2266 pka_set = {} 2267 supported_reactions = ["monoprotic_acid", 2268 "monoprotic_base"] 2269 for reaction in self.db._reactions.values(): 2270 if reaction.reaction_type not in supported_reactions: 2271 continue 2272 # Identify involved particle(s) 2273 particle_names = {participant.particle_name for participant in reaction.participants} 2274 particle_name = particle_names.pop() 2275 if particle_name in pka_set: 2276 raise ValueError(f"Multiple acid/base reactions found for particle '{particle_name}'.") 2277 pka_set[particle_name] = {"pka_value": reaction.pK} 2278 if reaction.reaction_type == "monoprotic_acid": 2279 acidity = "acidic" 2280 elif reaction.reaction_type == "monoprotic_base": 2281 acidity = "basic" 2282 pka_set[particle_name]["acidity"] = acidity 2283 return pka_set
Retrieve the pKa set for all titratable particles in the pyMBE database.
Returns:
('dict'): Dictionary of the form: {"particle_name": {"pka_value": float, "acidity": "acidic" | "basic"}}
Notes:
- If a particle participates in multiple acid/base reactions, an error is raised.
2285 def get_radius_map(self, dimensionless=True): 2286 """ 2287 Gets the effective radius of each particle defined in the pyMBE database. 2288 2289 Args: 2290 dimensionless ('bool'): 2291 If ``True``, return magnitudes expressed in ``reduced_length``. 2292 If ``False``, return Pint quantities with units. 2293 2294 Returns: 2295 ('dict'): 2296 {espresso_type: radius}. 2297 2298 Notes: 2299 - The radius corresponds to (sigma+offset)/2 2300 """ 2301 return self.db.get_radius_map(dimensionless)
Gets the effective radius of each particle defined in the pyMBE database.
Arguments:
- dimensionless ('bool'): If
True, return magnitudes expressed inreduced_length. IfFalse, return Pint quantities with units.
Returns:
('dict'): {espresso_type: radius}.
Notes:
- The radius corresponds to (sigma+offset)/2
2303 def get_reactions_df(self): 2304 """ 2305 Returns a dataframe with all reaction templates in the pyMBE database. 2306 2307 Returns: 2308 (Pandas.Dataframe): 2309 Dataframe with all reaction templates. 2310 """ 2311 return self.db._get_reactions_df()
Returns a dataframe with all reaction templates in the pyMBE database.
Returns:
(Pandas.Dataframe): Dataframe with all reaction templates.
2313 def get_reduced_units(self): 2314 """ 2315 Returns the current set of reduced units defined in pyMBE. 2316 2317 Returns: 2318 reduced_units_text ('str'): 2319 text with information about the current set of reduced units. 2320 2321 """ 2322 unit_length=self.units.Quantity(1,'reduced_length') 2323 unit_energy=self.units.Quantity(1,'reduced_energy') 2324 unit_charge=self.units.Quantity(1,'reduced_charge') 2325 reduced_units_text = "\n".join(["Current set of reduced units:", 2326 f"{unit_length.to('nm'):.5g} = {unit_length}", 2327 f"{unit_energy.to('J'):.5g} = {unit_energy}", 2328 f"{unit_charge.to('C'):.5g} = {unit_charge}", 2329 f"Temperature: {(self.kT/self.kB).to('K'):.5g}"]) 2330 return reduced_units_text
Returns the current set of reduced units defined in pyMBE.
Returns:
reduced_units_text ('str'): text with information about the current set of reduced units.
2332 def get_templates_df(self, pmb_type): 2333 """ 2334 Returns a dataframe with all templates of type 'pmb_type' in the pyMBE database. 2335 2336 Args: 2337 pmb_type ('str'): 2338 pmb type to search templates in the pyMBE database. 2339 2340 Returns: 2341 ('Pandas.Dataframe'): 2342 Dataframe with all templates of type given by 'pmb_type'. 2343 """ 2344 return self.db._get_templates_df(pmb_type=pmb_type)
Returns a dataframe with all templates of type 'pmb_type' in the pyMBE database.
Arguments:
- pmb_type ('str'): pmb type to search templates in the pyMBE database.
Returns:
('Pandas.Dataframe'): Dataframe with all templates of type given by 'pmb_type'.
2346 def get_type_map(self): 2347 """ 2348 Return the mapping of ESPResSo types for all particle states defined in the pyMBE database. 2349 2350 Returns: 2351 'dict[str, int]': 2352 A dictionary mapping each particle state to its corresponding ESPResSo type: 2353 {state_name: es_type, ...} 2354 """ 2355 2356 return self.db.get_es_types_map()
Return the mapping of ESPResSo types for all particle states defined in the pyMBE database.
Returns:
'dict[str, int]': A dictionary mapping each particle state to its corresponding ESPResSo type: {state_name: es_type, ...}
2358 def initialize_lattice_builder(self, diamond_lattice): 2359 """ 2360 Initialize the lattice builder with the DiamondLattice object. 2361 2362 Args: 2363 diamond_lattice ('DiamondLattice'): 2364 DiamondLattice object from the 'lib/lattice' module to be used in the LatticeBuilder. 2365 """ 2366 from .lib.lattice import LatticeBuilder, DiamondLattice 2367 if not isinstance(diamond_lattice, DiamondLattice): 2368 raise TypeError("Currently only DiamondLattice objects are supported.") 2369 self.lattice_builder = LatticeBuilder(lattice=diamond_lattice) 2370 logging.info(f"LatticeBuilder initialized with mpc={diamond_lattice.mpc} and box_l={diamond_lattice.box_l}") 2371 return self.lattice_builder
Initialize the lattice builder with the DiamondLattice object.
Arguments:
- diamond_lattice ('DiamondLattice'): DiamondLattice object from the 'lib/lattice' module to be used in the LatticeBuilder.
2373 def load_database(self, folder, format='csv'): 2374 """ 2375 Loads a pyMBE database stored in 'folder'. 2376 2377 Args: 2378 folder ('str' or 'Path'): 2379 Path to the folder where the pyMBE database was stored. 2380 2381 format ('str', optional): 2382 Format of the database to be loaded. Defaults to 'csv'. 2383 2384 Return: 2385 ('dict'): 2386 metadata with additional information about the source of the information in the database. 2387 2388 Notes: 2389 - The folder must contain the files generated by 'pmb.save_database()'. 2390 - Currently, only 'csv' format is supported. 2391 """ 2392 supported_formats = ['csv'] 2393 if format not in supported_formats: 2394 raise ValueError(f"Format {format} not supported. Supported formats are {supported_formats}") 2395 if format == 'csv': 2396 metadata =io._load_database_csv(self.db, 2397 folder=folder) 2398 return metadata
Loads a pyMBE database stored in 'folder'.
Arguments:
- folder ('str' or 'Path'): Path to the folder where the pyMBE database was stored.
- format ('str', optional): Format of the database to be loaded. Defaults to 'csv'.
Return:
('dict'): metadata with additional information about the source of the information in the database.
Notes:
- The folder must contain the files generated by 'pmb.save_database()'.
- Currently, only 'csv' format is supported.
2400 def load_pka_set(self, filename): 2401 """ 2402 Load a pKa set and attach chemical states and acid–base reactions 2403 to existing particle templates. 2404 2405 Args: 2406 filename ('str'): 2407 Path to a JSON file containing the pKa set. Expected format: 2408 {"metadata": {...}, 2409 "data": {"A": {"acidity": "acidic", "pka_value": 4.5}, 2410 "B": {"acidity": "basic", "pka_value": 9.8}}} 2411 2412 Returns: 2413 ('dict'): 2414 Dictionary with bibliographic metadata about the original work were the pKa set was determined. 2415 2416 Notes: 2417 - This method is designed for monoprotic acids and bases only. 2418 """ 2419 with open(filename, "r") as f: 2420 pka_data = json.load(f) 2421 pka_set = pka_data["data"] 2422 metadata = pka_data.get("metadata", {}) 2423 self._check_pka_set(pka_set) 2424 for particle_name, entry in pka_set.items(): 2425 acidity = entry["acidity"] 2426 pka = entry["pka_value"] 2427 self.define_monoprototic_acidbase_reaction(particle_name=particle_name, 2428 pka=pka, 2429 acidity=acidity, 2430 metadata=metadata) 2431 return metadata
Load a pKa set and attach chemical states and acid–base reactions to existing particle templates.
Arguments:
- filename ('str'): Path to a JSON file containing the pKa set. Expected format: {"metadata": {...}, "data": {"A": {"acidity": "acidic", "pka_value": 4.5}, "B": {"acidity": "basic", "pka_value": 9.8}}}
Returns:
('dict'): Dictionary with bibliographic metadata about the original work were the pKa set was determined.
Notes:
- This method is designed for monoprotic acids and bases only.
2433 def propose_unused_type(self): 2434 """ 2435 Propose an unused ESPResSo particle type. 2436 2437 Returns: 2438 ('int'): 2439 The next available integer ESPResSo type. Returns ''0'' if no integer types are currently defined. 2440 """ 2441 return self.db.propose_unused_type()
Propose an unused ESPResSo particle type.
Returns:
('int'): The next available integer ESPResSo type. Returns ''0'' if no integer types are currently defined.
2443 def read_protein_vtf(self, filename, unit_length=None): 2444 """ 2445 Loads a coarse-grained protein model from a VTF file. 2446 2447 Args: 2448 filename ('str'): 2449 Path to the VTF file. 2450 2451 unit_length ('Pint.Quantity'): 2452 Unit of length for coordinates (pyMBE UnitRegistry). Defaults to Angstrom. 2453 2454 Returns: 2455 ('tuple'): 2456 ('dict'): Particle topology. 2457 ('str'): One-letter amino-acid sequence (including n/c ends). 2458 """ 2459 logging.info(f"Loading protein coarse-grain model file: {filename}") 2460 if unit_length is None: 2461 unit_length = 1 * self.units.angstrom 2462 atoms = {} # atom_id -> atom info 2463 coords = [] # ordered coordinates 2464 residues = {} # resid -> resname (first occurrence) 2465 has_n_term = False 2466 has_c_term = False 2467 aa_3to1 = {"ALA": "A", "ARG": "R", "ASN": "N", "ASP": "D", 2468 "CYS": "C", "GLU": "E", "GLN": "Q", "GLY": "G", 2469 "HIS": "H", "ILE": "I", "LEU": "L", "LYS": "K", 2470 "MET": "M", "PHE": "F", "PRO": "P", "SER": "S", 2471 "THR": "T", "TRP": "W", "TYR": "Y", "VAL": "V", 2472 "n": "n", "c": "c"} 2473 # --- parse VTF --- 2474 with open(filename, "r") as f: 2475 for line in f: 2476 fields = line.split() 2477 if not fields: 2478 continue 2479 if fields[0] == "atom": 2480 atom_id = int(fields[1]) 2481 atom_name = fields[3] 2482 resname = fields[5] 2483 resid = int(fields[7]) 2484 chain_id = fields[9] 2485 radius = float(fields[11]) * unit_length 2486 atoms[atom_id] = {"name": atom_name, 2487 "resname": resname, 2488 "resid": resid, 2489 "chain_id": chain_id, 2490 "radius": radius} 2491 if resname == "n": 2492 has_n_term = True 2493 elif resname == "c": 2494 has_c_term = True 2495 # register residue 2496 if resid not in residues: 2497 residues[resid] = resname 2498 elif fields[0].isnumeric(): 2499 xyz = [(float(x) * unit_length).to("reduced_length").magnitude 2500 for x in fields[1:4]] 2501 coords.append(xyz) 2502 sequence = "" 2503 # N-terminus 2504 if has_n_term: 2505 sequence += "n" 2506 # protein residues only 2507 protein_resids = sorted(resid for resid, resname in residues.items() if resname not in ("n", "c", "Ca")) 2508 for resid in protein_resids: 2509 resname = residues[resid] 2510 try: 2511 sequence += aa_3to1[resname] 2512 except KeyError: 2513 raise ValueError(f"Unknown residue name '{resname}' in VTF file") 2514 # C-terminus 2515 if has_c_term: 2516 sequence += "c" 2517 last_resid = max(protein_resids) 2518 # --- build topology --- 2519 topology_dict = {} 2520 for atom_id in sorted(atoms.keys()): 2521 atom = atoms[atom_id] 2522 resname = atom["resname"] 2523 resid = atom["resid"] 2524 # apply labeling rules 2525 if resname == "n": 2526 label_resid = 0 2527 elif resname == "c": 2528 label_resid = last_resid + 1 2529 elif resname == "Ca": 2530 label_resid = last_resid + 2 2531 else: 2532 label_resid = resid # preserve original resid 2533 label = f"{atom['name']}{label_resid}" 2534 if label in topology_dict: 2535 raise ValueError(f"Duplicate particle label '{label}'. Check VTF residue definitions.") 2536 topology_dict[label] = {"initial_pos": coords[atom_id - 1], "chain_id": atom["chain_id"], "radius": atom["radius"],} 2537 return topology_dict, sequence
Loads a coarse-grained protein model from a VTF file.
Arguments:
- filename ('str'): Path to the VTF file.
- unit_length ('Pint.Quantity'): Unit of length for coordinates (pyMBE UnitRegistry). Defaults to Angstrom.
Returns:
('tuple'): ('dict'): Particle topology.
('str'): One-letter amino-acid sequence (including n/c ends).
2540 def save_database(self, folder, format='csv'): 2541 """ 2542 Saves the current pyMBE database into a file 'filename'. 2543 2544 Args: 2545 folder ('str' or 'Path'): 2546 Path to the folder where the database files will be saved. 2547 2548 """ 2549 supported_formats = ['csv'] 2550 if format not in supported_formats: 2551 raise ValueError(f"Format {format} not supported. Supported formats are: {supported_formats}") 2552 if format == 'csv': 2553 io._save_database_csv(self.db, 2554 folder=folder)
Saves the current pyMBE database into a file 'filename'.
Arguments:
- folder ('str' or 'Path'): Path to the folder where the database files will be saved.
2556 def set_particle_initial_state(self, particle_name, state_name): 2557 """ 2558 Sets the default initial state of a particle template defined in the pyMBE database. 2559 2560 Args: 2561 particle_name ('str'): 2562 Unique label that identifies the particle template. 2563 2564 state_name ('str'): 2565 Name of the state to be set as default initial state. 2566 """ 2567 part_tpl = self.db.get_template(name=particle_name, 2568 2569 pmb_type="particle") 2570 part_tpl.initial_state = state_name 2571 logging.info(f"Default initial state of particle {particle_name} set to {state_name}.")
Sets the default initial state of a particle template defined in the pyMBE database.
Arguments:
- particle_name ('str'): Unique label that identifies the particle template.
- state_name ('str'): Name of the state to be set as default initial state.
2573 def set_reduced_units(self, unit_length=None, unit_charge=None, temperature=None, Kw=None): 2574 """ 2575 Sets the set of reduced units used by pyMBE.units and it prints it. 2576 2577 Args: 2578 unit_length ('pint.Quantity', optional): 2579 Reduced unit of length defined using the 'pmb.units' UnitRegistry. Defaults to None. 2580 2581 unit_charge ('pint.Quantity', optional): 2582 Reduced unit of charge defined using the 'pmb.units' UnitRegistry. Defaults to None. 2583 2584 temperature ('pint.Quantity', optional): 2585 Temperature of the system, defined using the 'pmb.units' UnitRegistry. Defaults to None. 2586 2587 Kw ('pint.Quantity', optional): 2588 Ionic product of water in mol^2/l^2. Defaults to None. 2589 2590 Notes: 2591 - If no 'temperature' is given, a value of 298.15 K is assumed by default. 2592 - If no 'unit_length' is given, a value of 0.355 nm is assumed by default. 2593 - If no 'unit_charge' is given, a value of 1 elementary charge is assumed by default. 2594 - If no 'Kw' is given, a value of 10^(-14) * mol^2 / l^2 is assumed by default. 2595 """ 2596 if unit_length is None: 2597 unit_length= 0.355*self.units.nm 2598 if temperature is None: 2599 temperature = 298.15 * self.units.K 2600 if unit_charge is None: 2601 unit_charge = scipy.constants.e * self.units.C 2602 if Kw is None: 2603 Kw = 1e-14 2604 # Sanity check 2605 variables=[unit_length,temperature,unit_charge] 2606 dimensionalities=["[length]","[temperature]","[charge]"] 2607 for variable,dimensionality in zip(variables,dimensionalities): 2608 self._check_dimensionality(variable,dimensionality) 2609 self.Kw=Kw*self.units.mol**2 / (self.units.l**2) 2610 self.kT=temperature*self.kB 2611 self.units._build_cache() 2612 self.units.define(f'reduced_energy = {self.kT} ') 2613 self.units.define(f'reduced_length = {unit_length}') 2614 self.units.define(f'reduced_charge = {unit_charge}') 2615 logging.info(self.get_reduced_units())
Sets the set of reduced units used by pyMBE.units and it prints it.
Arguments:
- unit_length ('pint.Quantity', optional): Reduced unit of length defined using the 'pmb.units' UnitRegistry. Defaults to None.
- unit_charge ('pint.Quantity', optional): Reduced unit of charge defined using the 'pmb.units' UnitRegistry. Defaults to None.
- temperature ('pint.Quantity', optional): Temperature of the system, defined using the 'pmb.units' UnitRegistry. Defaults to None.
- Kw ('pint.Quantity', optional): Ionic product of water in mol^2/l^2. Defaults to None.
Notes:
- If no 'temperature' is given, a value of 298.15 K is assumed by default.
- If no 'unit_length' is given, a value of 0.355 nm is assumed by default.
- If no 'unit_charge' is given, a value of 1 elementary charge is assumed by default.
- If no 'Kw' is given, a value of 10^(-14) * mol^2 / l^2 is assumed by default.
2617 def set_simulation_engine(self,simulation_engine,box_l=None): 2618 """ 2619 Sets the instance attribute simulation_engine to an instance of a class of type SimulationEngine. 2620 2621 Args: 2622 simulation_engine (Any): object which contains the methods to setup molecular dynamics and montecarlo simulations 2623 box_l('list[float,float,float]'): list of floats with the dimensions of the box 2624 """ 2625 2626 if isinstance(simulation_engine, espressomd.System): 2627 self.simulation_engine=EspressoSimulation(box_l=simulation_engine.box_l, 2628 db=self.db, 2629 espresso_system=simulation_engine, 2630 units=self.units, 2631 kT=self.kT, 2632 Kw=self.Kw, 2633 seed=self.seed) 2634 elif isinstance(simulation_engine,LammpsProtocol): 2635 self.simulation_engine=LammpsSimulation(box_l=box_l, 2636 db=self.db, 2637 lammps=simulation_engine, 2638 units=self.units, 2639 kT=self.kT, 2640 Kw=self.Kw, 2641 seed=self.seed) 2642 else: 2643 raise ValueError('The specified simulation engine is not implemented yet')
Sets the instance attribute simulation_engine to an instance of a class of type SimulationEngine.
Arguments:
- simulation_engine (Any): object which contains the methods to setup molecular dynamics and montecarlo simulations
- box_l('list[float,float,float]'): list of floats with the dimensions of the box
2645 def setup_cpH (self, counter_ion, constant_pH, exclusion_range=None, use_exclusion_radius_per_type = False): 2646 """ 2647 Sets up the Acid/Base reactions for acidic/basic particles defined in the pyMBE database 2648 to be sampled in the constant pH ensemble. 2649 2650 Args: 2651 counter_ion ('str'): 2652 'name' of the counter_ion 'particle'. 2653 2654 constant_pH ('float'): 2655 pH-value. 2656 2657 exclusion_range ('pint.Quantity', optional): 2658 Below this value, no particles will be inserted. 2659 2660 use_exclusion_radius_per_type ('bool', optional): 2661 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2662 2663 Returns: 2664 ('reaction_methods.ConstantpHEnsemble'): 2665 Instance of a reaction_methods.ConstantpHEnsemble object from the espressomd library. 2666 """ 2667 2668 RE = self.simulation_engine.setup_cpH(counter_ion=counter_ion, 2669 constant_pH=constant_pH, 2670 exclusion_range=exclusion_range, 2671 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2672 return RE
Sets up the Acid/Base reactions for acidic/basic particles defined in the pyMBE database to be sampled in the constant pH ensemble.
Arguments:
- counter_ion ('str'): 'name' of the counter_ion 'particle'.
- constant_pH ('float'): pH-value.
- exclusion_range ('pint.Quantity', optional): Below this value, no particles will be inserted.
- use_exclusion_radius_per_type ('bool', optional): Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'.
Returns:
('reaction_methods.ConstantpHEnsemble'): Instance of a reaction_methods.ConstantpHEnsemble object from the espressomd library.
2674 def setup_gcmc(self, c_salt_res, salt_cation_name, salt_anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2675 """ 2676 Sets up grand-canonical coupling to a reservoir of salt. 2677 For reactive systems coupled to a reservoir, the grand-reaction method has to be used instead. 2678 2679 Args: 2680 c_salt_res ('pint.Quantity'): 2681 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2682 2683 salt_cation_name ('str'): 2684 Name of the salt cation (e.g. Na+) particle. 2685 2686 salt_anion_name ('str'): 2687 Name of the salt anion (e.g. Cl-) particle. 2688 2689 activity_coefficient ('callable'): 2690 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2691 2692 exclusion_range('pint.Quantity', optional): 2693 For distances shorter than this value, no particles will be inserted. 2694 2695 use_exclusion_radius_per_type('bool',optional): 2696 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2697 2698 Returns: 2699 ('reaction_methods.ReactionEnsemble'): 2700 Instance of a reaction_methods.ReactionEnsemble object from the espressomd library. 2701 """ 2702 RE = self.simulation_engine.setup_gcmc(c_salt_res=c_salt_res, 2703 salt_anion_name=salt_anion_name, 2704 salt_cation_name=salt_cation_name, 2705 activity_coefficient=activity_coefficient, 2706 exclusion_range=exclusion_range, 2707 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2708 return RE
Sets up grand-canonical coupling to a reservoir of salt. For reactive systems coupled to a reservoir, the grand-reaction method has to be used instead.
Arguments:
- c_salt_res ('pint.Quantity'): Concentration of monovalent salt (e.g. NaCl) in the reservoir.
- salt_cation_name ('str'): Name of the salt cation (e.g. Na+) particle.
- salt_anion_name ('str'): Name of the salt anion (e.g. Cl-) particle.
- activity_coefficient ('callable'): A function that calculates the activity coefficient of an ion pair as a function of the ionic strength.
- exclusion_range('pint.Quantity', optional): For distances shorter than this value, no particles will be inserted.
- use_exclusion_radius_per_type('bool',optional): Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'.
Returns:
('reaction_methods.ReactionEnsemble'): Instance of a reaction_methods.ReactionEnsemble object from the espressomd library.
2710 def setup_grxmc_reactions(self, pH_res, c_salt_res, proton_name, hydroxide_name, salt_cation_name, salt_anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2711 """ 2712 Sets up acid/base reactions for acidic/basic monoprotic particles defined in the pyMBE database, 2713 as well as a grand-canonical coupling to a reservoir of small ions. 2714 2715 2716 Args: 2717 pH_res ('float'): 2718 pH-value in the reservoir. 2719 2720 c_salt_res ('pint.Quantity'): 2721 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2722 2723 proton_name ('str'): 2724 Name of the proton (H+) particle. 2725 2726 hydroxide_name ('str'): 2727 Name of the hydroxide (OH-) particle. 2728 2729 salt_cation_name ('str'): 2730 Name of the salt cation (e.g. Na+) particle. 2731 2732 salt_anion_name ('str'): 2733 Name of the salt anion (e.g. Cl-) particle. 2734 2735 activity_coefficient ('callable'): 2736 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2737 2738 exclusion_range('pint.Quantity', optional): 2739 For distances shorter than this value, no particles will be inserted. 2740 2741 use_exclusion_radius_per_type('bool', optional): 2742 Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'. 2743 2744 Returns: 2745 For the Espresso system class: 2746 Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'): 2747 2748 'reaction_methods.ReactionEnsemble': 2749 espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 2750 2751 'pint.Quantity': 2752 Ionic strength of the reservoir (useful for calculating partition coefficients). 2753 2754 Notess: 2755 - This implementation uses the original formulation of the grand-reaction method by Landsgesell et al. [1]. 2756 2757 [1] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020. 2758 """ 2759 output=self.simulation_engine.setup_grxmc_reactions(pH_res=pH_res, 2760 c_salt_res=c_salt_res, 2761 proton_name=proton_name, 2762 hydroxide_name=hydroxide_name, 2763 salt_cation_name=salt_cation_name, 2764 salt_anion_name=salt_anion_name, 2765 activity_coefficient=activity_coefficient, 2766 exclusion_range=exclusion_range, 2767 use_exclusion_radius_per_type=use_exclusion_radius_per_type) 2768 2769 return output
Sets up acid/base reactions for acidic/basic monoprotic particles defined in the pyMBE database, as well as a grand-canonical coupling to a reservoir of small ions.
Arguments:
- pH_res ('float'): pH-value in the reservoir.
- c_salt_res ('pint.Quantity'): Concentration of monovalent salt (e.g. NaCl) in the reservoir.
- proton_name ('str'): Name of the proton (H+) particle.
- hydroxide_name ('str'): Name of the hydroxide (OH-) particle.
- salt_cation_name ('str'): Name of the salt cation (e.g. Na+) particle.
- salt_anion_name ('str'): Name of the salt anion (e.g. Cl-) particle.
- activity_coefficient ('callable'): A function that calculates the activity coefficient of an ion pair as a function of the ionic strength.
- exclusion_range('pint.Quantity', optional): For distances shorter than this value, no particles will be inserted.
- use_exclusion_radius_per_type('bool', optional): Controls if one exclusion_radius for each espresso_type is used. Defaults to 'False'.
Returns:
For the Espresso system class: Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'):
'reaction_methods.ReactionEnsemble': espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 'pint.Quantity': Ionic strength of the reservoir (useful for calculating partition coefficients).
Notess:
- This implementation uses the original formulation of the grand-reaction method by Landsgesell et al. [1].
[1] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020.
2771 def setup_grxmc_unified(self, pH_res, c_salt_res, cation_name, anion_name, activity_coefficient, exclusion_range=None, use_exclusion_radius_per_type = False): 2772 """ 2773 Sets up acid/base reactions for acidic/basic 'particles' defined in the pyMBE database, as well as a grand-canonical coupling to a 2774 reservoir of small ions using a unified formulation for small ions. 2775 2776 Args: 2777 pH_res ('float'): 2778 pH-value in the reservoir. 2779 2780 c_salt_res ('pint.Quantity'): 2781 Concentration of monovalent salt (e.g. NaCl) in the reservoir. 2782 2783 cation_name ('str'): 2784 Name of the cationic particle. 2785 2786 anion_name ('str'): 2787 Name of the anionic particle. 2788 2789 activity_coefficient ('callable'): 2790 A function that calculates the activity coefficient of an ion pair as a function of the ionic strength. 2791 2792 exclusion_range('pint.Quantity', optional): 2793 Below this value, no particles will be inserted. 2794 2795 use_exclusion_radius_per_type('bool', optional): 2796 Controls if one exclusion_radius per each espresso_type. Defaults to 'False'. 2797 2798 Returns: 2799 For the Espresso system class: 2800 Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'): 2801 2802 'reaction_methods.ReactionEnsemble': 2803 espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 2804 2805 'pint.Quantity': 2806 Ionic strength of the reservoir (useful for calculating partition coefficients). 2807 2808 Notes: 2809 - This implementation uses the formulation of the grand-reaction method by Curk et al. [1], which relies on "unified" ion types X+ = {H+, Na+} and X- = {OH-, Cl-}. 2810 - A function that implements the original version of the grand-reaction method by Landsgesell et al. [2] is also available under the name 'setup_grxmc_reactions'. 2811 2812 [1] Curk, T., Yuan, J., & Luijten, E. (2022). Accelerated simulation method for charge regulation effects. The Journal of Chemical Physics, 156(4). 2813 [2] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020. 2814 """ 2815 output=self.simulation_engine.setup_grxmc_unified(pH_res=pH_res, 2816 c_salt_res=c_salt_res, 2817 cation_name=cation_name, 2818 anion_name=anion_name, 2819 activity_coefficient=activity_coefficient, 2820 exclusion_range=exclusion_range, 2821 use_exclusion_radius_per_type = use_exclusion_radius_per_type) 2822 return output
Sets up acid/base reactions for acidic/basic 'particles' defined in the pyMBE database, as well as a grand-canonical coupling to a reservoir of small ions using a unified formulation for small ions.
Arguments:
- pH_res ('float'): pH-value in the reservoir.
- c_salt_res ('pint.Quantity'): Concentration of monovalent salt (e.g. NaCl) in the reservoir.
- cation_name ('str'): Name of the cationic particle.
- anion_name ('str'): Name of the anionic particle.
- activity_coefficient ('callable'): A function that calculates the activity coefficient of an ion pair as a function of the ionic strength.
- exclusion_range('pint.Quantity', optional): Below this value, no particles will be inserted.
- use_exclusion_radius_per_type('bool', optional): Controls if one exclusion_radius per each espresso_type. Defaults to 'False'.
Returns:
For the Espresso system class: Output ('tuple(reaction_methods.ReactionEnsemble,pint.Quantity)'):
'reaction_methods.ReactionEnsemble': espressomd reaction_methods object with all reactions necesary to run the GRxMC ensamble. 'pint.Quantity': Ionic strength of the reservoir (useful for calculating partition coefficients).
Notes:
- This implementation uses the formulation of the grand-reaction method by Curk et al. [1], which relies on "unified" ion types X+ = {H+, Na+} and X- = {OH-, Cl-}.
- A function that implements the original version of the grand-reaction method by Landsgesell et al. [2] is also available under the name 'setup_grxmc_reactions'.
[1] Curk, T., Yuan, J., & Luijten, E. (2022). Accelerated simulation method for charge regulation effects. The Journal of Chemical Physics, 156(4). [2] Landsgesell, J., Hebbeker, P., Rud, O., Lunkad, R., Košovan, P., & Holm, C. (2020). Grand-reaction method for simulations of ionization equilibria coupled to ion partitioning. Macromolecules, 53(8), 3007-3020.
2824 def setup_lj_interactions(self, shift_potential=True, combining_rule='Lorentz-Berthelot'): 2825 """ 2826 Sets up the Lennard-Jones (LJ) potential between all pairs of particle states defined in the pyMBE database. 2827 2828 Args: 2829 2830 shift_potential('bool', optional): 2831 If True, a shift will be automatically computed such that the potential is continuous at the cutoff radius. Otherwise, no shift will be applied. Defaults to True. 2832 2833 combining_rule('string', optional): 2834 combining rule used to calculate 'sigma' and 'epsilon' for the potential between a pair of particles. Defaults to 'Lorentz-Berthelot'. 2835 2836 warning('bool', optional): 2837 switch to activate/deactivate warning messages. Defaults to True. 2838 2839 Notes: 2840 - Currently, the only 'combining_rule' supported is Lorentz-Berthelot. 2841 - Check the documentation of ESPResSo for more info about the potential https://espressomd.github.io/doc4.2.0/inter_non-bonded.html 2842 2843 """ 2844 self.simulation_engine.setup_lj_interactions(shift_potential=shift_potential, 2845 combining_rule=combining_rule)
Sets up the Lennard-Jones (LJ) potential between all pairs of particle states defined in the pyMBE database.
Arguments:
- shift_potential('bool', optional): If True, a shift will be automatically computed such that the potential is continuous at the cutoff radius. Otherwise, no shift will be applied. Defaults to True.
- combining_rule('string', optional): combining rule used to calculate 'sigma' and 'epsilon' for the potential between a pair of particles. Defaults to 'Lorentz-Berthelot'.
- warning('bool', optional): switch to activate/deactivate warning messages. Defaults to True.
Notes:
- Currently, the only 'combining_rule' supported is Lorentz-Berthelot.
- Check the documentation of ESPResSo for more info about the potential https://espressomd.github.io/doc4.2.0/inter_non-bonded.html