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}