@@ -1040,12 +1040,7 @@ def _reversible_scaling_forward(self, iteration=1):
10401040 accumulator = None
10411041 pretrain_dump = None
10421042 if self .calc .structural_monitoring .enabled :
1043- if not STRUCTID_AVAILABLE :
1044- self .logger .warning (
1045- "Structural monitoring requested but structid is not available. "
1046- "Install structid to enable this feature."
1047- )
1048- else :
1043+ if STRUCTID_AVAILABLE :
10491044 self .logger .info (
10501045 "Initializing structural monitoring with FrameAccumulator"
10511046 )
@@ -1068,7 +1063,7 @@ def _reversible_scaling_forward(self, iteration=1):
10681063 )
10691064
10701065 # Set up dump for equilibration trajectory to pre-train
1071- pretrain_dump = sm .setup_pretraining_dump (
1066+ pretrain_dump , pretrain_dump_id = sm .setup_pretraining_dump (
10721067 lmp = lmp , simfolder = self .simfolder , dump_interval = 100
10731068 )
10741069 self .logger .info (
@@ -1081,15 +1076,12 @@ def _reversible_scaling_forward(self, iteration=1):
10811076
10821077 # Pre-train FrameAccumulator on equilibration trajectory
10831078 if accumulator is not None and pretrain_dump is not None :
1084- lmp .command ("undump _pretrain " )
1079+ lmp .command (f "undump { pretrain_dump_id } " )
10851080
10861081 success = sm .pretrain_accumulator (
10871082 accumulator = accumulator , pretrain_dump = pretrain_dump , logger = self .logger
10881083 )
10891084
1090- if not success :
1091- accumulator = None
1092-
10931085 # Clean up trajectory file
10941086 if os .path .exists (pretrain_dump ):
10951087 os .remove (pretrain_dump )
@@ -1293,12 +1285,10 @@ def _reversible_scaling_forward(self, iteration=1):
12931285
12941286 if stats ["is_flagged" ]:
12951287 self .logger .warning (
1296- f"⚠️ STRUCTURAL CHANGE DETECTED at step { current_step } (λ={ lambda_value :.4f} , T={ apparent_temp :.2f} K) "
1288+ f"STRUCTURAL CHANGE DETECTED at step { current_step } (λ={ lambda_value :.4f} , T={ apparent_temp :.2f} K) "
12971289 f"during forward sweep (iteration { iteration } )"
12981290 )
1299- self .logger .warning (
1300- "The system structure has changed significantly from the equilibrium state."
1301- )
1291+
13021292 except Exception as e :
13031293 self .logger .warning (
13041294 f"Failed to monitor structure at block { block_idx } : { e } "
@@ -1496,11 +1486,48 @@ def _reversible_scaling_backward(self, iteration=1):
14961486 lmp .command ("thermo_style custom step pe c_tcm press vol" )
14971487 lmp .command ("thermo 10000" )
14981488
1489+ # ---------------------------
1490+ # Initialize FrameAccumulator if structural monitoring is enabled
1491+ accumulator = None
1492+ pretrain_dump = None
1493+ if self .calc .structural_monitoring .enabled :
1494+
1495+ # Initialize accumulator using structural_monitoring module
1496+ accumulator = sm .initialize_accumulator (
1497+ l_values = self .calc .structural_monitoring .l_values ,
1498+ cutoff = self .calc .structural_monitoring .cutoff ,
1499+ detector_type = "cumulative" ,
1500+ threshold = 3 ,
1501+ )
1502+
1503+ # Set up dump for equilibration trajectory to pre-train
1504+ pretrain_dump , pretrain_dump_id = sm .setup_pretraining_dump (
1505+ lmp = lmp , simfolder = self .simfolder , dump_interval = 100
1506+ )
1507+
1508+ self .logger .info (
1509+ "Dumping equilibration trajectory every 100 steps for pre-training"
1510+ )
1511+
1512+ # ---------------------------
1513+
14991514 # middle equilibration
15001515 self .logger .info (f"Starting middle equilibration: { iteration } " )
15011516 lmp .command ("run %d" % self .calc .n_equilibration_steps )
15021517 self .logger .info (f"Finished middle equilibration: { iteration } " )
15031518
1519+ # Pre-train FrameAccumulator on equilibration trajectory
1520+ if accumulator is not None and pretrain_dump is not None :
1521+ lmp .command (f"undump { pretrain_dump_id } " )
1522+
1523+ success = sm .pretrain_accumulator (
1524+ accumulator = accumulator , pretrain_dump = pretrain_dump , logger = self .logger
1525+ )
1526+
1527+ # Clean up trajectory file
1528+ if os .path .exists (pretrain_dump ):
1529+ os .remove (pretrain_dump )
1530+
15041531 # check melting or freezing
15051532 if not self .calc .script_mode :
15061533 self .dump_current_snapshot (lmp , "traj.temp.dat" )
@@ -1608,7 +1635,184 @@ def _reversible_scaling_backward(self, iteration=1):
16081635 lmp .command ("reset_timestep 0" )
16091636
16101637 self .logger .info (f"Started backward sweep: { iteration } " )
1611- lmp .command ("run %d" % self .calc ._n_sweep_steps )
1638+ # Run backward sweep with optional structural monitoring
1639+ if accumulator is not None and self .calc .structural_monitoring .enabled :
1640+ temperature_interval = self .calc .structural_monitoring .temperature_interval
1641+
1642+ # Define total range for ramp() - same for all run segments
1643+ total_steps = self .calc ._n_sweep_steps
1644+
1645+ # Calculate temperature checkpoints - handle both increasing and decreasing temperature
1646+ temp_checkpoints = []
1647+
1648+ if t0 > tf :
1649+ # Temperature decreasing (cooling)
1650+ current_temp = t0
1651+ while current_temp >= tf :
1652+ temp_checkpoints .append (current_temp )
1653+ current_temp -= temperature_interval
1654+ if temp_checkpoints [- 1 ] != tf :
1655+ temp_checkpoints .append (tf )
1656+ else :
1657+ # Temperature increasing (heating)
1658+ current_temp = t0
1659+ while current_temp <= tf :
1660+ temp_checkpoints .append (current_temp )
1661+ current_temp += temperature_interval
1662+ if temp_checkpoints [- 1 ] != tf :
1663+ temp_checkpoints .append (tf )
1664+
1665+ # Convert temperature checkpoints to lambda values and then to step numbers
1666+ checkpoint_data = []
1667+ for temp in temp_checkpoints :
1668+ lambda_val = t0 / temp
1669+ step = int ((lambda_val - li ) * total_steps / (lf - li ))
1670+ step = max (0 , min (step , total_steps ))
1671+ checkpoint_data .append (
1672+ {"temp" : temp , "lambda" : lambda_val , "step" : step }
1673+ )
1674+
1675+ self .logger .info (
1676+ f"Temperature-based monitoring (backward): { len (checkpoint_data )- 1 } blocks"
1677+ )
1678+ for i , cp in enumerate (checkpoint_data ):
1679+ self .logger .info (
1680+ f" Checkpoint { i } : T={ cp ['temp' ]:.2f} K, λ={ cp ['lambda' ]:.4f} , step={ cp ['step' ]} "
1681+ )
1682+
1683+ # Open file to save monitoring data
1684+ monitor_file = os .path .join (
1685+ self .simfolder , f"structural_monitoring_backward_{ iteration } .dat"
1686+ )
1687+ monitor_out = open (monitor_file , "w" )
1688+ monitor_out .write (
1689+ "# block_idx step lambda temperature is_flagged distance mean std\n "
1690+ )
1691+
1692+ # Run blocks between consecutive checkpoints
1693+ current_step = 0
1694+ for block_idx in range (len (checkpoint_data ) - 1 ):
1695+ start_checkpoint = checkpoint_data [block_idx ]
1696+ end_checkpoint = checkpoint_data [block_idx + 1 ]
1697+
1698+ block_steps = end_checkpoint ["step" ] - start_checkpoint ["step" ]
1699+
1700+ if block_steps <= 0 :
1701+ continue
1702+
1703+ # Create fix print for this block
1704+ lmp .command (
1705+ 'fix f3 all print 1 "${dU} $(press) $(vol) ${blambda}" screen no file ts.backward_%d_cycle_%d.dat'
1706+ % (iteration , block_idx )
1707+ )
1708+
1709+ # All run commands use same start/stop for ramp() to work correctly
1710+ lmp .command (f"run { block_steps } start 0 stop { total_steps } " )
1711+
1712+ # Update current step counter
1713+ current_step += block_steps
1714+
1715+ # Unfix to avoid duplicates at boundaries
1716+ lmp .command ("unfix f3" )
1717+
1718+ # Get atomic data directly from LAMMPS
1719+ try :
1720+ atoms = sm .extract_atoms_from_lammps (lmp )
1721+
1722+ # Process frame and get statistics
1723+ stats = sm .process_monitoring_frame (
1724+ accumulator = accumulator , atoms = atoms , logger = self .logger
1725+ )
1726+
1727+ # Get detector statistics for logging
1728+ lambda_value = li + (lf - li ) * current_step / total_steps
1729+ apparent_temp = t0 / lambda_value
1730+
1731+ # Write to monitoring file
1732+ monitor_out .write (
1733+ f"{ block_idx } { current_step } { lambda_value :.6f} { apparent_temp :.2f} "
1734+ f"{ int (stats ['is_flagged' ])} { stats ['distance' ]:.6f} "
1735+ f"{ stats ['mean' ]:.6f} { stats ['std' ]:.6f} \n "
1736+ )
1737+ monitor_out .flush ()
1738+
1739+ if stats ["is_flagged" ]:
1740+ self .logger .warning (
1741+ f"STRUCTURAL CHANGE DETECTED at step { current_step } (λ={ lambda_value :.4f} , T={ apparent_temp :.2f} K) "
1742+ f"during backward sweep (iteration { iteration } )"
1743+ )
1744+ except Exception as e :
1745+ self .logger .warning (
1746+ f"Failed to monitor structure at backward block { block_idx } : { e } "
1747+ )
1748+ import traceback
1749+
1750+ self .logger .warning (f"Traceback: { traceback .format_exc ()} " )
1751+
1752+ # Run any remaining steps
1753+ remaining_steps = total_steps - current_step
1754+ if remaining_steps > 0 :
1755+ lmp .command (
1756+ 'fix f3 all print 1 "${dU} $(press) $(vol) ${blambda}" screen no file ts.backward_%d_cycle_%d.dat'
1757+ % (iteration , len (checkpoint_data ) - 1 )
1758+ )
1759+ lmp .command (f"run { remaining_steps } start 0 stop { total_steps } " )
1760+ lmp .command ("unfix f3" )
1761+
1762+ # Close monitoring data file
1763+ monitor_out .close ()
1764+ self .logger .info (f"Structural monitoring data saved to { monitor_file } " )
1765+
1766+ # Generate monitoring plot
1767+ plot_file = os .path .join (
1768+ self .simfolder , f"structural_monitoring_backward_{ iteration } .png"
1769+ )
1770+ success = sm .generate_monitoring_plot (
1771+ monitor_file = monitor_file , output_file = plot_file , iteration = iteration
1772+ )
1773+ if success :
1774+ self .logger .info (f"Monitoring plot saved to { plot_file } " )
1775+ else :
1776+ self .logger .warning ("Failed to generate monitoring plot (backward)" )
1777+
1778+ # Merge all cycle files into final output
1779+ self .logger .info (f"Merging cycle files into ts.backward_{ iteration } .dat" )
1780+ final_file = os .path .join (self .simfolder , f"ts.backward_{ iteration } .dat" )
1781+
1782+ import shutil
1783+
1784+ with open (final_file , "w" ) as outfile :
1785+ # Copy first cycle file entirely
1786+ cycle_file = os .path .join (
1787+ self .simfolder , f"ts.backward_{ iteration } _cycle_0.dat"
1788+ )
1789+ with open (cycle_file , "r" ) as infile :
1790+ shutil .copyfileobj (infile , outfile )
1791+ os .remove (cycle_file )
1792+
1793+ # Append subsequent cycles, skipping header and first data line (duplicate)
1794+ total_cycles = (
1795+ len (checkpoint_data ) - 1 + (1 if remaining_steps > 0 else 0 )
1796+ )
1797+ for cycle_idx in range (1 , total_cycles ):
1798+ cycle_file = os .path .join (
1799+ self .simfolder , f"ts.backward_{ iteration } _cycle_{ cycle_idx } .dat"
1800+ )
1801+ with open (cycle_file , "r" ) as infile :
1802+ # Skip first two lines: header + duplicate first data line
1803+ next (infile ) # Skip header
1804+ next (infile ) # Skip duplicate line
1805+ # Stream the rest
1806+ shutil .copyfileobj (infile , outfile )
1807+ os .remove (cycle_file )
1808+ else :
1809+ # Standard run without monitoring - single fix print
1810+ lmp .command (
1811+ 'fix f3 all print 1 "${dU} $(press) $(vol) ${blambda}" screen no file ts.backward_%d.dat'
1812+ % iteration
1813+ )
1814+ lmp .command ("run %d" % self .calc ._n_sweep_steps )
1815+ lmp .command ("unfix f3" )
16121816 self .logger .info (f"Finished backward sweep: { iteration } " )
16131817
16141818 if self .calc .monte_carlo .n_swaps > 0 :
@@ -1618,8 +1822,6 @@ def _reversible_scaling_backward(self, iteration=1):
16181822 lmp .command (f"unfix swap{ idx } " )
16191823 # lmp.command("unfix swap_print")
16201824
1621- lmp .command ("unfix f3" )
1622-
16231825 if self .calc .n_print_steps > 0 :
16241826 lmp .command ("undump d1" )
16251827
0 commit comments