Skip to content

Commit

Permalink
WIP - Incon behaviors
Browse files Browse the repository at this point in the history
Adds the InconBehavior interface and some of the surrounding machinery to connect it.

The thrust of this idea is to wire together incon reading, fincon writing, and cell creation,
such that the easiest way for a modeler to create a new cell is to correctly wire up the incons.
This means that wiring up those incons should be relatively painless,
it should minimize duplicating necessary information,
it should eliminate specifying any unnecessary information,
and side-stepping the incons process should require rebuilding a large portion of the resource stack, as a strong discouragement to doing so.

This commit does not compile. The status of various classes is:
1. I'm mostly happy with InconBehavior itself. It's a neatly packaged bit of behavior, it composes well (see InconBehavior.map), and I think it's legible as-is.
2. I may want to remove InconBehaviors static constructors "constant" and "serialized". I think those belong in MutableResource, as part of the "notSaving" and "serializing" helper methods instead.
3. I'm not happy with MutableResource yet, as we're now duplicating information for the incon behavior (name and ValueMapper) that we'll give later if/when we register it.
   I'm wondering if we should roll registration into this method as well, perhaps with a flag or alternate constructor to not register a resource...
4. Everywhere we build MutableResources needs to be updated, pending what we decide to do for the step above.
   Additionally, we'll need to document how this changes the MutableResource interface and include that in the migration notes should this be adopted by Aerie.
  • Loading branch information
David Legg committed Oct 30, 2024
1 parent 6d30dd1 commit d442c50
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gov.nasa.jpl.aerie.contrib.streamline.core;

