NetCDFFileExportMethod.java

/*
 * (C) Copyright 2015-2017 by MSDK Development Team
 *
 * This software is dual-licensed under either
 *
 * (a) the terms of the GNU Lesser General Public License version 2.1 as published by the Free
 * Software Foundation
 *
 * or (per the licensee's choosing)
 *
 * (b) the terms of the Eclipse Public License v1.0 as published by the Eclipse Foundation.
 */

package io.github.msdk.io.netcdf;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nonnull;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.github.msdk.MSDKException;
import io.github.msdk.MSDKMethod;
import io.github.msdk.datamodel.rawdata.MsScan;
import io.github.msdk.datamodel.rawdata.RawDataFile;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFileWriter;
import ucar.nc2.NetcdfFileWriter.Version;
import ucar.nc2.Variable;

/**
 * <p>
 * This class contains methods which can be used to write data contained in a
 * {@link o.github.msdk.datamodel.rawdata.RawDataFile RawDataFile} to a file, in netCDF-3 format
 * </p>
 *
 */
public class NetCDFFileExportMethod implements MSDKMethod<Void> {

  private final @Nonnull RawDataFile rawDataFile;
  private final @Nonnull File target;
  private final @Nonnull double massValueScaleFactor;
  private final @Nonnull double intensityValueScaleFactor;

  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  private boolean canceled = false;
  private int totalScans = 0;
  private float progress = 0f;
  private int lastLoggedProgress = 0;
  private List<MsScan> scans;
  private int[] scanStartPositions;
  private NetcdfFileWriter writer;

  /**
   * <p>
   * Constructor for NetCDFFileExportMethod.
   * </p>
   * 
   * @param rawDataFile the input {@link o.github.msdk.datamodel.rawdata.RawDataFile RawDataFile}
   *        which contains the data to be exported
   * @param target the target {@link java.io.File File} to write the data, in netCDF format
   */
  public NetCDFFileExportMethod(@Nonnull RawDataFile rawDataFile, @Nonnull File target) {
    this(rawDataFile, target, 1, 1);
  }

  /**
   * <p>
   * Constructor for NetCDFFileExportMethod.
   * </p>
   * 
   * @param rawDataFile the input {@link o.github.msdk.datamodel.rawdata.RawDataFile RawDataFile}
   *        which contains the data to be exported
   * @param target the target {@link java.io.File File} to write the data, in netCDF format
   * @param massValueScaleFactor double value by which the mass values have to be scaled by
   * @param intensityValueScaleFactor double value by which the intensity values have to be scaled
   *        by

   */
  public NetCDFFileExportMethod(@Nonnull RawDataFile rawDataFile, @Nonnull File target,
      double massValueScaleFactor, double intensityValueScaleFactor) {
    this.rawDataFile = rawDataFile;
    this.target = target;
    this.massValueScaleFactor = massValueScaleFactor;
    this.intensityValueScaleFactor = intensityValueScaleFactor;
  }

  /**
   * <p>
   * Execute the process of writing the data from the the input
   * {@link o.github.msdk.datamodel.rawdata.RawDataFile RawDataFile} to the target
   * {@link java.io.File File}
   * </p>
   * 
   * @return nothing, a void method
   */
  @Override
  public Void execute() throws MSDKException {

    logger.info("Started export of " + rawDataFile.getName() + " to " + target);

    scans = rawDataFile.getScans();
    totalScans = scans.size();
    scanStartPositions = new int[totalScans + 1];

    try {

      writer = NetcdfFileWriter.createNew(Version.netcdf3, target.getAbsolutePath());

      Array scanIndexArray = getScanIndexArray();
      Array scanTimeArray = getScanTimeArray();
      if (scanIndexArray == null)
        return null;

      List<Dimension> scanIndexDims = getScanIndexDims();
      Variable scanIndexVariable =
          writer.addVariable(null, "scan_index", DataType.INT, scanIndexDims);
      Variable scanTimeVariable =
          writer.addVariable(null, "scan_acquisition_time", DataType.FLOAT, scanIndexDims);

      progress = 0.2f;

      // data values storage dimension
      // Dimension to store the data arrays of various scans
      List<Dimension> pointNumValDims = getPointValDims();
      Variable massValueVariable = getMassValueVariable(pointNumValDims);
      Variable intensityValueVariable = getIntensityValueVariable(pointNumValDims);

      // populate the mass and the intensity values
      Array massValueArray =
          Array.factory(double.class, new int[] {scanStartPositions[totalScans]});
      Array intensityValueArray =
          Array.factory(double.class, new int[] {scanStartPositions[totalScans]});

      int idx = 0;
      double mzValues[] = new double[10000];
      float intensityValues[] = new float[10000];
      for (MsScan scan : scans) {
        mzValues = scan.getMzValues(mzValues);
        intensityValues = scan.getIntensityValues(intensityValues);

        if (canceled) {
          writer.close();
          return null;
        }

        for (int i = 0; i < scan.getNumberOfDataPoints(); i++) {
          massValueArray.setDouble(idx, mzValues[i]);
          intensityValueArray.setDouble(idx, intensityValues[i]);
          idx++;
        }

        progress = 0.2f + 0.6f * ((float) idx / scanStartPositions[totalScans]);
        if ((int) (progress * 100) >= lastLoggedProgress + 10) {
          lastLoggedProgress = (int) (progress * 10) * 10;
          logger.debug("Exporting in progress... " + lastLoggedProgress + "% completed");
        }

      }

      // Create the netCDF-3 file
      writer.create();

      // Write out the non-record variables
      writer.write(scanIndexVariable, scanIndexArray);
      writer.write(massValueVariable, massValueArray);
      writer.write(intensityValueVariable, intensityValueArray);
      writer.write(scanTimeVariable, scanTimeArray);

      // Close the writer
      writer.close();

      progress = 1.0f;

    } catch (IOException | InvalidRangeException e) {
      new MSDKException(e);
    }

    return null;
  }

