Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions amorphouspy/src/amorphouspy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
from amorphouspy.lammps.md import md_simulation
from amorphouspy.lammps.potentials._config import DsfConfig, EwaldConfig, InteractionConfig, PppmConfig, WolfConfig
from amorphouspy.lammps.potentials.potential import generate_potential
from amorphouspy.pipelines.meltquench import generate_structure, run_melt_quench
from amorphouspy.pipelines.structural import extract_equilibration_frames, run_structural_analysis
from amorphouspy.pipelines.viscosity import run_viscosity_workflow
from amorphouspy.properties.cte import cte_from_fluctuations_simulation, temperature_scan_simulation
from amorphouspy.properties.cte_analysis import (
cte_from_npt_fluctuations,
Expand Down Expand Up @@ -74,12 +77,14 @@
"cte_from_volume_temperature_data",
"elastic_simulation",
"extract_composition",
"extract_equilibration_frames",
"find_rdf_minimum",
"fit_vft",
"formula_mass_g_per_mol",
"frames_from_melt_quench_result",
"generate_bond_length_dict",
"generate_potential",
"generate_structure",
"get_ase_structure",
"get_atomic_mass",
"get_composition",
Expand All @@ -93,6 +98,9 @@
"parse_formula",
"plan_system",
"plot_analysis_results_plotly",
"run_melt_quench",
"run_structural_analysis",
"run_viscosity_workflow",
"running_mean",
"structure_from_parsed_output",
"temperature_scan_simulation",
Expand Down
24 changes: 21 additions & 3 deletions amorphouspy/src/amorphouspy/pipelines/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
"""Pipelines subpackage of amorphouspy.
"""Pipeline subpackage of amorphouspy.

Provides workflow orchestration utilities built on top of executorlib
for submitting fabrication and characterization pipelines to HPC resources.
High-level workflow functions that compose the lower-level building blocks
(fabrication, LAMMPS, properties) into end-to-end simulation pipelines.

Each pipeline module accepts plain Python arguments (ASE Atoms, dicts, floats)
and returns plain dicts — no web-framework or pydantic dependency.
"""

from amorphouspy.pipelines.meltquench import generate_structure, run_melt_quench
from amorphouspy.pipelines.structural import (
extract_equilibration_frames,
run_structural_analysis,
)
from amorphouspy.pipelines.viscosity import run_viscosity_workflow

__all__ = [
"extract_equilibration_frames",
"generate_structure",
"run_melt_quench",
"run_structural_analysis",
"run_viscosity_workflow",
]
140 changes: 140 additions & 0 deletions amorphouspy/src/amorphouspy/pipelines/meltquench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Melt-quench pipeline: structure generation and glass quenching.

Combines structure generation (composition → random atoms → potential)
with the LAMMPS melt-quench simulation into a two-step pipeline.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

import numpy as np

from amorphouspy.fabrication.geometry import get_ase_structure, get_structure_dict

if TYPE_CHECKING:
import pandas as pd
from ase import Atoms

from amorphouspy.lammps.potentials._config import InteractionConfig
from amorphouspy.fabrication.meltquench import melt_quench_simulation
from amorphouspy.fabrication.meltquench_protocols import DEFAULT_MELT_TEMPERATURES
from amorphouspy.lammps.potentials.potential import generate_potential

logger = logging.getLogger(__name__)


def generate_structure(
composition: dict[str, float],
n_atoms: int = 6000,
potential_type: str = "pmmcs",
density: float | None = None,
structure_seed: int = 42,
electrostatics_config: InteractionConfig | None = None,
) -> dict[str, Any]:
"""Generate an initial random structure and matching LAMMPS potential.

Args:
composition: Oxide composition as ``{oxide: mol%}``.
n_atoms: Target number of atoms.
potential_type: Name of the interatomic potential to generate.
density: Target density in g/cm³; ``None`` uses the Fluegel model.
structure_seed: Random seed for atom placement.
electrostatics_config: Electrostatics configuration (e.g. ``DsfConfig``).

Returns:
Dict with ``atoms_dict``, ``structure`` (ASE Atoms), and ``potential``.
"""
atoms_dict = get_structure_dict(
composition=composition,
target_atoms=n_atoms,
density=density,
random_seed=structure_seed,
)
structure = get_ase_structure(atoms_dict=atoms_dict)
potential = generate_potential(
atoms_dict=atoms_dict,
potential_type=potential_type,
melt=True,
electrostatics=electrostatics_config,
)

return {
"atoms_dict": atoms_dict,
"structure": structure,
"potential": potential,
}


def run_melt_quench(
structure: Atoms,
potential: pd.DataFrame,
*,
potential_type: str = "pmmcs",
heating_rate: float = 1e14,
cooling_rate: float = 1e12,
timestep: float = 1.0,
temperature_high: float | None = None,
temperature_low: float = 300.0,
equilibration_steps: int | None = None,
n_print: int = 100_000,
n_averaging_frames: int = 100,
server_kwargs: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Run a LAMMPS melt-quench simulation.

Args:
structure: Initial ASE Atoms object.
potential: LAMMPS potential object from ``generate_potential``.
potential_type: Potential name, used for default ``temperature_high`` lookup.
heating_rate: Heating rate in K/ps.
cooling_rate: Cooling rate in K/ps.
timestep: MD timestep in fs.
temperature_high: Melt temperature in K; ``None`` uses the protocol default.
temperature_low: Final quench temperature in K.
equilibration_steps: Override for equilibration step count; ``None`` = protocol default.
n_print: Thermodynamic output frequency (steps).
n_averaging_frames: Number of trajectory frames to keep for structural averaging.
server_kwargs: LAMMPS server/resource configuration.

Returns:
Dict with ``final_structure``, ``mean_temperature``, ``simulation_steps``,
``composition``, trajectory data, and simulation parameters.
"""
if server_kwargs is None:
server_kwargs = {}

if temperature_high is None:
temperature_high = DEFAULT_MELT_TEMPERATURES.get(potential_type, 5000.0)

mq = melt_quench_simulation(
structure=structure,
potential=potential,
n_print=n_print,
heating_rate=int(heating_rate),
cooling_rate=int(cooling_rate),
timestep=timestep,
temperature_high=temperature_high,
temperature_low=temperature_low,
equilibration_steps=equilibration_steps,
langevin=False,
server_kwargs=server_kwargs,
)

last_stage = next((s for s in reversed(mq["result"]) if s is not None), {})

return {
"final_structure": mq["structure"],
"mean_temperature": float(np.mean(last_stage["temperature"])),
"simulation_steps": len(last_stage["steps"]),
"temperature_trajectory": [float(t) for t in last_stage["temperature"]],
"steps_trajectory": [int(s) for s in last_stage["steps"]],
"simulation_history": mq["result"],
"timestep": timestep,
"cooling_rate": int(cooling_rate),
"heating_rate": int(heating_rate),
"temperature_high": temperature_high,
"temperature_low": temperature_low,
"n_averaging_frames": n_averaging_frames,
}
86 changes: 86 additions & 0 deletions amorphouspy/src/amorphouspy/pipelines/structural.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Structural analysis pipeline.

Runs structural characterisation (RDF, coordination, bond angles, etc.)
on a quenched glass structure, optionally averaging over trajectory frames
from the final equilibration stage.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from amorphouspy.properties.structural.all import StructureData, analyze_structure

if TYPE_CHECKING:
from ase import Atoms

logger = logging.getLogger(__name__)


def extract_equilibration_frames(
final_structure: Atoms,
simulation_history: list[dict[str, Any]] | None = None,
) -> list[Atoms]:
"""Reconstruct Atoms snapshots from the final equilibration stage.

Falls back to a single-element list with *final_structure* when no
simulation history is available or the history contains no position data.

Args:
final_structure: The quenched structure from the melt-quench pipeline.
simulation_history: Full stage-by-stage MD history (optional).

Returns:
List of ASE Atoms frames suitable for averaging.
"""
if not simulation_history:
return [final_structure]

last_stage = next((s for s in reversed(simulation_history) if s is not None), None)
if last_stage is None or "positions" not in last_stage:
return [final_structure]

positions = last_stage["positions"]
cells = last_stage["cells"]
n_frames = len(positions)

if n_frames <= 1:
return [final_structure]

frames: list[Atoms] = []
for i in range(n_frames):
frame = final_structure.copy()
frame.set_positions(positions[i])
frame.set_cell(cells[i])
frame.set_pbc(True)
frame.wrap()
frames.append(frame)

return frames


def run_structural_analysis(
final_structure: Atoms,
simulation_history: list[dict[str, Any]] | None = None,
) -> tuple[StructureData, StructureData | None, int]:
"""Run structural analysis, optionally averaging over trajectory frames.

Args:
final_structure: Quenched ASE Atoms object.
simulation_history: Full stage-by-stage MD history for frame averaging.

Returns:
Tuple of ``(mean_data, sem_data, n_frames)`` where *sem_data* is
``None`` when only one frame is used.
"""
frames = extract_equilibration_frames(final_structure, simulation_history)
n_frames = len(frames)

if n_frames > 1:
logger.info("Frame-averaging structural analysis over %d frames", n_frames)
mean_data, sem_data = analyze_structure(atoms=frames, frame_averaging=True)
else:
mean_data, sem_data = analyze_structure(atoms=frames[0])

return mean_data, sem_data, n_frames
Loading
Loading