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 altsteps: 2000
dice: dice:
nprocs: 4 nprocs: 1
nmol: [1, 1000] nmol: [1, 200]
dens: 1.5 dens: 1.5
nstep: [2000, 3000] nstep: [200, 300]
isave: 0 isave: 100
outname: 'phb' outname: 'phb'
progname: '~/.local/bin/dice' progname: 'dice'
ljname: 'phb.ljc.example' ljname: 'phb.ljc.example'
randominit: 'always' randominit: 'always'
seed: 12345 seed: 12345

View File

@@ -1,9 +1,8 @@
from pathlib import Path
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from typing_extensions import Literal from typing_extensions import Literal
import random import random
from pathlib import Path
class DiceConfig(BaseModel): class DiceConfig(BaseModel):
@@ -38,7 +37,7 @@ class DiceConfig(BaseModel):
) )
irdf: int = Field( irdf: int = Field(
0, 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( vstep: int = Field(
5000, description="Frequency of volume change moves in NPT simulations" 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") isave: int = Field(1000, description="Frequency of saving the simulation results")
press: float = Field(1.0, description="Pressure of the system") press: float = Field(1.0, description="Pressure of the system")
temp: float = Field(300.0, description="Temperature 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" "dice", description="Name of the program to run the simulation"
) )
randominit: str = Field( randominit: str = Field(

View File

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

View File

@@ -1,16 +1,16 @@
from diceplayer.config import PlayerConfig from diceplayer.config import PlayerConfig
from diceplayer.logger import logger from diceplayer.logger import logger
from pydantic import BaseModel, Field
from typing_extensions import Self from typing_extensions import Self
from abc import ABC import random
from dataclasses import dataclass, fields from enum import StrEnum
from pathlib import Path from pathlib import Path
from typing import Any, Sequence, TextIO from typing import Annotated, Any, Literal, TextIO
_ALLOWED_DICE_KEYWORD_IN_ORDER = [
DICE_KEYWORD_ORDER = [
"title", "title",
"ncores", "ncores",
"ljname", "ljname",
@@ -32,220 +32,226 @@ DICE_KEYWORD_ORDER = [
] ]
class DiceRoutineType(StrEnum):
@dataclass(slots=True) NVT_TER = "nvt.ter"
class BaseConfig(ABC): NVT_EQ = "nvt.eq"
ncores: int NPT_TER = "npt.ter"
ljname: str NPT_EQ = "npt.eq"
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:
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)
# ----------------------------------------------------- def get_nstep(config, idx: int) -> int:
# NVT BASE if len(config.dice.nstep) > idx:
# ----------------------------------------------------- return config.dice.nstep[idx]
return config.dice.nstep[-1]
@dataclass(slots=True) def get_seed(config) -> int:
class NVTConfig(BaseConfig): return config.dice.seed or random.randint(0, 2**32 - 1)
title: str = "Diceplayer Run - NVT"
dens: float = ...
nstep: int = ...
vstep: int = 0
@classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self: def get_ncores(config) -> int:
return super(NVTConfig, cls).from_config( return max(1, int(config.ncores / config.dice.nprocs))
config,
dens=config.dice.dens,
nstep=cls._get_nstep(config, 0),
)
# ----------------------------------------------------- # -----------------------------------------------------
# NVT THERMALIZATION # NVT THERMALIZATION
# ----------------------------------------------------- # -----------------------------------------------------
class NVTTerConfig(BaseModel):
type: Literal[DiceRoutineType.NVT_TER] = DiceRoutineType.NVT_TER
title: str = "NVT Thermalization"
@dataclass(slots=True) ncores: int
class NVTTerConfig(NVTConfig): ljname: str
title: str = "Diceplayer Run - NVT Thermalization" outname: str
upbuf: int = 360 nmol: list[int]
init: str = "yes" dens: float
temp: float
seed: int
init: Literal["yes"] = "yes"
nstep: int
vstep: Literal[0] = 0
isave: int
upbuf: int
@classmethod @classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self: def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTTerConfig, cls).from_config( return cls(
config, ncores=get_ncores(config),
nstep=cls._get_nstep(config, 0), 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, upbuf=config.dice.upbuf,
vstep=0,
**kwargs, **kwargs,
) )
def write(self, directory: Path, filename: str = "nvt.ter") -> Path:
return super(NVTTerConfig, self).write(directory, filename)
# ----------------------------------------------------- # -----------------------------------------------------
# NVT PRODUCTION # NVT PRODUCTION
# ----------------------------------------------------- # -----------------------------------------------------
class NVTEqConfig(BaseModel):
type: Literal[DiceRoutineType.NVT_EQ] = DiceRoutineType.NVT_EQ
title: str = "NVT Production"
@dataclass(slots=True) ncores: int
class NVTEqConfig(NVTConfig): ljname: str
title: str = "Diceplayer Run - NVT Production" outname: str
irdf: int = 0 nmol: list[int]
init: str = "yesreadxyz" dens: float
temp: float
seed: int
init: Literal["no", "yesreadxyz"] = "no"
nstep: int
vstep: int
isave: int
irdf: Literal[0] = 0
upbuf: int
@classmethod @classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self: def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NVTEqConfig, cls).from_config( return cls(
config, ncores=get_ncores(config),
nstep=cls._get_nstep(config, 1), ljname=str(config.dice.ljname),
irdf=config.dice.irdf, outname=config.dice.outname,
vstep=0, 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, **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 # NPT THERMALIZATION
# ----------------------------------------------------- # -----------------------------------------------------
class NPTTerConfig(BaseModel):
type: Literal[DiceRoutineType.NPT_TER] = DiceRoutineType.NPT_TER
title: str = "NPT Thermalization"
@dataclass(slots=True) ncores: int
class NPTTerConfig(NPTConfig): ljname: str
title: str = "Diceplayer Run - NPT Thermalization" outname: str
dens: float | None = None 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 @classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self: def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NPTTerConfig, cls).from_config( return cls(
config, ncores=get_ncores(config),
ljname=str(config.dice.ljname),
outname=config.dice.outname,
nmol=config.dice.nmol,
dens=config.dice.dens, dens=config.dice.dens,
nstep=cls._get_nstep(config, 1), temp=config.dice.temp,
vstep=config.dice.vstep, 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, **kwargs,
) )
def write(self, directory: Path, filename: str = "npt.ter") -> Path:
return super(NPTTerConfig, self).write(directory, filename)
# ----------------------------------------------------- # -----------------------------------------------------
# NPT PRODUCTION # NPT PRODUCTION
# ----------------------------------------------------- # -----------------------------------------------------
class NPTEqConfig(BaseModel):
type: Literal[DiceRoutineType.NPT_EQ] = DiceRoutineType.NPT_EQ
title: str = "NPT Production"
@dataclass(slots=True) ncores: int
class NPTEqConfig(NPTConfig): ljname: str
title: str = "Diceplayer Run - NPT Production" outname: str
dens: float | None = None 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 @classmethod
def from_config(cls, config: PlayerConfig, **kwargs) -> Self: def from_config(cls, config: PlayerConfig, **kwargs) -> Self:
return super(NPTEqConfig, cls).from_config( return cls(
config, ncores=get_ncores(config),
ljname=str(config.dice.ljname),
outname=config.dice.outname,
nmol=config.dice.nmol,
dens=config.dice.dens, dens=config.dice.dens,
nstep=cls._get_nstep(config, 2), temp=config.dice.temp,
press=config.dice.press,
seed=get_seed(config),
nstep=get_nstep(config, 2),
vstep=config.dice.vstep, vstep=config.dice.vstep,
isave=max(1, get_nstep(config, 2) // 10),
upbuf=config.dice.upbuf,
**kwargs, **kwargs,
) )
def write(self, directory: Path, filename: str = "npt.eq") -> Path:
return super(NPTEqConfig, self).write(directory, filename) 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")
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)
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")
def write_config(config: DiceInputConfig, directory: Path) -> Path:
input_path = directory / config.type
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)
return input_path

View File

@@ -1,5 +1,6 @@
import diceplayer.dice.dice_input as dice_input import diceplayer.dice.dice_input as dice_input
from diceplayer.config import DiceConfig from diceplayer.config import DiceConfig
from diceplayer.environment import System
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -15,14 +16,13 @@ class DiceWrapper:
self.dice_config = dice_config self.dice_config = dice_config
self.working_directory = working_directory self.working_directory = working_directory
def run(self, dice_config: dice_input.BaseConfig) -> None: def run(self, dice_config: dice_input.DiceInputConfig) -> None:
input_path = dice_config.write(self.working_directory) input_path = dice_input.write_config(dice_config, self.working_directory)
output_path = input_path.parent / (input_path.name + ".out") output_path = input_path.parent / (input_path.name + ".out")
with open(output_path, "w") as outfile, open(input_path, "r") as infile: with open(output_path, "w") as outfile, open(input_path, "r") as infile:
bin_path = self.dice_config.progname.expanduser()
exit_status = subprocess.call( 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: if exit_status != 0:
@@ -35,5 +35,9 @@ class DiceWrapper:
raise RuntimeError(f"Dice simulation failed with exit status {exit_status}") raise RuntimeError(f"Dice simulation failed with exit status {exit_status}")
def extract_results(self) -> dict: def parse_results(self, system: System) -> dict:
return {} 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 as np
import numpy.linalg as linalg import numpy.linalg as linalg
import numpy.typing as npt import numpy.typing as npt
from typing_extensions import List, Self, Tuple
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
from typing_extensions import List, Self, Tuple
import math import math
from copy import deepcopy from copy import deepcopy

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import pytest
class TestDiceConfig: class TestDiceConfig:
def test_class_instantiation(self): def test_class_instantiation(self):
dice_dto = DiceConfig( dice_dto = DiceConfig(
nprocs=1, nprocs=1,
ljname="test", ljname="test",
outname="test", outname="test",
dens=1.0, dens=1.0,

View File

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

View File

@@ -1,36 +1,35 @@
from pathlib import Path
from typing import Any
import pytest
from diceplayer.config import PlayerConfig from diceplayer.config import PlayerConfig
from diceplayer.environment import System from diceplayer.environment import System
from diceplayer.utils.potential import read_system_from_phb from diceplayer.utils.potential import read_system_from_phb
import pytest
class TestPotential: class TestPotential:
@pytest.fixture @pytest.fixture
def player_config(self) -> PlayerConfig: def player_config(self) -> PlayerConfig:
return PlayerConfig.model_validate({ return PlayerConfig.model_validate(
"type": "both", {
"mem": 12, "type": "both",
"max_cyc": 100, "mem": 12,
"switch_cyc": 50, "max_cyc": 100,
"ncores": 4, "switch_cyc": 50,
"dice": { "ncores": 4,
"nprocs": 4, "dice": {
"ljname": "phb.ljc.example", "nprocs": 4,
"outname": "test", "ljname": "phb.ljc.example",
"dens": 1.0, "outname": "test",
"nmol": [12, 16], "dens": 1.0,
"nstep": [1, 1], "nmol": [12, 16],
}, "nstep": [1, 1],
"gaussian": { },
"level": "test", "gaussian": {
"qmprog": "g16", "level": "test",
"keywords": "test", "qmprog": "g16",
}, "keywords": "test",
}) },
}
)
def test_read_phb(self, player_config: PlayerConfig): def test_read_phb(self, player_config: PlayerConfig):
system = read_system_from_phb(player_config) system = read_system_from_phb(player_config)