diff --git a/src/main/java/org/numenta/nupic/FieldMetaType.java b/src/main/java/org/numenta/nupic/FieldMetaType.java index 63c52067..578ad634 100644 --- a/src/main/java/org/numenta/nupic/FieldMetaType.java +++ b/src/main/java/org/numenta/nupic/FieldMetaType.java @@ -32,6 +32,7 @@ import org.numenta.nupic.encoders.SDRCategoryEncoder; import org.numenta.nupic.encoders.SDRPassThroughEncoder; import org.numenta.nupic.encoders.ScalarEncoder; +import org.numenta.nupic.util.Tuple; /** * Public values for the field data types @@ -97,7 +98,10 @@ public T decodeType(String input, Encoder enc) { case DATETIME : return (T)((DateEncoder)enc).parse(input); case BOOLEAN : return (T)(Boolean.valueOf(input) == true ? new Integer(1) : new Integer(0)); case COORD : - case GEO : return (T)new double[] { Double.parseDouble(input.split("\\;")[0]), Double.parseDouble(input.split("\\;")[1]) }; + case GEO : { + String[] parts = input.split("[\\s]*\\;[\\s]*"); + return (T)new Tuple(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]), Double.parseDouble(parts[2])); + } case INTEGER : case FLOAT : return (T)new Double(input); case SARR : diff --git a/src/main/java/org/numenta/nupic/network/sensor/HTMSensor.java b/src/main/java/org/numenta/nupic/network/sensor/HTMSensor.java index 463bca98..eea4c8df 100644 --- a/src/main/java/org/numenta/nupic/network/sensor/HTMSensor.java +++ b/src/main/java/org/numenta/nupic/network/sensor/HTMSensor.java @@ -307,12 +307,16 @@ public Stream getOutputStream() { } } + // NOTE: The "inputMap" here is a special local implementation + // of the "Map" interface, overridden so that we can access + // the keys directly (without hashing). This map is only used + // for this use case so it is ok to use this optimization as + // a convenience. if(inputMap == null) { inputMap = new InputMap(); inputMap.fTypes = fieldTypes; } - final boolean isParallel = delegate.getInputStream().isParallel(); output = new ArrayList<>(); @@ -791,7 +795,6 @@ private void configureGeoBuilder(Map> encoderSetting * @param m the map containing the values * @param key the key to be set. */ - @SuppressWarnings("unchecked") private void setGeoFieldBits(GeospatialCoordinateEncoder.Builder b, Map m, String key) { String t = (String)m.get(key); switch(key) { diff --git a/src/test/java/org/numenta/nupic/network/NetworkTestHarness.java b/src/test/java/org/numenta/nupic/network/NetworkTestHarness.java index 444c2917..63ad112d 100644 --- a/src/test/java/org/numenta/nupic/network/NetworkTestHarness.java +++ b/src/test/java/org/numenta/nupic/network/NetworkTestHarness.java @@ -247,5 +247,35 @@ public static Parameters getParameters() { return parameters; } + /** + * Parameters and meta information for the "Geospatial Test" encoder + * @return + */ + public static Map> getGeospatialFieldEncodingMap() { + Map> fieldEncodings = setupMap(null, 0, 0, 0.0D, 0.0D, 0.0D, 0.0D, (Boolean)null, (Boolean)null, (Boolean)null, "timestamp", "datetime", "DateEncoder"); + fieldEncodings = setupMap(fieldEncodings, 50, 21, 0.0D, 100.0D, 0.0D, 0.1D, (Boolean)null, Boolean.TRUE, (Boolean)null, "consumption", "float", "ScalarEncoder"); + fieldEncodings = setupMap(fieldEncodings, 999, 25, 0.0D, 100.0D, 0.0D, 0.1D, (Boolean)null, Boolean.TRUE, (Boolean)null, "location", "geo", "GeospatialCoordinateEncoder"); + + fieldEncodings.get("timestamp").put(Parameters.KEY.DATEFIELD_TOFD.getFieldName(), new Tuple(new Object[]{Integer.valueOf(21), Double.valueOf(9.5D)})); + fieldEncodings.get("timestamp").put(Parameters.KEY.DATEFIELD_PATTERN.getFieldName(), "MM/dd/YY HH:mm"); + + fieldEncodings.get("location").put("timestep", "60"); + fieldEncodings.get("location").put("scale", "30"); + + return fieldEncodings; + } + + /** + * Parameters and meta information for the "Geospatial Test" encoder + * @return + */ + public static Parameters getGeospatialTestEncoderParams() { + Map> fieldEncodings = getGeospatialFieldEncodingMap(); + + Parameters p = Parameters.getEncoderDefaultParameters(); + p.setParameterByKey(KEY.FIELD_ENCODING_MAP, fieldEncodings); + + return p; + } } diff --git a/src/test/java/org/numenta/nupic/network/sensor/HTMSensorTest.java b/src/test/java/org/numenta/nupic/network/sensor/HTMSensorTest.java index a67e9183..9f53e305 100644 --- a/src/test/java/org/numenta/nupic/network/sensor/HTMSensorTest.java +++ b/src/test/java/org/numenta/nupic/network/sensor/HTMSensorTest.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.stream.Stream; +import org.junit.Ignore; import org.junit.Test; import org.numenta.nupic.FieldMetaType; import org.numenta.nupic.Parameters; @@ -46,7 +47,9 @@ import org.numenta.nupic.encoders.MultiEncoder; import org.numenta.nupic.encoders.RandomDistributedScalarEncoder; import org.numenta.nupic.encoders.SDRCategoryEncoder; +import org.numenta.nupic.network.NetworkTestHarness; import org.numenta.nupic.network.sensor.SensorParams.Keys; +import org.numenta.nupic.util.MersenneTwister; import org.numenta.nupic.util.Tuple; /** @@ -618,5 +621,130 @@ public void testInputIntegerArray() { Stream outputStream = htmSensor.getOutputStream(); assertEquals(884, ((int[])outputStream.findFirst().get()).length); } + + @Test + public void testWithGeospatialEncoder() { + Publisher manual = Publisher.builder() + .addHeader("timestamp,consumption,location") + .addHeader("datetime,float,geo") + .addHeader("T,,").build(); + + Sensor> sensor = Sensor.create( + ObservableSensor::create, SensorParams.create(Keys::obs, "", manual)); + + Parameters p = NetworkTestHarness.getParameters().copy(); + p = p.union(NetworkTestHarness.getGeospatialTestEncoderParams()); + p.setParameterByKey(KEY.RANDOM, new MersenneTwister(42)); + p.setParameterByKey(KEY.AUTO_CLASSIFY, Boolean.TRUE); + + HTMSensor> htmSensor = (HTMSensor>)sensor; + + + ////////////////////////////////////////////////////////////// + // Test Header Configuration // + ////////////////////////////////////////////////////////////// + + // Cast the ValueList to the more complex type (Header) + Header meta = (Header)htmSensor.getMetaInfo(); + assertTrue(meta.getFieldTypes().stream().allMatch( + l -> l.equals(FieldMetaType.DATETIME) || l.equals(FieldMetaType.FLOAT) || l.equals(FieldMetaType.GEO))); + + // Negative test (Make sure "GEO" is configured and expected + assertFalse(meta.getFieldTypes().stream().allMatch( + l -> l.equals(FieldMetaType.DATETIME) || l.equals(FieldMetaType.FLOAT))); + + + assertTrue(meta.getFieldNames().stream().allMatch( + l -> l.equals("timestamp") || l.equals("consumption") || l.equals("location"))); + assertTrue(meta.getFlags().stream().allMatch( + l -> l.equals(SensorFlags.T) || l.equals(SensorFlags.B))); + + Encoder multiEncoder = htmSensor.getEncoder(); + assertNotNull(multiEncoder); + assertTrue(multiEncoder instanceof MultiEncoder); + + + ////////////////////////////////////////////////////////////// + // Test Encoder Composition // + ////////////////////////////////////////////////////////////// + + List encoders = null; + + // NEGATIVE TEST: first so that we can reuse the sensor below - APPLY WRONG PARAMS + try { + htmSensor.initEncoder(getTestEncoderParams()); // <--- WRONG PARAMS + // Should fail here + fail(); + encoders = multiEncoder.getEncoders(multiEncoder); + assertEquals(2, encoders.size()); + }catch(IllegalArgumentException e) { + assertEquals("Coordinate encoder never initialized: location", e.getMessage()); + } + + ///////////////////////////////////// + + // Recreate Sensor for POSITIVE TEST. Set the Local parameters on the Sensor + sensor = Sensor.create( + ObservableSensor::create, SensorParams.create(Keys::obs, "", manual)); + htmSensor = (HTMSensor>)sensor; + htmSensor.initEncoder(p); + + multiEncoder = htmSensor.getEncoder(); + assertNotNull(multiEncoder); + assertTrue(multiEncoder instanceof MultiEncoder); + encoders = multiEncoder.getEncoders(multiEncoder); + assertEquals(3, encoders.size()); + + Sensor> finalSensor = sensor; + + (new Thread() { + public void run() { + manual.onNext("7/12/10 13:10,35.3,40.6457;-73.7962;5"); //5 = meters per second + } + }).start(); + + + int[] output = ((HTMSensor>)finalSensor).getOutputStream().findFirst().get(); + + int[] expected = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + assertTrue(Arrays.equals(expected, output)); + } } diff --git a/vocabulary.dictionary b/vocabulary.dictionary index 2e2d323a..9de6083c 100644 --- a/vocabulary.dictionary +++ b/vocabulary.dictionary @@ -185,3 +185,5 @@ href substeps polyline toolbar +geospatial +ok