Skip to content

Commit

Permalink
Add parking meter example
Browse files Browse the repository at this point in the history
  • Loading branch information
Maarten committed Jun 18, 2016
1 parent 62b31f2 commit 4fc065b
Show file tree
Hide file tree
Showing 11 changed files with 528 additions and 159 deletions.
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,86 @@ On a side note, [Allauzen & Mohri](http://www.cs.nyu.edu/~allauzen/pdf/twins.pdf
* Arbitrary input tokens, and arbitrary side effect to state transitions. For example we may implement a finite state transducer by taking strings as input tokens and writing some output string to tape as a side effect.
* Compute possible transition paths in polynomial time! Using a [forward-backward-like algorithm](https://en.wikipedia.org/wiki/Forward%E2%80%93backward_algorithm), we can compute all paths through automaton *A* originating from state *S*, given input *I* all possible paths in O(|*S*| * |*I*| * |*A*|).
* Transition paths can be accessed through a Spliterator: Java 8 streaming APIs can automatically branch transition paths on states where one action may lead to multiple result states.

## Example
Here is a simple example of a parking meter that takes money:

```java
public class ParkingMeter {
/**
* How much money is in the machine
*/
private int ¢¢¢;

public static void main(String[] args) {
// Run example
new ParkingMeter().run();
}

public void run() {
// Say we can buy parking for 100 cents

// Define some actions
CoinDrop drop25cents = new CoinDrop(25);
CoinDrop drop50cents = new CoinDrop(50);

// Define our NFA
NFA<PayState, CoinDrop> nfa = new NFA.Builder<PayState, CoinDrop>()
.addTransition(PAYED_0, drop25cents, PAYED_25)
.addTransition(PAYED_0, drop50cents, PAYED_50)
.addTransition(PAYED_25, drop25cents, PAYED_50)
.addTransition(PAYED_25, drop50cents, PAYED_75)
.addTransition(PAYED_50, drop25cents, PAYED_75)
.addTransition(PAYED_50, drop50cents, PAYED_0)
.addTransition(PAYED_75, drop25cents, PAYED_0)
.addTransition(PAYED_75, drop50cents, PAYED_0) // Payed too much... no money back!
.build();

// Apply action step-by-step
Collection<State> endStates1 = nfa.start(PAYED_0)
.andThen(drop25cents)
.andThen(drop50cents)
.andThen(drop50cents)
.andThen(drop25cents)
.getState().collect(Collectors.toList());

// Or apply actions in bulk (this makes calculations of the possible paths more efficient, but it doesn't matter if we iterate over all transitions anyway)
Collection<State> endStates2 = nfa.apply(PAYED_0, new LinkedList<>(Arrays.asList(drop50cents, drop25cents, drop50cents, drop25cents)))
.collect(Collectors.toList());

System.out.println("Today earnings: ¢" + ¢¢¢ + ".");
}

private class CoinDrop implements Event<PayState> {
final int ¢;

CoinDrop(int value) {
this.¢ = value;
}

@Override
public void accept(PayState from, PayState to) {
System.out.println("Bleep Bloop. Added ¢" + ¢ + " to ¢" + from.¢ + ". ");
if (to.¢ <= 0 || to.¢ >= 100) System.out.println("You may park. Good day.");
else
System.out.println("You have paid ¢" + to.¢ + " in total. Please add ¢" + (100 - to.¢) + " before you may park.");
System.out.println("----------------------------------------------");
¢¢¢ += this.¢;
}

@Override
public String toString() {
return "¢" + ¢;
}
}

enum PayState implements State {
PAYED_0(0), PAYED_25(25), PAYED_50(50), PAYED_75(75);
public int ¢;

PayState(int ¢) {
this.¢ = ¢;
}
}
}
```
4 changes: 3 additions & 1 deletion src/main/java/org/leibnizcenter/nfa/Event.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.leibnizcenter.nfa;

import java.util.function.BiConsumer;

/**
* Created by maarten on 16-6-16.
*/
public interface Event extends Runnable {
public interface Event<S extends State> extends BiConsumer<S, S> {
}
127 changes: 81 additions & 46 deletions src/main/java/org/leibnizcenter/nfa/NFA.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,35 @@
import com.google.common.collect.*;

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Immutable NFA
* <p>
* Created by maarten on 15-6-16.
*/
public class NFA {
public final Map<State, Multimap<Event, Transition>> transitions;
public final Set<State> states;
public final Multimap<Event, State> statesThatAllowEvent;
@SuppressWarnings("WeakerAccess")
public class NFA<S extends State, E extends Event> {
public final Map<S, Multimap<E, Transition<S, E>>> transitions;
public final Set<S> states;
public final Multimap<E, S> statesThatAllowEvent;

private NFA(Builder builder) {
private NFA(Builder<S, E> builder) {
this.states = ImmutableSet.copyOf(builder.states);

ImmutableMap.Builder<State, Multimap<Event, Transition>> immTransitions = new ImmutableMap.Builder<>();
ImmutableMap.Builder<S, Multimap<E, Transition<S, E>>> immTransitions = new ImmutableMap.Builder<>();

// O(transitions.numberOfBranches())
builder.transitions.forEach((state, eventToTransitionMap) -> {
final ImmutableMultimap.Builder<Event, Transition> eventToTransitionMapBuilder = new ImmutableMultimap.Builder<>();
final ImmutableMultimap.Builder<E, Transition<S, E>> eventToTransitionMapBuilder = new ImmutableMultimap.Builder<>();
eventToTransitionMap.forEach(eventToTransitionMapBuilder::putAll);
immTransitions.put(state, eventToTransitionMapBuilder.build());
});
this.transitions = immTransitions.build();

// O(transitions.numberOfBranches())
ImmutableMultimap.Builder<Event, State> immStatesThatAllowEvent = new ImmutableMultimap.Builder<>();
ImmutableMultimap.Builder<E, S> immStatesThatAllowEvent = new ImmutableMultimap.Builder<>();
builder.transitions.forEach((state, eventMap) ->
eventMap.forEach((event, transitions) ->
immStatesThatAllowEvent.put(event, state)
Expand All @@ -52,48 +55,54 @@ private NFA(Builder builder) {
);
}

public PossibleStateTransitionPaths getPathsForInput(State start, LinkedList<Event> events) {
final List<Event> events1 = ImmutableList.copyOf(events); // O(n)
@SuppressWarnings("unused")
public PossibleStateTransitionPaths<S, E> getTransitions(S start, LinkedList<E> events) {
final List<E> events1 = ImmutableList.copyOf(events); // O(n)
return precomputePaths(events)
.get(start)
.get(events1);
}

public Stream<State> apply(S start, LinkedList<E> events) {
final PossibleStateTransitionPaths<S, E> transitions = getTransitions(start, events);
return transitions.applyRecursive();
}

/**
* O(path.numberOfBranches() * states.numberOfBranches() * transitions.numberOfBranches())
*
* @param event Input events to use for computing all possible paths along the NFA
* @return A map from starting states to a map of input events to an enumeration of possible branches
*/
public Map<State, Map<List<Event>, PossibleStateTransitionPaths>> precomputePaths(LinkedList<Event> event) {
PersistentList<Event> postFixPath = com.github.krukow.clj_lang.PersistentList.create(); // O(1)
Map<State, Map<List<Event>, PossibleStateTransitionPaths>> precomputedPaths = new HashMap<>(states.size());// O(1)
public Map<S, Map<List<E>, PossibleStateTransitionPaths<S, E>>> precomputePaths(LinkedList<E> event) {
PersistentList<E> postFixPath = com.github.krukow.clj_lang.PersistentList.create((Iterable<? extends E>) new ArrayList<E>(0)); // O(1)
Map<S, Map<List<E>, PossibleStateTransitionPaths<S, E>>> precomputedPaths = new HashMap<>(states.size());// O(1)

while (event.size() > 0) { // O(path.numberOfBranches()) *
Event lastEvent = event.removeLast(); // O(1)
E lastEvent = event.removeLast(); // O(1)

postFixPath = postFixPath.plus(lastEvent); // O(1)

// TODO filter only those states that *can* be reached through the previous action
for (State state : statesThatAllowEvent.get(lastEvent)) { // O(states.numberOfBranches()) *
Map<List<Event>, PossibleStateTransitionPaths> pathsForEvents = precomputedPaths.getOrDefault(state, new HashMap<>());//O(1)
final Collection<Transition> possibleTransitions = getTransitions(state, lastEvent); // O(1)
for (S state : statesThatAllowEvent.get(lastEvent)) { // O(states.numberOfBranches()) *
Map<List<E>, PossibleStateTransitionPaths<S, E>> pathsForEvents = precomputedPaths.getOrDefault(state, new HashMap<>());//O(1)
final Collection<Transition<S, E>> possibleTransitions = getTransitions(state, lastEvent); // O(1)

PossibleStateTransitionPaths possibleBranches;
PossibleStateTransitionPaths<S, E> possibleBranches;
if (postFixPath.size() == 1) {
possibleBranches = new PossibleStateTransitionPaths(state, possibleTransitions, postFixPath, null); // O(1)
possibleBranches = new PossibleStateTransitionPaths<>(state, possibleTransitions, postFixPath, null); // O(1)
} else {
final PersistentList<Event> furtherEvents = postFixPath.minus();
ImmutableMap.Builder<State, PossibleStateTransitionPaths> restPaths = ImmutableMap.builder(); // O(1)
final PersistentList<E> furtherEvents = postFixPath.minus();
ImmutableMap.Builder<S, PossibleStateTransitionPaths<S, E>> restPaths = ImmutableMap.builder(); // O(1)
possibleTransitions.stream() // O(possibleTransitions.numberOfBranches())
.map(Transition::getTo)
.distinct()
.forEach(possibleTargetState -> {
PossibleStateTransitionPaths restBranches = precomputedPaths.get(possibleTargetState).get(furtherEvents);
PossibleStateTransitionPaths<S, E> restBranches = precomputedPaths.get(possibleTargetState).get(furtherEvents);
assert restBranches != null : "Possible branches must have been calculated for state " + possibleTargetState;
restPaths.put(possibleTargetState, restBranches);
});
possibleBranches = new PossibleStateTransitionPaths(
possibleBranches = new PossibleStateTransitionPaths<>(
state,
possibleTransitions,
postFixPath,
Expand All @@ -108,32 +117,41 @@ public Map<State, Map<List<Event>, PossibleStateTransitionPaths>> precomputePath
return precomputedPaths;
}

public Collection<Transition> getTransitions(State from, Event event) {
final Multimap<Event, Transition> eventTransitionMultimap = transitions.get(from);

public Collection<Transition<S, E>> getTransitions(S from, E event) {
final Multimap<E, Transition<S, E>> eventTransitionMultimap = transitions.get(from);
if (eventTransitionMultimap != null) return eventTransitionMultimap.get(event);
else return Collections.emptySet();
}

public Set<State> getStates() {
public Set<S> getStates() {
return states;
}

public static class Builder {
private final Set<State> states;
private final Map<State, Map<Event, Set<Transition>>> transitions;
public StateContainer start(S state) {
return new StateContainer(Collections.singletonList(state));
}

@SuppressWarnings("unused")
public Collection<S> getStatesThatAllowEvent(E e) {
return statesThatAllowEvent.get(e);
}

public static class Builder<S extends State, E extends Event> {
private final Set<S> states;
private final Map<S, Map<E, Set<Transition<S, E>>>> transitions;

public Builder() {
this.states = new HashSet<>(50);
transitions = new HashMap<>(50);
}

public Builder addStates(Collection<State> states) {
@SuppressWarnings("unused")
public Builder<S, E> addStates(Collection<S> states) {
this.states.addAll(states);
return this;
}

public Builder addState(State states) {
public Builder<S, E> addState(S states) {
this.states.add(states);
return this;
}
Expand All @@ -146,11 +164,11 @@ public Builder addState(State states) {
* @param to To state
* @return This builder
*/
public Builder addTransition(State from, Event event, State to) {
public Builder<S, E> addTransition(S from, E event, S to) {
states.add(from);
states.add(to);

addTransition(new Transition(event, from, to), from, event);
addTransition(new Transition<>(event, from, to), from, event);

return this;
}
Expand All @@ -161,7 +179,7 @@ public Builder addTransition(State from, Event event, State to) {
* @param transitionsToAdd List of transitions. Implicit states will be added if necessary.
* @return This builder
*/
public Builder addTransitions(Collection<Transition> transitionsToAdd) {
public Builder<S, E> addTransitions(Collection<Transition> transitionsToAdd) {
transitionsToAdd.forEach(this::addTransition);
return this;
}
Expand All @@ -172,31 +190,48 @@ public Builder addTransitions(Collection<Transition> transitionsToAdd) {
* @param transition Implicit states will be added if necessary.
* @return This builder
*/
public Builder addTransition(Transition transition) {
State from = transition.from;
State to = transition.to;
Event event = transition.event;
public Builder<S, E> addTransition(Transition<S, E> transition) {
S from = transition.from;
S to = transition.to;
E event = transition.event;
states.add(from);
states.add(to);
addTransition(transition, from, event);
return this;
}

private void addTransition(Transition transition, State from, Event event) {
Map<Event, Set<Transition>> eventsForState = transitions.getOrDefault(from, new HashMap<>());
Set<Transition> transitionsForEvent = eventsForState.getOrDefault(event, new HashSet<>());
private void addTransition(Transition<S, E> transition, S from, E event) {
Map<E, Set<Transition<S, E>>> eventsForState = transitions.getOrDefault(from, new HashMap<>());
Set<Transition<S, E>> transitionsForEvent = eventsForState.getOrDefault(event, new HashSet<>());
transitionsForEvent.add(transition);
eventsForState.putIfAbsent(event, transitionsForEvent);
transitions.putIfAbsent(from, eventsForState);
}


public NFA build() {
return new NFA(this);
public NFA<S, E> build() {
return new NFA<>(this);
}
}

public Collection<State> getStatesThatAllowEvent(Event e) {
return statesThatAllowEvent.get(e);
public class StateContainer {
public Collection<S> states;

public StateContainer(Collection<S> ses) {
states = ses;
}

public StateContainer andThen(E e) {
states = states.stream()
.flatMap(from -> getTransitions(from, e).stream().map(transition -> {//noinspection unchecked
e.accept(transition.getFrom(), transition.getTo());
return transition;
})).map(Transition::getTo).collect(Collectors.toList());
return this;
}

public Stream<S> getState() {
return states.stream();
}
}
}
Loading

0 comments on commit 4fc065b

Please sign in to comment.