001/*
002 * Copyright (c) 2004-2010 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: welker $'
006 * '$Date: 2010-05-06 05:21:26 +0000 (Thu, 06 May 2010) $' 
007 * '$Revision: 24234 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029
030package org.dart.matlab;
031
032import java.io.BufferedInputStream;
033import java.io.File;
034import java.io.FileInputStream;
035import java.io.IOException;
036import java.util.Iterator;
037import java.util.List;
038import java.util.Random;
039import java.util.StringTokenizer;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043
044import ptolemy.actor.TypedAtomicActor;
045import ptolemy.actor.TypedIOPort;
046import ptolemy.actor.gui.style.TextStyle;
047import ptolemy.actor.parameters.PortParameter;
048import ptolemy.data.ArrayToken;
049import ptolemy.data.BooleanToken;
050import ptolemy.data.DoubleToken;
051import ptolemy.data.IntToken;
052import ptolemy.data.LongToken;
053import ptolemy.data.StringToken;
054import ptolemy.data.Token;
055import ptolemy.data.UnsignedByteToken;
056import ptolemy.data.expr.StringParameter;
057import ptolemy.data.type.ArrayType;
058import ptolemy.data.type.BaseType;
059import ptolemy.kernel.CompositeEntity;
060import ptolemy.kernel.util.IllegalActionException;
061import ptolemy.kernel.util.NameDuplicationException;
062
063//////////////////////////////////////////////////////////////////////////
064//// MatlabExpression
065/**
066 * <p>
067 * Allows the user to input a matlab script to be executed when the actor fires.
068 * The following actor takes into consideration differnt startup routines for
069 * MatlabSoftware for Unix/Windows os.
070 * </p>
071 * <p>
072 * Input ports can be added and are automatically loaded into variables in
073 * matlab which can be referenced by the port name.
074 * </p>
075 * <p>
076 * Similarly output can be made by adding output ports to the actor. The output
077 * values are taken from variables with the same names as the output ports.
078 * </p>
079 * <p>
080 * <B>NOTE</B>: windows is a bit more tempermental than unix system. the EXE
081 * file must be directly pointed to by the mlCmd property. E.g
082 * c:\\matlab7\\bin\\win32\\matlab.exe
083 * </p>
084 * <p>
085 * Also, windows command line matlab doesn't use the standard in and out,
086 * instead it uses it's own command window, which makes it impossible to read
087 * and write to the matlab process using the process input and output streams.
088 * So instead the actor writes the data to a file and read in the outputs from
089 * the file. The file is created with a random integer at the end of the file
090 * name to (in theory) allow multiple matlab actors to run at the same time. The
091 * file is deleted once it's been read.
092 * </p>
093 * <p>
094 * <B>TODO</B>: currently this actor only works with standard single value and
095 * array results. support for all forms of matlab output needs to be implemented
096 * </p>
097 * <p>
098 * <B>NOTE</B>: now with java 1.4 complience!! using the ProcessBuilder in java
099 * 1.5 makes things a lot easier but since we need to compile under 1.4 here it
100 * is.
101 * </p>
102 * <p>
103 * <B>Changelog 27/04/06</B>: * check for existance of executable under windows
104 * * kill matlab process when stop() is called * check to make sure variable for
105 * output port name exists if it doesn't, then set it to 0 * changed expression
106 * to PortParameter * changed error handling: sends error message to output port
107 * for some things
108 * </p>
109 * 
110 * @author Tristan King, Nandita Mangal
111 * @version $Id: MatlabExpression.java 24234 2010-05-06 05:21:26Z welker $
112 * 
113 */
114public class MatlabExpression extends TypedAtomicActor {
115
116        public MatlabExpression(CompositeEntity container, String name)
117                        throws NameDuplicationException, IllegalActionException {
118                super(container, name);
119
120                expression = new PortParameter(this, "expression");
121                // make a text style, so that the parameter has multiple lines
122                new TextStyle(expression, "Matlab Expression");
123                // set the default script
124                expression.setExpression("Enter Matlab function or script here...");
125                // set to string mode, so the parameter doesn't have to have surrounding
126                // ""s
127                expression.setStringMode(true);
128
129                output = new TypedIOPort(this, "output", false, true);
130                output.setTypeEquals(BaseType.STRING);
131
132                mlCmd = new StringParameter(this, "mlCmd");
133                mlCmd.setDisplayName("Matlab Executable");
134                // set default for specific os such as
135                // "c:\\matlab7\\bin\\win32\\matlab.exe"
136                mlCmd.setExpression("matlab");
137
138                triggerSwitch = new TypedIOPort(this, "triggerSwitch", true, false);
139
140        }
141
142        // /////////////////////////////////////////////////////////////////
143        // // logging variables ////
144
145        private static final Log log = LogFactory
146                        .getLog("org.dart.matlab.MatlabExpression");
147        // private static final boolean isDebugging = log.isDebugEnabled();
148
149        // /////////////////////////////////////////////////////////////////
150        // // ports and parameters ////
151
152        /**
153         * The output port. Outputs the matlab results.
154         */
155        public TypedIOPort output;
156
157        /**
158         * The expression that is evaluated : Matlab Function or Script from the
159         * parameter dialog box or input port.
160         */
161        public PortParameter expression;
162
163        /**
164         * Path to matlab execuatble. defaults to "matlab"
165         */
166        public StringParameter mlCmd;
167
168        /**
169         * The trigger switch ,whether to execute the actor or not. (True or False /
170         * 0 or 1(or more) as input enable the switch)
171         */
172        public TypedIOPort triggerSwitch;
173
174        // /////////////////////////////////////////////////////////////////
175        // // public methods ////
176        public synchronized void fire() throws IllegalActionException {
177                super.fire();
178                boolean fireSwitch = true;
179
180                if (triggerSwitch.getWidth() > 0) {
181                        Object inputSwitch = triggerSwitch.get(0);
182                        if (inputSwitch instanceof IntToken) {
183                                if (((IntToken) inputSwitch).intValue() >= 1) {
184                                        fireSwitch = true;
185                                } else {
186                                        fireSwitch = false;
187                                }
188
189                        } else if (inputSwitch instanceof BooleanToken) {
190
191                                if (((BooleanToken) inputSwitch).booleanValue()) {
192                                        fireSwitch = true;
193                                } else {
194                                        fireSwitch = false;
195                                }
196                        }
197
198                }
199                // if switch is set, then we execute actor
200                // else do nothing.
201                if (fireSwitch) {
202
203                        // get the script commands needed to pass into -r command
204                        String script = buildScript();
205
206                        String outputString = "";
207                        String randomFilename = tempFilename + "."
208                                        + Math.abs((new Random()).nextInt());
209                        boolean isWindows = (System.getProperty("os.name").indexOf(
210                                        "Windows") > -1);
211
212                        // build the command array
213
214                        if (isWindows) {
215                                File f = new File(mlCmd.getExpression());
216                                if (!f.exists()) {
217                                        throw new IllegalActionException("Matlab process "
218                                                        + mlCmd.getExpression() + " doesn't exist");
219                                }
220                        }
221                        // TODO: same as above under unix
222
223                        // List argList = new ArrayList();
224                        String[] argList;
225
226                        // for some reason windows matlab doesn't work when supplied with
227                        // an array of arguments, and linux matlab doesn't work when
228                        // supplied
229                        // a single string with all the arguments.......
230                        if (isWindows) {
231                                argList = new String[1];
232                                argList[0] = mlCmd.getExpression() + " -r" + " \"" + script
233                                                + "\"" + " -nodesktop" + " -nosplash" + " -logfile "
234                                                + randomFilename;
235                        } else {
236                                argList = new String[4];
237
238                                argList[0] = mlCmd.getExpression();
239                                argList[1] = "-nodesktop";
240                                argList[2] = "-nosplash" + " ";
241                                argList[3] = "-r" + " \"" + script + "\"";
242                        }
243
244                        // run the process and get output
245                        try {
246                                // Process p;
247                                if (isWindows) {
248
249                                        _p = Runtime.getRuntime().exec(argList[0]);
250                                } else {
251                                        _p = Runtime.getRuntime().exec(argList);
252                                }
253
254                                BufferedInputStream inputstream = null;
255                                int result = -1;
256
257                                // if we are running under windows
258                                if (isWindows) {
259                                        // wait for the process to end
260                                        result = _p.waitFor();
261                                        if (result == 0) {
262                                                // create the input stream from the log file created by
263                                                // matlab
264                                                inputstream = new BufferedInputStream(
265                                                                new FileInputStream(randomFilename));
266                                        }
267
268                                        // if not running under windows
269                                } else {
270                                        // get the processes input stream
271                                        inputstream = new BufferedInputStream(_p.getInputStream());
272                                }
273
274                                // read the stream until there is nothing left to read
275                                if (inputstream != null) {
276                                        while (true) {
277                                                int in;
278                                                try {
279                                                        in = inputstream.read();
280                                                } catch (NullPointerException e) {
281                                                        in = -1;
282                                                }
283                                                if (in == -1) {
284                                                        break;
285                                                } else {
286                                                        outputString += (char) in;
287                                                }
288                                        }
289
290                                        // close the input stream
291                                        inputstream.close();
292                                }
293
294                                if (isWindows) {
295                                        // delete the temp file. if it doesn't exist, then it will
296                                        // just return false
297                                        (new File(randomFilename)).delete();
298                                } else {
299                                        // make sure matlab exited OK.
300                                        result = _p.waitFor();
301                                }
302
303                                switch (result) {
304                                case 0:
305                                        break;
306                                case -113:
307                                        output.send(0, new StringToken(
308                                                        "Matlab process was forcfully killed"));
309                                        return;
310                                case 129:
311                                        output.send(0, new StringToken(
312                                                        "Matlab process was forcfully killed"));
313                                        return;
314                                default:
315                                        output.send(0, new StringToken("Matlab process returned \""
316                                                        + result + "\".\nSomething must have gone wrong"));
317                                        return;
318                                }
319
320                        } catch (IOException e) {
321                                log.error("IOException: " + e.getMessage());
322                        } catch (InterruptedException e) {
323                                log.debug("interupted!");
324                        }
325
326                        // process the output from matlab
327                        parseOutput(outputString);
328                }
329
330        }
331
332        public void stop() {
333                if (_p != null) {
334                        _p.destroy();
335                }
336        }
337
338        /**
339         * builds the script for matlab to run
340         * 
341         * @return commands to run under matlab
342         * @throws IllegalActionException
343         */
344        private String buildScript() throws IllegalActionException {
345                List ipList = inputPortList();
346                Iterator ipListIt = ipList.iterator();
347                String inputs = "";
348
349                while (ipListIt.hasNext()) {
350                        TypedIOPort tiop = (TypedIOPort) ipListIt.next();
351
352                        // if the input port is not the script itself.
353                        if (!(tiop.getName().equals("expression"))
354                                        && !(tiop.getName().equals("triggerSwitch"))) {
355                                // get the token waiting on the port
356                                Token[] token = new Token[1];
357
358                                // TODO: check to make sure token exists
359                                token[0] = tiop.get(0);
360
361                                // setup the variable assignment
362                                // looks like: port_name = [ token_values ]
363                                inputs += tiop.getName() + " = ";
364                                inputs += "[" + getTokenValue(token) + "]\n";
365                        }
366                }
367
368                List opList = outputPortList();
369                Iterator opListIt = opList.iterator();
370                String outputs = "";
371
372                while (opListIt.hasNext()) {
373                        TypedIOPort tiop = (TypedIOPort) opListIt.next();
374
375                        // make sure the output port found isn't output
376                        if (!tiop.equals(output)) {
377                                // add it to the list
378                                outputs += "if exist('" + tiop.getName() + "'),"
379                                                + tiop.getName() + ",else," + tiop.getName()
380                                                + "=0,end\n";
381                        }
382
383                }
384
385                // if there is a token waiting on the port input, grab it
386                expression.update();
387
388                String script = inputs + "sprintf('----')\n"
389                                + ((StringToken) expression.getToken()).stringValue() + "\n"
390                                + "sprintf('----')\n" + outputs + "quit\n";
391
392                // should probably do this inside the code rather than here, but here
393                // makes it easier to change later on.
394                script = script.replaceAll("\n", ",");
395
396                return script;
397        }
398
399        /**
400         * parses the output from matlab to grab the values for the output ports
401         * 
402         * @param outputString
403         * @throws IllegalActionException
404         */
405        private void parseOutput(String outputString) throws IllegalActionException {
406
407                // this is a special token put in to make it easy to find the results
408                // for ports
409                final String scriptDivider = "ans =\n\n----";
410
411                // process outputString
412                // windows matlab writes '\r' characters along with the '\n' character.
413                // remove them so the parsing script works properly.
414                outputString = outputString.replaceAll("\r", "");
415
416                // ensure indexof will work
417                if (outputString.indexOf(scriptDivider) < 0) {
418                        throw new IllegalActionException(
419                                        "Error parsing output: Matlab must not have fired");
420                }
421
422                // cut off the proceeding matlab hello message
423                String outs = outputString.substring(outputString
424                                .indexOf(scriptDivider)
425                                + scriptDivider.length(), outputString.length());
426
427                String outputSendString = "";
428
429                // send only the user entered results to the output port
430                if (outs.indexOf(scriptDivider) >= 0) {
431                        outputSendString = outs.substring(0, outs.indexOf(scriptDivider));
432                }
433                output.send(0, new StringToken(outputSendString));
434
435                // get all the results which are to be sent as tokens out of a specified
436                // port
437                String results = outs.substring(outs.indexOf(scriptDivider)
438                                + scriptDivider.length(), outs.length());
439
440                // string tokenizer doesn't seem to beable to use '\n's as seperators
441                // so to make tokenization easy, replace the combinations of '\n's with
442                // a unique character.
443                results = results.replaceAll("\n\n\n", "*");
444                results = results.replaceAll("\n\n", "");
445
446                // break the string up into seperate sections for each port result
447                StringTokenizer st = new StringTokenizer(results, "*");
448                while (st.hasMoreTokens()) {
449
450                        // break each result up into tokens to make port name and value easy
451                        // to extract
452                        String ssst = st.nextToken();
453
454                        StringTokenizer ist = new StringTokenizer(ssst);
455                        if (ist.countTokens() > 2) {
456                                String portName = ist.nextToken();
457                                String fss = ist.nextToken();
458                                if (fss.equals("Undefined") || !fss.equals("=")) {
459                                        // port is undefined, or something else has gone wrong
460                                        // TODO: should a nil token be passed?
461                                        // System.out.println("2nd token is \"" + fss + "\"");
462                                } else {
463                                        // we are good to continue
464                                        String[] value = new String[ist.countTokens()];
465                                        int count = 0;
466                                        while (ist.hasMoreTokens()) {
467                                                value[count++] = ist.nextToken();
468                                        }
469
470                                        // set the value of the output port
471                                        setOutputToken(portName, value);
472                                }
473                        }
474                }
475        }
476
477        /**
478         * recursive function to build a value for assignment to a matlab variable
479         * from an input token.
480         * 
481         * recursively dives into arraytokens, surrounding each array with [ ]
482         * brackets to conform with matlab array inputs
483         * 
484         * TODO: extend for more input types, e.g. MatrixTokens
485         * 
486         * @param token
487         *       * @throws IllegalActionException
488         */
489        private String getTokenValue(Token[] token) throws IllegalActionException {
490
491                String returnval = "";
492
493                for (int i = 0; i < token.length; i++) {
494                        if (token[i].getType()
495                                        .isCompatible(new ArrayType(BaseType.UNKNOWN))) {
496                                returnval += " [ "
497                                                + getTokenValue(((ArrayToken) token[i]).arrayValue())
498                                                + " ] ";
499                        } else if (token[i].getType().equals(BaseType.STRING)) {
500                                returnval += " '" + ((StringToken) token[i]).stringValue()
501                                                + "' ";
502                        } else if (token[i].getType().equals(BaseType.INT)) {
503                                returnval += " " + ((IntToken) token[i]).intValue() + " ";
504                        } else if (token[i].getType().equals(BaseType.DOUBLE)) {
505                                returnval += " " + ((DoubleToken) token[i]).doubleValue() + " ";
506                        } else if (token[i].getType().equals(BaseType.BOOLEAN)) {
507                                returnval += " " + ((BooleanToken) token[i]).toString() + " ";
508                        } else if (token[i].getType().equals(BaseType.LONG)) {
509                                returnval += " " + ((LongToken) token[i]).longValue() + " ";
510                        } else if (token[i].getType().equals(BaseType.UNSIGNED_BYTE)) {
511                                returnval += " " + ((UnsignedByteToken) token[i]).byteValue()
512                                                + " ";
513                        } else {
514                                throw new IllegalActionException("invalid token type: "
515                                                + token[i].getType().toString());
516                        }
517                }
518
519                return returnval;
520        }
521
522        /**
523         * sends a value out a specified port.
524         * 
525         * tries to figure out what data type the value is.
526         * 
527         * 
528         * @param portName
529         * @param value
530         * @throws IllegalActionException
531         */
532        private void setOutputToken(String portName, String[] value)
533                        throws IllegalActionException {
534                List opList = outputPortList();
535                Iterator opListIt = opList.iterator();
536
537                // make sure the portName isn't output
538                if (portName.equals(output.getName())) {
539                        throw new IllegalActionException(
540                                        "sending a custom token out of port " + output.getName()
541                                                        + " is bad!");
542                }
543
544                // iterate through the list of ports
545                while (opListIt.hasNext()) {
546                        TypedIOPort tiop = (TypedIOPort) opListIt.next();
547                        String thisPortName = tiop.getName();
548                        // check if the name is the same as the one we want to set
549                        if (thisPortName.equals(portName)) {
550
551                                // check the type of the array
552                                // type 2 = string > 1 = double > 0 = int > -1 = no value
553                                int type = -1;
554                                for (int i = 0; i < value.length; i++) {
555                                        try {
556                                                if (value[i].indexOf(".") > -1) {
557                                                        // check if it can be converted to a double
558                                                        Double.valueOf(value[i]);
559                                                        // if the current type is an int then we can change
560                                                        // the whole
561                                                        // array type to double, but if the current type is
562                                                        // a string
563                                                        // then we have to keep it as a string
564                                                        type = type > 1 ? type : 1;
565                                                } else {
566                                                        // check if it can be converted to an int
567                                                        Integer.valueOf(value[i]);
568                                                        type = type > 0 ? type : 0;
569                                                }
570                                        } catch (NumberFormatException e) {
571                                                // it has to be a string
572                                                type = type > 2 ? type : 2;
573                                        }
574                                }
575
576                                // build the array using the specified type
577                                Token[] token;
578                                if (type == 2) {
579                                        token = new StringToken[value.length];
580                                        for (int i = 0; i < token.length; i++) {
581                                                token[i] = new StringToken(value[i]);
582                                        }
583                                        if (value.length > 1) {
584                                                tiop.setTypeEquals(new ArrayType(BaseType.STRING));
585                                        } else {
586                                                tiop.setTypeEquals(BaseType.STRING);
587                                        }
588                                } else if (type == 1) {
589                                        token = new DoubleToken[value.length];
590                                        for (int i = 0; i < token.length; i++) {
591                                                token[i] = new DoubleToken(Double.valueOf(value[i])
592                                                                .doubleValue());
593                                        }
594                                        if (value.length > 1) {
595                                                tiop.setTypeEquals(new ArrayType(BaseType.DOUBLE));
596                                        } else {
597                                                tiop.setTypeEquals(BaseType.DOUBLE);
598                                        }
599                                } else if (type == 0) {
600                                        token = new IntToken[value.length];
601                                        for (int i = 0; i < token.length; i++) {
602                                                token[i] = new IntToken(Integer.valueOf(value[i])
603                                                                .intValue());
604                                        }
605                                        if (value.length > 1) {
606                                                tiop.setTypeEquals(new ArrayType(BaseType.INT));
607                                        } else {
608                                                tiop.setTypeEquals(BaseType.INT);
609                                        }
610                                } else {
611                                        // throw an error if something went wrong
612                                        throw new IllegalActionException(
613                                                        "invalid value passed for token");
614                                }
615
616                                // send the array over the output port
617                                if (token.length > 1) {
618                                        tiop.send(0, new ArrayToken(token));
619                                } else {
620                                        tiop.send(0, token, token.length);
621                                }
622
623                                break;
624                        }
625                }
626        }
627
628        // /////////////////////////////////////////////////////////////////
629        // // private variables ////
630
631        private final static String tempFilename = "matlab_results";
632        private Process _p;
633}