Using Java Swing to Explore Solutions of Differential Equations with NDSolve

Andreas Lauschke
(mooniac@gmail.com)

The following Mathematica functions use methods from Java Swing to create a visual application to analyse the solutions and errors computed by the Mathematica function NDSolve. They are based on the JLink Java connectivity package and use Java Swing code in Mathematica.

Discussion:
The functions use several widgets from Java Swing, such as push buttons, radio buttons, check marks, combo boxes, input fields, etc. and display the output of the NDSolve computation in a MathGraphicsJPanel (inherited form JPanel) and a MathJFrame from JLink.

The code can easily be adapted to suit different needs. Different Java layouts can be used, other widgets, event handlers, borders, sizes, colors, backgrounds, shapes, labels, etc. Basically everything that Java Swing supports in a manner that can be scripted in Mathematica can be used. The expressions must, however, represent valid Java expression according to the Java bean concept. One could, for example, use a GUI builder tool for Java, generate the Java code and use it in Mathematica, or compile the Java code in a .jar Java archive file and use that instead with JLink. (The latter would have the advantage that subclassing could be used, and it would also be more efficient from a performance perspective.)

Here we suppress various error messages that NDSolve would generate.

Off[General::"spell"];
Off[General::"spell1"];
Off[InterpolatingFunction::"dmval"];
Off[NDSolve::"ndsz"];
Off[NDSolve::"bvluc"];
Off[NDSolve::"sprkdm"];
Off[NDSolve::"initf"];
Off[NDSolve::"submtd"];
Off[NDSolve::"mxst"];
Off[NDSolve::"msols"];
Off[NDSolve::"nlnum"];
Off[NDSolve::"nlnum1"];
Off[NDSolve::"ininv"];
Off[NDSolve::"bvaux"];
Off[NDSolve::"csymb"];
Off[NDSolve::"opdims"];
Off[NDSolve::"sprkqv"];
Off[NDSolve::"ndstab"];
Off[NDSolve::"wprec"];
Off[NDSolve::"ndsol"];
Off[DSolve::"bvnul"];
Off[DSolve::"deqn"];
Off[Solve::"ifun"];
Off[Plot::"plnr"];
Off[ReplaceAll::"reps"];
Off[Transpose::"nmtx"];
Off[Show::"gcomb"];
Off[Power::"infy"];
Off[Infinity::"indet"];
Off[Graphics::"gptn"];
Off[Graphics::"gprim"];
Off[General::"ovfl"];

This loads the JLink package. And for an analysis of the residuals we will also need the InterpolatingFunctionsAnatomy package.

Needs["JLink`"];
Needs["DifferentialEquations`InterpolatingFunctionAnatomy`"];

Here we specify the Java runtime we want to use. Mathematica has a redistributable Java runtime as part of its installation, but it is always possible to use another Java runtime by specifying its location in the InstallJava command (see further down). Another possibility is to set the WRI_JAVA_HOME environment variable to the Java location (on Windows). It's also possible to specify additional runtime options to use to configure the virtual machine.

(* Set javalocation to the full path of the java.exe file of your Java installation if you want to specify a particular Java runtime. Leave it "" to use the default one. *)

javalocation="";
javalocation="E:\\Program Files\\Java\\jre1.5.0_06\\bin\\java.exe";
javalocation="E:\\Program Files\\Java\\jre1.5.0_08\\bin\\java.exe";
javalocation="E:\\Program Files\\Java\\jre1.6.0\\bin\\java.exe";
javalocation="";
javalocation="E:\\Program Files\\Java\\jre1.5.0_08\\bin\\java.exe -Dsun.java2d.d3d=true";

(* use -Dsun.java2d.opengl=true for OpenGL instead of Direct3D on non-Windows platforms *)

For most of the widgets placement, inset, orientation, and margin parameters should be "outsourced" and put in a separate function. Several constraints for the GridBag layout should be set, and it's more efficient to just call a function with the appropriate parameters when defining every widget instead of using these 5 lines for every widget in the code.

