| from mace.calculators import mace_mp |
| from ase.io import read, write |
| from ase.io.trajectory import Trajectory |
| from ase.optimize import FIRE, LBFGS |
| from matplotlib.patches import Patch |
| import numpy as np |
| import matplotlib.pyplot as plt |
| import matplotlib.ticker as ticker |
| import time |
|
|
| |
| atoms = read('POSCAR', format='vasp') |
| atoms.set_pbc(True) |
| atoms.set_positions(atoms.get_positions().astype(np.float64)) |
|
|
| |
| atoms.calc = mace_mp( |
| model="6_MACE.model", |
| default_dtype="float64", |
| device="cuda:1", |
| ) |
|
|
| |
| print("Initial energy:", atoms.get_potential_energy(), "eV") |
| print("Initial max force:", np.max(np.abs(atoms.get_forces())), "eV/Å") |
|
|
| |
| steps = [] |
| energies = [] |
| fmax_vals = [] |
| stages = [] |
| timestamps = [] |
|
|
| t_run_start = time.perf_counter() |
|
|
| class EnergyLogger: |
| def __init__(self, atoms, stage_label): |
| self.atoms = atoms |
| self.stage_label = stage_label |
| self.step = 0 |
|
|
| def __call__(self): |
| e = self.atoms.get_potential_energy() |
| f = np.max(np.abs(self.atoms.get_forces())) |
| steps.append(len(steps)) |
| energies.append(e) |
| fmax_vals.append(f) |
| stages.append(self.stage_label) |
| timestamps.append(time.perf_counter() - t_run_start) |
| self.step += 1 |
|
|
| class UnifiedLogger: |
| def __init__(self, atoms, stage_label, logfile): |
| self.atoms = atoms |
| self.stage_label = stage_label |
| self.logfile = logfile |
| self.step = 0 |
| self.t_stage_start = None |
|
|
| def __call__(self): |
| if self.t_stage_start is None: |
| self.t_stage_start = time.perf_counter() |
|
|
| e = self.atoms.get_potential_energy() |
| f = np.max(np.abs(self.atoms.get_forces())) |
| elapsed = time.perf_counter() - self.t_stage_start |
|
|
| with open(self.logfile, 'a') as fh: |
| if self.step == 0: |
| fh.write(f"\n# --- {self.stage_label} ---\n") |
| fh.write(f"{'Step':>6} {'Energy (eV)':>14} {'fmax (eV/Å)':>12} {'Time (s)':>10}\n") |
| fh.write(f"{self.step:>6} {e:>14.6f} {f:>12.6f} {elapsed:>10.2f}\n") |
| self.step += 1 |
|
|
| LOG_FILE = 'relax_MC.log' |
| TRAJ_FILE = 'relax_MC.traj' |
|
|
| open(LOG_FILE, 'w').close() |
| traj = Trajectory(TRAJ_FILE, mode='w', atoms=atoms) |
|
|
| |
| |
| |
| fire_energy_logger = EnergyLogger(atoms, stage_label="FIRE") |
| fire_unified_logger = UnifiedLogger(atoms, stage_label="FIRE", logfile=LOG_FILE) |
|
|
| fire = FIRE(atoms, logfile=None, dt=0.05, maxstep=0.05, dtmax=0.5, Nmin=10, finc=1.05, fdec=0.5) |
| fire.attach(fire_energy_logger, interval=1) |
| fire.attach(fire_unified_logger, interval=1) |
| fire.attach(traj.write, interval=1) |
|
|
| t_fire_start = time.perf_counter() |
| fire.run(fmax=0.1) |
| t_fire = time.perf_counter() - t_fire_start |
|
|
| nsteps_fire = fire.get_number_of_steps() |
| print(f"✓ FIRE done : {nsteps_fire} steps | " |
| f"fmax={np.max(np.abs(atoms.get_forces())):.4f} eV/Å | " |
| f"time={t_fire:.1f}s ({t_fire/60:.2f} min)") |
|
|
| |
| |
| |
| lbfgs_energy_logger = EnergyLogger(atoms, stage_label="LBFGS") |
| lbfgs_unified_logger = UnifiedLogger(atoms, stage_label="LBFGS", logfile=LOG_FILE) |
|
|
| lbfgs = LBFGS(atoms, logfile=None, maxstep=0.05, memory=100) |
| lbfgs.attach(lbfgs_energy_logger, interval=1) |
| lbfgs.attach(lbfgs_unified_logger, interval=1) |
| lbfgs.attach(traj.write, interval=1) |
|
|
| t_lbfgs_start = time.perf_counter() |
| lbfgs.run(fmax=0.01) |
| t_lbfgs = time.perf_counter() - t_lbfgs_start |
|
|
| traj.close() |
|
|
| nsteps_lbfgs = lbfgs.get_number_of_steps() |
| print(f"✓ LBFGS done: {nsteps_lbfgs} steps | " |
| f"fmax={np.max(np.abs(atoms.get_forces())):.4f} eV/Å | " |
| f"time={t_lbfgs:.1f}s ({t_lbfgs/60:.2f} min)") |
|
|
| t_total = t_fire + t_lbfgs |
| final_energy = atoms.get_potential_energy() |
|
|
| |
| stages_arr = np.array(stages) |
| times_arr = np.array(timestamps) |
| steps_arr = np.array(steps) |
| fire_mask = stages_arr == "FIRE" |
| lbfgs_mask = stages_arr == "LBFGS" |
|
|
| x_fire = times_arr[fire_mask] |
| x_lbfgs = times_arr[lbfgs_mask] |
| s_fire = steps_arr[fire_mask] |
| s_lbfgs = steps_arr[lbfgs_mask] |
|
|
| |
| dt_fire = np.diff(x_fire, prepend=x_fire[0]) if len(x_fire) > 0 else np.array([]) |
| dt_lbfgs = np.diff(x_lbfgs, prepend=x_lbfgs[0]) if len(x_lbfgs) > 0 else np.array([]) |
|
|
| avg_fire = dt_fire.mean() if len(dt_fire) > 0 else 0.0 |
| avg_lbfgs = dt_lbfgs.mean() if len(dt_lbfgs) > 0 else 0.0 |
|
|
| transition_step = s_lbfgs[0] if any(lbfgs_mask) else None |
|
|
| |
| all_steps_arr = np.concatenate([s_fire, s_lbfgs]) |
| all_dt = np.concatenate([dt_fire, dt_lbfgs]) |
| all_colors = ['#E07B39'] * len(s_fire) + ['#1D9E75'] * len(s_lbfgs) |
| avg_total = all_dt.mean() if len(all_dt) > 0 else 0.0 |
|
|
| |
| def fmt_time(seconds): |
| s = int(seconds) |
| h, rem = divmod(s, 3600) |
| m, sec = divmod(rem, 60) |
| if h > 0: |
| return f"{h}h {m}m {sec}s" |
| elif m > 0: |
| return f"{m}m {sec}s" |
| else: |
| return f"{sec}s" |
|
|
| |
| print(f"\n✓ Done! Final energy : {final_energy:.4f} eV") |
| print(f"Total steps : {nsteps_fire + nsteps_lbfgs} (FIRE={nsteps_fire}, LBFGS={nsteps_lbfgs})") |
| print(f"Final fmax : {np.max(np.abs(atoms.get_forces())):.4f} eV/Å") |
| print(f"FIRE wall time : {fmt_time(t_fire)} avg {avg_fire:.2f}s/step " |
| f"min {dt_fire.min():.2f}s max {dt_fire.max():.2f}s") |
| print(f"LBFGS wall time : {fmt_time(t_lbfgs)} avg {avg_lbfgs:.2f}s/step " |
| f"min {dt_lbfgs.min():.2f}s max {dt_lbfgs.max():.2f}s") |
| print(f"Total wall time : {fmt_time(t_total)}") |
|
|
| |
| write('POSCAR-relaxed', atoms, format='vasp', vasp5=True, sort=True) |
| write('CONTCAR-relaxed', atoms, format='vasp', direct=True) |
|
|
| |
| with open(LOG_FILE, 'a') as fh: |
| fh.write(f"\n# --- Timing Summary ---\n") |
| fh.write(f"{'Stage':<8} {'Steps':>6} {'Total':>12} {'Avg (s/step)':>13} " |
| f"{'Min (s)':>8} {'Max (s)':>8}\n") |
| fh.write(f"{'FIRE':<8} {nsteps_fire:>6} {fmt_time(t_fire):>12} {avg_fire:>13.3f} " |
| f"{dt_fire.min():>8.3f} {dt_fire.max():>8.3f}\n") |
| fh.write(f"{'LBFGS':<8} {nsteps_lbfgs:>6} {fmt_time(t_lbfgs):>12} {avg_lbfgs:>13.3f} " |
| f"{dt_lbfgs.min():>8.3f} {dt_lbfgs.max():>8.3f}\n") |
| fh.write(f"{'Total':<8} {nsteps_fire+nsteps_lbfgs:>6} {fmt_time(t_total):>12} " |
| f"{'—':>13} {'—':>8} {'—':>8}\n") |
|
|
| |
| fig, axes = plt.subplots(3, 1, figsize=(8, 8), sharex=True) |
| fig.suptitle('MACE Relaxation — FIRE → LBFGS', fontsize=13, fontweight='normal') |
| ax1, ax2, ax3 = axes |
|
|
| |
| ax1.plot(s_fire, np.array(energies)[fire_mask], |
| color='#E07B39', linewidth=1.5, marker='o', markersize=2.5, |
| markeredgewidth=0, label='FIRE') |
| ax1.plot(s_lbfgs, np.array(energies)[lbfgs_mask], |
| color='#1D9E75', linewidth=1.5, marker='o', markersize=2.5, |
| markeredgewidth=0, label='LBFGS') |
| ax1.axhline(y=final_energy, color='#D85A30', linestyle='--', |
| linewidth=1, label=f'Final: {final_energy:.4f} eV') |
| if transition_step is not None: |
| ax1.axvline(x=transition_step, color='gray', linestyle=':', linewidth=1, label='FIRE→LBFGS') |
| ax1.set_ylabel('Energy (eV)', fontsize=11) |
| ax1.legend(fontsize=9, framealpha=0.7) |
| ax1.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.3f')) |
| ax1.grid(True, alpha=0.3, linestyle='--') |
| ax1.tick_params(labelsize=9) |
|
|
| |
| ax2.plot(s_fire, np.array(fmax_vals)[fire_mask], |
| color='#E07B39', linewidth=1.5, marker='o', markersize=2.5, |
| markeredgewidth=0, label='FIRE') |
| ax2.plot(s_lbfgs, np.array(fmax_vals)[lbfgs_mask], |
| color='#1D9E75', linewidth=1.5, marker='o', markersize=2.5, |
| markeredgewidth=0, label='LBFGS') |
| ax2.axhline(y=0.01, color='#D85A30', linestyle='--', |
| linewidth=1, label='Convergence (0.01 eV/Å)') |
| ax2.axhline(y=0.1, color='gray', linestyle=':', |
| linewidth=1, label='FIRE target (0.1 eV/Å)') |
| if transition_step is not None: |
| ax2.axvline(x=transition_step, color='gray', linestyle=':', linewidth=1) |
| ax2.set_ylabel('Max Force (eV/Å)', fontsize=11) |
| ax2.legend(fontsize=9, framealpha=0.7) |
| ax2.set_yscale('log') |
| ax2.grid(True, alpha=0.3, linestyle='--') |
| ax2.tick_params(labelsize=9) |
|
|
| |
| ax3.bar(all_steps_arr, all_dt, color=all_colors, alpha=0.75, width=0.6) |
| ax3.axhline(y=avg_total, color='#D85A30', linestyle='--', linewidth=1, |
| label=f'Avg: {avg_total:.3f}s | Total: {fmt_time(t_total)}') |
| if transition_step is not None: |
| ax3.axvline(x=transition_step, color='gray', linestyle=':', linewidth=1) |
|
|
| legend_handles = [ |
| plt.Line2D([0], [0], color='#D85A30', linestyle='--', |
| linewidth=1, label=f'Avg: {avg_total:.3f}s | Total: {fmt_time(t_total)}'), |
| ] |
| ax3.legend(handles=legend_handles, fontsize=9, framealpha=0.7) |
| ax3.set_ylabel('Wall Time (s)', fontsize=11) |
| ax3.set_xlabel('Ionic Step', fontsize=11) |
| ax3.grid(True, alpha=0.3, linestyle='--', axis='y') |
| ax3.tick_params(labelsize=9) |
|
|
| plt.tight_layout() |
| plt.savefig('MC_relaxation_plot_FIRE_LBFGS.png', dpi=150, bbox_inches='tight') |
| plt.show() |
| print("Plot saved as MC_relaxation_plot_FIRE_LBFGS.png") |
|
|