Source: gmm/gmm-decoder.js

import * as gmmUtils from '../utils/gmm-utils';

/**
 * GMM decoder <br />
 * Loads a model trained by the XMM library and processes an input stream of float vectors in real-time.
 * If the model was trained for regression, outputs an estimation of the associated process.
 * @class
 */

class GmmDecoder {

  /**
   * @param {Number} [windowSize=1] - Size of the likelihood smoothing window.
   */
  constructor(windowSize = 1) {

    /**
     * The model, as generated by XMM from a training data set.
     * @type {Object}
     * @private
     */
    this._model = undefined;

    /**
     * The model results, containing intermediate results that will be passed to the callback in filter.
     * @type {Object}
     * @private
     */
    this._modelResults = undefined;

    /**
     * Size of the likelihood smoothing window.
     * @type {Number}
     * @private
     */
    this._likelihoodWindow = windowSize;

    this._weights = [];
  }

  /**
   * Callback handling estimation results.
   * @callback gmmResultsCallback
   * @param {String} err - Description of a potential error.
   * @param {gmmResults} res - Object holding the estimation results.
   */

  /**
   * Results of the filtering process.
   * @typedef gmmResults
   * @type {Object}
   * @name gmmResults
   * @property {String} likeliest - The likeliest model's label.
   * @property {Number} likeliestIndex - The likeliest model's index
   * @property {Array.number} likelihoods - The array of all models' smoothed normalized likelihoods.
   * @property {?Array.number} outputValues - If the model was trained with regression, the estimated float vector output.
   * @property {?Array.number} outputCovariance - If the model was trained with regression, the output covariance matrix.
   */

  /**
   * The decoding function.
   * @param {Array} observation - An input float vector to be estimated.
   * @param {gmmResultsCallback} [resultsCallback=null] - The callback handling the estimation results.
   * @returns {gmmResults}
   */
  filter(observation, resultsCallback = null) {
    let err = null;
    let res = null;

    if(!this._model) {
      err = 'no model loaded yet';
    } else {
      try {
        gmmUtils.gmmFilter(observation, this._model, this._modelResults);

        // create results object from relevant modelResults values :
        const likeliest = (this._modelResults.likeliest > -1)
                        ? this._model.models[this._modelResults.likeliest].label
                        : null;
        const likelihoods = this._modelResults.smoothed_normalized_likelihoods.slice(0);
        res = {
          likeliest: likeliest,
          likeliestIndex: this._modelResults.likeliest,
          likelihoods: likelihoods,
          outputValues: [],
          outputCovariance: [],
        };

        // add regression results to global results if bimodal :
        if (this._model.shared_parameters.bimodal) {
          res['outputValues'] = this._modelResults.output_values.slice(0);
          res['outputCovariance']
              = this._modelResults.output_covariance.slice(0);
        }
      } catch (e) {
        err = 'problem occured during filtering : ' + e;
      }
    }

    if (resultsCallback) {
      resultsCallback(err, res);
    }

    return res;
  }

  //=========================== GETTERS / SETTERS ============================//

  /***
   * Likelihood smoothing window size.
   * @type {Number}
   */
  // get likelihoodWindow() {
  //   return this._likelihoodWindow;
  // }

  // set likelihoodWindow(newWindowSize) {
  //   this._likelihoodWindow = newWindowSize;
  //   this._updateLikelihoodWindow();
  // }

  /**
   * Get the likelihood smoothing window size.
   * @returns {Number}
   */
  getLikelihoodWindow() {
    return this._likelihoodWindow;
  }

  /**
   * Set the likelihood smoothing window size.
   * @param {Number} newWindowSize - the new window size.
   */
  setLikelihoodWindow(newWindowSize) {
    this._likelihoodWindow = newWindowSize;
    this._updateLikelihoodWindow();
  }

  /** @private */
  _updateLikelihoodWindow() {
    if (this._model === undefined) return;

    const res = this._modelResults.singleClassGmmModelResults;

    for (let i = 0; i < this._model.models.length; i++) {
      res[i].likelihood_buffer = new Array(this._likelihoodWindow);

      for (let j = 0; j < this._likelihoodWindow; j++) {
        res[i].likelihood_buffer[j] = 1 / this._likelihoodWindow;
      }
    }
  }

  setWeights(newWeights) {
    if (!Array.isArray(newWeights)) {
      throw new Error('Weights must be an array');
    }

    this._weights = newWeights;
    this._updateWeights();
  }

  /** @private */
  _updateWeights() {
    if (this._model === undefined) return;

    const m = this._model;
    const params = m.shared_parameters;
    const dimIn = params.bimodal ? params.dimension_input : params.dimension;

    const w = this._weights.slice();

    if (w.length < dimIn) {
      const onesToAdd = dimIn - w.length;

      for (let i = 0; i < onesToAdd; i++) {
        w.push(1);
      }
    } else if (w.length > dimIn) {
      w.splice(dimIn - 1);
    }

    for (let i = 0; i < w.length; i++) {
      w[i] = Math.max(w[i], 0);
    }

    for (let i = 0; i < m.models.length; i++) {
      for (let j = 0; j < m.models[i].components.length; j++) {
        m.models[i].components[j].weights = w;
      }
    }
  }


