/* --------------------------------------------------------------------- * Numenta Platform for Intelligent Computing (NuPIC) * 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/ * --------------------------------------------------------------------- */ package org.numenta.nupic.network.sensor; import java.util.function.Consumer; import org.numenta.nupic.model.Persistable; import org.numenta.nupic.network.Layer; import org.numenta.nupic.network.Network; import rx.Observable; import rx.Observer; import rx.Scheduler; import rx.Subscription; import rx.subjects.PublishSubject; import rx.subjects.ReplaySubject; /** * Provides a clean way to create an {@link rx.Observable} with which one can input CSV * Strings. It ensures that the underlying Observable's stream is set up properly with * a header with exactly 3 lines. These header lines describe the input in such a way * as to specify input names and types together with control data for automatic Stream * consumption as designed by Numenta's input file format. * * <b>NOTE:</b> The {@link Publisher.Builder#addHeader(String)} method must be called * before adding the Publisher to the {@link Layer} (i.e. {@link Sensor}). * * Typical usage is as follows: * <pre> * <b>In the case of manual input</b> * Publisher manualPublisher = Publisher.builder() * .addHeader("timestamp,consumption") * .addHeader("datetime,float") * .addHeader("B") * .build(); * * ...then add the object to a {@link SensorParams}, and {@link Sensor} * * Sensor<ObservableSensor<String[]>> sensor = Sensor.create( * ObservableSensor::create, * SensorParams.create( * Keys::obs, new Object[] { "your name", manualPublisher })); * * ...you can then add the "sensor" to a {@link Layer} * * Layer<int[]> l = new Layer<>(n) * .addSensor(sensor); * * ...then manually input comma separated strings as such: * * String[] entries = { * "7/2/10 0:00,21.2", * "7/2/10 1:00,34.0", * "7/2/10 2:00,40.4", * "7/2/10 3:00,123.4", * }; * * manual.onNext(entries[0]); * manual.onNext(entries[1]); * manual.onNext(entries[2]); * manual.onNext(entries[3]); * </pre> * * @author David Ray * */ public class Publisher implements Persistable { private static final long serialVersionUID = 1L; private static final int HEADER_SIZE = 3; /** "Replays" the header lines for all new subscribers */ private transient ReplaySubject<String> subject; private Network parentNetwork; public static class Builder<T> { private ReplaySubject<String> subject; private Consumer<Publisher> notifier; // The 3 lines of the header String[] lines = new String[3]; int cursor = 0; /** * Creates a new {@code Builder} */ public Builder() { this(null); } /** * Instantiates a new {@code Builder} with the specified * {@link Consumer} used to propagate "build" events using a * plugged in function. * * @param c Consumer used to notify the {@link Network} of new * builds of a {@link Publisher} */ public Builder(Consumer<Publisher> c) { this.notifier = c; } /** * Adds a header line which in the case of a multi column input * is a comma separated string. * * @param s string representing one line of a header * @return this Builder */ @SuppressWarnings("unchecked") public Builder<PublishSubject<String>> addHeader(String s) { lines[cursor] = s; ++cursor; return (Builder<PublishSubject<String>>)this; } /** * Builds and validates the structure of the expected header then * returns an {@link Observable} that can be used to submit info to the * {@link Network} * @return a new Publisher */ public Publisher build() { subject = ReplaySubject.createWithSize(3); for(int i = 0;i < HEADER_SIZE;i++) { if(lines[i] == null) { throw new IllegalStateException("Header not properly formed (must contain 3 lines) see Header.java"); } subject.onNext(lines[i]); } Publisher p = new Publisher(); p.subject = subject; if(notifier != null) { notifier.accept(p); } return p; } } /** * Returns a builder that is capable of returning a configured {@link PublishSubject} * (publish-able) {@link Observable} * * @return */ public static Builder<PublishSubject<String>> builder() { return new Builder<>(); } /** * Builder that notifies a Network on every build of a new {@link Publisher} * @param c Consumer which consumes a Publisher and executes an Network notification. * @return a new Builder */ public static Builder<PublishSubject<String>> builder(Consumer<Publisher> c) { return new Builder<>(c); } /** * Sets the parent {@link Network} on this {@code Publisher} for use as a convenience. * @param n the Network to which the {@code Publisher} is connected. */ public void setNetwork(Network n) { this.parentNetwork = n; } /** * Returns the parent {@link Network} connected to this {@code Publisher} for use as a convenience. * @return this {@code Publisher}'s parent {@link Network} */ public Network getNetwork() { return parentNetwork; } /** * Provides the Observer with a new item to observe. * <p> * The {@link Observable} may call this method 0 or more times. * <p> * The {@code Observable} will not call this method again after it calls either {@link #onComplete} or * {@link #onError}. * * @param input the item emitted by the Observable */ public void onNext(String input) { subject.onNext(input); } /** * Notifies the Observer that the {@link Observable} has finished sending push-based notifications. * <p> * The {@link Observable} will not call this method if it calls {@link #onError}. */ public void onComplete() { subject.onCompleted(); } /** * Notifies the Observer that the {@link Observable} has experienced an error condition. * <p> * If the {@link Observable} calls this method, it will not thereafter call {@link #onNext} or * {@link #onComplete}. * * @param e the exception encountered by the Observable */ public void onError(Throwable e) { subject.onError(e); } /** * Subscribes to an Observable and provides an Observer that implements functions to handle the items the * Observable emits and any error or completion notification it issues. * <dl> * <dt><b>Scheduler:</b></dt> * <dd>{@code subscribe} does not operate by default on a particular {@link Scheduler}.</dd> * </dl> * * @param observer * the Observer that will handle emissions and notifications from the Observable * @return a {@link Subscription} reference with which the {@link Observer} can stop receiving items before * the Observable has completed * @see <a href="http://reactivex.io/documentation/operators/subscribe.html">ReactiveX operators documentation: Subscribe</a> */ public Subscription subscribe(Observer<String> observer) { return subject.subscribe(observer); } /** * Called within package to access this {@link Publisher}'s wrapped {@link Observable} * @return */ public Observable<String> observable() { return subject; } }