From 7029aeb7664094436596ff81c12cce67f529106b Mon Sep 17 00:00:00 2001
From: Marsette Vona
Date: Thu, 15 Feb 2024 15:50:37 -0500
Subject: [PATCH 1/6] reduce memory usage for tasks in AwaitingChildren state
---
.../nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
index efbddb54f2..2a5eb170c7 100644
--- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
+++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
@@ -208,7 +208,8 @@ private void stepEffectModel(
// Based on the task's return status, update its execution state and schedule its resumption.
if (status instanceof TaskStatus.Completed) {
- final var children = new LinkedList<>(this.taskChildren.getOrDefault(task, Collections.emptySet()));
+ final var children = new LinkedList<>(Optional.ofNullable(this.taskChildren.remove(task))
+ .orElseGet(Collections::emptySet));
this.tasks.put(task, progress.completedAt(currentTime, children));
this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime));
From 43959cf2dcc081f7dbda7058579aba771b2dbab0 Mon Sep 17 00:00:00 2001
From: Marsette Vona
Date: Thu, 8 Feb 2024 09:37:22 -0500
Subject: [PATCH 2/6] add tail call optimization for tasks
---
.../driver/engine/SimulationEngine.java | 21 ++++++++++++-------
.../jpl/aerie/merlin/framework/Context.java | 4 ++++
.../framework/InitializationContext.java | 5 +++++
.../aerie/merlin/framework/ModelActions.java | 12 +++++++++++
.../aerie/merlin/framework/QueryContext.java | 5 +++++
.../framework/ReplayingReactionContext.java | 8 +++++++
.../aerie/merlin/framework/ReplayingTask.java | 5 +++++
.../aerie/merlin/framework/TaskHandle.java | 2 ++
.../framework/ThreadedReactionContext.java | 6 ++++++
.../aerie/merlin/framework/ThreadedTask.java | 5 +++++
.../merlin/protocol/types/TaskStatus.java | 9 ++++++--
11 files changed, 72 insertions(+), 10 deletions(-)
diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
index 2a5eb170c7..bfc2a87ec1 100644
--- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
+++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
@@ -219,14 +219,19 @@ private void stepEffectModel(
this.tasks.put(task, progress.continueWith(s.continuation()));
this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime.plus(s.delay())));
} else if (status instanceof TaskStatus.CallingTask s) {
- final var target = TaskId.generate();
- SimulationEngine.this.tasks.put(target, new ExecutionState.InProgress<>(currentTime, s.child().create(this.executor)));
- SimulationEngine.this.taskParent.put(target, task);
- SimulationEngine.this.taskChildren.computeIfAbsent(task, $ -> new HashSet<>()).add(target);
- frame.signal(JobId.forTask(target));
-
- this.tasks.put(task, progress.continueWith(s.continuation()));
- this.waitingTasks.subscribeQuery(task, Set.of(SignalId.forTask(target)));
+ if (s.tailCall()) {
+ this.tasks.put(task, new ExecutionState.InProgress<>(progress.startOffset, s.child().create(this.executor)));
+ this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(currentTime));
+ } else {
+ final var target = TaskId.generate();
+ this.tasks.put(target, new ExecutionState.InProgress<>(currentTime, s.child().create(this.executor)));
+ this.taskParent.put(target, task);
+ this.taskChildren.computeIfAbsent(task, $ -> new HashSet<>()).add(target);
+ frame.signal(JobId.forTask(target));
+
+ this.tasks.put(task, progress.continueWith(s.continuation()));
+ this.waitingTasks.subscribeQuery(task, Set.of(SignalId.forTask(target)));
+ }
} else if (status instanceof TaskStatus.AwaitingCondition s) {
final var condition = ConditionId.generate();
this.conditions.put(condition, s.condition());
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java
index 6738237a37..8d90e41e8b 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Context.java
@@ -30,8 +30,12 @@ enum ContextType { Initializing, Reacting, Querying }
void emit(Event event, Topic topic);
void spawn(TaskFactory> task);
+
void call(TaskFactory task);
+ void tailCall(TaskFactory task);
+
void delay(Duration duration);
+
void waitUntil(Condition condition);
}
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java
index 9615830a69..adccdfc2c3 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/InitializationContext.java
@@ -61,6 +61,11 @@ public void call(final TaskFactory task) {
throw new IllegalStateException("Cannot yield during initialization");
}
+ @Override
+ public void tailCall(final TaskFactory task) {
+ throw new IllegalStateException("Cannot yield during initialization");
+ }
+
@Override
public void delay(final Duration duration) {
throw new IllegalStateException("Cannot yield during initialization");
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java
index 1c9019f328..5372c20fa4 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ModelActions.java
@@ -69,6 +69,18 @@ public static void call(final TaskFactory task) {
context.get().call(task);
}
+ public static void tailCall(final Runnable task) {
+ tailCall(threaded(task));
+ }
+
+ public static void tailCall(final Supplier task) {
+ tailCall(threaded(task));
+ }
+
+ public static void tailCall(final TaskFactory task) {
+ context.get().tailCall(task);
+ }
+
public static void defer(final Duration duration, final Runnable task) {
spawn(replaying(() -> { delay(duration); spawn(task); }));
}
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java
index 2425a9ab77..f20cb197e6 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/QueryContext.java
@@ -52,6 +52,11 @@ public void call(final TaskFactory task) {
throw new IllegalStateException("Cannot schedule tasks in a query-only context");
}
+ @Override
+ public void tailCall(final TaskFactory task) {
+ throw new IllegalStateException("Cannot schedule tasks in a query-only context");
+ }
+
@Override
public void delay(final Duration duration) {
throw new IllegalStateException("Cannot yield in a query-only context");
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java
index e9998ff256..f093dba2a9 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingReactionContext.java
@@ -78,6 +78,14 @@ public void call(final TaskFactory task) {
});
}
+ @Override
+ public void tailCall(final TaskFactory task) {
+ this.memory.doOnce(() -> {
+ this.scheduler = null; // Relinquish the current scheduler before yielding, in case an exception is thrown.
+ this.scheduler = this.handle.tailCall(task);
+ });
+ }
+
@Override
public void delay(final Duration duration) {
this.memory.doOnce(() -> {
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java
index b18f38f6ca..0ad2ad3dbd 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java
@@ -56,6 +56,11 @@ public Scheduler call(final TaskFactory> child) {
return this.yield(TaskStatus.calling(child, ReplayingTask.this));
}
+ @Override
+ public Scheduler tailCall(final TaskFactory> child) {
+ return this.yield(TaskStatus.tailCalling(child, ReplayingTask.this));
+ }
+
@Override
public Scheduler await(final gov.nasa.jpl.aerie.merlin.protocol.model.Condition condition) {
return this.yield(TaskStatus.awaiting(condition, ReplayingTask.this));
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java
index 609453f0c9..4afe2da452 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/TaskHandle.java
@@ -9,5 +9,7 @@ public interface TaskHandle {
Scheduler call(TaskFactory> child);
+ Scheduler tailCall(TaskFactory> child);
+
Scheduler await(gov.nasa.jpl.aerie.merlin.protocol.model.Condition condition);
}
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java
index 3edd568d9e..3f72af4edd 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java
@@ -63,6 +63,12 @@ public void call(final TaskFactory task) {
this.scheduler = this.handle.call(task);
}
+ @Override
+ public void tailCall(final TaskFactory task) {
+ this.scheduler = null; // Relinquish the current scheduler before yielding, in case an exception is thrown.
+ this.scheduler = this.handle.tailCall(task);
+ }
+
@Override
public void delay(final Duration duration) {
this.scheduler = null; // Relinquish the current scheduler before yielding, in case an exception is thrown.
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java
index 58e3e3b626..84c54aaade 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java
@@ -207,6 +207,11 @@ public Scheduler call(final TaskFactory> child) {
return this.yield(TaskStatus.calling(child, ThreadedTask.this));
}
+ @Override
+ public Scheduler tailCall(final TaskFactory> child) {
+ return this.yield(TaskStatus.tailCalling(child, ThreadedTask.this));
+ }
+
@Override
public Scheduler await(final gov.nasa.jpl.aerie.merlin.protocol.model.Condition condition) {
return this.yield(TaskStatus.awaiting(condition, ThreadedTask.this));
diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java
index 178b320ba4..2975aba538 100644
--- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java
+++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/TaskStatus.java
@@ -9,7 +9,8 @@ record Completed(Return returnValue) implements TaskStatus {}
record Delayed(Duration delay, Task continuation) implements TaskStatus {}
- record CallingTask(TaskFactory> child, Task continuation) implements TaskStatus {}
+ record CallingTask(TaskFactory> child, Task continuation, boolean tailCall)
+ implements TaskStatus {}
record AwaitingCondition(Condition condition, Task continuation) implements TaskStatus {}
@@ -23,7 +24,11 @@ static Delayed delayed(final Duration delay, final Task
}
static CallingTask calling(final TaskFactory> child, final Task continuation) {
- return new CallingTask<>(child, continuation);
+ return new CallingTask<>(child, continuation, false);
+ }
+
+ static CallingTask tailCalling(final TaskFactory> child, final Task continuation) {
+ return new CallingTask<>(child, continuation, true);
}
static AwaitingCondition awaiting(final Condition condition, final Task continuation) {
From 75267843a219839bac4d594935ef560610974dca Mon Sep 17 00:00:00 2001
From: Marsette Vona
Date: Thu, 8 Feb 2024 19:46:29 -0500
Subject: [PATCH 3/6] avoid BigDecimal when creating SerializedValue from
double, int, or long
---
.../protocol/types/SerializedValue.java | 80 +++++++++++++++++--
1 file changed, 75 insertions(+), 5 deletions(-)
diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
index 3187d8051f..69731c64b6 100644
--- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
+++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
@@ -1,6 +1,7 @@
package gov.nasa.jpl.aerie.merlin.protocol.types;
import java.math.BigDecimal;
+import java.math.MathContext;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -67,6 +68,10 @@ public T match(final Visitor visitor) {
}
}
+ interface DirectNumericValue {
+ NumericValue asNumericValue();
+ }
+
record NumericValue(BigDecimal value) implements SerializedValue {
@Override
public T match(final Visitor visitor) {
@@ -76,8 +81,12 @@ public T match(final Visitor visitor) {
// `BigDecimal#equals` is too strict -- values differing only in representation need to be considered the same.
@Override
public boolean equals(final Object obj) {
- if (!(obj instanceof NumericValue other)) return false;
- return (this.value.compareTo(other.value) == 0);
+ if (obj instanceof NumericValue other) {
+ return (this.value.compareTo(other.value) == 0);
+ } else if (obj instanceof DirectNumericValue other) {
+ return (this.value.compareTo(other.asNumericValue().value) == 0);
+ }
+ return false;
}
@Override
@@ -86,6 +95,57 @@ public int hashCode() {
}
}
+ record IntValue(int value) implements SerializedValue, DirectNumericValue {
+ @Override
+ public T match(final Visitor visitor) {
+ return visitor.onNumeric(new BigDecimal(value));
+ }
+
+ @Override
+ public NumericValue asNumericValue() {
+ return new NumericValue(new BigDecimal(value));
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return asNumericValue().equals(obj);
+ }
+ }
+
+ record LongValue(long value) implements SerializedValue, DirectNumericValue {
+ @Override
+ public T match(final Visitor visitor) {
+ return visitor.onNumeric(new BigDecimal(value));
+ }
+
+ @Override
+ public NumericValue asNumericValue() {
+ return new NumericValue(new BigDecimal(value));
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return asNumericValue().equals(obj);
+ }
+ }
+
+ record DoubleValue(double value) implements SerializedValue, DirectNumericValue {
+ @Override
+ public T match(final Visitor visitor) {
+ return visitor.onNumeric(new BigDecimal(value, MathContext.DECIMAL64));
+ }
+
+ @Override
+ public NumericValue asNumericValue() {
+ return new NumericValue(new BigDecimal(value, MathContext.DECIMAL64));
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ return asNumericValue().equals(obj);
+ }
+ }
+
record BooleanValue(boolean value) implements SerializedValue {
@Override
public T match(final Visitor visitor) {
@@ -136,11 +196,11 @@ static SerializedValue of(final BigDecimal value) {
/**
* Creates a {@link SerializedValue} containing a real number.
*
- * @param value Any double} value.
+ * @param value Any double value.
* @return A new {@link SerializedValue} containing a real number.
*/
static SerializedValue of(final double value) {
- return new NumericValue(BigDecimal.valueOf(value));
+ return new DoubleValue(value);
}
/**
@@ -150,7 +210,17 @@ static SerializedValue of(final double value) {
* @return A new {@link SerializedValue} containing an integral number.
*/
static SerializedValue of(final long value) {
- return new NumericValue(BigDecimal.valueOf(value));
+ return new LongValue(value);
+ }
+
+ /**
+ * Creates a {@link SerializedValue} containing an integral number.
+ *
+ * @param value Any integer value.
+ * @return A new {@link SerializedValue} containing an integral number.
+ */
+ static SerializedValue of(final int value) {
+ return new IntValue(value);
}
/**
From 6291dfcea9fd881876756733beece204c63f67fd Mon Sep 17 00:00:00 2001
From: Marsette Vona
Date: Thu, 8 Feb 2024 19:47:43 -0500
Subject: [PATCH 4/6] implement option for run length compression in resource
profiles
---
.../driver/engine/SimulationEngine.java | 37 ++++++++++++++-----
.../jpl/aerie/merlin/framework/Registrar.java | 29 +++++++++++++--
.../aerie/merlin/protocol/model/Resource.java | 14 +++++++
3 files changed, 67 insertions(+), 13 deletions(-)
diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
index bfc2a87ec1..0a71b8b3ae 100644
--- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
+++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java
@@ -444,19 +444,20 @@ public static SimulationResults computeResults(
final var name = id.id();
final var resource = state.resource();
+ final boolean allowRLE = resource.allowRunLengthCompression();
switch (resource.getType()) {
case "real" -> realProfiles.put(
name,
Pair.of(
resource.getOutputType().getSchema(),
- serializeProfile(elapsedTime, state, SimulationEngine::extractRealDynamics)));
+ serializeProfile(elapsedTime, state, SimulationEngine::extractRealDynamics, allowRLE)));
case "discrete" -> discreteProfiles.put(
name,
Pair.of(
resource.getOutputType().getSchema(),
- serializeProfile(elapsedTime, state, SimulationEngine::extractDiscreteDynamics)));
+ serializeProfile(elapsedTime, state, SimulationEngine::extractDiscreteDynamics, allowRLE)));
default ->
throw new IllegalArgumentException(
@@ -608,11 +609,24 @@ private interface Translator {
Target apply(Resource resource, Dynamics dynamics);
}
+ private static
+ void appendProfileSegment(ArrayList> profile, Duration duration, Target value,
+ boolean allowRunLengthCompression) {
+ final int s = profile.size();
+ final ProfileSegment lastSeg = s > 0 ? profile.get(s - 1) : null;
+ if (allowRunLengthCompression && lastSeg != null && value.equals(lastSeg.dynamics())) {
+ profile.set(s - 1, new ProfileSegment<>(lastSeg.extent().plus(duration), value));
+ } else {
+ profile.add(new ProfileSegment<>(duration, value));
+ }
+ }
+
private static
List> serializeProfile(
final Duration elapsedTime,
final ProfilingState state,
- final Translator translator
+ final Translator translator,
+ final boolean allowRunLengthCompression
) {
final var profile = new ArrayList>(state.profile().segments().size());
@@ -621,18 +635,21 @@ List> serializeProfile(
var segment = iter.next();
while (iter.hasNext()) {
final var nextSegment = iter.next();
-
- profile.add(new ProfileSegment<>(
- nextSegment.startOffset().minus(segment.startOffset()),
- translator.apply(state.resource(), segment.dynamics())));
+ appendProfileSegment(profile,
+ nextSegment.startOffset().minus(segment.startOffset()),
+ translator.apply(state.resource(), segment.dynamics()),
+ allowRunLengthCompression);
segment = nextSegment;
}
- profile.add(new ProfileSegment<>(
- elapsedTime.minus(segment.startOffset()),
- translator.apply(state.resource(), segment.dynamics())));
+ appendProfileSegment(profile,
+ elapsedTime.minus(segment.startOffset()),
+ translator.apply(state.resource(), segment.dynamics()),
+ allowRunLengthCompression);
}
+ profile.trimToSize();
+
return profile;
}
diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java
index f9287e34a5..16522c6df2 100644
--- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java
+++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/Registrar.java
@@ -13,18 +13,34 @@
import java.util.function.UnaryOperator;
public final class Registrar {
+
+ /**
+ * Whether to allow run length compression when saving resource profiles at the end of simulation by default.
+ *
+ * This compression is lossless in terms of the overall shape of the profile, but it will combine adjacent profile
+ * segments with the same value, thus obscuring the fact that multiple resource samples (again, all returning the same
+ * value) were taken within the segment.
+ */
+ private static final boolean ALLOW_RUN_LENGTH_COMPRESSION_BY_DEFAULT = false;
+
private final Initializer builder;
+ private boolean allowRunLengthCompression = ALLOW_RUN_LENGTH_COMPRESSION_BY_DEFAULT;
public Registrar(final Initializer builder) {
this.builder = Objects.requireNonNull(builder);
}
+ public void allowRunLengthCompression(final boolean allow) {
+ this.allowRunLengthCompression = allow;
+ }
+
public boolean isInitializationComplete() {
return (ModelActions.context.get().getContextType() != Context.ContextType.Initializing);
}
public void discrete(final String name, final Resource resource, final ValueMapper mapper) {
- this.builder.resource(name, makeResource("discrete", resource, mapper.getValueSchema(), mapper::serializeValue));
+ this.builder.resource(name, makeResource("discrete", resource, mapper.getValueSchema(), mapper::serializeValue,
+ allowRunLengthCompression));
}
public void real(final String name, final Resource resource) {
@@ -46,14 +62,16 @@ private void real(final String name, final Resource resource, Unar
"rate", ValueSchema.REAL))),
dynamics -> SerializedValue.of(Map.of(
"initial", SerializedValue.of(dynamics.initial),
- "rate", SerializedValue.of(dynamics.rate)))));
+ "rate", SerializedValue.of(dynamics.rate))),
+ allowRunLengthCompression));
}
private static gov.nasa.jpl.aerie.merlin.protocol.model.Resource makeResource(
final String type,
final Resource resource,
final ValueSchema valueSchema,
- final Function serializer
+ final Function serializer,
+ final boolean allowRunLengthCompression
) {
return new gov.nasa.jpl.aerie.merlin.protocol.model.Resource<>() {
@Override
@@ -82,6 +100,11 @@ public Value getDynamics(final Querier querier) {
return resource.getDynamics();
}
}
+
+ @Override
+ public boolean allowRunLengthCompression() {
+ return allowRunLengthCompression;
+ }
};
}
diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Resource.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Resource.java
index 65b1bda7c3..1b9fbbe80d 100644
--- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Resource.java
+++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Resource.java
@@ -16,4 +16,18 @@ public interface Resource {
* this resource. In other words, it cannot depend on any hidden state.
*/
Dynamics getDynamics(Querier querier);
+
+ /**
+ * After a simulation completes the entire evolution of the dynamics of this resource will typically be serialized as
+ * a resource profile consisting of some number of sequential segments.
+ *
+ * If run length compression is allowed for this resource then whenever there is a "run" of two or more such segments,
+ * one after another with the same dynamics, they will be compressed into a single segment during that serialization.
+ * This does not change the represented evolution of the dynamics of the resource, but it loses the information that a
+ * sample was taken at the start of each segment after the first in such a run. If a mission model prefers not to
+ * lose that information then it can return false here.
+ */
+ default boolean allowRunLengthCompression() {
+ return false;
+ }
}
From ae99f9f42594abc5320ee6773121001072622d97 Mon Sep 17 00:00:00 2001
From: Marsette Vona
Date: Tue, 20 Feb 2024 16:56:56 -0500
Subject: [PATCH 5/6] fix e2eTest regression - always serialize double values
as e.g. 1.0 not 1
---
.../protocol/types/SerializedValue.java | 20 +++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
index 69731c64b6..7da3ab6ef9 100644
--- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
+++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
@@ -1,6 +1,7 @@
package gov.nasa.jpl.aerie.merlin.protocol.types;
import java.math.BigDecimal;
+import java.math.BigInteger;
import java.math.MathContext;
import java.util.List;
import java.util.Map;
@@ -132,18 +133,33 @@ public boolean equals(final Object obj) {
record DoubleValue(double value) implements SerializedValue, DirectNumericValue {
@Override
public T match(final Visitor visitor) {
- return visitor.onNumeric(new BigDecimal(value, MathContext.DECIMAL64));
+ return visitor.onNumeric(toBigDecimal());
}
@Override
public NumericValue asNumericValue() {
- return new NumericValue(new BigDecimal(value, MathContext.DECIMAL64));
+ return new NumericValue(toBigDecimal());
}
@Override
public boolean equals(final Object obj) {
return asNumericValue().equals(obj);
}
+
+ private BigDecimal toBigDecimal() {
+ //without MathContext.DECIMAL64 then a double assigned to from a string (or code literal) "3.14"
+ //converts to a BigDecimal=3.140000000000000124344978758017532527446746826171875
+ //but since a double can only represent up to 15 decimal digits when going from string -> double -> string
+ //the nonzero values in the smaller decimal places are just an artifact of the representation
+ //and there are unit tests that assume that string -> double -> string will be an identity op for e.g. 3.14
+ //with MathContext.DECIMAL64 "3.14" converts to a BigDecimal=3.140000000000000
+ var bd = new BigDecimal(value, MathContext.DECIMAL64);
+ if (bd.scale() == 0) { //if the underlying value was actually an integer
+ //we want to always serialize as a real number, i.e. "1.0" not "1" in JSON
+ bd = new BigDecimal(bd.unscaledValue().multiply(BigInteger.valueOf(10)), 1); //yes scale=1 not -1
+ }
+ return bd;
+ }
}
record BooleanValue(boolean value) implements SerializedValue {
From 0794686d1706482ebe65667fc826f5baa8dd67d4 Mon Sep 17 00:00:00 2001
From: Marsette Vona
Date: Tue, 20 Feb 2024 18:29:45 -0500
Subject: [PATCH 6/6] fix grammar in comment
---
.../nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
index 7da3ab6ef9..d7cf409153 100644
--- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
+++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/types/SerializedValue.java
@@ -147,7 +147,7 @@ public boolean equals(final Object obj) {
}
private BigDecimal toBigDecimal() {
- //without MathContext.DECIMAL64 then a double assigned to from a string (or code literal) "3.14"
+ //without MathContext.DECIMAL64 then a double assigned from a string (or code literal) "3.14"
//converts to a BigDecimal=3.140000000000000124344978758017532527446746826171875
//but since a double can only represent up to 15 decimal digits when going from string -> double -> string
//the nonzero values in the smaller decimal places are just an artifact of the representation