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}