Home Reference Source Repository

src/icarus/classifier/index.js

import {EventEmitter} from 'events'
import {get} from 'object-path'

import packetHeuristicsConfig from './packet-heuristics'

/**
 * Calculates packet and station scores.
 * Not an AI classifier.
 * Packet scores are calculated based on the CRC checksum(60%)
 * and simple heuristics(min/max value, variation between packets)(40%).
 * The heuristics used are configured in the packet-heuristics.js map.
 * Station scores are calculated based on the average packet score. The weight of
 * each packet score is determined by the racional function 1/2x where x is a
 * positive non-zero integer that represents how recent the packet is (1 being the
 * most recent packet).
 */
export default class Classifier extends EventEmitter {
	/**
	 * Constructor.
	 * @param {Bunyan} logger Logger instance.
	 */
	constructor(logger) {
		super()

		/**
		 * The station score.
		 * @type {!Number}
		 */
		this.stationScore = undefined

		/**
		 * The last data received (all packets merge in here).
		 * Used for heuristic algorithms.
		 * @type {Object}
		 */
		this._lastData = Object.create(null)

		/**
		 * Logger instance.
		 * @type {Bunyan}
		 */
		this._log = logger

		this._log.info('classifier.construct')
		this._log.debug('classifier config', {packetHeuristicsConfig})
	}

	/**
	 * Classifies one packet and updates the station score (unless told otherwise).
	 * Packet scores are calculated based on the CRC checksum(60%)
	 * and simple heuristics(min/max value, variation between packets)(40%).
	 * @param {Object} packet The packet to classify.
	 * @param {Boolean} [updateStationClassification=true] Whether to update the station score.
	 * @emits stationScore(stationScore): when updateStationClassification is true, the station score is updated and the event is fired.
	 * @returns {Number} Packet score.
	 */
	classifyPacket(packet, updateStationClassification = true) {
		this._log.info('classifier.classifyPacket', {updateStationClassification})
		this._log.debug('classifier.classifyPacket', {packet})
		let score = 0

		// Iterate and run each heuristic
		let heuristicCount = 0
		for (const [fields, heuristics] of packetHeuristicsConfig) {
			for (const field of fields) {
				// Get field current value
				const val = get(packet, field)

				// When there is no value, the field does not exist => wrong kind of packet.
				if (val === undefined) {
					this._log.debug('no value, skipping heuristics for', field, {val})
					continue
				}

				// Get last values
				const lastVal = get(this._lastData, field, val)

				for (const heuristic of heuristics) {
					heuristicCount++
					this._log.trace('Running heuristic', heuristic.name)
					score += heuristic(val, lastVal)
				}
			}
		}

		// Bump score to a 0-40 range
		score *= 40 / heuristicCount

		// The CRC accounts for 60% of the score, unless there are no heuristics
		if (packet.crc.sent === packet.crc.local) {
			if (score === Infinity || score === -Infinity || isNaN(score)) {
				score = 100
			} else {
				score += 60
			}
		} else if (score === Infinity || score === -Infinity || isNaN(score)) {
			score = 0
		}

		// Update station classification
		if (updateStationClassification) {
			this.classifyStationInc(score)
		}

		// Update _lastData with the current packet fields
		Object.assign(this._lastData, packet)

		return score
	}

	/**
	 * Updates the station score(incrementally).
	 * Station scores are calculated based on the average packet score. The weight of
	 * each packet score is determined by the racional function 1/2x where x is a
	 * positive non-zero integer that represents how recent the packet is (1 being the
	 * most recent packet).
	 * @param {Number} packetScore The score of the previously unnacounted packet.
	 * @returns {Number} New station score.
	 * @emits stationScore(stationScore): because the station score was updated.
	 */
	classifyStationInc(packetScore) {
		if (this.stationScore === undefined) {
			this.stationScore = packetScore
		} else {
			this.stationScore += packetScore
			this.stationScore /= 2
		}

		// Warn about the change
		this.emit('stationScore', this.stationScore)

		return this.stationScore
	}
}