Skip to content

Commit 872e464

Browse files
committed
feat: parallelize viscosity calculation
Run 3 different temperatures in parallel.
1 parent 230cb0f commit 872e464

2 files changed

Lines changed: 135 additions & 65 deletions

File tree

amorphouspy/src/amorphouspy/pipelines/viscosity.py

Lines changed: 119 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Multi-temperature viscosity pipeline.
22
33
Sequentially cools a quenched glass to each target temperature via
4-
melt-quench and runs a Green-Kubo viscosity production simulation
5-
at each one.
4+
melt-quench, then runs Green-Kubo viscosity production simulations
5+
in parallel using an executor.
66
"""
77

88
from __future__ import annotations
@@ -14,14 +14,86 @@
1414
from amorphouspy.fabrication.meltquench import melt_quench_simulation
1515

1616
if TYPE_CHECKING:
17+
from concurrent.futures import Executor
18+
1719
import pandas as pd
1820
from ase import Atoms
1921
from amorphouspy.properties.viscosity import get_viscosity, viscosity_simulation
2022

2123
logger = logging.getLogger(__name__)
2224

2325

26+
# ---------------------------------------------------------------------------
27+
# Helpers submitted to the executor
28+
# ---------------------------------------------------------------------------
29+
30+
31+
def _cool_and_run_viscosity(
32+
structure: Atoms,
33+
potential: pd.DataFrame,
34+
temp_high: float,
35+
temp_low: float,
36+
heating_rate: float,
37+
cooling_rate: float,
38+
timestep: float,
39+
n_timesteps: int,
40+
n_print: int,
41+
max_lag: int | None,
42+
server_kwargs: dict[str, Any],
43+
) -> dict[str, Any]:
44+
"""Cool to *temp_low* via melt-quench, then run a viscosity simulation.
45+
46+
This function is submitted to the executor as a single unit of work
47+
so that the melt-quench and the production run share the same worker.
48+
"""
49+
mq_result = melt_quench_simulation(
50+
structure=structure,
51+
potential=potential,
52+
temperature_high=float(temp_high),
53+
temperature_low=float(temp_low),
54+
timestep=1.0,
55+
heating_rate=float(heating_rate),
56+
cooling_rate=float(cooling_rate),
57+
n_print=1000,
58+
langevin=False,
59+
server_kwargs=server_kwargs,
60+
)
61+
cooled_structure = mq_result["structure"]
62+
63+
visc_result = viscosity_simulation(
64+
structure=cooled_structure,
65+
potential=potential,
66+
temperature_sim=float(temp_low),
67+
timestep=float(timestep),
68+
initial_production_steps=int(n_timesteps),
69+
n_print=int(n_print),
70+
langevin=False,
71+
seed=12345,
72+
server_kwargs=server_kwargs,
73+
)
74+
75+
visc_data = get_viscosity(visc_result, timestep=float(timestep), max_lag=max_lag)
76+
77+
raw_lag = cast("list[float]", visc_data.get("lag_time_ps", []))
78+
raw_visc = cast("list[float]", visc_data.get("viscosity_integral", []))
79+
80+
return {
81+
"temperature": float(temp_low),
82+
"viscosity": float(visc_data["viscosity"]),
83+
"max_lag": float(visc_data["max_lag"]),
84+
"simulation_steps": int(n_timesteps),
85+
"lag_times_ps": downsample_log(raw_lag),
86+
"viscosity_integral": downsample_log(raw_visc),
87+
}
88+
89+
90+
# ---------------------------------------------------------------------------
91+
# Public API
92+
# ---------------------------------------------------------------------------
93+
94+
2495
def run_viscosity_workflow(
96+
executor: Executor,
2597
structure: Atoms,
2698
potential: pd.DataFrame,
2799
temperatures: list[float],
@@ -32,15 +104,19 @@ def run_viscosity_workflow(
32104
n_print: int = 1,
33105
max_lag: int | None = 1_000_000,
34106
server_kwargs: dict[str, Any] | None = None,
107+
resource_dict: dict[str, Any] | None = None,
35108
) -> dict[str, Any]:
36-
"""Run viscosity analysis at multiple temperatures.
109+
"""Run viscosity analysis at multiple temperatures using an executor.
37110
38-
Starting from the quenched structure, the workflow sequentially cools
39-
from the highest to the lowest requested temperature. At each step a
40-
melt-quench brings the structure to the target temperature, followed by
41-
a Green-Kubo viscosity production run.
111+
Each temperature is submitted as an independent task to *executor*.
112+
Every task first cools the starting structure to the target temperature
113+
via a melt-quench, then performs a Green-Kubo viscosity production run.
114+
Because each task starts from the same *structure* and cools
115+
independently, the tasks can run in parallel.
42116
43117
Args:
118+
executor: A ``concurrent.futures.Executor`` or executorlib executor
119+
used to submit per-temperature tasks.
44120
structure: Quenched ASE Atoms from the melt-quench pipeline.
45121
potential: LAMMPS potential object.
46122
temperatures: Target temperatures (K) for viscosity runs.
@@ -51,67 +127,57 @@ def run_viscosity_workflow(
51127
n_print: Thermodynamic output frequency.
52128
max_lag: Maximum correlation lag (steps) for Green-Kubo.
53129
server_kwargs: LAMMPS server/resource configuration.
130+
resource_dict: Optional resource dict forwarded to executorlib
131+
executors (e.g. ``{"cores": 8}``). Ignored for stdlib executors.
54132
55133
Returns:
56134
Result dict with ``temperatures``, ``viscosities``, ``max_lag``,
57135
``simulation_steps``, ``lag_times_ps``, and ``viscosity_integral``.
58136
"""
59137
if server_kwargs is None:
60138
server_kwargs = {}
139+
if resource_dict is None:
140+
resource_dict = {}
61141

