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; + } }