feat: add dice input generation and player routines for NVT/NPT simulations

This commit is contained in:
2026-03-14 18:28:15 -03:00
parent 53eb34a83e
commit 9f22304dd8
10 changed files with 534 additions and 51 deletions

View File

@@ -29,6 +29,13 @@ class DiceConfig(BaseModel):
upbuf: int = Field(
360, description="Buffer size for the potential energy calculations"
)
irdf: int = Field(
0,
description="Flag for calculating radial distribution functions (0: no, 1: yes)",
)
vstep: int = Field(
5000, description="Frequency of volume change moves in NPT simulations"
)
combrule: Literal["+", "*"] = Field(
"*", description="Combination rule for the Lennard-Jones potential"
)

View File

@@ -20,7 +20,20 @@ class RoutineType(str, Enum):
class PlayerConfig(BaseModel):
"""
Data Transfer Object for the player configuration.
Configuration for DICEPlayer simulations.
Attributes:
type: Type of simulation to perform (charge, geometry, or both).
max_cyc: Maximum number of cycles for the geometry optimization.
switch_cyc: Cycle at which to switch from charge to geometry optimization (if type is "both").
mem: Memory configuration for QM calculations.
nprocs: Number of processors to use for QM calculations.
ncores: Number of cores to use for QM calculations.
dice: Configuration parameters specific to DICE simulations.
gaussian: Configuration parameters specific to Gaussian calculations.
altsteps: Number of steps for the alternate simulation (default: 20000).
geoms_file: File name for the geometries output (default: "geoms.xyz").
simulation_dir: Directory name for the simulation files (default: "simfiles").
"""
model_config = ConfigDict(

174
diceplayer/dice/__init__.py Normal file
View File

@@ -0,0 +1,174 @@
"""
DICE Monte Carlo Simulation Interface
=====================================
This package provides utilities for configuring and running simulations with
the DICE Monte Carlo molecular simulation program.
DICE performs statistical sampling of molecular systems using the Metropolis
Monte Carlo algorithm. Simulations are defined by text input files containing
keywords that control the thermodynamic ensemble, system composition, and
simulation parameters.
---------------------------------------------------------------------
Simulation Ensembles
--------------------
DICE supports multiple statistical ensembles.
NVT
Canonical ensemble where the following properties remain constant:
- N: number of molecules
- V: system volume
- T: temperature
The system density is fixed and the simulation box volume does not change
during the simulation.
NPT
Isothermalisobaric ensemble where the following properties remain constant:
- N: number of molecules
- P: pressure
- T: temperature
The simulation box volume is allowed to fluctuate in order to maintain the
target pressure.
---------------------------------------------------------------------
Simulation Stages
-----------------
Simulations are typically executed in multiple stages.
Thermalization (TER)
Initial phase where the system relaxes to the desired thermodynamic
conditions. Molecular configurations stabilize and the system reaches
equilibrium.
During this stage statistical properties are **not accumulated**.
Production / Equilibration (EQ)
Main sampling phase after the system has equilibrated.
Statistical properties such as energies, densities, and radial
distribution functions are collected and configurations may be saved
for later analysis.
---------------------------------------------------------------------
Typical Simulation Pipeline
---------------------------
Two common execution workflows are used.
NVT Simulation
Used when the system density is known.
1. NVT.ter → thermalization at constant density
2. NVT.eq → production sampling
NPT Simulation
Used when the equilibrium density is unknown.
1. NVT.ter → initial thermalization at approximate density
2. NPT.ter → pressure relaxation (volume adjustment)
3. NPT.eq → production sampling at target pressure
---------------------------------------------------------------------
DICE Input Keywords
-------------------
The following keywords are used in the generated input files.
title
Descriptive title printed in the simulation output.
ncores
Number of CPU cores used by the DICE executable.
ljname
File containing Lennard-Jones parameters and molecular topology.
outname
Prefix used for simulation output files.
nmol
Number of molecules of each species in the system.
dens
System density (g/cm³). Used only in NVT simulations or for
initialization of NPT runs.
press
Target pressure used in NPT simulations.
temp
Simulation temperature.
nstep
Number of Monte Carlo cycles executed in the simulation stage.
init
Defines how the simulation initializes molecular coordinates.
yes
Random initial configuration.
no
Continue from a previous configuration.
yesreadxyz
Read coordinates from a previously saved XYZ configuration.
vstep
Frequency of volume-change moves in NPT simulations.
mstop
Molecule displacement control flag used internally by DICE.
accum
Enables or disables accumulation of statistical averages.
iprint
Frequency of simulation information printed to the output.
isave
Frequency at which configurations are written to trajectory files.
irdf
Controls calculation of radial distribution functions.
seed
Random number generator seed used by the Monte Carlo algorithm.
upbuf
Buffer size parameter used internally by DICE during thermalization.
---------------------------------------------------------------------
Output Files
------------
Important output files produced during the simulation include:
phb.xyz
XYZ trajectory containing sampled molecular configurations.
last.xyz
Final configuration of the simulation, often used as the starting
configuration for the next simulation cycle.
---------------------------------------------------------------------
References
----------
DICE is a Monte Carlo molecular simulation program developed primarily
by researchers at the University of São Paulo (USP) for studying liquids,
solutions, and solvation phenomena.
"""

View File

View File

@@ -0,0 +1,186 @@
from diceplayer.config import PlayerConfig
from typing_extensions import Self
import random
from abc import ABC
from dataclasses import dataclass, fields
from typing import Any, Sequence, TextIO
@dataclass(slots=True)
class BaseConfig(ABC):
ncores: int
ljname: str
outname: str
nmol: Sequence[int]
temp: float
seed: int
isave: int
press: float = 1.0
def write_dice_config(self, io_writer: TextIO) -> None:
for field in fields(self):
key = field.name
value = getattr(self, key)
if value is None:
continue
io_writer.write(f"{key} = {self._serialize_value(value)}\n")
io_writer.write("$end\n")
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
base_fields = cls._extract_base_fields(config)
return cls(**base_fields, **kwargs)
@staticmethod
def _extract_base_fields(config: PlayerConfig) -> dict[str, Any]:
seed: int
if config.dice.seed is not None:
seed = config.dice.seed
else:
seed = random.randint(0, 2**32 - 1)
return dict(
ncores=config.ncores,
ljname=config.dice.ljname,
outname=config.dice.outname,
nmol=config.dice.nmol,
temp=config.dice.temp,
seed=seed,
isave=config.dice.isave,
press=config.dice.press,
)
@staticmethod
def _get_nstep(config: PlayerConfig, idx: int) -> int:
if len(config.dice.nstep) > idx:
return config.dice.nstep[idx]
return config.dice.nstep[-1]
@staticmethod
def _serialize_value(value: Any) -> str:
if value is None:
raise ValueError("DICE configuration cannot serialize None values")
if isinstance(value, bool):
return "yes" if value else "no"
if isinstance(value, (list, tuple)):
return " ".join(str(v) for v in value)
return str(value)
# -----------------------------------------------------
# NVT BASE
# -----------------------------------------------------
@dataclass(slots=True)
class NVTConfig(BaseConfig):
title: str = "Diceplayer Run - NVT"
dens: float = 0.0
nstep: int = 0
vstep: int = 0
# -----------------------------------------------------
# NVT THERMALIZATION
# -----------------------------------------------------
@dataclass(slots=True)
class NVTTerConfig(NVTConfig):
title: str = "Diceplayer Run - NVT Thermalization"
upbuf: int = 360
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTTerConfig, cls).from_config(
config,
dens=config.dice.dens,
nstep=cls._get_nstep(config, 0),
upbuf=config.dice.upbuf,
vstep=0,
**kwargs,
)
# -----------------------------------------------------
# NVT PRODUCTION
# -----------------------------------------------------
@dataclass(slots=True)
class NVTEqConfig(NVTConfig):
title: str = "Diceplayer Run - NVT Production"
irdf: int = 0
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTEqConfig, cls).from_config(
config,
dens=config.dice.dens,
nstep=cls._get_nstep(config, 1),
irdf=config.dice.irdf,
vstep=0,
**kwargs,
)
# -----------------------------------------------------
# NPT BASE
# -----------------------------------------------------
@dataclass(slots=True)
class NPTConfig(BaseConfig):
title: str = "Diceplayer Run - NPT"
nstep: int = 0
vstep: int = 5000
# -----------------------------------------------------
# NPT THERMALIZATION
# -----------------------------------------------------
@dataclass(slots=True)
class NPTTerConfig(NPTConfig):
title: str = "Diceplayer Run - NPT Thermalization"
dens: float | None = None
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NPTTerConfig, cls).from_config(
config,
dens=config.dice.dens,
nstep=cls._get_nstep(config, 1),
vstep=config.dice.vstep,
**kwargs,
)
# -----------------------------------------------------
# NPT PRODUCTION
# -----------------------------------------------------
@dataclass(slots=True)
class NPTEqConfig(NPTConfig):
title: str = "Diceplayer Run - NPT Production"
dens: float | None = None
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NPTEqConfig, cls).from_config(
config,
dens=config.dice.dens,
nstep=cls._get_nstep(config, 2),
vstep=config.dice.vstep,
**kwargs,
)

