refactor: update Python version and optimize dice configuration parameters

This commit is contained in:
2026-03-24 23:01:45 -03:00
parent 0763c4a9e1
commit 0470200d00
12 changed files with 228 additions and 233 deletions

View File

@@ -1 +1 @@
3.10
3.12

View File

@@ -10,13 +10,13 @@ diceplayer:
altsteps: 2000
dice:
nprocs: 4
nmol: [1, 1000]
nprocs: 1
nmol: [1, 200]
dens: 1.5
nstep: [2000, 3000]
isave: 0
nstep: [200, 300]
isave: 100
outname: 'phb'
progname: '~/.local/bin/dice'
progname: 'dice'
ljname: 'phb.ljc.example'
randominit: 'always'
seed: 12345

View File

@@ -1,9 +1,8 @@
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import Literal
import random
from pathlib import Path
class DiceConfig(BaseModel):
@@ -38,7 +37,7 @@ class DiceConfig(BaseModel):
)
irdf: int = Field(
0,
description="Flag for calculating radial distribution functions (0: no, 1: yes)",
description="Controls the interval of Monte Carlo steps at which configurations are used at computation of radial distribution functions",
)
vstep: int = Field(
5000, description="Frequency of volume change moves in NPT simulations"
@@ -49,7 +48,7 @@ class DiceConfig(BaseModel):
isave: int = Field(1000, description="Frequency of saving the simulation results")
press: float = Field(1.0, description="Pressure of the system")
temp: float = Field(300.0, description="Temperature of the system")
progname: Path = Field(
progname: str = Field(
"dice", description="Name of the program to run the simulation"
)
randominit: str = Field(

View File

@@ -84,7 +84,7 @@ class DiceHandler:
npt_eq_config = NPTEqConfig.from_config(state.config)
dice.run(npt_eq_config)
results.append(dice.extract_results())
results.append(dice.parse_results(state.system))
@staticmethod
def _generate_phb_file(state: StateModel, proc_directory: Path) -> None:

View File

@@ -1,16 +1,16 @@
from diceplayer.config import PlayerConfig
from diceplayer.logger import logger
from pydantic import BaseModel, Field
from typing_extensions import Self
from abc import ABC
from dataclasses import dataclass, fields
import random
from enum import StrEnum
from pathlib import Path
from typing import Any, Sequence, TextIO
from typing import Annotated, Any, Literal, TextIO
DICE_KEYWORD_ORDER = [
_ALLOWED_DICE_KEYWORD_IN_ORDER = [
"title",
"ncores",
"ljname",
@@ -32,73 +32,190 @@ DICE_KEYWORD_ORDER = [
]
class DiceRoutineType(StrEnum):
NVT_TER = "nvt.ter"
NVT_EQ = "nvt.eq"
NPT_TER = "npt.ter"
NPT_EQ = "npt.eq"
@dataclass(slots=True)
class BaseConfig(ABC):
ncores: int
ljname: str
outname: str
nmol: Sequence[int]
temp: float
seed: int
isave: int
def write(self, directory: Path, filename: str = "input") -> Path:
input_path = directory / filename
if input_path.exists():
logger.info(
f"Dice input file {input_path} already exists and will be overwritten"
)
input_path.unlink()
input_path.parent.mkdir(parents=True, exist_ok=True)
with open(input_path, "w") as io:
self.write_dice_config(io)
return input_path
def write_dice_config(self, io_writer: TextIO) -> None:
values = {f.name: getattr(self, f.name) for f in fields(self)}
for key in DICE_KEYWORD_ORDER:
value = values.pop(key, None)
if value is None:
continue
io_writer.write(f"{key} = {self._serialize_value(value)}\n")
# write any remaining fields (future extensions)
for key, value in values.items():
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]:
return dict(
ncores=int(config.ncores / config.dice.nprocs),
ljname=config.dice.ljname,
outname=config.dice.outname,
nmol=config.dice.nmol,
temp=config.dice.temp,
seed=config.dice.seed,
isave=config.dice.isave,
)
@staticmethod
def _get_nstep(config: PlayerConfig, idx: int) -> int:
def get_nstep(config, idx: int) -> int:
if len(config.dice.nstep) > idx:
return config.dice.nstep[idx]
return config.dice.nstep[-1]
@staticmethod
def get_seed(config) -> int:
return config.dice.seed or random.randint(0, 2**32 - 1)
def get_ncores(config) -> int:
return max(1, int(config.ncores / config.dice.nprocs))
# -----------------------------------------------------
# NVT THERMALIZATION
# -----------------------------------------------------
class NVTTerConfig(BaseModel):
type: Literal[DiceRoutineType.NVT_TER] = DiceRoutineType.NVT_TER
title: str = "NVT Thermalization"
ncores: int
ljname: str
outname: str
nmol: list[int]
dens: float
temp: float
seed: int
init: Literal["yes"] = "yes"
nstep: int
vstep: Literal[0] = 0
isave: int
upbuf: int
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return cls(
ncores=get_ncores(config),
ljname=str(config.dice.ljname),
outname=config.dice.outname,
nmol=config.dice.nmol,
dens=config.dice.dens,
temp=config.dice.temp,
seed=get_seed(config),
nstep=get_nstep(config, 0),
isave=config.dice.isave,
upbuf=config.dice.upbuf,
**kwargs,
)
# -----------------------------------------------------
# NVT PRODUCTION
# -----------------------------------------------------
class NVTEqConfig(BaseModel):
type: Literal[DiceRoutineType.NVT_EQ] = DiceRoutineType.NVT_EQ
title: str = "NVT Production"
ncores: int
ljname: str
outname: str
nmol: list[int]
dens: float
temp: float
seed: int
init: Literal["no", "yesreadxyz"] = "no"
nstep: int
vstep: int
isave: int
irdf: Literal[0] = 0
upbuf: int
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return cls(
ncores=get_ncores(config),
ljname=str(config.dice.ljname),
outname=config.dice.outname,
nmol=config.dice.nmol,
dens=config.dice.dens,
temp=config.dice.temp,
seed=get_seed(config),
nstep=get_nstep(config, 1),
vstep=config.dice.vstep,
isave=max(1, get_nstep(config, 1) // 10),
upbuf=config.dice.upbuf,
**kwargs,
)
# -----------------------------------------------------
# NPT THERMALIZATION
# -----------------------------------------------------
class NPTTerConfig(BaseModel):
type: Literal[DiceRoutineType.NPT_TER] = DiceRoutineType.NPT_TER
title: str = "NPT Thermalization"
ncores: int
ljname: str
outname: str
nmol: list[int]
dens: float
temp: float
press: float
seed: int
init: Literal["yes", "yesreadxyz"] = "yes"
nstep: int
vstep: int
isave: int
upbuf: int
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return cls(
ncores=get_ncores(config),
ljname=str(config.dice.ljname),
outname=config.dice.outname,
nmol=config.dice.nmol,
dens=config.dice.dens,
temp=config.dice.temp,
press=config.dice.press,
seed=get_seed(config),
nstep=get_nstep(config, 1),
vstep=max(1, config.dice.vstep),
isave=config.dice.isave,
upbuf=config.dice.upbuf,
**kwargs,
)
# -----------------------------------------------------
# NPT PRODUCTION
# -----------------------------------------------------
class NPTEqConfig(BaseModel):
type: Literal[DiceRoutineType.NPT_EQ] = DiceRoutineType.NPT_EQ
title: str = "NPT Production"
ncores: int
ljname: str
outname: str
nmol: list[int]
dens: float
temp: float
press: float
seed: int
init: Literal["yes", "yesreadxyz"] = "yes"
nstep: int
vstep: int
isave: int
irdf: Literal[0] = 0
upbuf: int
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return cls(
ncores=get_ncores(config),
ljname=str(config.dice.ljname),
outname=config.dice.outname,
nmol=config.dice.nmol,
dens=config.dice.dens,
temp=config.dice.temp,
press=config.dice.press,
seed=get_seed(config),
nstep=get_nstep(config, 2),
vstep=config.dice.vstep,
isave=max(1, get_nstep(config, 2) // 10),
upbuf=config.dice.upbuf,
**kwargs,
)
DiceInputConfig = Annotated[
NVTTerConfig | NVTEqConfig | NPTTerConfig | NPTEqConfig,
Field(discriminator="type"),
]
def _serialize_value(value: Any) -> str:
if value is None:
raise ValueError("DICE configuration cannot serialize None values")
@@ -112,140 +229,29 @@ class BaseConfig(ABC):
return str(value)
# -----------------------------------------------------
# NVT BASE
# -----------------------------------------------------
def write_dice_config(obj: DiceInputConfig, io_writer: TextIO) -> None:
values = {f: getattr(obj, f) for f in obj.__class__.model_fields}
for key in _ALLOWED_DICE_KEYWORD_IN_ORDER:
value = values.pop(key, None)
if value is None:
continue
io_writer.write(f"{key} = {_serialize_value(value)}\n")
io_writer.write("$end\n")
@dataclass(slots=True)
class NVTConfig(BaseConfig):
title: str = "Diceplayer Run - NVT"
dens: float = ...
nstep: int = ...
vstep: int = 0
def write_config(config: DiceInputConfig, directory: Path) -> Path:
input_path = directory / config.type
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTConfig, cls).from_config(
config,
dens=config.dice.dens,
nstep=cls._get_nstep(config, 0),
if input_path.exists():
logger.info(
f"Dice input file {input_path} already exists and will be overwritten"
)
input_path.unlink()
input_path.parent.mkdir(parents=True, exist_ok=True)
with open(input_path, "w") as io:
write_dice_config(config, io)
# -----------------------------------------------------
# NVT THERMALIZATION
# -----------------------------------------------------
@dataclass(slots=True)
class NVTTerConfig(NVTConfig):
title: str = "Diceplayer Run - NVT Thermalization"
upbuf: int = 360
init: str = "yes"
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTTerConfig, cls).from_config(
config,
nstep=cls._get_nstep(config, 0),
upbuf=config.dice.upbuf,
vstep=0,
**kwargs,
)
def write(self, directory: Path, filename: str = "nvt.ter") -> Path:
return super(NVTTerConfig, self).write(directory, filename)
# -----------------------------------------------------
# NVT PRODUCTION
# -----------------------------------------------------
@dataclass(slots=True)
class NVTEqConfig(NVTConfig):
title: str = "Diceplayer Run - NVT Production"
irdf: int = 0
init: str = "yesreadxyz"
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTEqConfig, cls).from_config(
config,
nstep=cls._get_nstep(config, 1),
irdf=config.dice.irdf,
vstep=0,
**kwargs,
)
def write(self, directory: Path, filename: str = "nvt.eq") -> Path:
return super(NVTEqConfig, self).write(directory, filename)
# -----------------------------------------------------
# NPT BASE
# -----------------------------------------------------
@dataclass(slots=True)
class NPTConfig(BaseConfig):
title: str = "Diceplayer Run - NPT"
nstep: int = 0
vstep: int = 5000
press: float = 1.0
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NPTConfig, cls).from_config(
config,
press=config.dice.press,
)
# -----------------------------------------------------
# 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,
)
def write(self, directory: Path, filename: str = "npt.ter") -> Path:
return super(NPTTerConfig, self).write(directory, filename)
# -----------------------------------------------------
# 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,
)
def write(self, directory: Path, filename: str = "npt.eq") -> Path:
return super(NPTEqConfig, self).write(directory, filename)
return input_path

View File

@@ -1,5 +1,6 @@
import diceplayer.dice.dice_input as dice_input
from diceplayer.config import DiceConfig
from diceplayer.environment import System
import subprocess
from pathlib import Path
@@ -15,14 +16,13 @@ class DiceWrapper:
self.dice_config = dice_config
self.working_directory = working_directory
def run(self, dice_config: dice_input.BaseConfig) -> None:
input_path = dice_config.write(self.working_directory)
def run(self, dice_config: dice_input.DiceInputConfig) -> None:
input_path = dice_input.write_config(dice_config, self.working_directory)
output_path = input_path.parent / (input_path.name + ".out")
with open(output_path, "w") as outfile, open(input_path, "r") as infile:
bin_path = self.dice_config.progname.expanduser()
exit_status = subprocess.call(
bin_path, stdin=infile, stdout=outfile, cwd=self.working_directory
self.dice_config.progname, stdin=infile, stdout=outfile, cwd=self.working_directory
)
if exit_status != 0:
@@ -35,5 +35,9 @@ class DiceWrapper:
raise RuntimeError(f"Dice simulation failed with exit status {exit_status}")
def extract_results(self) -> dict:
return {}
def parse_results(self, system: System) -> dict:
results = {}
for output_file in sorted(self.working_directory.glob("phb*.xyz")):
...
return results

View File

@@ -9,8 +9,8 @@ from diceplayer.utils.ptable import GHOST_NUMBER
import numpy as np
import numpy.linalg as linalg
import numpy.typing as npt
from typing_extensions import List, Self, Tuple
from pydantic.dataclasses import dataclass
from typing_extensions import List, Self, Tuple
import math
from copy import deepcopy

View File

@@ -3,11 +3,10 @@ from diceplayer.dice.dice_handler import DiceHandler
from diceplayer.logger import logger
from diceplayer.state.state_handler import StateHandler
from diceplayer.state.state_model import StateModel
from diceplayer.utils.potential import read_system_from_phb
from typing_extensions import TypedDict, Unpack
from diceplayer.utils.potential import read_system_from_phb
class PlayerFlags(TypedDict):
continuation: bool
@@ -33,9 +32,7 @@ class Player:
if state is None:
system = read_system_from_phb(self.config)
state = StateModel(
config=self.config, system=system
)
state = StateModel(config=self.config, system=system)
else:
logger.info("Resuming from existing state.")

View File

@@ -1,11 +1,8 @@
from diceplayer.config import PlayerConfig
from diceplayer.environment import System, Molecule, Atom
from diceplayer.environment import Atom, Molecule, System
from pathlib import Path
from diceplayer.logger import logger
from diceplayer.state.state_model import StateModel
def read_system_from_phb(config: PlayerConfig) -> System:
phb_file = Path(config.dice.ljname)
@@ -41,9 +38,7 @@ def read_system_from_phb(config: PlayerConfig) -> System:
for j in range(nsites):
_fields = ljc_data.pop(0).split()
mol.add_atom(
Atom(*_fields)
)
mol.add_atom(Atom(*_fields))
sys.add_type(mol)
@@ -58,5 +53,3 @@ def read_system_from_phb(config: PlayerConfig) -> System:
# f.write(f"{state.config.dice.combrule}\n")
# f.write(f"{len(state.system.nmols)}\n")
# f.write(f"{state.config.dice.nmol}\n")

View File

@@ -4,6 +4,7 @@ from diceplayer.dice.dice_input import (
NPTTerConfig,
NVTEqConfig,
NVTTerConfig,
write_config,
)
import pytest
@@ -60,11 +61,7 @@ class TestDiceInput:
def test_write_dice_config(self, player_config: PlayerConfig, tmp_path: Path):
dice_input = NVTTerConfig.from_config(player_config)
output_file = tmp_path / "nvt_ter.inp"
with open(output_file, "w") as file:
dice_input.write_dice_config(file)
output_file = tmp_path / dice_input.type
write_config(dice_input, tmp_path)
assert output_file.exists()
print(output_file.read_text())

View File

@@ -1,17 +1,15 @@
from pathlib import Path
from typing import Any
import pytest
from diceplayer.config import PlayerConfig
from diceplayer.environment import System
from diceplayer.utils.potential import read_system_from_phb
import pytest
class TestPotential:
@pytest.fixture
def player_config(self) -> PlayerConfig:
return PlayerConfig.model_validate({
return PlayerConfig.model_validate(
{
"type": "both",
"mem": 12,
"max_cyc": 100,
@@ -30,7 +28,8 @@ class TestPotential:
"qmprog": "g16",
"keywords": "test",
},
})
}
)
def test_read_phb(self, player_config: PlayerConfig):
system = read_system_from_phb(player_config)