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