View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2024.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.ui;
39  
40  import ffx.algorithms.AlgorithmFunctions;
41  import ffx.algorithms.AlgorithmListener;
42  import ffx.algorithms.Terminatable;
43  import ffx.algorithms.cli.AlgorithmsScript;
44  import ffx.algorithms.dynamics.MolecularDynamics;
45  import ffx.algorithms.dynamics.integrators.IntegratorEnum;
46  import ffx.algorithms.dynamics.thermostats.ThermostatEnum;
47  import ffx.algorithms.optimize.Minimize;
48  import ffx.numerics.Potential;
49  import ffx.potential.ForceFieldEnergy;
50  import ffx.potential.MolecularAssembly;
51  import ffx.potential.bonded.MSNode;
52  import ffx.potential.bonded.RendererCache.ColorModel;
53  import ffx.potential.bonded.RendererCache.ViewModel;
54  import ffx.potential.cli.PotentialScript;
55  import ffx.potential.utils.PotentialsFunctions;
56  import ffx.utilities.Console;
57  import ffx.utilities.GroovyFileFilter;
58  import groovy.lang.Binding;
59  import groovy.lang.Script;
60  
61  import java.awt.Color;
62  import java.awt.Component;
63  import java.awt.Dimension;
64  import java.awt.EventQueue;
65  import java.awt.Menu;
66  import java.awt.MenuBar;
67  import java.awt.Point;
68  import java.awt.Rectangle;
69  import java.io.File;
70  import java.net.URL;
71  import java.nio.file.Path;
72  import java.nio.file.Paths;
73  import java.util.ArrayList;
74  import java.util.EventObject;
75  import java.util.List;
76  import java.util.Map;
77  import java.util.logging.Level;
78  import java.util.logging.Logger;
79  import javax.swing.ImageIcon;
80  import javax.swing.JFrame;
81  import javax.swing.JOptionPane;
82  import javax.swing.JScrollPane;
83  import javax.swing.JSplitPane;
84  import javax.swing.JTextPane;
85  import javax.swing.JViewport;
86  import javax.swing.SwingUtilities;
87  import javax.swing.text.Style;
88  import javax.swing.text.StyleConstants;
89  import javax.swing.text.StyleContext;
90  import javax.swing.text.StyledDocument;
91  
92  import org.codehaus.groovy.runtime.MethodClosure;
93  
94  import org.graalvm.polyglot.Context;
95  import org.graalvm.polyglot.Engine;
96  import org.graalvm.polyglot.Source;
97  import org.graalvm.polyglot.Value;
98  import org.graalvm.polyglot.Language;
99  import org.graalvm.polyglot.proxy.ProxyArray;
100 
101 /**
102  * The ModelingShell is used to script Multiscale Modeling Routines via the Groovy scripting
103  * language. Functionality available through the modeling shell includes the Force Field X API, Java
104  * API and Groovy extensions.
105  *
106  * @author Michael J. Schnieders
107  */
108 public class ModelingShell extends Console implements AlgorithmListener {
109 
110   /**
111    * The logger for this class.
112    */
113   private static final Logger logger = Logger.getLogger(ModelingShell.class.getName());
114   private static final double toSeconds = 1.0e-9;
115 
116   /**
117    * A reference to the main application container.
118    */
119   private final MainPanel mainPanel;
120   /**
121    * The flag headless is true for the CLI and false for the GUI.
122    */
123   private final boolean headless;
124   /**
125    * The flag interrupted is true if a script is running and the user requests it be canceled.
126    */
127   private boolean interrupted;
128   /**
129    * An algorithm that implements the Terminatable interface can be cleanly terminated before
130    * completion. For example, after completion of an optimization step or MD step.
131    */
132   private Terminatable terminatableAlgorithm = null;
133   /**
134    * Flag to indicate if a script is running.
135    */
136   private boolean scriptRunning;
137   /**
138    * Timing.
139    */
140   private long time;
141 
142   private long subTime;
143   private List<String> args;
144 
145   /**
146    * Constructor for ModelingShell.
147    *
148    * @param mainPanel a reference to the MainPanel.
149    */
150   public ModelingShell(MainPanel mainPanel) {
151     super();
152     this.mainPanel = mainPanel;
153     headless = java.awt.GraphicsEnvironment.isHeadless();
154     initContext(getShell().getContext());
155   }
156 
157   /**
158    * after
159    */
160   public void after() {
161     time = System.nanoTime() - time;
162     scriptRunning = false;
163     if (!interrupted) {
164       appendOutput(String.format("\n Script wall clock time: %6.3f (sec)", time * toSeconds),
165           getPromptStyle());
166     } else {
167       appendOutput(String.format("\n Script interrupted after: %6.3f (sec)", time * toSeconds),
168           getPromptStyle());
169     }
170     mainPanel.getModelingPanel().enableLaunch(true);
171   }
172 
173   /**
174    * {@inheritDoc}
175    */
176   @Override
177   public boolean algorithmUpdate(MolecularAssembly active) {
178     if (interrupted) {
179       return false;
180     }
181 
182     GraphicsCanvas graphics = mainPanel.getGraphics3D();
183     if (graphics != null) {
184       /*
185        Use the blocking graphics update method so that only
186        self-consistent coordinate sets are displayed.
187       */
188       // if (SwingUtilities.isEventDispatchThread()) {
189       graphics.updateSceneWait(active, true, false, null, false, null);
190       // }
191     }
192 
193     // The algorithm could have been interrupted during the graphics update.
194     return !interrupted;
195   }
196 
197   /**
198    * If at exit time, a script is running, the user is given an option to interrupt it first
199    *
200    * <p>{@inheritDoc}
201    */
202   @Override
203   public Object askToInterruptScript() {
204     if (!scriptRunning) {
205       return true;
206     }
207     int rc = JOptionPane.showConfirmDialog(getScrollArea(),
208         "Script executing. Press 'OK' to attempt to interrupt it before exiting.",
209         "Force Field X Shell", JOptionPane.OK_CANCEL_OPTION);
210     if (rc == JOptionPane.OK_OPTION) {
211       doInterrupt();
212       return true;
213     } else {
214       return false;
215     }
216   }
217 
218   /**
219    * {@inheritDoc}
220    *
221    * <p>Return false if user elects to cancel.
222    */
223   @Override
224   public boolean askToSaveFile() {
225     File file = (File) getScriptFile();
226     if (file == null || !getDirty()) {
227       return true;
228     }
229     return switch (JOptionPane.showConfirmDialog((Component) getFrame(),
230         "Save changes to " + file.getName() + "?", "Force Field X Shell",
231         JOptionPane.YES_NO_CANCEL_OPTION)) {
232       case JOptionPane.YES_OPTION -> fileSave();
233       case JOptionPane.NO_OPTION -> true;
234       default -> false;
235     };
236   }
237 
238   /**
239    * before
240    */
241   public void before() {
242     interrupted = false;
243     terminatableAlgorithm = null;
244     time = System.nanoTime();
245     subTime = time;
246     mainPanel.getModelingPanel().enableLaunch(false);
247     scriptRunning = true;
248   }
249 
250   @Override
251   public void clearContext() {
252     super.clearContext();
253     initContext(getShell().getContext());
254   }
255 
256   @Override
257   public void clearContext(EventObject evt) {
258     super.clearContext(evt);
259     initContext(getShell().getContext());
260   }
261 
262   /**
263    * {@inheritDoc}
264    *
265    * <p>Print out the Force Field X promo.
266    */
267   @Override
268   public void clearOutput() {
269     if (!java.awt.GraphicsEnvironment.isHeadless()) {
270       JTextPane output = getOutputArea();
271       output.setText("");
272       appendOutput(
273           MainPanel.border + "\n" + MainPanel.title + MainPanel.aboutString + "\n" + MainPanel.border
274               + "\n", getCommandStyle());
275     }
276   }
277 
278   @Override
279   public void clearOutput(EventObject evt) {
280     clearOutput();
281   }
282 
283   /**
284    * energy
285    *
286    * @return a {@link ffx.potential.ForceFieldEnergy} object.
287    */
288   public ForceFieldEnergy energy() {
289     if (interrupted) {
290       logger.info(" Algorithm interrupted - skipping energy.");
291       return null;
292     }
293     if (terminatableAlgorithm != null) {
294       logger.info(" Algorithm already running - skipping energy.");
295       return null;
296     }
297 
298     MolecularAssembly active = mainPanel.getHierarchy().getActive();
299     if (active != null) {
300       ForceFieldEnergy energy = active.getPotentialEnergy();
301       if (energy == null) {
302         energy = ForceFieldEnergy.energyFactory(active);
303         active.setPotential(energy);
304       }
305       energy.energy(false, true);
306       return energy;
307     }
308     return null;
309   }
310 
311   @Override
312   public void fileNewWindow() {
313     mainPanel.resetShell();
314   }
315 
316   @Override
317   public void fileNewWindow(EventObject evt) {
318     fileNewWindow();
319   }
320 
321   public AlgorithmFunctions getUIAlgorithmUtils() {
322     return new UIUtils(this, mainPanel);
323   }
324 
325   public PotentialsFunctions getUIPotentialsUtils() {
326     return new UIUtils(this, mainPanel);
327   }
328 
329   /**
330    * md
331    *
332    * @param nStep          The number of MD steps.
333    * @param timeStep       a double.
334    * @param printInterval  a double.
335    * @param saveInterval   a double.
336    * @param temperature    a double.
337    * @param initVelocities a boolean.
338    * @param dyn            a {@link java.io.File} object.
339    */
340   public void md(int nStep, double timeStep, double printInterval, double saveInterval,
341                  double temperature, boolean initVelocities, File dyn) {
342     if (interrupted || terminatableAlgorithm != null) {
343       return;
344     }
345     FFXSystem active = mainPanel.getHierarchy().getActive();
346     if (active != null) {
347       MolecularDynamics molecularDynamics = new MolecularDynamics(active,
348           active.getPotentialEnergy(), this, ThermostatEnum.BUSSI, IntegratorEnum.BEEMAN);
349       terminatableAlgorithm = molecularDynamics;
350       molecularDynamics.dynamic(nStep, timeStep, printInterval, saveInterval, temperature,
351           initVelocities, dyn);
352       terminatableAlgorithm = null;
353     }
354   }
355 
356   /**
357    * Configure the Swing GUI for the shell.
358    */
359   @Override
360   public void run() {
361     if (!headless) {
362       if (SwingUtilities.isEventDispatchThread()) {
363         init();
364       } else {
365         try {
366           SwingUtilities.invokeAndWait(this::init);
367         } catch (Exception e) {
368           //
369         }
370       }
371     }
372   }
373 
374   /**
375    * select
376    *
377    * @param node a {@link ffx.potential.bonded.MSNode} object.
378    */
379   public void select(MSNode node) {
380     if (node != null) {
381       mainPanel.getHierarchy().onlySelection(node);
382       sync();
383       logger.info(String.format(" Selected: %s.", node));
384     }
385   }
386 
387   /**
388    * setArgList
389    *
390    * @param argList a {@link java.util.List} object.
391    */
392   public void setArgList(List<String> argList) {
393     args = new ArrayList<>(argList);
394     setVariable("args", argList);
395   }
396 
397   @Override
398   public void showAbout() {
399     mainPanel.about();
400   }
401 
402   @Override
403   public void showAbout(EventObject evt) {
404     showAbout();
405   }
406 
407   /**
408    * time
409    *
410    * @return a {@link java.lang.Double} object.
411    */
412   public Double time() {
413     long current = System.nanoTime();
414     double timer = (current - subTime) * toSeconds;
415     subTime = current;
416     appendOutput(String.format("\n Intermediate time: %8.3f (sec)\n", timer), getPromptStyle());
417     return timer;
418   }
419 
420   /**
421    * {@inheritDoc}
422    */
423   @Override
424   public String toString() {
425     return "Force Field X Shell";
426   }
427 
428   @Override
429   public void updateTitle() {
430     JFrame frame = (JFrame) this.getFrame();
431     File file = (File) getScriptFile();
432     if (file != null) {
433       String name = file.getName();
434       frame.setTitle(name + " - Force Field X Shell");
435     } else {
436       frame.setTitle("Force Field X Shell");
437     }
438   }
439 
440   /**
441    * Initialize access to Force Field X variables and methods from with the Shell.
442    */
443   private void initContext(Binding binding) {
444     binding.setVariable("dat", mainPanel.getHierarchy());
445     binding.setVariable("cmd", mainPanel);
446     binding.setVariable("vis", mainPanel.getGraphics3D());
447     binding.setVariable("sh", this);
448     binding.setVariable("active", mainPanel.getHierarchy().getActive());
449     binding.setVariable("logger", logger);
450 
451     // Timer
452     binding.setVariable("time", new MethodClosure(this, "time"));
453 
454     // File
455     binding.setVariable("open", new MethodClosure(mainPanel, "openWait"));
456     binding.setVariable("convertWait", new MethodClosure(mainPanel, "convertWait"));
457     binding.setVariable("save", new MethodClosure(mainPanel, "saveAsXYZ"));
458     binding.setVariable("saveAsXYZ", new MethodClosure(mainPanel, "saveAsXYZ"));
459     binding.setVariable("saveAsP1", new MethodClosure(mainPanel, "saveAsP1"));
460     binding.setVariable("saveAsPDB", new MethodClosure(mainPanel, "saveAsPDB"));
461     binding.setVariable("close", new MethodClosure(mainPanel, "closeWait"));
462     binding.setVariable("closeAll", new MethodClosure(mainPanel, "closeAll"));
463 
464     // Select
465     binding.setVariable("select", new MethodClosure(this, "select"));
466 
467     // Display and View menus.
468     if (!headless) {
469       GraphicsCanvas graphics = mainPanel.getGraphics3D();
470 
471       // Display
472       int index = 0;
473       for (ViewModel view : ViewModel.values()) {
474         binding.setVariable(view.name(), view);
475         index++;
476         if (index > 8) {
477           break;
478         }
479       }
480       binding.setVariable("view", new MethodClosure(graphics, "viewWait"));
481 
482       // Color
483       index = 0;
484       for (ColorModel color : ColorModel.values()) {
485         binding.setVariable(color.name(), color);
486         index++;
487         if (index > 6) {
488           break;
489         }
490       }
491       binding.setVariable("color", new MethodClosure(graphics, "colorWait"));
492     }
493 
494     // Algorithms
495     binding.setVariable("returnEnergy", new MethodClosure(this, "returnEnergy"));
496     binding.setVariable("energy", new MethodClosure(this, "energy"));
497     binding.setVariable("analyze", new MethodClosure(this, "analyze"));
498     binding.setVariable("minimize", new MethodClosure(this, "minimize"));
499     binding.setVariable("minimize_2", new MethodClosure(this, "minimize_2"));
500     binding.setVariable("md", new MethodClosure(this, "md"));
501     binding.setVariable("potential", new MethodClosure(this, "potential"));
502     binding.setVariable("poledit", new MethodClosure(this, "poledit"));
503     binding.setVariable("superpose", new MethodClosure(this, "superpose"));
504 
505     // Obtain UIUtils object
506     binding.setVariable("functions", new UIUtils(this, mainPanel));
507 
508     // Define a listener variable to send updates back to the GUI.
509     binding.setVariable("listener", this);
510   }
511 
512   /**
513    * Update the shell menu items.
514    */
515   private void initMenus() {
516     JFrame frame = (JFrame) this.getFrame();
517     MenuBar menuBar = frame.getMenuBar();
518     // Remove "Capture Std. Out", "Capture Std. Error" & "Detached Output" from the View menu.
519     Menu menu = menuBar.getMenu(2);
520     menu.remove(5);
521     menu.remove(5);
522     menu.remove(9);
523 
524     // Edit the Script menu.
525     menu = menuBar.getMenu(4);
526     menu.remove(4);
527     menu.remove(4);
528     menu.remove(4);
529     menu.remove(5);
530     menu.remove(7);
531   }
532 
533   /**
534    * Initialize the Shell.
535    */
536   private void init() {
537     try {
538       super.run();
539       // Output JTextPane
540       JTextPane output = getOutputArea();
541       output.setBackground(Color.BLACK);
542       output.setForeground(Color.WHITE);
543       // Input JTextPane
544       JTextPane input = getInputArea();
545       input.setBackground(Color.WHITE);
546       input.setForeground(Color.BLACK);
547       // Output StyledDocument Styles
548       StyledDocument doc = output.getStyledDocument();
549       Style defStyle = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
550       Style regular = doc.addStyle("regular", defStyle);
551       Style prompt = doc.addStyle("prompt", regular);
552       Style command = doc.addStyle("command", regular);
553       Style result = doc.addStyle("result", regular);
554       StyleConstants.setFontFamily(regular, "Monospaced");
555       setPromptStyle(prompt);
556       setCommandStyle(command);
557       setResultStyle(result);
558       StyleConstants.setForeground(prompt, Color.ORANGE);
559       StyleConstants.setForeground(command, Color.GREEN);
560       StyleConstants.setForeground(result, Color.GREEN);
561       StyleConstants.setBackground(result, Color.BLACK);
562       clearOutput();
563       // initMenus();
564 
565       // Set labels and icon for Force Field X.
566       getStatusLabel().setText("Welcome to the Force Field X Shell.");
567       JFrame frame = (JFrame) this.getFrame();
568       frame.setTitle("Force Field X Shell");
569       URL iconURL = getClass().getClassLoader().getResource("ffx/ui/icons/icon64.png");
570       ImageIcon icon = new ImageIcon(iconURL);
571       frame.setIconImage(icon.getImage());
572       frame.setSize(600, 600);
573     } catch (Exception e) {
574       logger.warning(" Exception starting up the FFX console.\n" + e);
575     }
576   }
577 
578   List<String> getArgs() {
579     return new ArrayList<>(args);
580   }
581 
582   /**
583    * runFFXScript - Execute a FFX script.
584    *
585    * @param file    a {@link java.io.File} object.
586    * @param argList List of String inputs to the script.
587    * @return Returns a reference to the executed script.
588    */
589   public Script runFFXScript(File file, List<String> argList) {
590     GroovyFileFilter groovyFileFilter = new GroovyFileFilter();
591     // Check that the file is a Groovy script.
592     if (!groovyFileFilter.accept(file)) {
593       // If not, assume its a Python script.
594       return runNonGroovyScript(file, argList);
595     }
596 
597     logger.info(" Executing external Groovy script: " + file.getAbsolutePath() + "\n");
598     try {
599       before();
600       Script script = null;
601       try {
602         // Run the file using the current Shell and its Binding.
603         Object o = getShell().run(file, argList);
604 
605         if (o instanceof Script) {
606           script = (Script) o;
607         }
608 
609         // Do not destroy the system when using the GUI.
610         if (headless) {
611           if (o instanceof PotentialScript) {
612             ((PotentialScript) o).destroyPotentials();
613           } else if (o instanceof AlgorithmsScript) {
614             ((AlgorithmsScript) o).destroyPotentials();
615           }
616         }
617       } catch (Exception ex) {
618         logger.log(Level.SEVERE, " Uncaught error: FFX is shutting down.\n", ex);
619       }
620       after();
621       return script;
622     } catch (Exception e) {
623       // Replacing this with a "Multi-Catch" leads to specific Exceptions not present in
624       // some versions of Groovy.
625       String message = "Error evaluating script.";
626       logger.log(Level.WARNING, message, e);
627     }
628 
629     return null;
630   }
631 
632   /**
633    * runPythonScript - Execute a Python script.
634    *
635    * @param file    a {@link java.io.File} object.
636    * @param argList List of String inputs to the script.
637    * @return Returns a reference to the executed script.
638    */
639   public Script runNonGroovyScript(File file, List<String> argList) {
640     logger.info(" Attempting to execute Polyglot script:\n  " + file.getAbsolutePath() + "\n");
641 
642     if (logger.isLoggable(Level.FINE)) {
643       logger.fine(" Available languages: ");
644       try (Engine engine = Engine.create()) {
645         Map<String, Language> map = engine.getLanguages();
646         for (Map.Entry<String, Language> entry : map.entrySet()) {
647           logger.fine("  " + entry.getKey());
648         }
649       } catch (Exception ex) {
650         logger.log(Level.SEVERE, " Uncaught error: FFX is shutting down.\n", ex);
651       }
652     }
653 
654     try {
655       before();
656       Script script = null;
657       String language = Source.findLanguage(file);
658       logger.info(" Detected script language: " + language);
659       Source source = Source.newBuilder(language, file).build();
660       try (Context context = getContext(language)) {
661         // Get the bindings for the language.
662         Value bindings = context.getBindings(language);
663         // Use the Polyglot ProxyArray to pass the command line arguments to the script.
664         List<Object> objList = new ArrayList<>(argList);
665         ProxyArray argArray = ProxyArray.fromList(objList);
666         bindings.putMember("args", argArray);
667         // Run the file using the current Shell and its Binding.
668         Value result = context.eval(source);
669         logger.info(" Execution of Polyglot script completed.");
670       } catch (Exception ex) {
671         logger.log(Level.SEVERE, " Uncaught error: FFX is shutting down.\n", ex);
672       }
673       after();
674       return script;
675     } catch (Exception e) {
676       // Replacing this with a "Multi-Catch" leads to specific Exceptions not present in some versions of Groovy.
677       String message = "Error evaluating script.";
678       logger.log(Level.WARNING, message, e);
679     }
680     return null;
681   }
682 
683   private Context getContext(String language) {
684     if (language.equalsIgnoreCase("python")) {
685       // For Python, try to locate the Graal Python executable.
686 
687       // The default location is $FFX_HOME/ffx_venv/bin/graalpy.
688       String FFX_HOME = System.getProperty("basedir");
689       Path graalpy = Paths.get(FFX_HOME, "ffx_venv", "bin", "graalpy");
690 
691       // Override the default location with the -Dgraalpy=path.to.graalpy option.
692       String graalpyString = System.getProperty("graalpy", graalpy.toString());
693       graalpy = Paths.get(graalpyString);
694       if (graalpy.toFile().exists()) {
695         logger.fine(" graalpy (-Dgraalpy=path.to.graalpy):             " + graalpy);
696         return Context.newBuilder(language).allowAllAccess(true).option("python.Executable", graalpyString).build();
697       }
698     }
699     // Fall through to default.
700     return Context.newBuilder(language).allowAllAccess(true).build();
701   }
702 
703   /**
704    * runFFXScript - Execute a compiled FFX script.
705    *
706    * @param script  a compiled FFX script.
707    * @param argList List of String inputs to the script.
708    * @return Returns a reference to the executed script.
709    */
710   public Script runFFXScript(Class<? extends Script> script, List<String> argList) {
711     logger.info(" Executing internal script: " + script.getCanonicalName() + "\n");
712     try {
713       before();
714       Script groovyScript = null;
715       try {
716         // Create a Binding for command line arguments and FFX User Interface variables.
717         Binding binding = new Binding();
718         binding.setVariable("args", argList);
719         initContext(binding);
720 
721         // Create a new instance of the script and run it.
722         groovyScript = script.getDeclaredConstructor().newInstance();
723         groovyScript.setBinding(binding);
724         groovyScript.run();
725 
726         // Do not destroy the system when using the GUI.
727         if (headless) {
728           if (groovyScript instanceof PotentialScript) {
729             ((PotentialScript) groovyScript).destroyPotentials();
730           } else if (groovyScript instanceof AlgorithmsScript) {
731             ((AlgorithmsScript) groovyScript).destroyPotentials();
732           }
733         }
734       } catch (Exception ex) {
735         logger.log(Level.SEVERE, " Uncaught error: FFX is shutting down.\n", ex);
736       }
737       after();
738 
739       return groovyScript;
740     } catch (Exception e) {
741       // Replacing this with a "Multi-Catch" leads to specific Exceptions not present in
742       // some versions of Groovy.
743       String message = "Error evaluating script.";
744       logger.log(Level.WARNING, message, e);
745     }
746     return null;
747   }
748 
749   /**
750    * returnEnergy
751    *
752    * @return Current system energy (a double).
753    */
754   double returnEnergy() {
755     if (interrupted) {
756       logger.info(" Algorithm interrupted - skipping energy.");
757       return 0.0;
758     }
759     if (terminatableAlgorithm != null) {
760       logger.info(" Algorithm already running - skipping energy.");
761       return 0.0;
762     }
763 
764     MolecularAssembly active = mainPanel.getHierarchy().getActive();
765     if (active != null) {
766       ForceFieldEnergy energy = active.getPotentialEnergy();
767       if (energy == null) {
768         energy = ForceFieldEnergy.energyFactory(active);
769         active.setPotential(energy);
770       }
771       return energy.energy(false, true);
772     }
773     logger.warning(" Energy could not be calculated");
774     return 0.0;
775   }
776 
777   /**
778    * minimize
779    *
780    * @param eps a double.
781    * @return a {@link ffx.numerics.Potential} object.
782    */
783   Potential minimize(double eps) {
784     if (interrupted) {
785       logger.info(" Algorithm interrupted - skipping minimization.");
786       return null;
787     }
788     if (terminatableAlgorithm != null) {
789       logger.info(" Algorithm already running - skipping minimization.");
790       return null;
791     }
792     MolecularAssembly active = mainPanel.getHierarchy().getActive();
793     if (active != null) {
794       Minimize minimize = new Minimize(active, this);
795       terminatableAlgorithm = minimize;
796       Potential potential = minimize.minimize(eps);
797       terminatableAlgorithm = null;
798       return potential;
799     } else {
800       logger.info(" No active system to minimize.");
801     }
802     return null;
803   }
804 
805   /**
806    * Fix up the "Result: " message, then call the original method.
807    *
808    * @param string String to ouput.
809    * @param style  Style to use.
810    */
811   void appendOutputNl(String string, Style style) {
812     if (interrupted) {
813       return;
814     }
815     if (headless) {
816       logger.info(string);
817     }
818     if (string.equals("Result: ")) {
819       string = " Script result: \n";
820     } else if (string.equals("groovy> ")) {
821       string = " ffx> ";
822     }
823 
824     super.appendOutputNl(string, style);
825 
826     if (EventQueue.isDispatchThread()) {
827       scroll();
828     } else {
829       SwingUtilities.invokeLater(this::scroll);
830     }
831   }
832 
833   /**
834    * scroll
835    */
836   private void scroll() {
837     JTextPane output = getOutputArea();
838     JSplitPane splitPane = getSplitPane();
839     JScrollPane scrollPane = (JScrollPane) splitPane.getBottomComponent();
840     JViewport viewport = scrollPane.getViewport();
841     Rectangle visibleSize = viewport.getVisibleRect();
842     Dimension totalSize = output.getSize();
843     Point point = new Point(0, totalSize.height - visibleSize.height);
844     viewport.setViewPosition(point);
845   }
846 
847   /**
848    * appendOutput
849    *
850    * @param string a {@link java.lang.String} object.
851    * @param style  a {@link javax.swing.text.Style} object.
852    */
853   private void appendOutput(String string, Style style) {
854     if (interrupted) {
855       return;
856     }
857 
858     if (headless) {
859       logger.info(string);
860       return;
861     }
862 
863     super.appendOutput(string, style);
864     if (EventQueue.isDispatchThread()) {
865       scroll();
866     } else {
867       SwingUtilities.invokeLater(this::scroll);
868     }
869   }
870 
871   /**
872    * setMeasurement
873    *
874    * @param measurement a {@link java.lang.String} object.
875    * @param d           a double.
876    */
877   void setMeasurement(String measurement, double d) {
878     try {
879       appendOutput(measurement, getOutputStyle());
880     } catch (Exception e) {
881       String message = "Exception appending measurement to Shell.\n";
882       logger.log(Level.WARNING, message, e);
883     }
884   }
885 
886   /**
887    * sync
888    */
889   void sync() {
890     try {
891       setVariable("active", mainPanel.getHierarchy().getActive());
892     } catch (Exception e) {
893       String message = " Exception syncing shell variables.\n";
894       logger.log(Level.WARNING, message, e);
895     }
896   }
897 }