62142
sorted_temps = sorted(temperatures, reverse=True)
63-
logger.info("Viscosity temperatures (high→low): %s", sorted_temps)
64-
143+
logger.info("Submitting viscosity tasks for temperatures: %s", sorted_temps)
144+
145+
# Submit all temperatures in parallel — each cools independently
146+
# from the starting structure down to its target temperature.
147+
futures = {}
148+
for temp in sorted_temps:
149+
submit_kwargs: dict[str, Any] = {
150+
"structure": structure,
151+
"potential": potential,
152+
"temp_high": 5000.0,
153+
"temp_low": temp,
154+
"heating_rate": heating_rate,
155+
"cooling_rate": cooling_rate,
156+
"timestep": timestep,
157+
"n_timesteps": n_timesteps,
158+
"n_print": n_print,
159+
"max_lag": max_lag,
160+
"server_kwargs": server_kwargs,
161+
}
162+
if resource_dict:
163+
submit_kwargs["resource_dict"] = resource_dict
164+
futures[temp] = executor.submit(_cool_and_run_viscosity, **submit_kwargs)
165+
166+
# Collect results in temperature order
65167
viscosities: list[float] = []
66168
all_max_lags: list[float] = []
67169
sim_steps: list[int] = []
68170
lag_times_ps: list[list[float]] = []
69171
viscosity_integral: list[list[float]] = []
70172

