From 89dcc456a912c1d07d831692698e839355d46a2c Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 5 Sep 2025 14:06:32 -0600 Subject: [PATCH 01/31] Adding a LAMMPS agent --- .../lammps_execute/EOS_of_Cu.py | 48 +++ src/ursa/agents/__init__.py | 2 + src/ursa/agents/lammps_agent.py | 359 ++++++++++++++++++ 3 files changed, 409 insertions(+) create mode 100644 examples/two_agent_examples/lammps_execute/EOS_of_Cu.py create mode 100644 src/ursa/agents/lammps_agent.py diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py new file mode 100644 index 00000000..bdebe74a --- /dev/null +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -0,0 +1,48 @@ +from ursa.agents import LammpsAgent +from ursa.agents import ExecutionAgent + +from langchain_core.messages import HumanMessage +from langchain_openai import ChatOpenAI + +model = "gpt-5" + +llm = ChatOpenAI(model = model, + timeout = None, + max_retries = 2 + ) + + +wf = LammpsAgent( + llm = llm, + max_potentials=2, + max_fix_attempts=5, + mpi_procs=8, + lammps_cmd="lmp_mpi", + mpirun_cmd="mpirun", +) + +high_level="Carry out a LAMMPS simulation of Cu to determine its equation of state." +elements=["Cu"] + +final_lammps_state = wf.run(high_level,elements) + +if final_lammps_state.get("run_returncode") == 0: + + print ("\nNow handing things off to execution agent.....") + + executor = ExecutionAgent(llm=llm) + exe_plan = f""" + You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level} + + A LAMMPS simulation has been done and the output is located here: ../log.lammps + + Summarize the contents of this file in a markdown document. Include a plot, if relevent. + """ + + init = {"messages": [HumanMessage(content=exe_plan)]} + + final_results = executor.action.invoke(init, {"recursion_limit": 10000}) + + for x in final_results["messages"]: + print(x.content) + diff --git a/src/ursa/agents/__init__.py b/src/ursa/agents/__init__.py index 6f4b90d2..982ea75f 100644 --- a/src/ursa/agents/__init__.py +++ b/src/ursa/agents/__init__.py @@ -15,3 +15,5 @@ from .recall_agent import RecallAgent as RecallAgent from .websearch_agent import WebSearchAgent as WebSearchAgent from .websearch_agent import WebSearchState as WebSearchState +from .lammps_agent import LammpsAgent as LammpsAgent +from .lammps_agent import LammpsState as LammpsState diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py new file mode 100644 index 00000000..8cb44813 --- /dev/null +++ b/src/ursa/agents/lammps_agent.py @@ -0,0 +1,359 @@ +from typing import TypedDict, List, Optional, Any, Dict +import json +import subprocess + +import atomman as am +import trafilatura +import tiktoken + +from langchain_core.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser + +from langgraph.graph import StateGraph, END + +from .base import BaseAgent + + +class LammpsState(TypedDict, total=False): + high_level: str + elements: List[str] + + matches: List[Any] + db_message: str + + idx: int + summaries: List[str] + full_texts: List[str] + + summaries_combined: str + choice_json: str + chosen_index: int + + input_script: str + run_returncode: Optional[int] + run_stdout: str + run_stderr: str + + fix_attempts: int + + + +class LammpsAgent(BaseAgent): + def __init__( + self, + llm, + max_potentials: int = 5, + max_fix_attempts: int = 10, + mpi_procs: int = 8, + lammps_cmd: str = "lmp_mpi", + mpirun_cmd: str = "mpirun", + tiktoken_model: str = "o3", + max_tokens: int = 200000, + **kwargs, + ): + + self.max_potentials = max_potentials + self.max_fix_attempts = max_fix_attempts + self.mpi_procs = mpi_procs + self.lammps_cmd = lammps_cmd + self.mpirun_cmd = mpirun_cmd + self.tiktoken_model = tiktoken_model + self.max_tokens = max_tokens + + self.pair_styles = ["eam", "eam/alloy", "eam/fs", + "meam", "adp", # classical, HEA-relevant + "kim", # OpenKIM models + "snap", "quip", "mlip", "pace", "nep" # ML/ACE families (if available) + ] + + + super().__init__(llm, **kwargs) + + self.str_parser = StrOutputParser() + + self.summ_chain = (ChatPromptTemplate.from_template( + "Here is some data about an interatomic potential: {metadata}\n\n" + "Briefly summarize why it could be useful for this task: {high_level}." + ) | self.llm | self.str_parser) + + + self.choose_chain = ( + ChatPromptTemplate.from_template( + "Here are the summaries of a certain number of interatomic potentials: {summaries_combined}\n\n" + "Pick one potential which would be most useful for this task: {high_level}.\n\n" + "Return your answer **only** as valid JSON, with no extra text or formatting.\n\n" + "Use this exact schema:\n" + "{{\n" + ' "Chosen index": ,\n' + ' "rationale": "",\n' + ' "Potential name": ""\n' + "}}\n" + ) + | self.llm + | self.str_parser + ) + + self.author_chain = ( + ChatPromptTemplate.from_template( + "Your task is to write a LAMMPS input file for this purpose: {high_level}.\n" + "Here is metadata about the interatomic potential that will be used: {metadata}.\n" + "Note that all potential files are in the ./ directory.\n" + "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" + "Return your answer **only** as valid JSON, with no extra text or formatting.\n" + "Use this exact schema:\n" + "{{\n" + ' "input_script": ""\n' + "}}\n" + ) + | self.llm + | self.str_parser + ) + + self.fix_chain = ( + ChatPromptTemplate.from_template( + "You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level}\n" + "For this purpose, this input file for LAMMPS was written: {input_script}\n" + "However, when running the simulation, an error was raised.\n" + "Here is the full stdout message that includes the error message: {err_message}\n" + "Your task is to write a new input file that resolves the error.\n" + "Here is metadata about the interatomic potential that will be used: {metadata}.\n" + "Note that all potential files are in the ./ directory.\n" + "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" + "Return your answer **only** as valid JSON, with no extra text or formatting.\n" + "Use this exact schema:\n" + "{{\n" + ' "input_script": ""\n' + "}}\n" + ) + | self.llm + | self.str_parser + ) + + + self.graph = self._build_graph().compile() + + + + @staticmethod + def _safe_json_loads(s: str) -> Dict[str, Any]: + s = s.strip() + if s.startswith("```"): + s = s.strip("`") + i = s.find("\n") + if i != -1: + s = s[i + 1 :].strip() + return json.loads(s) + + def _fetch_and_trim_text(self, url: str) -> str: + downloaded = trafilatura.fetch_url(url) + if not downloaded: + return "No metadata available" + text = trafilatura.extract( + downloaded, + include_comments=False, + include_tables=True, + include_links=False, + favor_recall=True, + ) + if not text: + return "No metadata available" + text = text.strip() + try: + enc = tiktoken.encoding_for_model(self.tiktoken_model) + toks = enc.encode(text) + if len(toks) > self.max_tokens: + toks = toks[: self.max_tokens] + text = enc.decode(toks) + except Exception: + pass + return text + + + def _find_potentials(self, state: LammpsState) -> LammpsState: + db = am.library.Database(remote=True) + matches = db.get_lammps_potentials(pair_style=self.pair_styles, elements=state["elements"]) + msg_lines = [] + if not list(matches): + msg_lines.append("No potentials found for this task in NIST.") + else: + msg_lines.append("Found these potentials in NIST:") + for rec in matches: + msg_lines.append(f"{rec.id} {rec.pair_style} {rec.symbols}") + return { + **state, + "matches": list(matches), + "db_message": "\n".join(msg_lines), + "idx": 0, + "summaries": [], + "full_texts": [], + "fix_attempts": 0, + } + + def _should_summarize(self, state: LammpsState) -> str: + matches = state.get("matches", []) + i = state.get("idx", 0) + if not matches: + print ("No potentials found in NIST for this task. Exiting....") + return "done_no_matches" + if i < min(self.max_potentials, len(matches)): + return "summarize_one" + return "summarize_done" + + def _summarize_one(self, state: LammpsState) -> LammpsState: + i = state["idx"] + print (f"Summarizing potential #{i}") + match = state["matches"][i] + md = match.metadata() + + if md.get("comments") is None: + text = "No metadata available" + summary = "No summary available" + else: + lines = md["comments"].split("\n") + url = lines[1] if len(lines) > 1 else "" + text = self._fetch_and_trim_text(url) if url else "No metadata available" + summary = self.summ_chain.invoke({"metadata": text, "high_level": state["high_level"]}) + + return { + **state, + "idx": i + 1, + "summaries": [*state["summaries"], summary], + "full_texts": [*state["full_texts"], text], + } + + def _build_summaries(self, state: LammpsState) -> LammpsState: + parts = [] + for i, s in enumerate(state["summaries"]): + rec = state["matches"][i] + parts.append(f"\nSummary of potential #{i}: {rec.id}\n{s}\n") + return {**state, "summaries_combined": "".join(parts)} + + def _choose(self, state: LammpsState) -> LammpsState: + print ("Choosing one potential for this task...") + choice = self.choose_chain.invoke({ + "summaries_combined": state["summaries_combined"], + "high_level": state["high_level"] + }) + choice_dict = self._safe_json_loads(choice) + chosen_index = int(choice_dict["Chosen index"]) + print (f"Chosen potential #{chosen_index}") + return {**state, "choice_json": choice, "chosen_index": chosen_index} + + def _author(self, state: LammpsState) -> LammpsState: + print("First attempt at writing LAMMPS input file....") + match = state["matches"][state["chosen_index"]] + match.download_files("./") + text = state["full_texts"][state["chosen_index"]] + pair_info = match.pair_info() + authored_json = self.author_chain.invoke({ + "high_level": state["high_level"], + "metadata": text, + "pair_info": pair_info + }) + script_dict = self._safe_json_loads(authored_json) + input_script = script_dict["input_script"] + with open("in.lammps", "w") as f: + f.write(input_script) + return {**state, "input_script": input_script} + + def _run_lammps(self, state: LammpsState) -> LammpsState: + print ("Running LAMMPS....") + result = subprocess.run( + [self.mpirun_cmd, "-np", str(self.mpi_procs), self.lammps_cmd, "-in", "in.lammps"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False + ) + return { + **state, + "run_returncode": result.returncode, + "run_stdout": result.stdout, + "run_stderr": result.stderr, + } + + def _route_run(self, state: LammpsState) -> str: + rc = state.get("run_returncode", 0) + attempts = state.get("fix_attempts", 0) + if rc == 0: + print ("LAMMPS run successful! Exiting...") + return "done_success" + if attempts < self.max_fix_attempts: + print ("LAMMPS run Failed. Attempting to rewrite input file...") + return "need_fix" + print ("LAMMPS run Failed and maximum fix attempts reached. Exiting...") + return "done_failed" + + def _fix(self, state: LammpsState) -> LammpsState: + match = state["matches"][state["chosen_index"]] + text = state["full_texts"][state["chosen_index"]] + pair_info = match.pair_info() + err_blob = state.get("run_stdout") + + fixed_json = self.fix_chain.invoke({ + "high_level": state["high_level"], + "input_script": state["input_script"], + "err_message": err_blob, + "metadata": text, + "pair_info": pair_info + }) + script_dict = self._safe_json_loads(fixed_json) + new_input = script_dict["input_script"] + with open("in.lammps", "w") as f: + f.write(new_input) + return {**state, "input_script": new_input, "fix_attempts": state.get("fix_attempts", 0) + 1} + + + + def _build_graph(self): + g = StateGraph(LammpsState) + + g.add_node("find_potentials", self._find_potentials) + g.add_node("summarize_one", self._summarize_one) + g.add_node("build_summaries", self._build_summaries) + g.add_node("choose", self._choose) + g.add_node("author", self._author) + g.add_node("run_lammps", self._run_lammps) + g.add_node("fix", self._fix) + + g.set_entry_point("find_potentials") + + g.add_conditional_edges( + "find_potentials", + self._should_summarize, + { + "summarize_one": "summarize_one", + "summarize_done": "build_summaries", + "done_no_matches": END, + }, + ) + + g.add_conditional_edges( + "summarize_one", + self._should_summarize, + { + "summarize_one": "summarize_one", + "summarize_done": "build_summaries", + }, + ) + + g.add_edge("build_summaries", "choose") + g.add_edge("choose", "author") + g.add_edge("author", "run_lammps") + + g.add_conditional_edges( + "run_lammps", + self._route_run, + { + "need_fix": "fix", + "done_success": END, + "done_failed": END, + }, + ) + g.add_edge("fix", "run_lammps") + return g + + + def run(self,high_level,elements): + return self.graph.invoke({"high_level": high_level,"elements": elements}) + \ No newline at end of file From 36ea0ad36756374c44c1c1eb5870a890d3f842d8 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 5 Sep 2025 14:43:51 -0600 Subject: [PATCH 02/31] Added .ipynb_checkpoints/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 90205761..fb1a0c08 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ dist/ wheels/ *.egg-info *.sql +.ipynb_checkpoints/ # Virtual environments .venv/ From 686e8a29377cf662dc07b380211f82d8ed320c1e Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 5 Sep 2025 15:34:05 -0600 Subject: [PATCH 03/31] Added another example for the LAMMPS agent --- .../lammps_execute/stiffness_tensor_of_hea.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py new file mode 100644 index 00000000..39409afd --- /dev/null +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -0,0 +1,48 @@ +from ursa.agents import LammpsAgent +from ursa.agents import ExecutionAgent + +from langchain_core.messages import HumanMessage +from langchain_openai import ChatOpenAI + +model = "gpt-5" + +llm = ChatOpenAI(model = model, + timeout = None, + max_retries = 2 + ) + + +wf = LammpsAgent( + llm = llm, + max_potentials=5, + max_fix_attempts=10, + mpi_procs=8, + lammps_cmd="lmp_mpi", + mpirun_cmd="mpirun", +) + +high_level="Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." +elements=["Co","Cr","Fe","Mn","Ni"] + +final_lammps_state = wf.run(high_level,elements) + +if final_lammps_state.get("run_returncode") == 0: + + print ("\nNow handing things off to execution agent.....") + + executor = ExecutionAgent(llm=llm) + exe_plan = f""" + You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level} + + A LAMMPS simulation has been done and the output is located here: ../log.lammps + + Summarize the contents of this file in a markdown document. Include a plot, if relevent. + """ + + init = {"messages": [HumanMessage(content=exe_plan)]} + + final_results = executor.action.invoke(init, {"recursion_limit": 10000}) + + for x in final_results["messages"]: + print(x.content) + From cad9b7c1e3bf6f2e215382ccad322ae40d44cd5c Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 5 Sep 2025 16:24:14 -0600 Subject: [PATCH 04/31] Improved prompts in Lammps Agent --- .../lammps_execute/stiffness_tensor_of_hea.py | 2 +- src/ursa/agents/lammps_agent.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 39409afd..fc13ceb3 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -15,7 +15,7 @@ wf = LammpsAgent( llm = llm, max_potentials=5, - max_fix_attempts=10, + max_fix_attempts=15, mpi_procs=8, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 8cb44813..0c940741 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -99,6 +99,7 @@ def __init__( "Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the ./ directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" + "Ensure that all output data is written only to the ./log.lammps file. Do not create any other output file.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" "Use this exact schema:\n" "{{\n" @@ -119,6 +120,7 @@ def __init__( "Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the ./ directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" + "Ensure that all output data is written only to the ./log.lammps file. Do not create any other output file.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" "Use this exact schema:\n" "{{\n" From 3013e8083e382569398fc379baebc3c1d6cd9661 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Mon, 15 Sep 2025 15:24:35 -0600 Subject: [PATCH 05/31] Added workspace directory for lammps agent --- .../two_agent_examples/lammps_execute/EOS_of_Cu.py | 13 ++++++++----- .../lammps_execute/stiffness_tensor_of_hea.py | 12 ++++++++---- src/ursa/agents/lammps_agent.py | 12 +++++++++--- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index bdebe74a..74bc0a2b 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -11,12 +11,14 @@ max_retries = 2 ) +workspace = "./workspace_eos_cu" wf = LammpsAgent( llm = llm, max_potentials=2, max_fix_attempts=5, mpi_procs=8, + workspace = workspace, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", ) @@ -34,14 +36,15 @@ exe_plan = f""" You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level} - A LAMMPS simulation has been done and the output is located here: ../log.lammps + A LAMMPS simulation has been done and the output is located here in the file log.lammps. Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - - init = {"messages": [HumanMessage(content=exe_plan)]} - - final_results = executor.action.invoke(init, {"recursion_limit": 10000}) + + executor_config = {"recursion_limit": 999_999} + + final_results = executor.action.invoke({"messages": [HumanMessage(content=exe_plan)], + "workspace": workspace,},executor_config,) for x in final_results["messages"]: print(x.content) diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index fc13ceb3..8e597e94 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -12,11 +12,14 @@ ) +workspace = "./workspace_stiffness_tensor" + wf = LammpsAgent( llm = llm, max_potentials=5, max_fix_attempts=15, mpi_procs=8, + workspace = workspace, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", ) @@ -34,14 +37,15 @@ exe_plan = f""" You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level} - A LAMMPS simulation has been done and the output is located here: ../log.lammps + A LAMMPS simulation has been done and the output is located here in the file log.lammps. Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - init = {"messages": [HumanMessage(content=exe_plan)]} - - final_results = executor.action.invoke(init, {"recursion_limit": 10000}) + executor_config = {"recursion_limit": 999_999} + + final_results = executor.action.invoke({"messages": [HumanMessage(content=exe_plan)], + "workspace": workspace,},executor_config,) for x in final_results["messages"]: print(x.content) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 0c940741..161c5bde 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -1,6 +1,7 @@ from typing import TypedDict, List, Optional, Any, Dict import json import subprocess +import os import atomman as am import trafilatura @@ -45,6 +46,7 @@ def __init__( max_potentials: int = 5, max_fix_attempts: int = 10, mpi_procs: int = 8, + workspace: str = "./workspace", lammps_cmd: str = "lmp_mpi", mpirun_cmd: str = "mpirun", tiktoken_model: str = "o3", @@ -66,6 +68,9 @@ def __init__( "snap", "quip", "mlip", "pace", "nep" # ML/ACE families (if available) ] + self.workspace = workspace + os.makedirs(self.workspace, exist_ok=True) + super().__init__(llm, **kwargs) @@ -244,7 +249,7 @@ def _choose(self, state: LammpsState) -> LammpsState: def _author(self, state: LammpsState) -> LammpsState: print("First attempt at writing LAMMPS input file....") match = state["matches"][state["chosen_index"]] - match.download_files("./") + match.download_files(self.workspace) text = state["full_texts"][state["chosen_index"]] pair_info = match.pair_info() authored_json = self.author_chain.invoke({ @@ -254,7 +259,7 @@ def _author(self, state: LammpsState) -> LammpsState: }) script_dict = self._safe_json_loads(authored_json) input_script = script_dict["input_script"] - with open("in.lammps", "w") as f: + with open(os.path.join(self.workspace, "in.lammps"), "w") as f: f.write(input_script) return {**state, "input_script": input_script} @@ -262,6 +267,7 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: print ("Running LAMMPS....") result = subprocess.run( [self.mpirun_cmd, "-np", str(self.mpi_procs), self.lammps_cmd, "-in", "in.lammps"], + cwd=self.workspace, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, @@ -301,7 +307,7 @@ def _fix(self, state: LammpsState) -> LammpsState: }) script_dict = self._safe_json_loads(fixed_json) new_input = script_dict["input_script"] - with open("in.lammps", "w") as f: + with open(os.path.join(self.workspace, "in.lammps"), "w") as f: f.write(new_input) return {**state, "input_script": new_input, "fix_attempts": state.get("fix_attempts", 0) + 1} From 61f305ced44bc40ee479f7bd696129515b53fcf9 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Mon, 15 Sep 2025 17:32:24 -0600 Subject: [PATCH 06/31] Minor edits to lammps agent --- .../lammps_execute/EOS_of_Cu.py | 6 ++--- .../lammps_execute/stiffness_tensor_of_hea.py | 7 +++--- src/ursa/agents/lammps_agent.py | 22 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 74bc0a2b..e1d1fad1 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -23,10 +23,10 @@ mpirun_cmd="mpirun", ) -high_level="Carry out a LAMMPS simulation of Cu to determine its equation of state." +simulation_task="Carry out a LAMMPS simulation of Cu to determine its equation of state." elements=["Cu"] -final_lammps_state = wf.run(high_level,elements) +final_lammps_state = wf.run(simulation_task,elements) if final_lammps_state.get("run_returncode") == 0: @@ -34,7 +34,7 @@ executor = ExecutionAgent(llm=llm) exe_plan = f""" - You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level} + You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task} A LAMMPS simulation has been done and the output is located here in the file log.lammps. diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 8e597e94..6dc67e78 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -24,10 +24,11 @@ mpirun_cmd="mpirun", ) -high_level="Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." +simulation_task="Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." + elements=["Co","Cr","Fe","Mn","Ni"] -final_lammps_state = wf.run(high_level,elements) +final_lammps_state = wf.run(simulation_task,elements) if final_lammps_state.get("run_returncode") == 0: @@ -35,7 +36,7 @@ executor = ExecutionAgent(llm=llm) exe_plan = f""" - You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level} + You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task} A LAMMPS simulation has been done and the output is located here in the file log.lammps. diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 161c5bde..9e6f4666 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -16,7 +16,7 @@ class LammpsState(TypedDict, total=False): - high_level: str + simulation_task: str elements: List[str] matches: List[Any] @@ -78,14 +78,14 @@ def __init__( self.summ_chain = (ChatPromptTemplate.from_template( "Here is some data about an interatomic potential: {metadata}\n\n" - "Briefly summarize why it could be useful for this task: {high_level}." + "Briefly summarize why it could be useful for this task: {simulation_task}." ) | self.llm | self.str_parser) self.choose_chain = ( ChatPromptTemplate.from_template( "Here are the summaries of a certain number of interatomic potentials: {summaries_combined}\n\n" - "Pick one potential which would be most useful for this task: {high_level}.\n\n" + "Pick one potential which would be most useful for this task: {simulation_task}.\n\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n\n" "Use this exact schema:\n" "{{\n" @@ -100,7 +100,7 @@ def __init__( self.author_chain = ( ChatPromptTemplate.from_template( - "Your task is to write a LAMMPS input file for this purpose: {high_level}.\n" + "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" "Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the ./ directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" @@ -117,7 +117,7 @@ def __init__( self.fix_chain = ( ChatPromptTemplate.from_template( - "You are part of a larger scientific workflow whose purpose is to accomplish this task: {high_level}\n" + "You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task}\n" "For this purpose, this input file for LAMMPS was written: {input_script}\n" "However, when running the simulation, an error was raised.\n" "Here is the full stdout message that includes the error message: {err_message}\n" @@ -219,7 +219,7 @@ def _summarize_one(self, state: LammpsState) -> LammpsState: lines = md["comments"].split("\n") url = lines[1] if len(lines) > 1 else "" text = self._fetch_and_trim_text(url) if url else "No metadata available" - summary = self.summ_chain.invoke({"metadata": text, "high_level": state["high_level"]}) + summary = self.summ_chain.invoke({"metadata": text, "simulation_task": state["simulation_task"]}) return { **state, @@ -239,7 +239,7 @@ def _choose(self, state: LammpsState) -> LammpsState: print ("Choosing one potential for this task...") choice = self.choose_chain.invoke({ "summaries_combined": state["summaries_combined"], - "high_level": state["high_level"] + "simulation_task": state["simulation_task"] }) choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) @@ -253,7 +253,7 @@ def _author(self, state: LammpsState) -> LammpsState: text = state["full_texts"][state["chosen_index"]] pair_info = match.pair_info() authored_json = self.author_chain.invoke({ - "high_level": state["high_level"], + "simulation_task": state["simulation_task"], "metadata": text, "pair_info": pair_info }) @@ -299,7 +299,7 @@ def _fix(self, state: LammpsState) -> LammpsState: err_blob = state.get("run_stdout") fixed_json = self.fix_chain.invoke({ - "high_level": state["high_level"], + "simulation_task": state["simulation_task"], "input_script": state["input_script"], "err_message": err_blob, "metadata": text, @@ -362,6 +362,6 @@ def _build_graph(self): return g - def run(self,high_level,elements): - return self.graph.invoke({"high_level": high_level,"elements": elements}) + def run(self,simulation_task,elements): + return self.graph.invoke({"simulation_task": simulation_task,"elements": elements},{"recursion_limit": 999_999}) \ No newline at end of file From 3752ad1c71a325344950fc558af82fcb48610b5f Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Wed, 17 Sep 2025 22:49:31 -0600 Subject: [PATCH 07/31] Updated lammps_agent prompts --- .../two_agent_examples/lammps_execute/EOS_of_Cu.py | 2 +- .../lammps_execute/stiffness_tensor_of_hea.py | 2 +- src/ursa/agents/lammps_agent.py | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index e1d1fad1..b82a4cc2 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -36,7 +36,7 @@ exe_plan = f""" You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task} - A LAMMPS simulation has been done and the output is located here in the file log.lammps. + A LAMMPS simulation has been done and the output is located in the file 'log.lammps'. Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 6dc67e78..4e4bd498 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -38,7 +38,7 @@ exe_plan = f""" You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task} - A LAMMPS simulation has been done and the output is located here in the file log.lammps. + A LAMMPS simulation has been done and the output is located in the file 'log.lammps'. Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 9e6f4666..4549a17b 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -102,9 +102,10 @@ def __init__( ChatPromptTemplate.from_template( "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" "Here is metadata about the interatomic potential that will be used: {metadata}.\n" - "Note that all potential files are in the ./ directory.\n" + "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" - "Ensure that all output data is written only to the ./log.lammps file. Do not create any other output file.\n" + "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" + "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" "Use this exact schema:\n" "{{\n" @@ -123,9 +124,10 @@ def __init__( "Here is the full stdout message that includes the error message: {err_message}\n" "Your task is to write a new input file that resolves the error.\n" "Here is metadata about the interatomic potential that will be used: {metadata}.\n" - "Note that all potential files are in the ./ directory.\n" + "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" - "Ensure that all output data is written only to the ./log.lammps file. Do not create any other output file.\n" + "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" + "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" "Use this exact schema:\n" "{{\n" @@ -244,6 +246,8 @@ def _choose(self, state: LammpsState) -> LammpsState: choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) print (f"Chosen potential #{chosen_index}") + print ("Rationale for choosing this potential:") + print(choice_dict["rationale"]) return {**state, "choice_json": choice, "chosen_index": chosen_index} def _author(self, state: LammpsState) -> LammpsState: From 50b69fe294c0d385bc9ed8b24851d0f037f72e7a Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Thu, 18 Sep 2025 17:01:09 -0600 Subject: [PATCH 08/31] Added atomman and trafilatura to the environment --- pyproject.toml | 2 + uv.lock | 545 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 508 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 505c7812..01e6451b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,8 @@ dependencies = [ "langgraph-checkpoint-sqlite>=2.0.10,<3.0", "langchain-ollama>=0.3.6,<0.4", "ddgs>=9.5.5", + "atomman>=1.5.2", + "trafilatura>=1.6.1", ] classifiers = [ "Operating System :: OS Independent", diff --git a/uv.lock b/uv.lock index 5dc30bed..cccaf32a 100644 --- a/uv.lock +++ b/uv.lock @@ -4,14 +4,19 @@ requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] [[package]] @@ -213,7 +218,8 @@ version = "21.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "cffi", marker = "python_full_version >= '3.14'" }, @@ -238,13 +244,17 @@ version = "25.1.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "cffi", marker = "python_full_version < '3.14'" }, @@ -334,6 +344,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721, upload-time = "2023-08-10T16:35:55.203Z" }, ] +[[package]] +name = "atomman" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "datamodeldict" }, + { name = "matplotlib" }, + { name = "numericalunits" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "potentials" }, + { name = "requests" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "toolz" }, + { name = "xmltodict" }, + { name = "yabadaba" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/ce/d7bc59594756b28eaba406b00faa1efb0ca37063dd167034e149006a2a8d/atomman-1.5.2.tar.gz", hash = "sha256:0d301454e7a253c025529b50d043feb4d923f1f22210a5422606fe0f60508d5c", size = 1080536, upload-time = "2025-08-04T19:43:06.388Z" } + [[package]] name = "attrs" version = "25.3.0" @@ -491,7 +523,7 @@ name = "build" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, + { name = "colorama", marker = "os_name == 'nt' and sys_platform != 'darwin'" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, @@ -511,6 +543,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] +[[package]] +name = "cdcs" +version = "0.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "requests" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/eb/4416173d9aa45cf2665f4fa75903582201ac82ecc3cd530773996094c0e8/cdcs-0.2.6.tar.gz", hash = "sha256:23c010a3f85f5064a5680c9f505d347425d29b749acdcb505a8961204151b505", size = 24062, upload-time = "2025-06-13T20:14:19.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/7e/ce13bc175c347db8da40412a41e4bf2e516f83bddaf0be33e76da6e67217/cdcs-0.2.6-py3-none-any.whl", hash = "sha256:4ac41d75425113c6194ce4025565a8eeb26ca2ef15b8a8405237430089160223", size = 29471, upload-time = "2025-06-13T20:14:18.248Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -741,7 +789,8 @@ version = "1.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -813,12 +862,16 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -907,6 +960,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/b1/5745d7523d8ce53b87779f46ef6cf5c5c342997939c2fe967e607b944e43/coolname-2.2.0-py2.py3-none-any.whl", hash = "sha256:4d1563186cfaf71b394d5df4c744f8c41303b6846413645e31d31915cdeb13e8", size = 37849, upload-time = "2023-01-09T14:50:39.897Z" }, ] +[[package]] +name = "courlan" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "tld" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382, upload-time = "2024-10-29T16:40:20.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" }, +] + [[package]] name = "cryptography" version = "45.0.6" @@ -963,6 +1030,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "cython" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/f6/d762df1f436a0618455d37f4e4c4872a7cd0dcfc8dec3022ee99e4389c69/cython-3.1.4.tar.gz", hash = "sha256:9aefefe831331e2d66ab31799814eae4d0f8a2d246cbaaaa14d1be29ef777683", size = 3190778, upload-time = "2025-09-16T07:20:33.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/03/90fa9c3a336bd28f93e246d2b7f8767341134d0b6ab44dbabd1259abdd1a/cython-3.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:523110241408ef6511d897e9cebbdffb99120ac82ef3aea89baacce290958f93", size = 2993775, upload-time = "2025-09-16T07:21:42.648Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3b/6a694b3cda00bece130b86601148eb5091e7a9531afa598be2bbfb32c57c/cython-3.1.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd34f960c3809fa2a7c3487ce9b3cb2c5bbc5ae2107f073a1a51086885958881", size = 2918270, upload-time = "2025-09-16T07:21:44.677Z" }, + { url = "https://files.pythonhosted.org/packages/85/41/a6cf199f2011f988ca532d47e8e452a20a564ffc29c8f7a11a903853f969/cython-3.1.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90842e7fb8cddfd173478670297f6a6b3df090e029a31ea6ce93669030e67b81", size = 3511091, upload-time = "2025-09-16T07:21:46.79Z" }, + { url = "https://files.pythonhosted.org/packages/ba/43/6a3b0cabf2bb78a7f1b7714d0bce81f065c45dcefceb8a505a26c12a5527/cython-3.1.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c88234303e2c15a5a88ae21c99698c7195433280b049aa2ad0ace906e6294dab", size = 3265539, upload-time = "2025-09-16T07:21:48.979Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f6/b8c4b557f537fd26c7188444ab18b4b60049b3e6f9665e468af6ddc4a408/cython-3.1.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f06b037f7c244dda9fc38091e87a68498c85c7c27ddc19aa84b08cf42a8a84a", size = 3427305, upload-time = "2025-09-16T07:21:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/b0/da/e38cbedf1eeb1b13c7d53e57b7b1516b7e51b3d125225bc38399286cf3a1/cython-3.1.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1aba748e9dcb9c0179d286cdb20215246c46b69cf227715e46287dcea8de7372", size = 3280622, upload-time = "2025-09-16T07:21:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/0b9f0e93b3470b4ab20f178b337ce443ca9699b0b9fa0a5da7b403736038/cython-3.1.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:297b6042d764f68dc6213312578ef4b69310d04c963f94a489914efbf44ab133", size = 3525244, upload-time = "2025-09-16T07:21:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/4368bbbb75f0f73ebce6f1ce4bb93717b35992b577b5e3bd654becaee243/cython-3.1.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2ecf927e73bde50043d3a9fe3159f834b0e642b97e60a21018439fd25d344888", size = 3441779, upload-time = "2025-09-16T07:21:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f3/722ffaa4e2bb25b37c6581cf77afc9e0e40c4b1973d5c670633d89a23c37/cython-3.1.4-cp310-cp310-win32.whl", hash = "sha256:3d940d603f85732627795518f9dba8fa63080d8221bb5f477c7a156ee08714ad", size = 2484587, upload-time = "2025-09-16T07:21:58.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5d/c9f54171b461ebec92d16ac5c1173f2ef345ae80c41fcd76b286c06b4189/cython-3.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:1e0671be9859bb313d8df5ca9b9c137e384f1e025831c58cee9a839ace432d3c", size = 2709502, upload-time = "2025-09-16T07:22:00.349Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ab/0a568bac7c4c052db4ae27edf01e16f3093cdfef04a2dfd313ef1b3c478a/cython-3.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d1d7013dba5fb0506794d4ef8947ff5ed021370614950a8d8d04e57c8c84499e", size = 3026389, upload-time = "2025-09-16T07:22:02.212Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b7/51f5566e1309215a7fef744975b2fabb56d3fdc5fa1922fd7e306c14f523/cython-3.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eed989f5c139d6550ef2665b783d86fab99372590c97f10a3c26c4523c5fce9e", size = 2955954, upload-time = "2025-09-16T07:22:03.782Z" }, + { url = "https://files.pythonhosted.org/packages/28/fd/ad8314520000fe96292fb8208c640fa862baa3053d2f3453a2acb50cafb8/cython-3.1.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df3beb8b024dfd73cfddb7f2f7456751cebf6e31655eed3189c209b634bc2f2", size = 3412005, upload-time = "2025-09-16T07:22:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3b/e570f8bcb392e7943fc9a25d1b2d1646ef0148ff017d3681511acf6bbfdc/cython-3.1.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8354703f1168e1aaa01348940f719734c1f11298be333bdb5b94101d49677c0", size = 3191100, upload-time = "2025-09-16T07:22:07.144Z" }, + { url = "https://files.pythonhosted.org/packages/78/81/f1ea09f563ebab732542cb11bf363710e53f3842458159ea2c160788bc8e/cython-3.1.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a928bd7d446247855f54f359057ab4a32c465219c8c1e299906a483393a59a9e", size = 3313786, upload-time = "2025-09-16T07:22:09.15Z" }, + { url = "https://files.pythonhosted.org/packages/ca/17/06575eb6175a926523bada7dac1cd05cc74add96cebbf2e8b492a2494291/cython-3.1.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c233bfff4cc7b9d629eecb7345f9b733437f76dc4441951ec393b0a6e29919fc", size = 3205775, upload-time = "2025-09-16T07:22:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/10/ba/61a8cf56a76ab21ddf6476b70884feff2a2e56b6d9010e1e1b1e06c46f70/cython-3.1.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e9691a2cbc2faf0cd819108bceccf9bfc56c15a06d172eafe74157388c44a601", size = 3428423, upload-time = "2025-09-16T07:22:12.404Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/42cf9239088d6b4b62c1c017c36e0e839f64c8d68674ce4172d0e0168d3b/cython-3.1.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ada319207432ea7c6691c70b5c112d261637d79d21ba086ae3726fedde79bfbf", size = 3330489, upload-time = "2025-09-16T07:22:14.576Z" }, + { url = "https://files.pythonhosted.org/packages/b5/08/36a619d6b1fc671a11744998e5cdd31790589e3cb4542927c97f3f351043/cython-3.1.4-cp311-cp311-win32.whl", hash = "sha256:dae81313c28222bf7be695f85ae1d16625aac35a0973a3af1e001f63379440c5", size = 2482410, upload-time = "2025-09-16T07:22:17.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/58/7d9ae7944bcd32e6f02d1a8d5d0c3875125227d050e235584127f2c64ffd/cython-3.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:60d2f192059ac34c5c26527f2beac823d34aaa766ef06792a3b7f290c18ac5e2", size = 2713755, upload-time = "2025-09-16T07:22:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/2939c739cfdc67ab94935a2c4fcc75638afd15e1954552655503a4112e92/cython-3.1.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d26af46505d0e54fe0f05e7ad089fd0eed8fa04f385f3ab88796f554467bcb9", size = 3062976, upload-time = "2025-09-16T07:22:20.517Z" }, + { url = "https://files.pythonhosted.org/packages/eb/bd/a84de57fd01017bf5dba84a49aeee826db21112282bf8d76ab97567ee15d/cython-3.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66ac8bb5068156c92359e3f0eefa138c177d59d1a2e8a89467881fa7d06aba3b", size = 2970701, upload-time = "2025-09-16T07:22:22.644Z" }, + { url = "https://files.pythonhosted.org/packages/71/79/a09004c8e42f5be188c7636b1be479cdb244a6d8837e1878d062e4e20139/cython-3.1.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2e42714faec723d2305607a04bafb49a48a8d8f25dd39368d884c058dbcfbc", size = 3387730, upload-time = "2025-09-16T07:22:24.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bd/979f8c59e247f562642f3eb98a1b453530e1f7954ef071835c08ed2bf6ba/cython-3.1.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0fd655b27997a209a574873304ded9629de588f021154009e8f923475e2c677", size = 3167289, upload-time = "2025-09-16T07:22:26.35Z" }, + { url = "https://files.pythonhosted.org/packages/34/f8/0b98537f0b4e8c01f76d2a6cf75389987538e4d4ac9faf25836fd18c9689/cython-3.1.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9def7c41f4dc339003b1e6875f84edf059989b9c7f5e9a245d3ce12c190742d9", size = 3321099, upload-time = "2025-09-16T07:22:27.957Z" }, + { url = "https://files.pythonhosted.org/packages/f3/39/437968a2e7c7f57eb6e1144f6aca968aa15fbbf169b2d4da5d1ff6c21442/cython-3.1.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:196555584a8716bf7e017e23ca53e9f632ed493f9faa327d0718e7551588f55d", size = 3179897, upload-time = "2025-09-16T07:22:30.014Z" }, + { url = "https://files.pythonhosted.org/packages/2c/04/b3f42915f034d133f1a34e74a2270bc2def02786f9b40dc9028fbb968814/cython-3.1.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7fff0e739e07a20726484b8898b8628a7b87acb960d0fc5486013c6b77b7bb97", size = 3400936, upload-time = "2025-09-16T07:22:31.705Z" }, + { url = "https://files.pythonhosted.org/packages/21/eb/2ad9fa0896ab6cf29875a09a9f4aaea37c28b79b869a013bf9b58e4e652e/cython-3.1.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2754034fa10f95052949cd6b07eb2f61d654c1b9cfa0b17ea53a269389422e8", size = 3332131, upload-time = "2025-09-16T07:22:33.32Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bf/f19283f8405e7e564c3353302a8665ea2c589be63a8e1be1b503043366a9/cython-3.1.4-cp312-cp312-win32.whl", hash = "sha256:2e0808ff3614a1dbfd1adfcbff9b2b8119292f1824b3535b4a173205109509f8", size = 2487672, upload-time = "2025-09-16T07:22:35.227Z" }, + { url = "https://files.pythonhosted.org/packages/30/bf/32150a2e6c7b50b81c5dc9e942d41969400223a9c49d04e2ed955709894c/cython-3.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:f262b32327b6bce340cce5d45bbfe3972cb62543a4930460d8564a489f3aea12", size = 2705348, upload-time = "2025-09-16T07:22:37.922Z" }, + { url = "https://files.pythonhosted.org/packages/24/10/1acc34f4d2d14de38e2d3ab4795ad1c8f547cebc2d9e7477a49a063ba607/cython-3.1.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ab549d0fc187804e0f14fc4759e4b5ad6485ffc01554b2f8b720cc44aeb929cd", size = 3051524, upload-time = "2025-09-16T07:22:40.607Z" }, + { url = "https://files.pythonhosted.org/packages/04/85/8457a78e9b9017a4fb0289464066ff2e73c5885f1edb9c1b9faaa2877fe2/cython-3.1.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52eae5d9bcc515441a436dcae2cbadfd00c5063d4d7809bd0178931690c06a76", size = 2958862, upload-time = "2025-09-16T07:22:42.646Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/42989748b63ec56c5b950fd26ec01fc77f9cf72dc318eb2eee257a6b652c/cython-3.1.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f06345cfa583dd17fff1beedb237853689b85aa400ea9e0db7e5265f3322d15", size = 3364296, upload-time = "2025-09-16T07:22:44.444Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b27d5d402552932875f2b8f795385dabd27525a8a6645010c876fe84a0f9/cython-3.1.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d915556c757212cb8ddd4e48c16f2ab481dbb9a76f5153ab26f418c3537eb5", size = 3154391, upload-time = "2025-09-16T07:22:46.852Z" }, + { url = "https://files.pythonhosted.org/packages/65/55/742737e40f7a3f1963440d66322b5fa93844762dd7a3a23d9b5b1d0d594e/cython-3.1.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f3bb603f28b3c1df66baaa5cdbf6029578552b458f1d321bae23b87f6c3199", size = 3305883, upload-time = "2025-09-16T07:22:48.55Z" }, + { url = "https://files.pythonhosted.org/packages/98/3f/0baecd7ac0fac2dbb47acd7f0970c298f38d0504774c5552cf6224bdf3e6/cython-3.1.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7aff230893ee1044e7bc98d313c034ead70a3dd54d4d22e89ca1734540d94084", size = 3170437, upload-time = "2025-09-16T07:22:50.213Z" }, + { url = "https://files.pythonhosted.org/packages/74/18/1ec53e2cf10a8064c7faa305b105b9c45af619ee30a6f1f7eb91efbb304b/cython-3.1.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e83f114c04f72f85591ddb0b28f08ab2e40d250c26686d6509c0f70a9e2ca34", size = 3377458, upload-time = "2025-09-16T07:22:52.192Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8c/3d0839cf0b315157974bf283d4bd658f5c30277091ad34c093f286c59e0f/cython-3.1.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8096394960d38b793545753b73781bc0ec695f0b8c22454431704b297e296045", size = 3318723, upload-time = "2025-09-16T07:22:54.322Z" }, + { url = "https://files.pythonhosted.org/packages/c6/05/67b4de710a3109030d868e23d5dccf35559afa4c089b4c0aa9e22ffda1f1/cython-3.1.4-cp313-cp313-win32.whl", hash = "sha256:4e7c726ac753ca1a5aa30286cbadcd10ed4b4312ea710a8a16bb908d41e9c059", size = 2481433, upload-time = "2025-09-16T07:22:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/89/ef/f179b5a46185bc5550c07b328d687ee32251963a3a93e869b75fbf97181c/cython-3.1.4-cp313-cp313-win_amd64.whl", hash = "sha256:f2ee2bb77943044f301cec04d0b51d8e3810507c9c250d6cd079a3e2d6ba88f2", size = 2703057, upload-time = "2025-09-16T07:22:57.994Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/f1380e8370b470b218e452ba3995555524e3652f026333e6bad6c68770b5/cython-3.1.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7258739d5560918741cb040bd85ba7cc2f09d868de9116a637e06714fec1f69", size = 3045864, upload-time = "2025-09-16T07:22:59.854Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/54c7bc78df1e55ac311054cb2fd33908f23b8a6f350c30defeca416d8077/cython-3.1.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b2d522ee8d3528035e247ee721fb40abe92e9ea852dc9e48802cec080d5de859", size = 2967105, upload-time = "2025-09-16T07:23:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/02/02/89f70e71972f796863429b159c8e8e858b85bedbc9c747d167a5c6f6417e/cython-3.1.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4e0560baeb56c29d7d8d693a050dd4d2ed922d8d7c66f5c5715c6f2be84e903", size = 3363386, upload-time = "2025-09-16T07:23:03.39Z" }, + { url = "https://files.pythonhosted.org/packages/2a/34/eda836ae260013d4dd1c7aaa8dd6f7d7862206ba3354db5d8f55a8f6ef67/cython-3.1.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4223cacc81cba0df0f06f79657c5d6286e153b9a9b989dad1cdf4666f618c073", size = 3192314, upload-time = "2025-09-16T07:23:05.354Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fa/db8224f7fe7ec1ebdab0b5e71b5a8269c112645c4eac2464ef0735bb395e/cython-3.1.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff4d1f159edee6af38572318681388fbd6448b0d08b9a47494aaf0b698e93394", size = 3312222, upload-time = "2025-09-16T07:23:07.066Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/419262657800dee7202a76956cd52896a6e8793bbbecc2592a4ebba2e034/cython-3.1.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2537c53071a9a124e0bc502a716e1930d9bb101e94c26673016cf1820e4fdbd1", size = 3208798, upload-time = "2025-09-16T07:23:08.758Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d8/f140c7b9356a29660dc05591272e33062df964b9d1a072d09e89ade41087/cython-3.1.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:85416717c529fb5ccf908464657a5187753e76d7b6ffec9b1c2d91544f6c3628", size = 3379662, upload-time = "2025-09-16T07:23:10.511Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/83cf9a9cf64cbfe4eaf3987a633be08243f838b7d12e5739831297b77311/cython-3.1.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:18882e2f5c0e0c25f9c44f16f2fb9c48f33988885c5f9eae2856f10c6f089ffa", size = 3324255, upload-time = "2025-09-16T07:23:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f8/f2033044687cf6296275fa71cdf63a247d3646a3e276aa002e65bf505f46/cython-3.1.4-cp314-cp314-win32.whl", hash = "sha256:8ef8deadc888eaf95e5328fc176fb6c37bccee1213f07517c6ea55b5f817c457", size = 2503665, upload-time = "2025-09-16T07:23:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/7af75a803d55610d570d7b7a0fdc2bfd82fae030c728089cc628562d67f9/cython-3.1.4-cp314-cp314-win_amd64.whl", hash = "sha256:acb99ddec62ba1ea5de0e0087760fa834ec42c94f0488065a4f1995584e8e94e", size = 2734608, upload-time = "2025-09-16T07:23:16.025Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/f7351052cf9db771fe4f32fca47fd66e6d9b53d8613b17faf7d130a9d553/cython-3.1.4-py3-none-any.whl", hash = "sha256:d194d95e4fa029a3f6c7d46bdd16d973808c7ea4797586911fdb67cb98b1a2c6", size = 1227541, upload-time = "2025-09-16T07:20:29.595Z" }, +] + [[package]] name = "dataclasses-json" version = "0.6.7" @@ -976,6 +1102,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, ] +[[package]] +name = "datamodeldict" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xmltodict" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/da/c5709322a1ddab201d37e2614edb1a90c6b64d5c0f02f9eae70f26847075/DataModelDict-0.9.9.tar.gz", hash = "sha256:0da74146c73ca84bbd3d680c3659464b611228bee48012c3860e320ebf3b5919", size = 16379, upload-time = "2022-02-17T15:57:43.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/6b/fe3b022663e3589ee811fc5a499830370170b45244629670cdc99fd3be9e/DataModelDict-0.9.9-py3-none-any.whl", hash = "sha256:0549f80bf8e1d4725397fb737d495da8089828b1af4e5202e31c5d7b03fea852", size = 15040, upload-time = "2022-02-17T15:57:41.227Z" }, +] + +[[package]] +name = "dateparser" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/30/064144f0df1749e7bb5faaa7f52b007d7c2d08ec08fed8411aba87207f68/dateparser-1.2.2.tar.gz", hash = "sha256:986316f17cb8cdc23ea8ce563027c5ef12fc725b6fb1d137c14ca08777c5ecf7", size = 329840, upload-time = "2025-06-26T09:29:23.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" }, +] + [[package]] name = "ddgs" version = "9.5.5" @@ -1509,6 +1662,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "habanero" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/79/e1463d0829f1b9d9a140ea78dda9461c13a8d9589797ab1ff5b7fffd133f/habanero-2.3.0.tar.gz", hash = "sha256:871e5d088ef641b05514d44b004af512852b309bcd0d81f10545e58d54a654ab", size = 1756677, upload-time = "2025-06-06T16:30:24.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/76/3e7d4c17024e70b1be20623937bde945842480b84d6f7dd49bd0ef372d84/habanero-2.3.0-py3-none-any.whl", hash = "sha256:ba44dd33d562ff33bd778fd7f94bfac3bf805b0b0963976f379e0a1d6894329e", size = 28453, upload-time = "2025-06-06T16:30:22.535Z" }, +] + [[package]] name = "hf-xet" version = "1.1.8" @@ -1524,6 +1693,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/d3/0aaf279f4f3dea58e99401b92c31c0f752924ba0e6c7d7bb07b1dbd7f35e/hf_xet-1.1.8-cp37-abi3-win_amd64.whl", hash = "sha256:4171f31d87b13da4af1ed86c98cf763292e4720c088b4957cf9d564f92904ca9", size = 2801689, upload-time = "2025-08-18T22:01:04.81Z" }, ] +[[package]] +name = "htmldate" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "charset-normalizer", marker = "sys_platform == 'darwin'" }, + { name = "dateparser", marker = "sys_platform == 'darwin'" }, + { name = "lxml", marker = "sys_platform == 'darwin'" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin'" }, + { name = "urllib3", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/19/9457e697df4f3953bbc61b17d6b1119d812c8d893425fa7771bd2e237706/htmldate-1.4.3.tar.gz", hash = "sha256:ec50f084b997fdf6b26f8c31447e5789f4deb71fe69342cda1d7af0c9f91e01b", size = 53406, upload-time = "2023-05-03T09:11:48.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/16/769d73b31be20705a390479792fed45eaadda238746dbb1cf2615ad31402/htmldate-1.4.3-py3-none-any.whl", hash = "sha256:d529a319a2fae8329c2beaa54c45af9295d0eca425dfba33b81e4665e8e8a78e", size = 41031, upload-time = "2023-05-03T09:11:44.283Z" }, +] + +[[package]] +name = "htmldate" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", +] +dependencies = [ + { name = "charset-normalizer", marker = "sys_platform != 'darwin'" }, + { name = "dateparser", marker = "sys_platform != 'darwin'" }, + { name = "lxml", marker = "sys_platform != 'darwin'" }, + { name = "python-dateutil", marker = "sys_platform != 'darwin'" }, + { name = "urllib3", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/fb/26213bb6300dd7d17afb131d222b4f7e083d822d0fd72089eb60e3b134c1/htmldate-1.6.0.tar.gz", hash = "sha256:5827c8f626a16800a29e57e8188a3d32d0b08ca4c7bd662537b73bbbf22c45a6", size = 53558, upload-time = "2023-11-21T15:57:23.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/04/7441f54565d2da01ecab4220ff6bb0e959dbae648001a3eebbf44f9c77fb/htmldate-1.6.0-py3-none-any.whl", hash = "sha256:6ee374849fe7491b3e6c0b26066e8f6940367b0215e7c4fec88774af065a4dbc", size = 40727, upload-time = "2023-11-21T15:57:20.945Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -1707,7 +1927,8 @@ version = "8.37.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, @@ -1734,12 +1955,16 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, @@ -1771,6 +1996,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, ] +[[package]] +name = "ipywidgets" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/48/d3dbac45c2814cb73812f98dd6b38bbcc957a4e7bb31d6ea9c03bf94ed87/ipywidgets-8.1.7.tar.gz", hash = "sha256:15f1ac050b9ccbefd45dccfbb2ef6bed0029d8278682d569d71b8dd96bee0376", size = 116721, upload-time = "2025-05-05T12:42:03.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/6a/9166369a2f092bd286d24e6307de555d63616e8ddb373ebad2b5635ca4cd/ipywidgets-8.1.7-py3-none-any.whl", hash = "sha256:764f2602d25471c213919b8a1997df04bef869251db4ca8efba1b76b1bd9f7bb", size = 139806, upload-time = "2025-05-05T12:41:56.833Z" }, +] + [[package]] name = "isoduration" version = "20.11.0" @@ -2057,7 +2299,7 @@ dependencies = [ { name = "overrides" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt' and sys_platform != 'darwin'" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2075,7 +2317,7 @@ name = "jupyter-server-terminals" version = "0.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt' and sys_platform != 'darwin'" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } @@ -2135,6 +2377,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700, upload-time = "2024-07-16T17:02:01.115Z" }, ] +[[package]] +name = "jupyterlab-widgets" +version = "3.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/7d/160595ca88ee87ac6ba95d82177d29ec60aaa63821d3077babb22ce031a5/jupyterlab_widgets-3.0.15.tar.gz", hash = "sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b", size = 213149, upload-time = "2025-05-05T12:32:31.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/6a/ca128561b22b60bd5a0c4ea26649e68c8556b82bc70a0c396eebc977fe86/jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c", size = 216571, upload-time = "2025-05-05T12:32:29.534Z" }, +] + +[[package]] +name = "justext" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml", extra = ["html-clean"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -2652,6 +2915,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/db/8f620f1ac62cf32554821b00b768dd5957ac8e3fd051593532be5b40b438/lxml-6.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:51bd5d1a9796ca253db6045ab45ca882c09c071deafffc22e06975b7ace36300", size = 3518127, upload-time = "2025-08-22T10:37:51.66Z" }, ] +[package.optional-dependencies] +html-clean = [ + { name = "lxml-html-clean" }, +] + +[[package]] +name = "lxml-html-clean" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/b6/466e71db127950fb8d172026a8f0a9f0dc6f64c8e78e2ca79f252e5790b8/lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3", size = 21622, upload-time = "2025-04-09T11:33:59.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184, upload-time = "2025-04-09T11:33:57.988Z" }, +] + [[package]] name = "maggma" version = "0.72.0" @@ -3282,7 +3562,8 @@ version = "3.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ @@ -3296,12 +3577,16 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } wheels = [ @@ -3345,13 +3630,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] +[[package]] +name = "numericalunits" +version = "1.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/f3/f52ab9234b9dc057729a03f1b84dd7ad51b3a4ef018e68a44d563e880556/numericalunits-1.26.tar.gz", hash = "sha256:8a0b69945dd65eacf6eef8c868bcd3298d7439f5882f507bb6060ec20c723e12", size = 18263, upload-time = "2024-11-26T18:02:53.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/59/a811abad42d63d7b649a95f370dc30d8c56f254fd8e1c659207b995abe81/numericalunits-1.26-py3-none-any.whl", hash = "sha256:579ec38610052c2f4de88119ba0437a76834c4bf81739ac7045412029f0f300e", size = 14257, upload-time = "2024-11-26T18:02:51.937Z" }, +] + [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -3418,12 +3713,16 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", ] sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } wheels = [ @@ -4037,6 +4336,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, ] +[[package]] +name = "potentials" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bibtexparser" }, + { name = "cdcs" }, + { name = "datamodeldict" }, + { name = "habanero" }, + { name = "ipywidgets" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "requests" }, + { name = "unidecode" }, + { name = "xmltodict" }, + { name = "yabadaba" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/96/27415408e764469c53b58c73777752439241c01e8c7bb044b29826d917cc/potentials-0.4.1.tar.gz", hash = "sha256:4902c32da8586f74cd3bd05f0c02c0e910f26796d7d2ce3abead7df88b9b8ead", size = 170096, upload-time = "2025-07-07T20:21:57.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2e/011b2e1686fe4b47283848e3663a7c3a47fe3fbadc71074c4e10dc36ae7d/potentials-0.4.1-py3-none-any.whl", hash = "sha256:05fc865e5c80dc5e88827fc66e293d3a3fc347cbabf5261af5e004f7bfbcb2f4", size = 222618, upload-time = "2025-07-07T20:21:56.446Z" }, +] + [[package]] name = "pre-commit" version = "4.3.0" @@ -5429,7 +5752,8 @@ version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -5490,12 +5814,16 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", ] dependencies = [ { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -5791,7 +6119,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt' and sys_platform != 'darwin'" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -5856,6 +6184,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, ] +[[package]] +name = "tld" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" }, +] + [[package]] name = "tokenizers" version = "0.21.4" @@ -5920,6 +6257,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790, upload-time = "2024-10-04T16:17:04.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" }, +] + [[package]] name = "tornado" version = "6.5.2" @@ -5951,6 +6297,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "trafilatura" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "certifi", marker = "sys_platform == 'darwin'" }, + { name = "charset-normalizer", marker = "sys_platform == 'darwin'" }, + { name = "courlan", marker = "sys_platform == 'darwin'" }, + { name = "htmldate", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "justext", marker = "sys_platform == 'darwin'" }, + { name = "lxml", marker = "sys_platform == 'darwin'" }, + { name = "urllib3", marker = "sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d8/e9f20c95d2e1189258a84467956a00d85a68563cbaec19920302b8a414f6/trafilatura-1.6.1.tar.gz", hash = "sha256:a7792b037d624d04ab05fcce556cfe08b771dfc8c1db1494c750879530c9a30c", size = 3853959, upload-time = "2023-06-15T12:59:19.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/47/d3523bdbf4e49233a17c2af6964b3dc9d8707c862847c49b324fbd7f9ba4/trafilatura-1.6.1-py3-none-any.whl", hash = "sha256:fe94ed68fb50ec80ae698095010e1a0a7827bee0542ecd33c2cb55ca3e985aa7", size = 1031276, upload-time = "2023-06-15T12:59:12.869Z" }, +] + +[[package]] +name = "trafilatura" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'", +] +dependencies = [ + { name = "certifi", marker = "sys_platform != 'darwin'" }, + { name = "charset-normalizer", marker = "sys_platform != 'darwin'" }, + { name = "courlan", marker = "sys_platform != 'darwin'" }, + { name = "htmldate", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, + { name = "justext", marker = "sys_platform != 'darwin'" }, + { name = "lxml", marker = "sys_platform != 'darwin'" }, + { name = "urllib3", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/2d/e91ca57ca6ead5bf72e2c651fc81d47052c4c794a27d729598fba90404b4/trafilatura-1.6.3.tar.gz", hash = "sha256:671dd6e0000e101c4bce8d70f4408bcb79fcbf2275ee25591efe33e2c8a1600d", size = 3865064, upload-time = "2023-11-29T13:42:25.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/c3/4c3dc70948b8727dd4dd9634634bf5f985d14201b23cb3eb453a0d98d075/trafilatura-1.6.3-py3-none-any.whl", hash = "sha256:4f879045c6854d71e488cc669c42ee83b97fe584684bd288c752b7e5ae7402cf", size = 1033241, upload-time = "2023-11-29T13:42:16.879Z" }, +] + [[package]] name = "traitlets" version = "5.14.3" @@ -6027,6 +6428,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + [[package]] name = "uncertainties" version = "3.2.3" @@ -6036,6 +6449,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl", hash = "sha256:313353900d8f88b283c9bad81e7d2b2d3d4bcc330cbace35403faaed7e78890a", size = 60118, upload-time = "2025-04-21T19:58:26.864Z" }, ] +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -6059,6 +6481,7 @@ name = "ursa-ai" source = { editable = "." } dependencies = [ { name = "arxiv" }, + { name = "atomman" }, { name = "beautifulsoup4" }, { name = "chromadb" }, { name = "coolname" }, @@ -6079,6 +6502,8 @@ dependencies = [ { name = "pymupdf" }, { name = "pypdf" }, { name = "rich" }, + { name = "trafilatura", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "trafilatura", version = "1.6.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, ] [package.dev-dependencies] @@ -6092,6 +6517,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "arxiv", specifier = ">=2.2.0,<3.0" }, + { name = "atomman", specifier = ">=1.5.2" }, { name = "beautifulsoup4", specifier = ">=4.13.4,<5.0" }, { name = "chromadb", specifier = ">=1.0.20,<1.1" }, { name = "coolname", specifier = ">=2.2.0,<3.0" }, @@ -6112,6 +6538,7 @@ requires-dist = [ { name = "pymupdf", specifier = ">=1.26.0,<2.0" }, { name = "pypdf", specifier = ">=5.9.0,<6.0" }, { name = "rich", specifier = ">=13.9.4,<14.0" }, + { name = "trafilatura", specifier = ">=1.6.1" }, ] [package.metadata.requires-dev] @@ -6389,6 +6816,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] +[[package]] +name = "widgetsnbextension" +version = "4.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/53/2e0253c5efd69c9656b1843892052a31c36d37ad42812b5da45c62191f7e/widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af", size = 1097428, upload-time = "2025-04-10T13:01:25.628Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/51/5447876806d1088a0f8f71e16542bf350918128d0a69437df26047c8e46f/widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575", size = 2196503, upload-time = "2025-04-10T13:01:23.086Z" }, +] + [[package]] name = "wrapt" version = "1.17.3" @@ -6458,6 +6894,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] +[[package]] +name = "xmltodict" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/aa/917ceeed4dbb80d2f04dbd0c784b7ee7bba8ae5a54837ef0e5e062cd3cfb/xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649", size = 25725, upload-time = "2025-09-17T21:59:26.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/20/69a0e6058bc5ea74892d089d64dfc3a62ba78917ec5e2cfa70f7c92ba3a5/xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d", size = 13893, upload-time = "2025-09-17T21:59:24.859Z" }, +] + [[package]] name = "xxhash" version = "3.5.0" @@ -6531,6 +6976,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/e3/bef7b82c1997579c94de9ac5ea7626d01ae5858aa22bf4fcb38bf220cb3e/xxhash-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a74f23335b9689b66eb6dbe2a931a88fcd7a4c2cc4b1cb0edba8ce381c7a1da", size = 30064, upload-time = "2024-08-17T09:20:15.925Z" }, ] +[[package]] +name = "yabadaba" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cdcs" }, + { name = "datamodeldict" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "lxml" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pymongo" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/c5/0fa03ab364f8d66335802046e5ca38f241f0b7e82c790456c2cb77ff32d6/yabadaba-0.3.2.tar.gz", hash = "sha256:fd4bc16914c463df83f146d909709761aa158b842808e71c89f3462de08cea26", size = 59136, upload-time = "2025-07-07T20:03:51.548Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/98/196ac9775756c0788b4f660c0e3153e9a1e355df075ac086a2238a97ee33/yabadaba-0.3.2-py3-none-any.whl", hash = "sha256:82c9b28616ae7e955452f0a49ef48e1a97b3b5cf0e141b908b70dbc5128e2065", size = 89959, upload-time = "2025-07-07T20:03:50.349Z" }, +] + [[package]] name = "yarl" version = "1.20.1" From 576f1ebd9c8107f4a7a62685123db382bf175c98 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Thu, 18 Sep 2025 17:11:05 -0600 Subject: [PATCH 09/31] Linted and formatted code using ruff --- .../lammps_execute/EOS_of_Cu.py | 41 +++--- .../lammps_execute/stiffness_tensor_of_hea.py | 41 +++--- src/ursa/agents/__init__.py | 4 +- src/ursa/agents/lammps_agent.py | 135 ++++++++++-------- 4 files changed, 121 insertions(+), 100 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index b82a4cc2..48b5787e 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -1,36 +1,33 @@ -from ursa.agents import LammpsAgent -from ursa.agents import ExecutionAgent - from langchain_core.messages import HumanMessage from langchain_openai import ChatOpenAI +from ursa.agents import ExecutionAgent, LammpsAgent + model = "gpt-5" -llm = ChatOpenAI(model = model, - timeout = None, - max_retries = 2 - ) +llm = ChatOpenAI(model=model, timeout=None, max_retries=2) workspace = "./workspace_eos_cu" wf = LammpsAgent( - llm = llm, + llm=llm, max_potentials=2, - max_fix_attempts=5, + max_fix_attempts=5, mpi_procs=8, - workspace = workspace, + workspace=workspace, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", ) -simulation_task="Carry out a LAMMPS simulation of Cu to determine its equation of state." -elements=["Cu"] +simulation_task = ( + "Carry out a LAMMPS simulation of Cu to determine its equation of state." +) +elements = ["Cu"] -final_lammps_state = wf.run(simulation_task,elements) +final_lammps_state = wf.run(simulation_task, elements) if final_lammps_state.get("run_returncode") == 0: - - print ("\nNow handing things off to execution agent.....") + print("\nNow handing things off to execution agent.....") executor = ExecutionAgent(llm=llm) exe_plan = f""" @@ -42,10 +39,14 @@ """ executor_config = {"recursion_limit": 999_999} - - final_results = executor.action.invoke({"messages": [HumanMessage(content=exe_plan)], - "workspace": workspace,},executor_config,) - + + final_results = executor.action.invoke( + { + "messages": [HumanMessage(content=exe_plan)], + "workspace": workspace, + }, + executor_config, + ) + for x in final_results["messages"]: print(x.content) - diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 4e4bd498..c6a66c47 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -1,38 +1,33 @@ -from ursa.agents import LammpsAgent -from ursa.agents import ExecutionAgent - from langchain_core.messages import HumanMessage from langchain_openai import ChatOpenAI +from ursa.agents import ExecutionAgent, LammpsAgent + model = "gpt-5" -llm = ChatOpenAI(model = model, - timeout = None, - max_retries = 2 - ) +llm = ChatOpenAI(model=model, timeout=None, max_retries=2) workspace = "./workspace_stiffness_tensor" wf = LammpsAgent( - llm = llm, + llm=llm, max_potentials=5, - max_fix_attempts=15, + max_fix_attempts=15, mpi_procs=8, - workspace = workspace, + workspace=workspace, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", ) -simulation_task="Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." +simulation_task = "Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." -elements=["Co","Cr","Fe","Mn","Ni"] +elements = ["Co", "Cr", "Fe", "Mn", "Ni"] -final_lammps_state = wf.run(simulation_task,elements) +final_lammps_state = wf.run(simulation_task, elements) if final_lammps_state.get("run_returncode") == 0: - - print ("\nNow handing things off to execution agent.....") + print("\nNow handing things off to execution agent.....") executor = ExecutionAgent(llm=llm) exe_plan = f""" @@ -42,12 +37,16 @@ Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - + executor_config = {"recursion_limit": 999_999} - - final_results = executor.action.invoke({"messages": [HumanMessage(content=exe_plan)], - "workspace": workspace,},executor_config,) - + + final_results = executor.action.invoke( + { + "messages": [HumanMessage(content=exe_plan)], + "workspace": workspace, + }, + executor_config, + ) + for x in final_results["messages"]: print(x.content) - diff --git a/src/ursa/agents/__init__.py b/src/ursa/agents/__init__.py index 982ea75f..831c7972 100644 --- a/src/ursa/agents/__init__.py +++ b/src/ursa/agents/__init__.py @@ -9,11 +9,11 @@ from .execution_agent import ExecutionState as ExecutionState from .hypothesizer_agent import HypothesizerAgent as HypothesizerAgent from .hypothesizer_agent import HypothesizerState as HypothesizerState +from .lammps_agent import LammpsAgent as LammpsAgent +from .lammps_agent import LammpsState as LammpsState from .mp_agent import MaterialsProjectAgent as MaterialsProjectAgent from .planning_agent import PlanningAgent as PlanningAgent from .planning_agent import PlanningState as PlanningState from .recall_agent import RecallAgent as RecallAgent from .websearch_agent import WebSearchAgent as WebSearchAgent from .websearch_agent import WebSearchState as WebSearchState -from .lammps_agent import LammpsAgent as LammpsAgent -from .lammps_agent import LammpsState as LammpsState diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 4549a17b..4c503ab4 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -1,16 +1,14 @@ -from typing import TypedDict, List, Optional, Any, Dict import json +import os import subprocess -import os +from typing import Any, Dict, List, Optional, TypedDict import atomman as am -import trafilatura import tiktoken - -from langchain_core.prompts import ChatPromptTemplate +import trafilatura from langchain_core.output_parsers import StrOutputParser - -from langgraph.graph import StateGraph, END +from langchain_core.prompts import ChatPromptTemplate +from langgraph.graph import END, StateGraph from .base import BaseAgent @@ -18,7 +16,7 @@ class LammpsState(TypedDict, total=False): simulation_task: str elements: List[str] - + matches: List[Any] db_message: str @@ -35,8 +33,7 @@ class LammpsState(TypedDict, total=False): run_stdout: str run_stderr: str - fix_attempts: int - + fix_attempts: int class LammpsAgent(BaseAgent): @@ -44,16 +41,15 @@ def __init__( self, llm, max_potentials: int = 5, - max_fix_attempts: int = 10, + max_fix_attempts: int = 10, mpi_procs: int = 8, workspace: str = "./workspace", - lammps_cmd: str = "lmp_mpi", + lammps_cmd: str = "lmp_mpi", mpirun_cmd: str = "mpirun", tiktoken_model: str = "o3", max_tokens: int = 200000, **kwargs, ): - self.max_potentials = max_potentials self.max_fix_attempts = max_fix_attempts self.mpi_procs = mpi_procs @@ -62,26 +58,36 @@ def __init__( self.tiktoken_model = tiktoken_model self.max_tokens = max_tokens - self.pair_styles = ["eam", "eam/alloy", "eam/fs", - "meam", "adp", # classical, HEA-relevant - "kim", # OpenKIM models - "snap", "quip", "mlip", "pace", "nep" # ML/ACE families (if available) - ] + self.pair_styles = [ + "eam", + "eam/alloy", + "eam/fs", + "meam", + "adp", # classical, HEA-relevant + "kim", # OpenKIM models + "snap", + "quip", + "mlip", + "pace", + "nep", # ML/ACE families (if available) + ] self.workspace = workspace os.makedirs(self.workspace, exist_ok=True) - super().__init__(llm, **kwargs) - + self.str_parser = StrOutputParser() - self.summ_chain = (ChatPromptTemplate.from_template( - "Here is some data about an interatomic potential: {metadata}\n\n" - "Briefly summarize why it could be useful for this task: {simulation_task}." - ) | self.llm | self.str_parser) + self.summ_chain = ( + ChatPromptTemplate.from_template( + "Here is some data about an interatomic potential: {metadata}\n\n" + "Briefly summarize why it could be useful for this task: {simulation_task}." + ) + | self.llm + | self.str_parser + ) - self.choose_chain = ( ChatPromptTemplate.from_template( "Here are the summaries of a certain number of interatomic potentials: {summaries_combined}\n\n" @@ -97,7 +103,7 @@ def __init__( | self.llm | self.str_parser ) - + self.author_chain = ( ChatPromptTemplate.from_template( "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" @@ -115,7 +121,7 @@ def __init__( | self.llm | self.str_parser ) - + self.fix_chain = ( ChatPromptTemplate.from_template( "You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task}\n" @@ -138,11 +144,8 @@ def __init__( | self.str_parser ) - self.graph = self._build_graph().compile() - - @staticmethod def _safe_json_loads(s: str) -> Dict[str, Any]: s = s.strip() @@ -177,10 +180,11 @@ def _fetch_and_trim_text(self, url: str) -> str: pass return text - def _find_potentials(self, state: LammpsState) -> LammpsState: db = am.library.Database(remote=True) - matches = db.get_lammps_potentials(pair_style=self.pair_styles, elements=state["elements"]) + matches = db.get_lammps_potentials( + pair_style=self.pair_styles, elements=state["elements"] + ) msg_lines = [] if not list(matches): msg_lines.append("No potentials found for this task in NIST.") @@ -202,7 +206,7 @@ def _should_summarize(self, state: LammpsState) -> str: matches = state.get("matches", []) i = state.get("idx", 0) if not matches: - print ("No potentials found in NIST for this task. Exiting....") + print("No potentials found in NIST for this task. Exiting....") return "done_no_matches" if i < min(self.max_potentials, len(matches)): return "summarize_one" @@ -210,7 +214,7 @@ def _should_summarize(self, state: LammpsState) -> str: def _summarize_one(self, state: LammpsState) -> LammpsState: i = state["idx"] - print (f"Summarizing potential #{i}") + print(f"Summarizing potential #{i}") match = state["matches"][i] md = match.metadata() @@ -220,8 +224,15 @@ def _summarize_one(self, state: LammpsState) -> LammpsState: else: lines = md["comments"].split("\n") url = lines[1] if len(lines) > 1 else "" - text = self._fetch_and_trim_text(url) if url else "No metadata available" - summary = self.summ_chain.invoke({"metadata": text, "simulation_task": state["simulation_task"]}) + text = ( + self._fetch_and_trim_text(url) + if url + else "No metadata available" + ) + summary = self.summ_chain.invoke({ + "metadata": text, + "simulation_task": state["simulation_task"], + }) return { **state, @@ -238,15 +249,15 @@ def _build_summaries(self, state: LammpsState) -> LammpsState: return {**state, "summaries_combined": "".join(parts)} def _choose(self, state: LammpsState) -> LammpsState: - print ("Choosing one potential for this task...") + print("Choosing one potential for this task...") choice = self.choose_chain.invoke({ "summaries_combined": state["summaries_combined"], - "simulation_task": state["simulation_task"] + "simulation_task": state["simulation_task"], }) choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) - print (f"Chosen potential #{chosen_index}") - print ("Rationale for choosing this potential:") + print(f"Chosen potential #{chosen_index}") + print("Rationale for choosing this potential:") print(choice_dict["rationale"]) return {**state, "choice_json": choice, "chosen_index": chosen_index} @@ -259,7 +270,7 @@ def _author(self, state: LammpsState) -> LammpsState: authored_json = self.author_chain.invoke({ "simulation_task": state["simulation_task"], "metadata": text, - "pair_info": pair_info + "pair_info": pair_info, }) script_dict = self._safe_json_loads(authored_json) input_script = script_dict["input_script"] @@ -268,14 +279,21 @@ def _author(self, state: LammpsState) -> LammpsState: return {**state, "input_script": input_script} def _run_lammps(self, state: LammpsState) -> LammpsState: - print ("Running LAMMPS....") + print("Running LAMMPS....") result = subprocess.run( - [self.mpirun_cmd, "-np", str(self.mpi_procs), self.lammps_cmd, "-in", "in.lammps"], + [ + self.mpirun_cmd, + "-np", + str(self.mpi_procs), + self.lammps_cmd, + "-in", + "in.lammps", + ], cwd=self.workspace, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, - check=False + check=False, ) return { **state, @@ -288,35 +306,37 @@ def _route_run(self, state: LammpsState) -> str: rc = state.get("run_returncode", 0) attempts = state.get("fix_attempts", 0) if rc == 0: - print ("LAMMPS run successful! Exiting...") + print("LAMMPS run successful! Exiting...") return "done_success" if attempts < self.max_fix_attempts: - print ("LAMMPS run Failed. Attempting to rewrite input file...") + print("LAMMPS run Failed. Attempting to rewrite input file...") return "need_fix" - print ("LAMMPS run Failed and maximum fix attempts reached. Exiting...") + print("LAMMPS run Failed and maximum fix attempts reached. Exiting...") return "done_failed" def _fix(self, state: LammpsState) -> LammpsState: match = state["matches"][state["chosen_index"]] text = state["full_texts"][state["chosen_index"]] pair_info = match.pair_info() - err_blob = state.get("run_stdout") - + err_blob = state.get("run_stdout") + fixed_json = self.fix_chain.invoke({ "simulation_task": state["simulation_task"], "input_script": state["input_script"], "err_message": err_blob, "metadata": text, - "pair_info": pair_info + "pair_info": pair_info, }) script_dict = self._safe_json_loads(fixed_json) new_input = script_dict["input_script"] with open(os.path.join(self.workspace, "in.lammps"), "w") as f: f.write(new_input) - return {**state, "input_script": new_input, "fix_attempts": state.get("fix_attempts", 0) + 1} - + return { + **state, + "input_script": new_input, + "fix_attempts": state.get("fix_attempts", 0) + 1, + } - def _build_graph(self): g = StateGraph(LammpsState) @@ -365,7 +385,8 @@ def _build_graph(self): g.add_edge("fix", "run_lammps") return g - - def run(self,simulation_task,elements): - return self.graph.invoke({"simulation_task": simulation_task,"elements": elements},{"recursion_limit": 999_999}) - \ No newline at end of file + def run(self, simulation_task, elements): + return self.graph.invoke( + {"simulation_task": simulation_task, "elements": elements}, + {"recursion_limit": 999_999}, + ) From c7cedef70fd0e4b697eecc2ec5ac3abc53867670 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Wed, 15 Oct 2025 10:29:12 -0600 Subject: [PATCH 10/31] Fixed minor bugs created from merging main-->lammps_agent --- examples/two_agent_examples/lammps_execute/EOS_of_Cu.py | 5 +---- .../lammps_execute/stiffness_tensor_of_hea.py | 7 ++----- src/ursa/agents/lammps_agent.py | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index e8dc11b2..01826932 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -40,14 +40,11 @@ Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - executor_config = {"recursion_limit": 999_999} - final_results = executor.invoke( { "messages": [HumanMessage(content=exe_plan)], "workspace": workspace, - }, - executor_config, + } ) for x in final_results["messages"]: diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index c15f231c..e7f01645 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -39,15 +39,12 @@ Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - - executor_config = {"recursion_limit": 999_999} - + final_results = executor.invoke( { "messages": [HumanMessage(content=exe_plan)], "workspace": workspace, - }, - executor_config, + } ) for x in final_results["messages"]: diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 685b9c9d..804242d9 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -399,7 +399,7 @@ def _invoke( inputs: Mapping[str, Any], *, summarize: bool | None = None, - recursion_limit: int = 1000, + recursion_limit: int = 999_999, **_, ) -> str: config = self.build_config( From 7e6e277c8e00cdafc5f79028421b29e3e535dfda Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 17 Oct 2025 16:22:24 -0600 Subject: [PATCH 11/31] Added option of providing input file templates to lammps_agent --- .../lammps_execute/EOS_of_Cu.py | 5 +- .../lammps_execute/elastic_template.txt | 390 ++++++++++++++++++ .../lammps_execute/eos_template.txt | 74 ++++ .../lammps_execute/stiffness_tensor_of_hea.py | 6 +- src/ursa/agents/lammps_agent.py | 14 +- 5 files changed, 485 insertions(+), 4 deletions(-) create mode 100644 examples/two_agent_examples/lammps_execute/elastic_template.txt create mode 100644 examples/two_agent_examples/lammps_execute/eos_template.txt diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 01826932..ef9dbb7b 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -19,13 +19,16 @@ mpirun_cmd="mpirun", ) +with open("eos_template.txt", "r") as file: + template = file.read() + simulation_task = ( "Carry out a LAMMPS simulation of Cu to determine its equation of state." ) elements = ["Cu"] final_lammps_state = wf.invoke( - simulation_task=simulation_task, elements=elements + simulation_task=simulation_task, elements=elements, template=template ) if final_lammps_state.get("run_returncode") == 0: diff --git a/examples/two_agent_examples/lammps_execute/elastic_template.txt b/examples/two_agent_examples/lammps_execute/elastic_template.txt new file mode 100644 index 00000000..926a6652 --- /dev/null +++ b/examples/two_agent_examples/lammps_execute/elastic_template.txt @@ -0,0 +1,390 @@ +log ./log.lammps + +units metal +atom_style atomic +boundary p p p +newton on + +variable a0 equal xxx +variable nrep equal xxx +variable seed equal xxx +variable delta equal xxx +variable etol equal xxx +variable ftol equal xxx +variable maxiter equal xxx +variable maxeval equal xxx + +# ------------ SYSTEM / STRUCTURE ------------ +lattice xxx (fcc,bcc,etc....) ${a0} +region box block 0 ${nrep} 0 ${nrep} 0 ${nrep} +create_box xx box +create_atoms xx box + +# Example random equiatomic assignment across 5 types (1..5) +# Adjust according to your needs +set type 1 type/fraction 2 0.20 ${seed} +set type 1 type/fraction 3 0.25 ${seed} +set type 1 type/fraction 4 0.3333 ${seed} +set type 1 type/fraction 5 0.5 ${seed} + +# Masses +mass 1 xxxx +mass 2 xxxx +mass 3 xxxx +mass 4 xxxx +mass 5 xxxx + +# pair_style and pair_coeff details should be entered here + +neighbor 2.0 bin +neigh_modify delay 0 every 1 check yes + +# ------------ MINIMIZATION TO 0 K ------------ +reset_timestep 0 +thermo xxxxx +thermo_style custom step pe lx ly lz pxx pyy pzz pxy pxz pyz + +min_style cg +min_modify dmax 0.1 line quadratic +minimize ${etol} ${ftol} ${maxiter} ${maxeval} + +# Store reference cell (for shear normalization) +variable Lx0 equal lx +variable Ly0 equal ly +variable Lz0 equal lz + +# ------------ HELPER VARIABLES ------------ +# Precompute scale factors for normal strains +variable facP equal 1.0+v_delta # 1 + delta +variable facM equal 1.0-v_delta # 1 - delta +variable facPi equal 1.0/${facP} # 1/(1+delta) +variable facMi equal 1.0/${facM} # 1/(1-delta) + +# A macro to relax atoms after each strain change +variable relax string "minimize ${etol} ${ftol} ${maxiter} ${maxeval}" + +# ------------ STRESS (GPa, tension-positive) ------------ +# LAMMPS 'metal' pressure is bar (compression positive). Convert and flip sign. +variable S1 equal -pxx*1.0e-4 +variable S2 equal -pyy*1.0e-4 +variable S3 equal -pzz*1.0e-4 +variable S4 equal -pyz*1.0e-4 +variable S5 equal -pxz*1.0e-4 +variable S6 equal -pxy*1.0e-4 + +# Initialize holders for +/- stress states +variable Sp1 equal 0.0 +variable Sp2 equal 0.0 +variable Sp3 equal 0.0 +variable Sp4 equal 0.0 +variable Sp5 equal 0.0 +variable Sp6 equal 0.0 + +variable Sm1 equal 0.0 +variable Sm2 equal 0.0 +variable Sm3 equal 0.0 +variable Sm4 equal 0.0 +variable Sm5 equal 0.0 +variable Sm6 equal 0.0 + +variable Tp1 equal 0.0 +variable Tp2 equal 0.0 +variable Tp3 equal 0.0 +variable Tp4 equal 0.0 +variable Tp5 equal 0.0 +variable Tp6 equal 0.0 + +variable Tm1 equal 0.0 +variable Tm2 equal 0.0 +variable Tm3 equal 0.0 +variable Tm4 equal 0.0 +variable Tm5 equal 0.0 +variable Tm6 equal 0.0 + +variable Up1 equal 0.0 +variable Up2 equal 0.0 +variable Up3 equal 0.0 +variable Up4 equal 0.0 +variable Up5 equal 0.0 +variable Up6 equal 0.0 + +variable Um1 equal 0.0 +variable Um2 equal 0.0 +variable Um3 equal 0.0 +variable Um4 equal 0.0 +variable Um5 equal 0.0 +variable Um6 equal 0.0 + + +# ------------ NORMAL STRAINS ------------ +# X: +delta then -delta +label strain_x_plus +change_box all x scale ${facP} remap units box +${relax} +variable Sp1 equal ${S1} +variable Sp2 equal ${S2} +variable Sp3 equal ${S3} +variable Sp4 equal ${S4} +variable Sp5 equal ${S5} +variable Sp6 equal ${S6} +change_box all x scale ${facPi} remap units box +${relax} + +label strain_x_minus +change_box all x scale ${facM} remap units box +${relax} +variable Sm1 equal ${S1} +variable Sm2 equal ${S2} +variable Sm3 equal ${S3} +variable Sm4 equal ${S4} +variable Sm5 equal ${S5} +variable Sm6 equal ${S6} +change_box all x scale ${facMi} remap units box +${relax} + +# Y +label strain_y_plus +change_box all y scale ${facP} remap units box +${relax} +variable Tp1 equal ${S1} +variable Tp2 equal ${S2} +variable Tp3 equal ${S3} +variable Tp4 equal ${S4} +variable Tp5 equal ${S5} +variable Tp6 equal ${S6} +change_box all y scale ${facPi} remap units box +${relax} + +label strain_y_minus +change_box all y scale ${facM} remap units box +${relax} +variable Tm1 equal ${S1} +variable Tm2 equal ${S2} +variable Tm3 equal ${S3} +variable Tm4 equal ${S4} +variable Tm5 equal ${S5} +variable Tm6 equal ${S6} +change_box all y scale ${facMi} remap units box +${relax} + +# Z +label strain_z_plus +change_box all z scale ${facP} remap units box +${relax} +variable Up1 equal ${S1} +variable Up2 equal ${S2} +variable Up3 equal ${S3} +variable Up4 equal ${S4} +variable Up5 equal ${S5} +variable Up6 equal ${S6} +change_box all z scale ${facPi} remap units box +${relax} + +label strain_z_minus +change_box all z scale ${facM} remap units box +${relax} +variable Um1 equal ${S1} +variable Um2 equal ${S2} +variable Um3 equal ${S3} +variable Um4 equal ${S4} +variable Um5 equal ${S5} +variable Um6 equal ${S6} +change_box all z scale ${facMi} remap units box +${relax} + +# ------------ SHEAR STRAINS (triclinic + proper gamma) ------------ +# Convert to triclinic once so we can tilt +change_box all triclinic remap units box + +# Define tilt distances so that engineering shear gamma == delta +# divisors: xy->Ly, xz->Lz, yz->Lz +variable dxy equal v_delta*v_Ly0 +variable dxz equal v_delta*v_Lz0 +variable dyz equal v_delta*v_Lz0 + +# XY +label shear_xy_plus +change_box all xy delta ${dxy} remap units box +${relax} +variable Xp1 equal ${S1} +variable Xp2 equal ${S2} +variable Xp3 equal ${S3} +variable Xp4 equal ${S4} +variable Xp5 equal ${S5} +variable Xp6 equal ${S6} +change_box all xy delta -${dxy} remap units box +${relax} + +label shear_xy_minus +change_box all xy delta -${dxy} remap units box +${relax} +variable Xm1 equal ${S1} +variable Xm2 equal ${S2} +variable Xm3 equal ${S3} +variable Xm4 equal ${S4} +variable Xm5 equal ${S5} +variable Xm6 equal ${S6} +change_box all xy delta ${dxy} remap units box +${relax} + +# XZ +label shear_xz_plus +change_box all xz delta ${dxz} remap units box +${relax} +variable Yp1 equal ${S1} +variable Yp2 equal ${S2} +variable Yp3 equal ${S3} +variable Yp4 equal ${S4} +variable Yp5 equal ${S5} +variable Yp6 equal ${S6} +change_box all xz delta -${dxz} remap units box +${relax} + +label shear_xz_minus +change_box all xz delta -${dxz} remap units box +${relax} +variable Ym1 equal ${S1} +variable Ym2 equal ${S2} +variable Ym3 equal ${S3} +variable Ym4 equal ${S4} +variable Ym5 equal ${S5} +variable Ym6 equal ${S6} +change_box all xz delta ${dxz} remap units box +${relax} + +# YZ +label shear_yz_plus +change_box all yz delta ${dyz} remap units box +${relax} +variable Zp1 equal ${S1} +variable Zp2 equal ${S2} +variable Zp3 equal ${S3} +variable Zp4 equal ${S4} +variable Zp5 equal ${S5} +variable Zp6 equal ${S6} +change_box all yz delta -${dyz} remap units box +${relax} + +label shear_yz_minus +change_box all yz delta -${dyz} remap units box +${relax} +variable Zm1 equal ${S1} +variable Zm2 equal ${S2} +variable Zm3 equal ${S3} +variable Zm4 equal ${S4} +variable Zm5 equal ${S5} +variable Zm6 equal ${S6} +change_box all yz delta ${dyz} remap units box +${relax} + +# (Optional tidy-up: zero tilts and return to orthogonal) +change_box all xy final 0.0 xz final 0.0 yz final 0.0 remap units box +change_box all ortho remap units box + +# ------------ BUILD THE 6x6 C MATRIX (GPa) ------------ +# Voigt: 1=xx,2=yy,3=zz,4=yz,5=xz,6=xy +# Normal: epsilon = +/- delta +# Shear: epsilon4..6 = gamma/2 = +/- delta/2 -> divide by delta below + +# j=1 (exx) +variable C11 equal ( ${Sp1}-${Sm1} )/( 2*${delta} ) +variable C21 equal ( ${Sp2}-${Sm2} )/( 2*${delta} ) +variable C31 equal ( ${Sp3}-${Sm3} )/( 2*${delta} ) +variable C41 equal ( ${Sp4}-${Sm4} )/( 2*${delta} ) +variable C51 equal ( ${Sp5}-${Sm5} )/( 2*${delta} ) +variable C61 equal ( ${Sp6}-${Sm6} )/( 2*${delta} ) + +# j=2 (eyy) +variable C12 equal ( ${Tp1}-${Tm1} )/( 2*${delta} ) +variable C22 equal ( ${Tp2}-${Tm2} )/( 2*${delta} ) +variable C32 equal ( ${Tp3}-${Tm3} )/( 2*${delta} ) +variable C42 equal ( ${Tp4}-${Tm4} )/( 2*${delta} ) +variable C52 equal ( ${Tp5}-${Tm5} )/( 2*${delta} ) +variable C62 equal ( ${Tp6}-${Tm6} )/( 2*${delta} ) + +# j=3 (ezz) +variable C13 equal ( ${Up1}-${Um1} )/( 2*${delta} ) +variable C23 equal ( ${Up2}-${Um2} )/( 2*${delta} ) +variable C33 equal ( ${Up3}-${Um3} )/( 2*${delta} ) +variable C43 equal ( ${Up4}-${Um4} )/( 2*${delta} ) +variable C53 equal ( ${Up5}-${Um5} )/( 2*${delta} ) +variable C63 equal ( ${Up6}-${Um6} )/( 2*${delta} ) + +# j=6 (exy = gamma/2): divide by delta +variable C16 equal ( ${Xp1}-${Xm1} )/( ${delta} ) +variable C26 equal ( ${Xp2}-${Xm2} )/( ${delta} ) +variable C36 equal ( ${Xp3}-${Xm3} )/( ${delta} ) +variable C46 equal ( ${Xp4}-${Xm4} )/( ${delta} ) +variable C56 equal ( ${Xp5}-${Xm5} )/( ${delta} ) +variable C66 equal ( ${Xp6}-${Xm6} )/( ${delta} ) + +# j=5 (exz = gamma/2): divide by delta +variable C15 equal ( ${Yp1}-${Ym1} )/( ${delta} ) +variable C25 equal ( ${Yp2}-${Ym2} )/( ${delta} ) +variable C35 equal ( ${Yp3}-${Ym3} )/( ${delta} ) +variable C45 equal ( ${Yp4}-${Ym4} )/( ${delta} ) +variable C55 equal ( ${Yp5}-${Ym5} )/( ${delta} ) +variable C65 equal ( ${Yp6}-${Ym6} )/( ${delta} ) + +# j=4 (eyz = gamma/2): divide by delta +variable C14 equal ( ${Zp1}-${Zm1} )/( ${delta} ) +variable C24 equal ( ${Zp2}-${Zm2} )/( ${delta} ) +variable C34 equal ( ${Zp3}-${Zm3} )/( ${delta} ) +variable C44 equal ( ${Zp4}-${Zm4} )/( ${delta} ) +variable C54 equal ( ${Zp5}-${Zm5} )/( ${delta} ) +variable C64 equal ( ${Zp6}-${Zm6} )/( ${delta} ) + +# ------------ REPORT: raw, symmetrized, cubic averages, stability ------------ +print "Elastic stiffness matrix C_ij (GPa):" +print " ${C11} ${C12} ${C13} ${C14} ${C15} ${C16}" +print " ${C21} ${C22} ${C23} ${C24} ${C25} ${C26}" +print " ${C31} ${C32} ${C33} ${C34} ${C35} ${C36}" +print " ${C41} ${C42} ${C43} ${C44} ${C45} ${C46}" +print " ${C51} ${C52} ${C53} ${C54} ${C55} ${C56}" +print " ${C61} ${C62} ${C63} ${C64} ${C65} ${C66}" + + + +# ---- Symmetrize C (recommended for reporting) ---- +variable Cs12 equal 0.5*(${C12}+${C21}) +variable Cs13 equal 0.5*(${C13}+${C31}) +variable Cs23 equal 0.5*(${C23}+${C32}) +variable Cs14 equal 0.5*(${C14}+${C41}) +variable Cs15 equal 0.5*(${C15}+${C51}) +variable Cs16 equal 0.5*(${C16}+${C61}) +variable Cs24 equal 0.5*(${C24}+${C42}) +variable Cs25 equal 0.5*(${C25}+${C52}) +variable Cs26 equal 0.5*(${C26}+${C62}) +variable Cs34 equal 0.5*(${C34}+${C43}) +variable Cs35 equal 0.5*(${C35}+${C53}) +variable Cs36 equal 0.5*(${C36}+${C63}) +variable Cs44 equal ${C44} +variable Cs55 equal ${C55} +variable Cs66 equal ${C66} +variable Cs11 equal ${C11} +variable Cs22 equal ${C22} +variable Cs33 equal ${C33} + +print "Symmetrized C_ij (GPa) key entries:" +print "C11=${Cs11} C22=${Cs22} C33=${Cs33}" +print "C12=${Cs12} C13=${Cs13} C23=${Cs23}" +print "C44=${Cs44} C55=${Cs55} C66=${Cs66}" + +# ---- Cubic-averaged moduli (for near-cubic HEAs) ---- +variable C11_c equal ( ${Cs11}+${Cs22}+${Cs33} )/3.0 +variable C12_c equal ( ${Cs12}+${Cs13}+${Cs23} )/3.0 +variable C44_c equal ( ${Cs44}+${Cs55}+${Cs66} )/3.0 +print "Cubic-averaged: C11bar=${C11_c} GPa, C12bar=${C12_c} GPa, C44bar=${C44_c} GPa" + +# ---- Born stability (cubic) using cubic-averaged values ---- +variable born1 equal ${C11_c}-${C12_c} +variable born2 equal ${C11_c}+2.0*${C12_c} +variable born3 equal ${C44_c} +print "Born (cubic) checks: C11-C12=${born1} (>0), C11+2C12=${born2} (>0), C44=${born3} (>0)" + +# ------------ DERIVED MODULI (Voigt estimates from raw C) ------------ +variable K_V equal ( ( ${C11}+${C22}+${C33} ) + 2*( ${Cs12}+${Cs13}+${Cs23} ) )/9.0 +variable G_V equal ( ( ${C11}+${C22}+${C33} ) - ( ${Cs12}+${Cs13}+${Cs23} ) + 3*( ${C44}+${C55}+${C66} ) )/15.0 +variable E_V equal 9.0*${K_V}*${G_V}/(3.0*${K_V}+${G_V}) +variable nu_V equal (3.0*${K_V}-2.0*${G_V})/(2.0*(3.0*${K_V}+${G_V})) +print "Voigt estimates (polycrystal): K=${K_V} GPa, G=${G_V} GPa, E=${E_V} GPa, nu=${nu_V}" diff --git a/examples/two_agent_examples/lammps_execute/eos_template.txt b/examples/two_agent_examples/lammps_execute/eos_template.txt new file mode 100644 index 00000000..1cf5a4fd --- /dev/null +++ b/examples/two_agent_examples/lammps_execute/eos_template.txt @@ -0,0 +1,74 @@ +log ./log.lammps + +units metal +atom_style atomic +boundary p p p + +# Lattice and supercell +variable a0 equal xxx +variable nx equal xxx +variable ny equal xxx +variable nz equal xxx +variable Lx0 equal ${a0}*${nx} +variable Ly0 equal ${a0}*${ny} +variable Lz0 equal ${a0}*${nz} + +lattice xxx (fcc,bcc,etc....) ${a0} +region box block 0 ${Lx0} 0 ${Ly0} 0 ${Lz0} units box +create_box xx box +create_atoms xx box + +# Example random equiatomic assignment across 5 types (1..5) +# Adjust according to your needs +set type 1 type/fraction 2 0.20 ${seed} +set type 1 type/fraction 3 0.25 ${seed} +set type 1 type/fraction 4 0.3333 ${seed} +set type 1 type/fraction 5 0.5 ${seed} + +# Masses +mass 1 xxxx +mass 2 xxxx +mass 3 xxxx +mass 4 xxxx +mass 5 xxxx + +# pair_style and pair_coeff details should be entered here + +neighbor 2.0 bin +neigh_modify delay 0 every 1 check yes + +thermo 1 +thermo_style custom step pe press vol lx ly lz + +# Energy minimization settings +min_style cg +min_modify dmax 0.1 line quadratic + +# Variables for reporting +variable e equal pe +variable p equal press +variable vvol equal vol + +# Loop over scale factors (volume scan) +variable i loop 11 +label loop + variable scale equal 0.94 + (v_i-1)*0.01 + variable newa equal ${a0}*${scale} + variable newLx equal ${newa}*${nx} + variable newLy equal ${newa}*${ny} + variable newLz equal ${newa}*${nz} + + # Rescale box and atoms to target volume + change_box all x final 0 ${newLx} y final 0 ${newLy} z final 0 ${newLz} remap units box + + reset_timestep 0 + minimize 1.0e-12 1.0e-12 10000 10000 + run 0 + + # Report one line per state: scale, a(Ang), Vol(Ang^3), Pressure(bar), Energy(eV) + print "scale ${scale} a ${newa} vol ${vvol} press ${p} pe ${e}" + + next i +jump SELF loop + +print "# EOS scan complete." diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index e7f01645..2b63568c 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -7,7 +7,6 @@ llm = ChatOpenAI(model=model, timeout=None, max_retries=2) - workspace = "./workspace_stiffness_tensor" wf = LammpsAgent( @@ -20,12 +19,15 @@ mpirun_cmd="mpirun", ) +with open("elastic_template.txt", "r") as file: + template = file.read() + simulation_task = "Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." elements = ["Co", "Cr", "Fe", "Mn", "Ni"] final_lammps_state = wf.invoke( - simulation_task=simulation_task, elements=elements + simulation_task=simulation_task, elements=elements, template=template ) if final_lammps_state.get("run_returncode") == 0: diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 804242d9..1a95ce26 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -21,6 +21,7 @@ class LammpsState(TypedDict, total=False): simulation_task: str elements: List[str] + template: Optional[str] matches: List[Any] db_message: str @@ -119,6 +120,8 @@ def __init__( "Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" + "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" + "Template provided (if any): {template}\n" "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" @@ -141,6 +144,8 @@ def __init__( "Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" + "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" + "Template provided (if any): {template}\n" "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" @@ -280,6 +285,7 @@ def _author(self, state: LammpsState) -> LammpsState: "simulation_task": state["simulation_task"], "metadata": text, "pair_info": pair_info, + "template": state["template"], }) script_dict = self._safe_json_loads(authored_json) input_script = script_dict["input_script"] @@ -335,6 +341,7 @@ def _fix(self, state: LammpsState) -> LammpsState: "err_message": err_blob, "metadata": text, "pair_info": pair_info, + "template": state["template"], }) script_dict = self._safe_json_loads(fixed_json) new_input = script_dict["input_script"] @@ -411,4 +418,9 @@ def _invoke( "'simulation_task' and 'elements' are required arguments" ) - return self._action.invoke(inputs, config) \ No newline at end of file + if "template" not in inputs: + inputs = {**inputs, "template": "No template provided."} + + return self._action.invoke(inputs, config) + + \ No newline at end of file From f8f1f067bd533f750e4ee2a80c6d7118310dc9fc Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Mon, 20 Oct 2025 11:47:50 -0600 Subject: [PATCH 12/31] Cleaned up unrequired variables in LammpsState --- src/ursa/agents/lammps_agent.py | 35 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 1a95ce26..ea3ae127 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -24,15 +24,12 @@ class LammpsState(TypedDict, total=False): template: Optional[str] matches: List[Any] - db_message: str - idx: int summaries: List[str] full_texts: List[str] - summaries_combined: str - choice_json: str - chosen_index: int + + chosen_potential: Any input_script: str run_returncode: Optional[int] @@ -117,7 +114,7 @@ def __init__( self.author_chain = ( ChatPromptTemplate.from_template( "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" - "Here is metadata about the interatomic potential that will be used: {metadata}.\n" + #"Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" @@ -141,7 +138,7 @@ def __init__( "However, when running the simulation, an error was raised.\n" "Here is the full stdout message that includes the error message: {err_message}\n" "Your task is to write a new input file that resolves the error.\n" - "Here is metadata about the interatomic potential that will be used: {metadata}.\n" + #"Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" @@ -209,7 +206,6 @@ def _find_potentials(self, state: LammpsState) -> LammpsState: return { **state, "matches": list(matches), - "db_message": "\n".join(msg_lines), "idx": 0, "summaries": [], "full_texts": [], @@ -273,17 +269,20 @@ def _choose(self, state: LammpsState) -> LammpsState: print(f"Chosen potential #{chosen_index}") print("Rationale for choosing this potential:") print(choice_dict["rationale"]) - return {**state, "choice_json": choice, "chosen_index": chosen_index} + + chosen_potential = state["matches"][chosen_index] + + return {**state, "chosen_potential": chosen_potential} def _author(self, state: LammpsState) -> LammpsState: print("First attempt at writing LAMMPS input file....") - match = state["matches"][state["chosen_index"]] - match.download_files(self.workspace) - text = state["full_texts"][state["chosen_index"]] - pair_info = match.pair_info() + #match = state["matches"][state["chosen_index"]] + state["chosen_potential"].download_files(self.workspace) + #text = state["full_texts"][state["chosen_index"]] + pair_info = state["chosen_potential"].pair_info() authored_json = self.author_chain.invoke({ "simulation_task": state["simulation_task"], - "metadata": text, + #"metadata": text, "pair_info": pair_info, "template": state["template"], }) @@ -330,16 +329,16 @@ def _route_run(self, state: LammpsState) -> str: return "done_failed" def _fix(self, state: LammpsState) -> LammpsState: - match = state["matches"][state["chosen_index"]] - text = state["full_texts"][state["chosen_index"]] - pair_info = match.pair_info() + #match = state["matches"][state["chosen_index"]] + #text = state["full_texts"][state["chosen_index"]] + pair_info = state["chosen_potential"].pair_info().pair_info() err_blob = state.get("run_stdout") fixed_json = self.fix_chain.invoke({ "simulation_task": state["simulation_task"], "input_script": state["input_script"], "err_message": err_blob, - "metadata": text, + #"metadata": text, "pair_info": pair_info, "template": state["template"], }) From 7f8dfb8cff468d42f2a271ad0f6a072ec7ed5df2 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Mon, 20 Oct 2025 12:34:57 -0600 Subject: [PATCH 13/31] Added option for the user to provide the potential to the lammps_agent --- .../lammps_execute/EOS_of_Cu.py | 26 +++++++++--- src/ursa/agents/lammps_agent.py | 40 +++++++++---------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index ef9dbb7b..450853b6 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -3,6 +3,15 @@ from ursa.agents import ExecutionAgent, LammpsAgent +try: + import atomman as am +except Exception: + raise ImportError( + "This example requires the atomman dependency. " + "This can be installed using 'pip install ursa-ai[lammps]' or, " + "if working from a local installation, 'pip install -e .[lammps]' ." + ) + model = "gpt-5" llm = ChatOpenAI(model=model, timeout=None, max_retries=2) @@ -22,14 +31,19 @@ with open("eos_template.txt", "r") as file: template = file.read() -simulation_task = ( - "Carry out a LAMMPS simulation of Cu to determine its equation of state." -) +simulation_task = ("Carry out a LAMMPS simulation of Cu to determine its equation of state.") + elements = ["Cu"] -final_lammps_state = wf.invoke( - simulation_task=simulation_task, elements=elements, template=template -) +db = am.library.Database(remote=True) +matches = db.get_lammps_potentials(pair_style=["eam"], elements=elements) +chosen_potential = matches[-1] + +final_lammps_state = wf.invoke(simulation_task=simulation_task, + elements=elements, + template=template, + chosen_potential=chosen_potential, + ) if final_lammps_state.get("run_returncode") == 0: print("\nNow handing things off to execution agent.....") diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index ea3ae127..5f28dbcf 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -22,6 +22,7 @@ class LammpsState(TypedDict, total=False): simulation_task: str elements: List[str] template: Optional[str] + chosen_potential: Optional[Any] matches: List[Any] idx: int @@ -29,8 +30,6 @@ class LammpsState(TypedDict, total=False): full_texts: List[str] summaries_combined: str - chosen_potential: Any - input_script: str run_returncode: Optional[int] run_stdout: str @@ -114,7 +113,6 @@ def __init__( self.author_chain = ( ChatPromptTemplate.from_template( "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" - #"Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" @@ -138,7 +136,6 @@ def __init__( "However, when running the simulation, an error was raised.\n" "Here is the full stdout message that includes the error message: {err_message}\n" "Your task is to write a new input file that resolves the error.\n" - #"Here is metadata about the interatomic potential that will be used: {metadata}.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" @@ -191,18 +188,17 @@ def _fetch_and_trim_text(self, url: str) -> str: pass return text + + def _entry_router(self, state: LammpsState) -> dict: + return {} + + def _find_potentials(self, state: LammpsState) -> LammpsState: db = am.library.Database(remote=True) matches = db.get_lammps_potentials( pair_style=self.pair_styles, elements=state["elements"] ) - msg_lines = [] - if not list(matches): - msg_lines.append("No potentials found for this task in NIST.") - else: - msg_lines.append("Found these potentials in NIST:") - for rec in matches: - msg_lines.append(f"{rec.id} {rec.pair_style} {rec.symbols}") + return { **state, "matches": list(matches), @@ -269,20 +265,15 @@ def _choose(self, state: LammpsState) -> LammpsState: print(f"Chosen potential #{chosen_index}") print("Rationale for choosing this potential:") print(choice_dict["rationale"]) - chosen_potential = state["matches"][chosen_index] - return {**state, "chosen_potential": chosen_potential} def _author(self, state: LammpsState) -> LammpsState: print("First attempt at writing LAMMPS input file....") - #match = state["matches"][state["chosen_index"]] state["chosen_potential"].download_files(self.workspace) - #text = state["full_texts"][state["chosen_index"]] pair_info = state["chosen_potential"].pair_info() authored_json = self.author_chain.invoke({ "simulation_task": state["simulation_task"], - #"metadata": text, "pair_info": pair_info, "template": state["template"], }) @@ -329,8 +320,6 @@ def _route_run(self, state: LammpsState) -> str: return "done_failed" def _fix(self, state: LammpsState) -> LammpsState: - #match = state["matches"][state["chosen_index"]] - #text = state["full_texts"][state["chosen_index"]] pair_info = state["chosen_potential"].pair_info().pair_info() err_blob = state.get("run_stdout") @@ -338,7 +327,6 @@ def _fix(self, state: LammpsState) -> LammpsState: "simulation_task": state["simulation_task"], "input_script": state["input_script"], "err_message": err_blob, - #"metadata": text, "pair_info": pair_info, "template": state["template"], }) @@ -354,7 +342,8 @@ def _fix(self, state: LammpsState) -> LammpsState: def _build_graph(self): g = StateGraph(LammpsState) - + + self.add_node(g, self._entry_router) self.add_node(g, self._find_potentials) self.add_node(g, self._summarize_one) self.add_node(g, self._build_summaries) @@ -363,8 +352,17 @@ def _build_graph(self): self.add_node(g, self._run_lammps) self.add_node(g, self._fix) - g.set_entry_point("_find_potentials") + g.set_entry_point("_entry_router") + g.add_conditional_edges( + "_entry_router", + lambda state: "user_choice" if state.get("chosen_potential") else "agent_choice", + { + "user_choice": "_author", + "agent_choice": "_find_potentials", + }, + ) + g.add_conditional_edges( "_find_potentials", self._should_summarize, From 6d68b0c3903d9cd2a1f2fbd8f89f2d09aec98b86 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Mon, 20 Oct 2025 16:53:42 -0600 Subject: [PATCH 14/31] Added option of using the lammps_agent as a tool to summarize and choose potentials only, i.e. without actually running LAMMPS --- .../lammps_execute/EOS_of_Cu.py | 1 + .../lammps_execute/stiffness_tensor_of_hea.py | 1 + src/ursa/agents/lammps_agent.py | 47 +++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 450853b6..acd2d5aa 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -22,6 +22,7 @@ llm=llm, max_potentials=2, max_fix_attempts=5, + find_potential_only=False, mpi_procs=8, workspace=workspace, lammps_cmd="lmp_mpi", diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 2b63568c..7b42c7e5 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -13,6 +13,7 @@ llm=llm, max_potentials=5, max_fix_attempts=15, + find_potential_only=False, mpi_procs=8, workspace=workspace, lammps_cmd="lmp_mpi", diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 5f28dbcf..3d5597d0 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -44,6 +44,7 @@ def __init__( llm, max_potentials: int = 5, max_fix_attempts: int = 10, + find_potential_only: bool = False, mpi_procs: int = 8, workspace: str = "./workspace", lammps_cmd: str = "lmp_mpi", @@ -58,6 +59,7 @@ def __init__( ) self.max_potentials = max_potentials self.max_fix_attempts = max_fix_attempts + self.find_potential_only = find_potential_only self.mpi_procs = mpi_procs self.lammps_cmd = lammps_cmd self.mpirun_cmd = mpirun_cmd @@ -69,13 +71,13 @@ def __init__( "eam/alloy", "eam/fs", "meam", - "adp", # classical, HEA-relevant - "kim", # OpenKIM models + "adp", + "kim", "snap", "quip", "mlip", "pace", - "nep", # ML/ACE families (if available) + "nep", ] self.workspace = workspace @@ -190,6 +192,12 @@ def _fetch_and_trim_text(self, url: str) -> str: def _entry_router(self, state: LammpsState) -> dict: + if self.find_potential_only and state.get("chosen_potential"): + raise Exception("You cannot set find_potential_only=True and also specify your own potential!") + + if not state.get("chosen_potential"): + self.potential_summaries_dir = os.path.join(self.workspace, "potential_summaries") + os.makedirs(self.potential_summaries_dir, exist_ok=True) return {} @@ -240,6 +248,10 @@ def _summarize_one(self, state: LammpsState) -> LammpsState: "simulation_task": state["simulation_task"], }) + summary_file = os.path.join(self.potential_summaries_dir,"potential_"+str(i)+".txt") + with open(summary_file, "w") as f: + f.write(summary) + return { **state, "idx": i + 1, @@ -262,12 +274,28 @@ def _choose(self, state: LammpsState) -> LammpsState: }) choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) + print(f"Chosen potential #{chosen_index}") print("Rationale for choosing this potential:") print(choice_dict["rationale"]) + chosen_potential = state["matches"][chosen_index] + + out_file = os.path.join(self.potential_summaries_dir,"Rationale.txt") + with open(out_file, "w") as f: + f.write(f"Chosen potential #{chosen_index}") + f.write("\n") + f.write("Rationale for choosing this potential:") + f.write("\n") + f.write(choice_dict["rationale"]) + return {**state, "chosen_potential": chosen_potential} + def _route_after_summarization(self, state: LammpsState) -> str: + if self.find_potential_only: + return "Exit" + return "continue_author" + def _author(self, state: LammpsState) -> LammpsState: print("First attempt at writing LAMMPS input file....") state["chosen_potential"].download_files(self.workspace) @@ -320,7 +348,7 @@ def _route_run(self, state: LammpsState) -> str: return "done_failed" def _fix(self, state: LammpsState) -> LammpsState: - pair_info = state["chosen_potential"].pair_info().pair_info() + pair_info = state["chosen_potential"].pair_info() err_blob = state.get("run_stdout") fixed_json = self.fix_chain.invoke({ @@ -383,7 +411,16 @@ def _build_graph(self): ) g.add_edge("_build_summaries", "_choose") - g.add_edge("_choose", "_author") + + g.add_conditional_edges( + "_choose", + self._route_after_summarization, + { + "continue_author": "_author", + "Exit": END, + }, + ) + g.add_edge("_author", "_run_lammps") g.add_conditional_edges( From 16da86297a20563bacac2fc610a8a2d0e9ebc5a0 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Mon, 27 Oct 2025 11:15:03 -0600 Subject: [PATCH 15/31] Linted and Formatted Code using ruff --- .../lammps_execute/EOS_of_Cu.py | 27 +++++----- .../lammps_execute/stiffness_tensor_of_hea.py | 12 ++--- src/ursa/agents/lammps_agent.py | 52 ++++++++++--------- 3 files changed, 47 insertions(+), 44 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index acd2d5aa..228a38c6 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -11,7 +11,7 @@ "This can be installed using 'pip install ursa-ai[lammps]' or, " "if working from a local installation, 'pip install -e .[lammps]' ." ) - + model = "gpt-5" llm = ChatOpenAI(model=model, timeout=None, max_retries=2) @@ -32,7 +32,9 @@ with open("eos_template.txt", "r") as file: template = file.read() -simulation_task = ("Carry out a LAMMPS simulation of Cu to determine its equation of state.") +simulation_task = ( + "Carry out a LAMMPS simulation of Cu to determine its equation of state." +) elements = ["Cu"] @@ -40,11 +42,12 @@ matches = db.get_lammps_potentials(pair_style=["eam"], elements=elements) chosen_potential = matches[-1] -final_lammps_state = wf.invoke(simulation_task=simulation_task, - elements=elements, - template=template, - chosen_potential=chosen_potential, - ) +final_lammps_state = wf.invoke( + simulation_task=simulation_task, + elements=elements, + template=template, + chosen_potential=chosen_potential, +) if final_lammps_state.get("run_returncode") == 0: print("\nNow handing things off to execution agent.....") @@ -58,12 +61,10 @@ Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - final_results = executor.invoke( - { - "messages": [HumanMessage(content=exe_plan)], - "workspace": workspace, - } - ) + final_results = executor.invoke({ + "messages": [HumanMessage(content=exe_plan)], + "workspace": workspace, + }) for x in final_results["messages"]: print(x.content) diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 7b42c7e5..4f6739f6 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -42,13 +42,11 @@ Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - - final_results = executor.invoke( - { - "messages": [HumanMessage(content=exe_plan)], - "workspace": workspace, - } - ) + + final_results = executor.invoke({ + "messages": [HumanMessage(content=exe_plan)], + "workspace": workspace, + }) for x in final_results["messages"]: print(x.content) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 3d5597d0..e4cbbade 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -29,7 +29,7 @@ class LammpsState(TypedDict, total=False): summaries: List[str] full_texts: List[str] summaries_combined: str - + input_script: str run_returncode: Optional[int] run_stdout: str @@ -71,13 +71,13 @@ def __init__( "eam/alloy", "eam/fs", "meam", - "adp", - "kim", + "adp", + "kim", "snap", "quip", "mlip", "pace", - "nep", + "nep", ] self.workspace = workspace @@ -190,23 +190,25 @@ def _fetch_and_trim_text(self, url: str) -> str: pass return text - def _entry_router(self, state: LammpsState) -> dict: if self.find_potential_only and state.get("chosen_potential"): - raise Exception("You cannot set find_potential_only=True and also specify your own potential!") - + raise Exception( + "You cannot set find_potential_only=True and also specify your own potential!" + ) + if not state.get("chosen_potential"): - self.potential_summaries_dir = os.path.join(self.workspace, "potential_summaries") + self.potential_summaries_dir = os.path.join( + self.workspace, "potential_summaries" + ) os.makedirs(self.potential_summaries_dir, exist_ok=True) return {} - - + def _find_potentials(self, state: LammpsState) -> LammpsState: db = am.library.Database(remote=True) matches = db.get_lammps_potentials( pair_style=self.pair_styles, elements=state["elements"] ) - + return { **state, "matches": list(matches), @@ -248,10 +250,12 @@ def _summarize_one(self, state: LammpsState) -> LammpsState: "simulation_task": state["simulation_task"], }) - summary_file = os.path.join(self.potential_summaries_dir,"potential_"+str(i)+".txt") + summary_file = os.path.join( + self.potential_summaries_dir, "potential_" + str(i) + ".txt" + ) with open(summary_file, "w") as f: f.write(summary) - + return { **state, "idx": i + 1, @@ -274,21 +278,21 @@ def _choose(self, state: LammpsState) -> LammpsState: }) choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) - + print(f"Chosen potential #{chosen_index}") print("Rationale for choosing this potential:") print(choice_dict["rationale"]) - + chosen_potential = state["matches"][chosen_index] - out_file = os.path.join(self.potential_summaries_dir,"Rationale.txt") + out_file = os.path.join(self.potential_summaries_dir, "Rationale.txt") with open(out_file, "w") as f: f.write(f"Chosen potential #{chosen_index}") f.write("\n") f.write("Rationale for choosing this potential:") f.write("\n") f.write(choice_dict["rationale"]) - + return {**state, "chosen_potential": chosen_potential} def _route_after_summarization(self, state: LammpsState) -> str: @@ -370,7 +374,7 @@ def _fix(self, state: LammpsState) -> LammpsState: def _build_graph(self): g = StateGraph(LammpsState) - + self.add_node(g, self._entry_router) self.add_node(g, self._find_potentials) self.add_node(g, self._summarize_one) @@ -384,13 +388,15 @@ def _build_graph(self): g.add_conditional_edges( "_entry_router", - lambda state: "user_choice" if state.get("chosen_potential") else "agent_choice", + lambda state: "user_choice" + if state.get("chosen_potential") + else "agent_choice", { "user_choice": "_author", "agent_choice": "_find_potentials", }, ) - + g.add_conditional_edges( "_find_potentials", self._should_summarize, @@ -411,7 +417,7 @@ def _build_graph(self): ) g.add_edge("_build_summaries", "_choose") - + g.add_conditional_edges( "_choose", self._route_after_summarization, @@ -420,7 +426,7 @@ def _build_graph(self): "Exit": END, }, ) - + g.add_edge("_author", "_run_lammps") g.add_conditional_edges( @@ -456,5 +462,3 @@ def _invoke( inputs = {**inputs, "template": "No template provided."} return self._action.invoke(inputs, config) - - \ No newline at end of file From e5edf1b3ee293c6b5e219f6b64884305d3c9a899 Mon Sep 17 00:00:00 2001 From: Adela Habib Date: Mon, 1 Dec 2025 12:41:34 -0700 Subject: [PATCH 16/31] Added gpu run commands to the lammps agent --- .../lammps_execute/EOS_of_Cu.py | 1 + src/ursa/agents/lammps_agent.py | 60 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 228a38c6..7c2d1ec8 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -23,6 +23,7 @@ max_potentials=2, max_fix_attempts=5, find_potential_only=False, + ngpus= -1, # if -1 will not use gpus. Lammps executable must be installed with kokkos package for gpu usage mpi_procs=8, workspace=workspace, lammps_cmd="lmp_mpi", diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 76366d66..0256c65d 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -46,6 +46,7 @@ def __init__( max_potentials: int = 5, max_fix_attempts: int = 10, find_potential_only: bool = False, + ngpus: int = -1, mpi_procs: int = 8, workspace: str = "./workspace", lammps_cmd: str = "lmp_mpi", @@ -61,6 +62,7 @@ def __init__( self.max_potentials = max_potentials self.max_fix_attempts = max_fix_attempts self.find_potential_only = find_potential_only + self.ngpus = ngpus self.mpi_procs = mpi_procs self.lammps_cmd = lammps_cmd self.mpirun_cmd = mpirun_cmd @@ -318,21 +320,49 @@ def _author(self, state: LammpsState) -> LammpsState: def _run_lammps(self, state: LammpsState) -> LammpsState: print("Running LAMMPS....") - result = subprocess.run( - [ - self.mpirun_cmd, - "-np", - str(self.mpi_procs), - self.lammps_cmd, - "-in", - "in.lammps", - ], - cwd=self.workspace, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - check=False, - ) + if self.ngpus >= 0: + result = subprocess.run( + [ + self.mpirun_cmd, + self.lammps_cmd, + "-in", + "in.lammps", + "-k", + "on", + "g", + str(self.ngpus), + "-sf", + "kk", + "-pk", + "kokkos", + "neigh", + "full", + "newton", + "on" + ], + cwd=self.workspace, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) + print(result) + else: + result = subprocess.run( + [ + self.mpirun_cmd, + "-np", + str(self.mpi_procs), + self.lammps_cmd, + "-in", + "in.lammps", + ], + cwd=self.workspace, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + ) return { **state, "run_returncode": result.returncode, From 67f54d1e1a6edc5fe7e8daa594d52014ce46e265 Mon Sep 17 00:00:00 2001 From: Adela Habib Date: Tue, 2 Dec 2025 16:26:46 -0700 Subject: [PATCH 17/31] Added structure data file given as an input into the agent. --- src/ursa/agents/lammps_agent.py | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 0256c65d..771fbe14 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -46,6 +46,8 @@ def __init__( max_potentials: int = 5, max_fix_attempts: int = 10, find_potential_only: bool = False, + data_file: str = None, + data_max_lines: int = 50, ngpus: int = -1, mpi_procs: int = 8, workspace: str = "./workspace", @@ -62,6 +64,8 @@ def __init__( self.max_potentials = max_potentials self.max_fix_attempts = max_fix_attempts self.find_potential_only = find_potential_only + self.data_file = data_file + self.data_max_lines = data_max_lines self.ngpus = ngpus self.mpi_procs = mpi_procs self.lammps_cmd = lammps_cmd @@ -115,6 +119,7 @@ def __init__( | self.str_parser ) + self.author_chain = ( ChatPromptTemplate.from_template( "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" @@ -122,9 +127,13 @@ def __init__( "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" "Template provided (if any): {template}\n" + "If a data file is provided, use it in the input script via the 'read_data' command.\n" + "Name of data file (if any): {data_file}\n" + "First few lines of data file (if any):\n{data_content}\n" "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" + "IMPORTANT: Properly escape all special characters in the input_script string (use \\n for newlines, \\\\ for backslashes, etc.).\n" "Use this exact schema:\n" "{{\n" ' "input_script": ""\n' @@ -145,9 +154,13 @@ def __init__( "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" "Template provided (if any): {template}\n" + "If a data file is provided, use it in the input script via the 'read_data' command.\n" + "Name of data file (if any): {data_file}\n" + "First few lines of data file (if any):\n{data_content}\n" "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" + "IMPORTANT: Properly escape all special characters in the input_script string (use \\n for newlines, \\\\ for backslashes, etc.).\n" "Use this exact schema:\n" "{{\n" ' "input_script": ""\n' @@ -169,6 +182,33 @@ def _safe_json_loads(s: str) -> dict[str, Any]: s = s[i + 1 :].strip() return json.loads(s) + def _read_and_trim_data_file(self, data_file_path: str) -> str: + """Read LAMMPS data file and trim to token limit for LLM context.""" + if os.path.exists(data_file_path): + with open(data_file_path, 'r') as f: + content = f.read() + lines = content.splitlines() + if len(lines) > self.data_max_lines: + content = "\n".join(lines[:self.data_max_lines]) + print(f"Data file trimmed from {len(lines)} to {self.data_max_lines} lines") + return content + else: + return (f"Could not read data file.") + + def _copy_data_file(self, data_file_path: str) -> str: + """Copy data file to workspace and return new path.""" + if not os.path.exists(data_file_path): + raise FileNotFoundError(f"Data file not found: {data_file_path}") + + filename = os.path.basename(data_file_path) + dest_path = os.path.join(self.workspace, filename) + with open(data_file_path, 'r') as src: + with open(dest_path, 'w') as dst: + dst.write(src.read()) + + print(f"Data file copied to workspace: {dest_path}") + return dest_path + def _fetch_and_trim_text(self, url: str) -> str: downloaded = trafilatura.fetch_url(url) if not downloaded: @@ -198,6 +238,13 @@ def _entry_router(self, state: LammpsState) -> dict: raise Exception( "You cannot set find_potential_only=True and also specify your own potential!" ) + + if self.data_file: + try: + self._copy_data_file(self.data_file) + print(f"Data file copied to workspace.") + except Exception as e: + print(f"Warning: Could not process data file: {e}") if not state.get("chosen_potential"): self.potential_summaries_dir = os.path.join( @@ -307,10 +354,17 @@ def _author(self, state: LammpsState) -> LammpsState: print("First attempt at writing LAMMPS input file....") state["chosen_potential"].download_files(self.workspace) pair_info = state["chosen_potential"].pair_info() + + data_content = "" + if self.data_file: + data_content = self._read_and_trim_data_file(self.data_file) + authored_json = self.author_chain.invoke({ "simulation_task": state["simulation_task"], "pair_info": pair_info, "template": state["template"], + "data_file": self.data_file, + "data_content": data_content, }) script_dict = self._safe_json_loads(authored_json) input_script = script_dict["input_script"] @@ -386,12 +440,18 @@ def _fix(self, state: LammpsState) -> LammpsState: pair_info = state["chosen_potential"].pair_info() err_blob = state.get("run_stdout") + data_content = "" + if self.data_file: + data_content = self._read_and_trim_data_file(self.data_file) + fixed_json = self.fix_chain.invoke({ "simulation_task": state["simulation_task"], "input_script": state["input_script"], "err_message": err_blob, "pair_info": pair_info, "template": state["template"], + "data_file": self.data_file, + "data_content": data_content, }) script_dict = self._safe_json_loads(fixed_json) new_input = script_dict["input_script"] From 363ff50a58ca6897cfbdb80bc1866dbcc2ce9bb5 Mon Sep 17 00:00:00 2001 From: Adela Habib Date: Fri, 12 Dec 2025 11:37:30 -0700 Subject: [PATCH 18/31] Added the capability to take user defined potential files --- .../lammps_execute/EOS_of_Cu.py | 2 +- .../lammps_execute/stiffness_tensor_of_hea.py | 2 +- src/ursa/agents/lammps_agent.py | 78 ++++++++++++++++--- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 7c2d1ec8..cc76c859 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -26,7 +26,7 @@ ngpus= -1, # if -1 will not use gpus. Lammps executable must be installed with kokkos package for gpu usage mpi_procs=8, workspace=workspace, - lammps_cmd="lmp_mpi", + lammps_cmd="/usr/projects/artimis/ahabib/envs/p311g-alf-packages/lammps/build-ml4chem/lmp", mpirun_cmd="mpirun", ) diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 4f6739f6..970ccf40 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -16,7 +16,7 @@ find_potential_only=False, mpi_procs=8, workspace=workspace, - lammps_cmd="lmp_mpi", + lammps_cmd="/usr/projects/artimis/ahabib/envs/p311g-alf-packages/lammps/build-ml4chem/lmp", mpirun_cmd="mpirun", ) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 771fbe14..369efdd0 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -43,6 +43,9 @@ class LammpsAgent(BaseAgent): def __init__( self, llm: BaseChatModel, + potential_files: Optional[list[str]] = None, + pair_style: Optional[str] = None, + pair_coeff: Optional[str] = None, max_potentials: int = 5, max_fix_attempts: int = 10, find_potential_only: bool = False, @@ -61,6 +64,12 @@ def __init__( raise ImportError( "LAMMPS agent requires the atomman and trafilatura dependencies. These can be installed using 'pip install ursa-ai[lammps]' or, if working from a local installation, 'pip install -e .[lammps]' ." ) + + self.user_potential_files = potential_files + self.user_pair_style = pair_style + self.user_pair_coeff = pair_coeff + self.use_user_potential = (potential_files is not None and pair_style is not None and pair_coeff is not None) + self.max_potentials = max_potentials self.max_fix_attempts = max_fix_attempts self.find_potential_only = find_potential_only @@ -202,13 +211,44 @@ def _copy_data_file(self, data_file_path: str) -> str: filename = os.path.basename(data_file_path) dest_path = os.path.join(self.workspace, filename) - with open(data_file_path, 'r') as src: - with open(dest_path, 'w') as dst: - dst.write(src.read()) - + os.system(f"cp {data_file_path} {dest_path}") print(f"Data file copied to workspace: {dest_path}") return dest_path + def _copy_user_potential_files(self): + """Copy user-provided potential files to workspace.""" + print("Copying user-provided potential files to workspace...") + for pot_file in self.user_potential_files: + if not os.path.exists(pot_file): + raise FileNotFoundError(f"Potential file not found: {pot_file}") + + filename = os.path.basename(pot_file) + dest_path = os.path.join(self.workspace, filename) + + try: + os.system(f"cp {pot_file} {dest_path}") + print(f"Potential files copied to workspace: {dest_path}") + except Exception as e: + print(f"Error copying {filename}: {e}") + raise + + def _create_user_potential_wrapper(self, state: LammpsState) -> LammpsState: + """Create a wrapper object for user-provided potential to match atomman interface.""" + self._copy_user_potential_files() + + # Create a simple object that mimics the atomman potential interface + class UserPotential: + def __init__(self, pair_style, pair_coeff): + self._pair_style = pair_style + self._pair_coeff = pair_coeff + + def pair_info(self): + return f"pair_style {self._pair_style}\npair_coeff {self._pair_coeff}" + + user_potential = UserPotential(self.user_pair_style, self.user_pair_coeff) + + return {**state, "chosen_potential": user_potential, "fix_attempts": 0} + def _fetch_and_trim_text(self, url: str) -> str: downloaded = trafilatura.fetch_url(url) if not downloaded: @@ -234,6 +274,14 @@ def _fetch_and_trim_text(self, url: str) -> str: return text def _entry_router(self, state: LammpsState) -> dict: + # Check if using user-provided potential + if self.use_user_potential: + if self.find_potential_only: + raise Exception( + "Cannot set find_potential_only=True when providing your own potential!" + ) + print("Using user-provided potential files") + if self.find_potential_only and state.get("chosen_potential"): raise Exception( "You cannot set find_potential_only=True and also specify your own potential!" @@ -242,7 +290,6 @@ def _entry_router(self, state: LammpsState) -> dict: if self.data_file: try: self._copy_data_file(self.data_file) - print(f"Data file copied to workspace.") except Exception as e: print(f"Warning: Could not process data file: {e}") @@ -352,7 +399,9 @@ def _route_after_summarization(self, state: LammpsState) -> str: def _author(self, state: LammpsState) -> LammpsState: print("First attempt at writing LAMMPS input file....") - state["chosen_potential"].download_files(self.workspace) + + if not self.use_user_potential: + state["chosen_potential"].download_files(self.workspace) pair_info = state["chosen_potential"].pair_info() data_content = "" @@ -378,6 +427,8 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: result = subprocess.run( [ self.mpirun_cmd, + "-np", + str(self.mpi_procs), self.lammps_cmd, "-in", "in.lammps", @@ -390,9 +441,9 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: "-pk", "kokkos", "neigh", - "full", + "half", "newton", - "on" + "on", ], cwd=self.workspace, stdout=subprocess.PIPE, @@ -471,6 +522,7 @@ def _build_graph(self): self.add_node(g, self._summarize_one) self.add_node(g, self._build_summaries) self.add_node(g, self._choose) + self.add_node(g, self._create_user_potential_wrapper) self.add_node(g, self._author) self.add_node(g, self._run_lammps) self.add_node(g, self._fix) @@ -479,10 +531,11 @@ def _build_graph(self): g.add_conditional_edges( "_entry_router", - lambda state: "user_choice" - if state.get("chosen_potential") - else "agent_choice", - { + lambda state: "user_potential" if self.use_user_potential else ( + "user_choice" if state.get("chosen_potential") else "agent_choice" + ), + { + "user_potential": "_create_user_potential_wrapper", "user_choice": "_author", "agent_choice": "_find_potentials", }, @@ -518,6 +571,7 @@ def _build_graph(self): }, ) + g.add_edge("_create_user_potential_wrapper", "_author") g.add_edge("_author", "_run_lammps") g.add_conditional_edges( From 0879820c7a05e9ac8657bc593c61efb86be20d75 Mon Sep 17 00:00:00 2001 From: Adela Habib Date: Fri, 12 Dec 2025 11:39:24 -0700 Subject: [PATCH 19/31] Minor changes to the example files, reverted lammps executable command back to generic lmp_mpi --- examples/two_agent_examples/lammps_execute/EOS_of_Cu.py | 2 +- .../lammps_execute/stiffness_tensor_of_hea.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index cc76c859..7c2d1ec8 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -26,7 +26,7 @@ ngpus= -1, # if -1 will not use gpus. Lammps executable must be installed with kokkos package for gpu usage mpi_procs=8, workspace=workspace, - lammps_cmd="/usr/projects/artimis/ahabib/envs/p311g-alf-packages/lammps/build-ml4chem/lmp", + lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", ) diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 970ccf40..4f6739f6 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -16,7 +16,7 @@ find_potential_only=False, mpi_procs=8, workspace=workspace, - lammps_cmd="/usr/projects/artimis/ahabib/envs/p311g-alf-packages/lammps/build-ml4chem/lmp", + lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", ) From 4819fd4a715cd55886a96396c4efb469bacbbb13 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram - 365493 Date: Tue, 13 Jan 2026 12:41:12 -0700 Subject: [PATCH 20/31] Minor update to lammps agent prompt --- src/ursa/agents/lammps_agent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 369efdd0..6806e96b 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -139,8 +139,8 @@ def __init__( "If a data file is provided, use it in the input script via the 'read_data' command.\n" "Name of data file (if any): {data_file}\n" "First few lines of data file (if any):\n{data_content}\n" - "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" - "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" + "Ensure that all logs are recorded in a './log.lammps' file.\n" + "To create the log file, use may use the 'log ./log.lammps' command. \n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" "IMPORTANT: Properly escape all special characters in the input_script string (use \\n for newlines, \\\\ for backslashes, etc.).\n" "Use this exact schema:\n" @@ -166,8 +166,8 @@ def __init__( "If a data file is provided, use it in the input script via the 'read_data' command.\n" "Name of data file (if any): {data_file}\n" "First few lines of data file (if any):\n{data_content}\n" - "Ensure that all output data is written only to the './log.lammps' file. Do not create any other output file.\n" - "To create the log, use only the 'log ./log.lammps' command. Do not use any other command like 'echo' or 'screen'.\n" + "Ensure that all logs are recorded in a './log.lammps' file.\n" + "To create the log file, use may use the 'log ./log.lammps' command. \n" "Return your answer **only** as valid JSON, with no extra text or formatting.\n" "IMPORTANT: Properly escape all special characters in the input_script string (use \\n for newlines, \\\\ for backslashes, etc.).\n" "Use this exact schema:\n" From a0dfbb8d6aea64671a02246f1f10ff1a97880f9d Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram - 365493 Date: Thu, 15 Jan 2026 14:51:02 -0700 Subject: [PATCH 21/31] Full error history passed to the _fix node, instead of just the latest error --- src/ursa/agents/lammps_agent.py | 44 ++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 6806e96b..62f495d0 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -35,7 +35,8 @@ class LammpsState(TypedDict, total=False): run_returncode: Optional[int] run_stdout: str run_stderr: str - + run_history: list[dict[str, Any]] + fix_attempts: int @@ -157,7 +158,8 @@ def __init__( "You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task}\n" "For this purpose, this input file for LAMMPS was written: {input_script}\n" "However, when running the simulation, an error was raised.\n" - "Here is the full stdout message that includes the error message: {err_message}\n" + "Here is the run history across attempts (each includes the input script and its stdout/stderr):\n{err_message}\n" + "Use the history to identify what changed between attempts and avoid repeating failed approaches.\n" "Your task is to write a new input file that resolves the error.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" @@ -247,7 +249,7 @@ def pair_info(self): user_potential = UserPotential(self.user_pair_style, self.user_pair_coeff) - return {**state, "chosen_potential": user_potential, "fix_attempts": 0} + return {**state, "chosen_potential": user_potential, "fix_attempts": 0,"run_history": []} def _fetch_and_trim_text(self, url: str) -> str: downloaded = trafilatura.fetch_url(url) @@ -313,6 +315,7 @@ def _find_potentials(self, state: LammpsState) -> LammpsState: "summaries": [], "full_texts": [], "fix_attempts": 0, + "run_history": [], } def _should_summarize(self, state: LammpsState) -> str: @@ -468,13 +471,26 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: text=True, check=False, ) + + + hist = list(state.get("run_history", [])) + hist.append({ + "attempt": state.get("fix_attempts", 0), + "input_script": state.get("input_script", ""), + "returncode": result.returncode, + "stdout": result.stdout, + "stderr": result.stderr, + }) + return { **state, "run_returncode": result.returncode, "run_stdout": result.stdout, "run_stderr": result.stderr, + "run_history": hist, } + def _route_run(self, state: LammpsState) -> str: rc = state.get("run_returncode", 0) attempts = state.get("fix_attempts", 0) @@ -489,8 +505,28 @@ def _route_run(self, state: LammpsState) -> str: def _fix(self, state: LammpsState) -> LammpsState: pair_info = state["chosen_potential"].pair_info() - err_blob = state.get("run_stdout") + hist = state.get("run_history", []) + if not hist: + hist = [{ + "attempt": state.get("fix_attempts", 0), + "input_script": state.get("input_script", ""), + "returncode": state.get("run_returncode"), + "stdout": state.get("run_stdout", ""), + "stderr": state.get("run_stderr", ""), + }] + + parts = [] + for h in hist: + parts.append( + "=== Attempt {attempt} | returncode={returncode} ===\n" + "--- input_script ---\n{input_script}\n" + "--- stdout ---\n{stdout}\n" + "--- stderr ---\n{stderr}\n" + .format(**h) + ) + err_blob = "\n".join(parts) + data_content = "" if self.data_file: data_content = self._read_and_trim_data_file(self.data_file) From b14c05099550268e1be54277d1e2d0ee076f311b Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram - 365493 Date: Thu, 15 Jan 2026 16:30:35 -0700 Subject: [PATCH 22/31] Used Rich to improve terminal outputs --- src/ursa/agents/lammps_agent.py | 88 ++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 62f495d0..97ce740f 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -9,6 +9,16 @@ from langchain_core.prompts import ChatPromptTemplate from langgraph.graph import END, StateGraph +from rich.console import Console +from rich.panel import Panel +from rich.text import Text +from rich.syntax import Syntax +from rich.rule import Rule +from rich.table import Table +from rich import box +from rich.console import Group +import difflib + from .base import BaseAgent working = True @@ -83,6 +93,8 @@ def __init__( self.tiktoken_model = tiktoken_model self.max_tokens = max_tokens + self.console = Console() + self.pair_styles = [ "eam", "eam/alloy", @@ -156,11 +168,10 @@ def __init__( self.fix_chain = ( ChatPromptTemplate.from_template( "You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task}\n" - "For this purpose, this input file for LAMMPS was written: {input_script}\n" - "However, when running the simulation, an error was raised.\n" - "Here is the run history across attempts (each includes the input script and its stdout/stderr):\n{err_message}\n" + "Multiple attempts at writing and running a LAMMPS input file have been made.\n" + "Here is the run history across attempts (each includes the input script and its stdout/stderr):{err_message}\n" "Use the history to identify what changed between attempts and avoid repeating failed approaches.\n" - "Your task is to write a new input file that resolves the error.\n" + "Your task is to write a new input file that resolves the latest error.\n" "Note that all potential files are in the './' directory.\n" "Here is some information about the pair_style and pair_coeff that might be useful in writing the input file: {pair_info}.\n" "If a template for the input file is provided, you should adapt it appropriately to meet the task requirements.\n" @@ -183,6 +194,32 @@ def __init__( self._action = self._build_graph() + def _section(self, title: str): + self.console.print(Rule(f"[bold cyan]{title}[/bold cyan]")) + + def _panel(self, title: str, body: str, style: str = "cyan"): + self.console.print(Panel(body, title=f"[bold]{title}[/bold]", border_style=style)) + + def _code_panel(self, title: str, code: str, language: str = "bash", style: str = "magenta"): + syn = Syntax(code, language, theme="monokai", line_numbers=True, word_wrap=True) + self.console.print(Panel(syn, title=f"[bold]{title}[/bold]", border_style=style)) + + def _diff_panel(self, old: str, new: str, title: str = "LAMMPS input diff"): + diff = "\n".join( + difflib.unified_diff( + old.splitlines(), + new.splitlines(), + fromfile="in.lammps (before)", + tofile="in.lammps (after)", + lineterm="", + ) + ) + if not diff.strip(): + diff = "(no changes)" + syn = Syntax(diff, "diff", theme="monokai", line_numbers=False, word_wrap=True) + self.console.print(Panel(syn, title=f"[bold]{title}[/bold]", border_style="cyan")) + + @staticmethod def _safe_json_loads(s: str) -> dict[str, Any]: s = s.strip() @@ -330,7 +367,7 @@ def _should_summarize(self, state: LammpsState) -> str: def _summarize_one(self, state: LammpsState) -> LammpsState: i = state["idx"] - print(f"Summarizing potential #{i}") + self._section(f"Summarizing potential #{i}") match = state["matches"][i] md = match.metadata() @@ -371,20 +408,22 @@ def _build_summaries(self, state: LammpsState) -> LammpsState: return {**state, "summaries_combined": "".join(parts)} def _choose(self, state: LammpsState) -> LammpsState: - print("Choosing one potential for this task...") + self._section("Choosing potential") choice = self.choose_chain.invoke({ "summaries_combined": state["summaries_combined"], "simulation_task": state["simulation_task"], }) choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) - - print(f"Chosen potential #{chosen_index}") - print("Rationale for choosing this potential:") - print(choice_dict["rationale"]) - + chosen_potential = state["matches"][chosen_index] + self._panel( + "Chosen Potential", + f"[bold]Index:[/bold] {chosen_index}\n[bold]ID:[/bold] {chosen_potential.id}\n\n[bold]Rationale:[/bold]\n{choice_dict['rationale']}", + style="green", + ) + out_file = os.path.join(self.potential_summaries_dir, "Rationale.txt") with open(out_file, "w") as f: f.write(f"Chosen potential #{chosen_index}") @@ -401,7 +440,7 @@ def _route_after_summarization(self, state: LammpsState) -> str: return "continue_author" def _author(self, state: LammpsState) -> LammpsState: - print("First attempt at writing LAMMPS input file....") + self._section("First attempt at writing LAMMPS input file") if not self.use_user_potential: state["chosen_potential"].download_files(self.workspace) @@ -422,10 +461,15 @@ def _author(self, state: LammpsState) -> LammpsState: input_script = script_dict["input_script"] with open(os.path.join(self.workspace, "in.lammps"), "w") as f: f.write(input_script) + + self._section("Authored LAMMPS input") + self._code_panel("in.lammps", input_script, language="bash", style="magenta") + return {**state, "input_script": input_script} def _run_lammps(self, state: LammpsState) -> LammpsState: - print("Running LAMMPS....") + self._section("Running LAMMPS") + if self.ngpus >= 0: result = subprocess.run( [ @@ -472,7 +516,13 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: check=False, ) + status_style = "green" if result.returncode == 0 else "red" + self._panel("Run Result", f"returncode = {result.returncode}", style=status_style) + if result.returncode != 0: + err_view = (result.stderr.strip() + "\n" + result.stdout.strip()).strip() or "(no output captured)" + self._panel("Run error/output", err_view[-6000:], style="red") + hist = list(state.get("run_history", [])) hist.append({ "attempt": state.get("fix_attempts", 0), @@ -495,12 +545,12 @@ def _route_run(self, state: LammpsState) -> str: rc = state.get("run_returncode", 0) attempts = state.get("fix_attempts", 0) if rc == 0: - print("LAMMPS run successful! Exiting...") + self._section("LAMMPS run successful! Exiting...") return "done_success" if attempts < self.max_fix_attempts: - print("LAMMPS run Failed. Attempting to rewrite input file...") + self._section("LAMMPS run Failed. Attempting to rewrite input file...") return "need_fix" - print("LAMMPS run Failed and maximum fix attempts reached. Exiting...") + self._section("LAMMPS run Failed and maximum fix attempts reached. Exiting..") return "done_failed" def _fix(self, state: LammpsState) -> LammpsState: @@ -533,7 +583,6 @@ def _fix(self, state: LammpsState) -> LammpsState: fixed_json = self.fix_chain.invoke({ "simulation_task": state["simulation_task"], - "input_script": state["input_script"], "err_message": err_blob, "pair_info": pair_info, "template": state["template"], @@ -541,9 +590,14 @@ def _fix(self, state: LammpsState) -> LammpsState: "data_content": data_content, }) script_dict = self._safe_json_loads(fixed_json) + new_input = script_dict["input_script"] + old_input = state["input_script"] + self._diff_panel(old_input, new_input) + with open(os.path.join(self.workspace, "in.lammps"), "w") as f: f.write(new_input) + return { **state, "input_script": new_input, From 1ca41bf25883443bc82bb2f85bd5f4ac962241d5 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 16 Jan 2026 12:50:18 -0700 Subject: [PATCH 23/31] Added documentation for LAMMPS Agent --- docs/lammps_agent.md | 191 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/lammps_agent.md diff --git a/docs/lammps_agent.md b/docs/lammps_agent.md new file mode 100644 index 00000000..f9138731 --- /dev/null +++ b/docs/lammps_agent.md @@ -0,0 +1,191 @@ +# LammpsAgent Documentation + +`LammpsAgent` is a class that helps set up and run a LAMMPS simulation workflow. At the highest level, it can: + +- discover candidate interatomic potentials from the NIST database for a set of elements, +- summarize and choose a potential for the simulation task at hand, +- author a LAMMPS input script using the chosen potential (and an optional template / data file), +- execute LAMMPS via MPI (CPU or Kokkos GPU), +- iteratively “fix” the input script on failures by using run history until success or a max attempt limit. + +The agent writes the outputs into a local `workspace` directory and uses rich console panels to display progress, choices, diffs, and errors. + +--- + +## Dependencies + +The main dependency is the [LAMMPS](https://www.lammps.org) code that needs to be installed separately. LAMMPS is a classical molecular dynamics code developed by Sandia National Laboratories. Installation instructions can be found [here](https://docs.lammps.org/Install.html). On macOS and linux systems, the simplest way to install LAMMPS is often via [Conda](https://anaconda.org/channels/conda-forge/packages/lammps/overview), in the same conda environment where `ursa` is installed. + +Two additional dependencies, that are not installed along with `ursa`, are `atomman` and `trafilatura`. These can be installed via `pip install atomman` and `pip install trafilatura`. + +--- + +## Basic Usage + +```python +from ursa.agents import LammpsAgent +from langchain_openai import ChatOpenAI + +agent = LammpsAgent(llm = ChatOpenAI(model='gpt-5')) + +result = agent.invoke({ + "simulation_task": "Carry out a LAMMPS simulation of Cu to determine its equation of state.", + "elements": ["Cu"], + "template": "No template provided." #Template for the input file +}) +``` + +For more advanced usage see examples here: `ursa/examples/two_agent_examples/lammps_execute/`. + +--- + +## High-level flow + +The agent compiles a `StateGraph(LammpsState)` with this logic: + +### Entry routing +Chooses one of three paths: + +1. **User-provided potential**: + - This path is chosen when the user provides a specific potential file, along with the `pair_style`/`pair_coeff` information required to generate the input script + - In this case the autonomous potential search/selection by the agent is skipped + - The provided potential file is copied to `workspace` + +2. **User-chosen potential already in state** (`state["chosen_potential"]` exists): + - This is similar to the above path, but the user selects a potential from the `atomman` database and initializes the state with this entry before invoking the agent + - This path also skips the potential search/selection and goes straight to authoring a LAMMPS input script for the user-chosen potential + +3. **Agent-selected potential**: + - Agent queries NIST (via atomman) for potentials matching the requested elements + - Summarizes NIST's data on each potential (up to `max_potentials`) with regards to the applicability of the potential for the given `simulation task` + - Ultimately picks one potential + +If a `data_file` is provided to the agent, the entry router attempts to copy it into the workspace. + +### Potential search & selection (agent-selected path) +- `_find_potentials`: queries `atomman.library.Database(remote=True)` for potentials matching: + - `elements` from state + - supported `pair_styles` list (see `self.pair_styles`) +- `_summarize_one`: for each candidate potential: + - extracts data on potential from NIST + - trims extracted text to a token budget using `tiktoken` + - summarizes usefulness for the requested `simulation_task` + - writes summary to `workspace/potential_summaries/potential_.txt` +- `_build_summaries`: builds a combined string of summaries for selection +- `_choose`: the agent selects the final potential to be used and the rationale for choosing it + - writes rationale to `workspace/potential_summaries/Rationale.txt` + - stores `chosen_potential` in state + +If `find_potential_only=True`, the graph exits after choosing the potential (or finding no matches). + +### Author input +- Downloads potential files into `workspace` (only if not user-provided) +- Gets `pair_info` via `chosen_potential.pair_info()` +- Optionally includes: + - `template` from state for the LAMMPS input script + - `data_file` (usually for the atomic structure that can be included in the input script) +- The agent authors the input script: `{ "input_script": "" }` +- Writes `workspace/in.lammps` +- Enforces that logs should go to `./log.lammps` + +### Run LAMMPS + +Runs `` with `-np ` in `workspace`: + +Allowed options for `` are `mpirun` and `mpiexec` (see also Parameters section below). + +For example, LAMMPS run commands executed by the agent look like: + +- **CPU mode** (default, when `ngpus < 0`): + - `mpirun -np -in in.lammps` + +- **GPU/Kokkos mode** (when `ngpus >= 0`): + - `mpirun -np -in in.lammps -k on g -sf kk -pk kokkos neigh half newton on` + +Note that the running under GPU mode is preliminary. + +The agent captures `stdout`, `stderr`, and `returncode`, and appends an entry to `run_history`. + +### Fix loop +If the run fails: +- formats the entire `run_history` (scripts + stdout/stderr) into an error blob +- the agent produces a new `input_script` +- prints a unified diff between old and new scripts +- overwrites `workspace/in.lammps` +- increments `fix_attempts` +- reruns LAMMPS + +Stops when: +- run succeeds (`returncode == 0`), or +- `fix_attempts >= max_fix_attempts` + +--- + +## State model (`LammpsState`) + +The graph state is a `TypedDict` containing (key fields): + +- **Inputs / problem definition** + - `simulation_task: str` — natural language description of what to simulate + - `elements: list[str]` — chemical symbols used to identify candidate potentials + - `template: Optional[str]` — optional LAMMPS input template to adapt + - `chosen_potential: Optional[Any]` — selected potential object (user-chosen) + +- **Potential selection internals** + - `matches: list[Any]` — candidate potentials from atomman + - `idx: int` — index used for summarization loop + - `summaries: list[str]` — a brief summary of each potential + - `full_texts: list[str]` — the data/metadata on the potential from NIST (capped at `max_tokens`) + - `summaries_combined: str` - a single string with the summaries of all the considered potentials + +- **Run artifacts** + - `input_script: str` — current LAMMPS input text written to `in.lammps` + - `run_returncode: Optional[int]` - generally, `returncode = 0` indicates a successful simulation run + - `run_stdout: str` - the stdout from the LAMMPS execution + - `run_stderr: str` - the stderr from the LAMMPS execution + - `run_history: list[dict[str, Any]]` — attempt-by-attempt record + - `fix_attempts: int` - the number of times the agent has attempted to fix the LAMMPS input script + +--- + +## Parameters + +Key parameters you can tune: + +### Potential selection +- `potential_files`, `pair_style`, `pair_coeff`: if all provided, the agent uses the user's potential files and skips search +- `max_potentials` (default `5`): max number of candidate potentials to summarize before choosing one +- `find_potential_only` (default `False`): exit after selecting a potential (no input LAMMPS input writing/running) + +### Fix loop +- `max_fix_attempts` (default `10`): maximum number of input rewrite attempts after failures + +### Data file support +- `data_file` (default `None`): path to a LAMMPS data file; the agent copies it to `workspace` +- `data_max_lines` (default `50`): number of lines from data included in the agent's prompt + +### Execution +- `workspace` (default `./workspace`): where `in.lammps`, potentials, and summaries are written +- `mpi_procs` (default `8`): number of mpi processes for LAMMPS run +- `ngpus` (default `-1`): set `>= 0` to enable Kokkos GPU flags +- `lammps_cmd` (default `lmp_mpi`): the name of the LAMMPS executable to launch +- `mpirun_cmd` (default `mpirun`): currently available options are `mpirun` and `mpiexec`. Other options such as `srun` will be added soon + +### LLM / context trimming +- `tiktoken_model` (default `gpt-5-mini`): tokenizer model name used to trim fetched potential metadata text +- `max_tokens` (default `200000`): token cap for extracted metadata text + +--- + +## Files and directories created + +Inside `workspace/`: + +- `in.lammps` — generated/updated input script +- `log.lammps` — expected LAMMPS log output (the LLM is instructed to create it) +- `potential_summaries/` + - `potential_.txt` — per-potential LLM summaries + - `Rationale.txt` — rationale for the selected potential +- downloaded potential files (from atomman or copied from user paths) +- copied `data_file` (if provided) + From 12b7a9c76ae04b9534f040e6503dd36626f87820 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Fri, 16 Jan 2026 13:31:35 -0700 Subject: [PATCH 24/31] Linted and Formatted code with Ruff --- .../lammps_execute/EOS_of_Cu.py | 2 +- src/ursa/agents/lammps_agent.py | 187 +++++++++++------- 2 files changed, 114 insertions(+), 75 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 7c2d1ec8..95aadb5e 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -23,7 +23,7 @@ max_potentials=2, max_fix_attempts=5, find_potential_only=False, - ngpus= -1, # if -1 will not use gpus. Lammps executable must be installed with kokkos package for gpu usage + ngpus=-1, # if -1 will not use gpus. Lammps executable must be installed with kokkos package for gpu usage mpi_procs=8, workspace=workspace, lammps_cmd="lmp_mpi", diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index ce32349c..d9bb52b6 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -1,3 +1,4 @@ +import difflib import json import os import subprocess @@ -8,16 +9,10 @@ from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langgraph.graph import END - from rich.console import Console from rich.panel import Panel -from rich.text import Text -from rich.syntax import Syntax from rich.rule import Rule -from rich.table import Table -from rich import box -from rich.console import Group -import difflib +from rich.syntax import Syntax from .base import BaseAgent @@ -45,8 +40,8 @@ class LammpsState(TypedDict, total=False): run_returncode: Optional[int] run_stdout: str run_stderr: str - run_history: list[dict[str, Any]] - + run_history: list[dict[str, Any]] + fix_attempts: int @@ -79,12 +74,16 @@ def __init__( ) super().__init__(llm, **kwargs) - + self.user_potential_files = potential_files self.user_pair_style = pair_style self.user_pair_coeff = pair_coeff - self.use_user_potential = (potential_files is not None and pair_style is not None and pair_coeff is not None) - + self.use_user_potential = ( + potential_files is not None + and pair_style is not None + and pair_coeff is not None + ) + self.max_potentials = max_potentials self.max_fix_attempts = max_fix_attempts self.find_potential_only = find_potential_only @@ -98,7 +97,7 @@ def __init__( self.max_tokens = max_tokens self.console = Console() - + self.pair_styles = [ "eam", "eam/alloy", @@ -143,7 +142,6 @@ def __init__( | self.str_parser ) - self.author_chain = ( ChatPromptTemplate.from_template( "Your task is to write a LAMMPS input file for this purpose: {simulation_task}.\n" @@ -193,18 +191,30 @@ def __init__( | self.llm | self.str_parser ) - + self._action = self._build_graph() def _section(self, title: str): self.console.print(Rule(f"[bold cyan]{title}[/bold cyan]")) - + def _panel(self, title: str, body: str, style: str = "cyan"): - self.console.print(Panel(body, title=f"[bold]{title}[/bold]", border_style=style)) - - def _code_panel(self, title: str, code: str, language: str = "bash", style: str = "magenta"): - syn = Syntax(code, language, theme="monokai", line_numbers=True, word_wrap=True) - self.console.print(Panel(syn, title=f"[bold]{title}[/bold]", border_style=style)) + self.console.print( + Panel(body, title=f"[bold]{title}[/bold]", border_style=style) + ) + + def _code_panel( + self, + title: str, + code: str, + language: str = "bash", + style: str = "magenta", + ): + syn = Syntax( + code, language, theme="monokai", line_numbers=True, word_wrap=True + ) + self.console.print( + Panel(syn, title=f"[bold]{title}[/bold]", border_style=style) + ) def _diff_panel(self, old: str, new: str, title: str = "LAMMPS input diff"): diff = "\n".join( @@ -218,9 +228,13 @@ def _diff_panel(self, old: str, new: str, title: str = "LAMMPS input diff"): ) if not diff.strip(): diff = "(no changes)" - syn = Syntax(diff, "diff", theme="monokai", line_numbers=False, word_wrap=True) - self.console.print(Panel(syn, title=f"[bold]{title}[/bold]", border_style="cyan")) - + syn = Syntax( + diff, "diff", theme="monokai", line_numbers=False, word_wrap=True + ) + self.console.print( + Panel(syn, title=f"[bold]{title}[/bold]", border_style="cyan") + ) + @staticmethod def _safe_json_loads(s: str) -> dict[str, Any]: s = s.strip() @@ -234,21 +248,23 @@ def _safe_json_loads(s: str) -> dict[str, Any]: def _read_and_trim_data_file(self, data_file_path: str) -> str: """Read LAMMPS data file and trim to token limit for LLM context.""" if os.path.exists(data_file_path): - with open(data_file_path, 'r') as f: + with open(data_file_path, "r") as f: content = f.read() lines = content.splitlines() if len(lines) > self.data_max_lines: - content = "\n".join(lines[:self.data_max_lines]) - print(f"Data file trimmed from {len(lines)} to {self.data_max_lines} lines") + content = "\n".join(lines[: self.data_max_lines]) + print( + f"Data file trimmed from {len(lines)} to {self.data_max_lines} lines" + ) return content - else: - return (f"Could not read data file.") - + else: + return "Could not read data file." + def _copy_data_file(self, data_file_path: str) -> str: """Copy data file to workspace and return new path.""" if not os.path.exists(data_file_path): raise FileNotFoundError(f"Data file not found: {data_file_path}") - + filename = os.path.basename(data_file_path) dest_path = os.path.join(self.workspace, filename) os.system(f"cp {data_file_path} {dest_path}") @@ -261,10 +277,10 @@ def _copy_user_potential_files(self): for pot_file in self.user_potential_files: if not os.path.exists(pot_file): raise FileNotFoundError(f"Potential file not found: {pot_file}") - + filename = os.path.basename(pot_file) dest_path = os.path.join(self.workspace, filename) - + try: os.system(f"cp {pot_file} {dest_path}") print(f"Potential files copied to workspace: {dest_path}") @@ -275,20 +291,27 @@ def _copy_user_potential_files(self): def _create_user_potential_wrapper(self, state: LammpsState) -> LammpsState: """Create a wrapper object for user-provided potential to match atomman interface.""" self._copy_user_potential_files() - + # Create a simple object that mimics the atomman potential interface class UserPotential: def __init__(self, pair_style, pair_coeff): self._pair_style = pair_style self._pair_coeff = pair_coeff - + def pair_info(self): return f"pair_style {self._pair_style}\npair_coeff {self._pair_coeff}" - - user_potential = UserPotential(self.user_pair_style, self.user_pair_coeff) - - return {**state, "chosen_potential": user_potential, "fix_attempts": 0,"run_history": []} - + + user_potential = UserPotential( + self.user_pair_style, self.user_pair_coeff + ) + + return { + **state, + "chosen_potential": user_potential, + "fix_attempts": 0, + "run_history": [], + } + def _fetch_and_trim_text(self, url: str) -> str: downloaded = trafilatura.fetch_url(url) if not downloaded: @@ -326,7 +349,7 @@ def _entry_router(self, state: LammpsState) -> dict: raise Exception( "You cannot set find_potential_only=True and also specify your own potential!" ) - + if self.data_file: try: self._copy_data_file(self.data_file) @@ -416,7 +439,7 @@ def _choose(self, state: LammpsState) -> LammpsState: }) choice_dict = self._safe_json_loads(choice) chosen_index = int(choice_dict["Chosen index"]) - + chosen_potential = state["matches"][chosen_index] self._panel( @@ -424,7 +447,7 @@ def _choose(self, state: LammpsState) -> LammpsState: f"[bold]Index:[/bold] {chosen_index}\n[bold]ID:[/bold] {chosen_potential.id}\n\n[bold]Rationale:[/bold]\n{choice_dict['rationale']}", style="green", ) - + out_file = os.path.join(self.potential_summaries_dir, "Rationale.txt") with open(out_file, "w") as f: f.write(f"Chosen potential #{chosen_index}") @@ -446,7 +469,7 @@ def _author(self, state: LammpsState) -> LammpsState: if not self.use_user_potential: state["chosen_potential"].download_files(self.workspace) pair_info = state["chosen_potential"].pair_info() - + data_content = "" if self.data_file: data_content = self._read_and_trim_data_file(self.data_file) @@ -464,13 +487,15 @@ def _author(self, state: LammpsState) -> LammpsState: f.write(input_script) self._section("Authored LAMMPS input") - self._code_panel("in.lammps", input_script, language="bash", style="magenta") - + self._code_panel( + "in.lammps", input_script, language="bash", style="magenta" + ) + return {**state, "input_script": input_script} def _run_lammps(self, state: LammpsState) -> LammpsState: self._section("Running LAMMPS") - + if self.ngpus >= 0: result = subprocess.run( [ @@ -518,12 +543,18 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: ) status_style = "green" if result.returncode == 0 else "red" - self._panel("Run Result", f"returncode = {result.returncode}", style=status_style) + self._panel( + "Run Result", + f"returncode = {result.returncode}", + style=status_style, + ) if result.returncode != 0: - err_view = (result.stderr.strip() + "\n" + result.stdout.strip()).strip() or "(no output captured)" - self._panel("Run error/output", err_view[-6000:], style="red") - + err_view = ( + result.stderr.strip() + "\n" + result.stdout.strip() + ).strip() or "(no output captured)" + self._panel("Run error/output", err_view[-6000:], style="red") + hist = list(state.get("run_history", [])) hist.append({ "attempt": state.get("fix_attempts", 0), @@ -541,7 +572,6 @@ def _run_lammps(self, state: LammpsState) -> LammpsState: "run_history": hist, } - def _route_run(self, state: LammpsState) -> str: rc = state.get("run_returncode", 0) attempts = state.get("fix_attempts", 0) @@ -549,9 +579,13 @@ def _route_run(self, state: LammpsState) -> str: self._section("LAMMPS run successful! Exiting...") return "done_success" if attempts < self.max_fix_attempts: - self._section("LAMMPS run Failed. Attempting to rewrite input file...") + self._section( + "LAMMPS run Failed. Attempting to rewrite input file..." + ) return "need_fix" - self._section("LAMMPS run Failed and maximum fix attempts reached. Exiting..") + self._section( + "LAMMPS run Failed and maximum fix attempts reached. Exiting.." + ) return "done_failed" def _fix(self, state: LammpsState) -> LammpsState: @@ -559,25 +593,26 @@ def _fix(self, state: LammpsState) -> LammpsState: hist = state.get("run_history", []) if not hist: - hist = [{ - "attempt": state.get("fix_attempts", 0), - "input_script": state.get("input_script", ""), - "returncode": state.get("run_returncode"), - "stdout": state.get("run_stdout", ""), - "stderr": state.get("run_stderr", ""), - }] - + hist = [ + { + "attempt": state.get("fix_attempts", 0), + "input_script": state.get("input_script", ""), + "returncode": state.get("run_returncode"), + "stdout": state.get("run_stdout", ""), + "stderr": state.get("run_stderr", ""), + } + ] + parts = [] for h in hist: parts.append( "=== Attempt {attempt} | returncode={returncode} ===\n" "--- input_script ---\n{input_script}\n" "--- stdout ---\n{stdout}\n" - "--- stderr ---\n{stderr}\n" - .format(**h) + "--- stderr ---\n{stderr}\n".format(**h) ) err_blob = "\n".join(parts) - + data_content = "" if self.data_file: data_content = self._read_and_trim_data_file(self.data_file) @@ -591,14 +626,14 @@ def _fix(self, state: LammpsState) -> LammpsState: "data_content": data_content, }) script_dict = self._safe_json_loads(fixed_json) - + new_input = script_dict["input_script"] old_input = state["input_script"] self._diff_panel(old_input, new_input) - + with open(os.path.join(self.workspace, "in.lammps"), "w") as f: f.write(new_input) - + return { **state, "input_script": new_input, @@ -615,15 +650,19 @@ def _build_graph(self): self.add_node(self._author) self.add_node(self._run_lammps) self.add_node(self._fix) - + self.graph.set_entry_point("_entry_router") - + self.graph.add_conditional_edges( "_entry_router", - lambda state: "user_potential" if self.use_user_potential else ( - "user_choice" if state.get("chosen_potential") else "agent_choice" + lambda state: "user_potential" + if self.use_user_potential + else ( + "user_choice" + if state.get("chosen_potential") + else "agent_choice" ), - { + { "user_potential": "_create_user_potential_wrapper", "user_choice": "_author", "agent_choice": "_find_potentials", @@ -659,7 +698,7 @@ def _build_graph(self): "Exit": END, }, ) - + self.graph.add_edge("_create_user_potential_wrapper", "_author") self.graph.add_edge("_author", "_run_lammps") From a03f3da471d98fb70d5f56cd53674b1285f78015 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram Date: Tue, 20 Jan 2026 16:48:37 -0700 Subject: [PATCH 25/31] Minor bug fixed --- src/ursa/agents/lammps_agent.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index d9bb52b6..af2cad51 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -192,8 +192,6 @@ def __init__( | self.str_parser ) - self._action = self._build_graph() - def _section(self, title: str): self.console.print(Rule(f"[bold cyan]{title}[/bold cyan]")) From 0cef0cddc881872a972a8a87fa99d5af86e26436 Mon Sep 17 00:00:00 2001 From: Rahul Somasundaram - 365493 Date: Tue, 20 Jan 2026 17:30:21 -0700 Subject: [PATCH 26/31] Added ability to call Execution agent within Lammps agent for summarization/visualization of results --- .../lammps_execute/EOS_of_Cu.py | 24 ++--------- .../lammps_execute/stiffness_tensor_of_hea.py | 24 ++--------- src/ursa/agents/lammps_agent.py | 43 ++++++++++++++++++- 3 files changed, 48 insertions(+), 43 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index 95aadb5e..35d66d84 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -1,7 +1,5 @@ -from langchain_core.messages import HumanMessage from langchain_openai import ChatOpenAI - -from ursa.agents import ExecutionAgent, LammpsAgent +from ursa.agents import LammpsAgent try: import atomman as am @@ -28,6 +26,7 @@ workspace=workspace, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", + summarize_results=True, ) with open("eos_template.txt", "r") as file: @@ -51,21 +50,4 @@ ) if final_lammps_state.get("run_returncode") == 0: - print("\nNow handing things off to execution agent.....") - - executor = ExecutionAgent(llm=llm) - exe_plan = f""" - You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task} - - A LAMMPS simulation has been done and the output is located in the file 'log.lammps'. - - Summarize the contents of this file in a markdown document. Include a plot, if relevent. - """ - - final_results = executor.invoke({ - "messages": [HumanMessage(content=exe_plan)], - "workspace": workspace, - }) - - for x in final_results["messages"]: - print(x.content) + print("\nLAMMPS Workflow completed successfully. Exiting.....") diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 4f6739f6..dd4e1c60 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -1,7 +1,5 @@ -from langchain_core.messages import HumanMessage from langchain_openai import ChatOpenAI - -from ursa.agents import ExecutionAgent, LammpsAgent +from ursa.agents import LammpsAgent model = "gpt-5" @@ -18,6 +16,7 @@ workspace=workspace, lammps_cmd="lmp_mpi", mpirun_cmd="mpirun", + summarize_results=True, ) with open("elastic_template.txt", "r") as file: @@ -32,21 +31,4 @@ ) if final_lammps_state.get("run_returncode") == 0: - print("\nNow handing things off to execution agent.....") - - executor = ExecutionAgent(llm=llm) - exe_plan = f""" - You are part of a larger scientific workflow whose purpose is to accomplish this task: {simulation_task} - - A LAMMPS simulation has been done and the output is located in the file 'log.lammps'. - - Summarize the contents of this file in a markdown document. Include a plot, if relevent. - """ - - final_results = executor.invoke({ - "messages": [HumanMessage(content=exe_plan)], - "workspace": workspace, - }) - - for x in final_results["messages"]: - print(x.content) + print("\nLAMMPS Workflow completed successfully. Exiting.....") diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index af2cad51..98a31fe9 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -8,6 +8,7 @@ from langchain.chat_models import BaseChatModel from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate +from langchain_core.messages import HumanMessage from langgraph.graph import END from rich.console import Console from rich.panel import Panel @@ -15,6 +16,7 @@ from rich.syntax import Syntax from .base import BaseAgent +from ursa.agents.execution_agent import ExecutionAgent working = True try: @@ -66,6 +68,7 @@ def __init__( mpirun_cmd: str = "mpirun", tiktoken_model: str = "gpt-5-mini", max_tokens: int = 200000, + summarize_results: bool = True, **kwargs, ): if not working: @@ -95,6 +98,7 @@ def __init__( self.mpirun_cmd = mpirun_cmd self.tiktoken_model = tiktoken_model self.max_tokens = max_tokens + self.summarize_results = summarize_results self.console = Console() @@ -637,6 +641,28 @@ def _fix(self, state: LammpsState) -> LammpsState: "input_script": new_input, "fix_attempts": state.get("fix_attempts", 0) + 1, } + + + def _summarize(self, state: LammpsState) -> LammpsState: + self._section("Now handing things off to execution agent for summarization/visualization") + + executor = ExecutionAgent(llm=self.llm) + + exe_plan = f""" + You are part of a larger scientific workflow whose purpose is to accomplish this task: {state["simulation_task"]} + A LAMMPS simulation has been done and the output is located in the file 'log.lammps'. + Summarize the contents of this file in a markdown document. Include a plot, if relevent. + """ + + exe_results = executor.invoke({"messages": [HumanMessage(content=exe_plan)],"workspace": self.workspace}) + + for x in exe_results["messages"]: + print(x.content) + + return state + + def _post_run(self, state: LammpsState) -> LammpsState: + return state def _build_graph(self): self.add_node(self._entry_router) @@ -648,6 +674,8 @@ def _build_graph(self): self.add_node(self._author) self.add_node(self._run_lammps) self.add_node(self._fix) + self.add_node(self._post_run) + self.add_node(self._summarize) self.graph.set_entry_point("_entry_router") @@ -705,8 +733,21 @@ def _build_graph(self): self._route_run, { "need_fix": "_fix", - "done_success": END, + "done_success": "_post_run", "done_failed": END, }, ) + self.graph.add_edge("_fix", "_run_lammps") + + self.graph.add_conditional_edges( + "_post_run", + lambda _: "summarize" if self.summarize_results else "skip", + { + "summarize": "_summarize", + "skip": END, + }, + ) + + self.graph.add_edge("_summarize", END) + From 293f7127eae8dbc70972cd36d1bfaa24ab376e8f Mon Sep 17 00:00:00 2001 From: Mike Grosskopf Date: Thu, 29 Jan 2026 13:44:02 -0700 Subject: [PATCH 27/31] Also ruff formatting --- src/ursa/agents/lammps_agent.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/ursa/agents/lammps_agent.py b/src/ursa/agents/lammps_agent.py index 98a31fe9..7ad4ba9d 100644 --- a/src/ursa/agents/lammps_agent.py +++ b/src/ursa/agents/lammps_agent.py @@ -6,18 +6,19 @@ import tiktoken from langchain.chat_models import BaseChatModel +from langchain_core.messages import HumanMessage from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate -from langchain_core.messages import HumanMessage from langgraph.graph import END from rich.console import Console from rich.panel import Panel from rich.rule import Rule from rich.syntax import Syntax -from .base import BaseAgent from ursa.agents.execution_agent import ExecutionAgent +from .base import BaseAgent + working = True try: import atomman as am @@ -641,24 +642,28 @@ def _fix(self, state: LammpsState) -> LammpsState: "input_script": new_input, "fix_attempts": state.get("fix_attempts", 0) + 1, } - def _summarize(self, state: LammpsState) -> LammpsState: - self._section("Now handing things off to execution agent for summarization/visualization") - + self._section( + "Now handing things off to execution agent for summarization/visualization" + ) + executor = ExecutionAgent(llm=self.llm) - + exe_plan = f""" You are part of a larger scientific workflow whose purpose is to accomplish this task: {state["simulation_task"]} A LAMMPS simulation has been done and the output is located in the file 'log.lammps'. Summarize the contents of this file in a markdown document. Include a plot, if relevent. """ - exe_results = executor.invoke({"messages": [HumanMessage(content=exe_plan)],"workspace": self.workspace}) + exe_results = executor.invoke({ + "messages": [HumanMessage(content=exe_plan)], + "workspace": self.workspace, + }) for x in exe_results["messages"]: print(x.content) - + return state def _post_run(self, state: LammpsState) -> LammpsState: @@ -737,7 +742,7 @@ def _build_graph(self): "done_failed": END, }, ) - + self.graph.add_edge("_fix", "_run_lammps") self.graph.add_conditional_edges( @@ -750,4 +755,3 @@ def _build_graph(self): ) self.graph.add_edge("_summarize", END) - From a3ac5d178ae9e912a7eca8643faec073be6a8558 Mon Sep 17 00:00:00 2001 From: Mike Grosskopf Date: Wed, 4 Feb 2026 16:27:09 -0700 Subject: [PATCH 28/31] Addresses Arthurs review comments and merged updates to main. --- docs/lammps_agent.md | 4 ++-- .../lammps_execute/stiffness_tensor_of_hea.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/lammps_agent.md b/docs/lammps_agent.md index f9138731..538303b1 100644 --- a/docs/lammps_agent.md +++ b/docs/lammps_agent.md @@ -14,9 +14,9 @@ The agent writes the outputs into a local `workspace` directory and uses rich co ## Dependencies -The main dependency is the [LAMMPS](https://www.lammps.org) code that needs to be installed separately. LAMMPS is a classical molecular dynamics code developed by Sandia National Laboratories. Installation instructions can be found [here](https://docs.lammps.org/Install.html). On macOS and linux systems, the simplest way to install LAMMPS is often via [Conda](https://anaconda.org/channels/conda-forge/packages/lammps/overview), in the same conda environment where `ursa` is installed. +The main dependency is the [LAMMPS](https://www.lammps.org) code that needs to be separately installed. LAMMPS is a classical molecular dynamics code developed by Sandia National Laboratories. Installation instructions can be found [here](https://docs.lammps.org/Install.html). On MacOS and Linux systems, the simplest way to install LAMMPS is often via [Conda](https://anaconda.org/channels/conda-forge/packages/lammps/overview), in the same conda environment where `ursa` is installed. -Two additional dependencies, that are not installed along with `ursa`, are `atomman` and `trafilatura`. These can be installed via `pip install atomman` and `pip install trafilatura`. +One additional dependency, that are not installed along with `ursa`, is `atomman`. This can be installed via `pip install atomman`. --- diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index b63a5861..7d7d5f77 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -23,7 +23,10 @@ with open("elastic_template.txt", "r") as file: template = file.read() -simulation_task = "Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni to determine its stiffness tensor." +simulation_task = ( + "Carry out a LAMMPS simulation of the high entropy alloy Co-Cr-Fe-Mn-Ni " + "to determine its stiffness tensor." +) elements = ["Co", "Cr", "Fe", "Mn", "Ni"] From a9416fb143f570b7c0d603603cce69c8930f00bb Mon Sep 17 00:00:00 2001 From: Mike Grosskopf Date: Wed, 4 Feb 2026 16:33:54 -0700 Subject: [PATCH 29/31] Adding the console printing as recommended as well --- examples/two_agent_examples/lammps_execute/EOS_of_Cu.py | 7 ++++++- .../lammps_execute/stiffness_tensor_of_hea.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py index a793e754..dd20577f 100644 --- a/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py +++ b/examples/two_agent_examples/lammps_execute/EOS_of_Cu.py @@ -1,7 +1,10 @@ from langchain_openai import ChatOpenAI +from rich import get_console from ursa.agents import LammpsAgent +console = get_console() + try: import atomman as am except Exception: @@ -51,4 +54,6 @@ ) if final_lammps_state.get("run_returncode") == 0: - print("\nLAMMPS Workflow completed successfully. Exiting.....") + console.print( + "\n[green]LAMMPS Workflow completed successfully.[/green] Exiting....." + ) diff --git a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py index 7d7d5f77..1cb70eac 100644 --- a/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py +++ b/examples/two_agent_examples/lammps_execute/stiffness_tensor_of_hea.py @@ -1,7 +1,10 @@ from langchain_openai import ChatOpenAI +from rich import get_console from ursa.agents import LammpsAgent +console = get_console() + model = "gpt-5" llm = ChatOpenAI(model=model, timeout=None, max_retries=2) @@ -35,4 +38,6 @@ ) if final_lammps_state.get("run_returncode") == 0: - print("\nLAMMPS Workflow completed successfully. Exiting.....") + console.print( + "\n[green]LAMMPS Workflow completed successfully.[/green] Exiting....." + ) From 48c51099c664231ac95231dbe4242c73322d4490 Mon Sep 17 00:00:00 2001 From: Arthur Lui Date: Fri, 6 Feb 2026 11:11:01 -0700 Subject: [PATCH 30/31] move lammps to optional-dependencies --- pyproject.toml | 6 +++--- uv.lock | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b00537c..81699cdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,9 @@ Issues = "https://github.com/lanl/ursa/issues" [project.optional-dependencies] fm = ["torch>=2.9.0"] # Ursa's MCP FM interface +lammps = [ + "atomman>=1.5.2", +] [build-system] requires = ["setuptools>=74.1,<80", "setuptools-git-versioning>=2.0,<3"] @@ -113,9 +116,6 @@ docs = [ "mkdocs-material>=9.6.21", "mkdocstrings-python>=1.18.2", ] -lammps = [ - "atomman>=1.5.2", -] opt = [ "ortools>=9.14,<9.15", ] diff --git a/uv.lock b/uv.lock index 2bcab590..e8118307 100644 --- a/uv.lock +++ b/uv.lock @@ -7954,6 +7954,9 @@ dependencies = [ fm = [ { name = "torch" }, ] +lammps = [ + { name = "atomman" }, +] [package.dev-dependencies] dev = [ @@ -7971,9 +7974,6 @@ docs = [ { name = "mkdocs-material" }, { name = "mkdocstrings-python" }, ] -lammps = [ - { name = "atomman" }, -] opt = [ { name = "ortools" }, ] @@ -7985,6 +7985,7 @@ otel = [ [package.metadata] requires-dist = [ { name = "arxiv", specifier = ">=2.2.0,<3.0" }, + { name = "atomman", marker = "extra == 'lammps'", specifier = ">=1.5.2" }, { name = "beautifulsoup4", specifier = ">=4.13.4,<5.0" }, { name = "ddgs", specifier = ">=9.5.5" }, { name = "fastmcp", specifier = ">=2.13.3" }, @@ -8012,7 +8013,7 @@ requires-dist = [ { name = "trafilatura", specifier = ">=1.6.1,<1.7" }, { name = "typer", specifier = ">=0.16.1" }, ] -provides-extras = ["fm"] +provides-extras = ["fm", "lammps"] [package.metadata.requires-dev] dev = [ @@ -8030,7 +8031,6 @@ docs = [ { name = "mkdocs-material", specifier = ">=9.6.21" }, { name = "mkdocstrings-python", specifier = ">=1.18.2" }, ] -lammps = [{ name = "atomman", specifier = ">=1.5.2" }] opt = [{ name = "ortools", specifier = ">=9.14,<9.15" }] otel = [ { name = "opentelemetry-exporter-otlp", specifier = ">=1.39.0" }, @@ -8801,4 +8801,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, -] \ No newline at end of file +] From b7fed3fd286c545518b50db93f277347d18a15e9 Mon Sep 17 00:00:00 2001 From: Arthur Lui Date: Fri, 6 Feb 2026 11:23:05 -0700 Subject: [PATCH 31/31] added note on installing lammps --- docs/lammps_agent.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/lammps_agent.md b/docs/lammps_agent.md index 538303b1..2a4518c4 100644 --- a/docs/lammps_agent.md +++ b/docs/lammps_agent.md @@ -14,9 +14,17 @@ The agent writes the outputs into a local `workspace` directory and uses rich co ## Dependencies -The main dependency is the [LAMMPS](https://www.lammps.org) code that needs to be separately installed. LAMMPS is a classical molecular dynamics code developed by Sandia National Laboratories. Installation instructions can be found [here](https://docs.lammps.org/Install.html). On MacOS and Linux systems, the simplest way to install LAMMPS is often via [Conda](https://anaconda.org/channels/conda-forge/packages/lammps/overview), in the same conda environment where `ursa` is installed. - -One additional dependency, that are not installed along with `ursa`, is `atomman`. This can be installed via `pip install atomman`. +The main dependency is the [LAMMPS](https://www.lammps.org) code that needs to +be separately installed. LAMMPS is a classical molecular dynamics code +developed by Sandia National Laboratories. Installation instructions can be +found [here](https://docs.lammps.org/Install.html). On MacOS and Linux systems, +the simplest way to install LAMMPS is often via +[Conda](https://anaconda.org/channels/conda-forge/packages/lammps/overview), in +the same conda environment where `ursa` is installed. + +The dependencies for `LammpsAgent` are not included with the basic `ursa` +installation, but can be installed via `pip install 'ursa[lammps]'` or `uv add +'ursa[lammps]'`. ---