import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ErrorCatchingMonad;
import gov.nasa.jpl.aerie.contrib.streamline.utils.InvertibleFunction;
import gov.nasa.jpl.aerie.merlin.framework.CellRef;
import gov.nasa.jpl.aerie.merlin.protocol.model.CellType;
import gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait;
Expand All @@ -23,37 +24,47 @@ private CellRefV2() {}
/**
* Allocate a new resource with an explicitly given effect type and effect trait.
*/
public static <D extends Dynamics<?, D>, E extends DynamicsEffect<D>> CellRef<E, Cell<D>> allocate(ErrorCatching<Expiring<D>> initialDynamics, EffectTrait<E> effectTrait) {
return CellRef.allocate(new Cell<>(initialDynamics), new CellType<>() {
@Override
public EffectTrait<E> getEffectType() {
return effectTrait;
}

@Override
public Cell<D> duplicate(Cell<D> cell) {
return new Cell<>(cell.initialDynamics, cell.dynamics, cell.elapsedTime);
}

@Override
public void apply(Cell<D> cell, E effect) {
cell.initialDynamics = effect.apply(cell.dynamics).match(
ErrorCatching::success,
error -> failure(new RuntimeException(
"Applying effect '%s' failed.".formatted(getName(effect, null)), error)));
cell.dynamics = cell.initialDynamics;
cell.elapsedTime = ZERO;
}

@Override
public void step(Cell<D> cell, Duration duration) {
// Avoid accumulated round-off error in imperfect stepping
// by always stepping up from the initial dynamics
cell.elapsedTime = cell.elapsedTime.plus(duration);
cell.dynamics = ErrorCatchingMonad.map(cell.initialDynamics, d ->
expiring(d.data().step(cell.elapsedTime), d.expiry().minus(cell.elapsedTime)));
}
});
public static <D extends Dynamics<?, D>, E extends DynamicsEffect<D>> CellRef<E, Cell<D>> allocate(
InconBehavior<ErrorCatching<Expiring<D>>> inconBehavior,
EffectTrait<E> effectTrait) {
// Building the invertible function below ties together two key functions as de-facto inverses:
// 1. Reading the incon and using it to initialize the cell.
// 2. Reading the cell and using it to write the fincon.
// Registering that mapped incon behavior here then ensures that we always register the incon behavior,
// without the modeler needing to manage incons as a separate step, which they may forget / mismanage.
return InitialConditionManager.register(inconBehavior.map(InvertibleFunction.of(
initialDynamics -> CellRef.allocate(new Cell<>(initialDynamics), new CellType<>() {
@Override
public EffectTrait<E> getEffectType() {
return effectTrait;
}

@Override
public Cell<D> duplicate(Cell<D> cell) {
return new Cell<>(cell.initialDynamics, cell.dynamics, cell.elapsedTime);
}

@Override
public void apply(Cell<D> cell, E effect) {
cell.initialDynamics = effect.apply(cell.dynamics).match(
ErrorCatching::success,
error -> failure(new RuntimeException(
"Applying effect '%s' failed.".formatted(getName(effect, null)), error)));
cell.dynamics = cell.initialDynamics;
cell.elapsedTime = ZERO;
}

@Override
public void step(Cell<D> cell, Duration duration) {
// Avoid accumulated round-off error in imperfect stepping
// by always stepping up from the initial dynamics
cell.elapsedTime = cell.elapsedTime.plus(duration);
cell.dynamics = ErrorCatchingMonad.map(cell.initialDynamics, d ->
expiring(d.data().step(cell.elapsedTime), d.expiry().minus(cell.elapsedTime)));
}
}),
cellRef -> cellRef.get().dynamics
)));
}

public static <D extends Dynamics<?, D>> EffectTrait<DynamicsEffect<D>> noncommutingEffects() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package gov.nasa.jpl.aerie.contrib.streamline.core;

import gov.nasa.jpl.aerie.contrib.streamline.utils.InvertibleFunction;
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;

import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
* Combines the operations of getting initial state and saving final state,
* since these operations are intentionally closely coupled.
*/
public interface InconBehavior<T> {
T getIncon(InitialConditionManager.InitialConditions initialConditions);

void writeFincon(T state, InitialConditionManager.FinalConditions finalConditions);

static <T> InconBehavior<T> of(Function<InitialConditionManager.InitialConditions, T> getIncon, BiConsumer<T, InitialConditionManager.FinalConditions> writeFincon) {
return new InconBehavior<T>() {
@Override
public T getIncon(InitialConditionManager.InitialConditions initialConditions) {
return getIncon.apply(initialConditions);
}

@Override
public void writeFincon(T state, InitialConditionManager.FinalConditions finalConditions) {
writeFincon.accept(state, finalConditions);
}
};
}

/**
* The null case of incon behavior, when in fact the state does not get saved out.
*/
static <T> InconBehavior<T> constant(T value) {
return InconBehavior.of($ -> value, (s, f) -> {});
}

/**
* The standard case of incon behavior, where the state is serialized out under a single key.
*/
static <T> InconBehavior<T> serialized(String key, Function<Optional<SerializedValue>, T> constructor, Function<T, SerializedValue> serializer) {
return InconBehavior.of(
incons -> constructor.apply(incons.get(key)),
(state, fincons) -> fincons.put(key, serializer.apply(state)));
}

/**
* Extend this {@link InconBehavior} with an invertible function.
* <p>
* In particular, this can be used to apply {@link Resource} wrappers around the initial state.
* </p>
*/
default <U> InconBehavior<U> map(InvertibleFunction<T, U> f) {
return InconBehavior.of(
f.compose(this::getIncon),
(state, fincons) -> this.writeFincon(f.inverse().apply(state), fincons));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package gov.nasa.jpl.aerie.contrib.streamline.core;

import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;

import java.util.*;
import java.util.function.Consumer;

public final class InitialConditionManager {
private InitialConditionManager() {}

private static boolean initialized = false;
private static InitialConditions initialConditions;
private static Consumer<Map<String, SerializedValue>> finconHandler;
private static List<Consumer<FinalConditions>> finconHooks;

public static void init(InitialConditions initialConditions, Consumer<Map<String, SerializedValue>> finconHandler) {
if (initialized) {
throw new IllegalStateException("InitialConditionManager has already been initialized");
}

InitialConditionManager.initialConditions = initialConditions;
InitialConditionManager.finconHandler = finconHandler;
InitialConditionManager.finconHooks = new ArrayList<>();
initialized = true;
}

// The "correct" way to get an initial value also registers a way to write the final value.
// This is intended to remind the modeler that these operations are closely coupled.
public static <T> T register(InconBehavior<T> behavior) {
if (!initialized) {
throw new IllegalStateException("InitialConditionManager has not been initialized");
}

var result = behavior.getIncon(initialConditions);
// This looks admittedly strange, but the intent is for result to be something like a resource,
// which is a stable handle for a state that changes over the course of the simulation.
// This closure captures that stable handle, with the intent of querying it later to capture the final state.
finconHooks.add($ -> behavior.writeFincon(result, $));
return result;
}

public static void writeFincon() {
var transparentFincons = new HashMap<String, SerializedValue>();
// opaqueFincons is a write-only view of transparentFincons
var opaqueFincons = FinalConditions.of(transparentFincons);
// Each fincon hook appends its portion of the fincons
for (var finconHook : finconHooks) {
finconHook.accept(opaqueFincons);
}
// Finally we write the full object out
finconHandler.accept(transparentFincons);
}


public interface InitialConditions {
Optional<SerializedValue> get(String key);

static InitialConditions of(Map<String, SerializedValue> map) {
return key -> Optional.ofNullable(map.get(key));
}
}

public interface FinalConditions {
void put(String key, SerializedValue value);

static FinalConditions of(Map<String, SerializedValue> map) {
return (key, value) -> {
if (map.putIfAbsent(key, value) != null) {
throw new IllegalStateException(String.format(
"Final condition has already been written for %s", key));
}
};
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@
import gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling;
import gov.nasa.jpl.aerie.merlin.framework.CellRef;
import gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.Cell;
import gov.nasa.jpl.aerie.merlin.framework.Result;
import gov.nasa.jpl.aerie.merlin.framework.ValueMapper;
import gov.nasa.jpl.aerie.merlin.protocol.model.EffectTrait;
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;
import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema;

import java.util.Map;
import java.util.Optional;

import static gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers.duration;
import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.allocate;
import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects;
import static gov.nasa.jpl.aerie.contrib.streamline.core.Expiry.expiry;
import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.pure;
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*;
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Profiling.profile;
Expand All @@ -25,24 +34,18 @@ default void emit(String effectName, DynamicsEffect<D> effect) {
emit(name(effect, effectName));
}

static <D extends Dynamics<?, D>> MutableResource<D> resource(D initial) {
return resource(pure(initial));
}

static <D extends Dynamics<?, D>> MutableResource<D> resource(D initial, EffectTrait<DynamicsEffect<D>> effectTrait) {
return resource(pure(initial), effectTrait);
}

static <D extends Dynamics<?, D>> MutableResource<D> resource(ErrorCatching<Expiring<D>> initial) {
static <D extends Dynamics<?, D>> MutableResource<D> resource(InconBehavior<ErrorCatching<Expiring<D>>> inconBehavior) {
// Use autoEffects for a generic CellResource, on the theory that most resources
// have relatively few effects, and even fewer concurrent effects, so this is performant enough.
// If that doesn't hold, a more specialized solution can be constructed directly.
return resource(initial, autoEffects());
return resource(inconBehavior, autoEffects());
}

static <D extends Dynamics<?, D>> MutableResource<D> resource(ErrorCatching<Expiring<D>> initial, EffectTrait<DynamicsEffect<D>> effectTrait) {
static <D extends Dynamics<?, D>> MutableResource<D> resource(
InconBehavior<ErrorCatching<Expiring<D>>> inconBehavior,
EffectTrait<DynamicsEffect<D>> effectTrait) {
MutableResource<D> result = new MutableResource<>() {
private final CellRef<DynamicsEffect<D>, Cell<D>> cell = allocate(initial, effectTrait);
private final CellRef<DynamicsEffect<D>, Cell<D>> cell = allocate(inconBehavior, effectTrait);

@Override
public void emit(final DynamicsEffect<D> effect) {
Expand All @@ -68,6 +71,72 @@ public ErrorCatching<Expiring<D>> getDynamics() {
return result;
}

static <D> InconBehavior<ErrorCatching<Expiring<D>>> notSaving(D initialValue) {
return notSaving(pure(initialValue));
}

static <D> InconBehavior<ErrorCatching<Expiring<D>>> notSaving(ErrorCatching<Expiring<D>> initialValue) {
return InconBehavior.constant(initialValue);
}

// TODO - It would be nice if the name we set here could somehow auto-populate the name of the resource,
// and also be the name we register the resource as. Same for the value mapper, it would be nice to just use that for registration too.
// Alternatively, we could demand a name for every MutableResource, and combine that with the other info here later...?
static <D> InconBehavior<ErrorCatching<Expiring<D>>> serializing(String key, D defaultValue, ValueMapper<D> mapper) {
return serializing(key, pure(defaultValue), standardDynamicsMapper(mapper));
}

private static <D> ValueMapper<ErrorCatching<Expiring<D>>> standardDynamicsMapper(ValueMapper<D> baseMapper) {
return new ValueMapper<>() {
@Override
public ValueSchema getValueSchema() {
// Note: Both errorMessage and expiry are nullable.
return ValueSchema.ofStruct(Map.of(
"error", ValueSchema.STRING,
"expiry", ValueSchema.DURATION,
"dynamics", baseMapper.getValueSchema()));
}

@Override
public SerializedValue serializeValue(ErrorCatching<Expiring<D>> value) {
return value.match(
success -> SerializedValue.of(Map.of(
"expiry", success.expiry().value().map(duration()::serializeValue).orElse(SerializedValue.NULL),
"dynamics", baseMapper.serializeValue(success.data())
)),
error -> SerializedValue.of(Map.of(
"error", SerializedValue.of(error.getMessage())
))
);
}

@Override
public Result<ErrorCatching<Expiring<D>>, String> deserializeValue(SerializedValue serializedValue) {
try {
var map = serializedValue.asMap().orElseThrow();
if (map.containsKey("error")) {
return Result.success(ErrorCatching.failure(new Exception(map.get("error").asString().orElseThrow())));
} else {
var expiry = expiry(Optional.ofNullable(map.get("expiry"))
.map($ -> duration().deserializeValue($).getSuccessOrThrow()));
var dynamics = baseMapper.deserializeValue(map.get("dynamics")).getSuccessOrThrow();
return Result.success(ErrorCatching.success(Expiring.expiring(dynamics, expiry)));
}
} catch (Throwable e) {
// TODO - we need *way* better error reporting here, but I just can't be bothered tonight.
return Result.failure("Failed to deserialize value as a standard wrapped dynamics object.");
}
}
};
}

static <D> InconBehavior<ErrorCatching<Expiring<D>>> serializing(String key, ErrorCatching<Expiring<D>> defaultValue, ValueMapper<ErrorCatching<Expiring<D>>> mapper) {
return InconBehavior.serialized(
key,
serializedValue -> serializedValue.map($ -> mapper.deserializeValue($).getSuccessOrThrow()).orElse(defaultValue),
mapper::serializeValue);
}

static <D extends Dynamics<?, D>> void set(MutableResource<D> resource, D newDynamics) {
resource.emit(name(DynamicsMonad.effect(x -> newDynamics), "Set %s", newDynamics));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gov.nasa.jpl.aerie.contrib.streamline.utils;

import java.util.function.Function;

public interface InvertibleFunction<A, B> extends Function<A, B> {
InvertibleFunction<B, A> inverse();

default <C> InvertibleFunction<C, B> compose(InvertibleFunction<C, A> before) {
return new InvertibleFunction<>() {
@Override
public InvertibleFunction<B, C> inverse() {
return before.inverse().<B>compose(InvertibleFunction.this.inverse());
}

@Override
public B apply(C c) {
return InvertibleFunction.this.apply(before.apply(c));
}
};
}

default <C> InvertibleFunction<A, C> andThen(InvertibleFunction<B, C> after) {
return after.compose(this);
}

static <A, B> InvertibleFunction<A, B> of(Function<A, B> map, Function<B, A> inverse) {
return new InvertibleFunction<>() {
@Override
public B apply(A a) {
return map.apply(a);
}

@Override
public InvertibleFunction<B, A> inverse() {
return InvertibleFunction.of(inverse, map);
}
};
}
}

0 comments on commit d442c50

Please sign in to comment.