001/* An actor that writes NetCDF files. 002 * 003 * Copyright (c) 2011 The Regents of the University of California. 004 * All rights reserved. 005 * 006 * '$Author: crawl $' 007 * '$Date: 2012-11-26 22:19:36 +0000 (Mon, 26 Nov 2012) $' 008 * '$Revision: 31113 $' 009 * 010 * Permission is hereby granted, without written agreement and without 011 * license or royalty fees, to use, copy, modify, and distribute this 012 * software and its documentation for any purpose, provided that the above 013 * copyright notice and the following two paragraphs appear in all copies 014 * of this software. 015 * 016 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 017 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 018 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 019 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 020 * SUCH DAMAGE. 021 * 022 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 023 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 024 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 025 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 026 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 027 * ENHANCEMENTS, OR MODIFICATIONS. 028 * 029 */ 030package org.kepler.data.netcdf; 031 032import java.io.IOException; 033import java.util.ArrayList; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.LinkedHashMap; 037import java.util.LinkedList; 038import java.util.List; 039import java.util.Map; 040import java.util.Set; 041import java.util.regex.Matcher; 042import java.util.regex.Pattern; 043 044import ptolemy.actor.IOPort; 045import ptolemy.actor.TypedAtomicActor; 046import ptolemy.actor.TypedIOPort; 047import ptolemy.data.BooleanToken; 048import ptolemy.data.DoubleToken; 049import ptolemy.data.FloatToken; 050import ptolemy.data.IntToken; 051import ptolemy.data.LongToken; 052import ptolemy.data.ShortToken; 053import ptolemy.data.StringToken; 054import ptolemy.data.Token; 055import ptolemy.data.expr.FileParameter; 056import ptolemy.data.expr.Parameter; 057import ptolemy.data.expr.StringParameter; 058import ptolemy.data.type.BaseType; 059import ptolemy.data.type.Type; 060import ptolemy.kernel.CompositeEntity; 061import ptolemy.kernel.Port; 062import ptolemy.kernel.util.Attribute; 063import ptolemy.kernel.util.IllegalActionException; 064import ptolemy.kernel.util.NameDuplicationException; 065import ptolemy.kernel.util.Workspace; 066import ucar.ma2.Array; 067import ucar.ma2.ArrayBoolean; 068import ucar.ma2.ArrayDouble; 069import ucar.ma2.ArrayFloat; 070import ucar.ma2.ArrayInt; 071import ucar.ma2.ArrayLong; 072import ucar.ma2.ArrayShort; 073import ucar.ma2.DataType; 074import ucar.ma2.Index; 075import ucar.nc2.Dimension; 076import ucar.nc2.NetcdfFileWriteable; 077 078/** This actor writes a single variable in a new NetCDF file. There 079 * are input ports for each dimension and the variable. For example, 080 * if the variable is z[x,y], then there are input ports called x, 081 * y, and z. The input ports are automatically created when the 082 * <i>variable</i> and <i>dimensions</i> parameters are set. 083 * <p> 084 * The actor reads a token on each input port every time it executes. 085 * A token read by a port for a dimension is used as the index, and 086 * a token read by a port for the variable is used as the value. For 087 * example, if the variable is z[x,y], and the values read by ports 088 * x, y, and z, are 1, 2, and 10, respectively, then the value 089 * written to z[1,2] = 10. 090 * <p> 091 * 092 * @author Daniel Crawl 093 * @version $Id: NetCDFWriter.java 31113 2012-11-26 22:19:36Z crawl $ 094 * 095 * TODO 096 * 097 * deletes/recreates variable port on wf load 098 * add parameter to overwrite file 099 * write each element to disk instead of keeping in memory until wrapup 100 */ 101 102public class NetCDFWriter extends TypedAtomicActor 103{ 104 /** Construct a new NetCDFWriter in a container with a name. */ 105 public NetCDFWriter(CompositeEntity container, String name) 106 throws IllegalActionException, NameDuplicationException 107 { 108 super(container, name); 109 110 filename = new FileParameter(this, "filename"); 111 _filenameStr = ""; 112 113 variable = new StringParameter(this, "inputVariable"); 114 dimensions = new StringParameter(this, "dimensions"); 115 116 writeOnFinish = new Parameter(this, "writeOnFinish"); 117 writeOnFinish.setTypeEquals(BaseType.BOOLEAN); 118 writeOnFinish.setToken(BooleanToken.FALSE); 119 } 120 121 /** React to a change in an attribute. */ 122 public void attributeChanged(Attribute attribute) throws IllegalActionException 123 { 124 if(attribute == filename) 125 { 126 Token token = filename.getToken(); 127 if(token != null) 128 { 129 _filenameStr = ((StringToken)token).stringValue(); 130 } 131 } 132 else if(attribute == variable) 133 { 134 String varStr = variable.stringValue(); 135 if(!varStr.isEmpty() && (_variableName == null || !_variableName.equals(varStr))) 136 { 137 // add the port if not already there 138 Port port = getPort(varStr); 139 if(port == null) 140 { 141 try 142 { 143 port = new TypedIOPort(this, varStr, true, false); 144 new Attribute(port, "_showName"); 145 } 146 catch (NameDuplicationException e) 147 { 148 throw new IllegalActionException(this, e, "Error adding port " + varStr); 149 } 150 } 151 _variableName = varStr; 152 _removeOldInputPorts(); 153 } 154 } 155 else if(attribute == dimensions) 156 { 157 String dimStr = dimensions.stringValue(); 158 if(!dimStr.isEmpty() && (_dimensions == null || !_dimensions.equals(dimStr))) 159 { 160 _dimensions = dimStr; 161 _parseDimensions(); 162 _removeOldInputPorts(); 163 } 164 } 165 else if(attribute == writeOnFinish) 166 { 167 boolean val = ((BooleanToken)writeOnFinish.getToken()).booleanValue(); 168 if(val != _writeOnFinish) 169 { 170 _writeOnFinish = val; 171 // if turned off, reparse dimensions to make sure each dimension has a length. 172 if(!_writeOnFinish && _dimensions != null) 173 { 174 _parseDimensions(); 175 } 176 } 177 } 178 else 179 { 180 super.attributeChanged(attribute); 181 } 182 } 183 184 /** Clone this actor into the specified workspace. 185 * @param workspace The workspace for the cloned object. 186 * @exception CloneNotSupportedException If cloned ports cannot have 187 * as their container the cloned entity (this should not occur), or 188 * if one of the attributes cannot be cloned. 189 * @return A new NetCDFWriter. 190 */ 191 public Object clone(Workspace workspace) throws CloneNotSupportedException 192 { 193 NetCDFWriter newObject = (NetCDFWriter) super.clone(workspace); 194 newObject._array = null; 195 newObject._datatype = null; 196 newObject._dimensionMap = new HashMap<String,Integer>(); 197 newObject._dimensions = null; 198 newObject._filenameStr = null; 199 newObject._maxValue = new HashMap<String,Integer>(); 200 newObject._ncFile = null; 201 newObject._savedIndexes = new LinkedList<Map<String,Integer>>(); 202 newObject._savedValues = new LinkedList<Token>(); 203 newObject._variableName = null; 204 newObject._writeOnFinish = false; 205 return newObject; 206 } 207 208 /** Read the value and indexes, and update the array. */ 209 public void fire() throws IllegalActionException 210 { 211 // read value and add to array 212 final IOPort valuePort = (IOPort) getPort(_variableName); 213 final Token valueToken = valuePort.get(0); 214 215 if(_writeOnFinish) 216 { 217 final Map<String,Integer> map = new HashMap<String,Integer>(); 218 for(String dimName : _dimensionMap.keySet()) 219 { 220 final IOPort port = (IOPort) getPort(dimName); 221 final int val = ((IntToken)port.get(0)).intValue(); 222 map.put(dimName, val); 223 224 // see if we need to update max values. 225 final Integer max = _maxValue.get(dimName); 226 if(max == null || max < val) 227 { 228 _maxValue.put(dimName, val); 229 } 230 } 231 232 _savedValues.add(valueToken); 233 _savedIndexes.add(map); 234 235 } 236 else 237 { 238 239 // read dimension indexes 240 final Index index = _array.getIndex(); 241 int i = 0; 242 for(Map.Entry<String, Integer> entry : _dimensionMap.entrySet()) 243 { 244 final String dimName = entry.getKey(); 245 final int length = entry.getValue(); 246 247 final IOPort port = (IOPort) getPort(dimName); 248 final int val = ((IntToken)port.get(0)).intValue(); 249 if(val >= length) 250 { 251 throw new IllegalActionException(this, "Invalid value " + 252 val + " for dimension " + dimName + 253 " whose length is " + length); 254 } 255 256 index.setDim(i, val); 257 i++; 258 } 259 260 _writeOneValue(valueToken, index); 261 } 262 263 } 264 265 /** Create the NetCDF file, add the header information, and initialize the array. */ 266 public void initialize() throws IllegalActionException 267 { 268 super.initialize(); 269 270 _datatype = null; 271 272 _maxValue.clear(); 273 _savedValues.clear(); 274 _savedIndexes.clear(); 275 276 // sanity checks 277 278 if(_variableName.isEmpty()) 279 { 280 throw new IllegalActionException(this, "No variable specified."); 281 } 282 283 if(_dimensionMap.isEmpty()) 284 { 285 throw new IllegalActionException(this, "No dimensions specified."); 286 } 287 288 if(!_writeOnFinish) 289 { 290 _openFile(); 291 } 292 } 293 294 /** Write the array to the file and close it. */ 295 public void wrapup() throws IllegalActionException 296 { 297 298 if(_writeOnFinish) 299 { 300 _openFileAndWriteToArray(); 301 } 302 303 // write array to file. 304 if(_ncFile != null && _array != null) 305 { 306 try 307 { 308 _ncFile.write(_variableName, _array); 309 } 310 catch (Exception e) 311 { 312 throw new IllegalActionException(this, e, "Error writing array data to file."); 313 } 314 } 315 316 _closeFile(); 317 318 super.wrapup(); 319 } 320 321 /////////////////////////////////////////////////////////////////// 322 //// public fields //// 323 324 /** The name of the NetCDF file. */ 325 public FileParameter filename; 326 327 /** The name of the variable to write. */ 328 public StringParameter variable; 329 330 /** A space-separated list of dimensions and their length, e.g., x[10] y[4]. */ 331 public StringParameter dimensions; 332 333 /** If true, wait until the workflow is finished before writing data to 334 * the NetCDF file. Set this to true if the length of the dimensions 335 * are not known before the workflow starts. (A length number is still 336 * required for each dimension in the dimensions parameter, but the 337 * value is ignored). 338 */ 339 public Parameter writeOnFinish; 340 341 /////////////////////////////////////////////////////////////////// 342 //// private methods //// 343 344 /** Close the NetCDF file. */ 345 private void _closeFile() throws IllegalActionException 346 { 347 _array = null; 348 if(_ncFile != null) { 349 try { 350 _ncFile.close(); 351 } catch (IOException e) { 352 throw new IllegalActionException(this, e, "Error closing file."); 353 } 354 } 355 } 356 357 /** Close the NetCDF file ignoring any exception thrown. */ 358 private void _closeFileIgnoreException() 359 { 360 try { 361 _closeFile(); 362 } catch(Throwable t) { 363 System.err.println("Error closing file: " + t.getMessage()); 364 } 365 } 366 367 /** Open the NetCDF file and initialize the data array. */ 368 private void _openFile() throws IllegalActionException 369 { 370 try 371 { 372 _ncFile = NetcdfFileWriteable.createNew(_filenameStr); 373 374 // add the dimensions 375 376 List<Dimension> dimensions = new ArrayList<Dimension>(); 377 for(Map.Entry<String, Integer> entry : _dimensionMap.entrySet()) 378 { 379 dimensions.add(_ncFile.addDimension(entry.getKey(), entry.getValue())); 380 } 381 382 // add the variable 383 384 TypedIOPort port = (TypedIOPort) getPort(_variableName); 385 _datatype = _getNetCDFType(port.getType()); 386 _ncFile.addVariable(_variableName, _datatype, dimensions); 387 388 // create the file and leave define mode 389 _ncFile.create(); 390 391 // create the array 392 int[] dim = new int[_dimensionMap.size()]; 393 int i = 0; 394 for(Integer length : _dimensionMap.values()) 395 { 396 dim[i] = length; 397 i++; 398 } 399 400 if(_datatype == DataType.DOUBLE) { 401 _array = new ArrayDouble(dim); 402 } else if(_datatype == DataType.FLOAT) { 403 _array = new ArrayFloat(dim); 404 } else if(_datatype == DataType.SHORT) { 405 _array = new ArrayShort(dim); 406 } else if(_datatype == DataType.INT) { 407 _array = new ArrayInt(dim); 408 } else if(_datatype == DataType.LONG) { 409 _array = new ArrayLong(dim); 410 } else if(_datatype == DataType.BOOLEAN) { 411 _array = new ArrayBoolean(dim); 412 } 413 } 414 catch(IOException e) 415 { 416 _closeFileIgnoreException(); 417 throw new IllegalActionException(this, e, "Error creating netcdf file."); 418 } 419 } 420 421 /** Parse the dimensions parameter and change input ports accordingly. */ 422 private void _parseDimensions() throws IllegalActionException 423 { 424 // use LinkedHashMap for predictable iteration order 425 _dimensionMap.clear(); 426 427 String[] dimArray = _dimensions.split("\\s+"); 428 for(String dimStr : dimArray) 429 { 430 Matcher matcher = DIMENSION_PATTERN.matcher(dimStr); 431 432 if(!matcher.matches()) 433 { 434 throw new IllegalActionException(this, "Dimension not formatted correctly: " + dimStr); 435 } 436 437 String name = matcher.group(1); 438 int length = Integer.valueOf(matcher.group(2)); 439 440 // length must be >= 1 unless we write the data when finishing 441 if(length < 1 && !_writeOnFinish) 442 { 443 throw new IllegalActionException(this, "Dimension " + name + " must have length >= 1."); 444 } 445 446 _dimensionMap.put(name, length); 447 448 // add input port if not there 449 TypedIOPort port = (TypedIOPort) getPort(name); 450 if(port == null) 451 { 452 try { 453 port = new TypedIOPort(this, name, true, false); 454 new Attribute(port, "_showName"); 455 } catch(NameDuplicationException e) { 456 throw new IllegalActionException(this, e, "Error creating port for dimension " + name); 457 } 458 } 459 port.setTypeEquals(BaseType.INT); 460 461 } 462 } 463 464 /** Remove ports that are not named after the variable or one 465 * of the dimensions. 466 */ 467 private void _removeOldInputPorts() throws IllegalActionException 468 { 469 // remove output ports whose names are not dimensions 470 List<?> inputPorts = inputPortList(); 471 for(Object object : inputPorts) 472 { 473 TypedIOPort port = (TypedIOPort)object; 474 String name = port.getName(); 475 // make sure both variable and dimensions have been set before removing 476 if(_variableName != null && !name.equals(_variableName) && 477 !_dimensionMap.isEmpty() && !_dimensionMap.containsKey(name)) 478 { 479 try 480 { 481 port.setContainer(null); 482 } 483 catch (NameDuplicationException e) 484 { 485 throw new IllegalActionException(this, e, "Error removing " + name); 486 } 487 } 488 } 489 } 490 491 /** Get the corresponding NetCDF type for the Ptolemy type. */ 492 private DataType _getNetCDFType(Type type) throws IllegalActionException 493 { 494 if(type == BaseType.DOUBLE) { 495 return DataType.DOUBLE; 496 } else if(type == BaseType.FLOAT) { 497 return DataType.FLOAT; 498 } else if(type == BaseType.SHORT) { 499 return DataType.SHORT; 500 } else if(type == BaseType.INT) { 501 return DataType.INT; 502 } else if(type == BaseType.LONG) { 503 return DataType.LONG; 504 } else if(type == BaseType.BOOLEAN) { 505 return DataType.BOOLEAN; 506 } else { 507 throw new IllegalActionException(this, "Unsupported conversion to NetCDF type : " + type); 508 } 509 } 510 511 /** Write data collected during fire() to the array. */ 512 private void _openFileAndWriteToArray() throws IllegalActionException 513 { 514 515 // add length for each dimension 516 Set<String> dimensionNames = new HashSet<String>(_dimensionMap.keySet()); 517 for(String name : dimensionNames) 518 { 519 Integer max = _maxValue.get(name); 520 521 if(max == null) 522 { 523 throw new IllegalActionException("Could not find values for dimension " + name); 524 } 525 526 // add one since dimensions start at 1, not 0 527 _dimensionMap.put(name, max + 1); 528 } 529 530 // open the file for writing 531 _openFile(); 532 533 // write each value 534 while(!_savedValues.isEmpty()) 535 { 536 Token valueToken = _savedValues.remove(0); 537 Map<String,Integer> dimMap = _savedIndexes.remove(0); 538 539 final Index index = _array.getIndex(); 540 int i = 0; 541 542 for(String name : _dimensionMap.keySet()) 543 { 544 final Integer dimVal = dimMap.get(name); 545 546 if(dimVal == null) 547 { 548 throw new IllegalActionException("No value for dimension " + name); 549 } 550 551 index.setDim(i, dimVal); 552 i++; 553 } 554 _writeOneValue(valueToken, index); 555 } 556 } 557 558 /** Write a single value in a token to the array. */ 559 private void _writeOneValue(Token valueToken, Index index) 560 { 561 //System.out.println(getName() + " index " + index); 562 563 if(_datatype == DataType.DOUBLE) { 564 _array.setDouble(index, ((DoubleToken)valueToken).doubleValue()); 565 } else if(_datatype == DataType.FLOAT) { 566 _array.setFloat(index, ((FloatToken)valueToken).floatValue()); 567 } else if(_datatype == DataType.SHORT) { 568 _array.setShort(index, ((ShortToken)valueToken).shortValue()); 569 } else if(_datatype == DataType.INT) { 570 _array.setInt(index, ((IntToken)valueToken).intValue()); 571 } else if(_datatype == DataType.LONG) { 572 _array.setLong(index, ((LongToken)valueToken).longValue()); 573 } else if(_datatype == DataType.BOOLEAN) { 574 _array.setBoolean(index, ((BooleanToken)valueToken).booleanValue()); 575 } 576 } 577 578 /////////////////////////////////////////////////////////////////// 579 //// private fields //// 580 581 /** The name of the NetCDF file. */ 582 private String _filenameStr; 583 584 /** The name of the variable. */ 585 private String _variableName; 586 587 /** A space-separate list of dimensions and their lengths. */ 588 private String _dimensions; 589 590 /** A map of dimension name to its length. Use LinkedHashMap 591 * for predictable iteration order. 592 */ 593 private Map<String,Integer> _dimensionMap = new LinkedHashMap<String,Integer>(); 594 595 /** The NetCDF file object. */ 596 private NetcdfFileWriteable _ncFile; 597 598 /** The NetCDF type of the variable. */ 599 private DataType _datatype; 600 601 /** The array containing the values of the variable. */ 602 private Array _array; 603 604 /** Regular expression of a dimension. */ 605 private final static Pattern DIMENSION_PATTERN = Pattern.compile("(\\w+)\\[(\\d+)\\]"); 606 607 /** If true, write data to the NetCDF file when the workflow is finished. */ 608 private boolean _writeOnFinish; 609 610 /** The maximum value seen for each dimension. */ 611 private Map<String,Integer> _maxValue = new HashMap<String,Integer>(); 612 613 /** A list of values collected during fire(). */ 614 private List<Token> _savedValues = new LinkedList<Token>(); 615 616 /** A list of index names and values collected during fire(). */ 617 private List<Map<String,Integer>> _savedIndexes = new LinkedList<Map<String,Integer>>(); 618 619}