diff --git a/.gitignore b/.gitignore index fcd20fde..5bf8bce8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ build _build -_static -_templates hymd.egg-info *.hypothesis *.mypy_cache @@ -17,6 +15,6 @@ RUN/ *.so test-xinmeng-electrostatic/* *.h5 -!examples/*.h5 +examples/ !test-xinmeng-electrostatic/run-dppc/ #!test-xinmeng-electrostatic/run-sds/ diff --git a/README.md b/README.md index 03d455f3..ef2d7195 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,12 @@ --------- **HylleraasMD** (HyMD) is a massively parallel Python package for hybrid particle-field molecular dynamics (hPF-MD) simulations of coarse-grained bio- and soft-matter systems. -HyMD can run canonical hPF-MD simulations [[1]](#1), or filtered density Hamiltonian hPF (HhPF-MD) simulations [[2]](#2), with or without explicit PME electrostatic interactions [[3]](#3). It includes all standard intramolecular interactions, including stretching, bending, torsional, and combined bending-dihedral potentials. Additionally, topological reconstruction of permanent peptide chain backbone dipoles is possible for accurate recreation of protein conformational dynamics [[4]](#4). Martini style elastic networks (ElNeDyn) [[5]](#5) are also supported. +HyMD can run canonical hPF-MD simulations, or filtered density Hamiltonian hPF (HhPF-MD) simulations [[1]](#1),[[2]](#2),[[3]](#3) with or without explicit PME electrostatic interactions. It includes all standard intramolecular interactions, +including stretching, bending, torsional, and combined bending-dihedral potentials. Additionally, topological reconstruction of permanent peptide chain backbone dipoles is possible for accurate recreation of protein conformational dynamics. +It can run simulations in constant energy (NVE), constant volume (NVT) [[1]](#1) or constant pressure (NPT) conditions [[4]](#4). -HyMD uses the [pmesh](github.com/rainwoodman/pmesh) library for particle-mesh operations, with the PPFT [[6]](#6) backend for FFTs through the [pfft-python bindings](github.com/rainwoodman/pfft-python). File IO is done via HDF5 formats to allow MPI parallel reads. +HyMD uses the [pmesh](github.com/rainwoodman/pmesh) library for particle-mesh operations, with the PPFT [[5]](#5) backend for FFTs through the [pfft-python bindings](github.com/rainwoodman/pfft-python). +File IO is done via HDF5 formats to allow MPI parallel reads. ## User Guide Detailed installation and user guide, together with comprehensive example simulations are located in the [HylleraasMD documentation](https://cascella-group-uio.github.io/HyMD/index.html). @@ -73,23 +76,24 @@ chmod +x pytest-mpi pytest-mpi -oo -n 2 -ns ``` +## Please cite our work +If you use HyMD for your purposes, please cite the appropriate references from the section below. +If you cannot cite all, the fundamental works to be cited are [[1]](#1) and [[4]](#4). + --------- ### References [1] -Milano, G.; Kawakatsu, T. Hybrid particle-field molecular dynamics simulations for dense polymer systems. J. Chem. Phys. **2009**, 130, 214106. +Ledum, M.; Sen, S.; Li, X.; Carrer, M.; Feng Y.; Cascella, M.; Bore, S. L. HylleraasMD: A Domain Decomposition-Based Hybrid Particle-Field Software for Multi-Scale Simulations of Soft Matter. ChemRxiv 2021 [2] -Bore, S. L.; Cascella, M. Hamiltonian and alias-free hybrid particle–field molecular dynamics. J. Chem. Phys. **2020**, 153, 094106. +Ledum, M.; Carrer, M.; Sen, S.; Li, X.; Cascella, M.; Bore, S. L. HyMD: Massively parallel hybrid particle-field molecular dynamics in Python. [3] -Kolli, H. B.; De Nicola, A.; Bore, S. L.; Schäfer, K.; Diezemann, G.; Gauss, J.; Kawakatsu, T.;Lu, Z.-Y.; Zhu, Y.-L.; Milano, G.; Cascella, M. Hybrid Particle-Field Molecular DynamicsSimulations of Charged Amphiphiles in an Aqueous Environment. J. Chem. Theory Comput. **2018**, 14, 4928–4937. +Bore, S. L.; Cascella, M. Hamiltonian and alias-free hybrid particle–field molecular dynamics. J. Chem. Phys. 2020, 153, 094106. [4] -Bore, S. L.; Milano, G.; Cascella, M. Hybrid Particle-Field Model for Conformational Dynamics of Peptide Chains. J. Chem. Theory Comput. **2018**, 14, 1120–1130. +Sen, S.; Ledum, M.; Bore, S. L.; Cascella, M. Soft Matter under Pressure: Pushing Particle–Field Molecular Dynamics to the Isobaric Ensemble. ChemRxiv 2023 [5] -Periole, X.; Cavalli, M.; Marrink, S. J.; Ceruso, M. A. Combining an elastic network with a coarse-grained molecular force field: structure, dynamics, and intermolecular recognition. J. Chem. Theory Comput. **2009**, 5.9, 2531-2543. - -[6] -Pippig, M. PFFT: An extension of FFTW to massively parallel architectures. SIAM J. Sci. Comput. **2013**, 35, C213–C236. +Pippig, M. PFFT: An extension of FFTW to massively parallel architectures. SIAM J. Sci. Comput. 2013, 35, C213–C236. diff --git a/config.toml b/config.toml new file mode 100644 index 00000000..6a6b13d8 --- /dev/null +++ b/config.toml @@ -0,0 +1,102 @@ +[meta] +# Name of the simulation. May be ommitted. +name = "DPPC bilayer with ML interaction parameters" +# Tags classifying the simulation. May be ommitted. +tags = ["bilayer", "solvent", "DPPC"] + +[particles] +# Number of total particles in the simulation. If an input .hdf5 format file is +# specified, the number of particles will be inferred from this and *may* be +# ommited. +n_particles = 20336 +# Mass of the particles in [g/mol]. All masses are assumed equal. +mass = 72.0 +# Maximum number of particles per molecules present in the system. A default of +# 200 is assumed, and this keyword may be ommitted for any system with smaller +# molecules. +max_molecule_size = 15 + +[simulation] +# Number of total time steps in the simulation in [picoseconds]. +n_steps = 1 +# Frequency of trajectory/energy file output in time steps. +n_print = 1 +# Frequency of requesting that the HDF5 library flush the file output buffers +# to disk after in number of n_print timesteps. +n_flush = 5000 +# Time step used in the simulation in [picoseconds]. +time_step = 0.3 +# Simulation box size in [nanometers]. +box_size = [13.0, 13.0, 14.0] +# Time integrator used in the simulation. Either "velocity-verlet" or "respa". +# If "respa", specify also the number of small rRESPA time steps per large +# time_step with the 'respa_inner' keyword. +integrator = "respa" +respa_inner = 10 +# Perform MPI rank domain decomposition every x time steps to (hopefully) +# reduce the amount of neccessary communication between ranks in the pmesh +# procedures. Ommit or set to 'false' or '0' to not perform any domain +# decomposition. +domain_decomposition = 1000 +# Starting temperature to generate before simulation begins in [kelvin]. Ommit +# or set to 'false' to not change the temperature before starting. +start_temperature = 323 +# Target temperature used in the velocity rescale thermostat in [kelvin]. Ommit +# or set to 'false' to use no thermostat, i.e. a constant energy simulation. +target_temperature = 323 +# Thermostat collision frequency in [1/picoseconds]. +tau = 0.1 +# The energy functional W[phi] to use. Options: +# "SquaredPhi": φ² / 2κφ₀, +# "DefaultNoChi": (φ - φ₀)² / 2κφ₀ +# "DefaultWithChi": (φ - φ₀)² / 2κφ₀ + Σ χφφ' / φ₀ +# Subclass Hamiltonian to create a new energy functional. +hamiltonian = 'DefaultWithChi' + +[field] +# Particle-mesh grid size, either a single integer or an array of 3 integers +# (number of grid points in each dimension). In order to guarantee consistency +# and speed in the PFFT routines, the actual mesh grid will be changed to ensure +# that each dimension of the 2d PFFT process grid divides each dimension of the +# mesh grid size. +mesh_size = [24, 24, 24] +# Compressibility used in the relaxed incompressibility term of W(phi) in +# [mol/kJ]. +kappa = 0.05 +# Standard deviation in the Gaussian filter (window function) in [nanometers]. +# This value is a characzteristic length scale for the size of the particles in +# the simulation. +sigma = 0.5 +# Interaction matrix, chi, ((atom name 1, atom name 2), (mixing energy in +# [kJ/mol])). +chi = [ + [["C", "W"], [42.24]], + [["G", "C"], [10.47]], + [["N", "W"], [-3.77]], + [["G", "W"], [4.53]], + [["N", "P"], [-9.34]], + [["P", "G"], [8.04]], + [["N", "G"], [1.97]], + [["P", "C"], [14.72]], + [["P", "W"], [-1.51]], + [["N", "C"], [13.56]], +] + +[bonds] +# Two-particle bonds, ((atom name 1, atom name 2), (equilibrium length in +# [nanometers], bond strenght in [kJ/mol])). Note the two +bonds = [ + [["N", "P"], [0.47, 1250.0]], + [["P", "G"], [0.47, 1250.0]], + [["G", "G"], [0.37, 1250.0]], + [["G", "C"], [0.47, 1250.0]], + [["C", "C"], [0.47, 1250.0]], +] +# Three-particle angular bonds, ((atom name 1, atom name 2, atom name 3), +# (equilibrium angle in [degrees], bond strenght in [kJ/mol])). +angle_bonds = [ + [["P", "G", "G"], [120.0, 25.0]], + [["P", "G", "C"], [180.0, 25.0]], + [["G", "C", "C"], [180.0, 25.0]], + [["C", "C", "C"], [180.0, 25.0]], +] diff --git a/docs/doc_pages/api.rst b/docs/doc_pages/api.rst index c9f8837b..40f0838c 100644 --- a/docs/doc_pages/api.rst +++ b/docs/doc_pages/api.rst @@ -19,6 +19,24 @@ Thermostat :undoc-members: +Barostat +-------- + +Berendsen Barostat +^^^^^^^^^^^^^^^^^^ + +.. automodule:: hymd.barostat + :members: isotropic, semiisotropic + :undoc-members: + +SCR Barostat +^^^^^^^^^^^^^^^^^^ + +.. automodule:: hymd.barostat_bussi + :members: isotropic, semiisotropic + :undoc-members: + + Hamiltonian ----------- @@ -55,6 +73,14 @@ Force :undoc-members: +Pressure +-------- + +.. automodule:: hymd.pressure + :members: comp_pressure + :undoc-members: + + Logger ------ diff --git a/docs/doc_pages/config_file.rst b/docs/doc_pages/config_file.rst index 40ee0479..2ee880b7 100644 --- a/docs/doc_pages/config_file.rst +++ b/docs/doc_pages/config_file.rst @@ -192,3 +192,53 @@ Configuration keywords specifying electrostatics and electrostatic parameters. :code:`float` [**optional**, default: :code:`None`] Specifies the relative dielectric constant of the simulation medium which regulates the strength of the electrostatic interactions. When using helical propensity dihedrals, this keyword must be specified---even if electrostatics are not included with the :code:`coulombtype` keyword. + +Pressure keywords +^^^^^^^^^^^^^^^^^ +Configuration keywords specifying pressure and barostat parameters. + +Simulation keywords +========================================= + +:pressure: + :code:`boolean` [**optional**, default: :code:`False`] + + Specifies whether or not to calculate total internal pressure vector. + +:barostat: + :code:`string` [**optional**, default: :code:`None`](options: :code:`isotropic` or :code:`semiisotropic`) + + Specifies whether to apply pressure constraints equally in all 3 Cartesian directions (`isotropic`) or equally in xy and different in z (`semiisotropic`). + +:barostat_type: + :code:`string` [**optional**, default: :code:`berendsen`](options: :code:`berendsen` or :code:`scr`) + + Specifies the type of barostat to use. :code:`berendsen` is more suitable for equilibration and :code:`scr` for equilibrium data collection. + +:n_b: + :code:`integer` [**optional**, default: :code:`1`] + + Frequency of barostat call in number of outer rRESPA steps. + +:tau_p: + :code:`float` [**optional**, default: :code:`10 tau` if :code:`tau < 0.1` else :code:`1.0`] {units: :math:`\text{ps}=10^{-12}~\text{s}`} + + The time scale of the barostat coupling. + +:target_pressure: + :code:`array` [:code:`float`] [**optional**, default: :code:1] {units: `bar`} + + Couples the system to an external pressure set to `target_pressure`. + +Field keywords +========================================= + +:rho0: + :code:`float` [**optional**, default: :code:average density] {units: :math:`\text{nm}^{-3}`} + + Intrinsic parameter corresponding to the specific volume of a coarse-grained particle. Typically: `8.66` + +:a: + :code:`float` [:required:`required`] {units: :math:`\text{nm}^{-3}`} + + Calibrated parameter to obtain the correct average density at the target temperature and pressure. Typically: `9.21` diff --git a/docs/doc_pages/pressure.rst b/docs/doc_pages/pressure.rst new file mode 100644 index 00000000..be9c7cda --- /dev/null +++ b/docs/doc_pages/pressure.rst @@ -0,0 +1,37 @@ +.. _pressure-label: + +Pressure +######## +Internal pressure is calculated from internal energy according to + +.. math:: + + P_a = \frac{1}{\mathcal{V}} \left( 2T_a - \text{Vir}_a \right) \\ + \text{Vir}_a = L_a \frac{\partial \mathcal{U}}{\partial L_a} \\ + \mathcal{U} = \sum_{i=1}^M \mathcal{U}_0( \{ \mathbf{r}\}_i ) + W[\{ \tilde\phi \} ] + +where +:math:`\mathcal{V}` is the simulation volume, +:math:`{T_a}` is the kinetic energy +and :math:`L_a` the length of the box in the Cartesian direction :math:`a`, +Vir is the virial of the total interaction energy :math:`\mathcal{U}`. + +:math:`\mathcal{U}` comprises intramolecular bonded terms :math:`\mathcal{U}_0` (see :ref:`bonds-label` for details), +and field terms :math:`W[\{ \tilde\phi \} ]` (see :ref:`theory-label` for details). + +Using the above expressions, the following form for internal pressure is obtained: + +.. math:: + + P_a = \frac{2 T_a}{\mathcal{V}} -\frac{L_a}{\mathcal{V}} \sum_{i=1}^N \frac{\partial \mathcal{U}_{0i}}{\partial L_a} + P^{(3)}_a \\ + +.. P^{(3)}_a = - \frac{L_a}{\mathcal{V}} \frac{\partial W[\{ \tilde\phi \} ]}{\partial L_a} \\ + +.. math:: + + P^{(3)}_a = \frac{1}{\mathcal{V}}\left ( -W[\{ \tilde\phi(\mathbf{r}) \}] + \int \sum_t \bar{V}_t(\mathbf{r})\tilde\phi_t(\mathbf{r})d\mathbf{r} + + \int \sum_t \sigma^2\bar{V}_t(\mathbf{r})\nabla_a^2\tilde\phi_t(\mathbf{r}) d\mathbf{r} \right) + +where :math:`\bar{V}_t(\mathbf{r}) = \frac{\partial w(\{\tilde\phi\})}{\partial\tilde\phi_t}` +and :math:`σ` is a coarse-graining parameter (see :ref:`filtering-label` for details). +Note that the above expression is obtained for a Gaussian filter which is the most natural choice in HhPF theory. diff --git a/docs/doc_pages/theory.rst b/docs/doc_pages/theory.rst index 95d7842c..2429bc0d 100644 --- a/docs/doc_pages/theory.rst +++ b/docs/doc_pages/theory.rst @@ -107,6 +107,14 @@ The default form of the interaction energy functional in HyMD is W=\frac{1}{2\phi_0}\int\mathrm{d}\mathbf{r}\sum_{\text{i},\text{j}}\tilde\chi_{\text{i}-\text{j}}\tilde\phi_\text{i}(\mathbf{r})\tilde\phi_\text{j}(\mathbf{r}) + \frac{1}{2\kappa}\left(\sum_\text{k}\tilde\phi_\text{k}(\mathbf{r})-\phi_0\right)^2. +In the case of constant pressure simulations (NPT), the interaction energy functional becomes + +.. math:: + + W=\frac{1}{2\rho_0}\int\mathrm{d}\mathbf{r}\sum_{\text{i},\text{j}}\tilde\chi_{\text{i}-\text{j}}\tilde\phi_\text{i}(\mathbf{r})\tilde\phi_\text{j}(\mathbf{r}) + \frac{1}{2\kappa}\left(\sum_\text{k}\tilde\phi_\text{k}(\mathbf{r})-a\right)^2. + +where, :math:`\rho_0= 1 / ν` is an intrinsic parameter corresponding to the specific volume :math:`(ν)` of a coarse-grained particle, +:math:`a` is a calibrated parameter to obtain the correct average density at the target temperature and pressure. See :ref:`functionals-label` for details. diff --git a/docs/doc_pages/topology_input.rst b/docs/doc_pages/topology_input.rst index adabe129..02faa501 100644 --- a/docs/doc_pages/topology_input.rst +++ b/docs/doc_pages/topology_input.rst @@ -75,3 +75,10 @@ Optional datasets :code:`datatype` [:code:`float32` or :code:`float64`] The shape represents one particle charge per :code:`N` particles. The data type may be either four or eight byte reals. + +:/box: + :code:`array` shape [:code:`3`,] + + :code:`datatype` [:code:`float32` or :code:`float64`] + + The shape represents the initial box dimensions in Cartesian coordinates. diff --git a/docs/index.rst b/docs/index.rst index ff4059ba..cfac690d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -155,6 +155,7 @@ Indices and tables ./doc_pages/theory ./doc_pages/intramolecular_bonds ./doc_pages/electrostatics + ./doc_pages/pressure .. toctree:: :maxdepth: 2 diff --git a/hymd/barostat.py b/hymd/barostat.py new file mode 100644 index 00000000..e9afc4d6 --- /dev/null +++ b/hymd/barostat.py @@ -0,0 +1,309 @@ +"""Implements the Berendsen barostat. +Scales the box and particle positions during simulation to +simulate coupling to an external pressure bath set at a +target pressure. + +It calculates the scaling factor according to: +.. math:: + + \\alpha_{L,N} = 1 - \\frac{dt n_b}{\\tau_p}\\β(P_{L,N}^t - P_{L,N}) + +where :math:`dt` is the outer rRESPA time-step, :math:`n_b` is the frequency +of barostat calls, :math:`\\tau_p` is the pressure coupling time constant, +:math:`\\beta` is the isothermal compressibility, :math:`P_{L,N}^t` and +:math:`P_{L,N}` is the target and instantaneous internal pressure in the +lateral (L) and normal (N) directions respectively. Convention: Cartesian +z-direction is considered normal. + +The box and particle positions are scaled in the L and N directions according +to the nature of the barostat (see functions `isotropic` and `semiisotropic` +below by an amount :math:`\α^{\\frac{1}{3}}`. + +The updated system information is passed on to the pmesh objects. + +References +---------- +H. J. C. Berendsen, J. P. M. Postma, W. F. van Gunsteren, +A. DiNola, and J. R. Haak , "Molecular dynamics with coupling +to an external bath", J. Chem. Phys. 81, 3684-3690 (1984) +""" +import numpy as np +from mpi4py import MPI +from dataclasses import dataclass +from typing import Union +from .pressure import comp_pressure +from .field import initialize_pm + + +@dataclass +class Target_pressure: + P_L: Union[bool, float] + P_N: Union[bool, float] + + +def isotropic( + pmesh, + pm_stuff, + phi, + phi_q, + psi, + hamiltonian, + positions, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + bond_pr, + angle_pr, + step, + prng, + comm=MPI.COMM_WORLD, +): + """ + Implements an isotropic Berendsen barostat. + The box and particle positions are scaled uniformly + in the L and N directions. + + Parameters + ---------- + pmesh : module 'pmesh.pm' + pm_stuff : list[Union(pmesh.pm.RealField, pmesh.pm.ComplexField] + List of pmesh objects. + phi : list[pmesh.pm.RealField], (M,) + Pmesh :code:`RealField` objects containing discretized particle number + density values on the computational grid; one for each particle type + :code:`M`. Pre-allocated, but empty; any values in this field are discarded. + Changed in-place. Local for each MPI rank--the full computaional grid + is represented by the collective fields of all MPI ranks. + hamiltonian : Hamiltonian + Particle-field interaction energy handler object. Defines the + grid-independent filtering function, :math:`H`. + positions : (N,D) numpy.ndarray + Array of positions for :code:`N` particles in :code:`D` dimensions. + Local for each MPI rank. + velocities : (N, D) numpy.ndarray + Array of velocities of N particles in D dimensions. + config : Config + Configuration dataclass containing simulation metadata and parameters. + phi_fft : list[pmesh.pm.ComplexField], (M,) + Pmesh :code:`ComplexField` objects containing discretized particle + number density values in reciprocal space on the computational grid; + one for each particle type. Pre-allocated, but empty; any values in + this field are discarded Changed in-place. Local for each MPI rank--the + full computaional grid is represented by the collective fields of all + MPI ranks. + phi_laplacian : list[pmesh.pm.RealField], (M, 3) + Like phi, but containing the laplacian of particle number densities. + phi_transfer : list[pmesh.pm.ComplexField], (3,) + Like phi_fourier, used as an intermediary to perform FFT operations + to obtain the gradient or laplacian of particle number densities. + bond_pr : (3,) numpy.ndarray + Total bond pressure due all two-particle bonds. + angle_pr : (3,) numpy.ndarray + Total angle pressure due all three-particle bonds. + step : integer + MD step number + prng : np.random.Generator + Numpy object that provides a stream of random bits + comm : MPI.Intracomm, optional + MPI communicator to use for rank commuication. Defaults to + MPI.COMM_WORLD. + + Returns + ------- + pm_stuff : list[Union(pmesh.pm.RealField, pmesh.pm.ComplexField] + List of modified/unmodified pmesh objects. + change : Boolean + Indicates whether or not any pmesh objects were reinitialized. + """ + rank = comm.Get_rank() + beta = 4.6 * 10 ** (-5) # bar^(-1) #isothermal compressibility of water + change = False + + if np.mod(step, config.n_b) == 0: + change = True + # compute pressure + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + positions, + bond_pr, + angle_pr, + comm=comm, + ) + + # Total pressure across all ranks + P = np.average(pressure[-3:-1]) # kJ/(mol nm^3) + P = P * 16.61 # bar + + # scaling factor + alpha = ( + 1.0 + - config.time_step + * config.n_b + / config.tau_p + * beta + * (config.target_pressure.P_L - P) + ) ** (1 / 3) + + # length scaling + config.box_size *= alpha + + # position coordinates scaling + positions *= alpha + + # pmesh re-initialize + pm_stuff = initialize_pm(pmesh, config, comm) + return (pm_stuff, change) + + +def semiisotropic( + pmesh, + pm_stuff, + phi, + phi_q, + psi, + hamiltonian, + positions, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + bond_pr, + angle_pr, + step, + prng, + comm=MPI.COMM_WORLD, +): + """ + Implements a semiisotropic Berendsen barostat. + The box and particle positions are scaled by :math:`\\alpha_L^{\\frac{1}{3}}` + in the L direction and by :math:`\\alpha_N^{\\frac{1}{3}}` in the N direction. + + Parameters + ---------- + pmesh : module 'pmesh.pm' + pm_stuff : list[Union(pmesh.pm.RealField, pmesh.pm.ComplexField] + List of pmesh objects. + phi : list[pmesh.pm.RealField], (M,) + Pmesh :code:`RealField` objects containing discretized particle number + density values on the computational grid; one for each particle type + :code:`M`. Pre-allocated, but empty; any values in this field are discarded. + Changed in-place. Local for each MPI rank--the full computaional grid + is represented by the collective fields of all MPI ranks. + hamiltonian : Hamiltonian + Particle-field interaction energy handler object. Defines the + grid-independent filtering function, :math:`H`. + positions : (N,D) numpy.ndarray + Array of positions for :code:`N` particles in :code:`D` dimensions. + Local for each MPI rank. + velocities : (N, D) numpy.ndarray + Array of velocities of N particles in D dimensions. + config : Config + Configuration dataclass containing simulation metadata and parameters. + phi_fft : list[pmesh.pm.ComplexField], (M,) + Pmesh :code:`ComplexField` objects containing discretized particle + number density values in reciprocal space on the computational grid; + one for each particle type. Pre-allocated, but empty; any values in + this field are discarded Changed in-place. Local for each MPI rank--the + full computaional grid is represented by the collective fields of all + MPI ranks. + phi_laplacian : list[pmesh.pm.RealField], (M, 3) + Like phi, but containing the laplacian of particle number densities. + phi_transfer : list[pmesh.pm.ComplexField], (3,) + Like phi_fourier, used as an intermediary to perform FFT operations + to obtain the gradient or laplacian of particle number densities. + bond_pr : (3,) numpy.ndarray + Total bond pressure due all two-particle bonds. + angle_pr : (3,) numpy.ndarray + Total angle pressure due all three-particle bonds. + step : integer + MD step number + prng : np.random.Generator + Numpy object that provides a stream of random bits + comm : MPI.Intracomm, optional + MPI communicator to use for rank commuication. Defaults to + MPI.COMM_WORLD. + + Returns + ------- + pm_stuff : list[Union(pmesh.pm.RealField, pmesh.pm.ComplexField] + List of modified/unmodified pmesh objects. + change : Boolean + Indicates whether or not any pmesh objects were reinitialized. + """ + rank = comm.Get_rank() + beta = 4.6 * 10 ** (-5) # bar^(-1) #isothermal compressibility of water + change = False + if np.mod(step, config.n_b) == 0: + change = True + + # compute pressure + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + positions, + bond_pr, + angle_pr, + comm=comm, + ) + + # Total pressure across all ranks + # L: Lateral; N: Normal + [PL, PN] = [0, 0] + PL = (pressure[-3] + pressure[-2]) / 2 # kJ/(mol nm^3) + PN = pressure[-1] # kJ/(mol nm^3) + PL = PL * 16.61 # bar + PN = PN * 16.61 # bar + alphaL = 1.0 + alphaN = 1.0 + + if config.target_pressure.P_L: + # scaling factor + alphaL = ( + 1.0 + - config.time_step + * config.n_b + / config.tau_p + * beta + * (config.target_pressure.P_L - PL) + ) ** (1 / 3) + # length scaling + config.box_size[0:2] *= alphaL + + positions[:][0:2] *= alphaL + + if config.target_pressure.P_N: + # scaling factor + alphaN = ( + 1.0 + - config.time_step + * config.n_b + / config.tau_p + * beta + * (config.target_pressure.P_N - PN) + ) ** (1 / 3) + # length scaling + config.box_size[2] *= alphaN + + positions[:][2] *= alphaN + + # pmesh re-initialize + pm_stuff = initialize_pm(pmesh, config, comm) + return (pm_stuff, change) diff --git a/hymd/barostat_scr.py b/hymd/barostat_scr.py new file mode 100644 index 00000000..10489d5b --- /dev/null +++ b/hymd/barostat_scr.py @@ -0,0 +1,284 @@ +"""Implements the stochastic cell rescaling (SCR) barostat. +This is a first-order barostat that samples the correct +volume fluctuations through a suitable noise term. +Scales the box and particle positions by a factor (see functions +`isotropic` and `semiisotropic` for details) during simulation to +simulate coupling to an external pressure bath set at a +target pressure. + +The box and particle positions are scaled in the L and N directions according +to the nature of the barostat (see functions `isotropic` and `semiisotropic` +below by an amount :math:`\\alpha`. + +The updated system information is passed on to the pmesh objects. + +References +---------- +Mattia Bernetti and Giovanni Bussi , "Pressure control using stochastic +cell rescaling", J. Chem. Phys. 153, 114107 (2020) +""" +import numpy as np +from mpi4py import MPI +from dataclasses import dataclass +from typing import Union +from .pressure import comp_pressure +from .field import initialize_pm + + +@dataclass +class Target_pressure: + P_L: Union[bool, float] + P_N: Union[bool, float] + + +def isotropic( + pmesh, + pm_stuff, + phi, + phi_q, + psi, + hamiltonian, + positions, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + bond_pr, + angle_pr, + step, + prng, + comm=MPI.COMM_WORLD, +): + """ + It calculates the scaling factor according to + + .. math:: + + \\log \\alpha' = - \\frac{n_b\,dt\,\\beta}{\\tau_p}(P^t - P) + \\sqrt{\\frac{2n_b^2k_BT\\beta\,dt}{\mathcal{V}\\tau_p}}dW + + .. math:: + + \\alpha = \\exp{\\frac{1}{3}\\log \\alpha'} + + where :math:`dt` is the outer rRESPA time-step, :math:`n_b` is the frequency + of barostat calls, :math:`\\tau_p` is the pressure coupling time constant, + :math:`\\beta` is the isothermal compressibility, :math:`P_{L,N}^t` and + :math:`P_{L,N}` is the target and instantaneous internal pressure in the + lateral (L) and normal (N) directions respectively. Convention: Cartesian + z-direction is considered normal. + """ + rank = comm.Get_rank() + beta = 7.6 * 10 ** (-4) # bar^(-1) #isothermal compressibility of water + change = False + + if np.mod(step, config.n_b) == 0: + + R = prng.normal() + + change = True + # compute pressure + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + positions, + bond_pr, + angle_pr, + comm=comm, + ) + + # Total pressure across all ranks + P = np.average(pressure[-3:-1]) # kJ/(mol nm^3) + P = P * 16.61 # bar + + V = np.prod(config.box_size) + noise_term = ( + np.sqrt( + 2.0 + * config.n_b + * config.gas_constant + * config.target_temperature + * beta + * config.time_step + * config.n_b + / (V * config.tau_p) + ) + * R + ) + log_alpha = ( + -config.n_b + * config.time_step + * beta + / config.tau_p + * (config.target_pressure.P_L - P) + ) + log_alpha = log_alpha + noise_term + alpha = np.exp(log_alpha / 3.0) + + config.box_size *= alpha + + # position coordinates scaling + positions *= alpha + + # pmesh re-initialize + pm_stuff = initialize_pm(pmesh, config, comm) + return (pm_stuff, False) + + +def semiisotropic( + pmesh, + pm_stuff, + phi, + phi_q, + psi, + hamiltonian, + positions, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + bond_pr, + angle_pr, + step, + prng, + comm=MPI.COMM_WORLD, +): + """ + It calculates the scaling factor according to + + .. math:: + + \\log \\alpha'_{L} = - \\frac{2n_b\,dt\,\\beta}{3\\tau_p}(P_L^t - P_L - \\frac{\\gamma}{L_z}) + \\sqrt{\\frac{4n_b^2k_BT\\beta\,dt}{3\mathcal{V}\\tau_p}}dW_L + + .. math:: + + \\alpha_L = \\exp{\\frac{1}{2}\\log \\alpha'_L} + + .. math:: + + \\log \\alpha'_{N} = - \\frac{n_b\,dt\,\\beta}{3\\tau_p}(P_N^t - P_N) + \\sqrt{\\frac{2n_b^2k_BT\\beta\,dt}{3\mathcal{V}\\tau_p}}dW_N + + .. math:: + + \\alpha_L = \\log \\alpha'_N + + where :math:`dt` is the outer rRESPA time-step, :math:`n_b` is the frequency + of barostat calls, :math:`\\tau_p` is the pressure coupling time constant, + :math:`\\beta` is the isothermal compressibility, :math:`P_{L,N}^t` and + :math:`P_{L,N}` is the target and instantaneous internal pressure in the + lateral (L) and normal (N) directions respectively, :math:`\\gamma` is the + surface tension. Convention: Cartesian z-direction is considered normal. + """ + rank = comm.Get_rank() + beta = 7.6 * 10 ** (-4) # isothermal compressibility of water + change = False + if np.mod(step, config.n_b) == 0: + Rxy = prng.normal() + Rz = prng.normal() + change = True + # compute pressure + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fft, + phi_laplacian, + phi_transfer, + positions, + bond_pr, + angle_pr, + comm=comm, + ) + + # Total pressure across all ranks + # L: Lateral; N: Normal + [PL, PN] = [0, 0] + PL = (pressure[-3] + pressure[-2]) / 2 # kJ/(mol nm^3) + PN = pressure[-1] # kJ/(mol nm^3) + PL = PL * 16.61 # bar + PN = PN * 16.61 # bar + alphaL = 1.0 + alphaN = 1.0 + config.surface_tension = config.box_size[2] / 2 * (PN - PL) # bar nm + + if config.target_pressure.P_L: + V = np.prod(config.box_size) + noise_term = ( + np.sqrt( + 4.0 + * config.n_b + * config.gas_constant + * config.target_temperature + * beta + * config.time_step + * config.n_b + / (3 * V * config.tau_p) + ) + * Rxy + ) + log_alpha = ( + -2.0 + * config.n_b + * config.time_step + * beta + / (3 * config.tau_p) + * ( + config.target_pressure.P_L + - PL + - config.surface_tension / config.box_size[2] + ) + ) + log_alpha = log_alpha + noise_term + alpha = np.exp( + log_alpha / 2.0 + ) # not 100% sure about this factor 2, have to check it out <<< TODO + + config.box_size[0:2] *= alpha + + positions[:][0:2] *= alpha + + if config.target_pressure.P_N: + V = np.prod(config.box_size) + noise_term = ( + np.sqrt( + 2.0 + * config.n_b + * config.gas_constant + * config.target_temperature + * beta + * config.time_step + * config.n_b + / (3 * V * config.tau_p) + ) + * Rz + ) + log_alpha = ( + -config.n_b + * config.time_step + * beta + / (3 * config.tau_p) + * (config.target_pressure.P_N - PN) + ) + log_alpha = log_alpha + noise_term + alpha = np.exp( + log_alpha / 1.0 + ) # not 100% sure about this factor 2, have to check it out <<< TODO + + config.box_size[2] *= alpha + + positions[:][2] *= alpha + + # pmesh re-initialize + pm_stuff = initialize_pm(pmesh, config, comm) + return (pm_stuff, change) diff --git a/hymd/compute_angle_forces.f90 b/hymd/compute_angle_forces.f90 index 7ad85861..0393a84c 100644 --- a/hymd/compute_angle_forces.f90 +++ b/hymd/compute_angle_forces.f90 @@ -1,4 +1,4 @@ -subroutine caf(f, r, box, a, b, c, t0, k, energy) +subroutine caf(f, r, box, a, b, c, t0, k, energy, angle_pr) ! Compute three-particle bond forces and energy ! ! Parameters @@ -23,11 +23,13 @@ subroutine caf(f, r, box, a, b, c, t0, k, energy) ! Returns ! ------- ! energy : float - ! Total energy of all two-particle bonds. + ! Total energy of all three-particle bonds. + ! angle_pr : (3,) numpy.ndarray + ! Total angle pressure due all three-particle bonds. ! implicit none - real(4), dimension(:,:), intent(in out) :: f + real(4), dimension(:,:), intent(inout) :: f real(4), dimension(:,:), intent(in) :: r real(8), dimension(:), intent(in) :: box integer, dimension(:), intent(in) :: a @@ -35,7 +37,8 @@ subroutine caf(f, r, box, a, b, c, t0, k, energy) integer, dimension(:), intent(in) :: c real(8), dimension(:), intent(in) :: t0 real(8), dimension(:), intent(in) :: k - real(8), intent(out) :: energy + real(8), intent(out) :: energy + real(4), dimension(3), intent(out) :: angle_pr integer :: ind, aa, bb, cc real(8), dimension(3) :: ra, rc, ea, ec, fa, fc @@ -44,6 +47,7 @@ subroutine caf(f, r, box, a, b, c, t0, k, energy) real(8) :: cosphi, cosphi2, sinphi, theta energy = 0.0d00 + angle_pr = 0.0d00 f = 0.0d00 do ind = 1, size(a) @@ -74,7 +78,7 @@ subroutine caf(f, r, box, a, b, c, t0, k, energy) xrasin = -ff / (norm_a * sinphi) xrcsin = -ff / (norm_c * sinphi) - ! 𝜕θ/𝜕cos(θ) * 𝜕cos(θ)/𝜕r + ! d theta / d cos(theta) * d cos(theta) / dr fa = (ec - cosphi * ea) * xrasin fc = (ea - cosphi * ec) * xrcsin @@ -83,6 +87,7 @@ subroutine caf(f, r, box, a, b, c, t0, k, energy) f(bb, :) = f(bb, :) + fa + fc energy = energy + 0.5d0 * ff * d + angle_pr = angle_pr - (fa * ra) - (fc * rc) end if end do end subroutine diff --git a/hymd/compute_angle_forces__double.f90 b/hymd/compute_angle_forces__double.f90 index e5dbeae1..7645ba32 100644 --- a/hymd/compute_angle_forces__double.f90 +++ b/hymd/compute_angle_forces__double.f90 @@ -1,4 +1,4 @@ -subroutine caf_d(f, r, box, a, b, c, t0, k, energy) +subroutine caf_d(f, r, box, a, b, c, t0, k, energy, angle_pr) ! Compute three-particle bond forces and energy ! ! Parameters @@ -27,7 +27,7 @@ subroutine caf_d(f, r, box, a, b, c, t0, k, energy) ! implicit none - real(8), dimension(:,:), intent(in out) :: f + real(8), dimension(:,:), intent(inout) :: f real(8), dimension(:,:), intent(in) :: r real(8), dimension(:), intent(in) :: box integer, dimension(:), intent(in) :: a @@ -35,7 +35,8 @@ subroutine caf_d(f, r, box, a, b, c, t0, k, energy) integer, dimension(:), intent(in) :: c real(8), dimension(:), intent(in) :: t0 real(8), dimension(:), intent(in) :: k - real(8), intent(out) :: energy + real(8), intent(out) :: energy + real(8), dimension(3), intent(out) :: angle_pr integer :: ind, aa, bb, cc real(8), dimension(3) :: ra, rc, ea, ec, fa, fc @@ -44,6 +45,7 @@ subroutine caf_d(f, r, box, a, b, c, t0, k, energy) real(8) :: cosphi, cosphi2, sinphi, theta energy = 0.0d00 + angle_pr = 0.0d00 f = 0.0d00 do ind = 1, size(a) @@ -74,7 +76,7 @@ subroutine caf_d(f, r, box, a, b, c, t0, k, energy) xrasin = -ff / (norm_a * sinphi) xrcsin = -ff / (norm_c * sinphi) - ! 𝜕θ/𝜕cos(θ) * 𝜕cos(θ)/𝜕r + ! d theta / d cos(theta) * d cos(theta) / dr fa = (ec - cosphi * ea) * xrasin fc = (ea - cosphi * ec) * xrcsin @@ -83,6 +85,7 @@ subroutine caf_d(f, r, box, a, b, c, t0, k, energy) f(bb, :) = f(bb, :) + fa + fc energy = energy + 0.5d0 * ff * d + angle_pr = angle_pr - (fa * ra) - (fc * rc) end if end do end subroutine diff --git a/hymd/compute_bond_forces.f90 b/hymd/compute_bond_forces.f90 index f6c2b97c..608c9136 100644 --- a/hymd/compute_bond_forces.f90 +++ b/hymd/compute_bond_forces.f90 @@ -1,4 +1,4 @@ -subroutine cbf(f, r, box, a, b, r0, k, energy) +subroutine cbf(f, r, box, a, b, r0, k, energy, bond_pr) ! Compute two-particle bond forces and energy ! ! Parameters @@ -33,6 +33,7 @@ subroutine cbf(f, r, box, a, b, r0, k, energy) real(8), dimension(:), intent(in) :: r0 real(8), dimension(:), intent(in) :: k real(8), intent(out) :: energy + real(4), dimension(3), intent(out) :: bond_pr integer :: ind, aa, bb real(8), dimension(3) :: rab, fa @@ -56,5 +57,6 @@ subroutine cbf(f, r, box, a, b, r0, k, energy) f(bb, :) = f(bb, :) + fa energy = energy + 0.5d00 * k(ind) * (rab_norm - r0(ind))**2 + bond_pr = bond_pr + fa * rab end do -end subroutine +end subroutine \ No newline at end of file diff --git a/hymd/compute_bond_forces__double.f90 b/hymd/compute_bond_forces__double.f90 index 8c201a97..5585b262 100644 --- a/hymd/compute_bond_forces__double.f90 +++ b/hymd/compute_bond_forces__double.f90 @@ -1,4 +1,4 @@ -subroutine cbf_d(f, r, box, a, b, r0, k, energy) +subroutine cbf_d(f, r, box, a, b, r0, k, energy, bond_pr) ! Compute two-particle bond forces and energy ! ! Parameters @@ -33,6 +33,7 @@ subroutine cbf_d(f, r, box, a, b, r0, k, energy) real(8), dimension(:), intent(in) :: r0 real(8), dimension(:), intent(in) :: k real(8), intent(out) :: energy + real(8), dimension(3), intent(out) :: bond_pr integer :: ind, aa, bb real(8), dimension(3) :: rab, fa @@ -56,5 +57,6 @@ subroutine cbf_d(f, r, box, a, b, r0, k, energy) f(bb, :) = f(bb, :) + fa energy = energy + 0.5d00 * k(ind) * (rab_norm - r0(ind))**2 + bond_pr = bond_pr + fa * rab end do -end subroutine +end subroutine \ No newline at end of file diff --git a/hymd/compute_dihedral_forces.f90 b/hymd/compute_dihedral_forces.f90 index 27f9eea3..be8435e4 100644 --- a/hymd/compute_dihedral_forces.f90 +++ b/hymd/compute_dihedral_forces.f90 @@ -2,10 +2,10 @@ subroutine cdf(force, r, dipoles, transfer_matrix, box, a, b, c, d, coeff, dtype use dipole_reconstruction implicit none - real(4), intent(in out) :: force(:,:) + real(4), intent(inout) :: force(:,:) real(4), intent(in) :: r(:,:) - real(4), intent(in out) :: dipoles(:,:,:) - real(4), intent(in out) :: transfer_matrix(:,:,:,:) + real(4), intent(inout) :: dipoles(:,:,:) + real(4), intent(inout) :: transfer_matrix(:,:,:,:) real(8), intent(in) :: box(:) integer, intent(in) :: a(:), b(:), c(:), d(:), dtype(:), bb_index(:), dipole_flag real(4), intent(in) :: coeff(:,:,:) diff --git a/hymd/compute_dihedral_forces__double.f90 b/hymd/compute_dihedral_forces__double.f90 index 6d632733..2e42ac23 100644 --- a/hymd/compute_dihedral_forces__double.f90 +++ b/hymd/compute_dihedral_forces__double.f90 @@ -2,10 +2,10 @@ subroutine cdf_d(force, r, dipoles, transfer_matrix, box, a, b, c, d, coeff, dty use dipole_reconstruction_d implicit none - real(8), intent(in out) :: force(:,:) + real(8), intent(inout) :: force(:,:) real(8), intent(in) :: r(:,:) - real(8), intent(in out) :: dipoles(:,:,:) - real(8), intent(in out) :: transfer_matrix(:,:,:,:) + real(8), intent(inout) :: dipoles(:,:,:) + real(8), intent(inout) :: transfer_matrix(:,:,:,:) real(8), intent(in) :: box(:) integer, intent(in) :: a(:), b(:), c(:), d(:), dtype(:), bb_index(:), dipole_flag real(4), intent(in) :: coeff(:,:,:) diff --git a/hymd/configure_runtime.py b/hymd/configure_runtime.py index cb2d1188..d71bc909 100644 --- a/hymd/configure_runtime.py +++ b/hymd/configure_runtime.py @@ -32,73 +32,103 @@ def configure_runtime(args_in, comm): ap = ArgumentParser() ap.add_argument( - "-v", "--verbose", default=1, type=int, nargs="?", + "-v", + "--verbose", + default=1, + type=int, + nargs="?", help="Increase logging verbosity", ) ap.add_argument( - "--profile", default=False, action="store_true", + "--profile", + default=False, + action="store_true", help="Profile program execution with cProfile", ) ap.add_argument( - "--disable-field", default=False, action="store_true", + "--disable-field", + default=False, + action="store_true", help="Disable field forces", ) ap.add_argument( - "--disable-bonds", default=False, action="store_true", + "--disable-bonds", + default=False, + action="store_true", help="Disable two-particle bond forces", ) ap.add_argument( - "--disable-angle-bonds", default=False, action="store_true", + "--disable-angle-bonds", + default=False, + action="store_true", help="Disable three-particle angle bond forces", ) ap.add_argument( - "--disable-dihedrals", default=False, action="store_true", + "--disable-dihedrals", + default=False, + action="store_true", help="Disable four-particle dihedral forces", ) ap.add_argument( - "--disable-dipole", default=False, action="store_true", + "--disable-dipole", + default=False, + action="store_true", help="Disable BB dipole calculation", ) ap.add_argument( - "--double-precision", default=False, action="store_true", + "--double-precision", + default=False, + action="store_true", help="Use double precision positions/velocities", ) ap.add_argument( - "--double-output", default=False, action="store_true", + "--double-output", + default=False, + action="store_true", help="Use double precision in output h5md", ) ap.add_argument( - "--dump-per-particle", default=False, action="store_true", + "--dump-per-particle", + default=False, + action="store_true", help="Log energy values per particle, not total", ) ap.add_argument( - "--force-output", default=False, action="store_true", + "--force-output", + default=False, + action="store_true", help="Dump forces to h5md output", ) ap.add_argument( - "--velocity-output", default=False, action="store_true", + "--velocity-output", + default=False, + action="store_true", help="Dump velocities to h5md output", ) ap.add_argument( - "--disable-mpio", default=False, action="store_true", - help=( - "Avoid using h5py-mpi, potentially decreasing IO " "performance" - ), + "--disable-mpio", + default=False, + action="store_true", + help=("Avoid using h5py-mpi, potentially decreasing IO " "performance"), ) ap.add_argument( "--destdir", default=".", help="Write output to specified directory" ) ap.add_argument( - "--seed", default=None, type=int, + "--seed", + default=None, + type=int, help="Set the numpy random generator seed for every rank", ) ap.add_argument( - "--logfile", default="sim.log", + "--logfile", + default="sim.log", help="Redirect event logging to specified file", ) ap.add_argument( - "config", help="Config .py or .toml input configuration script" + "-p", "--topol", default=None, help="Gmx-like topology file in toml format" ) + ap.add_argument("config", help="Config .py or .toml input configuration script") ap.add_argument("input", help="input.hdf5") args = ap.parse_args(args_in) @@ -108,7 +138,7 @@ def configure_runtime(args_in, comm): # Safely define seeds seeds = None - if comm.Get_rank() == 0: + if comm.Get_rank() == 0: if args.seed is not None: ss = np.random.SeedSequence(args.seed) else: @@ -154,8 +184,22 @@ def profile_atexit(): try: Logger.rank0.log( logging.INFO, - f"Attempting to parse config file {args.config} as "".toml", + f"Attempting to parse config file {args.config} as " ".toml", ) + + if args.topol is not None: + topol = read_config_toml(args.topol) + # Check if we have single "itp" files and add their keys to topol + if os.path.dirname(args.topol) == "": + args.topol = "./" + args.topol + if "include" in topol["system"]: + for file in topol["system"]["include"]: + path = f"{os.path.dirname(args.topol)}/{file}" + itps = read_config_toml(path) + for mol, itp in itps.items(): + topol[mol] = itp + else: + topol = None toml_config = read_config_toml(args.config) config = parse_config_toml( toml_config, file_path=os.path.abspath(args.config), comm=comm @@ -170,4 +214,4 @@ def profile_atexit(): f"Unable to parse configuration file {args.config}" f"\n\ntoml parse traceback:" + repr(ve) ) - return args, config, prng + return args, config, prng, topol diff --git a/hymd/dipole_reconstruction.f90 b/hymd/dipole_reconstruction.f90 index 6ff5c0e9..34ba421a 100644 --- a/hymd/dipole_reconstruction.f90 +++ b/hymd/dipole_reconstruction.f90 @@ -111,13 +111,13 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt if (sin_gamma < 0.1) then print *, "DIHEDRAL ROUTINE WARNING (bending potential):" - print '(a, f5.2, a)', "The angle γ =", gamm, " is too close to 0 or π." - print *, "There's probably something wrong with the simulation. Setting sin(γ) = 0.1" + print '(a, f5.2, a)', "The angle gamma =", gamm, " is too close to 0 or pi." + print *, "There's probably something wrong with the simulation. Setting sin(gamma) = 0.1" sin_gamma = 0.1 end if ! Bending "forces" == f_gamma_i in the paper - ! 1/sin(γ) ∂cos(γ)/∂γ + ! 1 / sin(gamma) d cos(gamma) / d gamma fa = (v - cos_gamma * w) / norm_a fc = (w - cos_gamma * v) / norm_c @@ -143,7 +143,7 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt end if ! 2 - Dipole reconstruction - ! θ(γ) + ! theta(gamma) ! This function needs to be fit again fac = exp((gamm - 1.73d0) / 0.025d0) theta = -1.607d0 * gamm + 0.094d0 + 1.883d0 / (1.d0 + fac) @@ -186,7 +186,7 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt ! Last term is 0 for N_a, second term is 0 for N_c (S19) ! Minus in the last term because inverse cross_matrix - ! 1 / sin(γ) is already inside fa, fb, and fc + ! 1 / sin(gamma) is already inside fa, fb, and fc N_a = (cos_gamma * outer_product(fa, n) + cross_matrix(W_a, v) ) / sin_gamma N_b = (cos_gamma * outer_product(fb, n) + cross_matrix(W_b, v) - cross_matrix(V_b, w)) / sin_gamma N_c = (cos_gamma * outer_product(fc, n) - cross_matrix(V_c, w)) / sin_gamma @@ -195,9 +195,9 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt M_b = cross_matrix(N_b, v) - cross_matrix(V_b, n) M_c = cross_matrix(N_c, v) - cross_matrix(V_c, n) - ! A lot of terms in (S10) go away because ∂φ/∂γ = 0, - ! since φ = const. - ! 1 / sin(γ) is already inside fa, fb, and fc + ! A lot of terms in (S10) go away because d phi / d gamma = 0, + ! since phi = const. + ! 1 / sin(gamma) is already inside fa, fb, and fc FN_a = sin_theta * d_theta * outer_product(fa, n) FN_b = sin_theta * d_theta * outer_product(fb, n) FN_c = sin_theta * d_theta * outer_product(fc, n) diff --git a/hymd/dipole_reconstruction__double.f90 b/hymd/dipole_reconstruction__double.f90 index 76a758cc..f8a58de2 100644 --- a/hymd/dipole_reconstruction__double.f90 +++ b/hymd/dipole_reconstruction__double.f90 @@ -111,13 +111,13 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt if (sin_gamma < 0.1) then print *, "DIHEDRAL ROUTINE WARNING (bending potential):" - print '(a, f5.2, a)', "The angle γ =", gamm, " is too close to 0 or π." - print *, "There's probably something wrong with the simulation. Setting sin(γ) = 0.1" + print '(a, f5.2, a)', "The angle gamma =", gamm, " is too close to 0 or pi." + print *, "There's probably something wrong with the simulation. Setting sin(gamma) = 0.1" sin_gamma = 0.1 end if ! Bending "forces" == f_gamma_i in the paper - ! 1/sin(γ) ∂cos(γ)/∂γ + ! 1 / sin(gamma) d cos(gamma) / d gamma fa = (v - cos_gamma * w) / norm_a fc = (w - cos_gamma * v) / norm_c @@ -143,7 +143,7 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt end if ! 2 - Dipole reconstruction - ! θ(γ) + ! theta(gamma) ! This function needs to be fit again fac = exp((gamm - 1.73d0) / 0.025d0) theta = -1.607d0 * gamm + 0.094d0 + 1.883d0 / (1.d0 + fac) @@ -186,7 +186,7 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt ! Last term is 0 for N_a, second term is 0 for N_c (S19) ! Minus in the last term because inverse cross_matrix - ! 1 / sin(γ) is already inside fa, fb, and fc + ! 1 / sin(gamma) is already inside fa, fb, and fc N_a = (cos_gamma * outer_product(fa, n) + cross_matrix(W_a, v) ) / sin_gamma N_b = (cos_gamma * outer_product(fb, n) + cross_matrix(W_b, v) - cross_matrix(V_b, w)) / sin_gamma N_c = (cos_gamma * outer_product(fc, n) - cross_matrix(V_c, w)) / sin_gamma @@ -195,9 +195,9 @@ subroutine reconstruct(rab, rb, rcb, box, c_k, d_k, phi, dipole_flag, energy_cbt M_b = cross_matrix(N_b, v) - cross_matrix(V_b, n) M_c = cross_matrix(N_c, v) - cross_matrix(V_c, n) - ! A lot of terms in (S10) go away because ∂φ/∂γ = 0, - ! since φ = const. - ! 1 / sin(γ) is already inside fa, fb, and fc + ! A lot of terms in (S10) go away because d phi / d gamma = 0, + ! since phi = const. + ! 1 / sin(gamma) is already inside fa, fb, and fc FN_a = sin_theta * d_theta * outer_product(fa, n) FN_b = sin_theta * d_theta * outer_product(fb, n) FN_c = sin_theta * d_theta * outer_product(fc, n) diff --git a/hymd/field.py b/hymd/field.py index 9936e6b4..b264c963 100644 --- a/hymd/field.py +++ b/hymd/field.py @@ -4,6 +4,149 @@ import numpy as np from mpi4py import MPI from .logger import Logger +import warnings + + +def initialize_pm(pmesh, config, comm=MPI.COMM_WORLD): + """ + Creates the necessary pmesh objects for pfft operations. + + Parameters + ---------- + pmesh : module 'pmesh.pm' + config : Config + Configuration dataclass containing simulation metadata and parameters. + comm : MPI.Intracomm, optional + MPI communicator to use for rank commuication. Defaults to + MPI.COMM_WORLD. + + Returns + ------- + pm : object 'pmesh.pm.ParticleMesh' + field_list : list[pmesh.pm.RealField], (multiple) + Essential list of pmesh objects required for MD + list_coulomb : list[pmesh.pm.RealField], (multiple) + Additional list of pmesh objects required for electrostatics. + """ + + if config.dtype == np.float64: + pmeshtype = "f8" + else: + pmeshtype = "f4" + # Ignore numpy numpy.VisibleDeprecationWarning: Creating an ndarray from + # ragged nested sequences until it is fixed in pmesh + with warnings.catch_warnings(): + warnings.filterwarnings( + action="ignore", + category=np.VisibleDeprecationWarning, + message=r"Creating an ndarray from ragged nested sequences", + ) + # The first argument of ParticleMesh has to be a tuple + pm = pmesh.ParticleMesh( + config.mesh_size, BoxSize=config.box_size, dtype=pmeshtype, comm=comm + ) + phi = [pm.create("real", value=0.0) for _ in range(config.n_types)] + phi_fourier = [ + pm.create("complex", value=0.0) for _ in range(config.n_types) + ] # noqa: E501 + force_on_grid = [ + [pm.create("real", value=0.0) for d in range(3)] for _ in range(config.n_types) + ] + v_ext_fourier = [pm.create("complex", value=0.0) for _ in range(4)] + v_ext = [pm.create("real", value=0.0) for _ in range(config.n_types)] + + phi_transfer = [pm.create("complex", value=0.0) for _ in range(3)] + + phi_laplacian = [ + [pm.create("real", value=0.0) for d in range(3)] for _ in range(config.n_types) + ] + + # Initialize charge density fields + coulomb_list = [] + elec_common_list = [None, None, None, None] + _SPACE_DIM = config.box_size.size + + if config.coulombtype == "PIC_Spectral_GPE" or config.coulombtype == "PIC_Spectral": + phi_q = pm.create("real", value=0.0) + phi_q_fourier = pm.create("complex", value=0.0) + psi = pm.create("real", value=0.0) + elec_field = [pm.create("real", value=0.0) for _ in range(_SPACE_DIM)] + + elec_common_list = [phi_q, phi_q_fourier, psi, elec_field] + + if config.coulombtype == "PIC_Spectral": + elec_field_fourier = [ + pm.create("complex", value=0.0) for _ in range(_SPACE_DIM) + ] # for force calculation + psi_fourier = pm.create("complex", value=0.0) + + coulomb_list = [ + elec_field_fourier, + psi_fourier, + ] + + if ( + config.coulombtype == "PIC_Spectral_GPE" + ): ## initializing the density mesh #dielectric_flag + phi_eps = pm.create( + "real", value=0.0 + ) ## real contrib of the epsilon dielectric painted to grid + phi_eps_fourier = pm.create("complex", value=0.0) # complex contrib of phi eps + phi_eta = [ + pm.create("real", value=0.0) for _ in range(_SPACE_DIM) + ] ## real contrib of factor in polarization charge density + phi_eta_fourier = [ + pm.create("complex", value=0.0) for _ in range(_SPACE_DIM) + ] ## fourier of factor in polarization charge density + phi_pol = pm.create( + "real", value=0.0 + ) ## real contrib of the polarization charge + phi_pol_prev = pm.create("real", value=0.0) + elec_dot = pm.create("real", value=0.0) + elec_field_contrib = pm.create( + "real", value=0.0 + ) # needed for pol energies later + + # External potential and force meshes + Vbar_elec = [pm.create("real", value=0.0) for _ in range(config.n_types)] + Vbar_elec_fourier = [ + pm.create("complex", value=0.0) for _ in range(config.n_types) + ] + force_mesh_elec = [ + [pm.create("real", value=0.0) for d in range(3)] + for _ in range(config.n_types) + ] + force_mesh_elec_fourier = [ + [pm.create("complex", value=0.0) for d in range(3)] + for _ in range(config.n_types) + ] + + coulomb_list = [ + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol, + phi_pol_prev, + elec_dot, + elec_field_contrib, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + ] + + field_list = [ + phi, + phi_fourier, + force_on_grid, + v_ext_fourier, + v_ext, + phi_transfer, + phi_laplacian, + ] + + return (pm, field_list, elec_common_list, coulomb_list) def compute_field_force(layouts, r, force_mesh, force, types, n_types): @@ -38,7 +181,7 @@ def compute_field_force(layouts, r, force_mesh, force, types, n_types): Pmesh :code:`RealField` objects containing discretized particle-field force density values on the computational grid; :code:`D` fields in D dimensions for each particle type. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI + computational grid is represented by the collective fields of all MPI ranks. force : (N,D) numpy.ndarray Array of forces for :code:`N` particles in :code:`D` dimensions. Local @@ -87,93 +230,28 @@ def compute_self_energy_q(config, charges, comm=MPI.COMM_WORLD): """ elec_conversion_factor = config.coulomb_constant / config.dielectric_const - prefac = elec_conversion_factor * np.sqrt(1./(2.*np.pi*config.sigma*config.sigma)) + prefac = elec_conversion_factor * np.sqrt( + 1.0 / (2.0 * np.pi * config.sigma * config.sigma) + ) _squared_charges = charges * charges squared_charges_sum = comm.allreduce(np.sum(_squared_charges)) return prefac * squared_charges_sum -def compute_field_energy_q( - config, phi_q, phi_q_fourier, elec_potential, elec_potential_fourier, - field_q_self_energy, comm=MPI.COMM_WORLD, -): - """Calculate the electrostatic energy from a field configuration - - From the definition of the elecrostatic potential :math:`\\Psi`, the energy - is - - .. math:: - - E = \\frac{1}{2\\varepsilon_0 \\varepsilon_r}\\int\\mathrm{d}\\mathbf{r}\\, - \\rho(\\mathbf{r}) \\Psi(\\mathbf{r}), - - where :math:`\\rho(\\mathbf{r})` denotes the charge density at position - :math:`\\mathbf{r}`, :math:`\\varepsilon_0` is the vacuum permittivity - and :math:`\\varepsilon_r` is the relative dielectric of the simulation - medium. - - Parameters - ---------- - config : Config - Configuration object. - phi_q : pmesh.pm.RealField - Pmesh :code:`RealField` object containing the discretized - electrostatic potential values in real space on the - computational grid. Local for each MPI rank--the full computayional grid - is represented by the collective fields of all MPI ranks. - phi_q_fourier : pmesh.pm.ComplexField - Pmesh :code:`ComplexField` object containing the discretized - electrostatic potential values in reciprocal space on the - computational grid. Local for each MPI rank--the full computayional grid - is represented by the collective fields of all MPI ranks. - elec_potential : pmesh.pm.RealField - Pmesh :code:`RealField` object for storing calculated discretized - electrostatic potential values in real space on the - computational grid. Pre-allocated, but empty; any values in this field - are discarded. Changed in-place. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI - ranks. - elec_potential_fourier : pmesh.pm.ComplexField - Pmesh :code:`ComplexField` object for storing calculated discretized - electrostatic potential values in reciprocal space on the - computational grid. Pre-allocated, but empty; any values in this field - are discarded. Changed in-place. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI - ranks. - field_q_self_energy : float - Electrostatic self energy to be subtracted. - - Returns - ------- - field_q_energy : float - Total electrostatic energy. - """ - elec_conversion_factor = config.coulomb_constant / config.dielectric_const - V = np.prod(config.box_size) - n_mesh_cells = np.prod(np.full(3, config.mesh_size)) - volume_per_cell = V / n_mesh_cells - - # solve Poisson equation in Fourier space - def poisson_transfer_function(k, v): - return ( - 4. * np.pi * elec_conversion_factor * v - / k.normp(p=2, zeromode=1) - ) - phi_q_fourier.apply( - poisson_transfer_function, out=elec_potential_fourier - ) - - # get electrostatic potential in real space and compute energy - elec_potential_fourier.c2r(out=elec_potential) - field_q_energy = 0.5 * volume_per_cell * comm.allreduce(np.sum(phi_q*elec_potential)) - - field_q_energy -= field_q_self_energy # subtract self-energy - return field_q_energy - - def update_field_force_q( - charges, phi_q, phi_q_fourier, elec_field_fourier, elec_field, - elec_forces, layout_q, hamiltonian, pm, positions, config, + charges, + phi_q, + phi_q_fourier, + psi, + psi_fourier, + elec_field_fourier, + elec_field, + elec_forces, + layout_q, + hamiltonian, + pm, + positions, + config, ): """Calculate the electrostatic particle-field forces on the grid @@ -234,26 +312,26 @@ def update_field_force_q( Pmesh :code:`RealField` object for storing calculated discretized charge density density values on the computational grid. Pre-allocated, but empty; any values in this field are discarded. Changed in-place. - Local for each MPI rank--the full computaional grid is represented by + Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. phi_q_fourier : pmesh.pm.ComplexField Pmesh :code:`ComplexField` object for storing calculated discretized Fourier transformed charge density values in reciprocal space on the computational grid. Pre-allocated, but empty; any values in this field are discarded. Changed in-place. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI + computational grid is represented by the collective fields of all MPI ranks. elec_field_fourier : pmesh.pm.ComplexField Pmesh :code:`ComplexField` object for storing calculated discretized electric field values in reciprocal space on the computational grid. Pre-allocated, but empty; any values in this field are discarded. - Changed in-place. Local for each MPI rank--the full computaional grid + Changed in-place. Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. elec_field : pmesh.pm.RealField Pmesh :code:`RealField` object for storing calculated discretized electric field values on the computational grid. Pre-allocated, but empty; any values in this field are discarded. Changed in-place. - Local for each MPI rank--the full computaional grid is represented by + Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. elec_forces : (N,D) numpy.ndarray Array of electrostatic forces on :code:`N` particles in :code:`D` @@ -278,29 +356,44 @@ def update_field_force_q( V = np.prod(config.box_size) n_mesh_cells = np.prod(np.full(3, config.mesh_size)) volume_per_cell = V / n_mesh_cells - + n_dimensions = config.box_size.size + elec_conversion_factor = config.coulomb_constant / config.dielectric_const + # charges to grid pm.paint(positions, layout=layout_q, mass=charges, out=phi_q) phi_q /= volume_per_cell + # print("phi_q", np.sum(phi_q)) phi_q.r2c(out=phi_q_fourier) # smear charges with filter phi_q_fourier.apply(hamiltonian.H, out=phi_q_fourier) - phi_q_fourier.c2r(out=phi_q) + + # solve Poisson equation in Fourier space to get electrostatic potential + def poisson_transfer_function(k, v): + return 4.0 * np.pi * elec_conversion_factor * v / k.normp(p=2, zeromode=1) + + phi_q_fourier.apply(poisson_transfer_function, out=psi_fourier) + # print("psi_fourier", np.sum(psi_fourier)) + psi_fourier.c2r(out=psi) + # print("phi_q * psi update_field", np.sum(phi_q * psi)) + + # exit() # compute electric field directly from smeared charged densities in Fourier - n_dimensions = config.box_size.size - elec_conversion_factor = config.coulomb_constant / config.dielectric_const for _d in np.arange(n_dimensions): def poisson_transfer_function(k, v, d=_d): return ( - -1j * k[d] * 4.0 * np.pi * elec_conversion_factor * v + -1j + * k[d] + * 4.0 + * np.pi + * elec_conversion_factor + * v / k.normp(p=2, zeromode=1) ) - phi_q_fourier.apply( - poisson_transfer_function, out=elec_field_fourier[_d] - ) + + phi_q_fourier.apply(poisson_transfer_function, out=elec_field_fourier[_d]) elec_field_fourier[_d].c2r(out=elec_field[_d]) # get electrostatic force from electric field @@ -310,133 +403,44 @@ def poisson_transfer_function(k, v, d=_d): ) -def update_field_force_energy_q( - charges, phi_q, phi_q_fourier, elec_field_fourier, elec_field, elec_forces, - elec_energy_field, field_q_energy, layout_q, pm, positions, config, - compute_energy=False, comm=MPI.COMM_WORLD, +def comp_laplacian( + phi_fourier, + phi_transfer, + phi_laplacian, + hamiltonian, + config, ): - """Calculate the electrostatic particle-field energy and forces on the grid - - .. deprecated:: 1.0.0 - :code:`update_field_force_energy_q` was deprecated in favour of - :code:`update_field_force_q` and independent calls to - :code:`compute_field_energy_q` prior to 1.0.0 release. - - Parameters - ---------- - charges : (N,) numpy.ndarray - Array of particle charge values for :code:`N` particles. Local for each - MPI rank. - phi_q : pmesh.pm.RealField - Pmesh :code:`RealField` object for storing calculated discretized - charge density density values on the computational grid. Pre-allocated, - but empty; any values in this field are discarded. Changed in-place. - Local for each MPI rank--the full computaional grid is represented by - the collective fields of all MPI ranks. - phi_q_fourier : pmesh.pm.ComplexField - Pmesh :code:`ComplexField` object for storing calculated discretized - Fourier transformed charge density values in reciprocal space on the - computational grid. Pre-allocated, but empty; any values in this field - are discarded. Changed in-place. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI - ranks. - elec_field_fourier : pmesh.pm.ComplexField - Pmesh :code:`ComplexField` object for storing calculated discretized - electric field values in reciprocal space on the computational grid. - Pre-allocated, but empty; any values in this field are discarded. - Changed in-place. Local for each MPI rank--the full computaional grid - is represented by the collective fields of all MPI ranks. - elec_field : pmesh.pm.RealField - Pmesh :code:`RealField` object for storing calculated discretized - electric field values on the computational grid. Pre-allocated, - but empty; any values in this field are discarded. Changed in-place. - Local for each MPI rank--the full computaional grid is represented by - the collective fields of all MPI ranks. - elec_forces : (N,D) numpy.ndarray - Array of electrostatic forces on :code:`N` particles in :code:`D` - dimensions. - elec_energy_field : pmesh.pm.ComplexField - Pmesh :code:`ComplexField` object for storing calculated discretized - electrostatic energy density values in reciprocal space on the - computational grid. Pre-allocated, but empty; any values in this field - are discarded. Changed in-place. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI - ranks. - field_q_energy : float - Total elecrostatic energy. - layout_q : pmesh.domain.Layout - Pmesh communication layout object for domain decomposition of the full - system. Used as blueprint by :code:`pmesh.pm.paint` and - :code:`pmesh.pm.readout` for exchange of particle information across - MPI ranks as necessary. - pm : pmesh.pm.ParticleMesh - Pmesh :code:`ParticleMesh` object interfacing to the CIC window - function and the PFFT discrete Fourier transform library. - positions : (N,D) numpy.ndarray - Array of positions for :code:`N` particles in :code:`D` dimensions. - Local for each MPI rank. - config : Config - Configuration object. - compute_energy : bool, optional - Computes the electrostatic energy if :code:`True`, otherwise only - computes the electrostatic forces. - comm : mpi4py.Comm - MPI communicator to use for rank commuication. - - Returns - ------- - elec_field_energy : float - Total electrostatic energy. - """ - V = np.prod(config.box_size) - n_mesh_cells = np.prod(np.full(3, config.mesh_size)) - volume_per_cell = V / n_mesh_cells - pm.paint(positions, layout=layout_q, mass=charges, out=phi_q) - phi_q /= volume_per_cell - phi_q.r2c(out=phi_q_fourier) - - def phi_transfer_function(k, v): - return v * np.exp(-0.5 * config.sigma ** 2 * k.normp(p=2, zeromode=1)) - - phi_q_fourier.apply(phi_transfer_function, out=phi_q_fourier) - phi_q_fourier.c2r(out=phi_q) - n_dimensions = config.box_size.size - elec_conversion_factor = config.coulomb_constant / config.dielectric_const - - for _d in np.arange(n_dimensions): - def poisson_transfer_function(k, v, d=_d): - return ( - -1j * k[d] * 4.0 * np.pi * elec_conversion_factor * v - / k.normp(p=2, zeromode=1) - ) - - phi_q_fourier.apply( - poisson_transfer_function, out=elec_field_fourier[_d] - ) - elec_field_fourier[_d].c2r(out=elec_field[_d]) + for t in range(config.n_types): + np.copyto(phi_transfer[0].value, phi_fourier[t].value, casting="no", where=True) + np.copyto(phi_transfer[1].value, phi_fourier[t].value, casting="no", where=True) + np.copyto(phi_transfer[2].value, phi_fourier[t].value, casting="no", where=True) - for _d in np.arange(n_dimensions): - elec_forces[:, _d] = charges * ( - elec_field[_d].readout(positions, layout=layout_q) - ) + # Evaluate laplacian of phi in fourier space + for d in range(3): - if compute_energy: - def transfer_energy(k, v): - return ( - 4.0 * np.pi * elec_conversion_factor * np.abs(v)**2 - / k.normp(p=2, zeromode=1) - ) - phi_q_fourier.apply( - transfer_energy, kind="wavenumber", out=elec_energy_field - ) - field_q_energy = 0.5 * comm.allreduce(np.sum(elec_energy_field.value)) + def laplacian_transfer(k, v, d=d): + return -k[d] ** 2 * v - return field_q_energy.real + phi_transfer[d].apply(laplacian_transfer, out=Ellipsis) + phi_transfer[d].c2r(out=phi_laplacian[t][d]) def update_field( - phi, layouts, force_mesh, hamiltonian, pm, positions, types, config, v_ext, - phi_fourier, v_ext_fourier, compute_potential=False, + phi, + phi_laplacian, + phi_transfer, + layouts, + force_mesh, + hamiltonian, + pm, + positions, + types, + config, + v_ext, + phi_fourier, + v_ext_fourier, + m, + compute_potential=False, ): """Calculate the particle-field potential and force density @@ -492,8 +496,13 @@ def update_field( Pmesh :code:`RealField` objects containing discretized particle number density values on the computational grid; one for each particle type. Pre-allocated, but empty; any values in this field are discarded. - Changed in-place. Local for each MPI rank--the full computaional grid + Changed in-place. Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. + phi_laplacian : list[pmesh.pm.RealField], (M, 3) + Like phi, but containing the laplacian of particle number densities. + phi_transfer : list[pmesh.pm.ComplexField], (3,) + Like phi_fourier, used as an intermediary to perform FFT operations + to obtain the gradient or laplacian of particle number densities. layouts : list[pmesh.domain.Layout] Pmesh communication layout objects for domain decompositions of each particle type. Used as blueprint by :code:`pmesh.pm.readout` for @@ -503,7 +512,7 @@ def update_field( force density values on the computational grid; :code:`D` fields in D dimensions for each particle type. Pre-allocated, but empty; any values in this field are discarded. Changed in-place. Local for each MPI - rank--the full computaional grid is represented by the collective + rank--the full computational grid is represented by the collective fields of all MPI ranks. hamiltonian : Hamiltonian Particle-field interaction energy handler object. Defines the @@ -524,14 +533,14 @@ def update_field( external potential values on the computational grid; one for each particle type. Pre-allocated, but empty; any values in this field are discarded Changed in-place. Local for each MPI rank--the full - computaional grid is represented by the collective fields of all MPI + computational grid is represented by the collective fields of all MPI ranks. phi_fourier : list[pmesh.pm.ComplexField] Pmesh :code:`ComplexField` objects containing discretized particle number density values in reciprocal space on the computational grid; one for each particle type. Pre-allocated, but empty; any values in this field are discarded Changed in-place. Local for each MPI rank--the - full computaional grid is represented by the collective fields of all + full computational grid is represented by the collective fields of all MPI ranks. v_ext_fourier : list[pmesh.pm.ComplexField] Pmesh :code:`ComplesField` objects containing discretized @@ -542,8 +551,11 @@ def update_field( application differentiates the field in-place, ruining the contents for differentiation in the remaining :code:`D-1` spatial directions. Pre-allocated, but empty; any values in this field are discarded. - Changed in-place. Local for each MPI rank--the full computaional grid + Changed in-place. Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. + m: list[float], (M,) + pmesh.pm.ParticleMesh parameter for mass of particles in simulation unit. + Defaults to 1.0 for all particle types. compute_potential : bool, optional If :code:`True`, a :code:`D+1`-th copy of the Fourier transformed external potential field is made to be used later in particle-field @@ -559,7 +571,7 @@ def update_field( n_mesh_cells = np.prod(np.full(3, config.mesh_size)) volume_per_cell = V / n_mesh_cells for t in range(config.n_types): - pm.paint(positions[types == t], layout=layouts[t], out=phi[t]) + pm.paint(positions[types == t], mass=m[t], layout=layouts[t], out=phi[t]) phi[t] /= volume_per_cell phi[t].r2c(out=phi_fourier[t]) phi_fourier[t].apply(hamiltonian.H, out=Ellipsis) @@ -567,24 +579,33 @@ def update_field( # External potential for t in range(config.n_types): - hamiltonian.v_ext[t](phi).r2c(out=v_ext_fourier[0]) + v = hamiltonian.v_ext[t](phi) + + v.r2c(out=v_ext_fourier[0]) v_ext_fourier[0].apply(hamiltonian.H, out=Ellipsis) np.copyto( - v_ext_fourier[1].value, v_ext_fourier[0].value, casting="no", + v_ext_fourier[1].value, + v_ext_fourier[0].value, + casting="no", where=True, ) np.copyto( - v_ext_fourier[2].value, v_ext_fourier[0].value, casting="no", + v_ext_fourier[2].value, + v_ext_fourier[0].value, + casting="no", where=True, ) if compute_potential: np.copyto( - v_ext_fourier[3].value, v_ext_fourier[0].value, casting="no", + v_ext_fourier[3].value, + v_ext_fourier[0].value, + casting="no", where=True, ) # Differentiate the external potential in fourier space for d in range(3): + def force_transfer_function(k, v, d=d): return -k[d] * 1j * v @@ -596,10 +617,19 @@ def force_transfer_function(k, v, d=d): def compute_field_and_kinetic_energy( - phi, velocity, hamiltonian, positions, types, v_ext, config, layouts, + phi, + phi_q, + psi, + velocity, + hamiltonian, + positions, + types, + v_ext, + config, + layouts, comm=MPI.COMM_WORLD, ): - """Compute the particle-field and kintic energy contributions + """Compute the particle-field and kinetic energy contributions Calculates the kinetic energy through @@ -621,7 +651,7 @@ def compute_field_and_kinetic_energy( phi : list[pmesh.pm.RealField] Pmesh :code:`RealField` objects containing discretized particle number density values on the computational grid; one for each particle type. - Local for each MPI rank--the full computaional grid is represented by + Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. velocity : (N,D) numpy.ndarray Array of velocities for :code:`N` particles in :code:`D` dimensions. @@ -638,7 +668,7 @@ def compute_field_and_kinetic_energy( v_ext : list[pmesh.pm.RealField] Pmesh :code:`RealField` objects containing discretized particle-field external potential values on the computational grid; one for each - particle type. Local for each MPI rank--the full computaional grid is + particle type. Local for each MPI rank--the full computational grid is represented by the collective fields of all MPI ranks. config : Config Configuration object. @@ -659,15 +689,435 @@ def compute_field_and_kinetic_energy( n_mesh__cells = np.prod(np.full(3, config.mesh_size)) volume_per_cell = V / n_mesh__cells - w = hamiltonian.w(phi) * volume_per_cell - field_energy = w.csum() - kinetic_energy = comm.allreduce(0.5 * config.mass * np.sum(velocity ** 2)) - return field_energy, kinetic_energy + w_0 = hamiltonian.w_0(phi) * volume_per_cell + field_energy = w_0.csum() # w to W + + kinetic_energy = comm.allreduce(0.5 * config.mass * np.sum(velocity**2)) + + if config.coulombtype == "PIC_Spectral": + w_elec = hamiltonian.w_elec([phi_q, psi]) * volume_per_cell + field_q_energy = w_elec.csum() + else: + field_q_energy = 0.0 + + return field_energy, kinetic_energy, field_q_energy + + +def compute_field_energy_q_GPE( + config, + phi_eps, + field_q_energy, + dot_elec, + comm=MPI.COMM_WORLD, +): + """ + Compute the electrostatic energy after electrosatic forces is + calculated. + + From the definition of the elecrostatic potential :math:`\\Psi`, the energy + is + + W = \\frac{1}{2}\\int\\mathrm{d}\\mathbf{r}\\, + \\epsilon(\\mathbf{r})} \\left(\\mathbf{E}\\cdot \\mathbf{E}\\right), + + where :math:`\\epsilon(\\mathbf{r})}` is the anisotropic, spatially dependent, + relative dielectric of the simulation medium. + + Parameters + ---------- + config : hymd.input_parser.Config + Configuration object. + phi_eps : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + relative dielectric values on the computational grid. + Local for each MPI rank--the full computational grid is represented by + the collective fields of all MPI ranks. + field_q_energy : float + Total elecrostatic energy. + dot_elec : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing :math:`|\\mathbf{E(r)}|^{2}` + on the computational grid. Local for each MPI rank -- the full computational + grid is represented by the collective fields of all MPI ranks. + comm : mpi4py.Comm + MPI communicator to use for rank commuication. + + See also + -------- + update_field_force_q_GPE: + Compute the electrosatic force from an anisotropic dielectric general + Poisson equation. + """ + + V = np.prod(config.box_size) + n_mesh__cells = np.prod(np.full(3, config.mesh_size)) + volume_per_cell = V / n_mesh__cells + # ^ due to integration on local cell before allreduce + + eps_0 = 1.0 / (config.coulomb_constant * 4 * np.pi) + field_q_energy = ( + volume_per_cell * (0.5 * eps_0) * comm.allreduce(np.sum(phi_eps * dot_elec)) + ) + + return field_q_energy + + +def update_field_force_q_GPE( + conv_fun, + phi, + types, + charges, + config_charges, + phi_q, + phi_q_fourier, + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol_prev, + phi_pol, + elec_field, + elec_forces, + elec_field_contrib, + psi, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + hamiltonian, + layout_q, + layouts, + pm, + positions, + config, + comm=MPI.COMM_WORLD, +): + """ + Calculate the electrostatic particle-field forces on the grid, arising from + a general Poisson equation, i.e. anisotropic permittivity/dielectric. + The function is called when tomli input config.coulombtype = "PIC_Spectral_GPE." + + Computes the electrostatic potential :math:`\\Psi` from particle charges + through the smoothed charge density :math:`\\tilde\\rho`. With :math:`P` + being the cloud-in-cell (CIC) window function, the charge density and + filtered charge densities are computed as + + .. math:: + + \\rho(\\mathbf{r}) = \\sum_i q_i P(\\mathbf{r}-\\mathbf{r}_i), + + and + + .. math:: + + \\tilde\\rho(\\mathbf{r}) = \\int\\mathrm{x}\\mathbf{r}\\, + \\rho(\\mathbf{x})H(\\mathbf{r}-\\mathbf{x}), + + where :math:`H` is the grid-independent filtering function. The + electrostatic potential for a variable dielectric does not have an + analytical expression, and is computed in reciprocal through an iterative + method. + + The GPE states that + + .. math:: + + \\nabla \\cdot \\left(\\epsilon(\\mathbf{r}) + \\nabla{\\mathbf{\\psi(r)}}\\right) = -\\rho({\\mathbf{r}}). + + where :math:`\\epsilon(\\mathbf{r})` is the relative dielectric function. + + Parameters + ---------- + conv_fun : Convergence function. + Returns a scalar. Depends on MPI allreduce for similar convergence + across MPI ranks. + phi : list[pmesh.pm.RealField] + Pmesh :code:`RealField` objects containing discretized particle number + density values on the computational grid; one for each particle type. + Local for each MPI rank--the full computational grid is represented by + the collective fields of all MPI ranks. + types : (N,) numpy.ndarray + Array of type indices for each of :code:`N` particles. Local for each + MPI rank. + charges : (N,) numpy.ndarray + Array of particle charge values for :code:`N` particles. Local for each + MPI rank. + config_charges: (types,) numpy.ndarray + Array of particle charge values for each type ID. The same across + MPI ranks. + phi_q : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + charge density density values on the computational grid. Pre-allocated, + but empty. Changed in-place. + Local for each MPI rank--the full computational grid is represented by + the collective fields of all MPI ranks. + phi_q_fourier : pmesh.pm.ComplexField + Pmesh :code:`ComplexField` object for storing calculated discretized + Fourier transformed charge density values in reciprocal space on the + computational grid. Pre-allocated, but empty. Changed in-place. + Local for each MPI rank--the full computational grid is represented by + the collective fields of all MPI ranks. + phi_eps : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + relative dielectric values on the computational grid. Pre-allocated, + but empty. Changed in-place. + Local for each MPI rank--the full computational grid is represented by + the collective fields of all MPI ranks. + phi_eps_fourier : pmesh.pm.ComplexField + Pmesh :code:`ComplexField` object for storing calculated discretized + Fourier transformed relative dielectric values in reciprocal space on the + computational grid. Pre-allocated, but empty. Changed in-place. + Local for each MPI rank--the full computational grid is represented + by the collective fields of all MPI ranks. + phi_eta : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + gradients of the relative dielectric values on the computational grid. + Pre-allocated,but empty. Changed in-place.Local for each MPI rank--the + full computational grid is represented by the collective fields of all MPI ranks. + phi_eta_fourier : pmesh.pm.ComplexField + Pmesh :code:`ComplexField` object for storing the calculated discretized + Fourier transformed gradient relative dielectric values in reciprocal space on the + computational grid. Pre-allocated, but empty. Changed in-place. + Local for each MPI rank--the full computational grid is represented + by the collective fields of all MPI ranks. + phi_pol_prev : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + polarization charge values on the computational grid. Parameter in + the iterative method.Pre-allocated,but empty. Changedin-place. + Local for each MPI rank--the full computational grid is represented + by the collective fields of all MPI ranks. + phi_pol : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + polarization charges on the computational grid. Parameter in the iterative + method, updating the next quess in solving for the electrostatic potential. + Pre-allocated,but empty. Changed in-place.Local for each MPI rank-- + the full computational grid is represented by the collective fields of + all MPI ranks. + elec_field : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing calculated discretized + electric field values on the computational grid. Pre-allocated, + but empty. Changed in-place. Local for each MPI rank--the full + computational grid is represented by the collective fields of all + MPI ranks. + elec_forces : (N,D) numpy.ndarray + Array of electrostatic forces on :code:`N` particles in :code:`D` + dimensions. + elec_field_contrib : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing + :math:`|\\mathbf{E(r)}|^2/\\phi_{0}` on the computational grid. + Pre-allocated, but empty. Changed in-place. + Local for each MPI rank-- the full computational grid is represented by + the collective fields of all MPI ranks. + psi : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing electrostatic potential + on the computational grid. Pre-allocated, but empty. Changed in-place. + Local for each MPI rank-- the full computational grid is represented by + the collective fields of all MPI ranks. + Vbar_elec : mesh.pm.RealField + Pmesh :code:`RealField` object for storing functional derivatives of + :math:`\\|w(\\{ \\phi \\})_{elec}`on the computational grid. + Pre-allocated, but empty. Changed in-place. Local for each MPI rank-- + the full computational grid is represented by the collective fields of + all MPI ranks. + Vbar_elec_fourier : pmesh.pm.ComplexField + Pmesh :code:`ComplexField` object for storing the calculated functional + derivatives of :math:`\\|w(\\{ \\phi \\})_{elec}` in reciprocal space on the + computational grid. Pre-allocated, but empty. Changed in-place. + Local for each MPI rank--the full computational grid is represented + by the collective fields of all MPI ranks. + force_mesh_elec : pmesh.pm.RealField + Pmesh :code:`RealField` object for storing electrostatic force values + on the computational grid. Pre-allocated, but empty. Changed in-place. + Local for each MPI rank-- the full computational grid is represented by + the collective fields of all MPI ranks. + force_mesh_elec_fourier : pmesh.pm.ComplexField + Pmesh :code:`ComplexField` object for storing the calculated electrostatic + force values in reciprocal space on the computational grid. Local for + each MPI rank--the full computational grid is represented by the collective + fields of all MPI ranks. + hamiltonian : Hamiltonian + Particle-field interaction energy handler object. Defines the + grid-independent filtering function, :math:`H`. + layout_q : pmesh.domain.Layout + Pmesh communication layout object for domain decomposition of the full + system. Used as blueprint by :code:`pmesh.pm.paint` and + :code:`pmesh.pm.readout` for exchange of particle information across + MPI ranks as necessary. + layouts: list[pmesh.domain.Layout] + Pmesh communication layout objects for domain decompositions of each + particle type. Used as blueprint by :code:`pmesh.pm.readout` for + exchange of particle information across MPI ranks as necessary. + pm : pmesh.pm.ParticleMesh + Pmesh :code:`ParticleMesh` object interfacing to the CIC window + function and the PFFT discrete Fourier transform library. + positions : (N,D) numpy.ndarray + Array of positions for :code:`N` particles in :code:`D` dimensions. + Local for each MPI rank. + config : hymd.input_parser.Config + Configuration object. + comm: mpi4py.Comm + MPI communicator to use for rank commuication. + + See also + -------- + compute_field_energy_q_GPE: + Compute the electrostatic energy after electrosatic force is + calculated for a variable (anisotropic) dielectric general Poisson equation. + """ + + ## basic setup + V = np.prod(config.box_size) + n_mesh_cells = np.prod(np.full(3, config.mesh_size)) + volume_per_cell = V / n_mesh_cells + ## old protocol in gen_qe_hpf_use_self + pm.paint(positions, layout=layout_q, mass=charges, out=phi_q) ## + ## scale and fft + ## old protocol in gen_qe_hpf_use_self + phi_q /= volume_per_cell + phi_q.r2c(out=phi_q_fourier) + + phi_q_fourier.apply(hamiltonian.H, out=phi_q_fourier) + ## ^------ use the same gaussian as the \kai interaciton + phi_q_fourier.c2r(out=phi_q) ## this phi_q is after applying the smearing function + + denom_phi_tot = pm.create("real", value=0.0) + num_types = pm.create("real", value=0.0) + ### ^ ----- Calculate the relative dielectric (permittivity) to field + ### ------- from a mean contribution of particle number densities + + for t_ in range(config.n_types): + num_types = num_types + (config.dielectric_type[t_]) * phi[t_] + denom_phi_tot = denom_phi_tot + phi[t_] + + np.divide(num_types, denom_phi_tot, where=np.abs(denom_phi_tot > 1e-6), out=phi_eps) + + phi_eps.r2c(out=phi_eps_fourier) # FFT dielectric + + # phi_q_eps = (phi_q/phi_eps) + np.divide(phi_q, phi_eps, where=np.abs(phi_eps > 1e-6), out=phi_q) + + _SPACE_DIM = 3 + ##^--------- constants needed throughout the calculations + + ### method for finding the gradient (fourier space), using the spatial dimension of k + for _d in np.arange(_SPACE_DIM): + + def gradient_transfer_function(k, x, d=_d): + return 1j * k[d] * x + + phi_eps_fourier.apply(gradient_transfer_function, out=phi_eta_fourier[_d]) + phi_eta_fourier[_d].c2r(out=phi_eta[_d]) + np.divide(phi_eta[_d], phi_eps, where=np.abs(phi_eps > 1e-6), out=phi_eta[_d]) + + ### iterative GPE solver ### + ### ---------------------------------------------- + max_iter = 100 + i = 0 + delta = 1.0 + # phi_pol_prev = pm.create("real", value = 0.0) + ### ^------ set to zero before each iterative procedure or soft start + conv_criteria = config.conv_crit # conv. criteria (default 1e-6) + w = config.pol_mixing # polarization mixing param (default 0.6) + while i < max_iter and delta > conv_criteria: + (phi_q + phi_pol_prev).r2c(out=phi_q_fourier) + for _d in np.arange(_SPACE_DIM): + + def iterate_apply_k_vec(k, additive_terms, d=_d): + return additive_terms * (-1j * k[d]) / k.normp(p=2, zeromode=1) + + phi_q_fourier.apply(iterate_apply_k_vec, out=phi_eta_fourier[_d]) + phi_eta_fourier[_d].c2r(out=elec_field[_d]) + + phi_pol = -( + phi_eta[0] * elec_field[0] + + phi_eta[1] * elec_field[1] + + phi_eta[2] * elec_field[2] + ) + ### ^-- Following a negative sign convention (-ik) of the FT, a neg sign is + ### --- mathematically correct by the definition of the GPE (double - -> +) + phi_pol = w * phi_pol + (1.0 - w) * phi_pol_prev + diff = np.abs(phi_pol - phi_pol_prev) + delta = conv_fun(comm, diff) # decided from toml input + phi_pol_prev = phi_pol.copy() + i = i + 1 + # print("Stopping after iteration {:d} with stop crit {:.2e}, delta {:.2e}".format(i,conv_criteria,delta)) + + # compute_potential = True + def k_norm_divide(k, potential): + return potential / k.normp(p=2, zeromode=1) + + ## > Electrostatic potential + eps0_inv = config.coulomb_constant * 4 * np.pi + ## ^ the 1/(4pi eps0)*4*pi = 1/eps0 + ((eps0_inv) * (phi_q + phi_pol)).r2c(out=phi_q_fourier) + phi_q_fourier.apply(k_norm_divide, out=phi_q_fourier) + phi_q_fourier.c2r(out=psi) + ### ^ electrostatic potential for the GPE + + for _d in np.arange(_SPACE_DIM): + + def field_transfer_function(k, x, d=_d): + return ( + -1j * k[d] * x + ) ## negative sign relation, due to E = - nabla psi relation + + phi_q_fourier.apply(field_transfer_function, out=phi_eta_fourier[_d]) + phi_eta_fourier[_d].c2r(out=elec_field[_d]) + ## ^-------- Method: Obtaining the electric field from electrostatic potential + ## Assuming the electric field is conserved. + ## Assumption holds if no magnetic flux (magnetic induced fields) + + ############## Obtain forces ############## + elec_dot = ( + elec_field[0] * elec_field[0] + + elec_field[1] * elec_field[1] + + elec_field[2] * elec_field[2] + ) + # needed for energy calculations + + np.divide( + elec_dot, + denom_phi_tot, + where=np.abs(denom_phi_tot > 1e-6), + out=elec_field_contrib, + ) + + eps0_inv = config.coulomb_constant * 4 * np.pi + + for t_ in range(config.n_types): + Vbar_elec[t_] = ( + config_charges[t_] * psi + - (0.5 / eps0_inv) + * (config.dielectric_type[t_] - phi_eps) + * elec_field_contrib + ) + + # Obtain Vext,k + for t_ in range(config.n_types): + Vbar_elec[t_].r2c(out=Vbar_elec_fourier[t_]) + Vbar_elec_fourier[t_].apply(hamiltonian.H, out=Vbar_elec_fourier[t_]) + + # force terms + # F = - grad Vext + for t_ in range(config.n_types): + for _d in np.arange(_SPACE_DIM): + + def force_transfer_function(k, x, d=_d): + return -1j * k[_d] * x ## negative gradient + + Vbar_elec_fourier[t_].apply( + force_transfer_function, out=force_mesh_elec_fourier[t_][_d] + ) + force_mesh_elec_fourier[t_][_d].c2r(out=force_mesh_elec[t_][_d]) + elec_forces[types == t_, _d] = force_mesh_elec[t_][_d].readout( + positions[types == t_], layout=layouts[t_] + ) + + return Vbar_elec, phi_eps, elec_dot def domain_decomposition( - positions, pm, *args, molecules=None, bonds=None, verbose=0, - comm=MPI.COMM_WORLD + positions, pm, *args, molecules=None, bonds=None, verbose=0, comm=MPI.COMM_WORLD ): """Performs domain decomposition diff --git a/hymd/file_io.py b/hymd/file_io.py index deb0d4b6..0aa45eb9 100644 --- a/hymd/file_io.py +++ b/hymd/file_io.py @@ -10,11 +10,14 @@ class OutDataset: - """HDF5 dataset handler for file output - """ + """HDF5 dataset handler for file output""" def __init__( - self, dest_directory, config, double_out=False, disable_mpio=False, + self, + dest_directory, + config, + double_out=False, + disable_mpio=False, comm=MPI.COMM_WORLD, ): """Constructor @@ -48,15 +51,13 @@ def __init__( if disable_mpio: self.file = h5py.File( os.path.join( - dest_directory, - f"sim.hdf5-{comm.rank:6d}-of-{comm.size:6d}" + dest_directory, f"sim.hdf5-{comm.rank:6d}-of-{comm.size:6d}" ), "w", ) else: self.file = h5py.File( - os.path.join(dest_directory, "sim.H5"), "w", driver="mpio", - comm=comm + os.path.join(dest_directory, "sim.H5"), "w", driver="mpio", comm=comm ) def is_open(self, comm=MPI.COMM_WORLD): @@ -82,8 +83,7 @@ def close_file(self, comm=MPI.COMM_WORLD): self.file.close() def flush(self): - """Flushes output buffers, forcing file writes - """ + """Flushes output buffers, forcing file writes""" self.file.flush() @@ -112,15 +112,28 @@ def setup_time_dependent_element( step = group.create_dataset("step", (n_frames,), "int32") time = group.create_dataset("time", (n_frames,), "float32") value = group.create_dataset("value", (n_frames, *shape), dtype) + if units is not None: - group.attrs["units"] = units + value.attrs["unit"] = units + time.attrs["unit"] = "ps" return group, step, time, value def store_static( - h5md, rank_range, names, types, indices, config, bonds_2_atom1, - bonds_2_atom2, molecules=None, velocity_out=False, force_out=False, - charges=False, comm=MPI.COMM_WORLD, + h5md, + rank_range, + names, + types, + indices, + config, + bonds_2_atom1, + bonds_2_atom2, + molecules=None, + velocity_out=False, + force_out=False, + charges=False, + dielectrics=False, + comm=MPI.COMM_WORLD, ): """Outputs all static time-independent quantities to the HDF5 output file @@ -154,6 +167,8 @@ def store_static( If :code:`True`, forces are written to output HDF5 file. charges : (N,) numpy.ndarray Array of particle charge values for :code:`N` particles. + dielectrics : (N,) numpy.ndarray + Array of particle relative dielectric values for :code:`N` particles. comm : mpi4py.Comm MPI communicator to use for rank commuication. @@ -196,9 +211,7 @@ def store_static( h5md.particles_group = h5md.file.create_group("/particles") h5md.all_particles = h5md.particles_group.create_group("all") - mass = h5md.all_particles.create_dataset( - "mass", (config.n_particles,), dtype - ) + mass = h5md.all_particles.create_dataset("mass", (config.n_particles,), dtype) mass[...] = config.mass if charges is not False: @@ -206,14 +219,17 @@ def store_static( "charge", (config.n_particles,), dtype="float32" ) charge[indices] = charges + if dielectrics is not False: + dielectric = h5md.all_particles.create_dataset( + "dielectric", (config.n_particles,), dtype="float32" + ) + dielectric[indices] = dielectrics box = h5md.all_particles.create_group("box") box.attrs["dimension"] = 3 box.attrs["boundary"] = np.array( [np.string_(s) for s in 3 * ["periodic"]], dtype="S8" ) - h5md.edges = box.create_dataset("edges", (3,), dtype) - h5md.edges[:] = np.array(config.box_size) n_frames = config.n_steps // config.n_print if np.mod(config.n_steps - 1, config.n_print) != 0: @@ -224,7 +240,9 @@ def store_static( n_frames += 1 species = h5md.all_particles.create_dataset( - "species", (config.n_particles,), dtype="i", + "species", + (config.n_particles,), + dtype="i", ) ( @@ -266,7 +284,7 @@ def store_static( n_frames, (config.n_particles, 3), dtype, - units="kJ nm mol-1", + units="kJ mol-1 nm-1", ) ( _, @@ -274,7 +292,12 @@ def store_static( h5md.total_energy_time, h5md.total_energy, ) = setup_time_dependent_element( - "total_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "total_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) ( _, @@ -282,7 +305,12 @@ def store_static( h5md.kinetc_energy_time, h5md.kinetc_energy, ) = setup_time_dependent_element( - "kinetic_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "kinetic_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) ( _, @@ -290,7 +318,12 @@ def store_static( h5md.potential_energy_time, h5md.potential_energy, ) = setup_time_dependent_element( - "potential_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "potential_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) ( _, @@ -298,7 +331,12 @@ def store_static( h5md.bond_energy_time, h5md.bond_energy, ) = setup_time_dependent_element( - "bond_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "bond_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) ( _, @@ -306,7 +344,12 @@ def store_static( h5md.angle_energy_time, h5md.angle_energy, ) = setup_time_dependent_element( - "angle_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "angle_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) ( _, @@ -314,7 +357,12 @@ def store_static( h5md.dihedral_energy_time, h5md.dihedral_energy, ) = setup_time_dependent_element( - "dihedral_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "dihedral_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) ( _, @@ -322,7 +370,12 @@ def store_static( h5md.field_energy_time, h5md.field_energy, ) = setup_time_dependent_element( - "field_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "field_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) if charges is not False: ( @@ -331,8 +384,14 @@ def store_static( h5md.field_q_energy_time, h5md.field_q_energy, ) = setup_time_dependent_element( - "field_q_energy", h5md.observables, n_frames, (1,), dtype, units="kJ mol-1" # noqa: E501 + "field_q_energy", + h5md.observables, + n_frames, + (1,), + dtype, + units="kJ mol-1", # noqa: E501 ) # <-------- xinmeng + ( _, h5md.total_momentum_step, @@ -386,7 +445,23 @@ def store_static( h5md.thermostat_work_time, h5md.thermostat_work, ) = setup_time_dependent_element( - "thermostat_work", h5md.observables, n_frames, (1,), "float32", units="kJ mol-1" # noqa: E501 + "thermostat_work", + h5md.observables, + n_frames, + (1,), + "float32", + units="kJ mol-1", # noqa: E501 + ) + ( + _, + h5md.pressure_step, + h5md.pressure_time, + h5md.pressure, + ) = setup_time_dependent_element( + "pressure", h5md.observables, n_frames, (18,), "float32", units="Bar" + ) + (_, h5md.box_step, h5md.box_time, h5md.box_value,) = setup_time_dependent_element( + "edges", box, n_frames, (3, 3), "float32", units="nm" ) ind_sort = np.argsort(indices) @@ -406,7 +481,9 @@ def store_static( type_dataset = vmd_group.create_dataset("type", (config.n_types,), "S16") if molecules is not None: resid_dataset = vmd_group.create_dataset( - "resid", (config.n_particles,), "i", + "resid", + (config.n_particles,), + "i", ) # Change this @@ -439,11 +516,38 @@ def store_static( resid_dataset[indices[ind_sort]] = molecules +# store data old vs +""" +h5md, step, frame, indices, positions, velocities, forces, box_size, +temperature, kinetic_energy, bond2_energy, bond3_energy, bond4_energy, +field_energy, field_q_energy, time_step, config, velocity_out=False, +force_out=False, charge_out=False, dump_per_particle=False, +""" + + def store_data( - h5md, step, frame, indices, positions, velocities, forces, box_size, - temperature, kinetic_energy, bond2_energy, bond3_energy, bond4_energy, - field_energy, field_q_energy, time_step, config, velocity_out=False, - force_out=False, charge_out=False, dump_per_particle=False, + h5md, + step, + frame, + indices, + positions, + velocities, + forces, + box_size, + temperature, + pressure, + kinetic_energy, + bond2_energy, + bond3_energy, + bond4_energy, + field_energy, + field_q_energy, + time_step, + config, + velocity_out=False, + force_out=False, + charge_out=False, + dump_per_particle=False, comm=MPI.COMM_WORLD, ): """Writes time-step data to HDF5 output file @@ -517,6 +621,8 @@ def store_data( h5md.angular_momentum_step, h5md.torque_step, h5md.temperature_step, + h5md.pressure_step, + h5md.box_step, h5md.thermostat_work_step, ): dset[frame] = step @@ -534,6 +640,8 @@ def store_data( h5md.angular_momentum_time, h5md.torque_time, h5md.temperature_time, + h5md.pressure_time, + h5md.box_time, h5md.thermostat_work_time, ): dset[frame] = step * time_step @@ -559,13 +667,10 @@ def store_data( h5md.field_q_energy[frame] = field_q_energy potential_energy = ( - bond2_energy + bond3_energy + bond4_energy + field_energy - + field_q_energy + bond2_energy + bond3_energy + bond4_energy + field_energy + field_q_energy ) - total_momentum = config.mass * comm.allreduce( - np.sum(velocities, axis=0), MPI.SUM - ) + total_momentum = config.mass * comm.allreduce(np.sum(velocities, axis=0), MPI.SUM) angular_momentum = config.mass * comm.allreduce( np.sum(np.cross(positions, velocities), axis=0), MPI.SUM ) @@ -583,6 +688,9 @@ def store_data( h5md.angular_momentum[frame, :] = angular_momentum h5md.torque[frame, :] = torque h5md.temperature[frame] = temperature + h5md.pressure[frame] = pressure + for d in range(3): + h5md.box_value[frame, d, d] = box_size[d] h5md.thermostat_work[frame] = config.thermostat_work fmt_ = [ @@ -603,17 +711,19 @@ def store_data( "ΔH" if config.target_temperature else "ΔE", ] fmt_ = np.array(fmt_) - + # create mask to show only energies != 0 - en_array = np.array([ - field_energy, - field_q_energy, - bond2_energy, - bond3_energy, - bond4_energy, - ]) + en_array = np.array( + [ + field_energy, + field_q_energy, + bond2_energy, + bond3_energy, + bond4_energy, + ] + ) mask = np.full_like(fmt_, True, dtype=bool) - mask[range(6,11)] = en_array != 0. + mask[range(6, 11)] = en_array != 0.0 header_ = fmt_[mask].shape[0] * "{:>13}" if config.initial_energy is None: @@ -628,9 +738,7 @@ def store_data( total_energy = kinetic_energy + potential_energy if config.initial_energy is not None: if config.target_temperature: - H_tilde = ( - total_energy - config.initial_energy - config.thermostat_work - ) + H_tilde = total_energy - config.initial_energy - config.thermostat_work else: H_tilde = total_energy - config.initial_energy else: @@ -655,13 +763,12 @@ def store_data( total_momentum[2] / divide_by, H_tilde / divide_by, ] - data = data_fmt.format(*[val for i,val in enumerate(all_data) if mask[i]]) + data = data_fmt.format(*[val for i, val in enumerate(all_data) if mask[i]]) Logger.rank0.log(logging.INFO, ("\n" + header + "\n" + data)) def distribute_input( - in_file, rank, size, n_particles, max_molecule_size=201, - comm=MPI.COMM_WORLD + in_file, rank, size, n_particles, max_molecule_size=201, comm=MPI.COMM_WORLD ): """Assign global arrays onto MPI ranks, attempting load balancing @@ -717,9 +824,7 @@ def distribute_input( # Implicitly assuming no molecule is bigger than # min(max_molecule_size, n_particles // n_MPI_ranks) atoms. max_molecule_size += 2 - grab_extra = ( - max_molecule_size if np_per_MPI > max_molecule_size else np_per_MPI - ) + grab_extra = max_molecule_size if np_per_MPI > max_molecule_size else np_per_MPI if rank == 0: mpi_range_start = 0 if size == 1: @@ -747,15 +852,12 @@ def distribute_input( molecule_end_indices[molecule_end_indices >= np_per_MPI][0] + 1 ] elif rank == size - 1: - p_mpi_range[0] = ( - indices[molecule_end_indices[molecule_end_indices > 0][0]] + 1 - ) + p_mpi_range[0] = indices[molecule_end_indices[molecule_end_indices > 0][0]] + 1 p_mpi_range[1] = n_particles else: - p_mpi_range[0] = ( - indices[molecule_end_indices[molecule_end_indices > 0][0]] + 1 - ) + p_mpi_range[0] = indices[molecule_end_indices[molecule_end_indices > 0][0]] + 1 p_mpi_range[1] = ( - indices[molecule_end_indices[molecule_end_indices > np_per_MPI][0]] + 1 # noqa: E501 + indices[molecule_end_indices[molecule_end_indices > np_per_MPI][0]] + + 1 # noqa: E501 ) return list(range(p_mpi_range[0], p_mpi_range[1])), molecules_flag diff --git a/hymd/force.py b/hymd/force.py index 99119b8f..b4ea4a8f 100644 --- a/hymd/force.py +++ b/hymd/force.py @@ -5,9 +5,7 @@ from dataclasses import dataclass # Imported here so we can call from force import compute_bond_forces__fortran -from force_kernels import ( # noqa: F401 - cbf as compute_bond_forces__fortran -) +from force_kernels import cbf as compute_bond_forces__fortran # noqa: F401 from force_kernels import ( # noqa: F401 caf as compute_angle_forces__fortran, ) @@ -25,6 +23,12 @@ ) +@dataclass # might not be needed +class Dielectric_type: + atom_1: str + dielectric_value: float + + @dataclass class Bond: """Dataclass representing a single two-particle bond type @@ -57,6 +61,7 @@ class Bond: strength : float Harmonic bond strength coefficient (spring constant). """ + atom_1: str atom_2: str equilibrium: float @@ -111,6 +116,7 @@ class Angle(Bond): Bond : Two-particle bond type dataclass """ + atom_3: str @@ -205,6 +211,7 @@ class Dihedral: ---------- Bore et al. J. Chem. Theory Comput., 14(2): 1120–1130, 2018. """ + atom_1: str atom_2: str atom_3: str @@ -251,6 +258,7 @@ class Chi: hymd.hamiltonian.DefaultWithChi : Interaction energy functional using :math:`\\chi`-interactions. """ + atom_1: str atom_2: str interaction_energy: float @@ -368,12 +376,8 @@ def prepare_bonds_old(molecules, names, bonds, indices, config): name_j = bond_graph.nodes()[j]["name"] for b in config.bonds: - match_forward = ( - name_i == b.atom_1 and name_j == b.atom_2 - ) - match_backward = ( - name_i == b.atom_2 and name_j == b.atom_1 - ) + match_forward = name_i == b.atom_1 and name_j == b.atom_2 + match_backward = name_i == b.atom_2 and name_j == b.atom_1 if match_forward or match_backward: bonds_2.append( [ @@ -456,7 +460,61 @@ def prepare_bonds_old(molecules, names, bonds, indices, config): return bonds_2, bonds_3, bonds_4, bb_index -def prepare_bonds(molecules, names, bonds, indices, config): +def prepare_index_based_bonds(molecules, topol): + bonds_2 = [] + bonds_3 = [] + bonds_4 = [] + + different_molecules = np.unique(molecules) + for mol in different_molecules: + resid = mol + 1 + top_summary = topol["system"]["molecules"] + resname = None + test_mol_number = 0 + for molname in top_summary: + test_mol_number += molname[1] + if resid <= test_mol_number: + resname = molname[0] + break + + if resname is None: + break + + if "bonds" in topol[resname]: + first_id = np.where(molecules == mol)[0][0] + for bond in topol[resname]["bonds"]: + index_i = bond[0] - 1 + first_id + index_j = bond[1] - 1 + first_id + # bond[2] is the bond type, inherited by the itp format. Not used + equilibrium = bond[3] + strength = bond[4] + bonds_2.append([index_i, index_j, equilibrium, strength]) + + if "angles" in topol[resname]: + first_id = np.where(molecules == mol)[0][0] + for angle in topol[resname]["angles"]: + index_i = angle[0] - 1 + first_id + index_j = angle[1] - 1 + first_id + index_k = angle[2] - 1 + first_id + # angle[3] is the angle type, inherited by the itp format. Not used + equilibrium = np.radians(angle[4]) + strength = angle[5] + bonds_3.append([index_i, index_j, index_k, equilibrium, strength]) + + if "dihedrals" in topol[resname]: + first_id = np.where(molecules == mol)[0][0] + for angle in topol[resname]["dihedrals"]: + index_i = angle[0] - 1 + first_id + index_j = angle[1] - 1 + first_id + index_k = angle[2] - 1 + first_id + index_l = angle[3] - 1 + first_id + dih_type = angle[4] + coeff = angle[5] + bonds_4.append([index_i, index_j, index_k, index_l, coeff, dih_type, 0]) + return bonds_2, bonds_3, bonds_4 + + +def prepare_bonds(molecules, names, bonds, indices, config, topol=None): """Rearrange the bond information for usage in compiled Fortran kernels Restructures the lists resulting from the execution of @@ -538,9 +596,14 @@ def prepare_bonds(molecules, names, bonds, indices, config): connectivity information in the structure/topology input file and the bonded types specified in the configuration file. """ - bonds_2, bonds_3, bonds_4, bb_index = prepare_bonds_old( - molecules, names, bonds, indices, config - ) + if topol is not None: + bonds_2, bonds_3, bonds_4 = prepare_index_based_bonds( + molecules, topol + ) + else: + bonds_2, bonds_3, bonds_4, bb_index = prepare_bonds_old( + molecules, names, bonds, indices, config + ) # Bonds bonds_2_atom1 = np.empty(len(bonds_2), dtype=int) @@ -565,7 +628,6 @@ def prepare_bonds(molecules, names, bonds, indices, config): bonds_3_atom3[i] = b[2] bonds_3_equilibrium[i] = b[3] bonds_3_strength[i] = b[4] - # Dihedrals bonds_4_atom1 = np.empty(len(bonds_4), dtype=int) bonds_4_atom2 = np.empty(len(bonds_4), dtype=int) @@ -585,12 +647,28 @@ def prepare_bonds(molecules, names, bonds, indices, config): bonds_4_atom4[i] = b[3] bonds_4_coeff[i] = np.resize(b[4], (number_of_coeff, len_of_coeff)) bonds_4_type[i] = b[5] - bonds_4_last[bb_index] = 1 + if topol is not None: + bonds_4_last[i] = b[6] + if topol is None: + bonds_4_last[bb_index] = 1 return ( - bonds_2_atom1, bonds_2_atom2, bonds_2_equilibrium, bonds_2_strength, - bonds_3_atom1, bonds_3_atom2, bonds_3_atom3, bonds_3_equilibrium, bonds_3_strength, # noqa: E501 - bonds_4_atom1, bonds_4_atom2, bonds_4_atom3, bonds_4_atom4, bonds_4_coeff, bonds_4_type, bonds_4_last, # noqa: E501 + bonds_2_atom1, + bonds_2_atom2, + bonds_2_equilibrium, + bonds_2_strength, + bonds_3_atom1, + bonds_3_atom2, + bonds_3_atom3, + bonds_3_equilibrium, + bonds_3_strength, # noqa: E501 + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_coeff, + bonds_4_type, + bonds_4_last, # noqa: E501 ) @@ -647,7 +725,7 @@ def compute_angle_forces__plain(f_angles, r, bonds_3, box_size): cosphi = np.dot(ea, ec) theta = np.arccos(cosphi) - xsinph = 1.0 / np.sqrt(1.0 - cosphi ** 2) + xsinph = 1.0 / np.sqrt(1.0 - cosphi**2) d = theta - theta0 f = -k * d @@ -721,14 +799,14 @@ def compute_dihedral_forces__plain(f_dihedrals, r, bonds_4, box_size): def dipole_forces_redistribution( f_on_bead, f_dipoles, trans_matrices, a, b, c, d, type_array, last_bb ): - """Redistribute electrostatic forces calculated from topologically + """Redistribute electrostatic forces calculated from topologically reconstructed ghost dipole point charges to the backcone atoms of the protein. """ f_on_bead.fill(0.0) for i, j, k, l, fd, matrix, dih_type, is_last in zip( a, b, c, d, f_dipoles, trans_matrices, type_array, last_bb ): - if dih_type ==1: + if dih_type == 1: sum_force = fd[0] + fd[1] diff_force = fd[0] - fd[1] f_on_bead[i] += matrix[0] @ diff_force # Atom A diff --git a/hymd/hamiltonian.py b/hymd/hamiltonian.py index 5137b76d..770c95ca 100644 --- a/hymd/hamiltonian.py +++ b/hymd/hamiltonian.py @@ -14,8 +14,8 @@ class Hamiltonian: - """Interaction energy functional superclass - """ + """Interaction energy functional superclass""" + def __init__(self, config): """Constructor @@ -39,20 +39,26 @@ def _setup(self): functionals in Hamiltonian subclasses. """ if not hasattr(self.config, "simulation_volume"): - self.config.simulation_volume = np.prod( - np.asarray(self.config.box_size) - ) - self.config.rho0 = ( - self.config.n_particles / self.config.simulation_volume - ) - self.phi = sympy.var("phi:%d" % (len(self.config.unique_names))) + self.config.simulation_volume = np.prod(np.asarray(self.config.box_size)) + if not self.config.barostat: + self.config.rho0 = self.config.n_particles / self.config.simulation_volume + self.config.a = self.config.rho0 + if not self.config.rho0: + self.config.rho0 = self.config.n_particles / self.config.simulation_volume + self.phi = sympy.var("phi:%d" % (self.config.n_types)) k = sympy.var("k:%d" % (3)) + # electrostatics variables + self.psi = sympy.var("psi") + self.phi_q = sympy.var("phi_q") + if not self.config.self_energy: + self.config.self_energy = 0.0 + def fourier_space_window_function(k): return sympy.functions.elementary.exponential.exp( -0.5 - * self.config.sigma ** 2 - * (k0 ** 2 + k1 ** 2 + k2 ** 2) # noqa: F821, E501 + * self.config.sigma**2 + * (k0**2 + k1**2 + k2**2) # noqa: F821, E501 ) self.window_function_lambda = sympy.lambdify( @@ -116,6 +122,7 @@ class SquaredPhi(Hamiltonian): -------- hymd.hamiltonian.DefaultNoChi """ + def __init__(self, config): """Constructor @@ -133,16 +140,61 @@ def __init__(self, config): self.setup() def setup(self): - """Setup the interaction energy potential and the external potential - """ + """Setup the interaction energy potential and the external potential""" + def w(phi, kappa=self.config.kappa, rho0=self.config.rho0): return 0.5 / (kappa * rho0) * (sum(phi)) ** 2 + def w_elec( + phi_q, + psi, + volume=self.config.simulation_volume, + self_energy=self.config.self_energy, + ): + self_energy /= volume + return 0.5 * phi_q * psi - self_energy + + def V_bar_0( + phi, + k, + kappa=self.config.kappa, + rho0=self.config.rho0, + ): + V_incompressibility = 1 / (kappa * rho0) * sum(phi) + V_interaction = 0 + return V_interaction + V_incompressibility + + def V_bar_elec( + psi, + t, + type_charges=self.config.type_charges, + ): + return type_charges[t] * psi + + self.V_bar = [ + sympy.lambdify( + [(self.phi, self.psi)], V_bar_0(self.phi, t) + V_bar_elec(self.psi, t) + ) + for t in range(self.config.n_types) + ] + self.v_ext = [ sympy.lambdify([self.phi], sympy.diff(w(self.phi), self.phi[i])) - for i in range(len(self.config.unique_names)) + for i in range(self.config.n_types) ] - self.w = sympy.lambdify([self.phi], w(self.phi)) + + self.w_0 = sympy.lambdify([self.phi], w(self.phi)) + self.w_elec = sympy.lambdify( + [(self.phi_q, self.psi)], w_elec(self.phi_q, self.psi) + ) + + if self.config.coulombtype == "PIC_Spectral": + self.w = sympy.lambdify( + [(self.phi, self.phi_q, self.psi)], + w(self.phi) + w_elec(self.phi_q, self.psi), + ) + else: + self.w = self.w_0 class DefaultNoChi(Hamiltonian): @@ -153,13 +205,15 @@ class DefaultNoChi(Hamiltonian): .. math:: w[\\tilde\\phi] = \\frac{1}{2\\kappa} \\left( - \\sum_k \\tilde\\phi_k - \\rho_0 + \\sum_k \\tilde\\phi_k - a \\right)^2, - where :math:`\\kappa` is the compressibility and :math:`\\rho_0` is the - average density of the fully homogenous system. The :code:`SquaredPhi` - Hamiltonian implements a similar functional with an additional linear term - component depending on + where :math:`\\kappa` is the compressibility and :math:`a=\\rho_0` for + NVT runs where :math:`\\rho_0` is the average density of the fully + homogenous system. In case of NPT runs, :math:`a` is a calibrated + parameter to obtain the correct average density at the target temperature + and pressure. The :code:`SquaredPhi` Hamiltonian implements a similar + functional with an additional linear term component depending on .. math:: @@ -175,6 +229,7 @@ class DefaultNoChi(Hamiltonian): hymd.input_parser.Config : Configuration dataclass handler. """ + def __init__(self, config): """Constructor @@ -192,16 +247,62 @@ def __init__(self, config): self.setup() def setup(self): - """Setup the interaction energy potential and the external potential - """ - def w(phi, kappa=self.config.kappa, rho0=self.config.rho0): - return 0.5 / (kappa * rho0) * (sum(phi) - rho0) ** 2 + """Setup the interaction energy potential and the external potential""" + + def w(phi, kappa=self.config.kappa, rho0=self.config.rho0, a=self.config.a): + return 0.5 / (kappa * rho0) * (sum(phi) - a) ** 2 + + def w_elec( + phi_q, + psi, + volume=self.config.simulation_volume, + self_energy=self.config.self_energy, + ): + self_energy /= volume + return 0.5 * phi_q * psi - self_energy + + def V_bar_0( + phi, + k, + kappa=self.config.kappa, + rho0=self.config.rho0, + a=self.config.a, + ): + V_incompressibility = 1 / (kappa * rho0) * (sum(phi) - a) + V_interaction = 0 + return V_interaction + V_incompressibility + + def V_bar_elec( + psi, + t, + type_charges=self.config.type_charges, + ): + return type_charges[t] * psi + + self.V_bar = [ + sympy.lambdify( + [(self.phi, self.psi)], V_bar_0(self.phi, t) + V_bar_elec(self.psi, t) + ) + for t in range(self.config.n_types) + ] self.v_ext = [ sympy.lambdify([self.phi], sympy.diff(w(self.phi), self.phi[i])) - for i in range(len(self.config.unique_names)) + for i in range(self.config.n_types) ] - self.w = sympy.lambdify([self.phi], w(self.phi)) + + self.w_0 = sympy.lambdify([self.phi], w(self.phi)) + self.w_elec = sympy.lambdify( + [(self.phi_q, self.psi)], w_elec(self.phi_q, self.psi) + ) + + if self.config.coulombtype == "PIC_Spectral": + self.w = sympy.lambdify( + [(self.phi, self.phi_q, self.psi)], + w(self.phi) + w_elec(self.phi_q, self.psi), + ) + else: + self.w = self.w_0 class DefaultWithChi(Hamiltonian): @@ -216,13 +317,17 @@ class DefaultWithChi(Hamiltonian): \\sum_{k,l}\\chi_{kl} \\tilde\\phi_k \\tilde\\phi_l + \\frac{1}{2\\kappa} \\left( - \\sum_k \\tilde\\phi_k - \\rho_0 + \\sum_k \\tilde\\phi_k - a \\right)^2, - where :math:`\\kappa` is the incompressibility, :math:`\\rho_0` is the - average density of the fully homogenous system and :math:`\\chi_{ij}` is - the Flory-Huggins-like inter-species mixing energy. + where :math:`\\kappa` is the compressibility and :math:`a=\\rho_0` for + NVT runs where :math:`\\rho_0` is the average density of the fully + homogenous system. In case of NPT runs, :math:`a` is a calibrated + parameter to obtain the correct average density at the target temperature + and pressure. :math:`\\chi_{ij}` is the Flory-Huggins-like + inter-species mixing energy. """ + def __init__(self, config, unique_names, type_to_name_map): """Constructor @@ -266,17 +371,21 @@ def setup(self, unique_names, type_to_name_map): tuple(sorted([c.atom_1, c.atom_2])): c.interaction_energy for c in self.config.chi } + self.phi_laplacian = [ + sympy.var("phi_laplacian%d(0:%d)" % (t, 3)) + for t in range(self.config.n_types) + ] def w( phi, kappa=self.config.kappa, rho0=self.config.rho0, + a=self.config.a, chi=self.config.chi, type_to_name_map=self.type_to_name_map, chi_type_dictionary=self.chi_type_dictionary, ): - - interaction = 0 + interaction = 0.0 for i in range(self.config.n_types): for j in range(i + 1, self.config.n_types): ni = type_to_name_map[i] @@ -285,18 +394,79 @@ def w( c = chi_type_dictionary[tuple(names)] interaction += c * phi[i] * phi[j] / rho0 - incompressibility = 0.5 / (kappa * rho0) * (sum(phi) - rho0) ** 2 + incompressibility = 0.5 / (kappa * rho0) * (sum(phi) - a) ** 2 return incompressibility + interaction + def w_elec( + phi_q, + psi, + volume=self.config.simulation_volume, + self_energy=self.config.self_energy, + ): + self_energy /= volume + return 0.5 * phi_q * psi - self_energy + + def V_bar_0( + phi, + t, + kappa=self.config.kappa, + rho0=self.config.rho0, + a=self.config.a, + chi=self.config.chi, + type_to_name_map=self.type_to_name_map, + chi_type_dictionary=self.chi_type_dictionary, + ): + V_incompressibility = 1 / (kappa * rho0) * (sum(phi) - a) + + V_interaction = 0.0 + nk = type_to_name_map[t] + for i in range(self.config.n_types): + ni = type_to_name_map[i] + names = sorted([nk, ni]) + if ni != nk: + c = chi_type_dictionary[tuple(names)] + else: + c = 0.0 + # uncomment to count diagonal chi terms: + # c = chi_type_dictionary[tuple(names)] + V_interaction += c * phi[i] / rho0 + return V_interaction + V_incompressibility + + def V_bar_elec( + psi, + t, + type_charges=self.config.type_charges, + ): + return type_charges[t] * psi + + self.V_bar = [ + sympy.lambdify( + [(self.phi, self.psi)], V_bar_0(self.phi, t) + V_bar_elec(self.psi, t) + ) + for t in range(self.config.n_types) + ] + self.v_ext = [ - sympy.lambdify([self.phi], sympy.diff(w(self.phi), self.phi[i])) - for i in range(len(self.config.unique_names)) + sympy.lambdify([self.phi], sympy.diff(w(self.phi), self.phi[t])) + for t in range(self.config.n_types) ] - self.w = sympy.lambdify([self.phi], w(self.phi)) + + self.w_0 = sympy.lambdify([self.phi], w(self.phi)) + self.w_elec = sympy.lambdify( + [(self.phi_q, self.psi)], w_elec(self.phi_q, self.psi) + ) + + if self.config.coulombtype == "PIC_Spectral": + self.w = sympy.lambdify( + [(self.phi, self.phi_q, self.psi)], + w(self.phi) + w_elec(self.phi_q, self.psi), + ) + else: + self.w = self.w_0 def get_hamiltonian(config): - """Return appropriate Hamiltonian object based on the + """Return appropriate Hamiltonian object based on the config.hamiltonian string. Parameters @@ -318,4 +488,4 @@ def get_hamiltonian(config): elif config.hamiltonian.lower() == "squaredphi": hamiltonian = SquaredPhi(config) - return hamiltonian \ No newline at end of file + return hamiltonian diff --git a/hymd/input_parser.py b/hymd/input_parser.py index 1e51cc87..64150a23 100644 --- a/hymd/input_parser.py +++ b/hymd/input_parser.py @@ -9,7 +9,8 @@ from mpi4py import MPI from dataclasses import dataclass, field from typing import List, Union, ClassVar -from .force import Bond, Angle, Dihedral, Chi +from .force import Bond, Angle, Dihedral, Chi, Dielectric_type +from .barostat import Target_pressure from .logger import Logger @@ -138,6 +139,12 @@ class Config: using helical propensity dihedrals, this keyword must be specified—even if electrostatics are not included with the :code:`coulombtype` keyword. + dielectric_type: list[float], optional + Specifies the relative dielectric constant of the simulation medium + which regulates the strength of the electrostatic interactions. The list assigns + relative dielectric values to each bead type, and an anisotropic + dielectric function is obtained from a weighted average. + See also -------- @@ -148,16 +155,18 @@ class Config: hymd.input_parser.Dihedral : Four-particle bond type dataclass. """ + gas_constant: ClassVar[float] = 0.0083144621 # kJ mol-1 K-1 coulomb_constant: ClassVar[float] = 138.935458 # kJ nm mol-1 e-2 n_steps: int time_step: float - box_size: Union[List[float], np.ndarray] mesh_size: Union[Union[List[int], np.ndarray], int] sigma: float kappa: float + dtype: np.dtype = None + box_size: Union[List[float], np.ndarray] = None n_print: int = None tau: float = None start_temperature: Union[float, bool] = None @@ -182,15 +191,35 @@ class Config: initial_energy: float = None cancel_com_momentum: Union[int, bool] = False coulombtype: str = None + convergence_type: str = None + pol_mixing: float = None dielectric_const: float = None + conv_crit: float = None + dielectric_type: List[Dielectric_type] = field(default_factory=list) + self_energy: float = None + type_charges: Union[List[float], np.ndarray] = None + + # For NPT runs + rho0: float = None + a: float = None + pressure: bool = False + barostat: str = None + barostat_type: str = None + tau_p: float = None + target_pressure: List[Target_pressure] = field(default_factory=list) + n_b: int = None + m: List[float] = field(default_factory=list) def __str__(self): + target_pressure_str = "\ttarget_pressure:\n" + "".join( + "\t\tP_L: " + + f"{self.target_pressure.P_L}\n" + + "\t\tP_N: " + + f"{self.target_pressure.P_N}\n" + ) bonds_str = "\tbonds:\n" + "".join( [ - ( - f"\t\t{k.atom_1} {k.atom_2}: " - f"{k.equilibrium}, {k.strength}\n" - ) + (f"\t\t{k.atom_1} {k.atom_2}: " f"{k.equilibrium}, {k.strength}\n") for k in self.bonds ] ) @@ -211,9 +240,7 @@ def __str__(self): # there's an easier way + ( "\n\t\t" - + " " * len( - f"{k.atom_1} {k.atom_2} {k.atom_3} {k.atom_4}: " - ) + + " " * len(f"{k.atom_1} {k.atom_2} {k.atom_3} {k.atom_4}: ") ).join( map( str, @@ -227,9 +254,7 @@ def __str__(self): ) + ( "\n\t\t" - + " " * len( - f"{k.atom_1} {k.atom_2} {k.atom_3} {k.atom_4}: " - ) + + " " * len(f"{k.atom_1} {k.atom_2} {k.atom_3} {k.atom_4}: ") ) + f"dih_type = {k.dih_type}\n" ) @@ -242,6 +267,17 @@ def __str__(self): for k in self.chi ] ) + + """ If dielectric wanted as dictionary + dielectric_type_str = "\tdielectric_type:\n" + "".join( + [ + (f"\t\t{k.atom_1}: " + f"{k.dielectric_value}\n") + for k in self.dielectric_type + ] + ) + + """ + thermostat_coupling_groups_str = "" if any(self.thermostat_coupling_groups): thermostat_coupling_groups_str = ( @@ -257,6 +293,7 @@ def __str__(self): ret_str = f'\n\n\tConfig: {self.file_name}\n\t{50 * "-"}\n' for k, v in self.__dict__.items(): if k not in ( + "target_pressure", "bonds", "angle_bonds", "dihedrals", @@ -265,7 +302,8 @@ def __str__(self): ): ret_str += f"\t{k}: {v}\n" ret_str += ( - bonds_str + target_pressure_str + + bonds_str + angle_str + dihedrals_str + chi_str @@ -275,8 +313,8 @@ def __str__(self): def read_config_toml(file_path): - with open(file_path, "r") as in_file: - toml_content = in_file.read() + with open(file_path, "rb") as in_file: + toml_content = tomli.load(in_file) return toml_content @@ -329,11 +367,11 @@ def propensity_potential_coeffs(x: float, comm): def parse_config_toml(toml_content, file_path=None, comm=MPI.COMM_WORLD): - parsed_toml = tomli.loads(toml_content) config_dict = {} # Defaults = None for n in ( + "box_size", "n_print", "tau", "start_temperature", @@ -346,19 +384,43 @@ def parse_config_toml(toml_content, file_path=None, comm=MPI.COMM_WORLD): "n_particles", "max_molecule_size", "coulombtype", + "convergence_type", "dielectric_const", + "pol_mixing", + "conv_crit", + "dielectric_type", + "n_b", + "box_size", "n_flush", + "self_energy", + "dtype", ): config_dict[n] = None # Defaults = [] - for n in ("bonds", "angle_bonds", "dihedrals", "chi", "tags"): + + for n in ( + "bonds", + "angle_bonds", + "dihedrals", + "chi", + "tags", + "m", + "dielectric_type", + "target_pressure", + "type_charges", + ): config_dict[n] = [] + # Defaults: bool + config_dict["pressure"] = False + # Flatten the .toml dictionary, ignoring the top level [tag] directives (if # any). - for k, v in parsed_toml.items(): + for k, v in toml_content.items(): if isinstance(v, dict): + if k == "nn": # Don't parse diff-hymd optimization options + continue for nested_k, nested_v in v.items(): config_dict[nested_k] = nested_v else: @@ -390,19 +452,14 @@ def parse_config_toml(toml_content, file_path=None, comm=MPI.COMM_WORLD): dih_type = int(b[2][0]) except IndexError: Logger.rank0.log( - logging.WARNING, - "Dihedral type not provided, defaulting to 0." + logging.WARNING, "Dihedral type not provided, defaulting to 0." ) dih_type = 0 # Probably it's better to move this in check_dihedrals? wrong_len = len(b[1]) not in (1, 2) - wrong_type_1 = len(b[1]) == 1 and not isinstance( - b[1][0], float - ) - wrong_type_2 = len(b[1]) == 2 and not isinstance( - b[1][0], list - ) + wrong_type_1 = len(b[1]) == 1 and not isinstance(b[1][0], float) + wrong_type_2 = len(b[1]) == 2 and not isinstance(b[1][0], list) if wrong_len or wrong_type_1 or wrong_type_2: err_str = ( "The coefficients specified for the dihedral type " @@ -425,9 +482,7 @@ def parse_config_toml(toml_content, file_path=None, comm=MPI.COMM_WORLD): elif dih_type == 2: coeff = np.array(b[1]) else: - coeff = np.insert( - np.array(b[1]), 2, np.zeros((2, 5)), axis=0 - ) + coeff = np.insert(np.array(b[1]), 2, np.zeros((2, 5)), axis=0) config_dict["dihedrals"][i] = Dihedral( atom_1=b[0][0], @@ -437,9 +492,6 @@ def parse_config_toml(toml_content, file_path=None, comm=MPI.COMM_WORLD): coeffs=coeff, dih_type=dih_type, ) - # if k == "improper dihedrals": - # config_dict["improper dihedrals"] = [None] * len(v) - # ... if k == "chi": config_dict["chi"] = [None] * len(v) for i, c in enumerate(v): @@ -448,11 +500,31 @@ def parse_config_toml(toml_content, file_path=None, comm=MPI.COMM_WORLD): atom_1=c_[0], atom_2=c_[1], interaction_energy=c[2] ) + """ + if k == "dielectric_type": + config_dict["dielectric_type"] = [None] * len(v) + for i, c in enumerate(v): + c_ = sorted([c[0][0]]) + config_dict["dielectric_type"][i] = Dielectric_type( + atom_1=c_[0], dielectric_value=c[1][0] + ) + """ + if k == "target_pressure": + if len(v) == 2: + config_dict["target_pressure"] = Target_pressure( + P_L=v[0], + P_N=v[1], # check condition # V array (still read as array?) + ) + elif len(v) == 1: + config_dict["target_pressure"] = Target_pressure(P_L=v[0], P_N=None) + else: + config_dict["target_pressure"] = Target_pressure(P_L=None, P_N=None) + if file_path is not None: config_dict["file_name"] = file_path for n in ( - "n_steps", "time_step", "box_size", "mesh_size", "sigma", "kappa" + "n_steps", "time_step", "mesh_size", "sigma", "kappa" ): if n not in config_dict: err_str = ( @@ -639,9 +711,7 @@ def check_dihedrals(config, names, comm=MPI.COMM_WORLD): ] missing_names = [ atom - for i, atom in enumerate( - [d.atom_1, d.atom_2, d.atom_3, d.atom_4] - ) + for i, atom in enumerate([d.atom_1, d.atom_2, d.atom_3, d.atom_4]) if missing[i] ] missing_str = ", ".join(np.unique(missing_names)) @@ -686,7 +756,7 @@ def check_chi(config, names, comm=MPI.COMM_WORLD): warnings.warn(warn_str) for i, n in enumerate(unique_names): - for m in unique_names[i+1:]: + for m in unique_names[i + 1 :]: found = False for c in config.chi: if (c.atom_1 == n and c.atom_2 == m) or ( @@ -694,9 +764,7 @@ def check_chi(config, names, comm=MPI.COMM_WORLD): ): found = True if not found: - config.chi.append( - Chi(atom_1=n, atom_2=m, interaction_energy=0.0) - ) + config.chi.append(Chi(atom_1=n, atom_2=m, interaction_energy=0.0)) warn_str = ( f"Atom types {n} and {m} found in the " f"system, but no chi interaction {n}--{m} " @@ -709,7 +777,27 @@ def check_chi(config, names, comm=MPI.COMM_WORLD): return config -def check_box_size(config, comm=MPI.COMM_WORLD): +def check_box_size(config, input_box, comm=MPI.COMM_WORLD): + if config.box_size is not None: + config.box_size = np.array(config.box_size, dtype=np.float32) + if input_box.all() and not np.allclose(config.box_size, input_box, atol=0.009): + err_str = ( + f"Box size specified in {config.file_name}: " + f"{config.box_size} does not match input box:" + f"{input_box}" + ) + Logger.rank0.log(logging.ERROR, err_str) + if comm.Get_rank() == 0: + raise ValueError(err_str) + else: + if input_box.all(): + config.box_size = input_box + else: + err_str = f"No box information found" + Logger.rank0.log(logging.ERROR, err_str) + if comm.Get_rank() == 0: + raise ValueError(err_str) + for b in config.box_size: if b <= 0.0: err_str = ( @@ -719,7 +807,6 @@ def check_box_size(config, comm=MPI.COMM_WORLD): Logger.rank0.log(logging.ERROR, err_str) if comm.Get_rank() == 0: raise ValueError(err_str) - config.box_size = np.array(config.box_size, dtype=np.float64) return config @@ -763,10 +850,7 @@ def check_integrator(config, comm=MPI.COMM_WORLD): Logger.rank0.log(logging.ERROR, err_str) raise ValueError(err_str) - if ( - config.integrator.lower() == "velocity-verlet" - and config.respa_inner != 1 - ): + if config.integrator.lower() == "velocity-verlet" and config.respa_inner != 1: warn_str = ( f"Integrator type Velocity-Verlet specified in {config.file_name} " f"and inner rRESPA time steps set to {config.respa_inner}. " @@ -822,15 +906,15 @@ def check_n_flush(config, comm=MPI.COMM_WORLD): def check_n_print(config, comm=MPI.COMM_WORLD): - if (not isinstance(config.n_print, int) and - not isinstance(config.n_print, float) and - config.n_print is not None): - err_str = ( - f"invalid value for n_print ({config.n_print})" - ) + if ( + not isinstance(config.n_print, int) + and not isinstance(config.n_print, float) + and config.n_print is not None + ): + err_str = f"invalid value for n_print ({config.n_print})" Logger.rank0.log(logging.ERROR, err_str) - raise RuntimeError(err_str) - + raise RuntimeError(err_str) + if config.n_print is None or config.n_print <= 0: config.n_print = False elif not isinstance(config.n_print, int): @@ -845,9 +929,7 @@ def check_n_print(config, comm=MPI.COMM_WORLD): config.n_print = int(round(config.n_print)) else: - err_str = ( - f"invalid value for n_print ({config.n_print})" - ) + err_str = f"invalid value for n_print ({config.n_print})" Logger.rank0.log(logging.ERROR, err_str) raise RuntimeError(err_str) return config @@ -917,8 +999,7 @@ def check_mass(config, comm=MPI.COMM_WORLD): Logger.rank0.log(logging.INFO, info_str) elif not isinstance(config.mass, int) and not isinstance(config.mass, float): err_str = ( - f"specified mass is invalid type {config.mass}, " - f"({type(config.mass)})" + f"specified mass is invalid type {config.mass}, " f"({type(config.mass)})" ) Logger.rank0.log(logging.ERROR, err_str) raise TypeError(err_str) @@ -959,8 +1040,10 @@ def check_domain_decomposition(config, comm=MPI.COMM_WORLD): if comm.Get_rank() == 0: warnings.warn(warn_str) else: - err_str = (f"invalid value for domain_decomposition " - f"({config.domain_decomposition}) use an integer") + err_str = ( + f"invalid value for domain_decomposition " + f"({config.domain_decomposition}) use an integer" + ) Logger.rank0.log(logging.ERROR, err_str) raise ValueError(err_str) @@ -971,9 +1054,7 @@ def check_name(config, comm=MPI.COMM_WORLD): if config.name is None: root_current_time = "" if comm.Get_rank() == 0: - root_current_time = datetime.datetime.now().strftime( - "%m/%d/%Y, %H:%M:%S" - ) + root_current_time = datetime.datetime.now().strftime("%m/%d/%Y, %H:%M:%S") current_time = comm.bcast(root_current_time, root=0) if config.name is None: @@ -983,6 +1064,84 @@ def check_name(config, comm=MPI.COMM_WORLD): return config +def check_NPT_conditions(config, comm=MPI.COMM_WORLD): + """ + Check validity of barostat_type, barostat, + a, rho0, target_pressure, tau_p + """ + if config.barostat is None: + if ( + config.tau_p is not None + or (config.target_pressure.P_L and config.target_pressure.P_N) is not None + ): + err_str = ( + "barostat not specified but config.tau_p " + "or config.target_pressure specified, cannot start simulation {config.barostat}" + ) + Logger.rank0.log(logging.ERROR, err_str) + if comm.Get_rank() == 0: + raise TypeError(err_str) + if config.a: + warn_str = "a specified but no barostat," "setting a to average density" + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + if config.rho0: + warn_str = ( + "rho0 specified but no barostat," "setting rho0 to average density" + ) + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + + if config.barostat is not None: + if not config.barostat_type: + config.barostat_type = "berendsen" + warn_str = "barostat_type not specified," "setting to berendsen" + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + if config.barostat != "isotropic" and config.barostat != "semiisotropic": + err_str = "barostat option not recognised. Valid options: isotropic, semiisotropic" + Logger.rank0.log(logging.ERROR, err_str) + if comm.Get_rank() == 0: + raise TypeError(err_str) + if config.target_pressure.P_L is None: + config.target_pressure.P_L = 1.0 # bar + config.target_pressure.P_N = None + if config.barostat == "semiisotropic": + config.target_pressure.P_N = 1.0 # bar + warn_str = ( + "barostat specified but no target_pressure, defaulting to 1.0 bar" + ) + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + if config.tau_p is None: + if config.tau <= 0.1: + config.tau_p = config.tau * 10.0 + else: + config.tau_p = 1.0 + warn_str = "barostat specified but no tau_p, defaulting to " + str( + config.tau_p + ) + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + if not config.a: + err_str = "a not specified; cannot start simulation {config.a}" + Logger.rank0.log(logging.ERROR, err_str) + if comm.Get_rank() == 0: + raise TypeError(err_str) + if not config.rho0: + warn_str = "rho0 not specified;" "setting rho0 to average density" + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + + return config + + def check_thermostat_coupling_groups(config, comm=MPI.COMM_WORLD): if any(config.thermostat_coupling_groups): found = [0 for _ in config.unique_names] @@ -1022,6 +1181,22 @@ def check_thermostat_coupling_groups(config, comm=MPI.COMM_WORLD): return config +def check_m(config, comm=MPI.COMM_WORLD): + if config.m == []: + config.m = [1.0 for t in range(config.n_types)] + return config + + +def check_n_b(config, comm=MPI.COMM_WORLD): + if config.n_b is None: + warn_str = f"config.n_b not specified." "Defaulting to 1" + config.n_b = 1 + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + return config + + def check_cancel_com_momentum(config, comm=MPI.COMM_WORLD): if isinstance(config.cancel_com_momentum, int): if config.cancel_com_momentum == 0 or config.cancel_com_momentum < 0: @@ -1035,7 +1210,138 @@ def check_cancel_com_momentum(config, comm=MPI.COMM_WORLD): return config -def check_config(config, indices, names, types, comm=MPI.COMM_WORLD): +def sort_dielectric_by_type_id(config, charges, types): + """ + Creates a list of length N CG-particles, sorted after the charges from + the input HDF5 file. Used in file_io.py + """ + dielectric_val = np.zeros(config.n_types) + for j in range(config.n_types): + name = config.dielectric_type[j][0][0] + type_id = config.name_to_type_map[name] + dielectric_val[type_id] = config.dielectric_type[j][1][0] + config.dielectric_type = dielectric_val.copy() + + N = len(charges) + len_list = np.zeros(config.n_types) + dielectric_by_types = np.zeros(N) + for i in range(N): + dielectric_by_types[i] = dielectric_val[types[i]] + # print("types ", types) + # print("diel val", dielectric_by_types) + # print("charges", charges) + # print(config.name_to_type_map) + return dielectric_by_types # by types with each particle id + + +def check_charges_types_list(config, types, charges, comm=MPI.COMM_WORLD): + """ + Creates a list of charge values of length types. + Charges are sorted according to type ID. Used in field.py. + # TODO: this is messy, we should fix it + """ + if charges is None: + config.type_charges = [0.0] * config.n_types + return config + + check_val = -100.0 # some random value that will never be encountered + charges_list = np.full(config.n_types, check_val) # gatherv cant handle None + rank = comm.Get_rank() + # print(charges_list) + for t_ in range(config.n_types): + if t_ in types: + charges_list[t_] = charges[types == t_][0] + + nprocs = int(comm.Get_size()) + recv_charges = None + if rank == 0: + recv_charges = np.full( + config.n_types * nprocs, check_val + ) # gatherv cant handle None + comm.Gather(charges_list, recv_charges, root=0) + + ## make a charges list + if rank == 0: + config_charges = np.zeros(config.n_types) + test_config = np.full(config.n_types, check_val) + for j in range(nprocs): + for i in range(config.n_types): + if recv_charges[i + j * config.n_types] != check_val: + config_charges[i] = recv_charges[i + j * config.n_types] + if np.any([test_config, config_charges]): + continue + else: + break + + # print(config_charges) + # print(config.name_to_type_map) + # print(charges[types == 0][0],charges[types == 1][0],charges[types == 2][0],charges[types == 3][0], + # charges[types == 4][0]) + else: + config_charges = np.zeros(config.n_types) + + comm.Bcast(config_charges, root=0) + # print("config_charges", config_charges , "rank", rank) + # print(recv_charges) + # rank = comm.Get_rank() + # print(all_charges) + config.type_charges = config_charges + return config + + +def check_dielectric(config, comm=MPI.COMM_WORLD): + """ + Error handling for electrostatics. + Unit testing of toml/tomli input. + """ + err_str_const = "Dielectric constant not given." + if config.coulombtype == "PIC_Spectral": + assert config.dielectric_const != None, err_str_const + + err_str = "Dielectric list is empty." + if config.coulombtype == "PIC_Spectral_GPE": + assert len(config.dielectric_type) != 0, err_str + # default values + if config.pol_mixing is None: + config.pol_mixing = 0.6 + if config.conv_crit is None: + config.conv_crit = 1e-6 + return config + + +def check_charges(config, charges, comm=MPI.COMM_WORLD): + """Check if charges across ranks sum to zero. + + Parameters + ---------- + charges : (N,) numpy.ndarray + Array of floats with charges for :code:`N` particles. + comm : mpi4py.Comm, optional + MPI communicator, defaults to :code:`mpi4py.COMM_WORLD`. + """ + total_charge = comm.allreduce(np.sum(charges), MPI.SUM) + + if not np.isclose(total_charge, 0.0): + warn_str = ( + f"Charges in the input file do not sum to zero. " + f"Total charge is {total_charge}." + ) + Logger.rank0.log(logging.WARNING, warn_str) + if comm.Get_rank() == 0: + warnings.warn(warn_str) + + # if charges are ok, compute self energy + if config.coulombtype == "PIC_Spectral": + from .field import compute_self_energy_q + + config.self_energy = compute_self_energy_q(config, charges, comm=comm) + + return config + + +def check_config( + config, indices, names, types, charges, input_box, comm=MPI.COMM_WORLD +): """Performs various checks on the specfied config to ensure consistency Parameters @@ -1048,6 +1354,8 @@ def check_config(config, indices, names, types, comm=MPI.COMM_WORLD): Array of string names for :code:`N` particles. types : (N,) numpy.ndarray Array of integer type indices for :code:`N` particles. + charges : (N,) numpy.ndarray + Array of floats charges for :code:`N` particles. comm : mpi4py.Comm, optional MPI communicator, defaults to :code:`mpi4py.COMM_WORLD`. @@ -1056,11 +1364,10 @@ def check_config(config, indices, names, types, comm=MPI.COMM_WORLD): config : Config Validated configuration object. """ - config.box_size = np.array(config.box_size) config = _find_unique_names(config, names, comm=comm) if types is not None: config = _setup_type_to_name_map(config, names, types, comm=comm) - config = check_box_size(config, comm=comm) + config = check_box_size(config, input_box, comm=comm) config = check_integrator(config, comm=comm) config = check_max_molecule_size(config, comm=comm) config = check_tau(config, comm=comm) @@ -1074,31 +1381,16 @@ def check_config(config, indices, names, types, comm=MPI.COMM_WORLD): config = check_angles(config, names, comm=comm) config = check_dihedrals(config, names, comm=comm) config = check_hamiltonian(config, comm=comm) + config = check_NPT_conditions(config, comm=comm) + config = check_n_b(config, comm=comm) + config = check_m(config, comm=comm) config = check_thermostat_coupling_groups(config, comm=comm) config = check_cancel_com_momentum(config, comm=comm) + config = check_dielectric(config, comm=comm) config = check_n_print(config, comm=comm) config = check_n_flush(config, comm=comm) - return config - - -def check_charges(charges, comm=MPI.COMM_WORLD): - """Check if charges across ranks sum to zero. - - Parameters - ---------- - charges : (N,) numpy.ndarray - Array of floats with charges for :code:`N` particles. - comm : mpi4py.Comm, optional - MPI communicator, defaults to :code:`mpi4py.COMM_WORLD`. - """ - total_charge = comm.allreduce(np.sum(charges), MPI.SUM) - - if not np.isclose(total_charge, 0.): - warn_str = ( - f"Charges in the input file do not sum to zero. " - f"Total charge is {total_charge}." - ) - Logger.rank0.log(logging.WARNING, warn_str) - if comm.Get_rank() == 0: - warnings.warn(warn_str) + config = check_charges_types_list(config, types, charges, comm=comm) + if charges is not None: + config = check_charges(config, charges, comm=comm) + return config diff --git a/hymd/logger.py b/hymd/logger.py index b81dac20..7fe7f8fd 100644 --- a/hymd/logger.py +++ b/hymd/logger.py @@ -8,8 +8,8 @@ class MPIFilterRoot(logging.Filter): - """Log output Filter wrapper class for the root MPI rank log - """ + """Log output Filter wrapper class for the root MPI rank log""" + def filter(self, record): """Log event message filter @@ -29,8 +29,8 @@ def filter(self, record): class MPIFilterAll(logging.Filter): - """Log output Filter wrapper class for the all-MPI-ranks log - """ + """Log output Filter wrapper class for the all-MPI-ranks log""" + def filter(self, record): """Log event message filter @@ -86,6 +86,7 @@ class Logger: Default logger object for messages being emitted from all MPI ranks simultaneously. """ + level = None log_file = None format = " %(levelname)-8s [%(filename)s:%(lineno)d] <%(funcName)s> {rank %(rank)d/%(size)d} %(message)s" # noqa: E501 @@ -193,32 +194,33 @@ def print_header(): /____/ """ - refs = """ - [1] Milano, G.; Kawakatsu, T. Hybrid particle-field molecular dynamics - simulations for dense polymer systems. J. Chem. Phys. 2009, 130, 214106. - - [2] Bore, S. L.; Cascella, M. Hamiltonian and alias-free hybrid - particle–field molecular dynamics. J. Chem. Phys. 2020, 153, 094106. + refs_set1 = """ - [3] Kolli, H. B.; De Nicola, A.; Bore, S. L.; Schäfer, K.; Diezemann, G.; - Gauss, J.; Kawakatsu, T.;Lu, Z.-Y.; Zhu, Y.-L.; Milano, G.; Cascella, M. - Hybrid Particle-Field Molecular DynamicsSimulations of Charged Amphiphiles - in an Aqueous Environment. J. Chem. Theory Comput. 2018, 14, 4928–4937. - - [4] Bore, S. L.; Milano, G.; Cascella, M. Hybrid Particle-Field Model for - Conformational Dynamics of Peptide Chains. J. Chem. Theory Comput. 2018, - 14, 1120–1130. + [1] Ledum, M.; Sen, S.; Li, X.; Carrer, M.; Feng Y.; Cascella, M.; Bore, S. L. + HylleraasMD: A Domain Decomposition-Based Hybrid Particle-Field Software for Multi-Scale Simulations of Soft Matter. ChemRxiv 2021 + + [2] Ledum, M.; Carrer, M.; Sen, S.; Li, X.; Cascella, M.; Bore, S. L. + HyMD: Massively parallel hybrid particle-field molecular dynamics in Python. - [5] Periole, X.; Cavalli, M.; Marrink, S. J.; Ceruso, M. A. Combining an - elastic network with a coarse-grained molecular force field: structure, - dynamics, and intermolecular recognition. J. Chem. Theory Comput. 2009, - 5.9, 2531-2543. + [3] Bore, S. L.; Cascella, M. + Hamiltonian and alias-free hybrid particle–field molecular dynamics. J. Chem. Phys. 2020, 153, 094106. + + [4] Pippig, M. PFFT: An extension of FFTW to massively parallel architectures. SIAM J. Sci. Comput. 2013, 35, C213–C236. +""" + refs_set2 = """ + + [5] Sen, S.; Ledum, M.; Bore, S. L.; Cascella, M. + Soft Matter under Pressure: Pushing Particle–Field Molecular Dynamics to the Isobaric Ensemble. ChemRxiv 2023 """ version = f"Version {get_version()}" header = banner - header += version.center(56)+"\n\n" - header += " Please read and cite accordingly the references below:" - header += refs - - return header \ No newline at end of file + header += version.center(56) + "\n\n" + #header += " Please read and cite the references below:" + header += " PLEASE READ AND CITE THE REFERENCES BELOW:" + header += refs_set1 + #header += " \n\n For constant pressure (NPT) simulations, please cite:" + header += " \n\n FOR CONSTANT PRESSURE (NPT) SIMULATIONS, PLEASE CITE:" + header += refs_set2 + + return header diff --git a/hymd/main.py b/hymd/main.py index 48c3df4e..dec2ddb7 100644 --- a/hymd/main.py +++ b/hymd/main.py @@ -10,17 +10,27 @@ import warnings from .configure_runtime import configure_runtime from .hamiltonian import get_hamiltonian -from .input_parser import check_config, check_charges +from .input_parser import check_config, sort_dielectric_by_type_id from .logger import Logger, format_timedelta from .file_io import distribute_input, OutDataset, store_static, store_data -from .field import (compute_field_force, update_field, - compute_field_and_kinetic_energy, domain_decomposition, - update_field_force_q, compute_field_energy_q, - compute_self_energy_q) -from .thermostat import (csvr_thermostat, cancel_com_momentum, - generate_initial_velocities) +from .field import ( + compute_field_force, + update_field, + compute_field_and_kinetic_energy, + domain_decomposition, + update_field_force_q, + update_field_force_q_GPE, + compute_field_energy_q_GPE, + initialize_pm, +) +from .thermostat import ( + csvr_thermostat, + cancel_com_momentum, + generate_initial_velocities, +) from .force import dipole_forces_redistribution, prepare_bonds from .integrator import integrate_velocity, integrate_position +from .pressure import comp_pressure def main(): @@ -36,31 +46,24 @@ def main(): if rank == 0: start_time = datetime.datetime.now() - args, config, prng = configure_runtime(sys.argv[1:], comm) + args, config, prng, topol = configure_runtime(sys.argv[1:], comm) if args.double_precision: dtype = np.float64 + config.dtype = dtype + from .force import compute_bond_forces__fortran__double as compute_bond_forces + from .force import compute_angle_forces__fortran__double as compute_angle_forces from .force import ( - compute_bond_forces__fortran__double as compute_bond_forces - ) - from .force import ( - compute_angle_forces__fortran__double as compute_angle_forces - ) - from .force import ( - compute_dihedral_forces__fortran__double as compute_dihedral_forces + compute_dihedral_forces__fortran__double as compute_dihedral_forces, ) else: dtype = np.float32 - from .force import ( - compute_bond_forces__fortran as compute_bond_forces - ) - from .force import ( - compute_angle_forces__fortran as compute_angle_forces - ) - from .force import ( - compute_dihedral_forces__fortran as compute_dihedral_forces - ) + config.dtype = dtype + from .force import compute_bond_forces__fortran as compute_bond_forces + from .force import compute_angle_forces__fortran as compute_angle_forces + from .force import compute_dihedral_forces__fortran as compute_dihedral_forces + # read input .hdf5 driver = "mpio" if not args.disable_mpio else None _kwargs = {"driver": driver, "comm": comm} if not args.disable_mpio else {} with h5py.File(args.input, "r", **_kwargs) as in_file: @@ -83,24 +86,81 @@ def main(): names = in_file["names"][rank_range] + if "box" in in_file.attrs: + config.box_size = np.array(in_file.attrs["box"]) + else: + if getattr(config, "box_size") is None: + err_str = ( + f"No box size present in either config or input file. Unable to start" + f" simulation." + ) + Logger.rank0.log(logging.ERROR, err_str) + if comm.Get_rank() == 0: + raise ValueError(err_str) + types = None bonds = None + charges = None molecules = [] if "types" in in_file: types = in_file["types"][rank_range] if molecules_flag: molecules = in_file["molecules"][rank_range] - bonds = in_file["bonds"][rank_range] + if topol is None: + bonds = in_file["bonds"][rank_range] if "charge" in in_file: charges = in_file["charge"][rank_range] charges_flag = True else: charges_flag = False - if charges_flag: - check_charges(charges, comm=comm) + if "box" in in_file: + input_box = in_file["box"][:] + else: + input_box = np.array([None, None, None]) + + # finishes config setup and checks + config = check_config(config, indices, names, types, charges, input_box, comm=comm) + + # import barostat if necessary + if config.barostat_type == "berendsen": + from .barostat import isotropic, semiisotropic + elif config.barostat_type == "scr": + from .barostat_scr import isotropic, semiisotropic + + # dielectric from toml + if config.coulombtype == "PIC_Spectral_GPE": + dielectric_sorted = sort_dielectric_by_type_id(config, charges, types) + dielectric_flag = True + + if config.convergence_type is not None: + if config.convergence_type == "csum": + + def conv_fun(comm, diff_mesh): + return diff_mesh.csum() # check if allreduce needed + + elif config.convergence_type == "euclidean_norm": - config = check_config(config, indices, names, types, comm=comm) + def conv_fun(comm, diff_mesh): + return diff_mesh.cnorm() # check if allreduce nedded + + elif config.convergence_type == "max_diff": + + def conv_fun(comm, diffmesh): + msg = np.max(diffmesh) + res = comm.allreduce(sendobj=msg, op=MPI.MAX) + return res + + else: # default choice is the max difference if not specified + config.convergence_type = "max_diff" + + def conv_fun(comm, diffmesh): + msg = np.max(diffmesh) + res = comm.allreduce(sendobj=msg, op=MPI.MAX) + return res + + else: + dielectric_flag = False if config.start_temperature: velocities = generate_initial_velocities(velocities, config, prng, comm=comm) @@ -138,53 +198,51 @@ def main(): dihedral_energy = 0.0 kinetic_energy = 0.0 field_q_energy = 0.0 - field_q_self_energy = 0.0 - - # Ignore numpy numpy.VisibleDeprecationWarning: Creating an ndarray from - # ragged nested sequences until it is fixed in pmesh - with warnings.catch_warnings(): - warnings.filterwarnings( - action="ignore", - category=np.VisibleDeprecationWarning, - message=r"Creating an ndarray from ragged nested sequences", - ) - pm = pmesh.ParticleMesh( - config.mesh_size, BoxSize=config.box_size, - dtype="f4" if dtype == np.float32 else np.float64, comm=comm - ) + + # more initialization + bond_pr_ = np.zeros(3, dtype=dtype) + angle_pr_ = np.zeros(3, dtype=dtype) hamiltonian = get_hamiltonian(config) - Logger.rank0.log(logging.INFO, f"pfft-python processor mesh: {str(pm.np)}") + pm_objs = initialize_pm(pmesh, config, comm) + pm, field_list, elec_common_list, coulomb_list = pm_objs + ( + phi, + phi_fourier, + force_on_grid, + v_ext_fourier, + v_ext, + phi_transfer, + phi_laplacian, + ) = field_list + + # when electrostatics is not used, these objs are None + phi_q, phi_q_fourier, psi, elec_field = elec_common_list + + if len(coulomb_list) == 2: + ( + elec_field_fourier, + psi_fourier, + ) = coulomb_list + + elif len(coulomb_list) == 12: + [ + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol, + phi_pol_prev, + elec_dot, + elec_field_contrib, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + ] = coulomb_list - # Initialize density fields - phi = [pm.create("real", value=0.0) for _ in range(config.n_types)] - phi_fourier = [ - pm.create("complex", value=0.0) for _ in range(config.n_types) - ] # noqa: E501 - force_on_grid = [ - [pm.create("real", value=0.0) for _ in range(3)] for _ in range(config.n_types) # noqa: E501 - ] - v_ext_fourier = [pm.create("complex", value=0.0) for _ in range(4)] - v_ext = [pm.create("real", value=0.0) for _ in range(config.n_types)] - - # Initialize charge density fields - _SPACE_DIM = 3 - if charges_flag and config.coulombtype == "PIC_Spectral": - phi_q = pm.create("real", value=0.0) - phi_q_fourier = pm.create("complex", value=0.0) - elec_field_fourier = [ - pm.create("complex", value=0.0) for _ in range(_SPACE_DIM) - ] # for force calculation - elec_field = [ - pm.create("real", value=0.0) for _ in range(_SPACE_DIM) - ] # for force calculation - elec_potential = pm.create( - "real", value=0.0 - ) - elec_potential_fourier = pm.create( - "complex", value=0.0 - ) + Logger.rank0.log(logging.INFO, f"pfft-python processor mesh: {str(pm.np)}") args_in = [ velocities, @@ -201,19 +259,23 @@ def main(): if charges_flag: args_in.append(charges) args_in.append(elec_forces) + if dielectric_flag: + args_in.append(dielectric_sorted) if config.domain_decomposition: - (positions, - velocities, - indices, - bond_forces, - angle_forces, - dihedral_forces, - reconstructed_forces, - field_forces, - names, - types, - *optional) = domain_decomposition( + ( + positions, + velocities, + indices, + bond_forces, + angle_forces, + dihedral_forces, + reconstructed_forces, + field_forces, + names, + types, + *optional, + ) = domain_decomposition( positions, pm, *tuple(args_in), @@ -226,6 +288,8 @@ def main(): if charges_flag: charges = optional.pop(0) elec_forces = optional.pop(0) + if dielectric_flag: + dielectric_sorted = optional.pop(0) if molecules_flag: bonds = optional.pop(0) molecules = optional.pop(0) @@ -246,66 +310,140 @@ def main(): if charges_flag: args_in.append(charges) args_in.append(elec_forces) + if dielectric_flag: + args_in.append(dielectric_sorted) if not args.disable_field: - layouts = [pm.decompose(positions[types == t]) for t in range(config.n_types)] # noqa: E501 + layouts = [ + pm.decompose(positions[types == t]) for t in range(config.n_types) + ] # noqa: E501 update_field( - phi, layouts, force_on_grid, hamiltonian, pm, positions, types, - config, v_ext, phi_fourier, v_ext_fourier, compute_potential=True, + phi, + phi_laplacian, + phi_transfer, + layouts, + force_on_grid, + hamiltonian, + pm, + positions, + types, + config, + v_ext, + phi_fourier, + v_ext_fourier, + config.m, + compute_potential=True, ) - field_energy, kinetic_energy = compute_field_and_kinetic_energy( - phi, velocities, hamiltonian, positions, types, v_ext, config, - layouts, comm=comm, + ( + field_energy, + kinetic_energy, + field_q_energy, + ) = compute_field_and_kinetic_energy( + phi, + phi_q, + psi, + velocities, + hamiltonian, + positions, + types, + v_ext, + config, + layouts, + comm=comm, ) compute_field_force( - layouts, positions, force_on_grid, field_forces, types, - config.n_types + layouts, positions, force_on_grid, field_forces, types, config.n_types ) else: - kinetic_energy = comm.allreduce( - 0.5 * config.mass * np.sum(velocities**2) - ) + kinetic_energy = comm.allreduce(0.5 * config.mass * np.sum(velocities**2)) - if charges_flag and config.coulombtype == "PIC_Spectral": - field_q_self_energy = compute_self_energy_q(config, charges, comm=comm) + if charges_flag: layout_q = pm.decompose(positions) - update_field_force_q( - charges, phi_q, phi_q_fourier, elec_field_fourier, elec_field, - elec_forces, layout_q, hamiltonian, pm, positions, config, - ) - - field_q_energy = compute_field_energy_q( - config, phi_q, phi_q_fourier, elec_potential, - elec_potential_fourier, - field_q_self_energy, comm=comm, - ) + if config.coulombtype == "PIC_Spectral_GPE": # dielectric_flag + Vbar_elec, phi_eps, elec_dot = update_field_force_q_GPE( + conv_fun, + phi, + types, + charges, + config_charges, + phi_q, + phi_q_fourier, + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol_prev, + phi_pol, + elec_field, + elec_forces, + elec_field_contrib, + psi, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + hamiltonian, + layout_q, + layouts, + pm, + positions, + config, + comm=comm, + ) - if molecules_flag: - if not (args.disable_bonds - and args.disable_angle_bonds - and args.disable_dihedrals): + field_q_energy = compute_field_energy_q_GPE( + config, phi_eps, field_q_energy, elec_dot, comm=comm + ) - bonds_prep = prepare_bonds( - molecules, names, bonds, indices, config + if config.coulombtype == "PIC_Spectral": + update_field_force_q( + charges, + phi_q, + phi_q_fourier, + psi, + psi_fourier, + elec_field_fourier, + elec_field, + elec_forces, + layout_q, + hamiltonian, + pm, + positions, + config, ) + + if molecules_flag: + if not ( + args.disable_bonds and args.disable_angle_bonds and args.disable_dihedrals + ): + bonds_prep = prepare_bonds(molecules, names, bonds, indices, config, topol) ( # two-particle bonds - bonds_2_atom1, bonds_2_atom2, bonds_2_equilibrium, + bonds_2_atom1, + bonds_2_atom2, + bonds_2_equilibrium, bonds_2_stength, # three-particle bonds - bonds_3_atom1, bonds_3_atom2, bonds_3_atom3, - bonds_3_equilibrium, bonds_3_stength, + bonds_3_atom1, + bonds_3_atom2, + bonds_3_atom3, + bonds_3_equilibrium, + bonds_3_stength, # four-particle bonds - bonds_4_atom1, bonds_4_atom2, bonds_4_atom3, bonds_4_atom4, - bonds_4_coeff, bonds_4_type, bonds_4_last, + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_coeff, + bonds_4_type, + bonds_4_last, ) = bonds_prep bonds_4_coeff = np.asfortranarray(bonds_4_coeff) if bonds_4_type.any() > 1: err_str = ( - "0 and 1 are the only currently supported dihedral angle " - "types." + "0 and 1 are the only currently supported dihedral angle " "types." ) Logger.rank0.log(logging.ERROR, err_str) if rank == 0: @@ -332,11 +470,12 @@ def main(): dtype=dtype, ).flatten() dipole_forces = np.zeros_like(dipole_positions) - transfer_matrices = np.zeros( - shape=(n_tors, 6, 3, 3), dtype=dtype - ) + transfer_matrices = np.zeros(shape=(n_tors, 6, 3, 3), dtype=dtype) phi_dipoles = pm.create("real", value=0.0) phi_dipoles_fourier = pm.create("complex", value=0.0) + psi_dipoles = pm.create("real", value=0.0) + psi_dipoles_fourier = pm.create("complex", value=0.0) + _SPACE_DIM = 3 dipoles_field_fourier = [ pm.create("complex", value=0.0) for _ in range(_SPACE_DIM) ] @@ -353,26 +492,47 @@ def main(): transfer_matrices = np.asfortranarray(transfer_matrices) if not args.disable_bonds: - bond_energy_ = compute_bond_forces( - bond_forces, positions, config.box_size, bonds_2_atom1, - bonds_2_atom2, bonds_2_equilibrium, bonds_2_stength, + bond_energy_, bond_pr_ = compute_bond_forces( + bond_forces, + positions, + config.box_size, + bonds_2_atom1, + bonds_2_atom2, + bonds_2_equilibrium, + bonds_2_stength, ) bond_energy = comm.allreduce(bond_energy_, MPI.SUM) else: bonds_2_atom1, bonds_2_atom2 = [], [] if not args.disable_angle_bonds: - angle_energy_ = compute_angle_forces( - angle_forces, positions, config.box_size, bonds_3_atom1, - bonds_3_atom2, bonds_3_atom3, bonds_3_equilibrium, + angle_energy_, angle_pr_ = compute_angle_forces( + angle_forces, + positions, + config.box_size, + bonds_3_atom1, + bonds_3_atom2, + bonds_3_atom3, + bonds_3_equilibrium, bonds_3_stength, ) angle_energy = comm.allreduce(angle_energy_, MPI.SUM) + # angle_pr = comm.allreduce(angle_pr_, MPI.SUM) + if not args.disable_dihedrals: dihedral_energy_ = compute_dihedral_forces( - dihedral_forces, positions, dipole_positions, - transfer_matrices, config.box_size, bonds_4_atom1, - bonds_4_atom2, bonds_4_atom3, bonds_4_atom4, bonds_4_coeff, - bonds_4_type, bonds_4_last, dipole_flag, + dihedral_forces, + positions, + dipole_positions, + transfer_matrices, + config.box_size, + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_coeff, + bonds_4_type, + bonds_4_last, + dipole_flag, ) dihedral_energy = comm.allreduce(dihedral_energy_, MPI.SUM) @@ -382,9 +542,19 @@ def main(): layout_dipoles = pm.decompose(dipole_positions) update_field_force_q( - dipole_charges, phi_dipoles, phi_dipoles_fourier, - dipoles_field_fourier, dipoles_field, dipole_forces, - layout_dipoles, hamiltonian, pm, dipole_positions, config, + dipole_charges, + phi_dipoles, + phi_dipoles_fourier, + psi_dipoles, + psi_dipoles_fourier, + dipoles_field_fourier, + dipoles_field, + dipole_forces, + layout_dipoles, + hamiltonian, + pm, + dipole_positions, + config, ) dipole_positions = np.reshape(dipole_positions, (n_tors, 4, 3)) @@ -392,60 +562,137 @@ def main(): dipole_positions = np.asfortranarray(dipole_positions) dipole_forces_redistribution( - reconstructed_forces, dipole_forces, transfer_matrices, - bonds_4_atom1, bonds_4_atom2, bonds_4_atom3, bonds_4_atom4, - bonds_4_type, bonds_4_last, + reconstructed_forces, + dipole_forces, + transfer_matrices, + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_type, + bonds_4_last, ) else: bonds_2_atom1, bonds_2_atom2 = [], [] config.initial_energy = ( - field_energy + kinetic_energy + bond_energy + angle_energy - + dihedral_energy + field_q_energy + field_energy + + kinetic_energy + + bond_energy + + angle_energy + + dihedral_energy + + field_q_energy ) out_dataset = OutDataset( - args.destdir, config, double_out=args.double_output, + args.destdir, + config, + double_out=args.double_output, disable_mpio=args.disable_mpio, ) store_static( - out_dataset, rank_range, names, types, indices, config, bonds_2_atom1, - bonds_2_atom2, molecules=molecules if molecules_flag else None, - velocity_out=args.velocity_output, force_out=args.force_output, - charges=charges if charges_flag else False, comm=comm, + out_dataset, + rank_range, + names, + types, + indices, + config, + bonds_2_atom1, + bonds_2_atom2, + molecules=molecules if molecules_flag else None, + velocity_out=args.velocity_output, + force_out=args.force_output, + charges=charges if charges_flag else False, + dielectrics=dielectric_sorted if dielectric_flag else False, + comm=comm, ) if config.n_print > 0: step = 0 frame = 0 if not args.disable_field: - field_energy, kinetic_energy = compute_field_and_kinetic_energy( - phi, velocities, hamiltonian, positions, types, v_ext, config, - layouts, comm=comm, + ( + field_energy, + kinetic_energy, + field_q_energy, + ) = compute_field_and_kinetic_energy( + phi, + phi_q, + psi, + velocities, + hamiltonian, + positions, + types, + v_ext, + config, + layouts, + comm=comm, ) else: - kinetic_energy = comm.allreduce( - 0.5 * config.mass * np.sum(velocities ** 2) - ) + kinetic_energy = comm.allreduce(0.5 * config.mass * np.sum(velocities**2)) temperature = ( - (2 / 3) * kinetic_energy / (config.gas_constant * config.n_particles) # noqa: E501 + (2 / 3) + * kinetic_energy + / (config.gas_constant * config.n_particles) # noqa: E501 ) + + if config.pressure or config.barostat: + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fourier, + phi_laplacian, + phi_transfer, + positions, + bond_pr_, + angle_pr_, + comm=comm, + ) + + # if rank ==0 : print(pressure[9:12]) + # print('phi_fft after pressure call: phi_fft[d=0]',phi_fft[0].value[0][0][0:2]) + else: + pressure = 0.0 # 0.0 indicates not calculated. To be changed. + forces_out = ( - field_forces + bond_forces + angle_forces + dihedral_forces + field_forces + + bond_forces + + angle_forces + + dihedral_forces + reconstructed_forces ) if charges_flag: forces_out += elec_forces store_data( - out_dataset, step, frame, indices, positions, velocities, - forces_out, config.box_size, temperature, kinetic_energy, - bond_energy, angle_energy, dihedral_energy, field_energy, - field_q_energy, config.time_step, config, - velocity_out=args.velocity_output, force_out=args.force_output, - charge_out=charges_flag, dump_per_particle=args.dump_per_particle, + out_dataset, + step, + frame, + indices, + positions, + velocities, + forces_out, + config.box_size, + temperature, + pressure, + kinetic_energy, + bond_energy, + angle_energy, + dihedral_energy, + field_energy, + field_q_energy, + config.time_step, + config, + velocity_out=args.velocity_output, + force_out=args.force_output, + charge_out=charges_flag, + dump_per_particle=args.dump_per_particle, comm=comm, ) @@ -471,9 +718,7 @@ def main(): if rank == 0 and log_step and args.verbose > 1: step_t = current_step_time - last_step_time tot_t = current_step_time - loop_start_time - ns_sim = ( - (step + 1) * config.respa_inner * config.time_step / 1000.0 - ) + ns_sim = (step + 1) * config.respa_inner * config.time_step / 1000.0 seconds_per_day = 24 * 60 * 60 seconds_elapsed = tot_t.days * seconds_per_day @@ -496,17 +741,24 @@ def main(): # Initial outer rRESPA velocity step velocities = integrate_velocity( - velocities, field_forces / config.mass, + velocities, + field_forces / config.mass, config.respa_inner * config.time_step, ) - if charges_flag and config.coulombtype == "PIC_Spectral": + + if charges_flag and ( + config.coulombtype == "PIC_Spectral" + or config.coulombtype == "PIC_Spectral_GPE" + ): velocities = integrate_velocity( - velocities, elec_forces / config.mass, + velocities, + elec_forces / config.mass, config.respa_inner * config.time_step, ) if protein_flag: velocities = integrate_velocity( - velocities, reconstructed_forces / config.mass, + velocities, + reconstructed_forces / config.mass, config.respa_inner * config.time_step, ) @@ -517,37 +769,51 @@ def main(): (bond_forces + angle_forces + dihedral_forces) / config.mass, config.time_step, ) - positions = integrate_position( - positions, velocities, config.time_step - ) + positions = integrate_position(positions, velocities, config.time_step) positions = np.mod(positions, config.box_size[None, :]) # Update fast forces if molecules_flag: if not args.disable_bonds: - bond_energy_ = compute_bond_forces( - bond_forces, positions, config.box_size, bonds_2_atom1, - bonds_2_atom2, bonds_2_equilibrium, bonds_2_stength, + bond_energy_, bond_pr_ = compute_bond_forces( + bond_forces, + positions, + config.box_size, + bonds_2_atom1, + bonds_2_atom2, + bonds_2_equilibrium, + bonds_2_stength, ) if not args.disable_angle_bonds: - angle_energy_ = compute_angle_forces( - angle_forces, positions, config.box_size, - bonds_3_atom1, bonds_3_atom2, bonds_3_atom3, - bonds_3_equilibrium, bonds_3_stength, + angle_energy_, angle_pr_ = compute_angle_forces( + angle_forces, + positions, + config.box_size, + bonds_3_atom1, + bonds_3_atom2, + bonds_3_atom3, + bonds_3_equilibrium, + bonds_3_stength, ) if not args.disable_dihedrals: - if ( - inner == config.respa_inner - 1 - and not args.disable_dipole - ): + if inner == config.respa_inner - 1 and not args.disable_dipole: dipole_flag = 1 else: dipole_flag = 0 dihedral_energy_ = compute_dihedral_forces( - dihedral_forces, positions, dipole_positions, - transfer_matrices, config.box_size, bonds_4_atom1, - bonds_4_atom2, bonds_4_atom3, bonds_4_atom4, - bonds_4_coeff, bonds_4_type, bonds_4_last, dipole_flag, + dihedral_forces, + positions, + dipole_positions, + transfer_matrices, + config.box_size, + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_coeff, + bonds_4_type, + bonds_4_last, + dipole_flag, ) velocities = integrate_velocity( @@ -556,46 +822,187 @@ def main(): config.time_step, ) + # Barostat + if config.barostat: + if config.barostat.lower() == "isotropic": + pm_objs, change = isotropic( + pmesh, + pm_objs, + phi, + phi_q, + psi, + hamiltonian, + positions, + velocities, + config, + phi_fourier, + phi_laplacian, + phi_transfer, + bond_pr_, + angle_pr_, + step, + prng, + comm=comm, + ) + + elif config.barostat.lower() == "semiisotropic": + pm_objs, change = semiisotropic( + pmesh, + pm_objs, + phi, + phi_q, + psi, + hamiltonian, + positions, + velocities, + config, + phi_fourier, + phi_laplacian, + phi_transfer, + bond_pr_, + angle_pr_, + step, + prng, + comm=comm, + ) + pm, field_list, elec_common_list, coulomb_list = pm_objs + ( + phi, + phi_fourier, + force_on_grid, + v_ext_fourier, + v_ext, + phi_transfer, + phi_laplacian, + ) = field_list + + phi_q, phi_q_fourier, psi, elec_field = elec_common_list + + if len(coulomb_list) == 2: + elec_field_fourier, psi_fourier = coulomb_list + + elif len(coulomb_list) == 12: + ( + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol, + phi_pol_prev, + elec_dot, + elec_field_contrib, + psi, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + ) = coulomb_list + # Update slow forces if not args.disable_field: layouts = [ - pm.decompose(positions[types == t]) for t in range(config.n_types) # noqa: E501 + pm.decompose(positions[types == t]) + for t in range(config.n_types) # noqa: E501 ] update_field( - phi, layouts, force_on_grid, hamiltonian, pm, positions, types, - config, v_ext, phi_fourier, v_ext_fourier, + phi, + phi_laplacian, + phi_transfer, + layouts, + force_on_grid, + hamiltonian, + pm, + positions, + types, + config, + v_ext, + phi_fourier, + v_ext_fourier, + config.m, ) compute_field_force( - layouts, positions, force_on_grid, field_forces, types, + layouts, + positions, + force_on_grid, + field_forces, + types, config.n_types, ) - if charges_flag and config.coulombtype == "PIC_Spectral": + if charges_flag: layout_q = pm.decompose(positions) - update_field_force_q( - charges, phi_q, phi_q_fourier, elec_field_fourier, - elec_field, elec_forces, layout_q, hamiltonian, - pm, positions, config, - ) - field_q_energy = compute_field_energy_q( - config, phi_q, phi_q_fourier, elec_potential, - elec_potential_fourier, - field_q_self_energy, comm=comm, - ) + if config.coulombtype == "PIC_Spectral_GPE": # dielectric_flag and + Vbar_elec, phi_eps, elec_dot = update_field_force_q_GPE( + conv_fun, + phi, + types, + charges, + config_charges, + phi_q, + phi_q_fourier, + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol_prev, + phi_pol, + elec_field, + elec_forces, + elec_field_contrib, + psi, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + hamiltonian, + layout_q, + layouts, + pm, + positions, + config, + comm=comm, + ) + + field_q_energy = compute_field_energy_q_GPE( + config, phi_eps, field_q_energy, elec_dot, comm=comm + ) + + if config.coulombtype == "PIC_Spectral": + update_field_force_q( + charges, + phi_q, + phi_q_fourier, + psi, + psi_fourier, + elec_field_fourier, + elec_field, + elec_forces, + layout_q, + hamiltonian, + pm, + positions, + config, + ) if protein_flag and not args.disable_dipole: - dipole_positions = np.reshape( - dipole_positions, (4 * n_tors, 3) - ) - dipole_forces = np.reshape( - dipole_forces, (4 * n_tors, 3) - ) + dipole_positions = np.reshape(dipole_positions, (4 * n_tors, 3)) + dipole_forces = np.reshape(dipole_forces, (4 * n_tors, 3)) layout_dipoles = pm.decompose(dipole_positions) update_field_force_q( - dipole_charges, phi_dipoles, phi_dipoles_fourier, - dipoles_field_fourier, dipoles_field, dipole_forces, - layout_dipoles, hamiltonian, pm, dipole_positions, config, + dipole_charges, + phi_dipoles, + phi_dipoles_fourier, + psi_dipoles, + psi_dipoles_fourier, + dipoles_field_fourier, + dipoles_field, + dipole_forces, + layout_dipoles, + hamiltonian, + pm, + dipole_positions, + config, ) dipole_positions = np.reshape(dipole_positions, (n_tors, 4, 3)) @@ -603,25 +1010,37 @@ def main(): dipole_positions = np.asfortranarray(dipole_positions) dipole_forces_redistribution( - reconstructed_forces, dipole_forces, transfer_matrices, - bonds_4_atom1, bonds_4_atom2, bonds_4_atom3, bonds_4_atom4, - bonds_4_type, bonds_4_last, + reconstructed_forces, + dipole_forces, + transfer_matrices, + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_type, + bonds_4_last, ) # Second rRESPA velocity step velocities = integrate_velocity( - velocities, field_forces / config.mass, + velocities, + field_forces / config.mass, config.respa_inner * config.time_step, ) - if charges_flag and config.coulombtype == "PIC_Spectral": + if charges_flag and ( + config.coulombtype == "PIC_Spectral" + or config.coulombtype == "PIC_Spectral_GPE" + ): velocities = integrate_velocity( - velocities, elec_forces / config.mass, + velocities, + elec_forces / config.mass, config.respa_inner * config.time_step, ) if protein_flag and not args.disable_dipole: velocities = integrate_velocity( - velocities, reconstructed_forces / config.mass, + velocities, + reconstructed_forces / config.mass, config.respa_inner * config.time_step, ) @@ -642,17 +1061,19 @@ def main(): angle_forces = np.ascontiguousarray(angle_forces) dihedral_forces = np.ascontiguousarray(dihedral_forces) - (positions, - velocities, - indices, - bond_forces, - angle_forces, - dihedral_forces, - reconstructed_forces, - field_forces, - names, - types, - *optional) = domain_decomposition( + ( + positions, + velocities, + indices, + bond_forces, + angle_forces, + dihedral_forces, + reconstructed_forces, + field_forces, + names, + types, + *optional, + ) = domain_decomposition( positions, pm, *tuple(args_in), @@ -665,6 +1086,8 @@ def main(): if charges_flag: charges = optional.pop(0) elec_forces = optional.pop(0) + if dielectric_flag: + dielectric_sorted = optional.pop(0) if molecules_flag: bonds = optional.pop(0) molecules = optional.pop(0) @@ -685,6 +1108,8 @@ def main(): if charges_flag: args_in.append(charges) args_in.append(elec_forces) + if dielectric_flag: + args_in.append(dielectric_sorted) positions = np.asfortranarray(positions) bond_forces = np.asfortranarray(bond_forces) @@ -692,22 +1117,32 @@ def main(): dihedral_forces = np.asfortranarray(dihedral_forces) layouts = [ - pm.decompose(positions[types == t]) for t in range(config.n_types) # noqa: E501 + pm.decompose(positions[types == t]) + for t in range(config.n_types) # noqa: E501 ] if molecules_flag: bonds_prep = prepare_bonds( - molecules, names, bonds, indices, config + molecules, names, bonds, indices, config, topol ) ( # two-particle bonds - bonds_2_atom1, bonds_2_atom2, bonds_2_equilibrium, + bonds_2_atom1, + bonds_2_atom2, + bonds_2_equilibrium, bonds_2_stength, # three-particle bonds - bonds_3_atom1, bonds_3_atom2, bonds_3_atom3, - bonds_3_equilibrium, bonds_3_stength, + bonds_3_atom1, + bonds_3_atom2, + bonds_3_atom3, + bonds_3_equilibrium, + bonds_3_stength, # four-particle bonds - bonds_4_atom1, bonds_4_atom2, bonds_4_atom3, - bonds_4_atom4, bonds_4_coeff, bonds_4_type, + bonds_4_atom1, + bonds_4_atom2, + bonds_4_atom3, + bonds_4_atom4, + bonds_4_coeff, + bonds_4_type, bonds_4_last, ) = bonds_prep @@ -718,9 +1153,7 @@ def main(): # Each rank will have different n_tors, we don't need # to domain decompose dipoles n_tors = len(bonds_4_atom1) - dipole_positions = np.zeros( - (n_tors, 4, 3), dtype=dtype - ) + dipole_positions = np.zeros((n_tors, 4, 3), dtype=dtype) # 4 because we need to take into account the last angle # in the molecule @@ -740,7 +1173,9 @@ def main(): shape=(n_tors, 6, 3, 3), dtype=dtype ) dipole_positions = np.asfortranarray(dipole_positions) - transfer_matrices = np.asfortranarray(transfer_matrices) # noqa: E501 + transfer_matrices = np.asfortranarray( + transfer_matrices + ) # noqa: E501 for t in range(config.n_types): if args.verbose > 2: @@ -755,7 +1190,7 @@ def main(): ) # Thermostat - if config.target_temperature: + if config.target_temperature and np.mod(step, config.n_b) == 0: csvr_thermostat(velocities, names, config, prng, comm=comm) # Remove total linear momentum @@ -768,44 +1203,114 @@ def main(): if np.mod(step, config.n_print) == 0: frame = step // config.n_print if not args.disable_field: + layouts = [ + pm.decompose(positions[types == t]) + for t in range(config.n_types) + ] + update_field( + phi, + phi_laplacian, + phi_transfer, + layouts, + force_on_grid, + hamiltonian, + pm, + positions, + types, + config, + v_ext, + phi_fourier, + v_ext_fourier, + config.m, + compute_potential=True, + ) ( - field_energy, kinetic_energy, + field_energy, + kinetic_energy, + field_q_energy, ) = compute_field_and_kinetic_energy( - phi, velocities, hamiltonian, positions, types, v_ext, - config, layouts, comm=comm, + phi, + phi_q, + psi, + velocities, + hamiltonian, + positions, + types, + v_ext, + config, + layouts, + comm=comm, ) - if charges_flag and config.coulombtype == "PIC_Spectral": - field_q_energy = compute_field_energy_q( - config, phi_q, phi_q_fourier, elec_potential, - elec_potential_fourier, - field_q_self_energy, comm=comm, - ) + if charges_flag: + if config.coulombtype == "PIC_Spectral_GPE": + field_q_energy = compute_field_energy_q_GPE( + config, phi_eps, field_q_energy, elec_dot, comm=comm + ) else: kinetic_energy = comm.allreduce( - 0.5 * config.mass * np.sum(velocities ** 2) + 0.5 * config.mass * np.sum(velocities**2) ) temperature = ( - (2 / 3) * kinetic_energy + (2 / 3) + * kinetic_energy / (config.gas_constant * config.n_particles) ) if args.disable_field: field_energy = 0.0 + if config.pressure or config.barostat: + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fourier, + phi_laplacian, + phi_transfer, + positions, + bond_pr_, + angle_pr_, + comm=comm, + ) + else: + pressure = 0.0 # 0.0 indicates not calculated. To be changed. + forces_out = ( - field_forces + bond_forces + angle_forces - + dihedral_forces + reconstructed_forces + field_forces + + bond_forces + + angle_forces + + dihedral_forces + + reconstructed_forces ) if charges_flag: forces_out += elec_forces store_data( - out_dataset, step, frame, indices, positions, velocities, - forces_out, config.box_size, temperature, kinetic_energy, - bond_energy, angle_energy, dihedral_energy, field_energy, - field_q_energy, config.respa_inner * config.time_step, - config, velocity_out=args.velocity_output, - force_out=args.force_output, charge_out=charges_flag, - dump_per_particle=args.dump_per_particle, comm=comm, + out_dataset, + step, + frame, + indices, + positions, + velocities, + forces_out, + config.box_size, + temperature, + pressure, + kinetic_energy, + bond_energy, + angle_energy, + dihedral_energy, + field_energy, + field_q_energy, + config.respa_inner * config.time_step, + config, + velocity_out=args.velocity_output, + force_out=args.force_output, + charge_out=charges_flag, + dump_per_particle=args.dump_per_particle, + comm=comm, ) if np.mod(step, config.n_print * config.n_flush) == 0: out_dataset.flush() @@ -829,53 +1334,156 @@ def main(): if config.n_print > 0 and np.mod(config.n_steps, config.n_print) != 0: if not args.disable_field: update_field( - phi, layouts, force_on_grid, hamiltonian, pm, positions, types, - config, v_ext, phi_fourier, v_ext_fourier, + phi, + phi_laplacian, + phi_transfer, + layouts, + force_on_grid, + hamiltonian, + pm, + positions, + types, + config, + v_ext, + phi_fourier, + v_ext_fourier, + config.m, compute_potential=True, ) - field_energy, kinetic_energy = compute_field_and_kinetic_energy( - phi, velocities, hamiltonian, positions, types, v_ext, config, - layouts, comm=comm, + ( + field_energy, + kinetic_energy, + field_q_energy, + ) = compute_field_and_kinetic_energy( + phi, + phi_q, + psi, + velocities, + hamiltonian, + positions, + types, + v_ext, + config, + layouts, + comm=comm, ) - if charges_flag and config.coulombtype == "PIC_Spectral": + if charges_flag: layout_q = pm.decompose(positions) - update_field_force_q( - charges, phi_q, phi_q_fourier, elec_field_fourier, - elec_field, elec_forces, layout_q, hamiltonian, pm, - positions, config, - ) + if config.coulombtype == "PIC_Spectral_GPE": + Vbar_elec, phi_eps, elec_dot = update_field_force_q_GPE( + conv_fun, + phi, + types, + charges, + config_charges, + phi_q, + phi_q_fourier, + phi_eps, + phi_eps_fourier, + phi_eta, + phi_eta_fourier, + phi_pol_prev, + phi_pol, + elec_field, + elec_forces, + elec_field_contrib, + psi, + Vbar_elec, + Vbar_elec_fourier, + force_mesh_elec, + force_mesh_elec_fourier, + hamiltonian, + layout_q, + layouts, + pm, + positions, + config, + comm=comm, + ) - field_q_energy = compute_field_energy_q( - config, phi_q, phi_q_fourier, elec_potential, - elec_potential_fourier, - field_q_self_energy, comm=comm, - ) + field_q_energy = compute_field_energy_q_GPE( + config, phi_eps, field_q_energy, elec_dot, comm=comm + ) + + if config.coulombtype == "PIC_Spectral": + update_field_force_q( + charges, + phi_q, + phi_q_fourier, + psi, + psi_fourier, + elec_field_fourier, + elec_field, + elec_forces, + layout_q, + hamiltonian, + pm, + positions, + config, + ) else: - kinetic_energy = comm.allreduce( - 0.5 * config.mass * np.sum(velocities**2) - ) + kinetic_energy = comm.allreduce(0.5 * config.mass * np.sum(velocities**2)) frame = (step + 1) // config.n_print temperature = ( - (2 / 3) * kinetic_energy - / (config.gas_constant * config.n_particles) + (2 / 3) * kinetic_energy / (config.gas_constant * config.n_particles) ) if args.disable_field: field_energy = 0.0 + + if config.pressure or config.barostat: + pressure = comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fourier, + phi_laplacian, + phi_transfer, + positions, + bond_pr_, + angle_pr_, + Vbar_elec, + comm=comm, + ) + else: + pressure = 0.0 # 0.0 indicates not calculated. To be changed. + forces_out = ( - field_forces + bond_forces + angle_forces + dihedral_forces + field_forces + + bond_forces + + angle_forces + + dihedral_forces + reconstructed_forces ) if charges_flag: forces_out += elec_forces store_data( - out_dataset, step, frame, indices, positions, velocities, - forces_out, config.box_size, temperature, kinetic_energy, - bond_energy, angle_energy, dihedral_energy, field_energy, - field_q_energy, config.respa_inner * config.time_step, config, - velocity_out=args.velocity_output, force_out=args.force_output, - charge_out=charges_flag, dump_per_particle=args.dump_per_particle, + out_dataset, + step, + frame, + indices, + positions, + velocities, + forces_out, + config.box_size, + temperature, + pressure, + kinetic_energy, + bond_energy, + angle_energy, + dihedral_energy, + field_energy, + field_q_energy, + config.respa_inner * config.time_step, + config, + velocity_out=args.velocity_output, + force_out=args.force_output, + charge_out=charges_flag, + dump_per_particle=args.dump_per_particle, comm=comm, ) diff --git a/hymd/pressure.py b/hymd/pressure.py new file mode 100644 index 00000000..d6542953 --- /dev/null +++ b/hymd/pressure.py @@ -0,0 +1,176 @@ +import logging +import numpy as np +from mpi4py import MPI +from .logger import Logger +import sympy +from .field import comp_laplacian + + +def comp_pressure( + phi, + phi_q, + psi, + hamiltonian, + velocities, + config, + phi_fourier, + phi_laplacian, + phi_transfer, + positions, + bond_pr, + angle_pr, + comm=MPI.COMM_WORLD, +): + """ + Computes total internal pressure of the system. + Kinetic pressure is trivially calculated from the kinetic energy. + Already computed bond and angle pressure terms are inserted into the + total internal pressure. + The field pressure equation is implemented. + + Parameters + ---------- + phi : list[pmesh.pm.RealField], (M,) + Pmesh :code:`RealField` objects containing discretized particle number + density values on the computational grid; one for each particle type + :code:`M`. Pre-allocated, but empty; any values in this field are discarded. + Changed in-place. Local for each MPI rank--the full computaional grid + is represented by the collective fields of all MPI ranks. + hamiltonian : Hamiltonian + Particle-field interaction energy handler object. Defines the + grid-independent filtering function, :math:`H`. + velocities : (N, D) numpy.ndarray + Array of velocities of N particles in D dimensions. + config : Config + Configuration dataclass containing simulation metadata and parameters. + phi_fourier : list[pmesh.pm.ComplexField], (M,) + Pmesh :code:`ComplexField` objects containing discretized particle + number density values in reciprocal space on the computational grid; + one for each particle type. Pre-allocated, but empty; any values in + this field are discarded Changed in-place. Local for each MPI rank--the + full computaional grid is represented by the collective fields of all + MPI ranks. + phi_laplacian : list[pmesh.pm.RealField], (M, 3) + Like phi, but containing the laplacian of particle number densities. + phi_transfer : list[pmesh.pm.ComplexField], (3,) + Like phi_fourier, used as an intermediary to perform FFT operations + to obtain the gradient or laplacian of particle number densities. + positions : (N,D) numpy.ndarray + Array of positions for :code:`N` particles in :code:`D` dimensions. + Local for each MPI rank. + bond_pr : (3,) numpy.ndarray + Total bond pressure due all two-particle bonds. + angle_pr : (3,) numpy.ndarray + Total angle pressure due all three-particle bonds + comm : MPI.Intracomm, optional + MPI communicator to use for rank commuication. Defaults to + MPI.COMM_WORLD. + + Returns + ------- + pressure : (18,) numpy.ndarray + Pressure contributions from various energy terms. + 0: due to kinetic energy + 1-5: due to field energy + 6-8: due to two-particle bonded terms + 9-11: due to three-particle bonded terms (called angle terms) + 12-14: due to four-particle bonded terms (called dihedral terms) + (defaults to 0 currently. Yet to be implemented) + 15-17: total pressure in x,y,z directions. + """ + rank = comm.Get_rank() + size = comm.Get_size() + + V = np.prod(config.box_size) + n_mesh__cells = np.prod(np.full(3, config.mesh_size)) + volume_per_cell = V / n_mesh__cells + + w_0 = hamiltonian.w_0(phi) * volume_per_cell + w_elec = 0.0 + if (config.coulombtype == "PIC_Spectral_GPE") or ( + config.coulombtype == "PIC_Spectral" + ): + w_elec = hamiltonian.w_elec([phi_q, psi]) * volume_per_cell + w = w_0 + w_elec + + # Kinetic term + kinetic_energy = 0.5 * config.mass * np.sum(velocities**2) + p_kin = 2 / (3 * V) * kinetic_energy + + # Term 1 + p0 = -1 / V * np.sum(w) + + # Term 2 + V_bar = np.array([hamiltonian.V_bar[k]([phi, psi]) for k in range(config.n_types)]) + + p1 = np.sum((volume_per_cell / V) * V_bar * phi) + + # Term 3 + comp_laplacian( + phi_fourier, + phi_transfer, + phi_laplacian, + hamiltonian, + config, + ) + + p2 = np.sum( + volume_per_cell + / V + * config.sigma**2 + * np.repeat(V_bar[:, np.newaxis, :, :, :], 3, axis=1) + * phi_laplacian, + axis=(0, 2, 3, 4), + ) + + # Bonded force term: linking 2 particles + p_bond = bond_pr / V + + # Angle force term: linking 3 particles + p_angle = angle_pr / V + + # TODO: Dihedral angle force term: linking 4 atoms + p_dihedral = np.zeros(3) + + # Add formal parameter dihedral_forces as: comp_pressure(..., dihedral_forces) + # Define dictionary: + # forces = { + # 'x': dihedral_forces[:,0], + # 'y': dihedral_forces[:,1], + # 'z': dihedral_forces[:,2] + # } + # Compute the pressure due to dihedrals as: + # p_dihedral = { + # 'x': np.sum( np.multiply(forces['x'],positions[:,0]) )*(1/V), + # 'y': np.sum( np.multiply(forces['y'],positions[:,1]) )*(1/V), + # 'z': np.sum( np.multiply(forces['z'],positions[:,2]) )*(1/V) + # } + + # Total pressure in x, y, z + p_tot = p_kin + p0 + p1 + p2 + p_bond + p_angle + p_dihedral + + return_value = np.array( + [ + p_kin, + p0, + p1, + p2[0], + p2[1], + p2[2], + p_bond[0], + p_bond[1], + p_bond[2], + p_angle[0], + p_angle[1], + p_angle[2], + p_dihedral[0], + p_dihedral[1], + p_dihedral[2], + p_tot[0], + p_tot[1], + p_tot[2], + ] + ) + return_value = comm.allreduce(return_value, MPI.SUM) + + return return_value diff --git a/hymd/thermostat.py b/hymd/thermostat.py index 1e2e6e6a..baf6e529 100644 --- a/hymd/thermostat.py +++ b/hymd/thermostat.py @@ -23,16 +23,15 @@ def generate_initial_velocities(velocities, config, prng, comm=MPI.COMM_WORLD): ) velocities = cancel_com_momentum(velocities, config, comm=comm) kinetic_energy = comm.allreduce( - 0.5 * config.mass * np.sum(velocities ** 2), MPI.SUM + 0.5 * config.mass * np.sum(velocities**2), MPI.SUM ) start_kinetic_energy_target = ( - 1.5 * config.gas_constant * config.n_particles - * config.start_temperature + 1.5 * config.gas_constant * config.n_particles * config.start_temperature ) factor = np.sqrt(1.5 * config.n_particles * kT_start / kinetic_energy) velocities[...] = velocities[...] * factor kinetic_energy = comm.allreduce( - 0.5 * config.mass * np.sum(velocities ** 2), MPI.SUM + 0.5 * config.mass * np.sum(velocities**2), MPI.SUM ) Logger.rank0.log( logging.INFO, @@ -110,11 +109,15 @@ def _random_chi_squared(prng: np.random.Generator, M: int) -> float: def csvr_thermostat( - velocity: np.ndarray, names: np.ndarray, config: Config, + velocity: np.ndarray, + names: np.ndarray, + config: Config, prng: np.random.Generator, comm: MPI.Intracomm = MPI.COMM_WORLD, random_gaussian: Callable[[np.random.Generator], float] = _random_gaussian, - random_chi_squared: Callable[[np.random.Generator, int, float], float] = _random_chi_squared, + random_chi_squared: Callable[ + [np.random.Generator, int, float], float + ] = _random_chi_squared, remove_center_of_mass_momentum: bool = True, ) -> np.ndarray: """Canonical sampling through velocity rescaling thermostat @@ -144,6 +147,8 @@ def csvr_thermostat( Array of particle names. config : Config Configuration dataclass containing simulation metadata and parameters. + prng : np.random.Generator + Numpy object that provides a stream of random bits comm : MPI.Intracomm, optional MPI communicator to use for rank commuication. Defaults to MPI.COMM_WORLD. @@ -182,13 +187,11 @@ def csvr_thermostat( if remove_center_of_mass_momentum and group_n_particles > 1: com_velocity = comm.allreduce(np.sum(velocity[ind], axis=0)) velocity_clean = velocity[ind] - com_velocity / group_n_particles - K = comm.allreduce(0.5 * config.mass - * np.sum(velocity_clean[...]**2)) + K = comm.allreduce(0.5 * config.mass * np.sum(velocity_clean[...] ** 2)) else: - K = comm.allreduce(0.5 * config.mass * np.sum(velocity[...]**2)) + K = comm.allreduce(0.5 * config.mass * np.sum(velocity[...] ** 2)) K_target = ( - 1.5 * config.gas_constant * group_n_particles - * config.target_temperature + 1.5 * config.gas_constant * group_n_particles * config.target_temperature ) N_f = 3 * group_n_particles c = np.exp(-(config.time_step * config.respa_inner) / config.tau) @@ -199,7 +202,8 @@ def csvr_thermostat( SNf = random_chi_squared(prng, N_f - 1) alpha2 = ( - c + (1 - c) * (SNf + R**2) * K_target / (N_f * K) + c + + (1 - c) * (SNf + R**2) * K_target / (N_f * K) + 2 * R * np.sqrt(c * (1 - c) * K_target / (N_f * K)) ) dK = K * (alpha2 - 1) @@ -209,9 +213,7 @@ def csvr_thermostat( # Assign velocities and reapply the previously removed center # of mass momentum removed velocity_clean *= alpha - velocity[ind] = ( - velocity_clean + com_velocity / group_n_particles - ) + velocity[ind] = velocity_clean + com_velocity / group_n_particles else: velocity *= alpha config.thermostat_work += dK diff --git a/test/test_file_io.py b/test/test_file_io.py index 96e9e596..86eff807 100644 --- a/test/test_file_io.py +++ b/test/test_file_io.py @@ -10,6 +10,7 @@ ) from hymd.input_parser import Config, parse_config_toml, _setup_type_to_name_map from hymd.force import prepare_bonds +from hymd.barostat import Target_pressure def test_OutDataset(config_toml, tmp_path): _, config_toml_str = config_toml @@ -108,7 +109,8 @@ def test_store_static(molecules_with_solvent, tmp_path): box_size = np.array([10, 10, 10], dtype=np.float64) config = Config(n_steps=0, n_print=1, time_step=0.03, box_size=box_size, mesh_size=[5, 5, 5], sigma=0.5, kappa=0.05, - n_particles=len(indices)) + n_particles=len(indices), + target_pressure=Target_pressure(P_L=None,P_N=None)) Bond = collections.namedtuple( "Bond", ["atom_1", "atom_2", "equilibrium", "strength"] ) @@ -185,7 +187,8 @@ def test_store_data(molecules_with_solvent, tmp_path): box_size = np.array([10, 10, 10], dtype=np.float64) config = Config(n_steps=100, n_print=1, time_step=0.03, box_size=box_size, mesh_size=[5, 5, 5], sigma=0.5, kappa=0.05, - n_particles=len(indices), mass=72.) + n_particles=len(indices), mass=72., + target_pressure=Target_pressure(P_L=None,P_N=None)) Bond = collections.namedtuple( "Bond", ["atom_1", "atom_2", "equilibrium", "strength"] ) @@ -245,7 +248,7 @@ def test_store_data(molecules_with_solvent, tmp_path): store_data( out, 0, 0, indices_, positions_, velocities_, forces, - box_size, 300., 1., 2., 3., 4., 5., 6., 0.02, config + box_size, 300., 1., 1., 2., 3., 4., 5., 6., 0.02, config ) outdataset_step = [ @@ -292,6 +295,7 @@ def test_store_data(molecules_with_solvent, tmp_path): assert out.potential_energy[0] == pytest.approx(20.) assert out.kinetc_energy[0] == pytest.approx(1.) assert out.temperature[0] == pytest.approx(300.) + assert out.pressure[0] == pytest.approx(1.) assert out.bond_energy[0] == pytest.approx(2.) assert out.angle_energy[0] == pytest.approx(3.) assert out.dihedral_energy[0] == pytest.approx(4.) diff --git a/test/test_hamiltonian.py b/test/test_hamiltonian.py index 8123a2de..4323771d 100644 --- a/test/test_hamiltonian.py +++ b/test/test_hamiltonian.py @@ -33,6 +33,8 @@ def test_DefaultNoChi_window_function( config = _find_unique_names(config, names) config.sigma = sigma + config.type_charges = [0.] * config.n_types + W = DefaultNoChi(config) k_ = np.array( @@ -82,6 +84,7 @@ def test_DefaultNoChi_energy_functional(filter, v_ext, caplog): names = np.array([np.string_(x) for x in ["A", "A", "B", "B", "B"]]) n_types = len(np.unique(types)) config_conf = _find_unique_names(config_conf, names) + config_conf.type_charges = [0.] * config_conf.n_types W = DefaultNoChi(config_conf) @@ -181,6 +184,7 @@ def test_Hamiltonian_no_chi_gaussian_core(v_ext, caplog): ) names = np.array([np.string_(s) for s in ['A', 'A']]) config = _find_unique_names(config, names) + config.type_charges = [0.] * config.n_types r = np.array( [[1.50, 0.75, 2.25], [2.25, 0.00, 3.00], @@ -282,6 +286,8 @@ def test_Hamiltonian_with_chi_gaussian_core(v_ext, caplog): config.box_size = np.array([15.0, 15.0, 15.0]) config.mesh_size = np.array([160, 160, 160]) config.n_particles = 5 + config.type_charges = [0.] * config.n_types + r = np.array( [[1.50, 0.75, 2.25], [2.25, 0.00, 3.00], @@ -366,6 +372,8 @@ def test_get_hamiltonian(config_toml): config.unique_names = ["N","P","G","C","W"] config.type_to_name_map = {0: 'N', 1: 'P', 2: 'G', 3: 'C', 4: 'W'} config.n_types = 5 + config.type_charges = [0., 0., 0., 0., 0.] + config.hamiltonian = "DefaultNoChi" hamiltonian = get_hamiltonian(config) diff --git a/test/test_input_parser.py b/test/test_input_parser.py index fc6e34af..a864e62a 100644 --- a/test/test_input_parser.py +++ b/test/test_input_parser.py @@ -363,13 +363,23 @@ def test_input_parser_check_box_size(config_toml, caplog): config_toml_str, "box_size = [", "box_size = [2.25, -3.91, 4.11]" ) config = parse_config_toml(changed_box_toml_str) + input_box = np.array([2.25, -3.91, 4.11]) with pytest.raises(ValueError) as recorded_error: - _ = check_box_size(config) + _ = check_box_size(config, input_box) log = caplog.text assert all([(s in log) for s in ("Invalid", "box")]) message = str(recorded_error.value) assert all([(s in message) for s in ("Invalid", "box")]) + + input_box = np.ones(3) + with pytest.raises(ValueError) as recorded_error: + _ = check_box_size(config, input_box) + log = caplog.text + assert all([(s in log) for s in ("Box", "not", "match")]) + message = str(recorded_error.value) + assert all([(s in message) for s in ("Box", "not", "match")]) + caplog.clear() @@ -749,6 +759,10 @@ def test_input_parser_check_config(config_toml, dppc_single): _, config_toml_str = config_toml config = parse_config_toml(config_toml_str) config.n_particles = 13 + charges = np.zeros(config.n_particles, dtype=np.float32) + input_box = np.array(config.box_size) + config.n_b = 1 + config.barostat_type = "berendsen" indices, _, names, _, _, _ = dppc_single @@ -758,7 +772,7 @@ def test_input_parser_check_config(config_toml, dppc_single): types = np.array([names_to_types[n.decode('UTF-8')] for n in names], dtype=int) - config = check_config(config, indices, names, types) + config = check_config(config, indices, names, types, charges, input_box) assert isinstance(config, Config) @@ -840,8 +854,13 @@ def test_input_parser__setup_type_to_name_map(config_toml, dppc_single): @pytest.mark.mpi() -def test_input_parser_check_charges(caplog): +def test_input_parser_check_charges(config_toml, caplog): caplog.set_level(logging.WARNING) + _, config_toml_str = config_toml + config = parse_config_toml(config_toml_str) + config.coulombtype = "PIC_Spectral" + config.dielectric_const = 80. + charges = np.array( [1.0, 0.0, 0.5, 0.2, -0.3, 0.0, 0.3, 0.0, -0.5, 0.0, -0.5, 0.99, -0.2, 0.3, 0.5, 0.0, 0.0, -0.3, 0.0, 0.0, -0.99, -1.0], @@ -860,15 +879,16 @@ def test_input_parser_check_charges(caplog): rank_charges = charges[ind_rank[rank]:ind_rank[rank+1]] - check_charges(rank_charges, comm=comm) # warnings are not expected + newconf = check_charges(config, rank_charges, comm=comm) # warnings are not expected assert len(caplog.text) == 0 + assert newconf.self_energy != 0. # TODO: compare to the right value # change charges to generate a warning charges[1] = 1000. rank_charges = charges[ind_rank[rank]:ind_rank[rank+1]] with pytest.warns(Warning) as recorded_warning: - config = check_charges(rank_charges) + newconf = check_charges(config, rank_charges) # only rank 0 gives the warning if rank == 0: message = recorded_warning[0].message.args[0] diff --git a/utils/add_charges.py b/utils/add_charges.py new file mode 100644 index 00000000..22172e66 --- /dev/null +++ b/utils/add_charges.py @@ -0,0 +1,70 @@ +import os +import argparse +import h5py +import numpy as np + +def extant_file(x): + """ + 'Type' for argparse - checks that file exists but does not open. + From https://stackoverflow.com/a/11541495 + """ + if not os.path.exists(x): + # Argparse uses the ArgumentTypeError to give a rejection message like: + # error: argument input: x does not exist + raise argparse.ArgumentTypeError("{0} does not exist".format(x)) + return x + + +def add_charges(hdf5_file, charges, out_path=None, overwrite=False): + if out_path is None: + out_path = os.path.join(os.path.abspath(os.path.dirname(hdf5_file)), + os.path.splitext(hdf5_file)[0]+"_new.hdf5") + if os.path.exists(out_path) and not overwrite: + error_str = (f'The specified output file {out_path} already exists. ' + f'use overwrite=True ("-f" flag) to overwrite.') + raise FileExistsError(error_str) + + # read charges + charge_dict = {} + with open(charges, "r") as f: + for line in f: + beadtype, beadchrg = line.split() + charge_dict[beadtype] = float(beadchrg) + + # create output file with the same values + f_in = h5py.File(hdf5_file, 'r') + f_out = h5py.File(out_path, 'w') + + for k in f_in.keys(): + f_in.copy(k, f_out) + + # create charges array + charges_dataset = f_out.create_dataset( + "charge", + f_out["names"][:].shape, + dtype="float32", + ) + charges_list = [] + for bead in f_out["names"][:].tolist(): + charges_list.append(charge_dict[bead.decode()]) + + charges_dataset[:] = np.array(charges_list) + + f_in.close() + f_out.close() + + +if __name__ == '__main__': + description = 'Given a .hdf5 input and a charges.txt file assigning bead type to charges, create new .hdf5.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('hdf5_file', type=str, help='input .hdf5 file name') + parser.add_argument('charges', type=extant_file, default=None, + help='file containing the charge for each atom type') + parser.add_argument('--out', type=str, default=None, dest='out_path', + metavar='file name', help='output hymd HDF5 file name') + parser.add_argument('-f', action='store_true', default=False, dest='force', + help='overwrite existing output file') + args = parser.parse_args() + + add_charges(args.hdf5_file, args.charges, + overwrite=args.force, out_path=args.out_path) diff --git a/utils/center_group.py b/utils/center_group.py deleted file mode 100644 index 10dc2048..00000000 --- a/utils/center_group.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -import argparse -import h5py -import numpy as np -import re - -# based on https://stackoverflow.com/a/6512463/3254658 -def parse_bead_list(string): - m = re.match(r'(\d+)(?:-(\d+))?$', string) - if not m: - raise argparse.ArgumentTypeError("'" + string + "' is not a range of number. Expected forms like '0-5' or '2'.") - start = int(m.group(1), base=10) - end = int(m.group(2), base=10) or start - if end < start: - raise argparse.ArgumentTypeError(f"Start value ({start}) should be larger than final value ({end})") - return list(range(start, end + 1)) - -def get_centers(positions, box): - centers = np.empty((0,positions.shape[2])) - # based on the position of the first atom get minimal distances - for frame in range(positions.shape[0]): - deltas = positions[frame,1:,:]-positions[frame,0,:] - subtract = np.where(deltas > 0.5 * box, True, False) - add = np.where(-deltas > 0.5 * box, True, False) - - newpos = np.where(subtract, positions[frame,1:,:]-box, positions[frame,1:,:]) - newpos = np.where(add, positions[frame,1:,:]+box, newpos[:,:]) - newpos = np.insert(newpos, 0, positions[frame,0,:], axis=0) - centers = np.append(centers, [newpos.mean(axis=0)], axis=0) # get the centroid - - return centers - -def center_trajectory(h5md_file, bead_list, overwrite=False, out_path=None): - if out_path is None: - out_path = os.path.join(os.path.abspath(os.path.dirname(h5md_file)), - os.path.splitext(os.path.split(h5md_file)[-1])[0]+'_new' - +os.path.splitext(os.path.split(h5md_file)[-1])[1]) - if os.path.exists(out_path) and not overwrite: - error_str = (f'The specified output file {out_path} already exists. ' - f'use overwrite=True ("-f" flag) to overwrite.') - raise FileExistsError(error_str) - - f_in = h5py.File(h5md_file, 'r') - f_out = h5py.File(out_path, 'w') - - for k in f_in.keys(): - f_in.copy(k, f_out) - - box_size = f_in['particles/all/box/edges'][:] - - beads_pos = f_in['particles/all/position/value'][:][:,bead_list,:] - centers = get_centers(beads_pos, box_size) - - translate = (0.5 * box_size) - centers - - translations = np.repeat(translate[:,np.newaxis,:], - f_in['particles/all/position/value'].shape[1], - axis=1) - - tpos = f_in['particles/all/position/value'] + translations - f_out['particles/all/position/value'][:] = np.mod(tpos, box_size) - - f_in.close() - f_out.close() - - -if __name__ == '__main__': - description = 'Center geometric center of beads in the box for each frame in a .H5 trajectory' - parser = argparse.ArgumentParser(description=description) - parser.add_argument('h5md_file', type=str, help='input .H5MD file name') - parser.add_argument('-b', '--beads', type=parse_bead_list, nargs='+', required=True, - help='bead list to center (e.g.: 1-100 102-150)') - parser.add_argument('-o', '--out', type=str, default=None, dest='out_path', - metavar='file name', help='output hymd HDF5 file name') - parser.add_argument('-f', action='store_true', default=False, dest='force', - help='overwrite existing output file') - args = parser.parse_args() - - bead_list = [] - for interval in args.beads: - bead_list += interval - - bead_list = np.array(sorted(bead_list))-1 - - center_trajectory(args.h5md_file, bead_list, overwrite=args.force, - out_path=args.out_path) diff --git a/utils/change_chi.py b/utils/change_chi.py deleted file mode 100644 index 5f007199..00000000 --- a/utils/change_chi.py +++ /dev/null @@ -1,140 +0,0 @@ -import warnings -import argparse -import os -import itertools -import re - - -def parse_and_change_config_chi(config_file_path, parser, out=None): - """Parse and change the chi-matrix in a config file - - Parameters - ---------- - config_file_path : str - File path of the config file to change. - parser : Argparse.Namespace - Parsed command line arguments given to change_chi. - out : str, optional - File path of the changed output config file. - - Returns - ------- - out_file_path : str - File path of the changed output config file. None if no out parameter - was specified. - out_file_contents : list of int - Lines of the changed output config file. - """ - with open(config_file_path, "r") as in_file: - contents = in_file.readlines() - chi_re = re.compile(r"^\s*chi\s*=\s*\[\s*") - - chi_start_line = None - for i, line in enumerate(contents): - chi_match = re.match(chi_re, line) - if chi_match is not None: - chi_start_line = i - break - - chi_end_line = None - left_brackets = 0 - right_brackets = 0 - for i, line in enumerate(contents[chi_start_line:]): - for j, char in enumerate(line): - if char == "[": - left_brackets += 1 - elif char == "]": - right_brackets += 1 - if left_brackets > 0: - if left_brackets == right_brackets: - chi_end_line = chi_start_line + i - break - if chi_end_line is not None: - break - - chi_arg_re = re.compile(r"[A-Z,a-z]+,[A-Z,a-z]+") - chi_args = [] - for arg, value in vars(parser).items(): - if value is not None: - if re.fullmatch(chi_arg_re, arg): - a, b = arg.split(",") - re_str = ( - f'\[\s*\[\s*"{a}"\s*,\s*"{b}"\s*\]\s*,' # noqa: W605 - f'\s*\[\s*[\+-]?\d+\.\d*\s*\]\s*\]' # noqa: W605 - ) - re_str_r = ( - f'\[\s*\[\s*"{b}"\s*,\s*"{a}"\s*\]\s*,' # noqa: W605 - f'\s*\[\s*[\+-]?\d+\.\d*\s*\]\s*\]' # noqa: W605 - ) - chi_matrix_re = re.compile(re_str) - chi_matrix_re_r = re.compile(re_str_r) - chi_args.append([(a, b), chi_matrix_re, value]) - chi_args.append([(a, b), chi_matrix_re_r, value]) - - for i, line in enumerate(contents[chi_start_line:chi_end_line+1]): - replaced_line = line - for (a, b), pattern, chi_value in chi_args: - replaced_line = re.sub( - pattern, - f'[["{a}", "{b}"], [{chi_value}]]', - replaced_line, - ) - contents[chi_start_line+i] = replaced_line - - if out is not None: - with open(out, "w") as out_file: - for line in contents: - out_file.write(line) - return contents - - -if __name__ == '__main__': - description = "" - parser = argparse.ArgumentParser( - description=( - "Change elements in the chi-matrix in config.toml" - ) - ) - parser.add_argument( - "--config-file", type=str, metavar="config file", - help="config.toml file to edit", - ) - parser.add_argument( - "--out", type=str, default=None, metavar="file name", - help="output file path (config file with changed chi matrix)", - ) - parser.add_argument( - "-w", action="store_true", help="suppress all user warnings", - dest="suppress_warnings", - ) - parser.add_argument( - "-v", "--verbose", action="store_true", dest="verbose", - help="increase logging verbosity", - ) - beads = ["N", "P", "G", "C", "D", "L", "W"] - combinations = itertools.combinations(beads, 2) - for a, b in combinations: - bead_1, bead_2 = sorted((a, b,)) - parser.add_argument( - f"-{bead_1},{bead_2}", f"-{bead_2},{bead_1}", type=float, - dest=f"{bead_1},{bead_2}", default=None, metavar="", - help=f"{bead_1}-{bead_2} interaction energy", - ) - args = parser.parse_args() - - config_file_path = os.path.abspath(args.config_file) - out_file_path = None - if args.out is None: - if not args.suppress_warnings: - warnings.warn( - "No output file specified, dumping result to stdout." - ) - else: - out_file_path = os.path.abspath(args.out) - - replaced_file_content = parse_and_change_config_chi( - config_file_path, args, out=out_file_path - ) - if (args.out is not None and args.verbose) or args.out is None: - for line in replaced_file_content: - print(line.strip("\n")) diff --git a/utils/check_displacements.py b/utils/check_displacements.py new file mode 100644 index 00000000..5ff1ba71 --- /dev/null +++ b/utils/check_displacements.py @@ -0,0 +1,46 @@ +import numpy as np +import sys +import h5py + +""" +A possible error that can arise due to bad allocation of cores wrt system size, +is sending random particles to the origin in specific frames. +This does not affect the MD but can affect post-MD analysis. To check if the trajectory suffers +from this error, run this util as: + python3 utils/check_displacements.py sim.h5 all + +sys.argv[1]: sim.h5: trajectory in h5md format +sys.argv[2:]: i) all : checks for displacements across all frames in the trajectory + ii) 32 : checks in frames 32 + iii) 32 34 36 .... m: checks in frames 32, 34, 36, .... , mth frame +""" + + +flag = False +values = h5py.File(sys.argv[1], 'r') +steps = values['particles/all/position/step'] + +if(sys.argv[2] == 'all'): + #frames = list(range(0,67)) + frames = list(range(0,len(steps))) +else: + frames = list(sys.argv[2:]) + frames = list(map(int, frames)) + +for frame in frames: + positions = values['particles/all/position/value'][frame, :, :] + #velocities = values['particles/all/velocity/value'][frame, :, :] + species_type = values['particles/all/species'] + displaced_index = [] + for i in range(len(positions)): + if(positions[i][0] == 0.000 and not species_type[i] == 4): + displaced_index.append(i) + if(len(displaced_index)>0): + flag = True + print('--- Frame : ', frame, '---') + print('Number of particles displaced to origin: ',len(displaced_index)) + print('Index of particles displaced to origin: ', displaced_index) + +if(flag == False): + print('Number of particles displaced to origin is 0 in all frames') + diff --git a/utils/compute_density_profile.py b/utils/compute_density_profile.py new file mode 100644 index 00000000..d067a556 --- /dev/null +++ b/utils/compute_density_profile.py @@ -0,0 +1,125 @@ +import numpy as np +import matplotlib.pyplot as plt +import pathlib +import h5py +from collections.abc import Iterable +import argparse + + +def h5md_density_profile(file_path, time_steps=(0, -1), species="all", + dimension="z", bins=50): + try: + in_file = h5py.File(file_path, 'r') + time = in_file["particles"]["all"]["position"]["time"][...] + positions = in_file["particles"]["all"]["position"]["value"][...] + species_index = in_file["parameters"]["vmd_structure"]["indexOfSpecies"][...] + species_names = in_file["parameters"]["vmd_structure"]["name"][...] + types = in_file["particles"]["all"]["species"][...] + in_file.close() + except IsADirectoryError: + raise + except FileNotFoundError: + raise + except OSError: + raise + + if species == "all": + species = tuple([s.decode("UTF-8") for s in species_names]) + else: + def check_species_present(s, species_list): + species_list_ = [s_.decode("UTF-8") for s_ in species_list] + if s not in species_list_: + raise ValueError( + f"No {species} type particles found in the system. " + f"Only species present are: {species_list_}." + ) + + if isinstance(species, Iterable): + for s in species: + check_species_present(s, species_names) + + n_steps = time.size + time_inds = np.arange(n_steps) + if isinstance(time_steps, int): + time_steps = (time_steps, time_steps) + try: + time_steps_ = [] + for t in time_steps: + time_steps_.append(time_inds[t]) + time_steps = tuple(time_steps_) + except (TypeError, IndexError) as e: + raise TypeError( + f"Could not interpret time_steps {time_steps} as a list of " + f"integers." + ) from e + + + positions_slice = positions[time_steps[0]:time_steps[1], ...] + n_steps_average = positions_slice.shape[0] + dim = 0 if dimension == "x" else (1 if dimension == "y" else 2) + max_pos = np.max(positions_slice[..., dim]) + min_pos = np.min(positions_slice[..., dim]) + + for s in species: + t = species_index[np.where(species_names == np.string_(s))][0] + ind_t = np.where(types == t) + hist, bin_edges = np.histogram( + positions_slice[:, ind_t, dim], bins=bins, + range=(min_pos, max_pos), + ) + hist = hist.astype(np.float64) / float(n_steps_average) + edge_width = np.mean(np.diff(bin_edges)) + bin_edges_ = bin_edges[:-1] + 0.5 * edge_width + ax.plot(bin_edges_, hist, label=str(s)+' '+str(time_steps)) + + ax.set_xlabel(f"{dimension} position / nm", fontsize=13) + ax.set_ylabel("average number density / 1/nm³", fontsize=13) + plt.legend() + +description = """Plot density profiles from H5MD files + +Accumulates the densities from step `time_steps[0]` to `time_steps[1]` and +averages the resulting density profiles. The `species` argument may specify +one or more species types to calculate the profile for. By default, all +particle types found in the system are used (corresponds to the default +argument 'all'). The profile is computed across the `dimension` axis. + +Parameters +---------- +file_path : str or pathlib.Path + File path to H5MD file. +time_steps : int or tuple of int, optional + Start and end points for averaging the density. If one number is provided, + a single frame is considered for the profile and no averaging is done. +species : str or tuple of str, optional + String or list of strings of particle species to plot the density for. + By default, all particle types found are treated. +dimension : {'x', 'y', 'z'} + Dimension to compute the density profile across. +bins : int, optional + Number of bins used in the density profile histogram. +""" +parser = argparse.ArgumentParser(description=description) +parser.add_argument('traj_file', type=str, help='trajectory file with path. Eg: home/sim.h5') +parser.add_argument('--start_frames', type=int, nargs="+", + help='first frames to be considered. Eg: -50 means range starts from 50th fram from last') +parser.add_argument('--end_frames', type=int, nargs="+", + help='last frames to be considered. Eg: -1 means range ends at the last frame') +parser.add_argument('--dimension', type=str, default='z', help='axis along which density profile is computed'\ + ' Eg: x [/y/z]') +parser.add_argument('--species', type=str, default='all', help='species whose density profile is computed'\ + ' Eg: all [/W/N]') +parser.add_argument('--bins', type=int, default='60', help='number of bins along axis of plot') +args = parser.parse_args() +home_directory = pathlib.Path.home() +file_path = args.traj_file + +fig, ax = plt.subplots(1, 1) +fig.set_figwidth(8) +fig.set_figheight(5) +for i in range(len(args.start_frames)): + _ = h5md_density_profile( + file_path, species=args.species, time_steps=(args.start_frames[i], args.end_frames[i]), dimension=args.dimension, bins=args.bins, + ) + +plt.show() diff --git a/utils/genbox_random.py b/utils/genbox_random.py new file mode 100644 index 00000000..320dffd8 --- /dev/null +++ b/utils/genbox_random.py @@ -0,0 +1,133 @@ +import os +import argparse +import h5py +import math +import numpy as np +from openbabel import openbabel +openbabel.obErrorLog.SetOutputLevel(0) # uncomment to suppress warnings + + +def extant_file(x): + """ + 'Type' for argparse - checks that file exists but does not open. + From https://stackoverflow.com/a/11541495 + """ + if not os.path.exists(x): + # Argparse uses the ArgumentTypeError to give a rejection message like: + # error: argument input: x does not exist + raise argparse.ArgumentTypeError("{0} does not exist".format(x)) + return x + + +def a2nm(x): + return x/10. + + +def genbox(input_file, box_size, prng, charges=None, out_path=None, overwrite=False): + if out_path is None: + out_path = os.path.join(os.path.abspath(os.path.dirname(input_file)), + os.path.splitext(input_file)[0]+".hdf5") + if os.path.exists(out_path) and not overwrite: + error_str = (f'The specified output file {out_path} already exists. ' + f'use overwrite=True ("-f" flag) to overwrite.') + raise FileExistsError(error_str) + + # read particles + particles = {} + lbl_to_type = {} + n_particles = 0 + with open(input_file, "r") as f: + for i, line in enumerate(f): + name, num = line.split() + lbl_to_type[name] = i + particles[name] = int(num) + n_particles += int(num) + + # read charges + if charges: + charge_dict = {} + with open(charges, "r") as f: + for line in f: + beadtype, beadchrg = line.split() + charge_dict[beadtype] = float(beadchrg) + + # write the topology + with h5py.File(out_path, "w") as out_file: + position_dataset = out_file.create_dataset( + "coordinates", + (1, n_particles, 3,), + dtype="float32", + ) + types_dataset = out_file.create_dataset( + "types", + (n_particles,), + dtype="i", + ) + names_dataset = out_file.create_dataset( + "names", + (n_particles,), + dtype="S10", + ) + indices_dataset = out_file.create_dataset( + "indices", + (n_particles,), + dtype="i", + ) + molecules_dataset = out_file.create_dataset( + "molecules", + (n_particles,), + dtype="i", + ) + bonds_dataset = out_file.create_dataset( + "bonds", + (n_particles, 4,), + dtype="i", + ) + if charges: + charges_dataset = out_file.create_dataset( + "charge", + (n_particles,), + dtype="float32", + ) + + bonds_dataset[...] = -1 + + # generate the positions randomly inside the box + initial = 0 + for name, num_beads in particles.items(): + for i in range(initial, initial+num_beads): + indices_dataset[i] = i + position_dataset[0, i, :] = np.array( + [prng.uniform(0.,box_size[0]), + prng.uniform(0.,box_size[1]), + prng.uniform(0.,box_size[2])], + dtype=np.float32 + ) + charges_dataset[i] = charge_dict[name] + molecules_dataset[i] = i + names_dataset[i] = np.string_(name) + types_dataset[i] = lbl_to_type[name] + initial += num_beads + + +if __name__ == '__main__': + description = 'Generate a hdf5 input placing beads in random positions' + parser = argparse.ArgumentParser(description=description) + parser.add_argument('input_file', type=extant_file, help='input .txt with "NAME #particles" for each type') + parser.add_argument('box_size', type=float, nargs="+", help='input .txt with "NAME #particles" for each type') + parser.add_argument('--charges', type=extant_file, default=None, + help='file containing the charge for each atom type') + parser.add_argument('--out', type=str, default=None, dest='out_path', + metavar='file name', help='output hymd HDF5 file name') + parser.add_argument('--seed', type=int, help='random seed') + parser.add_argument('-f', action='store_true', default=False, dest='force', + help='overwrite existing output file') + args = parser.parse_args() + + if args.seed is not None: + ss = np.random.SeedSequence(args.seed) + else: + ss = np.random.SeedSequence() + prng = np.random.default_rng(ss.generate_state(1)) + + genbox(args.input_file, args.box_size, prng, charges=args.charges, out_path=args.out_path, overwrite=args.force) \ No newline at end of file diff --git a/utils/get_next_point.py b/utils/get_next_point.py deleted file mode 100644 index fc064ef1..00000000 --- a/utils/get_next_point.py +++ /dev/null @@ -1,161 +0,0 @@ -import os -import numpy as np -import argparse -from read_parameter_file import read_bounds_file -from bayes_opt import BayesianOptimization -from bayes_opt import UtilityFunction -from sklearn.gaussian_process.kernels import Matern, WhiteKernel -from sklearn.gaussian_process import GaussianProcessRegressor - - -def get_next_point_BO(bounds_file_path, opt_file_path, kappa): - if os.path.exists(opt_file_path): - opt_data = np.loadtxt(opt_file_path, dtype=str) - else: - with open(opt_file_path, "w") as _: - ... - opt_data = np.empty(shape=(1, 1,), dtype=np.float64) - - parameter_names_correct_order, lower_bounds, upper_bounds = ( - read_bounds_file(bounds_file_path) - ) - bounds = {} - for param, lower, upper in zip( - parameter_names_correct_order, lower_bounds, upper_bounds - ): - bounds[param] = (lower, upper) - - optimizer = BayesianOptimization(f=None, pbounds=bounds, verbose=2) - white_kernel = WhiteKernel(noise_level_bounds=(1e-5, 1e+3)) - matern_kernel = Matern(nu=2.5, length_scale_bounds=(1e-3, 1e3)) - kernel = matern_kernel + white_kernel - optimizer._gp = GaussianProcessRegressor( - kernel=kernel, normalize_y=True, n_restarts_optimizer=100, alpha=0.0 - ) - utility = UtilityFunction(kind="ucb", kappa=args.kappa, xi=None) - - print("Registering optimization results for previous runs:") - print(f" {'':4} |------------------------------------------------------") - print(f" {'':<4} | ", end="") - for parameter in parameter_names_correct_order: - print(f"{parameter:>20} ", end="") - print(f"{args.fitness:>20}") - print(f" {'':4} |------------------------------------------------------") - - for i in range(opt_data.shape[1]): - if opt_data[0, i] == args.fitness: - target_ind = i - break - - param = [] - target = [] - - for iteration in range(1, opt_data.shape[0]): - param.append({}) - for i in range(len(parameter_names_correct_order)): - param[-1][opt_data[0, i]] = float(opt_data[iteration, i]) - target.append(float(opt_data[iteration, target_ind])) - - target = np.array(target, dtype=np.float64) - target_flip = np.array(target, dtype=np.float64) - max_target = np.max(target) - - for i, t in enumerate(target): - if args.fitness in ("MSE", "RMSE", "MAE", "MAPE", "SMAPE"): - target_flip[i] = max_target - t - else: - target_flip[i] = t - - iteration = 0 - for p, t, t_print in zip(param, target_flip, target): - iteration += 1 - added = False - tries = 0 - random_key = None - while not added: - try: - if tries > 0: - random_key = np.random.choice(list(p.keys()), 1)[0] - p[random_key] += np.random.normal(loc=0.0, scale=1e-14) - optimizer.register(params=p, target=t) - added = True - except KeyError: - tries += 1 - - print(f" {iteration:>4} | ", end="") - for parameter in parameter_names_correct_order: - print(f"{p[parameter]:20.15f} ", end="") - print(f"{t_print:20.15f} ", - end=f" ({tries}, {random_key})\n" if tries != 0 else "\n") - print(f" {'':4} |------------------------------------------------------") - - print("Evaluating acquisition function for next parameter space point:") - print(" |------------------------------------------------------") - next_point = optimizer.suggest(utility) - for i, parameter in enumerate(parameter_names_correct_order): - print(f"{i+1:>3}| {parameter:>5} {next_point[parameter]:>20.15f} " - f"[{lower_bounds[i]:>6.2f}, {upper_bounds[i]:>6.2f}]") - print(" |------------------------------------------------------") - - with open(next_point_file_path, "w") as out_file: - for parameter in parameter_names_correct_order: - out_file.write(f"{next_point[parameter]:20.15f}") - out_file.write("") - - print("Kernel hyper-parameters fitted to the data (max. log. likelihood):") - print(" |------------------------------------------------------") - print(" | ", optimizer._gp.kernel) - print(" |------------------------------------------------------") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=( - "Get the next point in parameter space, acquisition func maximum" - ) - ) - parser.add_argument( - "--random-seed", "-rseed", "-seed", dest="seed", type=int, - default=None, help="seed for the random number generator" - ) - parser.add_argument( - "--bounds-file", type=str, default="bounds.txt", metavar="FILE_NAME", - help="input bounds file path (default 'bounds.txt')", - ) - parser.add_argument( - "--parameters-file", type=str, default="parameters.txt", - metavar="FILE_NAME", - help="input bounds file path (default 'parameters.txt')", - ) - parser.add_argument( - "--opt-file", type=str, default="opt_data.txt", - metavar="FILE_NAME", - help="input bounds file path (default 'opt_data.txt')", - ) - parser.add_argument( - "--out", type=str, default="next_point.txt", metavar="FILE_NAME", - help="output file path (default 'next_point.txt')", - ) - parser.add_argument( - "--fitness", type=str, default="R2", - help=( - "name of the fitness function to use for the acquisition function" - ), - ) - parser.add_argument( - "--kappa", type=float, default=1.0, dest="kappa", - help="exploration/exploitation tradeoff parameter", - ) - - args = parser.parse_args() - if args.seed is not None: - np.random.seed(args.seed) - - bounds_file_path = os.path.abspath(args.bounds_file) - parameters_file_path = os.path.abspath(args.parameters_file) - next_point_file_path = os.path.abspath(args.out) - opt_file_path = os.path.abspath(args.opt_file) - - get_next_point_BO( - bounds_file_path, opt_file_path, args.kappa, - ) diff --git a/utils/get_next_point_random.py b/utils/get_next_point_random.py deleted file mode 100644 index 5a6780bb..00000000 --- a/utils/get_next_point_random.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import numpy as np -import argparse -from read_parameter_file import read_bounds_file - - -def get_next_point(bounds_file_path, verbose=True): - parameter_names_correct_order, lower_bounds, upper_bounds = ( - read_bounds_file(bounds_file_path) - ) - values = [None for _ in parameter_names_correct_order] - if verbose: - print("Picking random points for parameters:") - print(" |------------------------------------------------------") - for i, p in enumerate(parameter_names_correct_order): - val = np.random.uniform(low=lower_bounds[i], high=upper_bounds[i]) - values[i] = val - if verbose: - print(f"{i+1:>3}| {p:>5} {val:>20.15f} " - f"[{lower_bounds[i]:>6.2f}, {upper_bounds[i]:>6.2f}]") - if verbose: - print(" |------------------------------------------------------") - - with open(next_point_file_path, "w") as out_file: - for i, param in enumerate(parameter_names_correct_order): - out_file.write(f"{values[i]:20.15f} ") - - -if __name__ == "__main__": - description = "Get a random point in parameter space" - parser = argparse.ArgumentParser( - description=description, - formatter_class=lambda prog: argparse.HelpFormatter( - prog, max_help_position=50 - ), - ) - parser.add_argument( - "--random-seed", "-rseed", "-seed", dest="seed", type=int, - default=None, help="seed for the random number generator", - ) - parser.add_argument( - "--bounds-file", type=str, default="bounds.txt", metavar="FILE_NAME", - help="input bounds file path (default 'bounds.txt')", - ) - parser.add_argument( - "--out", type=str, default="next_point.txt", metavar="FILE_NAME", - help="output file path (default 'next_point.txt')", - ) - parser.add_argument( - "-s", "--silent", action="store_true", default=False, - help="suppress terminal output", - ) - args = parser.parse_args() - if args.seed is not None: - np.random.seed(args.seed) - - bounds_file_path = os.path.abspath(args.bounds_file) - next_point_file_path = os.path.abspath(args.out) - - get_next_point(bounds_file_path, verbose=not args.silent) diff --git a/utils/h5md2input.py b/utils/h5md2input.py index 45fb9826..62596283 100644 --- a/utils/h5md2input.py +++ b/utils/h5md2input.py @@ -5,65 +5,88 @@ def h5md_to_input(h5md_file, old_input, frame, out_path=None, overwrite=False): if out_path is None: - out_path = os.path.join(os.path.abspath(os.path.dirname(old_input)), - os.path.split(old_input)[-1] + '_new') + out_path = os.path.join( + os.path.abspath(os.path.dirname(old_input)), + os.path.split(old_input)[-1] + "_new", + ) if os.path.exists(out_path) and not overwrite: - error_str = (f'The specified output file {out_path} already exists. ' - f'use overwrite=True ("-f" flag) to overwrite.') + error_str = ( + f"The specified output file {out_path} already exists. " + f'use overwrite=True ("-f" flag) to overwrite.' + ) raise FileExistsError(error_str) - f_in = h5py.File(old_input, 'r') - f_out = h5py.File(out_path, 'w') - new_values = h5py.File(h5md_file, 'r') + f_in = h5py.File(old_input, "r") + f_out = h5py.File(out_path, "w") + new_values = h5py.File(h5md_file, "r") + + for k, v in f_in.attrs.items(): + f_out.attrs[k] = v for k in f_in.keys(): f_in.copy(k, f_out) - in_steps = new_values['particles/all/position/step'] - in_time = new_values['particles/all/position/time'] + in_steps = new_values["particles/all/position/step"] + in_time = new_values["particles/all/position/time"] n_frames = len(in_steps) if frame >= n_frames: raise ValueError( - f'File {h5md_file} only contains {n_frames} frames, cannot ' - f'extract frame #{frame}' + f"File {h5md_file} only contains {n_frames} frames, cannot " + f"extract frame #{frame}" ) if frame < 0: if n_frames + frame < 0: raise ValueError( - f'File {h5md_file} only contains {n_frames} frames, cannot ' - f'extract frame #{-frame} from the end' + f"File {h5md_file} only contains {n_frames} frames, cannot " + f"extract frame #{-frame} from the end" ) frame_ = n_frames + frame print( - f'Extracting frame #{frame_} (simulation step {in_steps[frame]} ' - f'at time {in_time[frame]} ps)' + f"Extracting frame #{frame_} (simulation step {in_steps[frame]} " + f"at time {in_time[frame]} ps)" ) - new_positions = new_values['particles/all/position/value'][frame, :, :] - f_out['coordinates'][:, :] = new_positions - if 'particles/all/velocity' in new_values: - new_velocities = new_values['particles/all/velocity/value'][frame, :, :] - f_out['velocities'][:, :] = new_velocities + new_positions = new_values["particles/all/position/value"][frame, :, :] + f_out["coordinates"][:, :] = new_positions + if "particles/all/velocity" in new_values: + new_velocities = new_values["particles/all/velocity/value"][frame, :, :] + f_out["velocities"][:, :] = new_velocities f_in.close() f_out.close() new_values.close() -if __name__ == '__main__': - description = 'Convert .h5md files to the hymd input format' +if __name__ == "__main__": + description = "Convert .h5md files to the hymd input format" parser = argparse.ArgumentParser(description=description) - parser.add_argument('h5md_file', type=str, help='input .H5MD file name') - parser.add_argument('old_input_file', type=str, - help='previous .H5 file name') - parser.add_argument('--out', type=str, default=None, dest='out_path', - metavar='file name', help='output hymd HDF5 file name') - parser.add_argument('--frame', type=int, default=-1, - help='the frame number to extract') - parser.add_argument('-f', action='store_true', default=False, dest='force', - help='overwrite existing output file') + parser.add_argument("h5md_file", type=str, help="input .H5MD file name") + parser.add_argument("old_input_file", type=str, help="previous .H5 file name") + parser.add_argument( + "--out", + type=str, + default=None, + dest="out_path", + metavar="file name", + help="output hymd HDF5 file name", + ) + parser.add_argument( + "--frame", type=int, default=-1, help="the frame number to extract" + ) + parser.add_argument( + "-f", + action="store_true", + default=False, + dest="force", + help="overwrite existing output file", + ) args = parser.parse_args() - h5md_to_input(args.h5md_file, args.old_input_file, args.frame, - overwrite=args.force, out_path=args.out_path) + h5md_to_input( + args.h5md_file, + args.old_input_file, + args.frame, + overwrite=args.force, + out_path=args.out_path, + ) diff --git a/utils/hymd_optimize.py b/utils/hymd_optimize.py index f6b45bed..7c67f5bc 100644 --- a/utils/hymd_optimize.py +++ b/utils/hymd_optimize.py @@ -3,6 +3,7 @@ import numpy as np import tables import MDAnalysis as mda +import matplotlib import matplotlib.pyplot as plt import matplotlib.colors import itertools @@ -11,9 +12,10 @@ import argparse import warnings from sklearn import metrics as sklm +import shutil +import h5py - -def find_species_indices(names, species_name): +def find_species_indices(names=None, species_name=None, indexrange=None): """Calculate the index of each particle of species `species_name` in a topology HDF5 file or a MDAnalysis trajectory @@ -34,7 +36,9 @@ def find_species_indices(names, species_name): indices : numpy.ndarray Indices of particles with names matching `species_name`. """ - if isinstance(names, np.ndarray): + if isinstance(indexrange, np.ndarray): + indices = np.arange(indexrange[0], indexrange[1]+1) + elif isinstance(names, np.ndarray): indices = np.where(names == np.string_(species_name)) elif isinstance(names, mda.Universe): indices = names.atoms.select_atoms(f"type {species_name}").indices @@ -149,14 +153,19 @@ def parse_molecule_species(names, molecules=None): ) ] ) - toy_lipid = ( + tg = ( [ - np.string_("type:" + s) for s in ("N", "C", "C", "", "", "", "", - "", "", "", "", "", "", "",) + np.string_("type:" + s) for s in ("C", "G", "G", "G", # head + "C", "C", "C", "C", # tail 1 + "C", "C", "C", "C", # tail 2 + "C", "C", "C", "C") #tail 3 ], [ "name:" + s for s in ( - "GL0", "C1A", "C2A", + "TC", "TGL1", "TGL2", "TGL3", # head + "TC1A", "TC2A", "TC3A", "TC4A", # tail 1 + "TC1B", "TC2B", "TC3B", "TC4B", # tail 2 + "TC1C", "TC2C", "TC3C", "TC4C", # tail 2 ) ] ) @@ -173,7 +182,7 @@ def parse_molecule_species(names, molecules=None): ) species = {} - for d in (dppc, dmpc, dspc, dopc, popg, toy_lipid, w): + for d in (dppc, dmpc, dspc, dopc, popg, tg, w): t = {s.decode("UTF-8"): None for s in d[0] if s != b"type:"} n = {s: None for s in d[1]} species = {**species, **{**t, **n}} @@ -183,7 +192,7 @@ def parse_molecule_species(names, molecules=None): "lipid:DSPC": None, "lipid:DOPC": None, "lipid:POPG": None, - "lipid:toy": None, + "lipid:TG": None, "solvent": None, "all": None, }} @@ -265,10 +274,23 @@ def add_(dictionary, keys, element): add_(species, ("name:C1B", "type:C", "lipid:POPG"), mol[0][7]) add_(species, ("name:C2B", "type:C", "lipid:POPG"), mol[0][8]) add_(species, ("name:C3B", "type:C", "lipid:POPG"), mol[0][9]) - elif np.array_equal(n, toy_lipid[0][:n_mol]) and n_mol == 3: - add_(species, ("name:GL0", "type:N", "lipid:toy"), mol[0][0]) - add_(species, ("name:C1A", "type:C", "lipid:toy"), mol[0][1]) - add_(species, ("name:C2A", "type:C", "lipid:toy"), mol[0][2]) + elif np.array_equal(n, tg[0][:n_mol]) and n_mol == 16: + add_(species, ("name:TC", "type:C", "lipid:TG"), mol[0][0]) + add_(species, ("name:TGL1", "type:G", "lipid:TG"), mol[0][1]) + add_(species, ("name:TGL2", "type:G", "lipid:TG"), mol[0][2]) + add_(species, ("name:TGL3", "type:G", "lipid:TG"), mol[0][3]) + add_(species, ("name:TC1A", "type:C", "lipid:TG"), mol[0][4]) + add_(species, ("name:TC2A", "type:C", "lipid:TG"), mol[0][5]) + add_(species, ("name:TC3A", "type:C", "lipid:TG"), mol[0][6]) + add_(species, ("name:TC4A", "type:C", "lipid:TG"), mol[0][7]) + add_(species, ("name:TC1B", "type:C", "lipid:TG"), mol[0][8]) + add_(species, ("name:TC2B", "type:C", "lipid:TG"), mol[0][9]) + add_(species, ("name:TC3B", "type:C", "lipid:TG"), mol[0][10]) + add_(species, ("name:TC4B", "type:C", "lipid:TG"), mol[0][11]) + add_(species, ("name:TC1C", "type:C", "lipid:TG"), mol[0][12]) + add_(species, ("name:TC2C", "type:C", "lipid:TG"), mol[0][13]) + add_(species, ("name:TC3C", "type:C", "lipid:TG"), mol[0][14]) + add_(species, ("name:TC4C", "type:C", "lipid:TG"), mol[0][15]) elif np.array_equal(n, w[0][:n_mol]) and n_mol == 1: add_(species, ("name:W", "type:W", "solvent"), mol[0][0]) else: @@ -394,6 +416,7 @@ def load_hymd_simulation(topology_file_path, trajectory_file_path): trajectory_file_hdf5 : tables.file In-memory representation of HDF5 file `trajectory_file_path`. """ + print(topology_file_path) topology_file_hdf5 = tables.open_file( topology_file_path, driver="H5FD_CORE", ) @@ -433,10 +456,10 @@ def load_gromacs_simulation(topology_file_path, trajectory_file_path): def compute_centered_histogram( centers, positions, simulation_box, species, axis, bins=10, density=False, symmetrize=False, skip_first=0, skip=1, frames=None, time=None, - file_names="", silent=False, range_=None, + file_names="", silent=False, range_=None, xyrange=None ): """Calculate the density histogram along a direction of positions at - specified indices realative to a given simlulation box center + specified indices relative to a given simlulation box center Calculates the average histogram of `positions` along direction `axis` centered at position `center` in the `simulation_box`. Only positions @@ -479,7 +502,7 @@ def compute_centered_histogram( time : (M,) np.ndarray, optional Array of times for each frame in the simulation. range_ : float, optional - X-axis range used to compute the histogram. + axis range of `axis` used to compute the histogram. file_names : str, optional Name of the files containing the trajectory data for printing to terminal. @@ -513,20 +536,25 @@ def compute_centered_histogram( s: np.zeros(shape=(bins,), dtype=np.float64) for s in species } - def compute_histogram_single(p, c, b, s, h, d, bins, box, axis_range): - p += 0.5 * b - c - p[p > b] -= b - p[p < 0.0] += b + def compute_histogram_single(p, c, b, s, h, d, bins, box, axis_range, xyrange): + p = shift_positions(p, c, b) for ss, ind in s.items(): - hist, _ = np.histogram(p[ind], bins=bins, density=d) if d: scaling_factor = 1.0 else: box[axis, axis] = axis_range + if isinstance(xyrange, list): + if xyrange[0]x_start) & + (positions[time_step,:,0]y_start) & + (positions[time_step,:,1] 1000.0: t0 = time[skip_first] / 1000.0 @@ -585,18 +627,21 @@ def compute_histogram_single(p, c, b, s, h, d, bins, box, axis_range): box_length = simulation_box[time_step, ...] / 10.0 box_length_axis = simulation_box[time_step, axis, axis] / 10.0 - box_mid = 0.5 * box_length_axis - x_start = box_mid - 0.5 * range_ - x_end = box_mid + 0.5 * range_ - range__ = (x_start, x_end) - axis_range = x_end - x_start - + if range_ is not None: + box_mid = 0.5 * box_length_axis + x_start = box_mid - 0.5 * range_ + x_end = box_mid + 0.5 * range_ + range__ = (x_start, x_end) + axis_range = x_end - x_start + else: + range__ = (0.0, box_length_axis) histogram_bins = np.histogram_bin_edges( np.zeros(shape=(1,)), bins=bins, range=range__, ) histograms = compute_histogram_single( p, centers[time_step] / 10.0, box_length_axis, species, histograms, density, histogram_bins, box_length, axis_range, + xyrange, ) last_frame = time_step H += 1 @@ -637,10 +682,12 @@ def compute_histogram_single(p, c, b, s, h, d, bins, box, axis_range): def plot_histogram( - bin_midpoints, histogram, figwidth=4.0, figheight=3.0, show=True, + bin_midpoints, histogram, figwidth=13.0, figheight=10, show=True, ignore=None, one_side=False, xlim=None, ylim=None, vmd_colors=False, remove_xlabel=False, remove_ylabel=False, remove_xticks=False, - remove_yticks=False, tight=False, no_marker=None, remove_legend=False, + remove_yticks=False, tight=False, no_marker=None, linestyle=None, remove_legend=False, + fontsize=None, box_scale=None, out=False, + labels=[], fig=None, ax=None, ): """Visualize a histogram @@ -678,9 +725,10 @@ def plot_histogram( Do not display tick labels on the y axis. tight : bool, optional Use matplotlib tight_layout for the figure. - vmd_colors : bool, optional - If True, plot the bead type profiles using the same color map as used + vmd_colors : str, optional + "default": plot the bead type profiles using the same color map as used by default in VMD for that corresponding bead name. + "custom_dppc": ....color map customized for dppc on vmd no_marker : bool, optional If True, do not show markers on lines in the plot. remove_legend : bool, optional @@ -693,16 +741,27 @@ def plot_histogram( ax : matplotlib.pyplot.Axes Matplotlib axes used in the plot. """ - fig, ax = plt.subplots(1, 1) - fig.set_figwidth(figwidth) - fig.set_figheight(figheight) + c = {'blue':'#001C7F', + 'green':'#017517', + 'golden':'#B8860B', + 'purple':'#633974', + 'red':'#8C0900', + 'teal':'#006374', + 'brown':'#573D1C', + 'orange':'#DC901D'} + matplotlib.rcParams['axes.prop_cycle'] = matplotlib.cycler('color',c.values()) + if ax is None: + #ax = plt.figure().add_subplot(111) + fig, ax = plt.subplots(1, 1) + fig.set_figwidth(figwidth) + fig.set_figheight(figheight) - colors = matplotlib.colors.TABLEAU_COLORS + #colors = matplotlib.colors.TABLEAU_COLORS markers = ( ".", "o", "v", "^", ">", "<", "s", "p", "x", "D", "H", "1", "2", "3", ) - colors_cycle = itertools.cycle(colors) - markers_cycle = itertools.cycle(markers) + colors_cycle = itertools.cycle(c.values()) + #colors_cycle = itertools.cycle(colors) ignore = [re.compile(i) for i in ignore] keys = list(histogram.keys()) @@ -716,12 +775,13 @@ def plot_histogram( bin_midpoints -= np.mean(bin_midpoints[N//2 - 1:N//2 + 1]) else: bin_midpoints -= bin_midpoints[N//2] - + if not linestyle: + linestyle = 'solid' for s in keys: label = s marker = next(markers_cycle) if len(histogram[s]) < 26 else None - if vmd_colors and "type" in s: + if vmd_colors=="default" and "type" in s: if s == "type:N": color = (0.0, 0.0, 1.0) # blue label = "N" @@ -742,14 +802,42 @@ def plot_histogram( color = (0.0, 0.0, 0.0) # black label = "W" marker = "s" - elif s == "type:D": - color = (0.3, 0.8, 0.8) - label = "D" - marker = "<" - elif s == "type:L": - color = (0.3, 0.3, 0.3) - label = "L" - marker = ">" + elif vmd_colors=="custom_dppc" and "type" in s: + if s == "type:N": + color = (0.711, 0.581, 0.139) # yellow + label = "N" + marker = "o" + elif s == "type:C": + color = (0.537, 0.537, 0.537) # gray + label = "C" + marker = "x" + elif s == "type:P": + color = (0.567, 0.291, 0.0) # ochre + label = "P" + marker = "^" + elif s == "type:G": + #color = (0.672, 0.0, 0.0) # red3 + color = '#da0e00' # bright red + label = "G" + marker = "v" + elif s == "type:W": + #color = (0.296, 0.792, 0.910) # blue2 + color = (0, 0.582, 0.910) # deep bright blue + label = "W" + marker = "s" + elif vmd_colors=="custom_dppc" and "name" in s: + if s == "name:GL2": + color = (0.672, 0.0, 0.0) # red3 + label = "DPPC" + marker = "o" + labels.append(label) + elif s == "name:TGL3": + histogram[s] *= 2 #scaling up by 2 for clarity + #color = (0.107, 0.211, 0.255) # blue2 + color = (0.296, 0.792, 0.910) # blue2 + label = "TG" + marker = "o" + labels.append(label) else: color = next(colors_cycle) @@ -757,24 +845,31 @@ def plot_histogram( if no_marker: marker = "" + if box_scale: + xaxis = np.linspace(-int(box_scale)/2, int(box_scale)/2, len(bin_midpoints)) + else: + xaxis = bin_midpoints + ax.plot( - bin_midpoints, histogram[s], color=color, label=label, - marker=marker, + xaxis, histogram[s], color=color, label=label, + marker=marker, linestyle=linestyle, linewidth = 3.4, ) - fontsize = 15 - plt.subplots_adjust(bottom=0.2, left=0.22, right=0.97, top=0.97) + #plt.subplots_adjust(bottom=0.2, left=0.22, right=0.97, top=0.97) if not remove_legend: - ax.legend(loc="best", fontsize=fontsize, framealpha=0) + #ax.legend(loc=(0.03,0.25), fontsize=fontsize, framealpha=0) + ax.legend(loc=(0.003,0.25), fontsize=fontsize, framealpha=0) - ax.tick_params(axis='both', which='major', labelsize=15) - ax.tick_params(axis='both', which='minor', labelsize=15) + ax.tick_params(axis='both', which='major', labelsize=fontsize) + ax.tick_params(axis='both', which='minor', labelsize=fontsize) if not remove_xlabel: - ax.set_xlabel("Position along normal, nm", fontsize=fontsize) + ax.set_xlabel("Position along normal "+r"$\mathrm{(nm)}$", fontsize=fontsize) + # ax.xaxis.label.set_color("white") if not remove_ylabel: - ax.set_ylabel("Number density, nm⁻³", fontsize=fontsize) + ax.set_ylabel("Number density "+r"$\mathrm{(nm^{-3})}$", fontsize=fontsize) + # ax.yaxis.label.set_color("white") if remove_xticks: # https://stackoverflow.com/a/4762002/4179419 @@ -795,15 +890,10 @@ def plot_histogram( if tight: fig.set_tight_layout(True) - if show: - plt.show() - - return fig, ax - def compute_histogram_fitness( bin_midpoints, histogram_reference, histogram_test, ignore=None, - resolution="names", area_per_lipid_test=None, area_per_lipid_ref=None, + resolution="names", ): """Computes various metrics of the similarity between two histograms @@ -879,6 +969,13 @@ def compute_histogram_fitness( ), 2, ).T print("Area per lipid: ", y[0]) + """ + y_true = np.tile( + np.array( # area per lipid in Å + [100 * np.mean(area_per_lipid_ref)], dtype=np.float64 + ), 2, + ).T + """ y_true = np.array([63.0, 63.0], dtype=np.float64) else: @@ -907,8 +1004,6 @@ def compute_histogram_fitness( where=(np.abs(y_true) + np.abs(y)) > np.finfo(np.float64).eps, ) ) - if s == "area_per_lipid": - accuracy[s]["SMAPE"] = accuracy[s]["MSE"] if resolution == "names": total_re = re.compile("name:.*") @@ -1050,8 +1145,29 @@ def action_compute_histogram(parser, ref=False): top_file_hdf5, traj_file_hdf5 = load_hymd_simulation( top_path, traj_path ) +# simulation_box = ( +# traj_file_hdf5.root.particles.all.box.edges.value.read() +# ) +# +# simulation_box = ( +# np.array([list(val.diagonal()) for val in simulation_box]) +# ) +# names = top_file_hdf5.root.names.read() molecules = top_file_hdf5.root.molecules.read() +# try: +# #still allowing old format +# #if box exists as .../box.edges.value (timeframes,3,3) +# #using just the first frame value: +# simulation_box = np.array([list(val.diagonal()) for val in simulation_box]) +# except: +# #if box exists as .../box.edges (3,) +# warnings.warn("------------------ \ +# Using old box format (3,). Soon to be deprecated. \ +# Not valid for NPT runs." +# ) +# first_frame = parser.skip_first+1 +# last_frame = parser.skip_first+1+parser.frames positions = traj_file_hdf5.root.particles.all.position.value.read() times = traj_file_hdf5.root.particles.all.position.time.read() @@ -1084,7 +1200,7 @@ def action_compute_histogram(parser, ref=False): skip_first=parser.skip_first, frames=parser.frames, time=times, file_names=( os.path.abspath(top_path) + ", " + os.path.abspath(traj_path) - ), range_=parser.range, + ), range_=parser.range, xyrange=parser.xyrange, ) top_file_hdf5.close() traj_file_hdf5.close() @@ -1135,8 +1251,33 @@ def action_compute_histogram(parser, ref=False): area_per_lipid = None return histogram_bins, histograms, area_per_lipid - -def action_plot(parser): +def action_multiplot(parser): + samiran_signature = plt.style.use('samiran-signature') + #using matplotlibstyle: /Users/samiransen23/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/mpl-data/stylelib/samiran-signature.mplstyle + fig, ax = plt.subplots(1, 1) + fig.set_size_inches(15.6, 8.0) + linestyles = ( + "solid", "dashed", "dotted", + ) + linestyles_cycle = itertools.cycle(linestyles) + fontsize = 32 + labels = [] + for i in range(len(parser.traj)): + print(parser.traj[i], ) + linestyle = next(linestyles_cycle) if len(parser.traj) < 4 else None + parser_single = copy.deepcopy(parser) + parser_single.traj = parser_single.traj[i] + parser_single.top = parser_single.top[i] + parser_single.skip_first = parser_single.skip_first[i] + parser_single.frames = parser_single.frames[i] + action_plot(parser_single, linestyle=linestyle, remove_legend=True, fontsize=fontsize, + labels=labels, fig=fig, ax=ax) + ax.legend(labels[:len(parser.traj)], loc='best', fontsize=fontsize, framealpha=0) + #ax.legend(loc=(0.03,0.45), fontsize=fontsize, framealpha=0) + + +def action_plot(parser, linestyle=None, remove_legend=False, fontsize=34, + labels=[], fig=None, ax=None,): """Execute the 'plot' action of hymd_optimize Parameters @@ -1154,22 +1295,23 @@ def action_plot(parser): histogram_bins, histograms, area_per_lipid = action_compute_histogram( parser ) - fig, ax = plot_histogram( + + plot_histogram( histogram_bins, histograms, ignore=parser.ignore, show=not parser.no_show, one_side=parser.one_side, xlim=parser.xlim, ylim=parser.ylim, vmd_colors=parser.vmd_colors, remove_xlabel=parser.remove_xlabel, remove_ylabel=parser.remove_ylabel, remove_yticks=parser.remove_yticks, remove_xticks=parser.remove_xticks, - tight=parser.tight, no_marker=parser.no_marker, - remove_legend=parser.remove_legend, + tight=parser.tight, no_marker=parser.no_marker, linestyle=linestyle, + remove_legend=remove_legend, + fontsize=fontsize, box_scale=parser.box_scale, out=parser.out, + labels=labels, fig=fig, ax=ax, ) """ import pickle pickle.dump(histogram_bins, open("bins_tmp.p", "wb",),) pickle.dump(histograms, open("hist_tmp.p", "wb",),) """ - return fig, ax - def action_fitness(parser): """Execute the 'fitness' action of hymd_optimize @@ -1356,6 +1498,103 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): return fitness_range +def shift_positions(p,c,b): + p_new = p + 0.5 * b - c + p_new[p_new > b] -= b + p_new[p_new < 0.0] += b + return p_new + +def action_centre_sim(parser): + """Execute the 'centre-sim' action of hymd_optimize + Parameters + ---------- + parser : Argparse.Namespace + Parsed command line arguments given to hymd_optimize. + """ + + def write_sim_new(p, parser): + #make copy of trajectory + if parser.out is None: + parser.out = os.path.join(os.path.abspath(os.path.dirname(parser.traj)), + #os.path.split(parser.traj)[-1] + + 'sim_centred.h5') + src = os.path.abspath(parser.traj[0]) + shutil.copyfile(src, parser.out) + f = h5py.File(parser.out,'r+') + #rewrite positions/value with p + f['particles/all/position/value'][:, :, axis] = p[:, :, axis] + f.close() + + axis = parser.axis + #assuming only 1 file is sent at a time + traj_path = parser.traj[0] + top_path = parser.top[0] + skip_first = parser.skip_first[0] + frames = parser.frames[0] + + skip = parser.skip + + top_file_hdf5, traj_file_hdf5 = load_hymd_simulation( + top_path, traj_path + ) + names = top_file_hdf5.root.names.read() + molecules = top_file_hdf5.root.molecules.read() + simulation_box_group = traj_file_hdf5.root.particles.all.box + if isinstance(simulation_box_group.edges, tables.Leaf): + simulation_box = np.zeros( + shape=(positions.shape[0], 3, 3,), dtype=np.float64, + ) + for i in range(positions.shape[0]): + simulation_box[i, ...] = np.diag(simulation_box_group.edges[:]) + elif isinstance(simulation_box_group.edges, tables.group.Group): + simulation_box = simulation_box_group.edges.value[...] + else: + raise TypeError( + f"Expected /root/particles/all/box/edges to be either a " + f"h5py.Group or a h5py.Dataset, not " + f"{type(simulation_box_group.edges)}" + ) + #try: + # #if box exists as .../box.edges.value (timeframes,3,3) + # simulation_box = np.array([list(val.diagonal()) for val in simulation_box]) + #except: + # #if box exists as .../box.edges (3,) + # warnings.warn(""" + # Using old box format (3,). Soon to be deprecated. + # """ + # ) + first_frame = skip_first+1 + last_frame = skip_first+1+frames + positions = traj_file_hdf5.root.particles.all.position.value.read() + times = traj_file_hdf5.root.particles.all.position.time.read() + top_file_hdf5.close() + traj_file_hdf5.close() + if isinstance(parser.indexrange, list): + indexrange = np.array(parser.indexrange) + custom_indices = find_species_indices(indexrange=indexrange) + carbon_center_of_mass = calculate_center_of_mass( + positions, simulation_box, custom_indices, axis, + ) + else: + carbon_indices = find_species_indices(names, "C") + carbon_center_of_mass = calculate_center_of_mass( + positions, simulation_box, carbon_indices, axis, + ) + species = parse_molecule_species(names, molecules=molecules) + + H = 0 + L = positions.shape[0] + M = min(L - 1, skip_first + frames * skip) if frames is not None else (L-1) + positions_new = np.copy(positions) + for time_step in range(skip_first, M+1, skip): + p = positions[time_step, :, axis] + box_length = simulation_box[time_step, ...] + box_length_axis = simulation_box[time_step, axis, axis] + positions_new[time_step, :, axis] = shift_positions( + p, + carbon_center_of_mass[time_step], + box_length_axis) + write_sim_new(positions_new, parser) if __name__ == '__main__': parser = argparse.ArgumentParser( @@ -1365,7 +1604,7 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): ) parser.add_argument( "action", type=str, default=None, help="action to perform", - choices=["plot", "fitness", "fitness-range"], + choices=["plot", "fitness", "fitness-range", "centre-sim"], ) parser.add_argument( "--out", type=str, default=None, metavar="file name", @@ -1404,20 +1643,22 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): help="refence topology file path (.gro)", ) parser.add_argument( - "--traj", type=str, default=None, + #"--traj", type=str, default=None, + "--traj", nargs='+', default=None, help="test trajectory file path (.trr or .H5MD)", ) parser.add_argument( - "--top", type=str, default=None, - help="test topology file path (.gro or HyMD-input .H5)", + #"--top", type=str, default=None, + "--top", nargs='+', default=None, + help="test topology file path (.gro or HyMD-input .H5)" ) parser.add_argument( "--bins", type=int, default=25, - help="number of bins to use in the histograms and histogram plots", + help="number of bins to use in the histograms and histogram plots" ) parser.add_argument( "--density", action="store_true", - help="compute density distribution histograms, not the number density", + help="compute density distribution histograms, not the number density" ) parser.add_argument( "--symmetrize", action="store_true", default=False, @@ -1437,11 +1678,11 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): ) parser.add_argument( "--axis", type=int, choices=[0, 1, 2], default=2, - help="the direction in which to calculate the histogram(s)", + help="the direction in which to calculate the histogram(s)" ) parser.add_argument( "--axis-ref", type=int, choices=[0, 1, 2], default=None, - help="the direction in which to calculate the reference histogram(s)", + help="the direction in which to calculate the reference histogram(s)" ) parser.add_argument( "--ignore", default=[], nargs="+", @@ -1488,46 +1729,63 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): ) parser.add_argument( "--skip", type=int, default=1, - help="consider only every N frames when calculating the histograms", + help="consider only every N frames when calculating the histograms" ) parser.add_argument( - "--skip-first", type=int, default=0, - help="skip the first N frames when calculating the histograms", + #"--skip-first", type=int, default=0, + "--skip-first", nargs='+', type=int, default=0, + help="skip the first N frames when calculating the histograms" ) parser.add_argument( - "--frames", type=int, default=None, + #"--frames", type=int, default=None, + "--frames", nargs='+', type=int, default=None, help=( "Only consider N frames, starting at --skip_first (with step " "size --skip)." - ), + ) ) parser.add_argument( "--skip-ref", type=int, default=1, help=( "consider only every N reference frames when calculating the " "histograms" - ), + ) ) parser.add_argument( "--skip-first-ref", type=int, default=0, help=( "skip the first N reference frames when calculating the histograms" - ), + ) ) parser.add_argument( "--frames-ref", type=int, default=None, help=( "Only consider N reference frames, starting at --skip_first (with " "step size --skip)." - ), + ) ) parser.add_argument( "--metric", choices=["MSE", "RMSE", "MAE", "MAPE", "SMAPE", "R2"], default="R2", help="which fitness metric to use in the fitness-range", ) parser.add_argument( - "--vmd-colors", "--vmdcolors", "-vmdcolors", action="store_true", - default=False, help="use the same colors for beads as default in vmd", + "--vmd-colors", "--vmdcolors", "-vmdcolors", type=str, + default=None, help="use the colors for beads as in vmd. See plot_histogram function\ + for more details", + ) + parser.add_argument( + "--box_scale", type=float, default=None, + help='plot profiles with box length in z scaled to this value' + ) + parser.add_argument( + "--indexrange", type=int, default=None, nargs="+", + help='choose custom indices for where to center mass. Eg: 12288 13183' + ) + parser.add_argument( + "--xyrange", type=float, default=None, nargs="+", + help='crop box in xy to compute histogram.\ + Format: x_range y_range. Eg: 16 18 means the box will be\ + cropped in x and y at centre+/-8 and centre +/-9 respectively' ) args = parser.parse_args() @@ -1546,18 +1804,48 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): warnings.warn( "No --out path specified and, not saving plot." ) - fig, _ = action_plot(args) - if args.out is not None: - if args.force and os.path.exists(args.out): - print(f"Saving figure to {args.out} (overwriting existing).") - elif os.path.exists(args.out): - raise FileExistsError( - f"The file {args.out} already exists. To force overwrite, " - "use the -f option." - ) + if 'names' in args.ignore: + args.ignore.remove('names') + for s in ["NC3", "PO4", "GL1", "GL2", # head + "C1A", "C2A", "C3A", "C4A", # tail 1 + "C1B", "C2B", "C3B", "C4B", # tail 2 + "W" #water + ]: + args.ignore.append('name:'+s) + if 'types' in args.ignore: + args.ignore.remove('types') + for s in ["N", "P", "G", "C", # lipid + "W", #water + ]: + args.ignore.append('type:'+s) + if 'name:TC*' in args.ignore: + args.ignore.remove('name:TC*') + for s in ["TC1A", "TC2A", "TC3A", "TC4A", + "TC1B", "TC2B", "TC3B", "TC4B", + "TC1C", "TC2C", "TC3C", "TC4C", + "TC", + ]: + args.ignore.append('name:'+s) + if 'name:C*' in args.ignore: + args.ignore.remove('name:C*') + for s in ["C1A", "C2A", "C3A", "C4A", + "C1B", "C2B", "C3B", "C4B", + ]: + args.ignore.append('name:'+s) + #if isinstance(args.traj, str): + # action_plot(args) + #elif isinstance(args.traj, list): + # action_multiplot(args) + action_multiplot(args) + if not args.no_show: + plt.show() + else: + if args.out: + out = os.path.abspath(args.out) else: - print(f"Saving figure to {args.out}.") - fig.savefig(args.out, format="pdf", transparent=True) + out = os.getcwd()+'/fig_temp.pdf' + plt.savefig(out, format='pdf', bbox_inches='tight') + print('Saving fig as: ',out) elif args.action == "fitness": if (args.traj is None or args.top is None or @@ -1616,3 +1904,6 @@ def action_fitness_range(parser, figwidth=4.0, figheight=3.0, show=True): print(f"Saving fitness range to {args.out}.") with open(args.out, "wb") as out_file: pickle.dump(fitness_range, out_file) + elif args.action == 'centre-sim': + action_centre_sim(args) + diff --git a/utils/make_whole_h5md.py b/utils/make_whole_h5md.py deleted file mode 100644 index 2a1d88ef..00000000 --- a/utils/make_whole_h5md.py +++ /dev/null @@ -1,158 +0,0 @@ -import h5py -import tqdm -import argparse -import numpy as np - - -def make_whole( - file_path, hymd_inp, out_path=None, frames=None, scale=None, shift=None, -): - if out_path is None: - out_path = ( - "".join(file_path.split(".")[:-1]) - + "_whole." - + file_path.split(".")[-1] - ) - - with h5py.File(out_path, "w") as out_file: - with h5py.File(hymd_inp, "r") as inp_file: - with h5py.File(file_path, "r") as in_file: - - class Copy: - def __init__(self, source, dest): - self.source = source - self.dest = dest - - def __call__(self, name): - if isinstance(self.source[name], h5py.Group): - # print(f"Copying Group -> {name}") - self.dest["/"].create_group(name) - - elif isinstance(self.source[name], h5py.Dataset): - # print(f"Copying Dataset -> {name}") - self.dest.create_dataset( - name, shape=self.source[name].shape, - dtype=self.source[name].dtype, - data=self.source[name][...], - ) - - visit_func = Copy(in_file, out_file) - in_file["/"].visit(visit_func) - - molecules = inp_file["/molecules"][...] - - n_frames = out_file["/particles/all/position/value"].shape[0] - # n_particles = out_file["/particles/all/position/value"].shape[1] - # n_dimensions = out_file["/particles/all/position/value"].shape[2] - box_size = out_file["/particles/all/box/edges"][...] - bond_from = out_file["/parameters/vmd_structure/bond_from"][...] - bond_to = out_file["/parameters/vmd_structure/bond_to"][...] - - bond_sort_ind = np.argsort(bond_from) - - out_file["/parameters/vmd_structure/bond_from"][...] = bond_from[bond_sort_ind] # noqa: E501 - out_file["/parameters/vmd_structure/bond_to"][...] = bond_to[bond_sort_ind] # noqa: E501 - - if ":" in frames[0]: - from_ = frames[0].split(":")[0] - to_ = frames[0].split(":")[1] - frames = list(np.arange(int(from_), int(to_))) - print(frames) - try: - frames_ = [int(f) for f in frames] - frames = frames_ - - except (TypeError, ValueError,): - if frames[0].lower() == "all": - frames = [i for i in range(n_frames)] - - print(f"Making frames whole whole: {frames}") - - if shift is not None: - out_file["/particles/all/position/value"][..., 0] = ( - out_file["/particles/all/position/value"][..., 0] - shift[0] - ) - out_file["/particles/all/position/value"][..., 1] = ( - out_file["/particles/all/position/value"][..., 1] - shift[1] - ) - out_file["/particles/all/position/value"][..., 2] = ( - out_file["/particles/all/position/value"][..., 2] - shift[2] - ) - out_file["/particles/all/position/value"][..., :] = ( - np.mod(out_file["/particles/all/position/value"][..., :], box_size[None, :]) - ) - - bond_from = out_file["/parameters/vmd_structure/bond_from"][...] - 1 - bond_to = out_file["/parameters/vmd_structure/bond_to"][...] - 1 - - for frame in tqdm.tqdm(frames): - positions = out_file["/particles/all/position/value"] - for molecule_index in np.unique(molecules): - particle_indices = np.where(molecules == molecule_index) - for particle in particle_indices[0]: - r = positions[frame, particle, :] - for dim in range(3): - if r[dim] > box_size[dim]: - positions[frame, particle, dim] -= box_size[dim] - if r[dim] < 0.0: - positions[frame, particle, dim] += box_size[dim] - - for particle in particle_indices[0]: - bonds = bond_to[np.where(bond_from == particle)[0]] - for b in bonds: - for dim in range(3): - ri = positions[frame, particle, dim] - rj = positions[frame, b, dim] - dr = rj - ri - if dr > 0.5 * box_size[dim]: - positions[frame, b, dim] -= box_size[dim] - if dr <= -0.5 * box_size[dim]: - positions[frame, b, dim] += box_size[dim] - - if scale is not None: - out_file["/particles/all/position/value"][...] = ( - scale * out_file["/particles/all/position/value"][...] - ) - out_file["/particles/all/box/edges"][...] = ( - scale * out_file["/particles/all/box/edges"][...] - ) - out_file["/particles/all/box/edges"][...] = ( - scale * out_file["/particles/all/box/edges"][...] - ) - - -if __name__ == '__main__': - description = "Make molecules whole in .H5MD files" - parser = argparse.ArgumentParser(description=description) - parser.add_argument( - "H5MD_file", type=str, help=".H5MD file path", metavar="PATH", - ) - parser.add_argument( - "H5_input", type=str, help="HyMD input .H5 file path", metavar="PATH", - ) - parser.add_argument( - "--frame", type=str, default=0, metavar="FRAME", nargs="+", - help=( - "frame(s) to extract ('all' selects all frames, 'i:j' selects all " - "frames between i and j)" - ) - - ) - parser.add_argument( - "--out", type=str, help="output file path", metavar="PATH", - default=None, required=False, - ) - parser.add_argument( - "--scale", type=float, help="scale positions by X", metavar="X", - default=None, required=False, - ) - parser.add_argument( - "--shift", type=float, default=None, required=False, nargs=3, - help="shift all positions by vector X Y Z", - ) - args = parser.parse_args() - - make_whole( - args.H5MD_file, args.H5_input, out_path=args.out, frames=args.frame, - scale=args.scale, shift=args.shift, - ) diff --git a/utils/nm2a_h5md.py b/utils/nm2a_h5md.py index 7766309f..be0ab071 100644 --- a/utils/nm2a_h5md.py +++ b/utils/nm2a_h5md.py @@ -19,11 +19,11 @@ def nm2a_h5md(h5md_file, overwrite=False, out_path=None): for k in f_in.keys(): f_in.copy(k, f_out) - box_size = f_in['particles/all/box/edges'][:] * 10. - f_out['particles/all/box/edges'][:] = box_size + box_size = f_in['particles/all/box/edges/value'][:] * 10. + f_out['particles/all/box/edges/value'][:] = box_size tpos = f_in['particles/all/position/value'][:] * 10. - f_out['particles/all/position/value'][:] = np.mod(tpos, box_size) + f_out['particles/all/position/value'][:] = tpos f_in.close() f_out.close() diff --git a/utils/pdb2hdf5.py b/utils/pdb2hdf5.py index d457907b..e396ce90 100644 --- a/utils/pdb2hdf5.py +++ b/utils/pdb2hdf5.py @@ -4,7 +4,9 @@ import math import numpy as np from openbabel import openbabel -# openbabel.obErrorLog.SetOutputLevel(0) # uncomment to suppress warnings + +openbabel.obErrorLog.SetOutputLevel(0) # uncomment to suppress warnings + def extant_file(x): """ @@ -17,16 +19,24 @@ def extant_file(x): raise argparse.ArgumentTypeError("{0} does not exist".format(x)) return x + def a2nm(x): - return x/10. + return x / 10.0 -def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=False): + +def pdb_to_input( + pdb_file, box=None, charges=None, bonds=None, out_path=None, overwrite=False +): if out_path is None: - out_path = os.path.join(os.path.abspath(os.path.dirname(pdb_file)), - os.path.splitext(pdb_file)[0]+".hdf5") + out_path = os.path.join( + os.path.abspath(os.path.dirname(pdb_file)), + os.path.splitext(pdb_file)[0] + ".hdf5", + ) if os.path.exists(out_path) and not overwrite: - error_str = (f'The specified output file {out_path} already exists. ' - f'use overwrite=True ("-f" flag) to overwrite.') + error_str = ( + f"The specified output file {out_path} already exists. " + f'use overwrite=True ("-f" flag) to overwrite.' + ) raise FileExistsError(error_str) # read charges @@ -38,8 +48,8 @@ def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=Fa charge_dict[beadtype] = float(beadchrg) # read bonds + bonds_list = [] if bonds: - bonds_list = [] with open(bonds, "r") as f: for line in f: bnd1 = "{} {}".format(line.split()[0], line.split()[1]) @@ -48,17 +58,18 @@ def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=Fa bonds_list.append(bnd2) obConversion = openbabel.OBConversion() - obConversion.SetInAndOutFormats("pdb","xyz") + obConversion.SetInAndOutFormats("pdb", "xyz") # ignore ring perception and CONECTs for faster processing - obConversion.AddOption("b", openbabel.OBConversion.INOPTIONS) + obConversion.AddOption("b", openbabel.OBConversion.INOPTIONS) obConversion.AddOption("c", openbabel.OBConversion.INOPTIONS) # read molecule to OBMol object mol = openbabel.OBMol() obConversion.ReadFile(mol, pdb_file) - if not bonds: - mol.ConnectTheDots() # necessary because of the 'b' INOPTION + # if not bonds: + # mol.ConnectTheDots() # necessary because of the 'b' INOPTION n_particles = mol.NumAtoms() + print("Found {} particles in .pdb".format(n_particles)) # get atomic labels from pdb atom_to_lbl = {} @@ -76,10 +87,10 @@ def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=Fa for atom1 in openbabel.OBResidueAtomIter(res): for atom2 in openbabel.OBResidueAtomIter(res): id1, id2 = atom1.GetId(), atom2.GetId() - if id1 == id2: + if id1 == id2: continue if "{} {}".format(atom_to_lbl[id1], atom_to_lbl[id2]) in bonds_list: - mol.AddBond(id1+1, id2+1, 1) + mol.AddBond(id1 + 1, id2 + 1, 1) molecules = mol.Separate() n_molecules = len(molecules) @@ -92,9 +103,16 @@ def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=Fa # write the topology with h5py.File(out_path, "w") as out_file: + if box: + out_file.attrs["box"] = np.array(box) + out_file.attrs["n_molecules"] = n_molecules position_dataset = out_file.create_dataset( "coordinates", - (1, n_particles, 3,), + ( + 1, + n_particles, + 3, + ), dtype="float32", ) types_dataset = out_file.create_dataset( @@ -119,45 +137,73 @@ def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=Fa ) bonds_dataset = out_file.create_dataset( "bonds", - (n_particles, 4,), + ( + n_particles, + 4, + ), dtype="i", ) if charges: charges_dataset = out_file.create_dataset( - "charge", - (n_particles,), - dtype="float32", + "charge", + (n_particles,), + dtype="float32", ) bonds_dataset[...] = -1 for i, atom in enumerate(openbabel.OBMolAtomIter(mol)): indices_dataset[i] = i position_dataset[0, i, :] = np.array( - [a2nm(atom.GetX()), - a2nm(atom.GetY()), - a2nm(atom.GetZ())], - dtype=np.float32 - ) + [a2nm(atom.GetX()), a2nm(atom.GetY()), a2nm(atom.GetZ())], + dtype=np.float32, + ) charges_dataset[i] = charge_dict[atom_to_lbl[atom.GetId()]] molecules_dataset[i] = atom_to_mol[atom.GetId()] names_dataset[i] = np.string_(atom_to_lbl[atom.GetId()]) types_dataset[i] = lbl_to_type[atom_to_lbl[atom.GetId()]] - for j, nbr in enumerate(openbabel.OBAtomAtomIter(atom)): - bonds_dataset[i, j] = nbr.GetId() + if len(bonds_list) != 0: + for j, nbr in enumerate(openbabel.OBAtomAtomIter(atom)): + bonds_dataset[i, j] = nbr.GetId() -if __name__ == '__main__': - description = 'Convert .pdb files to the hymd hdf5 input format' +if __name__ == "__main__": + description = "Convert .pdb files to the hymd hdf5 input format" parser = argparse.ArgumentParser(description=description) - parser.add_argument('pdb_file', type=extant_file, help='input .pdb file name') - parser.add_argument('--charges', type=extant_file, default=None, - help='file containing the charge for each atom type') - parser.add_argument('--bonds', type=extant_file, default=None, - help='file containing the connected beads for each residue') - parser.add_argument('--out', type=str, default=None, dest='out_path', - metavar='file name', help='output hymd HDF5 file name') - parser.add_argument('-f', action='store_true', default=False, dest='force', - help='overwrite existing output file') + parser.add_argument("pdb_file", type=extant_file, help="input .pdb file name") + parser.add_argument( + "--charges", + type=extant_file, + default=None, + help="file containing the charge for each atom type", + ) + parser.add_argument( + "--bonds", + type=extant_file, + default=None, + help="file containing the connected beads for each residue", + ) + parser.add_argument( + "--out", + type=str, + default=None, + dest="out_path", + metavar="file name", + help="output hymd HDF5 file name", + ) + parser.add_argument( + "--box", + type=float, + default=None, + nargs="+", + help="box dimensions", + ) + parser.add_argument( + "-f", + action="store_true", + default=False, + dest="force", + help="overwrite existing output file", + ) args = parser.parse_args() # get basename and file extension @@ -167,4 +213,15 @@ def pdb_to_input(pdb_file, charges=None, bonds=None, out_path=None, overwrite=Fa if ext.lower()[1:] != "pdb": parser.error("pdb_file extension is not .pdb") - pdb_to_input(args.pdb_file, charges=args.charges, bonds=args.bonds, out_path=args.out_path, overwrite=args.force) \ No newline at end of file + if args.box: + if len(args.box) != 3: + parser.error("should provide 3 dimensional box (3 floats)") + + pdb_to_input( + args.pdb_file, + box=args.box, + charges=args.charges, + bonds=args.bonds, + out_path=args.out_path, + overwrite=args.force, + ) diff --git a/utils/plots.py b/utils/plots.py new file mode 100644 index 00000000..16df34d1 --- /dev/null +++ b/utils/plots.py @@ -0,0 +1,797 @@ +import matplotlib +import matplotlib.pyplot as plt +import fileinput +import numpy as np +import math +from argparse import ArgumentParser +import os +import h5py +from scipy.optimize import curve_fit +import warnings + +def PLOT_aVSdensity(parameter, a, folder_path, no_show, out, ax): + ''' + folder_path: path to folder containing parameter (eg:sigma) and a valued folders. + Example: /Users/samiransen23/aparametrization + >ls /Users/samiransen23/aparametrization + sigma=0.238 sigma=0.338 sigma=0.438 + >ls /Users/samiransen23/aparametrization/sigma\=0.238 + a=9.25 a=9.30 a=9.35 + a=9.25 should contain the trajectory sim.h5 + ''' + if len(parameter) == 1: + p1 = [] + for f in os.listdir(folder_path): + if f.startswith(parameter[0]): + p1.append(str(f.split(parameter[0]+'=')[1])) + dp = len(p1[0].split('.')[-1]) + p1 = list(map(float, p1)) + p1.sort() + form = "{:."+str(dp)+"f}" + parameter.extend([form.format(val) for val in list(map(float, p1))]) + if a == 'all': + p1 = [] + for f in os.listdir(folder_path+"/"+parameter[0]+"="+parameter[1]): + p1.append(str(f.split('a=')[1])) + dp = len(p1[0].split('.')[-1]) + p1 = list(map(float, p1)) + p1.sort() + form = "{:."+str(dp)+"f}" + a = [form.format(val) for val in list(map(float, p1))] + V = np.zeros((len(parameter) - 1,len(a))) + density = np.zeros((len(parameter) - 1,len(a))) + for ii in range(len(parameter)-1): + for i in range(len(a)): + #file_path = os.path.abspath("/Users/samiransen23/hymdtest/test_aparametrization/a="+a[i]+"/sim.h5") + #h5md_file = open_h5md_file(file_path) + file_path = os.path.abspath(folder_path+"/"+parameter[0]+"="+parameter[ii+1]+"/a="+a[i]+"/sim.h5") + f = h5py.File(file_path, "r") + N = len(list(f["particles/all/species"])) + try: + V[ii,i] = np.prod(f["particles/all/box/edges/value"][-1].diagonal()) + except: + V[ii,i] = np.prod(f["particles/all/box/edges/value"][-1]) + density[ii,i] = N/V[ii,i] * 0.11955 #gm/cc + f.close() + + ##PLOTS + #fig, (ax1, ax2) = plt.subplots(1,2, figsize=(10,5)) + ax1 = ax[0] + ax2 = ax[1] + fontsize=36 + + #a vs density for different parameter values + a = [float(i) for i in a] + ax1.tick_params(axis='both', labelsize=fontsize-2) + ax1.set_xlabel(r'$a \, \mathrm{(nm^{-3})}$', fontsize=fontsize+2) + ax1.set_ylabel("Density"+ r"$\, \mathrm{(g \, cm^{-3} )}$", fontsize=fontsize+2) + ax1.axhline(y=1, color='0.8', linestyle='--') + for ii in range(len(parameter)-1): + ax1.plot(a, density[ii], marker='.', + linewidth=2.6, label='$\%s=%s$'%(parameter[0], parameter[ii+1])) + ax1.legend(loc='center right', bbox_to_anchor=(-0.26, 0.5), fontsize=fontsize+2) + + #parameter vs a + p = [float(i) for i in parameter[1:]] + a_star = np.zeros(len(parameter) -1) + for ii in range(len(parameter)-1): + m, c = np.polyfit(a, density[ii], deg=1) + a_star[ii] = (1 - c)/m + if (parameter[0]=='kappa'): + ax2.set_xlabel('$\%s$'%(parameter[0] + "\, \mathrm{(mol \, kJ^{-1})}"), + fontsize=fontsize+2) + elif (parameter[0]=='sigma'): + ax2.set_xlabel('$\%s$'%(parameter[0] + "\, \mathrm{(nm)}"), + fontsize=fontsize+2) + else: + ax2.set_xlabel(parameter[0], fontsize=fontsize+2) + ax2.tick_params(axis='both', labelsize=fontsize-2) + ax2.set_ylabel(r'$a^* \, \mathrm{(nm^{-3})}$', fontsize=fontsize+2) + ax2.plot(p, a_star, marker='o', + linewidth=2.6) + + if no_show: + if out: + out = os.path.abspath(out) + else: + out = os.getcwd()+'/fig_temp.pdf' + plt.savefig(out, format='pdf') + else: + plt.show() + + +def PLOT_mVSvolume(m, folder_path): + ''' + This function plots a list of chosen density factors m versus + the equilibrium volume under constant ext pressure. + folder_path: path to folder containing m valued folders. + Example: /Users/samiransen23/out + >ls /Users/samiransen23/out + m=1.0 m=1.1 m=1.2 m=1.3 + m=1.0 should contain the trajectory sim.h5 + ''' + volume = np.zeros(len(m)) + density = np.zeros(len(m)) + areapl = np.zeros(len(m)) + numperleaf = 264 + for ii in range(len(m)): + file_path = os.path.abspath(folder_path+"/m="+m[ii]+"/sim.h5") + f = h5py.File(file_path, "r") + volume[ii] = np.prod(f["particles/all/box/edges/value"][-1]) + N = len(list(f["particles/all/species"])) + density[ii] = N/volume[ii] * 0.11955 #gm/cc + areapl[ii] = np.prod(f["particles/all/box/edges/value"][-1][0:2])/numperleaf + f.close() + + ##PLOTS + [float(i) for i in m] + plt.xlabel("m") + plt.ylabel("volume (nm^3)") + plt.plot(m, volume, marker='o', label="m vs volume") + plt.legend() + plt.show() + plt.xlabel("m") + plt.ylabel("density (gm/cm^3)") + plt.plot(m, density, marker='o', label="m vs density") + plt.legend() + plt.show() + plt.xlabel("m") + plt.ylabel("area per lipid (nm^2)") + plt.plot(m, areapl, marker='o', label="m vs area per lipid") + plt.legend() + plt.show() + +def PLOT_tVSbox(trajectories, first_frame, last_frame, no_show, out, extract_data, + no_density=False, label=None, styles=None): + + def PLOT_single(trajectory, first_frame, last_frame, no_show, out, extract_data, + fontsize, ax, plot2dlist, c_iter, no_density=False, label=None, style=None): + f = h5py.File(trajectory, "r") + box_value = list(f["particles/all/box/edges/value"]) + if last_frame<0: + last_frame = len(list(f['observables/pressure/value']))+last_frame+1 + + try: + x_value = [_[0][0] for _ in box_value[first_frame:last_frame]] + y_value = [_[1][1] for _ in box_value[first_frame:last_frame]] + z_value = [_[2][2] for _ in box_value[first_frame:last_frame]] + time = list(f["particles/all/box/edges/time"])[first_frame:last_frame] + except: + #before commit 36330cec518a7f2f5ee6bcdcebb9e32c6d6b3d93 + x_value = [_[0] for _ in list(f["particles/all/box/edges/value"])[first_frame:last_frame]] + y_value = [_[1] for _ in list(f["particles/all/box/edges/value"])[first_frame:last_frame]] + z_value = [_[2] for _ in list(f["particles/all/box/edges/value"])[first_frame:last_frame]] + time = list(f["particles/all/box/edges/time"])[first_frame:last_frame] + + volume = [x_value[i] * y_value[i] * z_value[i] for i in range(len(x_value))] + N = len(list(f["particles/all/species"])) + f.close() + + density = [0.11955 * N / _ for _ in volume] #gm/cc + stillbox=[] + for i in range(len(x_value)-1): + if(x_value[i] == x_value[i+1]): + stillbox.append("(%s)"%(str(i))) + average_final_box_size = [np.average(x_value[round(0.75*(last_frame-first_frame)):last_frame]), + np.average(y_value[round(0.75*(last_frame-first_frame)):last_frame]), + np.average(z_value[round(0.75*(last_frame-first_frame)):last_frame])] + figtext = "Average final box_size: {0:.3f}, {1:.3f}, {2:.3f}".format(*average_final_box_size) + ##PLOTS + if not no_density: + ax[1].plot(time, density) + ax[1].annotate(figtext, + xy = (1.4, 24), xycoords='figure points', fontsize=11) + if any(val > 9000 for val in time): + time = [val / 1000 for val in time] #ps --> ns + try: + ax[0].set_xlabel("Time (ns)", fontsize=fontsize) + except: + ax.set_xlabel("Time (ns)", fontsize=fontsize) + if not None in styles: + if 'scr' in style.lower(): + cx = cz = '#808080' #gray + ls = 'solid'; lw = 2.0 + else: + ls = 'solid'; lw = 3.4 + if 'nmlberendsen' in style.lower(): + cx = cz = '#002dcd' #bright blue + elif 'omlberendsen' in style.lower(): + cx = cz = '#da0e00' #bright red + else: + cx = next(c_iter) + cz = next(c_iter) + else: + ls = 'solid'; lw = 3.4; cx = next(c_iter); cz = next(c_iter) + + try: + ax[0].plot(time, x_value, label=label[0], color=cx) + ax[0].plot(time, z_value, label=label[1], color=cz) + except: + plot2dlist.append( ax.plot(time, x_value, label=label[0], color=cx, ls=ls, lw=lw,) ) + plot2dlist.append( ax.plot(time, z_value, label=label[1], color=cz, ls=ls, lw=lw,) ) + + print('File: ',trajectory) + print('Last frame:', last_frame, '; box_size: [',x_value[-1],' ',y_value[-1],' ',z_value[-1],']') + print(figtext) + print("Area per lipid for 264/layer:", average_final_box_size[0]*average_final_box_size[1]/264) + if no_show: + if extract_data: + index = trajectory.split('constA_f')[1].split('/')[0] + with open(extract_data,'a') as f: + f.write("{ind}\t{a0:.2f}\t{a1:.2f}\t{a2:.2f}\n".format(ind=index, + a0=average_final_box_size[0], a1=average_final_box_size[1], a2=average_final_box_size[2])) + else: + if(stillbox): print("unchanged box frames:",stillbox) + print('---------') + + #PLOT SETUP + #using matplotlibstyle: /Users/samiransen23/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/mpl-data/stylelib/samiran-signature.mplstyle + c = {'blue':'#001C7F','green':'#017517','golden':'#B8860B','purple':'#633974', + 'red':'#8C0900','teal':'#006374','brown':'#573D1C','orange':'#DC901D', + 'bright red': '#da0e00'} + matplotlib.rcParams['axes.prop_cycle'] = matplotlib.cycler('color',c.values()) + c_iter = iter(matplotlib.rcParams['axes.prop_cycle'].by_key()['color']) + + fontsize = 30 + if no_density: + fig = plt.figure(figsize=(10,10)) + ax = fig.add_subplot(111) + ax.set_xlabel("Time (ps)", fontsize=fontsize+2) + ax.set_ylabel("Length of box (nm)", fontsize=fontsize+2) + ax.tick_params(axis='both', labelsize=fontsize-2) + ax.set_ylim(11.5,18.5) + else: + fig, ax = plt.subplots(2) + ax[0].set_ylabel("Length of box (nm)", fontsize=fontsize) + ax[1].set_xlabel("Time (ps)", fontsize=fontsize) + ax[1].set_ylabel('Density (gm/cc)', fontsize=fontsize) + + if not label: + label = np.array([None, None]) + + plot2dlist = [] + if styles is None: + styles = [None for _ in trajectories] + for trajectory, style in zip(trajectories, styles): + PLOT_single(trajectory, first_frame, last_frame, no_show, out, extract_data, + fontsize, ax, plot2dlist, c_iter, no_density, label, style,) + try: + ax[0].legend(fontsize=fontsize) + except: + ax.legend(["1", "2", "3", "4", "5"], fontsize=fontsize, loc='center left', bbox_to_anchor=(1, 0.5)) + #ax.legend(fontsize=fontsize, loc='center left', bbox_to_anchor=(1, 0.5)) + if no_show: + if out: + out = os.path.abspath(out) + else: + out = os.getcwd()+'/fig_temp.pdf' + plt.tight_layout() + plt.savefig(out, format='pdf') + print('Saving plot to: ',out) + else: + plt.show() + + +def PLOT_tVSpressure(trajectory, first_frame, last_frame, no_show=False): + if len(trajectory) == 1: + trajectory = trajectory[0] + f = h5py.File(trajectory, "r") + value = f['observables/pressure/value'] + if last_frame<0: + last_frame = len(list(f['observables/pressure/value']))+last_frame + time = list(f['observables/pressure/time'] )[first_frame:last_frame] + box_z = [_[2] for _ in list(f["particles/all/box/edges/value"])[first_frame:last_frame]] + + value = np.array(list(value))[first_frame:last_frame] + f.close() + if(len(value[0])==9): + pr = [ + p_kin, p0, p1, + p2x, p2y, p2z, + p_tot_x, p_tot_y, p_tot_z + ] = [value[:,i] for i in range(len(value[0]))] + elif( len(value[0])==18 ): + pr = [ + p_kin, p0, p1, + p2x, p2y, p2z, + p_bond_x, p_bond_y, p_bond_z, + p_angle_x, p_angle_y, p_angle_z, + p_dihedral_x, p_dihedral_y, p_dihedral_z, + p_tot_x, p_tot_y, p_tot_z + ] = [value[:,i] for i in range(len(value[0]))] + elif( len(value[0])==22 ): + pr = [ + p_kin, p0, p1, + p2x, p2y, p2z, + p_w1_0, + p_w1_x, p_w1_y, p_w1_z, + p_bond_x, p_bond_y, p_bond_z, + p_angle_x, p_angle_y, p_angle_z, + p_dihedral_x, p_dihedral_y, p_dihedral_z, + p_tot_x, p_tot_y, p_tot_z + ] = [value[:,i] for i in range(len(value[0]))] + elif( len(value[0])==25 ): + pr = [ + p_kin, p0, p1, + p2x, p2y, p2z, + p_w1_0, + p_w1_1_x, p_w1_2_x, p_w1_1_y, p_w1_2_y, p_w1_1_z, p_w1_2_z, + p_bond_x, p_bond_y, p_bond_z, + p_angle_x, p_angle_y, p_angle_z, + p_dihedral_x, p_dihedral_y, p_dihedral_z, + p_tot_x, p_tot_y, p_tot_z + ] = [value[:,i] for i in range(len(value[0]))] + + #AVERAGE VALUE PRINTS + p_avg = [ np.average(val[round(0.25*(last_frame-first_frame)):last_frame]) for val in [p_tot_x, p_tot_y, p_tot_z] ] + box_z_avg = np.average( box_z[round(0.75*(last_frame-first_frame)):last_frame] ) + factor = 0.1 * 16.6 # 1nm kJ/(mol nm^3) = 1nm × 16.6 bar = 16.6 × 10⁻⁹ m × 10⁸ mN/m^2 = 0.1 × 16.6 mN/m + st = 0.5 * box_z_avg * (p_avg[2] - (p_avg[0]+p_avg[1])/2 ) * factor + p_avg = [val*16.6 for val in p_avg] # 1 kJ/(mol nm^3) = 16.6 bar + print("p_tot_x: {0:.3f}, p_tot_y: {1:.3f}, p_tot_z: {2:.3f} {3:s}".format(*p_avg, "bar")) + #print("p_tot_x: {0:.3f}, p_tot_y: {1:.3f}, p_tot_z: {2:.3f} {3:s}".format(*p_avg, "kJ mol^(-1) nm^(-3)")) + print('Avg final surface tension: ',st, '(mN / m)') + + #PLOTS + color = ['b','g','r','c','m','y','k','brown','gray','orange','purple'] + plt.xlabel('Time (ps)') + #plt.plot(list(time), p_kin, label='p_kin', color=color[0]) + #plt.plot(list(time), p0, label='p0', color=color[1]) + #plt.plot(list(time), p1, label='p1', color=color[2]) + + #plt.plot(list(time), (p2x + p2y)/2, label='p2 in x,y', color=color[3]) + #plt.plot(list(time), p2x, label='p2x', color=color[3]) + #plt.plot(list(time), p2y, label='p2y', color=color[4]) + #plt.plot(list(time), p2z, label='p2z', color=color[5]) + + if len(value[0])==25: + #plt.plot(list(time), p_w1_0, label='p_w1_0', color=color[6]) + #plt.plot(list(time), (p_w1_1_x + p_w1_1_y)/2, label='p_w1_1 in x,y', color=color[0]) + #plt.plot(list(time), (p_w1_2_x + p_w1_2_y)/2, label='p_w1_2 in x,y', color=color[1]) + #plt.plot(list(time), p_w1_1_z, label='p_w1_1 in z', color=color[2]) + #plt.plot(list(time), p_w1_2_z, label='p_w1_2 in z', color=color[3]) + pass + elif len(value[0])==22: + #plt.plot(list(time), p_w1_0, label='p_w1_0', color=color[6]) + #plt.plot(list(time), (p_w1_x + p_w1_y)/2 , label='p_w1 in x,y', color=color[7]) + #plt.plot(list(time), p_w1_z, label='p_w1_z', color=color[9]) + pass + #plt.plot(list(time), p2z - (p2x+p2y)/2, label='p_field_N - p_field_L', color=color[2]) + + #plt.plot(list(time), (p_bond_x+p_bond_y)/2, label='Avg p_bond in x,y', color=color[6]) + #plt.plot(list(time), p_bond_x, label='p_bond_x', color=color[6]) + #plt.plot(list(time), p_bond_y, label='p_bond_y', color=color[7]) + #plt.plot(list(time), p_bond_z, label='p_bond_z', color=color[8]) + + #plt.plot(list(time), (p_angle_x+p_angle_y)/2, label='Avg p_angle in x,y', color=color[9]) + #plt.plot(list(time), p_angle_x, label='p_angle_x', color=color[9]) + #plt.plot(list(time), p_angle_y, label='p_angle_y') + #plt.plot(list(time), p_angle_z, label='p_angle_z', color=color[10]) + + #plt.plot(list(time), p_bond_z - (p_bond_x+p_bond_y)/2, label='p_bond_N - p_bond_L', color=color[0]) + #plt.plot(list(time), p_angle_z - (p_angle_x+p_angle_y)/2, label='p_angle_N - p_angle_L', color=color[1]) + + #plt.plot(list(time), p_bond_z + p_angle_z - (p_bond_x+p_bond_y)/2 - (p_angle_x+p_angle_y)/2, label='p_(bond+angle)_N + - p_(bond+angle)_L', color=color[9]) + + #plt.plot(list(time), (p_tot_x+p_tot_y)/2, label='p_total in x,y') + #plt.plot(list(time), p_tot_z, label='p_total in z') + plt.plot(list(time), (p_tot_x+p_tot_y)/2*16.6, label='p_total in x,y (bar)') + plt.plot(list(time), p_tot_z*16.6, label='p_total in z (bar)') + plt.legend(loc='upper center', ncol = 3, fontsize = 'small') + if no_show: + plt.savefig('tVSp.pdf', format='pdf') + else: + plt.show() + +def PLOT_areaVSsurfacetension(trajectory, first_frame, last_frame, no_show, out, + extract_data, index=1): + f = h5py.File(trajectory, "r") + pressure = f['observables/pressure/value'] + if last_frame<0: + last_frame = len(list(pressure))+last_frame + time = list(f['observables/pressure/time'] )[first_frame:last_frame] + box_value = list(f["particles/all/box/edges/value"]) + pressure = np.array(list(pressure))[first_frame:last_frame] + f.close() + + numperleaf = 264 + try: + x_value = [_[0][0] for _ in box_value[first_frame:last_frame]] + y_value = [_[1][1] for _ in box_value[first_frame:last_frame]] + z_value = [_[2][2] for _ in box_value[first_frame:last_frame]] + except: + #before commit 36330cec518a7f2f5ee6bcdcebb9e32c6d6b3d93 + x_value = [_[0] for _ in box_value[first_frame:last_frame]] + y_value = [_[1] for _ in box_value[first_frame:last_frame]] + z_value = [_[2] for _ in box_value[first_frame:last_frame]] + + [ + p_tot_x, p_tot_y, p_tot_z + ] = [pressure[:,i] for i in range(-3, 0)] + p_N = p_tot_z + p_L = ( p_tot_x + p_tot_y )/2 + #gamma: surface tension per interface + factor = 0.1 * 16.6 # 1nm kJ/(mol nm^3) = 1nm × 16.6 bar = 16.6 × 10⁻⁹ m × 10⁸ mN/m^2 = 0.1 × 16.6 mN/m + gamma = [0.5 * z_value[i] * (p_N[i] - p_L[i]) * factor for i in range(len(z_value))] + #A: area per lipid + A = [ x_value[_] * y_value[_] / numperleaf for _ in range(len(x_value)) ] + #PLOTS + ax = plt.figure().add_subplot(111) + plt.xlabel('Area per lipid ( nm^2 )') + plt.ylabel('Surface tension ( mN/m )') + plt.scatter(A, gamma) + #fit_straight(A, gamma, ax) + if no_show: + if out: + out = os.path.abspath(out) + else: + out = os.getcwd()+'fig_temp.pdf' + plt.savefig(out, format='pdf') + if extract_data: + print('Writing data from:',trajectory) + with open(extract_data,'a') as f: + f.write("{0:d}\t{1:.3f}\t{2:.3f}\t{3:.3f}\n".format(index,np.average(A), np.average(gamma), np.std(gamma))) + else: + #PRINTS + print('%s\t%s%.3f%s'%('Avg area per lipid', r'A=', np.average(A), r' (nm^2)')) + print('%s\t%s%.3f%s'%('Avg surface tension', r'<γ>=', np.average(gamma), r' (mN/m)')) + print('%s%s\t%.3f%s'%('Std deviation of ', r'γ', np.std(gamma), r' (mN/m)')) + plt.show() + +def fit_straight(x, y, ax, ind, col): + y_coord = [0.2, 0.1] + label = ['Fit set 1', 'Fit set 2'] + #col = ['#da0e00','#002dcd'] + m, c = np.polyfit( x, y, deg=1) + line = [(m * xval + c) for xval in x] + figtext="" + #print('hardcoded 0.64 multiplying factor') + #figtext = '%s%d%s%.3f'%(r'$m$',ind,':', m) + #text = 'm=%.2f c=%.2f\n %s'%(m, c, figtext) + #ax.text(0.6, 0.9, text , ha='center', va='center', transform = ax.transAxes) + #ax.vlines(-c/m, plt.ylim()[0], 0, linestyle = 'dotted', color = 'y') + ax.plot(x, line, linewidth = 1.6, linestyle = '--', + color = col, label=label[ind]) + #ax.text(0.95, y_coord[ind], figtext, + # verticalalignment='bottom', horizontalalignment='right', + # transform=ax.transAxes, + # fontsize=14) + print('Slope for ',label[ind],': ',m) + return m,c + +def fit_sigmoid(x, y, ax, species): + def sigmoid(x, a, b, y_m): + return y_m / (1.0 + np.exp(-a*(x-b))) + start_x = 0 + end_x = 10 + start_index = np.where(np.round(x,0) == start_x)[0][0] + end_index = np.where(np.round(x,0) == end_x)[0][-1] + if species == 'C': + y_m_guess = np.average(y[(end_index - 2) : end_index]) + elif species == 'W': + y_m_guess = np.average(y[start_index : (start_index+2)]) + b_guess = x[int((start_index + end_index) / 2)] + popt, pcov = curve_fit(sigmoid, x[start_index : end_index], y[start_index : end_index], p0 = [1, b_guess, y_m_guess]) + print('popt: a=',popt[0],'; b=',popt[1],'; y_m=',popt[2]) + xfit = np.linspace(x[start_index] , x[end_index], 100) + yfit = sigmoid( xfit , popt[0], popt[1], popt[2]) + ax.plot(xfit, yfit, label='fit') + #ax.plot( x[start_index : end_index] , yfit ) + #write fitted data to file + with open('fit_%s.dat'%(species),'w') as f: + [ f.write(str(xfit[i])+' '+str(yfit[i])+'\n') for i in range(len(xfit)) ] + +def fit_fromfile(ax, file, own_scale, ind): + #c = ['#5b5b5b','#375bcf'] + c = ['#002dcd', '#B8860B'] + label = ['$hPF_\mathrm{lit}$','$UA_\mathrm{ref}$',] + linestyle = ["dashed","dotted"] + with open(file, 'r') as f: + data = f.read() + data = data.split('\n')[0:-1] + index = []; x = []; y = [] + pr_unit = data[0].split()[1].split('(')[1].split(')')[0] + for line in data[1:]: + f_list = [float(i) for i in line.split()] + x.append(f_list[0]) + y.append(f_list[1]) + if own_scale=='True': + ax2 = ax.twinx() + ax2.set_ylabel('('+pr_unit+')', color='#006374') + ax2.scatter(x, y, marker='.', color='#006374', s=8.2, label='fit') + else: + if ind == 0: + ax.plot(x, y, linestyle=linestyle[ind], linewidth=2.7, color=c[ind], label=label[ind]) + else: + ax.scatter(x, y, marker='.', color=c[ind], s=36, label=label[ind]) + + + + +def PLOT_pressure_profile(trajectory, first_frame, last_frame, path, terms, box_scale, config, own_scale, + dir='z', no_show=False, out=None, sqgrad=False, labels=None, axis=None, fit=None, + legend_loc = None, yaxis=None,): + """ + Parameters + ---------- + terms: list, str + Names of pressure terms to plot: + 'p_kin', 'p0', 'p1', etc. as named in list pr_names; + Other possible terms: + 'diff-field': plots the difference between normal and lateral components of field pressure + 'diff-bond': plots the difference between normal and lateral components of bonded pressure + 'diff-angle': plots the difference between normal and lateral components of angle (bonded) pressure + 'diff-total': plots the difference between normal and lateral components of total pressure + 'all': plots all terms in list pr_names + """ + def plot(box_avg, pr_avg, no_show, sqgrad, terms, box_scale, yaxis, label='',): + #units + #pr_avg is in kJ/(mol nm^3) + #to plot pr in bar: + #1 kJ/(mol nm^3) = 16.61 bar + pr_avg = 16.61 * pr_avg + pr_unit = 'bar' + #make pressure dictionary + pr_names = ['p_kin','p0','p1', + 'p2x','p2y','p2z', + 'pW1_0','pW1_1x','pW1_2x', + 'pW1_1y','pW1_2y', 'pW1_1z','pW1_2z', + 'p_bond_x', 'p_bond_y', 'p_bond_z', + 'p_angle_x', 'p_angle_y', 'p_angle_z', + 'p_dihedral_x', 'p_dihedral_y', 'p_dihedral_z', + 'p_tot_x', 'p_tot_y', 'p_tot_z' + ] + pr_dict = {} + terms_individual = [] + terms_derived = [] + for i in range(25): + pr_dict[pr_names[i]] = pr_avg[i] + for term in terms: + if term in pr_dict: + terms_individual.append(term) + else: + terms_derived.append(term) + if 'all' in terms_derived: + terms_individual = pr_names + + #SIMPLE CALCULATIONS TO PLOT + if sqgrad: + pr_iso = np.sum(pr_avg[0:3], axis=0) + pr_avg[6] + pr_field_N = pr_iso + pr_avg[5] + pr_avg[11] + pr_avg[12] + pr_field_L = pr_iso + (pr_avg[3] + pr_avg[4])/2 + np.sum(pr_avg[7:11], axis=0)/2 + else: + pr_iso = np.sum(pr_avg[0:3], axis=0) + pr_field_N = pr_iso + pr_avg[5] + pr_field_L = pr_iso + (pr_avg[3] + pr_avg[4])/2 + + #PLOTS + #using matplotlibstyle: /Users/samiransen23/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/mpl-data/stylelib/samiran-signature.mplstyle + c = {'blue':'#001C7F', + 'green':'#017517', + 'golden':'#B8860B', + 'purple':'#633974', + 'red':'#8C0900', + 'teal':'#006374', + 'brown':'#573D1C', + 'orange':'#DC901D'} + matplotlib.rcParams['axes.prop_cycle'] = matplotlib.cycler('color',c.values()) + + if box_scale is not None: + xaxis = np.linspace(-int(box_scale)/2, int(box_scale)/2, len(pr_avg[0])) + else: + xaxis = np.linspace(-box_avg[2]/2, box_avg[2]/2, len(pr_avg[0])) + if yaxis: + ax.set_ylim([float(yaxis[0]), float(yaxis[1])]) + + for term in terms_individual: + ax.plot(xaxis, pr_dict[term], label=term) + #ax.plot(xaxis, pr_avg[1], color=c['darkblue'], label='p0') + #ax.plot(xaxis, pr_avg[2], color=c['B'], label='p1') + #ax.plot(xaxis, pr_avg[3], color='r', label='p2x') + #ax.plot(xaxis, pr_avg[5], color=c['C'], label='p2z') + #ax.plot(xaxis, pr_avg[6], color=c['D'], label='pW1_0') + #ax.plot(xaxis, pr_z_sum[7]+pr_z_sum[8], label='pW1_1x+pW1_2x') + #ax.plot(xaxis, pr_z_sum[9]+pr_z_sum[10], label='pW1_1y+pW1_2y') + #ax.plot(xaxis, pr_avg[11]+pr_avg[12], color=c['E'], label='pW1_1z+pW1_2z') + #plt.legend() + #plt.show() + #ax = plt.figure().add_subplot(111) + #ax.plot(xaxis, (pr_avg[13]+pr_avg[14])/2, color=c['blue'], label=r'$P_{x,y}^{bond}$') + #ax.plot(xaxis, pr_avg[15], color=c['orange'], label=r'$P_{z}^{bond}$') + #ax.plot(xaxis, (pr_avg[16]+pr_avg[17])/2, color=c['blue'], label=r'$P_{x,y}^{angle}$') + #ax.plot(xaxis, pr_avg[18], color=c['orange'], label=r'$P_{z}^{angle}$') + #ax.plot(xaxis, (pr_avg[22]+pr_avg[23])/2, color=c['blue'], label='Total p in x,y') + #ax.plot(xaxis, pr_avg[24], c['orange'], label='Total p in z') + for term in terms_derived: + if term=='diff-field': + ax.plot(xaxis, pr_field_N - pr_field_L, color=c['blue'], linewidth=3.2, + #label=r'$(P_{N}^{field} - P_{L}^{field})$ '+label) + label = r'$Δ P_{\mathrm{field}}$ ') + if term=='diff-bond': + ax.plot(xaxis, pr_avg[15] - (pr_avg[13]+pr_avg[14])/2, color=c['green'], linewidth=3.2, + #label=r'$(P_{N}^{bond} - P_{L}^{bond})$') + label = r'$Δ P_{\mathrm{bond}}$ ') + if term=='diff-angle': + ax.plot(xaxis, pr_avg[18] - (pr_avg[16]+pr_avg[17])/2, color=c['golden'], linewidth=3.2, + #label=r'$(P_{N}^{angle} - P_{L}^{angle})$') + label = r'$Δ P_{\mathrm{angle}}$ ') + if term=='diff-total': + label = 'HhPF' if fit else r'$\Delta P_{\mathrm{tot}}$' + ax.plot(xaxis, pr_avg[24] - (pr_avg[22]+pr_avg[23])/2, + color = '#da0e00', #bright red + #color=c['red'], + lw=3.2, label=label) + if term=='pr_field_L': + ax.plot(xaxis, pr_field_L, #color=c['blue'], + linestyle='dashed', label=r'$P_{L}^{field}$'+label) + if term=='pr_field_N': + ax.plot(xaxis, pr_field_N, #color=c['orange'], + linestyle='dashed', label=r'$P_N^{field}$'+label) + ax.tick_params(axis='both', labelsize=fontsize-2) + if fit: + ax.set_xlabel('Position along normal '+r"$\mathrm{(nm)}$", fontsize=fontsize) + else: + ax.set_xlabel('Position along normal '+r"$\mathrm{(nm)}$", fontsize=fontsize) + if(len(terms)==1): + if(terms[0]=='diff-total'): + ax.set_ylabel(r'$\Delta P_{\mathrm{tot}}$'+ r'$\;\mathrm{( %s )}$'%(str(pr_unit)), fontsize=fontsize) + else: + ax.set_ylabel(terms[0]+ ' \mathrm{( '+ pr_unit + ' )}', fontsize=fontsize) + else: + ax.set_ylabel(r'$\Delta P \;\mathrm{( '+ pr_unit + ' )}$', fontsize=fontsize) + + def pr_profile_single(trajectory, first_frame, last_frame, + sqgrad, dir='z', config=None): + if dir == 'z': + dir = 2 + f = h5py.File(trajectory, "r") + pressure = f['observables/pressure/value'] + if last_frame<0: + last_frame = len(list(pressure))+last_frame + box_value = list(f["particles/all/box/edges/value"]) + pressure = np.array(list(pressure))[first_frame:last_frame+1] + f.close() + + #selecting first frame only + pressure = pressure[0] + pr_avg = np.zeros([25,pressure.shape[1]]) + box_value = [box_value[0][i][i] for i in range(3)] + #sum over all x,y + #NON-BONDED TERMS + for i in range(13): + pr_avg[i] = np.sum(pressure[i], axis=(0,1)) + #BONDED TERMS + for i in range(13,22): + pr_avg[i] = pressure[i][0,0,:] + #TOTAL PRESSURE + if sqgrad: + pr_iso = np.sum(pr_avg[0:3], axis=0) + pr_avg[6] + x_ind = [3,7,8, 13,16,19] + y_ind = [4,9,10, 14,17,20] + z_ind = [5,11,12, 15,18,21] + else: + pr_iso = np.sum(pr_avg[0:3], axis=0) + x_ind = [3,13,16,19] + y_ind = [4,14,17,20] + z_ind = [5,15,18,21] + pr_avg[22] += pr_iso + pr_avg[23] += pr_iso + pr_avg[24] += pr_iso + for i,j,k in zip(x_ind,y_ind,z_ind): + pr_avg[22] += pr_avg[i] + pr_avg[23] += pr_avg[j] + pr_avg[24] += pr_avg[k] + + #Normalization when slicing z (to maintain intensiveness) + if config: + import toml + config = toml.load(config) + N_z = config['field']['mesh_size'][dir] + else: + warnings.warn( + r"No config file provided. Grid points could not "\ + "be determined", + ) + + pr_avg = N_z * pr_avg + + return pr_avg, box_value + + fontsize = 32 + if path: + paths=[] + if isinstance(path, str): + paths.append(path) + else: + paths = path + #samiran_signature = plt.style.use('samiran-signature') + if not axis: + fig = plt.figure(figsize=(10,7)) + ax = fig.add_subplot(111) + pr_trajs = [] + box_trajs = [] + for path in paths: + extracted_frames = [int(f.split('pressure')[1].split('.h5')[0]) + for f in os.listdir(path) + ] + pr_traj = [] + box_traj = [] + for val in extracted_frames: + trajectory = os.path.join(os.path.abspath(path),'pressure'+str(val)+'.h5') + pr_avg, box_value = pr_profile_single(trajectory, first_frame, last_frame, sqgrad, config=config) + pr_traj.append(pr_avg) + box_traj.append(box_value) + pr_trajs.append(np.average( np.array(pr_traj), axis=0 )) + box_trajs.append(np.average( np.array(box_traj), axis=0 )) + for i in range(len(paths)): + plot(box_trajs[i], pr_trajs[i], no_show=no_show, sqgrad=sqgrad, terms=terms, + box_scale=box_scale, yaxis=yaxis,) + elif trajectory: + pr_avg, box_value = pr_profile_single(trajectory, first_frame, last_frame, sqgrad) + plot(box_value, pr_avg, no_show=no_show, sqgrad=sqgrad, terms=terms, + box_scale=box_scale) + + if fit: + fits = [] + if isinstance(fit, str): + fits.append(fit) + else: + fits = fit + for fit,own_scale_val in zip(fits,own_scale): + fit_fromfile(ax, fit, own_scale_val, fits.index(fit)) + if fit: + lgnd = fig.legend(loc=legend_loc, fontsize=fontsize-2, markerscale = 4) + else: + lgnd = fig.legend(loc=legend_loc, fontsize=fontsize, markerscale = 4) + plt.tight_layout() + if no_show: + if out: + out = os.path.abspath(out) + else: + out = os.getcwd()+'fig_temp.pdf' + plt.savefig(out, format='pdf') + else: + plt.show() + +if __name__ == "__main__": + ap = ArgumentParser() + ap.add_argument("--traj", default=None, nargs='+', help="sim.h5") + ap.add_argument("--first", type=int, default=0, help="first frame for plot. Eg: 0") + ap.add_argument("--last", type=int, default=-1, help="last frame for plot. Eg: -1") + ap.add_argument("--config", default=None, help="config.toml") + ap.add_argument("--path", default=None, help="path of folder containing folders/files as reqd. See inside function") + ap.add_argument("--plot_aVSdensity", action='store_true') + ap.add_argument("--plot_mVSvolume", action='store_true') + ap.add_argument("--plot_tVSpressure", action='store_true') + ap.add_argument("--plot_tVSbox", action='store_true') + ap.add_argument("--plot_areaVSsurfacetension", action='store_true') + ap.add_argument("--plot_pressure_profile", action='store_true') + ap.add_argument("--no_show", action='store_true', help='Do not show plot. Save figure instead') + ap.add_argument("--no_density", action='store_true', help='Do not show time vs density plot.') + ap.add_argument("--parameter", type=str, nargs="+", default=None, help="If parameter is sigma: sigma 0.238 0.338 0.438") + ap.add_argument("--a", type=str, nargs="+", default='all', help="9.25 9.30 9.35") + ap.add_argument("--m", type=str, nargs="+", default=None, help="1.0 1.1 1.2") + ap.add_argument("--out", default=None, help="pathname of figure to be saved") + ap.add_argument("--sqgrad", action='store_true', help='include sq grad pressure terms in plot') + ap.add_argument("--terms", type=str, nargs="+", default='all', help="See description of PLOT_pressure_profile") + ap.add_argument("--yaxis", type=str, nargs="+", default=None, help="-600 400") + ap.add_argument("--box_scale", type=float, default=None, help='plot pr profiles with box length in z scaled to this value') + ap.add_argument("--own_scale", nargs="+", default='False', help='plot the fitting fn(s) with its own vertical axis') + ap.add_argument("--extract_data", type=str, default=None, help='save average final box in a file in this path') + ap.add_argument("--fit", default=None, nargs="+", help='path(str) or paths(ndarray) to file with data to fit with') + ap.add_argument("--legend_loc", type=float, default=None, nargs="+", help='legend location given by (x,y) coordinates in ([0,1],[0,1]) limits') + ap.add_argument("--label", default=None, nargs="+", help='custom label in order of plots') + ap.add_argument("--style", default=None, nargs='+', help='berendsen scr: list of barostat types for tVSbox plot for multiple files') + args = ap.parse_args() + if args.plot_aVSdensity: PLOT_aVSdensity(args.parameter, args.a, args.path, args.no_show, args.out) + if args.plot_mVSvolume: PLOT_mVSvolume(args.m, args.path) + if args.plot_tVSpressure: PLOT_tVSpressure(args.traj, args.first, args.last, no_show=args.no_show) + if args.plot_tVSbox: + PLOT_tVSbox(args.traj, args.first, args.last, no_show=args.no_show, out=args.out, extract_data=args.extract_data, + no_density=args.no_density, label=args.label, styles=args.style) + if args.plot_areaVSsurfacetension: PLOT_areaVSsurfacetension(args.traj, args.first, args.last, no_show=args.no_show, out=args.out, extract_data=args.extract_data) + if args.plot_pressure_profile: + PLOT_pressure_profile(args.traj, args.first, args.last, args.path, + no_show=args.no_show, out=args.out, sqgrad=args.sqgrad, terms=args.terms, + box_scale=args.box_scale, config=args.config, fit=args.fit, own_scale=args.own_scale, + legend_loc=args.legend_loc, yaxis=args.yaxis,) diff --git a/utils/read_parameter_file.py b/utils/read_parameter_file.py deleted file mode 100644 index ed1d8442..00000000 --- a/utils/read_parameter_file.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import argparse - - -def is_float(string): - try: - float(string) - except (ValueError, TypeError): - return False - return True - - -def read_bounds_file(file_path, verbose=False): - if not (os.path.exists(file_path) and os.path.isfile(file_path)): - raise FileNotFoundError(f"Could not find file {file_path}.") - - with open(file_path, 'r') as in_file: - contents = in_file.readlines() - if verbose: - print(f"Reading file '{file_path}' in directory '{os.getcwd()}':") - print(" |------------------------------------------------------") - for i, line in enumerate(contents): - print(f"{i+1:>3}|", line.rstrip('\n')) - print(" |------------------------------------------------------") - parameters, lower_bounds, upper_bounds = [], [], [] - - for i, line in enumerate(contents): - line = line.rstrip('\n') - if not line.isspace() and not len(line) == 0: - sline = line.split() - if len(sline) != 3: - raise SyntaxError(f"Format of line nr. {i+1} '{line}' of file " - f"'{file_path}' in directory '{os.getcwd()}'" - f" not understood. Expected format 'PARAM " - f"LOWER_BOUND UPPER_BOUND'.") - if (not is_float(sline[1])) or (not is_float(sline[2])): - raise ValueError(f"Lower or upper bound '{sline[1]}' or " - f"'{sline[2]}' could not be parsed as a " - f"number.") - if not len(sline[0].split(',')) == 2: - raise ValueError(f"Expected two comma separated strings (no " - f"spaces) for parameter name, not " - f"'{sline[0]}' in line nr. {i+1}.") - - parameters.append(sline[0]) - lower_bounds.append(float(sline[1])) - upper_bounds.append(float(sline[2])) - return parameters, lower_bounds, upper_bounds - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description=( - "Read and parse Bayesian optimization bounds file" - ) - ) - parser.add_argument( - "--bounds-file", type=str, default="bounds.txt", metavar="file name", - help="input file path (default 'bounds.txt')", - ) - parser.add_argument( - "--out", type=str, default="parameters.txt", metavar="file name", - help="output file path (default 'parameters.txt')", - ) - parser.add_argument( - "--silent", action="store_true", default=False, - help="dont print any output", - ) - args = parser.parse_args() - file_path = os.path.abspath(args.bounds_file) - out_file_path = os.path.abspath(args.out) - - parameters, lower_bounds, upper_bounds = read_bounds_file( - file_path, verbose=not args.silent, - ) - - with open(out_file_path, 'w') as out_file: - for parameter in parameters: - out_file.write(f"{parameter} ") - - with open(out_file_path, 'r') as in_file: - contents = in_file.readlines() - - if not args.silent: - print(f"Writing file '{out_file_path}' in directory '{os.getcwd()}':") - print(" |------------------------------------------------------") - for i, line in enumerate(contents): - print(f"{i+1:>3}|", line.rstrip('\n')) - print(" |------------------------------------------------------") diff --git a/utils/split_gro_molecules.py b/utils/split_gro_molecules.py index ed300bba..b8e0025a 100644 --- a/utils/split_gro_molecules.py +++ b/utils/split_gro_molecules.py @@ -1,6 +1,7 @@ import os import subprocess import argparse +import numpy as np # unnecessary import ? def split_gro_molecules(path, out_path, overwrite=False): diff --git a/utils/write_to_opt_file_parallel.py b/utils/write_to_opt_file_parallel.py deleted file mode 100644 index c6ecc0c5..00000000 --- a/utils/write_to_opt_file_parallel.py +++ /dev/null @@ -1,99 +0,0 @@ -import os -import sys -import argparse -import warnings -import numpy as np - - -def write_to_opt_file( - fitness_file_paths, - parameters_file_path, - point_file_path, - opt_file_path -): - with open(parameters_file_path, "r") as parameters_file: - parameters = parameters_file.readline().split() - with open(point_file_path, "r") as point_file: - point = [float(p) for p in point_file.readline().split()] - - print("Writing fitness data from files:") - print(" |------------------------------------------------------") - for i, f in enumerate(fitness_file_paths): - print(f"{i+1:>3}| {os.path.abspath(f):>5} ") - print(" |------------------------------------------------------") - - total_fitnesses = [] - for f in fitness_file_paths: - if os.path.exists(f): - with open(f, "r") as fitness_file: - total_fitness = fitness_file.readlines()[3:-1][-1] - total_fitness = [float(f_) for f_ in total_fitness.split()[1:]] - total_fitnesses.append(total_fitness) - - N = len(total_fitnesses) # number of files - if N == 0: - warn_str = ( - f"No fitness data found in paths {fitness_file_paths}" - ) - warnings.warn(warn_str) - sys.exit(0) - M = len(total_fitnesses[0]) # number of metrics - total_fitnesses_array = np.empty(shape=(N, M,), dtype=np.float64) - for i in range(N): - for j in range(M): - total_fitnesses_array[i, j] = total_fitnesses[i][j] - - new_file = False - if not os.path.exists(opt_file_path): - new_file = True - open_key = "w" - else: - open_key = "a" - - with open(opt_file_path, open_key) as out_file: - print(f"Writing to opt. data file: {os.path.abspath(opt_file_path)}") - if new_file: - for parameter in parameters: - out_file.write(f"{parameter:>15}") - for f in ("MSE", "RMSE", "MAE", "MAPE", "R2", "SMAPE"): - out_file.write(f"{f:>25}") - out_file.write("\n") - - for p in point: - out_file.write(f"{p:>15.10f}") - for i in range(M): - f = np.mean(total_fitnesses_array[:, i]) - out_file.write(f"{f:>25.15g}") - out_file.write("\n") - - -if __name__ == '__main__': - parser = argparse.ArgumentParser( - description=( - "Write data from multiple fitness files to the collective opt-file" - ) - ) - parser.add_argument( - "--fitness-files", type=str, default=None, metavar="file names", - nargs="+", dest="fitness_files", required=True, - help="multiple input fitness file paths", - ) - parser.add_argument( - "--parameters-file", type=str, default=None, metavar="file name", - dest="parameters_file", help="parameters file path", required=True, - ) - parser.add_argument( - "--point-file", type=str, default=None, metavar="file name", - dest="point_file", help="next point file path", required=True, - ) - parser.add_argument( - "--opt-file", type=str, default="opt_data.txt", metavar="file name", - dest="opt_file", help="output opt data file (default opt_data.txt)", - ) - args = parser.parse_args() - write_to_opt_file( - args.fitness_files, - args.parameters_file, - args.point_file, - args.opt_file - )