View File

@@ -1,4 +1,4 @@
from diceplayer.config.player_config import PlayerConfig
from diceplayer.config.player_config import PlayerConfig, RoutineType
from diceplayer.logger import logger
from diceplayer.state.state_handler import StateHandler
from diceplayer.state.state_model import StateModel
@@ -14,17 +14,16 @@ class PlayerFlags(TypedDict):
class Player:
def __init__(self, config: PlayerConfig):
self.config = config
self._state_handler = StateHandler(config.simulation_dir)
def play(self, **flags: Unpack[PlayerFlags]):
state_handler = StateHandler(self.config.simulation_dir)
if not flags["continuation"]:
logger.info(
"Continuation flag is not set. Starting a new simulation and deleting any existing state."
)
state_handler.delete()
self._state_handler.delete()
state = state_handler.get(self.config, force=flags["force"])
state = self._state_handler.get(self.config, force=flags["force"])
if state is None:
state = StateModel.from_config(self.config)
@@ -35,7 +34,36 @@ class Player:
logger.info(
f"Starting cycle {state.current_cycle + 1} of {self.config.max_cyc}."
)
step_directory = self.config.simulation_dir / f"{state.current_cycle::02d}"
if not step_directory.exists():
step_directory.mkdir(parents=True)
current_routine = self._fetch_current_routine(state.current_cycle)
if current_routine == RoutineType.CHARGE:
self._charge_opt_routine(state)
elif current_routine == RoutineType.GEOMETRY:
self._geometry_opt_routine(state)
else:
logger.error(f"Invalid routine type: {current_routine}")
return
state.current_cycle += 1
state_handler.save(state)
self._state_handler.save(state)
logger.info("Reached maximum number of cycles. Simulation complete.")
def _fetch_current_routine(self, current_cycle: int) -> RoutineType:
if self.config.type != RoutineType.BOTH:
return self.config.type
if current_cycle < self.config.switch_cyc:
return RoutineType.CHARGE
return RoutineType.GEOMETRY
def _charge_opt_routine(self, state: StateModel) -> None:
pass
def _geometry_opt_routine(self, state: StateModel) -> None:
pass