  /**
   * A valid XMM GMM model
   * @typedef xmmGmmModel
   * @type {Object}
   * @name xmmGmmModel
   * @property {String} TODO - LIST REAL GMM MODEL PROPERTIES HERE
   */

  /***
   * The model generated by XMM.
   * It is mandatory for the class to have a model in order to do its job.
   * @type {xmmGmmModel}
   */
  // get model() {
  //   return this.getModel();
  // }

  // set model(model) {
  //   this.setModel(model);
  // }

  /**
   * Get the actual XMM GMM model.
   * @returns {xmmGmmModel}
   */
  getModel() {
    if (this._model) {
      return JSON.parse(JSON.stringify(this._model));
    }
    return undefined;
  }

  /**
   * Set the actual XMM GMM model.
   * @param {xmmGmmModel} model
   */
  setModel(model) {
    this._setModel(model);
  }

  /** @private */
  _setModel(model) {
    this._model = undefined;
    this._modelResults = undefined;

    if (!model) return;

    // test if model is valid here (TODO : write a better test)
    if (model.models !== undefined) {
      this._model = model;

      // adds user defined weights to the model (default [1, 1, ..., 1])
      this._updateWeights();

      const m = this._model;
      const nmodels = m.models.length;

      this._modelResults = {
        instant_likelihoods: new Array(nmodels),
        smoothed_log_likelihoods: new Array(nmodels),
        smoothed_likelihoods: new Array(nmodels),
        instant_normalized_likelihoods: new Array(nmodels),
        smoothed_normalized_likelihoods: new Array(nmodels),
        likeliest: -1,
        singleClassGmmModelResults: []
      };

      // the following variables are used for regression :
      const params = m.shared_parameters;
      const dimOut = params.dimension - params.dimension_input;
      this._modelResults.output_values = new Array(dimOut);

      for (let i = 0; i < dimOut; i++) {
        this._modelResults.output_values[i] = 0.0;
      }

      let outCovarSize;
      //------------------------------------------------------------------- full
      if (m.configuration.default_parameters.covariance_mode == 0) {
        outCovarSize = dimOut * dimOut;
      //--------------------------------------------------------------- diagonal
      } else {
        outCovarSize = dimOut;
      }

      this._modelResults.output_covariance = new Array(outCovarSize);

      for (let i = 0; i < dimOut; i++) {
        this._modelResults.output_covariance[i] = 0.0;
      }


      for(let i = 0; i < nmodels; i++) {
        this._modelResults.instant_likelihoods[i] = 0;
        this._modelResults.smoothed_log_likelihoods[i] = 0;
        this._modelResults.smoothed_likelihoods[i] = 0;
        this._modelResults.instant_normalized_likelihoods[i] = 0;
        this._modelResults.smoothed_normalized_likelihoods[i] = 0;

        const res = {
          instant_likelihood: 0,
          log_likelihood: 0
        };

        res.likelihood_buffer = new Array(this._likelihoodWindow);

        for (let j = 0; j < this._likelihoodWindow; j++) {
          res.likelihood_buffer[j] = 1 / this._likelihoodWindow;
        }

        res.likelihood_buffer_index = 0;

        // the following variables are used for regression :
        res.beta = new Array(m.models[i].components.length);

        for (let j = 0; j < res.beta.length; j++) {
          res.beta[j] = 1 / res.beta.length;
        }

        res.output_values = this._modelResults.output_values.slice(0);
        res.output_covariance = this._modelResults.output_covariance.slice(0);

        // now add this singleModelResults object
        // to the global modelResults object :
        this._modelResults.singleClassGmmModelResults.push(res);
      }
    }
  }

  /***
   * Currently estimated likeliest label.
   * @readonly
   * @type {String}
   */
  // get likeliestLabel() {
  //   return this.getLikeliestLabel();
  // }

  /**
   * Get the currently estimated likeliest label.
   * @returns {String}
   */
  getLikeliestLabel() {
    if (this._modelResults) {
      if (this._modelResults.likeliest > -1) {
        return this._model.models[this._modelResults.likeliest].label;
      }
    }
    return 'unknown';
  }

  /***
   * Number of classes contained in the model.
   * @readonly
   * @type {Number}
   */
  // get nbClasses() {
  //   return this.getNumberOfClasses();
  // }

  /**
   * Get the total number of classes the model was trained with.
   * @returns {Number}
   */
  getNumberOfClasses() {
    if (this._model) {
      return this._model.models.length;
    }
    return 0;
  }

  /***
   * Size of the regression vector if model is bimodal.
   * @readonly
   * @type {Number}
   */
  // get regressionSize() {
  //   return this.getRegressionVectorSize();
  // }

  /**
   * Get the output dimension of the model (size of a regression vector).
   * @returns {Number}
   */
  getRegressionVectorSize() {
    if (this._model) {
      const params = this._model.shared_parameters;
      return params['bimodal']
           ? params['dimension'] - params['dimension_input']
           : 0;
    }
    return 0;
  }
};

export default GmmDecoder;