Skip to content

Commit

Permalink
Add InstantClock and VariableInstantClock support
Browse files Browse the repository at this point in the history
Adds "Instant" counterparts to Clock and VariableClock, which report absolute times as Java instants, but still step time according to an Aerie duration.
Also adds some minimal interoperability between absolute and relative clocks, and some useful derivations including comparisons.

Using these, also adds a global Instant-based clock to Resources, along with exposing both the duration- and instant-based clocks as resources.

In doing so, we add a new parameter for the plan's start time to Resources.init().
This in turn required refactoring some unit tests, and I took the opportunity to clean up the test construction a little bit as well.
This revealed a way to correctly initialize Resources, i.e., to call Resources.init() exactly once per initialization of each test model.
As a result, I refactored the clock handling to remove the awkward reinitialization pattern, since that pattern was added to handle test code.
  • Loading branch information
David Legg committed Oct 30, 2024
1 parent 524474c commit 7e85f30
Show file tree
Hide file tree
Showing 15 changed files with 291 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package gov.nasa.jpl.aerie.contrib.streamline.core;

import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.InstantClock;
import gov.nasa.jpl.aerie.merlin.framework.Condition;
import gov.nasa.jpl.aerie.merlin.framework.Scoped;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
import gov.nasa.jpl.aerie.merlin.protocol.types.Unit;

