wouterbulten/slacjs

View on GitHub
src/app/models/landmark-particle-set.js

Summary

Maintainability
A
2 hrs
Test Coverage
import { randn, pdfn, variance } from '../util/math';
import { lowVarianceSampling, numberOfEffectiveParticles, normalizeWeights } from '../util/sampling';
import { polarToCartesian } from '../util/motion';

class LandmarkParticleSet {
    /**
     * Create a new particle set for finding the initial position of a landmark
     * @param  {Number} nParticles                 Number of particles
     * @param  {Number} stdRange                   SD of range measurements
     * @param  {Number} randomParticles            Number of random particles to use each update
     * @param  {Number} effectiveParticleThreshold Threshold for resampling
     * @param  {Number} maxVariance                   The maximum variance before a landmark estimate is returned
     * @return {LandmarkParticleSet}
     */
    constructor(nParticles, stdRange, randomParticles, effectiveParticleThreshold, maxVariance) {
        this.nParticles = nParticles;
        this.stdRange = stdRange;
        this.effectiveParticleThreshold = effectiveParticleThreshold;
        this.randomParticles = randomParticles;
        this.maxVariance = maxVariance;

        this.measurements = 0;
        this.particles = [];
    }

    /**
     * Integrate a new measurement in the particle set
     * @param {Number} x
     * @param {Number} y
     * @param {Number} r
     */
    addMeasurement(x, y, r) {

        if (this.measurements === 0) {

            //Init the particle set by adding random particles around the user
            this.particles = this._randomParticles(this.nParticles, x, y, r);
        }
        else {
            this._updateWeights(x, y, r);

            //Determine whether resampling is effective now
            //Is based on the normalised weights
            const weights = this.particles.map(p => p.weight);
            if (numberOfEffectiveParticles(weights) < this.effectiveParticleThreshold) {

                //Use low variance resampling to generate a set of new particles
                //Returns a list of N-randomParticles particles
                let set = this._resample(this.nParticles - this.randomParticles);

                //Add new uniformly distributed particles tot the set
                //Random particles are distributed around the current position
                this.particles = set.concat(this._randomParticles(this.randomParticles, x, y, r));
            }
        }

        this.measurements++;
        return this;
    }

    /**
     * Return the current estimate of this landmark's position
     * @return {Object}
     */
    positionEstimate() {

        //Fast check, never return before we have at least multiple measurements
        if (this.measurements < 10) {
            return {estimate: 0, x: 0, y: 0, varX: 1, varY: 1};
        }

        const {varX, varY} = this._particleVariance();

        //@todo Make this constraint configurable
        if (varX < this.maxVariance && varY < this.maxVariance) {

            //Compute a weighted average of the particles
            const {x, y} = this.averagePosition();

            return {
                estimate: 1,
                x, y,
                varX, varY
            };
        }

        return {estimate: 0, x: 0, y: 0, varX: 1, varY: 1};
    }

    /**
     * Return the particle with the heighest weight
     * @return {Particle}
     */
    bestParticle() {
        let best = this.particles[0];

        this.particles.forEach((p) => {
            if (p.weight > best.weight) {
                best = p;
            }
        });

        return best;
    }

    /**
     * Return a weighted average of this particle set
     * @return {Object} x,y
     */
    averagePosition() {

        const weights = normalizeWeights(this.particles.map((p) => p.weight));

        return {
            x: this.particles.reduce((prev, p, i) => prev + (weights[i] * p.x), 0),
            y: this.particles.reduce((prev, p, i) => prev + (weights[i] * p.y), 0)
        };
    }

    /**
     * Return the particle variance in X and Y
     * @return {Object} varx, vary
     */
    _particleVariance() {

        return {
            varX: variance(this.particles, (p) => p.x),
            varY: variance(this.particles, (p) => p.y)
        };
    }

    /**
     * Resample the particle set and return a given number of new particles
     * @param  {Number} nSamples Number of particles to return
     * @return {Array}
     */
    _resample(nSamples) {
        const weights = this.particles.map(p => p.weight);

        return lowVarianceSampling(nSamples, weights).map((i) => {
            return {
                x: randn(this.particles[i].x, this.stdRange / 4),
                y: randn(this.particles[i].y, this.stdRange / 4),
                weight: 1
            };
        });
    }

    /**
     * Init the particle set
     *
     * Creates a set of particles distributed around x,y at a distance
     * following a normal distribution with r as mean.
     *
     * @param  {Number} x Center x
     * @param  {Number} y Center y
     * @param  {Number} r range
     * @return {void}
     */
    _randomParticles(n, x, y, r) {

        const deltaTheta = 2 * Math.PI / n;
        const particles = [];

        for (let i = 0; i < n; i++) {
            const theta = i * deltaTheta;
            const range = r + randn(0, this.stdRange);
            const {dx, dy} = polarToCartesian(range, theta);

            particles.push({x: x + dx, y: y + dy, weight: 1});
        }

        return particles;
    }

    /**
     * Update each particle by updating their weights
     * @param  {Number} x
     * @param  {Number} y
     * @param  {Number} r
     * @return {void}
     */
    _updateWeights(x, y, r) {

        this.particles.forEach((p) => {

            //Calculate distance estimate
            const dist = Math.sqrt(Math.pow(p.x - x, 2) + Math.pow(p.y - y, 2));

            //What is the probability of r given dist? p(r|dist)
            //Update the weight accordingly
            //p(r) = N(r|dist,sd)

            const weight = pdfn(r, dist, this.stdRange);

            p.weight = p.weight * weight;
        });
    }
}

export default LandmarkParticleSet;