Skip to content

Commit 7ea230d

Browse files
committed
feat: add support for lammps problem size validator
Signed-off-by: vsoch <vsoch@users.noreply.github.com>
1 parent af772c0 commit 7ea230d

5 files changed

Lines changed: 144 additions & 9 deletions

File tree

resource_secretary/apps/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ def get_applications():
1111
return get_providers(root_path=path)
1212

1313

14+
def discover_applications():
15+
"""
16+
Analogous to discover_providers, but add prefix. Leave out general.
17+
"""
18+
apps = {}
19+
for name, app in get_applications().items():
20+
if name == "general":
21+
continue
22+
# This should prevent any potential namespace conflicts.
23+
apps[f"app_{name}"] = app
24+
return apps
25+
26+
1427
def get_application(name):
1528
"""
1629
Finds an application by name and returns an instance.

resource_secretary/apps/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Note that this base provides probe, etc.
22
from dataclasses import dataclass
3+
from typing import Callable, Dict
34

45
from resource_secretary.providers import get_providers
56
from resource_secretary.providers.provider import BaseProvider
@@ -17,10 +18,10 @@ class PromptMatrix:
1718
class BaseApplication(BaseProvider):
1819
is_provider = True
1920

20-
def __init__(self):
21-
super().__init__()
21+
def __init__(self, *args, **kwargs):
2222
self.category = "application"
2323
self.default_workload = None
24+
super().__init__(*args, **kwargs)
2425

2526
def get_prompt_matrix(self, flatten=False, filters=None, **params) -> dict:
2627
"""

resource_secretary/apps/molecular_dynamics/lammps.py

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import shlex
2+
from typing import Annotated, Any, Dict
3+
14
from resource_secretary.apps.app import BaseApplication
25
from resource_secretary.apps.prompts import AppPromptGenerator
3-
from resource_secretary.providers import get_providers
6+
from resource_secretary.providers.provider import dispatch_tool
47

