Source code for arbdmodel.sim_config

# -*- coding: utf-8 -*-
"""
Simulation configuration module. Provides SimConf and DefaultSimConf 
classes for configuring simulation parameters.
"""

import os
import numpy as np
from copy import copy, deepcopy
from .logger import logger, devlogger
from .binary_manager import BinaryManager

def _get_properties_and_dict_keys(obj):
    import inspect
    cls = obj.__class__

    def filter_props(name_type):
        nt = name_type
        return not nt[0].startswith('_') and isinstance(nt[1], property)
    properties = [name for name, type_ in filter(filter_props, inspect.getmembers(obj.__class__))]
    return properties + list(obj.__dict__.keys())

[docs] class SimConf: """ Class describing properties for a simulation (ARBD or NAMD). This class stores various simulation parameters, manages binary paths, and provides methods to combine simulation configurations. Parameters: num_steps (int): Total number of simulation steps. output_period (int): Frequency of output generation. integrator (str): Type of integrator used (e.g., 'MD', 'BD'). timestep (float): Simulation time step. thermostat (str): Type of thermostat used. barostat (str): Type of barostat used. temperature (float): Simulation temperature (must be positive). pressure (float): Simulation pressure. cutoff (float): Cutoff distance for interactions. pairlist_distance (float): Distance for generating pair lists. decomp_period (int): Period for decomposition. gpu (bool): Whether to use GPU acceleration. seed (int): Random seed for reproducibility. restart_file (str): Path to restart file. # ARBD-specific parameters rigid_body_integrator (str): Type of rigid body integrator. rigid_body_grid_grid_period (int): Period for rigid body grid updates. # SimpleARBD parameters viscosity (float): Solvent viscosity. solvent_density (float): Solvent density. in g/cm^3 num_heavy_cluster (int): Number of heavy clusters. """ def __init__(self, num_steps=None, output_period=None, integrator=None, timestep=None, thermostat=None, barostat=None, temperature=None, pressure=None, cutoff=None, pairlist_distance=None, decomp_period=None, gpu=None, seed=None, restart_file=None, ## ARBD-specific rigid_body_integrator=None, rigid_body_grid_grid_period=None, ## SimpleARBD parameters viscosity=None, solvent_density=None, num_heavy_cluster=None, ## Binary paths arbd_path=None, namd_path=None, vmd_path=None, hydropro_path=None, apbs_path=None, gmsh_path=None, **kwargs): self.num_steps = num_steps self.output_period = output_period self.integrator = integrator self.timestep = timestep self.thermostat = thermostat self.barostat = barostat self.temperature = temperature self.pressure = pressure self.cutoff = cutoff self.pairlist_distance = pairlist_distance self.decomp_period = decomp_period self.seed = seed self.restart_file = restart_file self.gpu = gpu self.rigid_body_integrator = rigid_body_integrator self.rigid_body_grid_grid_period = rigid_body_grid_grid_period self.viscosity = viscosity self.solvent_density = solvent_density self.num_heavy_cluster = num_heavy_cluster # Set binary paths if arbd_path: BinaryManager.set_binary_path('arbd', arbd_path) if namd_path: BinaryManager.set_binary_path('namd', namd_path) if vmd_path: BinaryManager.set_binary_path('vmd', vmd_path) if hydropro_path: BinaryManager.set_binary_path('hydropro', hydropro_path) if apbs_path: BinaryManager.set_binary_path('apbs', apbs_path) if gmsh_path: BinaryManager.set_binary_path('gmsh', gmsh_path)
[docs] def get_binary(self, name): """ Get the path to a specific binary with improved error handling. Args: name: The name of the binary (e.g., 'arbd', 'hydropro') Returns: Path to the binary if found, None otherwise This method does not raise exceptions when binaries are not found, allowing for graceful handling of missing optional dependencies. """ binary_path = BinaryManager.get_binary_path(name) # Check if we found a binary and convert to string if needed if binary_path is not None: return str(binary_path) # Determine if this is an essential binary if BinaryManager.is_binary_essential(name): logger.warning(f"Essential binary '{name}' not found. Core functionality may be limited.") return None
[docs] def set_binary(self, name, path): """ Set the path to a specific binary. This method updates the path for a binary in the BinaryManager. It associates the given binary name with the specified file path. Parameters ---------- name : str The name of the binary to set the path for. path : str The file system path to the binary. Returns ------- str The path that was set. Examples -------- >>> config.set_binary('ffmpeg', '/usr/local/bin/ffmpeg') '/usr/local/bin/ffmpeg' """ """Set the path to a specific binary.""" BinaryManager.set_binary_path(name, path) return path
@property def temperature(self): return self.__temperature @temperature.setter def temperature(self, value): if value is not None and value <= 0: raise ValueError("Temperature must be positive") self.__temperature = value
[docs] def combine(self, other, policy='override', warn=False): """ Combines two SimConf objects into a new one based on a specified policy. This method creates a new SimConf object whose properties are initialized from 'self', but are potentially overridden with properties from 'other' according to the specified policy. Parameters ---------- other : SimConf The SimConf object whose properties may override the properties of 'self'. policy : str, optional The policy to use when combining properties. Options are: - 'override': Always use the value from 'other' if it's not None (default). - 'best': Use the more appropriate value based on property-specific rules: - For 'timestep', 'output_period', 'decomp_period': uses the minimum value. - For 'num_steps', 'cutoff', 'pairlist_distance': uses the maximum value. - For 'integrator': prefers 'MD' over 'BD'. - For other attributes: uses the value from 'other' with a warning. warn : bool, optional If True, log warnings when attribute values differ and policy='best' (default: False). Returns ------- SimConf A new SimConf object with combined properties. Raises ------ ValueError If an unrecognized policy is specified. """ new_conf = copy(self) for attr in _get_properties_and_dict_keys(other): oldval = None val = other.__getattribute__(attr) if val is not None: try: oldval = self.__getattribute__(attr) except: pass if oldval != val and (oldval is not None) and \ (val is not None) and policy != 'override': if policy == 'best': if attr in ('timestep', 'output_period', 'decomp_period'): if warn: logger.warning(f'Combining attribute {attr}: {oldval} != {val}, using {min([oldval,val])}') new_conf.__setattr__(attr, min([oldval,val])) elif attr in ('num_steps', 'cutoff', 'pairlist_distance'): if warn: logger.warning(f'Combining attribute {attr}: {oldval} != {val}, using {max([oldval,val])}') new_conf.__setattr__(attr, max([oldval,val])) elif attr == 'integrator': if 'MD' in (oldval,val) and 'BD' in (oldval,val): if warn: logger.warning(f'Combining attribute {attr}: {oldval} != {val}, using "MD"') new_conf.__setattr__(attr,'MD') else: logger.warning(f'Unsure how to combine {oldval} and {val} for {attr} under policy {policy}; using {val}') new_conf.__setattr__(attr, val) else: logger.warning(f'Unsure how to combine {oldval} and {val} for {attr} under policy {policy}; using {val}') new_conf.__setattr__(attr, val) else: raise ValueError(f'Unrecognized policy "{policy}" for combining SimConfs') else: new_conf.__setattr__(attr, val) return new_conf
[docs] def items(self): for attr in _get_properties_and_dict_keys(self): val = self.__getattribute__(attr) yield attr, val
[docs] class DefaultSimConf(SimConf): """ Generic class describing properties for a simulation with default binary paths. This class extends SimConf to provide default binary paths and configuration parameters for molecular dynamics simulations using ARBD. Parameters ---------- num_steps : float, optional Number of simulation steps to run, default 1e5. output_period : float, optional Period for output generation, default 1e3. integrator : str, optional Integration method, default 'MD'. timestep : float, optional Simulation timestep in ns, default 20e-6. thermostat : str, optional Thermostat type, default 'Langevin'. barostat : str or None, optional Barostat type if any, default None. temperature : float, optional Simulation temperature in K, default 295. pressure : float, optional Pressure value for barostat if used, default 1. cutoff : float, optional Interaction cutoff distance, default 50. pairlist_distance : float or None, optional Pairlist buffer distance, default None. decomp_period : int, optional Period for domain decomposition updates, default 40. seed : int or None, optional Random seed for simulation, default None (random). restart_file : str or None, optional Path to restart file if continuing a simulation, default None. gpu : int, optional GPU device index to use, default 0. viscosity : float, optional Solvent viscosity, default 0.01. solvent_density : float, optional Density of the solvent, default 1.0. num_heavy_cluster : int, optional Number of heavy atoms per cluster, default 3. **kwargs Additional keyword arguments to pass to SimConf. Parameters ---------- num_steps : float Number of simulation steps. output_period : float Period for output generation. pressure : float Pressure value for barostat if used. viscosity : float Solvent viscosity. solvent_density : float Density of the solvent. num_heavy_cluster : int Number of heavy atoms per cluster. Properties ---------- temperature : float Simulation temperature with validation (must be positive). Notes ----- The class automatically searches for binary dependencies like 'arbd', 'hydropro', 'apbs', 'vmd', and 'namd', and adds their paths to the configuration if found. Essential binaries (currently only 'arbd') will generate warnings if not found. """ def __init__(self, num_steps=1e5, output_period=1e3, integrator='MD', timestep=20e-6, thermostat='Langevin', barostat=None, temperature=295, pressure=1, cutoff=50, pairlist_distance=None, decomp_period=40, seed=None, restart_file=None, gpu=0, viscosity=0.01, solvent_density=1.0, num_heavy_cluster=3, **kwargs): # Set default paths only for essential binaries or those that exist default_paths = {} essential_binaries = ["arbd"] # These are required for basic functionality optional_binaries = ["hydropro", "apbs", "vmd", "namd"] # These are optional # First add essential binaries for binary_name in essential_binaries: resource_path = BinaryManager.get_binary_path(binary_name) if resource_path: default_paths[f"{binary_name}_path"] = resource_path else: logger.warning(f"Essential binary '{binary_name}' not found. Some functionality may be limited.") # Then add optional binaries only if they exist for binary_name in optional_binaries: resource_path = BinaryManager.get_binary_path(binary_name) if resource_path: default_paths[f"{binary_name}_path"] = resource_path # Initialize with binary paths and other parameters SimConf.__init__(self, num_steps=num_steps, output_period=output_period, integrator=integrator, timestep=timestep, thermostat=thermostat, barostat=barostat, temperature=temperature, pressure=pressure, cutoff=cutoff, pairlist_distance=pairlist_distance, decomp_period=decomp_period, seed=seed, restart_file=restart_file, gpu=gpu, viscosity=viscosity, solvent_density=solvent_density, num_heavy_cluster=num_heavy_cluster, **{**default_paths, **kwargs}) # User-provided values override defaults # Store these for direct access self.num_steps = num_steps self.output_period = output_period self.__temperature = temperature self.pressure = pressure self.viscosity = viscosity self.solvent_density = solvent_density self.num_heavy_cluster = num_heavy_cluster @property def temperature(self): return self.__temperature @temperature.setter def temperature(self, value): if (value <= 0): raise ValueError("Temperature must be positive") self.__temperature = value