  /**
   * 
   * @return scanIndexArray an {@link ucar.ma2.Array Array} containing scan indices of all scans
   * @throws IOException
   */
  private Array getScanIndexArray() throws IOException {
    // Populate the scan indices
    // Create a simple 1D array of type int, element i of the shape array contains the length of
    // the (i+1)th dimension of the array
    Array scanIndexArray = Array.factory(int.class, new int[] {totalScans});
    int idx = 0;
    for (MsScan scan : scans) {

      if (canceled) {
        writer.close();
        return null;
      }

      scanStartPositions[idx + 1] = scanStartPositions[idx] + scan.getNumberOfDataPoints();
      idx++;

    }

    for (int i = 0; i < scanStartPositions.length - 1; i++)
      scanIndexArray.setInt(i, scanStartPositions[i]);

    return scanIndexArray;
  }

  /**
   * 
   * @return scanIndexDims a {@link java.util.List List} containing all dimension definitions for
   *         the Scan Index Variable
   */
  private List<Dimension> getScanIndexDims() {
    // Write the scan indices
    // Create a variable and set the dimension scan_number=totalScans to it
    Dimension scanNumberDim = writer.addDimension(null, "scan_number", totalScans);
    List<Dimension> scanIndexDims = new ArrayList<>();
    scanIndexDims.add(scanNumberDim);

    return scanIndexDims;
  }

  /**
   * 
   * @return pointValDims a {@link java.util.List List} containing all dimension definitions for the
   *         mass and intensity value variables
   */
  private List<Dimension> getPointValDims() {
    // data values storage dimension
    // Dimension to store the data arrays of various scans
    Dimension pointNumDim =
        writer.addDimension(null, "point_number", scanStartPositions[totalScans]);
    List<Dimension> pointNumValDims = new ArrayList<>();
    pointNumValDims.add(pointNumDim);
    return pointNumValDims;
  }

  /**
   * 
   * @return scanTimeArray an {@link ucar.ma2.Array Array} containing scan retention times for all
   *         scans
   */
  private Array getScanTimeArray() {
    // Populate scan times
    Array scanTimeArray = Array.factory(float.class, new int[] {totalScans});
    int idx = 0;
    for (MsScan scan : scans)
      scanTimeArray.setFloat(idx++, scan.getRetentionTime());

    return scanTimeArray;
  }

  /**
   * Create mass values variable add its attributes and give it its dimension, short hand name and
   * data type
   * 
   * @param pointNumValDims a {@link java.util.List List} containing all dimension definitions for
   *        the mass and intensity value variables
   * @return massValueVariable {@link ucar.nc2.Variable Variable} containing the m/z data of the
   *         scans
   */
  private Variable getMassValueVariable(List<Dimension> pointNumValDims) {
    // Create mass values variable and add its attributes
    Variable massValueVariable =
        writer.addVariable(null, "mass_values", DataType.FLOAT, pointNumValDims);
    massValueVariable.addAttribute(new Attribute("units", "M/Z"));
    massValueVariable.addAttribute(new Attribute("scale_factor", massValueScaleFactor));

    return massValueVariable;
  }

  /**
   * Create intensity values variable add its attributes and give it its dimension, short hand name
   * and data type
   * 
   * @param pointNumValDims a {@link java.util.List List} containing all dimension definitions for
   *        the mass and intensity value variables
   * @return intensityValueVariable {@link ucar.nc2.Variable Variable} containing the intensity data
   *         of the scans
   */
  private Variable getIntensityValueVariable(List<Dimension> pointNumValDims) {
    // Create intensity values variable and add its attributes
    Variable intensityValueVariable =
        writer.addVariable(null, "intensity_values", DataType.FLOAT, pointNumValDims);
    intensityValueVariable.addAttribute(new Attribute("units", "Arbitrary Intensity Units"));
    intensityValueVariable.addAttribute(new Attribute("scale_factor", intensityValueScaleFactor));

    return intensityValueVariable;
  }

  /** {@inheritDoc} */
  @Override
  public Float getFinishedPercentage() {
    return progress;
  }

  /** {@inheritDoc} */
  @Override
  public Void getResult() {
    return null;
  }

  /** {@inheritDoc} */
  @Override
  public void cancel() {
    this.canceled = true;
  }

}