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
- )