71-
structure_current = structure
72-
73-
for idx, temp in enumerate(sorted_temps):
74-
temp_high = 5000.0 if idx == 0 else sorted_temps[idx - 1]
75-
76-
logger.info("Cooling from %.1f K to %.1f K", temp_high, temp)
77-
mq_result = melt_quench_simulation(
78-
structure=structure_current,
79-
potential=potential,
80-
temperature_high=float(temp_high),
81-
temperature_low=float(temp),
82-
timestep=1.0,
83-
heating_rate=float(heating_rate),
84-
cooling_rate=float(cooling_rate),
85-
n_print=1000,
86-
langevin=False,
87-
server_kwargs=server_kwargs,
88-
)
89-
structure_current = mq_result["structure"]
90-
logger.info("Cooled to %.1f K, %d atoms", temp, len(structure_current))
91-
92-
logger.info("Running viscosity simulation at %.1f K", temp)
93-
visc_result = viscosity_simulation(
94-
structure=structure_current,
95-
potential=potential,
96-
temperature_sim=float(temp),
97-
timestep=float(timestep),
98-
initial_production_steps=int(n_timesteps),
99-
n_print=int(n_print),
100-
langevin=False,
101-
seed=12345,
102-
server_kwargs=server_kwargs,
103-
)
104-
105-
visc_data = get_viscosity(visc_result, timestep=float(timestep), max_lag=max_lag)
106-
logger.info("Viscosity at %.1f K: %.3e Pa·s", temp, visc_data["viscosity"])
107-
108-
viscosities.append(float(visc_data["viscosity"]))
109-
all_max_lags.append(float(visc_data["max_lag"]))
110-
sim_steps.append(int(n_timesteps))
111-
raw_lag_data = cast("list[float]", visc_data.get("lag_time_ps", []))
112-
raw_visc_data = cast("list[float]", visc_data.get("viscosity_integral", []))
113-
lag_times_ps.append(downsample_log(raw_lag_data))
114-
viscosity_integral.append(downsample_log(raw_visc_data))
173+
for temp in sorted_temps:
174+
result = futures[temp].result()
175+
logger.info("Viscosity at %.1f K: %.3e Pa·s", temp, result["viscosity"])
176+
viscosities.append(result["viscosity"])
177+
all_max_lags.append(result["max_lag"])
178+
sim_steps.append(result["simulation_steps"])
179+
lag_times_ps.append(result["lag_times_ps"])
180+
viscosity_integral.append(result["viscosity_integral"])
115181

116182
return {
117183
"temperatures": sorted_temps,

amorphouspy_api/src/amorphouspy_api/pipeline.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,22 +88,26 @@ def _run_structural_analysis(submission: JobSubmission, config: StructureAnalysi
8888

8989
def _run_viscosity(submission: JobSubmission, config: ViscosityAnalysis, result: dict) -> dict:
9090
"""Multi-temperature viscosity analysis on the quenched glass."""
91+
from concurrent.futures import ProcessPoolExecutor
92+
9193
from amorphouspy.pipelines.viscosity import run_viscosity_workflow
9294

9395
from amorphouspy_api.executor import get_lammps_server_kwargs
9496

95-
return run_viscosity_workflow(
96-
structure=result["melt_quench"]["final_structure"],
97-
potential=result["structure_generation"]["potential"],
98-
temperatures=config.temperatures,
99-
heating_rate=int(submission.simulation.quench_rate * 100),
100-
cooling_rate=int(submission.simulation.quench_rate),
101-
timestep=config.timestep,
102-
n_timesteps=config.n_timesteps,
103-
n_print=config.n_print,
104-
max_lag=config.max_lag,
105-
server_kwargs=get_lammps_server_kwargs(),
106-
)
97+
with ProcessPoolExecutor() as exe:
98+
return run_viscosity_workflow(
99+
executor=exe,
100+
structure=result["melt_quench"]["final_structure"],
101+
potential=result["structure_generation"]["potential"],
102+
temperatures=config.temperatures,
103+
heating_rate=int(submission.simulation.quench_rate * 100),
104+
cooling_rate=int(submission.simulation.quench_rate),
105+
timestep=config.timestep,
106+
n_timesteps=config.n_timesteps,
107+
n_print=config.n_print,
108+
max_lag=config.max_lag,
109+
server_kwargs=get_lammps_server_kwargs(),
110+
)
107111

108112

109113
def _run_cte(submission: JobSubmission, config: CTEFluctuations | CTETemperatureScan, result: dict) -> dict:

0 commit comments

Comments
 (0)