import java.time.Instant;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -20,6 +22,7 @@
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Dependencies.addDependency;
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.*;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock.clock;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.InstantClockResources.addToInstant;
import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.*;
import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete;
Expand All @@ -38,22 +41,26 @@ private Resources() {}
* This method is idempotent; calling it multiple times is the same as calling it once.
* </p>
*/
public static void init() {
currentTime();
public static void init(Instant planStart) {
CLOCK = resource(clock(ZERO));
ABSOLUTE_CLOCK = name(addToInstant(planStart, CLOCK), "Global Absolute Simulation Clock");
}

// TODO if Aerie provides either a `getElapsedTime` method or dynamic allocation of Cells, we can avoid this mutable static variable
private static Resource<Clock> CLOCK = resource(clock(ZERO));
private static Resource<Clock> CLOCK;
private static Resource<InstantClock> ABSOLUTE_CLOCK;
public static Duration currentTime() {
try {
return currentValue(CLOCK);
} catch (Scoped.EmptyDynamicCellException | IllegalArgumentException e) {
// If we're running unit tests, several simulations can happen without reloading the Resources class.
// In that case, we'll have discarded the clock resource we were using, and get the above exception.
// REVIEW: Is there a cleaner way to make sure this resource gets (re-)initialized?
CLOCK = resource(clock(ZERO));
return currentValue(CLOCK);
}
return currentValue(CLOCK);
}
public static Instant currentInstant() {
return currentValue(ABSOLUTE_CLOCK);
}

public static Resource<Clock> simulationClock() {
return CLOCK;
}
public static Resource<InstantClock> absoluteClock() {
return ABSOLUTE_CLOCK;
}

public static <D> D currentData(Resource<D> resource) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics;
import gov.nasa.jpl.aerie.merlin.protocol.types.Unit;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.Reactions.whenever;
import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentData;
import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue;
Expand Down Expand Up @@ -54,8 +56,8 @@ public enum ErrorBehavior {
Throw
}

public Registrar(final gov.nasa.jpl.aerie.merlin.framework.Registrar baseRegistrar, final ErrorBehavior errorBehavior) {
Resources.init();
public Registrar(final gov.nasa.jpl.aerie.merlin.framework.Registrar baseRegistrar, final Instant planStart, final ErrorBehavior errorBehavior) {
Resources.init(planStart);
Logging.init(baseRegistrar);
this.baseRegistrar = baseRegistrar;
this.errorBehavior = errorBehavior;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks;

import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.time.Instant;
import java.time.temporal.ChronoUnit;

import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.addToInstant;

/**
* A variation on {@link Clock} that represents an absolute {@link Instant}
* instead of a relative {@link Duration}.
*/
public record InstantClock(Instant extract) implements Dynamics<Instant, InstantClock> {
@Override
public InstantClock step(Duration t) {
return new InstantClock(addToInstant(extract, t));
}

static Duration durationBetween(Instant start, Instant end) {
return Duration.of(ChronoUnit.MICROS.between(start, end), Duration.MICROSECONDS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks;

import gov.nasa.jpl.aerie.contrib.streamline.core.*;
import gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource;
import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map;
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.constant;

public class InstantClockResources {
/**
* Create an absolute clock that starts now at the given start time.
*/
public static MutableResource<InstantClock> absoluteClock(Instant startTime) {
return resource(new InstantClock(startTime));
}

public static Resource<InstantClock> addToInstant(Instant zeroTime, Resource<Clock> relativeClock) {
return addToInstant(constant(zeroTime), relativeClock);
}

public static Resource<InstantClock> addToInstant(Resource<Discrete<Instant>> zeroTime, Resource<Clock> relativeClock) {
return name(
map(zeroTime, relativeClock, (zero, clock) ->
new InstantClock(Duration.addToInstant(zero.extract(), clock.extract()))),
"%s + %s",
zeroTime,
relativeClock);
}

public static Resource<Clock> relativeTo(Resource<InstantClock> clock, Resource<Discrete<Instant>> zeroTime) {
return name(ResourceMonad.map(clock, zeroTime, (c, t) -> new Clock(InstantClock.durationBetween(t.extract(), c.extract()))),
"%s relative to %s", clock, zeroTime);
}

public static Resource<Discrete<Boolean>> lessThan(Resource<InstantClock> clock, Resource<Discrete<Instant>> threshold) {
return ClockResources.lessThan(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<Discrete<Boolean>> lessThanOrEquals(Resource<InstantClock> clock, Resource<Discrete<Instant>> threshold) {
return ClockResources.lessThanOrEquals(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<Discrete<Boolean>> greaterThan(Resource<InstantClock> clock, Resource<Discrete<Instant>> threshold) {
return ClockResources.greaterThan(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<Discrete<Boolean>> greaterThanOrEquals(Resource<InstantClock> clock, Resource<Discrete<Instant>> threshold) {
return ClockResources.greaterThanOrEquals(relativeTo(clock, threshold), constant(Duration.ZERO));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks;

import gov.nasa.jpl.aerie.contrib.streamline.core.Dynamics;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.time.Instant;

import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.addToInstant;

/**
* A variation on {@link VariableClock} that represents an absolute {@link Instant}
* instead of a relative {@link Duration}.
*/
public record VariableInstantClock(Instant extract, int multiplier) implements Dynamics<Instant, VariableInstantClock> {
@Override
public VariableInstantClock step(Duration t) {
return new VariableInstantClock(addToInstant(extract, t.times(multiplier)), multiplier);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks;

import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.DynamicsMonad.effect;
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name;

public final class VariableInstantClockEffects {
private VariableInstantClockEffects() {}

/**
* Stop the clock without affecting the current time.
*/
public static void pause(MutableResource<VariableInstantClock> clock) {
clock.emit("Pause", effect($ -> new VariableInstantClock($.extract(), 0)));
}

/**
* Start the clock without affecting the current time.
*/
public static void start(MutableResource<VariableInstantClock> clock) {
clock.emit("Start", effect($ -> new VariableInstantClock($.extract(), 1)));
}

/**
* Reset the clock to the given time, without affecting how fast it's running.
*/
public static void reset(MutableResource<VariableInstantClock> clock, Instant newTime) {
clock.emit(name(effect($ -> new VariableInstantClock(newTime, $.multiplier())), "Reset to %s", newTime));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks;

import gov.nasa.jpl.aerie.contrib.streamline.core.Resource;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.map;
import static gov.nasa.jpl.aerie.contrib.streamline.debugging.Naming.name;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.InstantClock.durationBetween;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources.constant;

public final class VariableInstantClockResources {
private VariableInstantClockResources() {}

public static Resource<VariableClock> relativeTo(Resource<VariableInstantClock> clock, Resource<Discrete<Instant>> zeroTime) {
return name(map(clock, zeroTime, (c, t) ->
new VariableClock(durationBetween(c.extract(), t.extract()), c.multiplier())),
"%s relative to %s", clock, zeroTime);
}

public static Resource<Discrete<Boolean>> lessThan(Resource<VariableInstantClock> clock, Resource<Discrete<Instant>> threshold) {
return VariableClockResources.lessThan(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<Discrete<Boolean>> lessThanOrEquals(Resource<VariableInstantClock> clock, Resource<Discrete<Instant>> threshold) {
return VariableClockResources.lessThanOrEquals(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<Discrete<Boolean>> greaterThan(Resource<VariableInstantClock> clock, Resource<Discrete<Instant>> threshold) {
return VariableClockResources.greaterThan(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<Discrete<Boolean>> greaterThanOrEquals(Resource<VariableInstantClock> clock, Resource<Discrete<Instant>> threshold) {
return VariableClockResources.greaterThanOrEquals(relativeTo(clock, threshold), constant(Duration.ZERO));
}

public static Resource<VariableClock> between(Resource<VariableInstantClock> start, Resource<VariableInstantClock> end) {
return map(start, end, (s, e) -> new VariableClock(durationBetween(s.extract(), e.extract()), e.multiplier() - s.multiplier()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.autoEffects;
import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.commutingEffects;
import static gov.nasa.jpl.aerie.contrib.streamline.core.CellRefV2.noncommutingEffects;
import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource;
import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.monads.DiscreteDynamicsMonad.effect;
Expand All @@ -27,10 +28,11 @@ class MutableResourceTest {
@TestInstance(Lifecycle.PER_CLASS)
class NonCommutingEffects {
public NonCommutingEffects(final Registrar registrar) {
Resources.init();
Resources.init(Instant.EPOCH);
cell = MutableResource.resource(discrete(42), noncommutingEffects());
}

private final MutableResource<Discrete<Integer>> cell = MutableResource.resource(discrete(42), noncommutingEffects());
private final MutableResource<Discrete<Integer>> cell;

@Test
void gets_initial_value_if_no_effects_are_emitted() {
Expand Down Expand Up @@ -66,10 +68,11 @@ void throws_exception_when_concurrent_effects_are_applied() {
@TestInstance(Lifecycle.PER_CLASS)
class CommutingEffects {
public CommutingEffects(final Registrar registrar) {
Resources.init();
Resources.init(Instant.EPOCH);
cell = MutableResource.resource(discrete(42), commutingEffects());
}

private final MutableResource<Discrete<Integer>> cell = MutableResource.resource(discrete(42), commutingEffects());
private final MutableResource<Discrete<Integer>> cell;

@Test
void gets_initial_value_if_no_effects_are_emitted() {
Expand Down Expand Up @@ -108,11 +111,12 @@ void applies_concurrent_effects_in_any_order() {
@ExtendWith(MerlinExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
class AutoEffects {
public AutoEffects(final Registrar registrar) {
Resources.init();
public AutoEffects() {
Resources.init(Instant.EPOCH);
cell = MutableResource.resource(discrete(42), autoEffects());
}

private final MutableResource<Discrete<Integer>> cell = MutableResource.resource(discrete(42), autoEffects());
private final MutableResource<Discrete<Integer>> cell;

@Test
void gets_initial_value_if_no_effects_are_emitted() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gov.nasa.jpl.aerie.contrib.streamline.debugging;

import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource;
import gov.nasa.jpl.aerie.contrib.streamline.core.Resource;
import gov.nasa.jpl.aerie.contrib.streamline.core.Resources;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteResources;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial;
Expand All @@ -11,6 +11,8 @@
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource;
import static gov.nasa.jpl.aerie.contrib.streamline.core.monads.ResourceMonad.*;
import static gov.nasa.jpl.aerie.contrib.streamline.modeling.polynomial.Polynomial.polynomial;
Expand All @@ -21,12 +23,22 @@
@ExtendWith(MerlinExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DependenciesTest {
Resource<Discrete<Boolean>> constantTrue = DiscreteResources.constant(true);
Resource<Polynomial> constant1234 = constant(1234);
Resource<Polynomial> constant5678 = constant(5678);
Resource<Polynomial> polynomialCell = resource(polynomial(1));
Resource<Polynomial> derived = map(constantTrue, constant1234, constant5678,
(b, x, y) -> b.extract() ? x : y);
public DependenciesTest() {
Resources.init(Instant.EPOCH);

constantTrue = DiscreteResources.constant(true);
constant1234 = constant(1234);
constant5678 = constant(5678);
polynomialCell = resource(polynomial(1));
derived = map(constantTrue, constant1234, constant5678,
(b, x, y) -> b.extract() ? x : y);
}

Resource<Discrete<Boolean>> constantTrue;
Resource<Polynomial> constant1234;
Resource<Polynomial> constant5678;
Resource<Polynomial> polynomialCell;
Resource<Polynomial> derived;

@Test
void constants_are_named_by_their_value() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import gov.nasa.jpl.aerie.contrib.streamline.core.Resources;
import gov.nasa.jpl.aerie.contrib.streamline.modeling.clocks.Clock;
import gov.nasa.jpl.aerie.contrib.streamline.unit_aware.UnitAware;
import gov.nasa.jpl.aerie.merlin.framework.Registrar;
import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;

import java.time.Instant;

import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource;
import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentTime;
import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue;
Expand Down Expand Up @@ -40,8 +41,10 @@
@ExtendWith(MerlinExtension.class)
@TestInstance(Lifecycle.PER_CLASS)
class DiscreteEffectsTest {
public DiscreteEffectsTest(final Registrar registrar) {
Resources.init();
{
// We need to initialize this up front, so we can use in-line initializers for other resources after.
// I think in-line initializers for the other resources make the tests easier to read.
Resources.init(Instant.EPOCH);
}

private final MutableResource<Discrete<Integer>> settable = resource(discrete(42));
Expand Down
Loading

0 comments on commit 7e85f30

Please sign in to comment.