cons[gridxc_Integer,gridyc_Integer,insetsc_List]:=With[{},
gridBagConstraints=JavaNew["java.awt.GridBagConstraints"];
gridBagConstraints@gridx=gridxc;
gridBagConstraints@gridy=gridyc;
gridBagConstraints@anchor=GridBagConstraints`WEST;
gridBagConstraints@insets=JavaNew["java.awt.Insets",Sequence@@insetsc];
];

The following defines the main function. We launch the Java runtime, create a window, set a title for the window, and instantiate several widget objects to be used later. We divide the window in two panels, a top panel that contains the widgets/input parameters, and a plot panel that displays the solution (The exact symbolic solution, if it's possible to compute one with DSolve, together with the NDSolve solution, or only the NDSolve solution, if no solution can be found with DSolve). Then we define layouts for the panels and add several widgets such as labels, text areas, radio buttons, push buttons, etc.

A couple of differential systems and methods are predefined in two combo boxes, more can be added, or additional ones can be specified in the text field under "Your Own".

Additional details must be defined, e. g. the combo box model that Java Swing uses, the radio button group (to ensure that at most only one radio button is ever selected and that a new selection deselects previous selections), colors, check marks, etc. These can mostly be done with a GUI builder if developing them "by hand" appears too cumbersome.

It's also possible to generate additional windows by checking the boxes for error plots in the top panel, or to turn them off again.

At the end we define all event handlers using MathActionListener classes from JLink (inherited from ActionListener).

explore[]:=JavaBlock[Module[{window,topPanel,plotPanel,title},If[javalocation=!="",InstallJava[CommandLine->javalocation],InstallJava[]];

(* overall window *)
window=JavaNew["com.wolfram.jlink.MathJFrame"];
window[setLocation[1,1]];
window[setTitle["NDSolve Explorer"]];

(* instantiate several widgets *)
buttonGroup1=JavaNew["javax.swing.ButtonGroup"];
topPanel=JavaNew["javax.swing.JPanel"];
topPanel[setLayout[JavaNew["java.awt.GridBagLayout"]]];
jComboBox1=JavaNew["javax.swing.JComboBox"];
jComboBox2=JavaNew["javax.swing.JComboBox"];
jCheckBox1=JavaNew["javax.swing.JCheckBox"];
jCheckBox2=JavaNew["javax.swing.JCheckBox"];
jScrollPane1=JavaNew["javax.swing.JScrollPane"];
jTextArea1=JavaNew["javax.swing.JTextArea"];
jRadioButton1=JavaNew["javax.swing.JRadioButton"];
jRadioButton2=JavaNew["javax.swing.JRadioButton"];
jTextField1=JavaNew["javax.swing.JTextField"];
jTextField2=JavaNew["javax.swing.JTextField"];
jTextField3=JavaNew["javax.swing.JTextField"];
jTextField4=JavaNew["javax.swing.JTextField"];
jTextField5=JavaNew["javax.swing.JTextField"];
jTextField6=JavaNew["javax.swing.JTextField"];
jTextField7=JavaNew["javax.swing.JTextField"];
jLabel1=JavaNew["javax.swing.JLabel"];
jLabel2=JavaNew["javax.swing.JLabel"];
jLabel3=JavaNew["javax.swing.JLabel"];
jLabel4=JavaNew["javax.swing.JLabel"];
jLabel5=JavaNew["javax.swing.JLabel"];
jLabel6=JavaNew["javax.swing.JLabel"];
jLabel7=JavaNew["javax.swing.JLabel"];
jLabel8=JavaNew["javax.swing.JLabel"];
jButton1=JavaNew["javax.swing.JButton"];
jButton2=JavaNew["javax.swing.JButton"];
jButton3=JavaNew["javax.swing.JButton"];

(* to use static members we have to load the class first *)
LoadJavaClass["javax.swing.BorderFactory"];
LoadJavaClass["java.awt.Color"];
LoadJavaClass["java.awt.BorderLayout"];

(* define a few systems *)
systems=MakeJavaObject[{"{y'[t]==y[t],y[0]==1}","{y'[t]==y[t]^2,y[0]==1}","{y'[t]==1/y[t]^2,y[0]==1}","{y''[t]==-y[t],y[0]==1,y[2 Pi]==1}","{y''[t]==y[t],y[0]==1,y[2 Pi]==1}","{y'[t]==1+y[t]^2,y[0]==0}","{y'[t]==1+y[t]^2,y[0]==1}","{y''[t]==-y[t],y[0]==1,y'[0]==0}","{2 t y'[t]+y''[t]==0,y[0]==0,y'[0]==2/Sqrt@Pi}","{y'[t]==-t y[t],y[0]==1}"}];

(* instantiate and define/place more widgets *)
combo1model=JavaNew["javax.swing.DefaultComboBoxModel",ReturnAsJavaObject[systems]];
jComboBox1[setModel[combo1model]];
jComboBox1[setPreferredSize[JavaNew["java.awt.Dimension",200,22]]];
cons[1,0,{2,2,2,2}];
gridBagConstraints[fill]=GridBagConstraints`HORIZONTAL;
gridBagConstraints[gridwidth]=3;
topPanel[add[jComboBox1,gridBagConstraints]];
jTextArea1[setColumns[20]];
jTextArea1[setRows[5]];
jTextArea1[setEnabled[False]];
jScrollPane1[setViewportView[jTextArea1]];
cons[1,1,{2,2,2,2}];
gridBagConstraints[gridwidth]=3;
gridBagConstraints[fill]=GridBagConstraints`HORIZONTAL;
topPanel[add[jScrollPane1,gridBagConstraints]];
jRadioButton1[setSelected[True]];
jRadioButton1[setText["Select Example"]];
jRadioButton1[setBorder[BorderFactory`createEmptyBorder[0,0,0,0]]];ins3=JavaNew["java.awt.Insets",Sequence@@{0,0,0,0}];
jRadioButton1[setMargin[ins3]];
cons[0,0,{0,0,0,2}];
topPanel[add[jRadioButton1,gridBagConstraints]];
jRadioButton2[setText["Your Own"]];
jRadioButton2[setBorder[BorderFactory`createEmptyBorder[0,0,0,0]]];ins4=JavaNew["java.awt.Insets",Sequence@@{0,0,0,0}];
jRadioButton1[setMargin[ins4]];
cons[0,1,{0,0,0,2}];
gridBagConstraints[anchor]=GridBagConstraints`NORTHWEST;
topPanel[add[jRadioButton2,gridBagConstraints]];
jLabel8[setText["lower t"]];
cons[0,3,{0,0,0,2}];
topPanel[add[jLabel8,gridBagConstraints]];
jTextField2[setText["-3"]];
jTextField2[setPreferredSize[JavaNew["java.awt.Dimension",87,20]]];
cons[1,3,{2,2,2,2}];
topPanel[add[jTextField2,gridBagConstraints]];
jLabel1[setText["upper t"]];
cons[2,3,{2,2,2,2}];
topPanel[add[jLabel1,gridBagConstraints]];
jTextField1[setText["3"]];
jTextField1[setPreferredSize[JavaNew["java.awt.Dimension",87,20]]];
cons[3,3,{2,2,2,2}];
topPanel[add[jTextField1,gridBagConstraints]];
jLabel2[setText["MaxSteps"]];
cons[0,4,{0,0,0,2}];
topPanel[add[jLabel2,gridBagConstraints]];
jTextField3[setText["10000"]];
jTextField3[setPreferredSize[JavaNew["java.awt.Dimension",87,20]]];
cons[1,4,{2,2,2,2}];
topPanel[add[jTextField3,gridBagConstraints]];
jLabel3[setText["MaxStepSize"]];
cons[2,4,{2,2,2,2}];
topPanel[add[jLabel3,gridBagConstraints]];
jTextField4[setText["Infinity"]];
jTextField4[setPreferredSize[JavaNew["java.awt.Dimension",87,20]]];
cons[3,4,{2,2,2,2}];
topPanel[add[jTextField4,gridBagConstraints]];
jLabel4[setText["PrecisionGoal"]];
cons[0,5,{0,0,0,2}];
topPanel[add[jLabel4,gridBagConstraints]];
jTextField5[setText["Automatic"]];
jTextField5[setPreferredSize[JavaNew["java.awt.Dimension",87,20]]];
cons[1,5,{2,2,2,2}];
topPanel[add[jTextField5,gridBagConstraints]];
jLabel5[setText["WorkingPrecision"]];
cons[2,5,{2,2,2,2}];
topPanel[add[jLabel5,gridBagConstraints]];
jTextField6[setText["MachinePrecision"]];
cons[3,5,{2,2,2,2}];
topPanel[add[jTextField6,gridBagConstraints]];
jLabel6[setText["Controller Method"]];
cons[0,2,{0,0,0,2}];
topPanel[add[jLabel6,gridBagConstraints]];


(* method options *)methodchoices=MakeJavaObject[{"Automatic","Adams","BDF","ExplicitRungeKutta","ImplicitRungeKutta","SymplecticPartitionedRungeKutta","DoubleStep","Extrapolation","FixedStep","OrthogonalProjection","Projection","Splitting","StiffnessSwitching","ExplicitEuler","ExplicitMidpoint","ExplicitModifiedMidpoint","LinearlyImplicitMidpoint","LinearlyImplicitEuler","LinearlyImplicitMidpoint","LinearlyImplicitModifiedMidpoint","LocallyExact"}];

(* instantiate and define/place more widgets *)
combo2model=JavaNew["javax.swing.DefaultComboBoxModel",ReturnAsJavaObject[methodchoices]];jComboBox2[setModel[combo2model]];
cons[1,2,{2,2,2,2}];
gridBagConstraints[gridwidth]=3;
gridBagConstraints[fill]=GridBagConstraints`HORIZONTAL;
topPanel[add[jComboBox2,gridBagConstraints]];
jButton1[setText["Go!"]];
cons[1,8,{2,2,12,2}];
gridBagConstraints[gridwidth]=2;
gridBagConstraints[fill]=GridBagConstraints`HORIZONTAL;
topPanel[add[jButton1,gridBagConstraints]];
jLabel7[setText["Max Time (secs)"]];
cons[0,6,{2,2,2,2}];
topPanel[add[jLabel7,gridBagConstraints]];
jTextField7[setText["0.5"]];
jTextField7[setPreferredSize[JavaNew["java.awt.Dimension",87,20]]];
cons[1,6,{2,2,2,2}];
topPanel[add[jTextField7,gridBagConstraints]];
jButton2[setText["Restore Defaults"]];
jButton2[setMargin[JavaNew["java.awt.Insets",2,2,2,2]]];
cons[2,6,{2,2,2,2}];
gridBagConstraints[fill]=GridBagConstraints`HORIZONTAL;
topPanel[add[jButton2,gridBagConstraints]];
jButton3[setText["Placeholder"]];
cons[3,6,{2,2,2,2}];
gridBagConstraints[fill]=GridBagConstraints`HORIZONTAL;
topPanel[add[jButton3,gridBagConstraints]];
jCheckBox1[setText["Include Diff Plots"]];
jCheckBox1[setBorder[BorderFactory`createEmptyBorder[0,0,0,0]]];
ins=JavaNew["java.awt.Insets",Sequence@@{0,0,0,0}];
jCheckBox1[setMargin[ins]];
cons[0,7,{0,0,0,2}];
topPanel[add[jCheckBox1,gridBagConstraints]];
jCheckBox2[setText["Include Mesh Plot"]];
jCheckBox2[setBorder[BorderFactory`createEmptyBorder[0,0,0,0]]];
ins2=JavaNew["java.awt.Insets",Sequence@@{0,0,0,0}];
jCheckBox2[setMargin[ins2]];
cons[2,7,{2,2,2,2}];
topPanel[add[jCheckBox2,gridBagConstraints]];
bg=JavaNew["javax.swing.ButtonGroup"];
bg[add[jRadioButton1]];
bg[add[jRadioButton2]];
plotPanel=JavaNew["com.wolfram.jlink.MathGraphicsJPanel"];
plotPanel[setBackground[Color`white]];
plotPanel[setPreferredSize[JavaNew["java.awt.Dimension",500,350]]];

window[getContentPane[][add[topPanel,ReturnAsJavaObject[BorderLayout`NORTH]]]];window[getContentPane[][add[plotPanel,ReturnAsJavaObject[BorderLayout`SOUTH]]]];
window[pack[]];

(* define computation function *)
computeexpr[___]:=Module[{plot,sol,sol2,ll=ToExpression[jTextField2[getText[]]],ul=ToExpression[jTextField1[getText[]]],
system=ToExpression[If[model=jRadioButton1[getModel[]];model[getGroup[][isSelected[model]]],jComboBox1[getSelectedItem[]],jTextArea1[getText[]]]],
method=ToExpression[jComboBox2[getSelectedItem[]]],
maxsteps=ToExpression[jTextField3[getText[]]],
maxstepsize=ToExpression[jTextField4[getText[]]],
precisiongoal=ToExpression[jTextField5[getText[]]],
workingprecision=ToExpression[jTextField6[getText[]]],
diffplot,diffplot2,diffplot3,timeout=ToExpression[jTextField7[getText[]]],sfound,coords,values},

(* solve the system with NDSolve *)
sol=TimeConstrained[First[y/.NDSolve[system,y,{t,ll,ul},Method->method,MaxSteps->maxsteps,MaxStepSize->maxstepsize,PrecisionGoal->precisiongoal,WorkingPrecision->workingprecision]],timeout];

(* attempt to solve the system with DSolve as well *)sol2=TimeConstrained[First[y[t]/.(DSolve[system,y[t],t]/.C[_]->0)],timeout];
sfound=False;


(* plot the solution *)plot=If[sol=!=$Aborted,If[sol2=!=$Aborted,sfound=True;Plot[{sol[t],sol2},{t,ll,ul},PlotStyle->{Black,Red},DisplayFunction->Identity,PlotLabel->Style[sol2,FontSize->12]],Plot[sol[t],{t,ll,ul},DisplayFunction->Identity]],"No solution could be found in "<>ToString[timeout]<>" seconds."];

(* plot mesh if mesh is selected *)
If[jCheckBox2[isSelected[]]&&sfound,coords=First[InterpolatingFunctionCoordinates[sol]];values=InterpolatingFunctionValuesOnGrid[sol];plot=Show[plot,Graphics[{(Line[{{#1[[1]],0},{#1[[1]],#1[[2]]}}]&)/@Transpose[{coords,values}]}],Graphics[{PointSize[0.01`],Red,(Point[{#1[[1]],#1[[2]]}]&)/@Transpose[{coords,values}]}],Graphics[{PointSize[0.01`],Red,(Point[{#1[[1]],#1[[2]]}]&)/@Transpose[{coords,Table[0,{Length[values]}]}]}],DisplayFunction->Identity];];plotPanel[setMathCommand[ToString[plot,InputForm]]];

(* use error plots in additional windows if selected *)
If[jCheckBox1[isSelected[]]&&sfound,diffplot=Plot[sol[t]-sol2,{t,ll,ul},DisplayFunction->Identity];If[diffwindow[isShowing[]]=!=True,diffwindow=JavaNew["com.wolfram.jlink.MathJFrame"];diffwindow[setLocation[window[getWidth[]]+1,1]];diffwindow[setTitle["DiffPlot -- Adaptive Plot Range"]];diffpanel=JavaNew["com.wolfram.jlink.MathGraphicsJPanel"];diffpanel[setBackground[Color`white]];diffpanel[setPreferredSize[JavaNew["java.awt.Dimension",350,200]]];diffpanel[setLayout[JavaNew["javax.swing.BoxLayout",diffpanel,BoxLayout`YUAXIS]]];diffwindow[getContentPane[][add[diffpanel,ReturnAsJavaObject[BorderLayout`CENTER]]]];diffwindow[pack[]];JavaShow[diffwindow];];diffpanel[setMathCommand[ToString[diffplot,InputForm]]];diffplot2=Plot[sol[t]-sol2,{t,ll,ul},PlotRange->All,DisplayFunction->Identity];If[diffwindow2[isShowing[]]=!=True,diffwindow2=JavaNew["com.wolfram.jlink.MathJFrame"];diffwindow2[setLocation[window[getWidth[]]+1,diffwindow[getHeight[]]+1]];diffwindow2[setTitle["DiffPlot -- Full Plot Range"]];diffpanel2=JavaNew["com.wolfram.jlink.MathGraphicsJPanel"];diffpanel2[setBackground[Color`white]];diffpanel2[setPreferredSize[JavaNew["java.awt.Dimension",350,200]]];diffpanel2[setLayout[JavaNew["javax.swing.BoxLayout",diffpanel2,BoxLayout`YUAXIS]]];diffwindow2[getContentPane[][add[diffpanel2,ReturnAsJavaObject[BorderLayout`CENTER]]]];diffwindow2[pack[]];JavaShow[diffwindow2];];diffpanel2[setMathCommand[ToString[diffplot2,InputForm]]];diffplot3=Plot[Log[10,Abs[1-((sol[t])/sol2)]],{t,ll,ul},PlotRange->All,DisplayFunction->Identity];If[diffwindow3[isShowing[]]=!=True,diffwindow3=JavaNew["com.wolfram.jlink.MathJFrame"];diffwindow3[setLocation[window[getWidth[]]+1,diffwindow2[getHeight[]]+diffwindow[getHeight[]]+1]];diffwindow3[setTitle["DiffPlot -- Base-10 Log of Relative Error"]];diffpanel3=JavaNew["com.wolfram.jlink.MathGraphicsJPanel"];diffpanel3[setBackground[Color`white]];diffpanel3[setPreferredSize[JavaNew["java.awt.Dimension",350,200]]];diffpanel3[setLayout[JavaNew["javax.swing.BoxLayout",diffpanel3,BoxLayout`YUAXIS]]];diffwindow3[getContentPane[][add[diffpanel3,ReturnAsJavaObject[BorderLayout`CENTER]]]];diffwindow3[pack[]];JavaShow[diffwindow3];];diffpanel3[setMathCommand[ToString[diffplot3,InputForm]]];,diffwindow[dispose[]];diffwindow2[dispose[]];diffwindow3[dispose[]];];];

restoredefaults[___]:=With[{},jTextField2[setText["-3"]];jTextField1[setText["3"]];jTextField3[setText["10000"]];jTextField4[setText["Infinity"]];jTextField5[setText["Automatic"]];jTextField6[setText["MachinePrecision"]];jTextField7[setText["0.5"]];diffwindow[dispose[]];diffwindow2[dispose[]];diffwindow3[dispose[]];jComboBox1[setEnabled[True]];jTextArea1[setEnabled[False]];jTextArea1[setText[""]];jRadioButton1[setSelected[True]];jCheckBox1[setSelected[False]];jCheckBox2[setSelected[False]];jComboBox1[setSelectedIndex[0]];jComboBox2[setSelectedIndex[0]];];

(* define event handlers *)
jRadioButton2ActionPerformed[___]:=With[{},jTextArea1[setEnabled[True]];jComboBox1[setEnabled[False]];];jRadioButton1ActionPerformed[___]:=With[{},jComboBox1[setEnabled[True]];jTextArea1[setEnabled[False]];];buttonListener=JavaNew["com.wolfram.jlink.MathActionListener"];buttonListener[setHandler["actionPerformed",ToString[computeexpr]]];jButton1[addActionListener[buttonListener]];buttonListener2=JavaNew["com.wolfram.jlink.MathActionListener"];buttonListener2[setHandler["actionPerformed",ToString[restoredefaults]]];jButton2[addActionListener[buttonListener2]];rbl1=JavaNew["com.wolfram.jlink.MathActionListener"];rbl1[setHandler["actionPerformed",ToString[jRadioButton1ActionPerformed]]];jRadioButton1[addActionListener[rbl1]];rbl2=JavaNew["com.wolfram.jlink.MathActionListener"];rbl2[setHandler["actionPerformed",ToString[jRadioButton2ActionPerformed]]];jRadioButton2[addActionListener[rbl2]];
(* show window and compute expression *)
window[setModal[]];
JavaShow[window];
computeexpr[];
DoModal[];]];

We are now ready to use the function explore[]:

explore[]