001/* An actor that reads 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-09-18 18:39:51 +0000 (Tue, 18 Sep 2012) $' 
008 * '$Revision: 30701 $'
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.HashMap;
034import java.util.HashSet;
035import java.util.LinkedList;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042import ptolemy.actor.TypedIOPort;
043import ptolemy.actor.lib.LimitedFiringSource;
044import ptolemy.actor.parameters.FilePortParameter;
045import ptolemy.actor.parameters.PortParameter;
046import ptolemy.data.ArrayToken;
047import ptolemy.data.BooleanToken;
048import ptolemy.data.DoubleMatrixToken;
049import ptolemy.data.DoubleToken;
050import ptolemy.data.FloatToken;
051import ptolemy.data.IntMatrixToken;
052import ptolemy.data.IntToken;
053import ptolemy.data.LongToken;
054import ptolemy.data.ShortToken;
055import ptolemy.data.StringToken;
056import ptolemy.data.Token;
057import ptolemy.data.type.ArrayType;
058import ptolemy.data.type.BaseType;
059import ptolemy.data.type.Type;
060import ptolemy.kernel.CompositeEntity;
061import ptolemy.kernel.util.Attribute;
062import ptolemy.kernel.util.IllegalActionException;
063import ptolemy.kernel.util.NameDuplicationException;
064import ptolemy.kernel.util.Workspace;
065import ptolemy.math.DoubleMatrixMath;
066import ptolemy.math.IntegerMatrixMath;
067import ucar.ma2.Array;
068import ucar.ma2.DataType;
069import ucar.ma2.InvalidRangeException;
070import ucar.ma2.Range;
071import ucar.ma2.Section;
072import ucar.nc2.NetcdfFile;
073import ucar.nc2.Variable;
074
075/**
076 * This actor reads values from a NetCDF file. The <i>constraint</i> parameter
077 * specifies the variables to read and optionally how to subset them. For each
078 * variable, an output port with the same name is created. The type of the
079 * output port depends on how many dimensions are left unconstrained in the
080 * variable: scalar tokens for zero, array tokens for one, and matrix tokens for
081 * two. Unconstrained dimensions of greater than two are not supported.
082 * <p>
083 * The syntax for the <i>constraint</i> parameter is a space-separated list of
084 * variables. Each variable may optionally have a set of dimensional constraints
085 * in the form of [start:end:stride], where start is the starting index, end is
086 * the ending index, and stride is the increment. A dimension may be left
087 * unconstrained by specifying [:]. For example, suppose the variable is a
088 * two-dimensional matrix z[x,y]. To read the entire matrix, use z. To read all
089 * the values where y = 3, use z[:][3].
090 * <p>
091 * 
092 * @author Daniel Crawl
093 * @version $Id: NetCDFReader.java 30701 2012-09-18 18:39:51Z crawl $
094 * 
095 *          TODO
096 * 
097 *          test reading hdf5, other types
098 * 
099 */
100public class NetCDFReader extends LimitedFiringSource {
101
102    public NetCDFReader(CompositeEntity container, String name)
103            throws NameDuplicationException, IllegalActionException {
104        super(container, name);
105        
106        filename = new FilePortParameter(this, "filename");
107        
108        constraint = new PortParameter(this, "constraint");
109        constraint.setStringMode(true);
110        constraint.getPort().setTypeEquals(BaseType.STRING);
111   
112        // hide output port name
113        new Attribute(output, "_hide");
114    }
115    
116    /** React to a change in an attribute. */
117    public void attributeChanged(Attribute attribute) throws IllegalActionException
118    {
119        if(attribute == filename)
120        {
121            _updateFileName();
122            _updateOutputPorts();
123        }
124        else if(attribute == constraint)
125        {
126            _parseConstraintExpression();
127            _updateOutputPorts();
128        }
129        else
130        {
131            super.attributeChanged(attribute);
132        }
133    }
134    
135    /** Clone this actor into the specified workspace.
136     *  @param workspace The workspace for the cloned object.
137     *  @exception CloneNotSupportedException If cloned ports cannot have
138     *   as their container the cloned entity (this should not occur), or
139     *   if one of the attributes cannot be cloned.
140     *  @return A new NetCDFReader.
141     */
142    public Object clone(Workspace workspace) throws CloneNotSupportedException
143    {
144        NetCDFReader newObject = (NetCDFReader) super.clone(workspace);
145        newObject._constraint = null;
146        newObject._constraintMap = new HashMap<String,Section>();
147        newObject._filenameStr = null;
148        newObject._ncFile = null;
149        return newObject;
150    }
151    
152    public void fire() throws IllegalActionException
153    {
154        // read tokens in port parameters
155        constraint.update();
156        filename.update();
157                
158        // output the variables for each connected output port.
159        for(Object object : outputPortList())
160        {
161            TypedIOPort port = (TypedIOPort)object;
162            if(port.numberOfSinks() > 0)
163            {
164                _outputData(port);
165            }
166        }
167    }
168      
169    public void initialize() throws IllegalActionException
170    {
171        super.initialize();
172        _amFiring = true;
173        
174        if(_ncFile == null) {
175            _openFile();
176        }
177    }
178    
179    /** Close the NetCDF file. */
180    public void wrapup() throws IllegalActionException
181    {
182        _amFiring = false;
183        _closeFile();
184        super.wrapup();        
185    }
186
187    ///////////////////////////////////////////////////////////////////
188    ////                         public fields                     ////
189
190    /** The name of the NetCDF file to read. */
191    public FilePortParameter filename;
192    
193    /** Space-separated list of variables with an optional set of
194     *  constraints. Each dimension may be constrained using the
195     *  syntax [start:end:stride], or use [:] for no constraint.
196     */
197    public PortParameter constraint;
198    
199    ///////////////////////////////////////////////////////////////////
200    ////                         private methods                   ////
201
202    /** Close the NetCDF file. */
203    private void _closeFile() throws IllegalActionException
204    {
205        if(_ncFile != null) {
206            try {
207                _ncFile.close();
208                _ncFile = null;
209            } catch (IOException e) {
210                throw new IllegalActionException(this, e, "Error closing " + _filenameStr);
211            }
212        }
213        
214    }
215        
216    /** Open the NetCDF file. */
217    private void _openFile() throws IllegalActionException
218    {     
219        if(_filenameStr != null && !_filenameStr.isEmpty())
220        {
221            try {
222                _ncFile = NetcdfFile.open(_filenameStr);
223            } catch (IOException e) {
224                throw new IllegalActionException(this, e, "Error opening " + _filenameStr);
225            }
226        }
227    }
228
229    /** Read a token from the file name port parameter. */
230    private void _updateFileName() throws IllegalActionException
231    {
232        Token token = filename.getToken();
233        if(token != null)
234        {
235            String fileStr = ((StringToken)token).stringValue();
236            if(_filenameStr == null || !_filenameStr.equals(fileStr))
237            {
238                _filenameStr = fileStr;
239                
240                // if the old file is open, close and open the new one
241                if(_ncFile != null) {
242                    _closeFile();
243                }
244                
245                if(_ncFile == null) {
246                    _openFile();
247                }
248            }
249        }
250    }
251    
252    private void _updateOutputPorts() throws IllegalActionException
253    {
254        if(!_amFiring && _filenameStr != null && !_filenameStr.isEmpty() && _constraint != null)
255        {
256        
257            if(_ncFile == null) {
258                _openFile();
259            }
260
261            Set<String> variableNames = new HashSet<String>();
262            List<Variable> variables = null;
263                        
264            // see if there are constraints
265            if(_constraintMap.size() > 0)
266            {
267                // add all the variables in the constraints
268                variableNames.addAll(_constraintMap.keySet());
269                variables = new LinkedList<Variable>();
270                for(String name : variableNames)
271                {
272                    Variable variable = _ncFile.findVariable(name);
273                    if(variable == null)
274                    {
275                        throw new IllegalActionException(this, "Variable " + name + " is in constraint " +
276                            "expression, but not found in file " + _filenameStr);
277                    }
278                    variables.add(variable);
279                }
280            }
281            else
282            {
283                // add all the variables in the file
284                variables = _ncFile.getVariables();
285                for(Variable variable : variables)
286                {
287                    variableNames.add(variable.getFullName());                
288                }
289            }
290        
291            // add ports and set types
292            for(Variable variable : variables)
293            {
294                String name = variable.getFullName();
295
296                // see if we need to add the port
297                TypedIOPort port = (TypedIOPort) getPort(name);
298                if(port == null)
299                {
300                    try
301                    {
302                        port = new TypedIOPort(this, name, false, true);
303                    }
304                    catch (NameDuplicationException e)
305                    {
306                        throw new IllegalActionException(this, e, "Error creating port " + name);
307                    }
308                }
309                // see if variable is named "output". we already have port called output,
310                // so unhide it if it is hidden.
311                else if(name.equals("output"))
312                {
313                    Attribute attribute = port.getAttribute("_hide");
314                    if(attribute != null)
315                    {
316                        try
317                        {
318                            attribute.setContainer(null);
319                        }
320                        catch (NameDuplicationException e)
321                        {
322                            throw new IllegalActionException(this, e, "Unable to show output port.");
323                        }
324                    }
325                }
326
327                Type oldType = port.getType();
328
329                // set the port type based on the netcdf type and decimation
330                Type newType = _getTokenTypeForVariable(variable);
331
332                if(!oldType.equals(newType))
333                {
334                    port.setTypeEquals(newType);
335                    //System.out.println("setting type for " + name);
336                }
337            }
338            
339            // delete ports for non-existing variables
340            for(Object obj : outputPortList())
341            {
342                TypedIOPort port = (TypedIOPort) obj;
343                String portName = port.getName();
344                if(!variableNames.contains(portName))
345                {
346                    // can't delete output port since belongs to parent class
347                    if(portName.equals("output"))
348                    {
349                        if(port.getAttribute("_hide") == null)
350                        {
351                            // hide output port
352                            try
353                            {
354                                new Attribute(port, "_hide");
355                            }
356                            catch (NameDuplicationException e)
357                            {
358                                throw new IllegalActionException(this, e, "Unable to hide output port.");
359                            }
360                        }
361                    }
362                    else
363                    {
364                        // remove port
365                        try
366                        {
367                            port.setContainer(null);
368                        }
369                        catch (NameDuplicationException e)
370                        {
371                            throw new IllegalActionException(this, e, "Error deleting " + port.getName());
372                        }
373                    }
374                }
375            }
376        }
377    }
378    
379    /** Get the number of dimensions of a variable after decimating. */
380    private int _getDimensionsRemaining(Variable variable)
381    {
382        int decimationAmount = 0;
383        
384        // see if this variable was decimated in the constraint expression
385        Section section = _constraintMap.get(variable.getFullName());
386        
387        if(section != null)
388        {
389            for(Range range : section.getRanges())
390            {
391                // NOTE: range can be null if decimation specified as [:]
392                if(range != null && range.length() == 1)
393                {
394                    decimationAmount++;
395                }
396            }
397        }
398        
399        return variable.getShape().length - decimationAmount;
400    }
401    
402    private Type _getTokenTypeForVariable(Variable variable) throws IllegalActionException
403    {
404        
405        Type retval = null;
406        
407        if(variable.isMetadata())
408        {
409            throw new IllegalActionException(this, "variable " + variable.getFullName() + " is metadata.");
410        }
411        
412        String name = variable.getFullName();
413                
414        DataType dataType = variable.getDataType();
415        
416        int dimensionsRemaining = _getDimensionsRemaining(variable);
417        
418        //System.out.println("dim rem for " + name + " is " + dimensionsRemaining);
419        
420        if(dimensionsRemaining < 0)
421        {
422            throw new IllegalActionException(this, "Variable " + name + " has " +
423                variable.getShape().length + " dimension(s), but more " +
424                " dimensions have been constrained.");
425        }
426        else if(dimensionsRemaining < 2)
427        {            
428            switch(dataType)
429            {
430            case DOUBLE:
431                retval = BaseType.DOUBLE;
432                break;
433            case FLOAT:
434                retval = BaseType.FLOAT;
435                break;
436            case SHORT:
437                retval = BaseType.SHORT;
438                break;
439            case INT:
440                retval = BaseType.INT;
441                break;
442            case LONG:
443                retval = BaseType.LONG;
444                break;
445            case BOOLEAN:
446                retval = BaseType.BOOLEAN;
447                break;
448            default:
449                throw new IllegalActionException(this, "Variable " + name +
450                        "has unsupported data type: " + dataType);
451            }
452            
453            if(dimensionsRemaining == 1)
454            {
455                retval = new ArrayType(retval); 
456            }
457        }
458        else if(dimensionsRemaining == 2)
459        {
460            switch(dataType)
461            {
462            case DOUBLE:
463            case FLOAT:
464                retval = BaseType.DOUBLE_MATRIX;
465                break;
466            case INT:
467                retval = BaseType.INT_MATRIX;
468                break;
469            case LONG:
470                retval = BaseType.LONG_MATRIX;
471                break;
472            case BOOLEAN:
473                retval = BaseType.BOOLEAN_MATRIX;
474                break;
475            default:
476                throw new IllegalActionException(this, "Unsupported matrix " +
477                    "type for variable " + name + "(" + dataType + ")");
478            }
479        }
480        else if(dimensionsRemaining > 2)
481        {
482            throw new IllegalActionException(this, "Variable " + name + " has" +
483                " been decimated to have more than two dimensions, which is" +
484                " currently not supported.");
485        }
486        
487        return retval;
488    }
489    
490    /** Write the data from the file to an output port. */
491    private void _outputData(TypedIOPort port) throws IllegalActionException
492    {
493        Token token = null;
494
495        String name = port.getName();
496        Variable variable = _ncFile.findVariable(name);
497        if(variable == null)
498        {
499            throw new IllegalActionException("Could not find variable " + name + " in file " + _filenameStr);
500        }
501        
502        int dimensionsRemaing = _getDimensionsRemaining(variable);
503
504        Array array;
505        try {
506            Section section = _constraintMap.get(name);
507            if(section != null) {
508                array = variable.read(section);
509            } else {
510                array = variable.read(null, variable.getShape());
511            }
512        } catch (Exception e) {
513            throw new IllegalActionException(this, e, "Unable to read variable " + name);
514        }
515
516        // get the shape from the read array
517        int[] readShape = array.getShape();
518
519        Object arrayStorage = array.getStorage();
520
521        DataType dataType = variable.getDataType();
522
523        if(dimensionsRemaing == 0)
524        {
525           switch(dataType)
526           {
527           case DOUBLE:
528               token = new DoubleToken(((double[])arrayStorage)[0]);
529               break;
530           case FLOAT:
531               token = new FloatToken(((float[])arrayStorage)[0]);
532               break;
533           case SHORT:
534               token = new ShortToken(((short[])arrayStorage)[0]);
535               break;
536           case INT:
537               token = new IntToken(((int[])arrayStorage)[0]);
538               break;
539           case LONG:
540               token = new LongToken(((long[])arrayStorage)[0]);
541               break;
542           case BOOLEAN:
543               token = new BooleanToken(((boolean[])arrayStorage)[0]);
544               break;
545           default:
546               throw new IllegalActionException(this, "Variable " + name +
547                   " has unsupported data type: " + dataType);
548           }
549        }
550        else if(dimensionsRemaing == 1)
551        {
552            int length = java.lang.reflect.Array.getLength(arrayStorage);
553            StringBuilder arrayStr = new StringBuilder("{");
554            for(int i = 0; i < length - 1; i++)
555            {
556                arrayStr.append(java.lang.reflect.Array.get(arrayStorage, i));
557                arrayStr.append(",");
558            }
559            arrayStr.append(java.lang.reflect.Array.get(arrayStorage, length - 1));
560            arrayStr.append("}");
561            token = new ArrayToken(arrayStr.toString());
562        }
563        else // dimensionsRemaing == 2
564        {
565            switch(dataType)
566            {
567            case DOUBLE:
568                
569                /*
570                double[][] data = new double[shape[0]][shape[1]];
571                for(int i = 0; i < shape[0]; i++)
572                {
573                    for(int j = 0; j < shape[1]; j++)
574                    {
575                        data[i][j] = arrayDouble.get(i,j);
576                    }
577                }
578                */
579                
580                token = new DoubleMatrixToken((double[])array.getStorage(),
581                        readShape[0], readShape[1]);
582
583                // XXX take the transpose to get elements in correct position
584                token = new DoubleMatrixToken(
585                        DoubleMatrixMath.transpose(((DoubleMatrixToken)token).doubleMatrix()));
586                
587                
588                //System.out.println(getName() + ": array 5000,73 = " + arrayDouble.get(5000, 73));
589                //System.out.println("rows = " + ((DoubleMatrixToken)token).getRowCount());
590                //System.out.println("cols = " + ((DoubleMatrixToken)token).getColumnCount());
591                //System.out.println(getName() + ": token 5000,73 = " + 
592                       //((DoubleMatrixToken)token).getElementAt(73, 5000));
593
594                
595                break;
596                
597            case INT:
598                                                
599                token = new IntMatrixToken((int[])array.getStorage(),
600                        readShape[0], readShape[1]);
601
602                // XXX take the transpose to get elements in correct position
603                token = new IntMatrixToken(
604                        IntegerMatrixMath.transpose(((IntMatrixToken)token).intMatrix()));
605                
606                break;
607                
608             default:
609                 throw new IllegalActionException(this, "Variable " + name +
610                         " has unsupported data type: " + dataType);
611            }            
612        }
613        
614        if(token != null)
615        {
616            //System.out.println(getFullName() + " output token type " + token.getType());
617            port.broadcast(token);
618        }           
619    }
620    
621    private void _parseConstraintExpression() throws IllegalActionException
622    {
623        String origConstraintStr = ((StringToken)constraint.getToken()).stringValue();
624        // see if constraint expression has changed
625        if(_constraint == null || !_constraint.equals(origConstraintStr))
626        {
627            _constraint = origConstraintStr.trim();
628            _constraintMap.clear();
629            
630            String constraintStr = _constraint;
631            while(constraintStr.length() > 0)
632            {
633                Matcher matcher = _VARIABLE_PATTERN.matcher(constraintStr);
634                if(!matcher.matches())
635                {
636                    throw new IllegalActionException(this, "Bad constraint: " + origConstraintStr);
637                }
638                String variableName = matcher.group(1);
639                
640                Section section = null;
641                String sectionStr = "";
642                if(matcher.groupCount() > 1)
643                {
644                    sectionStr = matcher.group(2);
645                    String formattedSectionStr = 
646                        sectionStr.replaceAll("\\[", "").replaceAll("\\]", ",").replaceAll(",$", "");
647                    
648                    try {
649                        section = new Section(formattedSectionStr);
650                    } catch (InvalidRangeException e) {
651                        throw new IllegalActionException(this, e, "Invalid decimation " + sectionStr);
652                    }
653                }
654                
655                _constraintMap.put(variableName, section);
656                
657                constraintStr = constraintStr.substring(variableName.length() + sectionStr.length());
658            }
659        }
660    }
661        
662    ///////////////////////////////////////////////////////////////////
663    ////                         private fields                    ////
664
665    /** Variable pattern: variable name optionally followed by dimension(s). */
666    private static final Pattern _VARIABLE_PATTERN =
667        Pattern.compile("(\\w+)([\\d\\:\\[\\]]*).*");
668        
669    /** The name of the NetCDF file. */
670    private String _filenameStr;
671    
672    /** The constraint expression. */
673    private String _constraint;
674    
675    /** A map of variable name to dimension decimation. */
676    private Map<String,Section> _constraintMap = new HashMap<String,Section>();
677    
678    /** NetCDF file object. */
679    private NetcdfFile _ncFile;
680    
681    /** If true, workflow is executing. */
682    private boolean _amFiring;
683}