58
LAMMPS_WORKLOADS = {
69
"reaxff": {
@@ -45,12 +48,130 @@ class LammpsApplication(BaseApplication):
4548
def name(self):
4649
return "lammps"
4750

48-
def __init__(self, **kwargs):
49-
super().__init__(**kwargs)
51+
def __init__(self):
52+
super().__init__()
5053
self.workloads = LAMMPS_WORKLOADS
5154
self.modifiers = LAMMPS_MODIFIERS
55+
self.tools = {}
5256
self.default_workload = "reaxff"
5357

58+
@dispatch_tool
59+
def validate_lmp_problem_size(
60+
self,
61+
command: Annotated[str, "The full LAMMPS command string generated by the agent."],
62+
expected_x: Annotated[
63+
int, "The required replication size or grid count in the X dimension."
64+
],
65+
expected_y: Annotated[
66+
int, "The required replication size or grid count in the Y dimension."
67+
],
68+
expected_z: Annotated[
69+
int, "The required replication size or grid count in the Z dimension."
70+
],
71+
) -> Annotated[
72+
Dict[str, Any], "A dictionary containing validation results and diagnostic messages."
73+
]:
74+
"""
75+
Parses a generated LAMMPS command string to verify that the required scientific
76+
problem size variables (X, Y, Z) are present and match the expected values.
77+
78+
Use case for Agent:
79+
1. Pre-Submission Check: After generating a complex command, call this tool to ensure no vital variables were dropped.
80+
81+
Returns:
82+
A dictionary with:
83+
- is_valid (bool): True if all variables are present and match exactly.
84+
- message (str): A human-readable description of what was found or missing.
85+
- parsed_dimensions (Dict[str, float]): The values of x, y, and z parsed from the command.
86+
- missing_dimensions (List[str]): List of missing required variables.
87+
- mismatched_dimensions (List[str]): List of variables that were present but didn't match the expectation.
88+
"""
89+
if not command:
90+
return {
91+
"is_valid": False,
92+
"message": "Command string is empty.",
93+
"parsed_dimensions": {},
94+
"missing_dimensions": ["x", "y", "z"],
95+
"mismatched_dimensions": [],
96+
}
97+
98+
try:
99+
tokens = shlex.split(command)
100+
except Exception as e:
101+
return {
102+
"is_valid": False,
103+
"message": f"Failed to parse command string tokens: {str(e)}",
104+
"parsed_dimensions": {},
105+
"missing_dimensions": [],
106+
"mismatched_dimensions": [],
107+
}
108+
109+
parsed_vars = {}
110+
i = 0
111+
112+
# Scan through tokens to find LAMMPS variable setting patterns
113+
while i < len(tokens):
114+
token = tokens[i]
115+
116+
# Standard LAMMPS: -v x X OR -var x Y OR --variable x 32
117+
if token in ("-v", "-var", "--variable") and i + 2 < len(tokens):
118+
var_name = tokens[i + 1]
119+
var_val = tokens[i + 2]
120+
if var_name in ("x", "y", "z"):
121+
try:
122+
parsed_vars[var_name] = float(var_val)
123+
except ValueError:
124+
pass # Handle non-numeric edge cases gracefully
125+
i += 3
126+
127+
# Support direct flags if asked for: -x 32, -y 32, -z 32
128+
elif token in ("-x", "-y", "-z") and i + 1 < len(tokens):
129+
return {
130+
"is_valid": False,
131+
"message": f"Parameters for -x, -y, -z are not Valid. Should be -v x X -v y Y -v z Z",
132+
"parsed_dimensions": {},
133+
"missing_dimensions": [],
134+
"mismatched_dimensions": [],
135+
}
136+
else:
137+
i += 1
138+
139+
# Compare parsed against expected
140+
expected = {"x": float(expected_x), "y": float(expected_y), "z": float(expected_z)}
141+
missing = []
142+
mismatched = []
143+
144+
for dim, expected_val in expected.items():
145+
if dim not in parsed_vars:
146+
missing.append(dim)
147+
elif parsed_vars[dim] != expected_val:
148+
mismatched.append(dim)
149+
150+
# Compile the resulting status
151+
if not missing and not mismatched:
152+
return {
153+
"is_valid": True,
154+
"message": f"Perfect match! Command correctly contains x={expected_x}, y={expected_y}, z={expected_z}.",
155+
"parsed_dimensions": parsed_vars,
156+
"missing_dimensions": [],
157+
"mismatched_dimensions": [],
158+
}
159+
160+
# Build failure messaging
161+
error_parts = []
162+
if missing:
163+
error_parts.append(f"Missing variable(s): {', '.join(missing)}")
164+
if mismatched:
165+
error_parts.append(f"Mismatched variable(s): {', '.join(mismatched)}")
166+
167+
return {
168+
"is_valid": False,
169+
"message": f"Validation failed. " + " | ".join(error_parts),
170+
"parsed_dimensions": parsed_vars,
171+
"missing_dimensions": missing,
172+
"mismatched_dimensions": mismatched,
173+
}
174+
54175
def get_prompt_matrix(
55176
self, workload="reaxff", manager="flux", flatten=False, filters=None, count=None, **params
56177
):

resource_secretary/providers/provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class BaseProvider:
7878

7979
is_provider = True
8080

81-
def __init__(self):
81+
def __init__(self, *args, **kwargs):
8282
self.tools: Dict[str, Callable] = {}
8383
self.category: str = "unknown"
8484

resource_secretary/providers/workload/flux.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,14 @@ def submit_job(
249249
import flux.job
250250

251251
# Sets this wrong a lot
252-
affinity_options = ["null", "None", "per-task"]
253-
if gpu_affinity and isinstance(gpu_affinity, str) and gpu_affinity not in affinity_options:
252+
affinity_options = ["per-task"]
253+
if gpu_affinity is not None and gpu_affinity not in affinity_options:
254254
return {
255255
"success": False,
256256
"error": "gpu_affinity must be unset or set to per-task",
257257
"job_id": None,
258258
}
259-
if cpu_affinity and isinstance(cpu_affinity, str) and cpu_affinity not in affinity_options:
259+
if cpu_affinity is not None and cpu_affinity not in affinity_options:
260260
return {
261261
"success": False,
262262
"error": "cpu_affinity must be unset or set to per-task",

0 commit comments

Comments
 (0)