Skip to content

Commit 869d342

Browse files
Added parsing of , conversion to
1 parent a9840bf commit 869d342

File tree

2 files changed

+222
-1
lines changed

2 files changed

+222
-1
lines changed

src/nomad_simulation_parsers/parsers/lammps/parser.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,42 @@ def _extract_pressure_params(
125125
_pressure_params = None
126126
return _pressure_params
127127

128+
def _extract_modulus(self, fix_cmd: list[str]) -> np.ndarray | None:
129+
"""Extract modulus (bulk modulus) from fix command and convert to compressibility.
130+
131+
LAMMPS uses 'modulus' keyword in fix npt/nph commands to specify bulk modulus.
132+
Compressibility = 1 / modulus (in consistent units).
133+
134+
Args:
135+
fix_cmd: Fix command arguments list
136+
137+
Returns:
138+
3x3 compressibility matrix (1/pascal) or None if modulus not found
139+
"""
140+
result = None
141+
if 'modulus' not in fix_cmd:
142+
return result
143+
144+
try:
145+
modulus_idx = fix_cmd.index('modulus')
146+
# Modulus value follows the 'modulus' keyword
147+
modulus_value = float(fix_cmd[modulus_idx + 1])
148+
149+
# Apply pressure units to modulus (same units as pressure in LAMMPS)
150+
modulus_with_units = self.apply_unit(modulus_value, 'pressure')
151+
152+
# Compressibility = 1 / modulus
153+
# Units: 1/pascal = 1 / (kg/(m·s²)) = m·s²/kg = Pa⁻¹
154+
compressibility_value = 1.0 / modulus_with_units.magnitude
155+
compressibility_unit = 1.0 / modulus_with_units.units
156+
157+
# Create 3x3 diagonal matrix (isotropic compressibility)
158+
result = np.eye(3) * compressibility_value * compressibility_unit
159+
except (ValueError, IndexError, TypeError, ZeroDivisionError):
160+
result = None
161+
162+
return result
163+
128164
def _extract_timestep(self) -> Any | None:
129165
"""Extract integration timestep from log parser."""
130166
timestep_value = self._log_parser.get('timestep')
@@ -183,6 +219,51 @@ def _set_parser_masses(self, masses: np.ndarray | None) -> None:
183219
if isinstance(parser, TrajParser):
184220
parser.masses = masses
185221

222+
def _extract_integrator_type(self) -> str | None:
223+
"""Extract integrator type from run_style command.
224+
225+
LAMMPS run_style options: verlet, verlet/split, respa, respa/omp
226+
Maps to NOMAD integrator_type enum.
227+
228+
Returns:
229+
Integrator type string or None if not found
230+
"""
231+
run_style = self._log_parser.get('run_style')
232+
if run_style is None:
233+
return None
234+
235+
# Handle nested list structure from multiple run_style commands
236+
# E.g., [['verlet'], ['respa', '4', '2', ...]] -> take last command
237+
if isinstance(run_style, list) and len(run_style) > 0:
238+
if isinstance(run_style[0], list):
239+
# Nested list: take last command
240+
run_style = run_style[-1]
241+
242+
# Extract style name from command arguments
243+
# E.g., ['respa', '4', '2', ...] -> 'respa'
244+
# 'verlet' -> 'verlet'
245+
if isinstance(run_style, list) and len(run_style) > 0:
246+
style = run_style[0]
247+
else:
248+
style = run_style
249+
250+
if style is None:
251+
return None
252+
253+
result = None
254+
style_lower = str(style).lower()
255+
256+
# Map LAMMPS run_style to NOMAD integrator_type
257+
integrator_map = {
258+
'verlet': 'velocity_verlet',
259+
'verlet/split': 'velocity_verlet_split',
260+
'respa': 'respa', # reversible reference system propagator algorithm
261+
'respa/omp': 'respa',
262+
}
263+
264+
result = integrator_map.get(style_lower, 'velocity_verlet') # default fallback
265+
return result
266+
186267
def _extract_barostat_settings(
187268
self, fix_commands: list[list[str]] | None
188269
) -> BarostatParameters | None:
@@ -228,6 +309,16 @@ def _extract_barostat_settings(
228309
pressure_params
229310
)
230311
break
312+
# Extract modulus and convert to compressibility
313+
compressibility = self._extract_modulus(fix_cmd)
314+
if compressibility is not None:
315+
params.compressibility = compressibility
316+
317+
# Extract modulus and convert to compressibility
318+
compressibility = self._extract_modulus(fix_cmd)
319+
if compressibility is not None:
320+
params.compressibility = compressibility
321+
231322
return params
232323
return None
233324

@@ -328,7 +419,10 @@ def parse_method(self, simulation: Simulation) -> None:
328419
return
329420

330421
method = MolecularDynamicsMethod()
331-
method.integrator_type = 'velocity_verlet'
422+
423+
integrator_type = self._extract_integrator_type()
424+
if integrator_type is not None:
425+
method.integrator_type = integrator_type
332426

333427
timestep = self._extract_timestep()
334428
if timestep is not None:

tests/parsers/test_lammps_parser.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import pytest
2727
from nomad.client import normalize_all
2828
from nomad.datamodel import EntryArchive, EntryMetadata
29+
from nomad.units import ureg
2930
from nomad.utils import get_logger
3031

3132
from nomad_simulation_parsers.parsers.lammps.file_parsers import LogParser
@@ -836,6 +837,74 @@ def test_md_method_barostat_extraction():
836837
assert barostat is None
837838

838839

840+
def test_md_method_barostat_modulus_extraction():
841+
"""Test modulus extraction and conversion to compressibility."""
842+
843+
writer = LammpsArchiveWriter()
844+
writer._log_parser = LogParser()
845+
# Initialize units system for apply_unit to work
846+
writer._log_parser._units = {
847+
'pressure': ureg.atmosphere,
848+
'time': ureg.femtosecond,
849+
}
850+
851+
# Test NPT with modulus keyword (bulk modulus = 20000 atm)
852+
# compressibility should be 1/20000 atm^-1
853+
fix_npt_modulus = [
854+
[
855+
'1',
856+
'all',
857+
'npt',
858+
'temp',
859+
'300',
860+
'300',
861+
'100',
862+
'iso',
863+
'1.0',
864+
'1.0',
865+
'1000.0',
866+
'modulus',
867+
'20000.0',
868+
]
869+
]
870+
barostat = writer._extract_barostat_settings(fix_npt_modulus)
871+
assert barostat is not None
872+
assert barostat.compressibility is not None
873+
874+
# Check compressibility value (1/20000 atm in SI units)
875+
# 1 atm = 101325 Pa, so 1/20000 atm^-1 = 1/(20000*101325) Pa^-1
876+
expected_compressibility = 1.0 / (20000.0 * 101325.0) # Pa^-1
877+
assert barostat.compressibility[0, 0].magnitude == pytest.approx(
878+
expected_compressibility, rel=1e-6
879+
)
880+
assert barostat.compressibility[1, 1].magnitude == pytest.approx(
881+
expected_compressibility, rel=1e-6
882+
)
883+
assert barostat.compressibility[2, 2].magnitude == pytest.approx(
884+
expected_compressibility, rel=1e-6
885+
)
886+
887+
# Test NPH with modulus
888+
fix_nph_modulus = [
889+
['1', 'all', 'nph', 'iso', '1.0', '1.0', '1000.0', 'modulus', '10000.0']
890+
]
891+
barostat = writer._extract_barostat_settings(fix_nph_modulus)
892+
assert barostat is not None
893+
assert barostat.compressibility is not None
894+
expected_compressibility = 1.0 / (10000.0 * 101325.0)
895+
assert barostat.compressibility[0, 0].magnitude == pytest.approx(
896+
expected_compressibility, rel=1e-6
897+
)
898+
899+
# Test without modulus (should be None)
900+
fix_npt_no_modulus = [
901+
['1', 'all', 'npt', 'temp', '300', '300', '100', 'iso', '1.0', '1.0', '1000.0']
902+
]
903+
barostat = writer._extract_barostat_settings(fix_npt_no_modulus)
904+
assert barostat is not None
905+
assert barostat.compressibility is None
906+
907+
839908
def test_md_method_ensemble_detection():
840909
"""Test ensemble type detection."""
841910

@@ -1014,3 +1083,61 @@ def test_md_method_helper_methods():
10141083
writer._log_parser._results = {'thermo': [50, 100, 200]}
10151084
thermo_freq = writer._extract_thermo_frequency()
10161085
assert thermo_freq == 200
1086+
1087+
1088+
def test_md_method_integrator_type_extraction():
1089+
"""Test integrator type extraction from run_style command."""
1090+
1091+
class MockLogParser:
1092+
"""Mock LogParser for testing."""
1093+
1094+
def __init__(self):
1095+
self._results = {}
1096+
1097+
def get(self, key, default=None):
1098+
return self._results.get(key, default)
1099+
1100+
writer = LammpsArchiveWriter()
1101+
writer._log_parser = MockLogParser()
1102+
1103+
# Test verlet
1104+
writer._log_parser._results = {'run_style': 'verlet'}
1105+
integrator = writer._extract_integrator_type()
1106+
assert integrator == 'velocity_verlet'
1107+
1108+
# Test verlet/split
1109+
writer._log_parser._results = {'run_style': 'verlet/split'}
1110+
integrator = writer._extract_integrator_type()
1111+
assert integrator == 'velocity_verlet_split'
1112+
1113+
# Test respa
1114+
writer._log_parser._results = {'run_style': 'respa'}
1115+
integrator = writer._extract_integrator_type()
1116+
assert integrator == 'respa'
1117+
1118+
# Test respa/omp
1119+
writer._log_parser._results = {'run_style': 'respa/omp'}
1120+
integrator = writer._extract_integrator_type()
1121+
assert integrator == 'respa'
1122+
1123+
# Test with command args (respa with levels)
1124+
writer._log_parser._results = {
1125+
'run_style': ['respa', '4', '2', '2', '2', 'bond', '1', 'kspace', '4']
1126+
}
1127+
integrator = writer._extract_integrator_type()
1128+
assert integrator == 'respa'
1129+
1130+
# Test with nested list of commands (take last)
1131+
writer._log_parser._results = {'run_style': [['verlet'], ['respa', '4', '2']]}
1132+
integrator = writer._extract_integrator_type()
1133+
assert integrator == 'respa'
1134+
1135+
# Test None case
1136+
writer._log_parser._results = {}
1137+
integrator = writer._extract_integrator_type()
1138+
assert integrator is None
1139+
1140+
# Test unknown style (fallback to velocity_verlet)
1141+
writer._log_parser._results = {'run_style': 'unknown_style'}
1142+
integrator = writer._extract_integrator_type()
1143+
assert integrator == 'velocity_verlet'

0 commit comments

Comments
 (0)