diff --git a/DESIGN.md b/DESIGN.md index d84033c..6259d21 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -38,9 +38,81 @@ Data, either streaming or batched, that contains the state of the HTM system. Sh * * * -# Proposed API -To see the proposed API, build out the docs as described in the [README](README.md). +# Configuring an HTM Network for Highbrow + +HTM Networks consiste of CorticalColumns, Layers, Neurons, and (sometimes) MiniColumns. HTM Networks can be defined in a configuration file, which is enough information to render the structures in 3D. The configuration is a JSON object that looks like this: + +```json +{ + name: "simple HtmNetwork", + origin: {x:0, y:0, z:0}, + corticalColumns: [{ + name: "CorticalColumn 1", + layers: [ + { + name: "Layer 1", + miniColumns: false, + neuronCount: 100, + dimensions: { + x: 10, y: 10, z: 1 + } + } + ] + }] +} +``` + +Each node in this tree represents a Renderable object. The top level is an HtmNetwork. It contains an array of `corticalColumns`. Each cortical column contains an array of `layers`. + +Layers must have dimensions. The dimensions of CorticalColumns and HtmNetworks are calculated from the layers. + + +# Objects + +## Neuron + +Represents a pyramidal neuron. Neurons can be put into different states. Must be created with a `position` corresponding to its XYZ location in the layer cell grid. Neurons are created by their parent Layer objects. + +## Layer + +A collection of Neurons. They might just be in an array, or structured into MiniColumns (TODO). Layers have X, Y, and Z dimensions. The Y dimension will represent MiniColumns, if they exist. Because there may be less neurons in the structure than the dimension allows, a `neuronCount` must be provided in the layer config. Layer configuration looks like this: + +```json +{ + name: "layer 1", + miniColumns: false, + neuronCount: 100, + dimensions: { + x: 10, y: 10, z: 1 + } +} +``` + +## CorticalColumn + +A collection of Layers. Each Layer will be positioned below the proceeding Layer to align the configuration and the visualization with biological reality (input comes into the bottom, moves upward). Configuration: + +```json +{ + name: "column 1", + layers: [...] +} +``` + +CorticalColumns are created by their parent HtmNetwork, and are assigned an origin point. + +## HtmNetwork + +An HtmNetwork is a collection of CorticalColumns. It must have an `origin` to be created. + +```json +{ + name: "one column, two layers", + origin: {x: 0, y: 0, z: 0}, + corticalColumns: [...] +} +``` * * * diff --git a/README.md b/README.md index cf0a066..b86f600 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > HTM 3D Translation Library -Currently under development, does not actually work yet. See design at [DESIGN](DESIGN.md) for motivation and notes. For more up-to-date architecture, build docs below. +Currently under development. See design at [DESIGN](DESIGN.md) for motivation and notes. For more up-to-date architecture, build docs below. ## Install @@ -21,13 +21,13 @@ Currently under development, does not actually work yet. See design at [DESIGN]( npm run build -Installs into `bin/highbrow.bundle.js`. +Installs into `bin/highbrow-{X.Y.Z}.bundle.js`. ## Examples ### WEBGL -There is a simple WEBGL example in [`examples/webgl`](examples/webgl/). This example interfaces with [cell-viz](https://github.com/numenta/cell-viz), an existing platform I've used for HTM cell rendering in THREE.JS. For details, see [`HighbrowLayerVisualization`](https://github.com/numenta/cell-viz/blob/master/src/HighbrowLayerVisualization.js). +There is a simple WEBGL example in [`examples/webgl`](examples/webgl/). This example interfaces with [cell-viz](https://github.com/numenta/cell-viz), an existing platform I've used for HTM cell rendering in THREE.JS. For details, see [`HighbrowLayerVisualization`](https://github.com/numenta/cell-viz/blob/master/src/HighbrowLayerVisualization.js). It currently only models one layer and is still missing key features, but if you open [`examples/webgl/index.html`](examples/webgl/index.html) in a browser and hold `s` to strafe backward you can see the animated cells with global indices and XYZ positions rendered upon them. diff --git a/bin/highbrow-0.0.1.bundle.js b/bin/highbrow-0.0.1.bundle.js index e518ac1..b20f67a 100644 --- a/bin/highbrow-0.0.1.bundle.js +++ b/bin/highbrow-0.0.1.bundle.js @@ -72,29 +72,19 @@ // Copyright © 2017 Numenta /** @ignore */ -const DEFAULT_ORIGIN = { x: 0, y: 0, z: 0 - /** @ignore */ -};const DEFAULT_SCALE = 1.0; -/** @ignore */ -const DEFAULT_OFFSET = { x: 0, y: 0, z: 0 }; - -function getConfigValueOrDefault(name, config, def) { - let out = def; - if (config.hasOwnProperty(name)) { - out = config[name]; - } - return out; -} +const CONFIG_DEFAULTS = { + scale: 1, + spacing: 0 -/** - * Abstract base class for renderable objects. All renderable objects must - * provide the following function implementations: - * - {@link getChildren} - * - * NOTE: The size of {@link Renderable} objects is not controlled by this API. - * Clients of this API are responsible for sizing. - */ -class Renderable { + /** + * Abstract base class for renderable objects. All renderable objects must + * provide the following function implementations: + * - {@link getChildren} + * + * NOTE: The size of {@link Renderable} objects is not controlled by this API. + * Clients of this API are responsible for sizing. + */ +};class Renderable { /** * @param {Object} config - Contains all the details the Renderable needs to * know to calculate origins for itself and its children. @@ -102,18 +92,38 @@ class Renderable { * @param {Renderable} parent - The parent Renderable object (optional). * @param {number} scale - Default 1.0, used for UI clients to scale the * drawings. - * @param {Object} offset - Moves the object in 3D space, will affect all - * point calculations. Can be used to adjust for `scale` changes. - * @param {number} offset.x - X coordinate - * @param {number} offset.y - Y coordinate - * @param {number} offset.z - Z coordinate */ constructor(config, parent = undefined) { - this._config = config; + // Clone the config so we don't change it in case it is reused somewhere + // else. + this._config = Object.assign({}, config); this._parent = parent; - this._scale = getConfigValueOrDefault("scale", config, DEFAULT_SCALE); - this._offset = getConfigValueOrDefault("offset", config, DEFAULT_OFFSET); - this._origin = getConfigValueOrDefault("origin", config, DEFAULT_ORIGIN); + this._scale = this._getConfigValueOrDefault("scale"); + this._origin = this._getConfigValueOrDefault("origin"); + this._spacing = this._getConfigValueOrDefault("spacing"); + } + + // Utility for overridding default values and throwing error if no value + // exists. + _getConfigValueOrDefault(name) { + let out = CONFIG_DEFAULTS[name]; + if (this._config.hasOwnProperty(name)) { + out = this._config[name]; + } + if (out == undefined) { + throw new Error("Cannot create Renderable without " + name); + } + return out; + } + + // Utility for apply this object's scale to any xyz point. + _applyScale(point) { + let scale = this.getScale(); + return { + x: point.x * scale, + y: point.y * scale, + z: point.z * scale + }; } /** @@ -124,14 +134,7 @@ class Renderable { } getDimensions() { - let dimensions = this._config.dimensions; - let scale = this.getScale(); - let dimensionsOut = { - x: dimensions.x * scale, - y: dimensions.y * scale, - z: dimensions.z * scale - }; - return dimensionsOut; + throw new Error("Renderable Highbrow objects must provide getDimensions()"); } /** @@ -143,15 +146,8 @@ class Renderable { * @property {number} z z coordinate */ getOrigin() { - let origin = this._origin; - let scale = this.getScale(); - let offset = this.getOffset(); - let originOut = { - x: origin.x * scale + offset.x, - y: origin.y * scale + offset.y, - z: origin.z * scale + offset.z - }; - return originOut; + // Returns a copy or else someone could inadvertantly change the origin. + return Object.assign({}, this._origin); } /** @@ -175,28 +171,8 @@ class Renderable { return this._scale; } - /** - * Moves the object in 3D space, will affect all point calculations. Can be - * used to adjust for scale changes. - * @param {number} x - X offset - * @param {number} y - Y offset - * @param {number} z - Z offset - */ - setOffset(x = 0, y = 0, z = 0) { - let offset = this._offset; - offset.x = x; - offset.y = y; - offset.z = z; - } - - /** - * @return {Object} offset - * @property {number} x x coordinate - * @property {number} y y coordinate - * @property {number} z z coordinate - */ - getOffset() { - return this._offset; + getSpacing() { + return this._spacing; } /** @@ -355,9 +331,51 @@ function getXyzPositionFromIndex(idx, xsize, ysize) { class Layer extends Renderable { constructor(config, parent) { super(config, parent); + if (config.dimensions == undefined) { + throw Error("Cannot create Layer without dimensions"); + } + this._dimensions = config.dimensions; this._buildLayer(); } + /** + * Builds out the layer from scratch using the config object. Creates an + * array of {@link Neuron}s that will be used for the lifespan of the Layer. + */ + _buildLayer() { + this._neurons = []; + let count = this._config.neuronCount; + let layerOrigin = this.getOrigin(); + let spacing = this.getSpacing(); + for (let i = 0; i < count; i++) { + let position = getXyzPositionFromIndex(i, this._dimensions.x, this._dimensions.y); + // When creating children, we must apply the scale to the origin + // points to render them in the right perspective. + let scaledPosition = this._applyScale(position); + // Start from the layer origin and add the scaled position. + let origin = { + x: layerOrigin.x + scaledPosition.x + position.x * spacing, + y: layerOrigin.y + scaledPosition.y + position.y * spacing, + z: layerOrigin.z + scaledPosition.z + position.z * spacing + }; + let neuron = new Neuron({ + name: `Neuron ${i}`, + state: NeuronState.inactive, + index: i, + position: position, + origin: origin + }, this); + this._neurons.push(neuron); + } + if (this._config.miniColumns) { + // TODO: implement minicolumns. + } + } + + getDimensions() { + return this._dimensions; + } + /** * This function accepts HTM state data and updates the positions and states * of all {@link Renderable} HTM components. @@ -419,34 +437,11 @@ class Layer extends Renderable { } } } else { - out += ` contains ${this._neurons.length} neurons`; + out += ` contains ${this._neurons.length} neurons scaled by ${this.getScale()}`; } return out; } - /** - * Builds out the layer from scratch using the config object. Creates an - * array of {@link Neuron}s that will be used for the lifespan of the Layer. - */ - _buildLayer() { - this._neurons = []; - let count = this._config.neuronCount; - for (let i = 0; i < count; i++) { - let neuron = new Neuron({ - name: `Neuron ${i}`, - state: NeuronState.inactive, - index: i, - position: getXyzPositionFromIndex(i, this._config.dimensions.x, this._config.dimensions.y), - scale: this.getScale(), - offset: this.getOffset() - }, this); - this._neurons.push(neuron); - } - if (this._config.miniColumns) { - // TODO: implement minicolumns. - } - } - } /** @@ -535,9 +530,57 @@ const Layer = __webpack_require__(3); class CorticalColumn extends Renderable { constructor(config, parent) { super(config, parent); - this._layers = this._config.layers.map(config => { - return new Layer(config, this); + this._buildColumn(); + } + + _buildColumn() { + let columnOrigin = this.getOrigin(); + let scale = this.getScale(); + // let accumulationForLayerY = 0 + let processedLayers = []; + + // Reverse the layer configuration so that they render from bottom to + // top. slice() copies the array first so the config is not altered. + let reversedLayers = this._config.layers.slice().reverse(); + reversedLayers.map((layerConfigOriginal, layerIndex) => { + let layerConfig = Object.assign({}, layerConfigOriginal); + layerConfig.scale = scale; + layerConfig.origin = this.getOrigin(); + + // Default cell spacing for layers will be 10% of scale, or 0 + if (layerConfig.spacing == undefined) { + layerConfig.spacing = scale / 10; + if (layerConfig.spacing < 1) layerConfig.spacing = 0; + } + + // Get the total height of previously processed layers. + let layerBuffer = processedLayers.map(processedLayer => { + let ydim = processedLayer.getDimensions().y; + let cellHeight = ydim * processedLayer.getScale(); + let spacingHeight = (ydim - 1) * processedLayer.getSpacing(); + let columnSpacing = this.getSpacing(); + console.log("---- %s Y dimensions:", processedLayer.getName()); + console.log("cell height: %s\tspacing height: %s\tcolumn spacing: %s", cellHeight, spacingHeight, columnSpacing); + return cellHeight + spacingHeight + columnSpacing; + }).reduce((sum, value) => { + return sum + value; + }, 0); + + // Layers need spacing in between them, which will affect their + // origin points in the Y direction. If there are multiple layers, + // their Y origins get updated here using the scale, column spacing, + // and the sizes of lower layers. Each layer is rendered below the + // last to keep the config alignment the same as the visual + // alignment. + layerConfig.origin.y = layerConfig.origin.y + layerBuffer; + + let layer = new Layer(layerConfig, this); + // accumulationForLayerY += layer.getDimensions().y + processedLayers.push(layer); + return layer; }); + // The layers were processed in reverse order, reverse them again. + this._layers = processedLayers.reverse(); } /** @@ -612,6 +655,8 @@ class HtmNetwork extends Renderable { this._corticalColumns = this._config.corticalColumns.map(config => { // Attach the same origin as the parent, but a clone. config.origin = Object.assign({}, this.getOrigin()); + // use the same scale as well + config.scale = this.getScale(); return new CorticalColumn(config, this); }); } @@ -695,6 +740,9 @@ class Neuron extends Renderable { constructor(config, parent) { super(config, parent); this._state = NeuronState.inactive; + if (config.position == undefined) { + throw Error("Cannot create Neuron without position"); + } this._position = config.position; } @@ -707,20 +755,11 @@ class Neuron extends Renderable { } /** - * Neurons are not created with an origin initially like other - * {@link Renderable} objects, because they are laid out in a grid - * within the Layer space. But we know the position, so we can calculate the - * origin using the scale. + * The Neuron is the atomic unit of this visualization. It will always + * return dimensions of 1,1,1. */ - getOrigin() { - let pos = this._position; - let scale = this.getScale(); - let offset = this.getOffset(); - return { - x: pos.x * scale + offset.x, - y: pos.y * scale + offset.y, - z: pos.z * scale + offset.z - }; + getDimensions() { + return { x: 1, y: 1, z: 1 }; } /** @@ -743,10 +782,9 @@ class Neuron extends Renderable { */ toString() { let n = this.getName(); - let p = this.position; + let p = this.getPosition(); let o = this.getOrigin(); - let s = this.getScale(); - return `${n} at position [${p.x}, ${p.y}, ${p.z}], coordinate [${o.x}, ${o.y}, ${o.z}] (scaled by ${s})`; + return `${n} at position [${p.x}, ${p.y}, ${p.z}], coordinate [${o.x}, ${o.y}, ${o.z}]`; } setState(state) { diff --git a/examples/webgl/index.html b/examples/webgl/index.html index 5cb9d4d..0279310 100644 --- a/examples/webgl/index.html +++ b/examples/webgl/index.html @@ -17,53 +17,74 @@ diff --git a/examples/webgl/js/cell-viz-1.1.0.bundle.js b/examples/webgl/js/cell-viz-1.1.0.bundle.js index af405d9..a08f331 100644 --- a/examples/webgl/js/cell-viz-1.1.0.bundle.js +++ b/examples/webgl/js/cell-viz-1.1.0.bundle.js @@ -54,7 +54,9 @@ __webpack_require__(11); __webpack_require__(12); __webpack_require__(13); - module.exports = __webpack_require__(14); + __webpack_require__(14); + __webpack_require__(15); + module.exports = __webpack_require__(16); /***/ }, @@ -50213,96 +50215,6 @@ /***/ }, /* 14 */ -/***/ function(module, exports, __webpack_require__) { - - /* - # ---------------------------------------------------------------------- - # Copyright (C) 2016, Numenta, Inc. Unless you have an agreement - # with Numenta, Inc., for a separate license for this software code, the - # following terms and conditions apply: - # - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU Affero Public License version 3 as - # published by the Free Software Foundation. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - # See the GNU Affero Public License for more details. - # - # You should have received a copy of the GNU Affero Public License - # along with this program. If not, see http://www.gnu.org/licenses. - # - # http://numenta.org/licenses/ - # ---------------------------------------------------------------------- - */ - - window.THREE = __webpack_require__(2); - window.HtmCellStates = __webpack_require__(15); - window.SingleLayerVisualization = __webpack_require__(12); - window.SpToInputVisualization = __webpack_require__(13); - window.CompleteHtmVisualization = __webpack_require__(11); - window.HighbrowLayerVisualization = __webpack_require__(16); - window.HtmCells = __webpack_require__(5); - window.InputCells = __webpack_require__(6); - window.HtmMiniColumns = __webpack_require__(7); - - -/***/ }, -/* 15 */ -/***/ function(module, exports) { - - module.exports = { - inactive: { - state: 'inactive', - color: new THREE.Color('#FFFEEE'), - description: 'cell is inactive' - }, - withinActiveColumn: { - state: 'withinActiveColumn', - color: new THREE.Color('yellow'), - description: 'cell is inactive, but within a currently active column' - }, - active: { - state: 'active', - color: new THREE.Color('orange'), - description: 'cell is active, but was not predicted last step' - }, - previouslyPredictive: { - state: 'previouslyPredictive', - color: new THREE.Color('cyan'), - description: 'cell is predictive for this time step' - }, - correctlyPredicted: { - state: 'correctlyPredicted', - color: new THREE.Color('limegreen'), - description: 'cell is active and was correctly predicted last step' - }, - predictiveActive: { - state: 'predictiveActive', - color: new THREE.Color('indigo'), - description: 'cell is active and predictive' - }, - predictive: { - state: 'predictive', - color: new THREE.Color('blue'), - description: 'cell is predicted to be active on the next time step' - }, - wronglyPredicted: { - state: 'wronglyPredicted', - color: new THREE.Color('red'), - description: 'cell was predicted to be active, but was not' - }, - input: { - state: 'input', - color: new THREE.Color('green'), - description: 'input bit is on' - } - }; - - -/***/ }, -/* 16 */ /***/ function(module, exports, __webpack_require__) { var THREE = __webpack_require__(2); @@ -50649,5 +50561,398 @@ module.exports = HighbrowLayerVisualization; +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { + + var THREE = __webpack_require__(2); + var OBJLoader = __webpack_require__(9); + var ColladaLoader = __webpack_require__(10); + + function addGuides(scene) { + // Add guide lines for axes + var material = new THREE.LineBasicMaterial({ + color: "blue" + }); + + var geometry = new THREE.Geometry(); + geometry.vertices.push( + new THREE.Vector3( 0, 0, 0 ), + new THREE.Vector3( 10000, 0, 0 ) + ); + var xline = new THREE.Line( geometry, material ); + + material = new THREE.LineBasicMaterial({ + color: "red" + }); + geometry = new THREE.Geometry(); + geometry.vertices.push( + new THREE.Vector3( 0, 0, 0 ), + new THREE.Vector3( 0, 10000, 0 ) + ); + var yline = new THREE.Line( geometry, material ); + + material = new THREE.LineBasicMaterial({ + color: "green" + }); + geometry = new THREE.Geometry(); + geometry.vertices.push( + new THREE.Vector3( 0, 0, 0 ), + new THREE.Vector3( 0, 0, 10000 ) + ); + var zline = new THREE.Line( geometry, material ); + + scene.add( xline ); + scene.add( yline ); + scene.add( zline ); + } + + /** + * experiment + */ + function HighbrowColumnVisualization(highbrowColumn, opts) { + if (!opts) opts = {}; + this.column = highbrowColumn; + this.meshCells = []; + this.opts = opts; + this.spacing = opts.spacing; + this.width = undefined; + this.height = undefined; + this.$container = undefined; + this.camera = undefined; + this.controls = undefined; + this.light = undefined; + this.scene = undefined; + this.renderer = undefined; + this.loader = new ColladaLoader(); + this.projector = new THREE.Projector(); + this.cubeSize = opts.cubeSize || 100; + this.clock = new THREE.Clock(); + + this.loader.options.centerGeometry = true; + + this.geometry = new THREE.BoxGeometry( + this.cubeSize, this.cubeSize, this.cubeSize + ); + + // Use a default spacing. + if (! this.spacing) { + this.spacing = { + x: 1.4, y: 1.4, z: 1.4 + }; + } + + this._setupContainer(opts.elementId); + this._setupCamera(); + this._setupScene(); + this._setupControls(); + } + + HighbrowColumnVisualization.prototype._getCellValue = function(neuron) { + let neuronState = neuron.getState() + let out = { state: neuronState } + if (neuronState == "inactive") { + out.color = new THREE.Color('#FFFEEE') + } else { + out.color = new THREE.Color('orange') + } + return out; + }; + + HighbrowColumnVisualization.prototype._setupContainer = function(elementId) { + if (elementId) { + this.$container = $('#' + elementId); + this.width = this.$container.innerWidth(); + this.height = this.$container.innerHeight(); + } else { + this.$container = $('body'); + this.width = window.innerWidth; + this.height = window.innerHeight; + } + }; + + HighbrowColumnVisualization.prototype._setupCamera = function() { + // Set up camera position. + this.camera = new THREE.PerspectiveCamera( + 25, this.width / this.height, 50, 1e7 + ); + }; + + HighbrowColumnVisualization.prototype._setupControls = function() { + var controls = this.controls = new THREE.FlyControls( + this.camera, this.renderer.domElement + ); + controls.movementSpeed = 1000; + controls.rollSpeed = Math.PI / 24; + controls.autoForward = false; + controls.dragToLook = true; + }; + + HighbrowColumnVisualization.prototype._setupScene = function() { + var scene; + var renderer; + this.scene = new THREE.Scene(); + scene = this.scene; + this.light = new THREE.PointLight(0xFFFFFF); + scene.add(this.light); + + renderer = this.renderer = new THREE.WebGLRenderer(); + renderer.setClearColor(0xf0f0f0); + renderer.setPixelRatio(window.devicePixelRatio); + renderer.setSize(this.width, this.height); + renderer.sortObjects = false; + this.$container.append(renderer.domElement); + }; + + /** + * Creates all the geometries within the grid. These are only created once and + * updated as cells change over time, so this function should only be called + * one time for each grid of cells created in the scene. + */ + HighbrowColumnVisualization.prototype._createMeshCells = function(grid) { + var me = this; + var scene = this.scene; + // meshCells is a 2-d array indexed by layer, then neuron. + var meshCells = []; + var spacing = this.spacing; + var cubeSize = this.cubeSize; + var layerIndex, cellIndex; + + var textTextures = this.textTextures = [] + + this.column.getLayers().forEach(function(layer, layerIndex) { + var layerMesh = [] + var layerTextures = [] + layer.getNeurons().forEach(function(neuron, cellIndex) { + var cellValue = me._getCellValue(neuron); + var cellOrigin = neuron.getOrigin(); + var cellColor = cellValue.color; + var textTexture = new THREEx.DynamicTexture( + 64, 64 + ); + textTexture.context.font = "18px Verdana"; + // So we can update the text on each cell. + layerTextures.push(textTexture) + + var material = new THREE.MeshPhongMaterial({ + color: cellColor, + transparent: true, + opacity: 1.0, + map: textTexture.texture + }); + material.alphaTest = 0.15; + + var cube = new THREE.Mesh(me.geometry, material); + + // Wireframe. + var geo = new THREE.EdgesGeometry( cube.geometry ); + var mat = new THREE.LineBasicMaterial( + { color: 0x333, linewidth: 1 } + ); + var wireframe = new THREE.LineSegments( geo, mat ); + cube.add( wireframe ); + + cube.position.x = cellOrigin.x; + cube.position.y = cellOrigin.y; + cube.position.z = cellOrigin.z; + + cube.updateMatrix(); + cube.matrixAutoUpdate = false; + grid.add(cube); + layerMesh.push(cube); + console.log( + "Created layer %s cell %s at %s,%s,%s", + layerIndex, cellIndex, cube.position.x, cube.position.y, cube.position.z + ); + console.log(neuron.toString()) + }); + meshCells.push(layerMesh); + textTextures.push(layerTextures); + }); + + scene.add(grid); + addGuides(scene); + return meshCells; + }; + + /* + * Updates the mesh cell colors based on the cells, which might have changed. + * This function should only be called when the cells change. + */ + HighbrowColumnVisualization.prototype._applyMeshCells = + function(meshCells) { + var me = this; + var spacing = this.spacing; + var cubeSize = this.cubeSize; + + this.column.getLayers().forEach(function(layer, layerIndex) { + layer.getNeurons().forEach(function(neuron, cellIndex) { + var cube = meshCells[layerIndex][cellIndex]; + var cellValue = me._getCellValue(neuron); + var cellOrigin = neuron.getOrigin(); + cube.material.color = new THREE.Color(cellValue.color); + cube.position.x = cellOrigin.x; + cube.position.y = cellOrigin.y; + cube.position.z = cellOrigin.z; + + // This will display positional information on the cell texture for + // debugging purposes. + var cellPosition = neuron.getPosition() + var textTexture = me.textTextures[layerIndex][cellIndex] + textTexture.clear('white') + textTexture.drawText(cellIndex, undefined, 30, 'black') + textTexture.drawText( + cellPosition.x + ", " + cellPosition.y + ", " + cellPosition.z, + undefined, + 50, + 'black' + ) + textTexture.texture.needsUpdate = true + cube.updateMatrix(); + }); + }); + + }; + + HighbrowColumnVisualization.prototype.render = function(opts) { + if (!opts) opts = {}; + var me = this; + var renderer = this.renderer; + var scene = this.scene; + var controls = this.controls; + var camera = this.camera; + var light = this.light; + var w = this.width; + var h = this.height; + var grid = new THREE.Group(); + + this.meshCells = this._createMeshCells(grid); + + window.addEventListener('resize', function() { + w = me.width = me.$container.innerWidth(); + h = me.height = me.$container.innerHeight(); + camera.aspect = w / h; + camera.updateProjectionMatrix(); + renderer.setSize(w, h); + innerRender(); + }, false ); + + this.$container.append(renderer.domElement); + + function animate() { + requestAnimationFrame(animate); + innerRender(); + } + + function innerRender() { + var delta = me.clock.getDelta(); + me.controls.update( delta ); + light.position.x = camera.position.x; + light.position.y = camera.position.y; + light.position.z = camera.position.z; + renderer.render(scene, camera); + } + + animate(); + }; + + HighbrowColumnVisualization.prototype.redraw = function() { + this._applyMeshCells(this.meshCells); + }; + + module.exports = HighbrowColumnVisualization; + + +/***/ }, +/* 16 */ +/***/ function(module, exports, __webpack_require__) { + + /* + # ---------------------------------------------------------------------- + # Copyright (C) 2016, Numenta, Inc. Unless you have an agreement + # with Numenta, Inc., for a separate license for this software code, the + # following terms and conditions apply: + # + # This program is free software: you can redistribute it and/or modify + # it under the terms of the GNU Affero Public License version 3 as + # published by the Free Software Foundation. + # + # This program is distributed in the hope that it will be useful, + # but WITHOUT ANY WARRANTY; without even the implied warranty of + # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + # See the GNU Affero Public License for more details. + # + # You should have received a copy of the GNU Affero Public License + # along with this program. If not, see http://www.gnu.org/licenses. + # + # http://numenta.org/licenses/ + # ---------------------------------------------------------------------- + */ + + window.THREE = __webpack_require__(2); + window.HtmCellStates = __webpack_require__(17); + window.SingleLayerVisualization = __webpack_require__(12); + window.SpToInputVisualization = __webpack_require__(13); + window.CompleteHtmVisualization = __webpack_require__(11); + window.HighbrowLayerVisualization = __webpack_require__(14); + window.HighbrowColumnVisualization = __webpack_require__(15); + window.HtmCells = __webpack_require__(5); + window.InputCells = __webpack_require__(6); + window.HtmMiniColumns = __webpack_require__(7); + + +/***/ }, +/* 17 */ +/***/ function(module, exports) { + + module.exports = { + inactive: { + state: 'inactive', + color: new THREE.Color('#FFFEEE'), + description: 'cell is inactive' + }, + withinActiveColumn: { + state: 'withinActiveColumn', + color: new THREE.Color('yellow'), + description: 'cell is inactive, but within a currently active column' + }, + active: { + state: 'active', + color: new THREE.Color('orange'), + description: 'cell is active, but was not predicted last step' + }, + previouslyPredictive: { + state: 'previouslyPredictive', + color: new THREE.Color('cyan'), + description: 'cell is predictive for this time step' + }, + correctlyPredicted: { + state: 'correctlyPredicted', + color: new THREE.Color('limegreen'), + description: 'cell is active and was correctly predicted last step' + }, + predictiveActive: { + state: 'predictiveActive', + color: new THREE.Color('indigo'), + description: 'cell is active and predictive' + }, + predictive: { + state: 'predictive', + color: new THREE.Color('blue'), + description: 'cell is predicted to be active on the next time step' + }, + wronglyPredicted: { + state: 'wronglyPredicted', + color: new THREE.Color('red'), + description: 'cell was predicted to be active, but was not' + }, + input: { + state: 'input', + color: new THREE.Color('green'), + description: 'input bit is on' + } + }; + + /***/ } /******/ ]); \ No newline at end of file diff --git a/src/cortical-column.js b/src/cortical-column.js index 13d88b6..52e242f 100644 --- a/src/cortical-column.js +++ b/src/cortical-column.js @@ -13,25 +13,48 @@ const Layer = require("./layer") class CorticalColumn extends Renderable { constructor(config, parent) { super(config, parent) - this._layers = this._config.layers.map((layerConfig, index) => { - // Attach the same origin as the parent, but a clone. - layerConfig.origin = Object.assign({}, this.getOrigin()) - // Layers need spacing in between them, which will affect their - // origin points in the Y direction. If there are multiple layers, - // their Y origins get updated here using the column spacing and the - // sizes of lower layers. + this._buildColumn() + } + + _buildColumn() { + let columnOrigin = this.getOrigin() + let scale = this.getScale() + let processedLayers = [] + + // Reverse the layer configuration so that they render from bottom to + // top. slice() copies the array first so the config is not altered. + let reversedLayers = this._config.layers.slice().reverse() + reversedLayers.map((layerConfigOriginal, layerIndex) => { + let layerConfig = Object.assign({}, layerConfigOriginal) + layerConfig.scale = scale + layerConfig.origin = this.getOrigin() - // FIXME: I think there is abug here, but my tests don't uncover it. - // It's because only the layer immediately under the current - // layer has its Y dimension counted, lower layers may have - // other Y dimensions. - if (index > 0) { - layerConfig.origin.y = - this._config.layers[index - 1].dimensions.y * index - + this.getSpacing() * index + // Default cell spacing for layers will be 10% of scale, or 0 + if (layerConfig.spacing == undefined) { + layerConfig.spacing = scale / 10 + if (layerConfig.spacing < 1) layerConfig.spacing = 0 } - return new Layer(layerConfig, this) + + // Get the total height of previously processed layers so we know + // where to put the origin for this layer. + let layerY = processedLayers.map((processedLayer) => { + let ydim = processedLayer.getDimensions().y + let cellHeight = ydim * processedLayer.getScale() + let spacingHeight = (ydim - 1) * processedLayer.getSpacing() + let columnSpacing = this.getSpacing() + return cellHeight + spacingHeight + columnSpacing + }).reduce((sum, value) => { + return sum + value + }, 0) + + layerConfig.origin.y = layerConfig.origin.y + layerY + + let layer = new Layer(layerConfig, this) + processedLayers.push(layer) + return layer }) + // The layers were processed in reverse order, reverse them again. + this._layers = processedLayers.reverse() } /** diff --git a/src/htm-network.js b/src/htm-network.js index 54df6c3..34f1642 100644 --- a/src/htm-network.js +++ b/src/htm-network.js @@ -16,6 +16,8 @@ class HtmNetwork extends Renderable { this._corticalColumns = this._config.corticalColumns.map((config) => { // Attach the same origin as the parent, but a clone. config.origin = Object.assign({}, this.getOrigin()) + // use the same scale as well + config.scale = this.getScale() return new CorticalColumn(config, this) }) } diff --git a/src/layer.js b/src/layer.js index cd1a475..511f5f1 100644 --- a/src/layer.js +++ b/src/layer.js @@ -48,9 +48,53 @@ function getXyzPositionFromIndex(idx, xsize, ysize) { class Layer extends Renderable { constructor(config, parent) { super(config, parent) + if (config.dimensions == undefined) { + throw Error("Cannot create Layer without dimensions") + } + this._dimensions = config.dimensions this._buildLayer() } + /** + * Builds out the layer from scratch using the config object. Creates an + * array of {@link Neuron}s that will be used for the lifespan of the Layer. + */ + _buildLayer() { + this._neurons = [] + let count = this._config.neuronCount + let layerOrigin = this.getOrigin() + let spacing = this.getSpacing() + for (let i = 0; i < count; i++) { + let position = getXyzPositionFromIndex( + i, this._dimensions.x, this._dimensions.y + ) + // When creating children, we must apply the scale to the origin + // points to render them in the right perspective. + let scaledPosition = this._applyScale(position) + // Start from the layer origin and add the scaled position. + let origin = { + x: layerOrigin.x + scaledPosition.x + position.x * spacing, + y: layerOrigin.y + scaledPosition.y + position.y * spacing, + z: layerOrigin.z + scaledPosition.z + position.z * spacing, + } + let neuron = new Neuron({ + name: `Neuron ${i}`, + state: NeuronState.inactive, + index: i, + position: position, + origin: origin + }, this) + this._neurons.push(neuron) + } + if (this._config.miniColumns) { + // TODO: implement minicolumns. + } + } + + getDimensions() { + return this._dimensions + } + /** * This function accepts HTM state data and updates the positions and states * of all {@link Renderable} HTM components. @@ -114,37 +158,11 @@ class Layer extends Renderable { } } } else { - out += ` contains ${this._neurons.length} neurons` + out += ` contains ${this._neurons.length} neurons scaled by ${this.getScale()}` } return out } - /** - * Builds out the layer from scratch using the config object. Creates an - * array of {@link Neuron}s that will be used for the lifespan of the Layer. - */ - _buildLayer() { - this._neurons = [] - let count = this._config.neuronCount - for (let i = 0; i < count; i++) { - let neuron = new Neuron({ - name: `Neuron ${i}`, - state: NeuronState.inactive, - index: i, - position: getXyzPositionFromIndex(i, - this._config.dimensions.x, - this._config.dimensions.y - ), - scale: this.getScale(), - offset: this.getOffset() - }, this) - this._neurons.push(neuron) - } - if (this._config.miniColumns) { - // TODO: implement minicolumns. - } - } - } /** diff --git a/src/neuron.js b/src/neuron.js index ba65750..b4c2585 100644 --- a/src/neuron.js +++ b/src/neuron.js @@ -14,6 +14,9 @@ class Neuron extends Renderable { constructor(config, parent) { super(config, parent) this._state = NeuronState.inactive + if (config.position == undefined) { + throw Error("Cannot create Neuron without position") + } this._position = config.position } @@ -26,20 +29,11 @@ class Neuron extends Renderable { } /** - * Neurons are not created with an origin initially like other - * {@link Renderable} objects, because they are laid out in a grid - * within the Layer space. But we know the position, so we can calculate the - * origin using the scale. + * The Neuron is the atomic unit of this visualization. It will always + * return dimensions of 1,1,1. */ - getOrigin() { - let pos = this._position - let scale = this.getScale() - let offset = this.getOffset() - return { - x: pos.x * scale + offset.x, - y: pos.y * scale + offset.y, - z: pos.z * scale + offset.z, - } + getDimensions() { + return {x: 1, y: 1, z: 1} } /** @@ -62,10 +56,9 @@ class Neuron extends Renderable { */ toString() { let n = this.getName() - let p = this.position + let p = this.getPosition() let o = this.getOrigin() - let s = this.getScale() - return `${n} at position [${p.x}, ${p.y}, ${p.z}], coordinate [${o.x}, ${o.y}, ${o.z}] (scaled by ${s})` + return `${n} at position [${p.x}, ${p.y}, ${p.z}], coordinate [${o.x}, ${o.y}, ${o.z}]` } setState (state) { diff --git a/src/renderable.js b/src/renderable.js index 46f365c..de40b57 100644 --- a/src/renderable.js +++ b/src/renderable.js @@ -4,10 +4,8 @@ /** @ignore */ const CONFIG_DEFAULTS = { - origin: {x:0, y:0, z:0}, - offset: {x:0, y:0, z:0}, - scale: 1.0, - spacing: 1.0, + scale: 1, + spacing: 0, } /** @@ -26,29 +24,40 @@ class Renderable { * @param {Renderable} parent - The parent Renderable object (optional). * @param {number} scale - Default 1.0, used for UI clients to scale the * drawings. - * @param {Object} offset - Moves the object in 3D space, will affect all - * point calculations. Can be used to adjust for `scale` changes. - * @param {number} offset.x - X coordinate - * @param {number} offset.y - Y coordinate - * @param {number} offset.z - Z coordinate */ constructor(config, parent = undefined) { - this._config = config + // Clone the config so we don't change it in case it is reused somewhere + // else. + this._config = Object.assign({}, config) this._parent = parent this._scale = this._getConfigValueOrDefault("scale") - this._offset = this._getConfigValueOrDefault("offset") this._origin = this._getConfigValueOrDefault("origin") this._spacing = this._getConfigValueOrDefault("spacing") } + // Utility for overridding default values and throwing error if no value + // exists. _getConfigValueOrDefault(name) { let out = CONFIG_DEFAULTS[name] if (this._config.hasOwnProperty(name)) { out = this._config[name] } + if (out == undefined) { + throw new Error("Cannot create Renderable without " + name) + } return out } + // Utility for apply this object's scale to any xyz point. + _applyScale(point) { + let scale = this.getScale() + return { + x: point.x * scale, + y: point.y * scale, + z: point.z * scale, + } + } + /** * @return {Object} Configuration object used to create this. */ @@ -57,14 +66,9 @@ class Renderable { } getDimensions() { - let dimensions = this._config.dimensions - let scale = this.getScale() - let dimensionsOut = { - x: dimensions.x * scale, - y: dimensions.y * scale, - z: dimensions.z * scale, - } - return dimensionsOut + throw new Error( + "Renderable Highbrow objects must provide getDimensions()" + ) } /** @@ -76,15 +80,8 @@ class Renderable { * @property {number} z z coordinate */ getOrigin() { - let origin = this._origin - let scale = this.getScale() - let offset = this.getOffset() - let originOut = { - x: origin.x * scale + offset.x, - y: origin.y * scale + offset.y, - z: origin.z * scale + offset.z, - } - return originOut + // Returns a copy or else someone could inadvertantly change the origin. + return Object.assign({}, this._origin) } /** @@ -106,30 +103,6 @@ class Renderable { return this._scale } - /** - * Moves the object in 3D space, will affect all point calculations. Can be - * used to adjust for scale changes. - * @param {number} x - X offset - * @param {number} y - Y offset - * @param {number} z - Z offset - */ - setOffset(x = 0, y = 0, z = 0) { - let offset = this._offset - offset.x = x - offset.y = y - offset.z = z - } - - /** - * @return {Object} offset - * @property {number} x x coordinate - * @property {number} y y coordinate - * @property {number} z z coordinate - */ - getOffset() { - return this._offset - } - getSpacing() { return this._spacing } diff --git a/tests/config.js b/tests/config.js index 400a3af..0025a6a 100644 --- a/tests/config.js +++ b/tests/config.js @@ -1,6 +1,7 @@ // Simple network configuration. One column, one layer, no mini-columns. const simple = { name: "simple network", + origin: {x: 0, y: 0, z: 0}, corticalColumns: [{ name: "column 1", layers: [ @@ -18,6 +19,7 @@ const simple = { const oneColTwoLayers = { name: "one column, two layers", + origin: {x: 0, y: 0, z: 0}, corticalColumns: [{ name: "column 1", layers: [ @@ -42,6 +44,7 @@ const oneColTwoLayers = { } const oneColThreeLayers = { name: "one column, three layers", + origin: {x: 0, y: 0, z: 0}, corticalColumns: [{ name: "column 1", layers: [ @@ -75,6 +78,7 @@ const oneColThreeLayers = { // Complex network with multiple columns and layers, includes mini-columns. const complex = { name: "simple network", + origin: {x: 0, y: 0, z: 0}, corticalColumns: [{ name: "column 1", layers: [ diff --git a/tests/unit/cortical-column-tests.js b/tests/unit/cortical-column-tests.js index 85eb184..5e1e30c 100644 --- a/tests/unit/cortical-column-tests.js +++ b/tests/unit/cortical-column-tests.js @@ -1,58 +1,182 @@ -const fs = require("fs") const assert = require("chai").assert const expect = require("chai").expect -const testConfigs = require("../config") -const HtmNetwork = require("../../src/htm-network") +const CorticalColumn = require("../../src/cortical-column") describe("CorticalColumn Unit Tests", () => { - describe("when constructing a simple column", () => { - let config = Object.assign({}, testConfigs.simple); - config.origin = {x: 1, y: 345, z: -94}; - const network = new HtmNetwork(config) - const column = network.getCorticalColumns()[0] + describe("upon construction", () => { + let config = { + name: "column 1", + origin: {x: 1, y: 345, z: -94}, + scale: 44, + layers: [ + { + name: "layer 1", + miniColumns: false, + neuronCount: 100, + dimensions: { + x: 10, y: 10, z: 1 + } + } + ] + } + const column = new CorticalColumn(config) + const layer = column.getLayers()[0] it("creates the first layer at the same origin as itself", () => { - let layer = column.getLayers()[0] expect(layer.getOrigin()).to.deep.equal({x: 1, y: 345, z: -94}) }) + it("creates the first layer with the same scale as itself", () => { + expect(layer.getScale()).to.deep.equal(44) + }) }) - describe("when constructing a 2-layer column", () => { - const network = new HtmNetwork(testConfigs.oneColTwoLayers) - const column = network.getCorticalColumns()[0] - it("creates the first layer at the same origin as itself", () => { - let layer = column.getLayers()[0] - expect(layer.getOrigin()).to.deep.equal({x: 0, y: 0, z: 0}) + describe("when scale is applied upon construction", () => { + let config = { + name: "column 1", + origin: {x: 1, y: 345, z: -94}, + scale: 2.2, + layers: [ + { + name: "layer 1", + miniColumns: false, + neuronCount: 100, + dimensions: { + x: 10, y: 10, z: 1 + } + } + ] + } + const column = new CorticalColumn(config) + const layer = column.getLayers()[0] + it("does not scale its own origin", () => { + expect(column.getOrigin()).to.deep.equal({x: 1, y: 345, z: -94}) }) - it("creates the second layer above first layer with one empty position between", () => { - let layer = column.getLayers()[1] - expect(layer.getOrigin()).to.deep.equal({x: 0, y: 11, z: 0}) + it("does not scale the layer's origin", () => { + expect(layer.getOrigin()).to.deep.equal({x: 1, y: 345, z: -94}) }) - it("allows user defined layer spacing", () => { - let config = Object.assign({}, testConfigs.oneColTwoLayers); - config.corticalColumns[0].spacing = 5 - const network = new HtmNetwork(config) - const column = network.getCorticalColumns()[0] - let layer = column.getLayers()[1] - expect(layer.getOrigin()).to.deep.equal({x: 0, y: 15, z: 0}) + }) + + describe("when constructing a 2-layer column", () => { + let config = { + scale: 1, + spacing: 0, + origin: {x:0, y:0, z:0}, + name: "column 1", + layers: [ + { + name: "layer 1", + miniColumns: false, + neuronCount: 1000, + dimensions: { + x: 10, y: 10, z: 10 + } + }, + { + name: "layer 2", + miniColumns: false, + neuronCount: 1000, + dimensions: { + x: 10, y: 10, z: 10 + } + } + ] + } + describe("without scale or spacing", () => { + const column = new CorticalColumn(config) + it("creates the second layer at the same origin as itself", () => { + let layer = column.getLayers()[1] + expect(layer.getOrigin()).to.deep.equal({x: 0, y: 0, z: 0}) + }) + it("creates the first layer directly above the second layer", () => { + let layer = column.getLayers()[0] + expect(layer.getOrigin()).to.deep.equal({x: 0, y: 10, z: 0}) + }) + }) + describe("with spacing and scale", () => { + let cfg = Object.assign({}, config); + cfg.scale = 100 + cfg.spacing = 50 + cfg.layers[1].spacing = 10 + const column = new CorticalColumn(cfg) + it("creates the second layer at the same origin as itself", () => { + let layer = column.getLayers()[1] + expect(layer.getOrigin()).to.deep.equal({x: 0, y: 0, z: 0}) + }) + it("creates the first layer directly above the second layer", () => { + let layer = column.getLayers()[0] + let bottomLayerHeight = 10 * 100 + 9 * 10; + expect(layer.getOrigin()).to.deep.equal({x: 0, y: bottomLayerHeight + 50, z: 0}) + }) }) }) describe("when constructing a 3-layer column", () => { - const network = new HtmNetwork(testConfigs.oneColThreeLayers) - const column = network.getCorticalColumns()[0] - it("creates the third layer above second layer with one empty position between", () => { - let layer = column.getLayers()[2] - expect(layer.getOrigin()).to.deep.equal({x: 0, y: 12, z: 0}) - }) - it("allows user defined layer spacing", () => { - let config = Object.assign({}, testConfigs.oneColThreeLayers); - config.corticalColumns[0].spacing = 5 - const network = new HtmNetwork(config) - const column = network.getCorticalColumns()[0] - let layer = column.getLayers()[2] - expect(layer.getOrigin()).to.deep.equal({x: 0, y: 20, z: 0}) + let config = { + origin: {x:0, y:0, z:0}, + scale: 1, + spacing: 0, + name: "3-layer column", + layers: [ + { + name: "layer 1", + miniColumns: false, + neuronCount: 1000, + dimensions: { + x: 10, y: 4, z: 10 + } + }, + { + name: "layer 2", + miniColumns: false, + neuronCount: 1000, + dimensions: { + x: 10, y: 5, z: 10 + } + }, + { + name: "layer 3", + miniColumns: false, + neuronCount: 1000, + dimensions: { + x: 10, y: 6, z: 10 + } + } + ] + } + + describe("with layer spacing of 0", () => { + let column = new CorticalColumn(config) + it("creates the third layer at the same origin as itself", () => { + let layer = column.getLayers()[2] + expect(layer.getOrigin()).to.deep.equal({x: 0, y: 0, z: 0}) + }) + it("creates the second layer above the third layer with one empty position between", () => { + let layer = column.getLayers()[1] + expect(layer.getOrigin()).to.deep.equal({x: 0, y: 6, z: 0}) + }) + it("creates the first layer above the second layer with one empty position between", () => { + let layer = column.getLayers()[0] + expect(layer.getOrigin()).to.deep.equal({x: 0, y: 11, z: 0}) + }) + }) + + describe("with layer spacing of 1", () => { + let cfg = Object.assign({}, config) + cfg.origin = {x: 0, y: 0, z: 0} + cfg.scale = 1 + cfg.spacing = 1 + let col = new CorticalColumn(cfg) + console.log(JSON.stringify(col._config, null, 2)) + it("third layer is at origin", () => { + expect(col.getLayers()[2].getOrigin()).to.deep.equal({x: 0, y: 0, z: 0}) + }) + it("second layer is 1 space above third", () => { + expect(col.getLayers()[1].getOrigin()).to.deep.equal({x: 0, y: 7, z: 0}) + }) + it("first layer is 1 space above second", () => { + expect(col.getLayers()[0].getOrigin()).to.deep.equal({x: 0, y: 13, z: 0}) + }) }) }) diff --git a/tests/unit/htm-network-tests.js b/tests/unit/htm-network-tests.js index 24b9f2e..0ff2f0e 100644 --- a/tests/unit/htm-network-tests.js +++ b/tests/unit/htm-network-tests.js @@ -1,4 +1,3 @@ -const fs = require("fs") const assert = require("chai").assert const expect = require("chai").expect const testConfigs = require("../config") @@ -9,12 +8,16 @@ describe("HtmNetwork Unit Tests", () => { describe("when constructing a simple network", () => { let config = Object.assign({}, testConfigs.simple); + config.scale = 24 config.origin = {x: 1, y: 345, z: -94}; const network = new HtmNetwork(config) + var column = network.getCorticalColumns()[0] it("creates the column at the same origin as itself", () => { - var column = network.getCorticalColumns()[0] expect(column.getOrigin()).to.deep.equal({x: 1, y: 345, z: -94}) }) + it("creates the column at the same scale as itself", () => { + expect(column.getScale()).to.deep.equal(24) + }) }) }) diff --git a/tests/unit/layer-tests.js b/tests/unit/layer-tests.js index e857110..2af78a0 100644 --- a/tests/unit/layer-tests.js +++ b/tests/unit/layer-tests.js @@ -1,4 +1,3 @@ -const fs = require("fs") const assert = require("chai").assert const expect = require("chai").expect const testConfigs = require("../config") @@ -10,13 +9,49 @@ describe("Layer Unit Tests", () => { const config = { name: "layer 1", + origin: {x:0, y:0, z:0}, miniColumns: false, - neuronCount: 1000, + neuronCount: 100, dimensions: { - x: 10, y: 10, z: 10 + x: 10, y: 10, z: 1 } } + describe("upon creation with origin", () => { + let cfg = Object.assign({}, config) + cfg.origin = {x: 1, y: 345, z: -94} + const layer = new Layer(cfg) + it("creates the first layer at the same origin as itself", () => { + expect(layer.getOrigin()).to.deep.equal({x: 1, y: 345, z: -94}) + }) + describe("when scale is applied", () => { + let cfg = Object.assign({}, config) + cfg.scale = 2.2 + cfg.origin = {x: 1, y: 345, z: -94} + const layer = new Layer(cfg) + it("does not scale its own origin", () => { + expect(layer.getOrigin()).to.deep.equal({x: 1, y: 345, z: -94}) + }) + it("scales the neuron origins", () => { + layer.getNeurons().forEach(function(neuron, cellIndex) { + let pos = neuron.getPosition() + let expected = { + x: 1 + pos.x * 2.2, + y: 345 + pos.y * 2.2, + z: -94 + pos.z * 2.2, + } + expect(neuron.getOrigin()).to.deep.equal(expected) + }); + }) + }) + describe("if created without a dimension", () => { + it("throw an execption", () => { + expect(() => new Layer({origin: {x: 0, y: 0, z: 0}})) + .to.throw('Cannot create Layer without dimensions'); + }) + }) + }) + describe("regarding state updates", () => { let layer = new Layer(config) @@ -64,7 +99,7 @@ describe("Layer Unit Tests", () => { let layer = new Layer(config) - it("has y,x,z dimensional order", () => { + it("has y,x,z dimensional ordered neuron positions", () => { for (let zcount = 0; zcount < config.dimensions.z; zcount++) { for (let xcount = 0; xcount < config.dimensions.x; xcount++) { for (let ycount = 0; ycount < config.dimensions.y; ycount++) { @@ -72,7 +107,7 @@ describe("Layer Unit Tests", () => { + xcount * config.dimensions.y + ycount let neuron = layer.getNeuronByIndex(globalIndex) - expect(neuron.getOrigin()).to.deep.equal({ + expect(neuron.getPosition()).to.deep.equal({ x: xcount, y: ycount, z: zcount }) } @@ -83,9 +118,12 @@ describe("Layer Unit Tests", () => { }) describe("at default scale of 1.0", () => { - let layer = new Layer(config) + let cfg = Object.assign({}, config) + cfg.scale = 1 + cfg.origin = {x:0, y:0, z:0} + let layer = new Layer(cfg) - it("neuron origins are not scaled", () => { + it("positions and origins are not scaled", () => { for (let zcount = 0; zcount < config.dimensions.z; zcount++) { for (let xcount = 0; xcount < config.dimensions.x; xcount++) { for (let ycount = 0; ycount < config.dimensions.y; ycount++) { @@ -93,8 +131,10 @@ describe("Layer Unit Tests", () => { + xcount * config.dimensions.y + ycount let neuron = layer.getNeuronByIndex(globalIndex) - let origin = neuron.getOrigin() - expect(origin).to.deep.equal({ + expect(neuron.getPosition()).to.deep.equal({ + x: xcount, y: ycount, z: zcount + }) + expect(neuron.getOrigin()).to.deep.equal({ x: xcount, y: ycount, z: zcount }) } @@ -104,10 +144,11 @@ describe("Layer Unit Tests", () => { }) describe("at scale of 2.0", () => { - let scaledConfig = Object.assign({}, config) - scaledConfig.scale = 2.0 - let layer = new Layer(scaledConfig, "test") - it("neuron origin coordinates are doubled", () => { + let cfg = Object.assign({}, config) + cfg.scale = 2 + cfg.origin = {x:0, y:0, z:0} + let layer = new Layer(cfg) + it("neuron origin coordinates are doubled and position stays the same", () => { for (let zcount = 0; zcount < config.dimensions.z; zcount++) { for (let xcount = 0; xcount < config.dimensions.x; xcount++) { for (let ycount = 0; ycount < config.dimensions.y; ycount++) { @@ -128,9 +169,10 @@ describe("Layer Unit Tests", () => { }) describe("at scale of 0.5", () => { - let scaledConfig = Object.assign({}, config) - scaledConfig.scale = 0.5 - let layer = new Layer(scaledConfig, "test") + let cfg = Object.assign({}, config) + cfg.scale = 0.5 + cfg.origin = {x:0, y:0, z:0} + let layer = new Layer(cfg) it("neuron origin coordinates are halved", () => { for (let zcount = 0; zcount < config.dimensions.z; zcount++) { for (let xcount = 0; xcount < config.dimensions.x; xcount++) { @@ -151,39 +193,14 @@ describe("Layer Unit Tests", () => { }) }) - describe("with no offset", () => { - let layer = new Layer(config) - it("does not alter origin points", () => { - expect(layer.getOrigin()).to.deep.equal({x: 0, y: 0, z: 0}) - }) - it("does not offset neurons", () => { - for (let zcount = 0; zcount < config.dimensions.z; zcount++) { - for (let xcount = 0; xcount < config.dimensions.x; xcount++) { - for (let ycount = 0; ycount < config.dimensions.y; ycount++) { - let globalIndex = zcount * config.dimensions.x * config.dimensions.y - + xcount * config.dimensions.y - + ycount - let neuron = layer.getNeuronByIndex(globalIndex) - let origin = neuron.getOrigin() - expect(origin).to.deep.equal({ - x: xcount, - y: ycount, - z: zcount, - }) - } - } - } - }) - }) + describe("with no scale and spacing of 1", () => { + let cfg = Object.assign({}, config) + cfg.scale = 1 + cfg.spacing = 1 + cfg.origin = {x:0, y:0, z:0} + let layer = new Layer(cfg) - describe("at offset of 10,10,10", () => { - let offsetConfig = Object.assign({}, config) - offsetConfig.offset = {x: 10, y: 10, z: 10} - let layer = new Layer(offsetConfig, "test") - it("layer origin is offset properly", () => { - expect(layer.getOrigin()).to.deep.equal({x: 10, y: 10, z: 10}) - }) - it("offsets neurons properly", () => { + it("positions and origins are properly spaced", () => { for (let zcount = 0; zcount < config.dimensions.z; zcount++) { for (let xcount = 0; xcount < config.dimensions.x; xcount++) { for (let ycount = 0; ycount < config.dimensions.y; ycount++) { @@ -191,43 +208,19 @@ describe("Layer Unit Tests", () => { + xcount * config.dimensions.y + ycount let neuron = layer.getNeuronByIndex(globalIndex) - let origin = neuron.getOrigin() - expect(origin).to.deep.equal({ - x: xcount + 10, - y: ycount + 10, - z: zcount + 10, + expect(neuron.getPosition()).to.deep.equal({ + x: xcount, y: ycount, z: zcount }) - } - } - } - }) - }) - - describe("at offset of -10,20,1000", () => { - let offsetConfig = Object.assign({}, config) - offsetConfig.offset = {x: -10, y: 20, z: 1000} - let layer = new Layer(offsetConfig, "test") - it("layer origin is offset properly", () => { - expect(layer.getOrigin()).to.deep.equal({x: -10, y: 20, z: 1000}) - }) - it("offsets neurons properly", () => { - for (let zcount = 0; zcount < config.dimensions.z; zcount++) { - for (let xcount = 0; xcount < config.dimensions.x; xcount++) { - for (let ycount = 0; ycount < config.dimensions.y; ycount++) { - let globalIndex = zcount * config.dimensions.x * config.dimensions.y - + xcount * config.dimensions.y - + ycount - let neuron = layer.getNeuronByIndex(globalIndex) - let origin = neuron.getOrigin() - expect(origin).to.deep.equal({ - x: xcount - 10, - y: ycount + 20, - z: zcount + 1000, + expect(neuron.getOrigin()).to.deep.equal({ + x: xcount + xcount * cfg.spacing, + y: ycount + ycount * cfg.spacing, + z: zcount + zcount * cfg.spacing, }) } } } }) + }) }) diff --git a/tests/unit/neuron-tests.js b/tests/unit/neuron-tests.js index b69a45f..209e66e 100644 --- a/tests/unit/neuron-tests.js +++ b/tests/unit/neuron-tests.js @@ -1,4 +1,3 @@ -const fs = require("fs") const assert = require("chai").assert const expect = require("chai").expect const testConfigs = require("../config") @@ -11,6 +10,7 @@ describe("Neuron Unit Tests", () => { const config = { name: "layer 1", + origin: {x: 0, y: 0, z: 0}, miniColumns: false, neuronCount: 1000, dimensions: { @@ -21,6 +21,12 @@ describe("Neuron Unit Tests", () => { let neurons = layer.getNeurons() describe("upon creation by Layer", () => { + describe("if created without a position", () => { + it("throw an execption", () => { + expect(() => new Neuron({origin: {x: 0, y: 0, z: 0}})) + .to.throw("Cannot create Neuron without position"); + }) + }) it("returns the proper cell index", () => { neurons.forEach((neuron, index) => { expect(neuron.getIndex()).to.equal(index) @@ -73,6 +79,7 @@ describe("Neuron Unit Tests", () => { it("returns the correct origin with scale applied", () => { let config = { name: "layer 1", + origin: {x: 0, y: 0, z: 0}, miniColumns: false, neuronCount: 1000, dimensions: { diff --git a/tests/unit/renderable-tests.js b/tests/unit/renderable-tests.js new file mode 100644 index 0000000..78e9653 --- /dev/null +++ b/tests/unit/renderable-tests.js @@ -0,0 +1,28 @@ +const assert = require("chai").assert +const expect = require("chai").expect +const testConfigs = require("../config") + +const Renderable = require("../../src/renderable") + +// class MockRenderable extends Renderable { +// getChildren() { +// [{ +// getOrigin: () => {x:2, y:2, z:2} +// }, { +// getOrigin: () => {x:20, y:20, z:23} +// }] +// } +// } + +describe("Renderable Unit Tests", () => { + describe("upon construction", () => { + it("origin is not affected by scale", () => { + let r = new Renderable({origin: {x:2, y:2, z:2}, scale: 2.0}) + expect(r.getOrigin()).to.deep.equal({x:2, y:2, z:2}) + }) + it("throws exception if no origin is provided", () => { + expect(() => new Renderable({})).to.throw('Cannot create Renderable without origin') + }) + }) + +})