diff --git a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/activities/LateRiserActivity.java b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/activities/LateRiserActivity.java new file mode 100644 index 0000000000..bdee8702e0 --- /dev/null +++ b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/activities/LateRiserActivity.java @@ -0,0 +1,18 @@ +package gov.nasa.jpl.aerie.foomissionmodel.activities; + +import gov.nasa.jpl.aerie.foomissionmodel.Mission; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; +import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType.EffectModel; + +/** + * An activity that can't be scheduled in the first 60 seconds of the horizon + */ +@ActivityType("LateRiser") +public record LateRiserActivity() { + @EffectModel + public void run(final Mission mission) { + if (mission.timeTrackerDaemon.getMinutesElapsed() < 1) { + throw new RuntimeException("Can't be scheduled THAT early"); + } + } +} diff --git a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/models/TimeTrackerDaemon.java b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/models/TimeTrackerDaemon.java index 5507d1b6d2..31567a013b 100644 --- a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/models/TimeTrackerDaemon.java +++ b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/models/TimeTrackerDaemon.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.foomissionmodel.models; +import gov.nasa.jpl.aerie.contrib.models.counters.Counter; import gov.nasa.jpl.aerie.merlin.framework.ModelActions; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; @@ -7,21 +8,19 @@ * A daemon task that tracks the number of minutes since plan start */ public class TimeTrackerDaemon { - private int minutesElapsed; + private Counter minutesElapsed; public int getMinutesElapsed() { - return minutesElapsed; + return minutesElapsed.get(); } - public TimeTrackerDaemon(){ - minutesElapsed = 0; - } + public TimeTrackerDaemon(){ minutesElapsed = Counter.ofInteger(0);} public void run(){ - minutesElapsed = 0; + minutesElapsed.add(-minutesElapsed.get()); while(true) { ModelActions.delay(Duration.MINUTE); - minutesElapsed++; + minutesElapsed.add(1); } } diff --git a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/package-info.java b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/package-info.java index 9677f18605..1a0637363a 100644 --- a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/package-info.java +++ b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/package-info.java @@ -18,6 +18,7 @@ @WithActivityType(DecompositionTestActivities.ParentActivity.class) @WithActivityType(DecompositionTestActivities.ChildActivity.class) +@WithActivityType(LateRiserActivity.class) package gov.nasa.jpl.aerie.foomissionmodel; @@ -29,6 +30,7 @@ import gov.nasa.jpl.aerie.foomissionmodel.activities.DaemonCheckerActivity; import gov.nasa.jpl.aerie.foomissionmodel.activities.DecompositionTestActivities; import gov.nasa.jpl.aerie.foomissionmodel.activities.FooActivity; +import gov.nasa.jpl.aerie.foomissionmodel.activities.LateRiserActivity; import gov.nasa.jpl.aerie.foomissionmodel.activities.OtherControllableDurationActivity; import gov.nasa.jpl.aerie.foomissionmodel.activities.SolarPanelNonLinear; import gov.nasa.jpl.aerie.foomissionmodel.activities.SolarPanelNonLinearTimeDependent; diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/EquationSolvingAlgorithms.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/EquationSolvingAlgorithms.java index 6430ddb7f9..2cd3f868d2 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/EquationSolvingAlgorithms.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/EquationSolvingAlgorithms.java @@ -1,52 +1,24 @@ package gov.nasa.jpl.aerie.scheduler; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.math3.analysis.UnivariateFunction; -import org.apache.commons.math3.analysis.solvers.BrentSolver; -import org.apache.commons.math3.exception.NoBracketingException; -import org.apache.commons.math3.exception.TooManyEvaluationsException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - public class EquationSolvingAlgorithms { private static final Logger logger = LoggerFactory.getLogger(EquationSolvingAlgorithms.class); - public record History (List> history){} - public record RootFindingResult(T x, T fx, History history){} + public record RootFindingResult(T x, T fx, History history){} - public interface BracketingAlgorithm{ - /** - * Solves f(x) in [y - toleranceYLow, y + toleranceYHigh] for x in [xLow, xHigh]. - * the sign of f(bracket1) must be different from the sign of f(bracket2) - * - * xLow {@literal <} xHigh - * toleranceYLow > 0, toleranceYHigh > 0 - */ - RootFindingResult findRoot( - Function f, - T bracket1, - T bracket2, - T y, - T toleranceYLow, - T toleranceYHigh, - T xLow, - T xHigh, - int maxNbIterations) throws ExceededMaxIterationException, WrongBracketingException, - NoSolutionException; - } /** * Solves f(x) = y for x in [xLow, xHigh] with confidence interval [yLow, yHigh] around y such that we stop when * value y is in [y-yLow, y+yHigh]. * x0 and x1 are initial guesses for x, they must be close to the solution to avoid diverging * It is considered that algorithm is diverging when the iterated value of x goes out of [xLow, xHigh]. */ - public interface SecantAlgorithm{ - RootFindingResult findRoot(Function f, + public interface SecantAlgorithm{ + RootFindingResult findRoot(Function f, + History history, T x0, T x1, T y, @@ -58,34 +30,11 @@ RootFindingResult findRoot(Function f, ExceededMaxIterationException, NoSolutionException; } - /** - * Solves f(x) in [y - yLow, y + yHigh] for x in [xLow, xHigh] with confidence interval [yLow, yHigh] around y such that we stop when - * value y is in [y - yLow, y + yHigh]. - * x0 is a first guess for x, it must be close to the solution to avoid diverging. - * It is considered that algorithm is diverging when the iterated value of x goes out of [xLow, xHigh]. - */ - public interface NewtonAlgorithm{ - RootFindingResult findRoot(FunctionWithDerivative f, - T x0, - T y, - T toleranceYLow, - T toleranceYHigh, - T xLow, - T xHigh, - int maxNbIterations) throws DivergenceException, - ZeroDerivativeException, - ExceededMaxIterationException; - } - - public interface Function { - T valueAt(T x); + public interface Function { + T valueAt(T x, History historyType); boolean isApproximation(); } - public interface FunctionWithDerivative extends Function { - T derivativeAt(T x); - } - public static class ZeroDerivativeException extends Exception{ public ZeroDerivativeException() {} } @@ -95,15 +44,9 @@ public InfiniteDerivativeException() {} } public static class DivergenceException extends Exception{ - public History history; - public DivergenceException(String errorMessage) { super(errorMessage); } - public DivergenceException(String errorMessage, History history) { - super(errorMessage); - this.history = history; - } } public static class WrongBracketingException extends Exception{ public WrongBracketingException(String errorMessage) { @@ -112,14 +55,9 @@ public WrongBracketingException(String errorMessage) { } public static class ExceededMaxIterationException extends Exception{ - public History history; public ExceededMaxIterationException() { super(); } - public ExceededMaxIterationException(History history) { - super(); - this.history = history; - } } public static class NoSolutionException extends Exception{ @@ -131,220 +69,11 @@ public NoSolutionException(String errorMessage) { } } - final static class BrentDoubleAlgorithm implements BracketingAlgorithm { - public RootFindingResult findRoot( - Function f, - Double bracket1, - Double bracket2, - Double y, - Double toleranceYLow, - Double toleranceYHigh, - Double xLow, - Double xHigh, - int maxNbIterations) - throws ExceededMaxIterationException, WrongBracketingException, NoSolutionException - { - if (bracket1 < xLow || bracket2 > xHigh) { - throw new WrongBracketingException("brackets are out of prescribed root domain"); - } - if (Math.abs(toleranceYLow - toleranceYHigh) > 1E-6) { - throw new NoSolutionException("To use Brent's method, you must provide equal lower and upper confidence bounds."); - } - BrentSolver solver = new BrentSolver(toleranceYHigh); - //the brent implementation solves f(x) = 0 so we provide lf(x) = f(x) - y - var lf = new UnivariateFunction() { - @Override - public double value(final double v) { - return f.valueAt(v) - y; - } - }; - try { - final var root = solver.solve(maxNbIterations, lf, bracket1, bracket2); - return new RootFindingResult<>(root, f.valueAt(root) + y,null); - } catch (TooManyEvaluationsException e) { - throw new ExceededMaxIterationException(); - } catch (NoBracketingException e) { - throw new WrongBracketingException(e.getMessage()); - } - } - } - public final static class BisectionDoubleAlgorithm implements BracketingAlgorithm { - - public RootFindingResult findRoot( - Function f, - Double bracket1, - Double bracket2, - Double y, - Double toleranceYLow, - Double toleranceYHigh, - Double xLow, - Double xHigh, - int maxNbIterations) - throws WrongBracketingException, ExceededMaxIterationException - { - if (bracket1 < xLow || bracket1 > xHigh || bracket2 < xLow || bracket2 > xHigh) { - throw new WrongBracketingException("Brackets are out of prescribed root domain"); - } - final var ff = new Function(){ - @Override - public Double valueAt(final Double x) { - return f.valueAt(x) - y; - } - - @Override - public boolean isApproximation() { - return f.isApproximation(); - } - }; - - double fx_m = ff.valueAt(bracket1); - double fx_p = ff.valueAt(bracket2); - if (fx_m * fx_p > 0) { - throw new WrongBracketingException("f(x0)=" + fx_m + " and f(x1)=" + fx_p + " are of the same sign"); - } - for (int nbIt = 0; nbIt < maxNbIterations; nbIt++) { - final double xg = (bracket1 + bracket2) / 2.; - final var fxg = ff.valueAt(xg); - if (fxg >= - toleranceYLow && fxg <= toleranceYHigh) { - return new RootFindingResult<>(xg, fxg + y, null); - } else { - if (fxg > 0 && fx_m > 0 || fxg < 0 && fx_m < 0) { - bracket1 = xg; - } else { - bracket2 = xg; - } - fx_m = ff.valueAt(bracket1); - } - - } - throw new ExceededMaxIterationException(); - } - - } - - public final static class BisectionDurationAlgorithm implements BracketingAlgorithm { + public static class SecantDurationAlgorithm implements SecantAlgorithm{ - public RootFindingResult findRoot( - Function f, - Duration bracket1, - Duration bracket2, - Duration y, - Duration toleranceYLow, - Duration toleranceYHigh, - Duration xLow, - Duration xHigh, - int maxNbIterations) - throws WrongBracketingException, ExceededMaxIterationException - { - if (bracket1.shorterThan(xLow) || bracket1.longerThan(xHigh) || bracket2.shorterThan(xLow) || bracket2.longerThan(xHigh)) { - throw new WrongBracketingException("Brackets are out of prescribed root domain"); - } - final var ff = new Function(){ - @Override - public Duration valueAt(final Duration x) { - return f.valueAt(x).minus(y); - } - - @Override - public boolean isApproximation() { - return f.isApproximation(); - } - }; - var bracket1Long = bracket1.in(Duration.MICROSECONDS); - var bracket2Long = bracket2.in(Duration.MICROSECONDS); - var ff_bracket1 = ff.valueAt(bracket1); - var ff_bracket2 = ff.valueAt(bracket2); - if (ff_bracket1.in(Duration.MICROSECONDS) * ff_bracket2.in(Duration.MICROSECONDS) > 0) { - throw new WrongBracketingException("f(bracket1) and f(bracket2) are of the same sign"); - } - for (int nbIt = 0; nbIt < maxNbIterations; nbIt++) { - long xg = Math.round(((float) bracket1Long + bracket2Long) / 2); - final var ff_xg = ff.valueAt(Duration.of(xg, Duration.MICROSECONDS)); - if (ff_xg.noShorterThan(Duration.negate(toleranceYLow)) && ff_xg.noLongerThan(toleranceYHigh)) { - return new RootFindingResult<>(Duration.of(xg, Duration.MICROSECONDS), ff_xg.plus(y), null); - } else { - if ((ff_xg.longerThan(Duration.ZERO) && ff_bracket1.longerThan(Duration.ZERO) || (ff_xg.shorterThan(Duration.ZERO) && ff_bracket1.shorterThan(Duration.ZERO)))) { - bracket1Long = xg; - } else { - bracket2Long = xg; - } - ff_bracket1 = ff.valueAt(bracket1); - } - - } - throw new ExceededMaxIterationException(); - } - } - - public final static class SecantDoubleAlgorithm implements SecantAlgorithm { - - public RootFindingResult findRoot( - Function f, - Double x0, - Double x1, - Double y, - Double toleranceYLow, - Double toleranceYHigh, - Double xLow, - Double xHigh, - int maxNbIterations) - throws ZeroDerivativeException, InfiniteDerivativeException, ExceededMaxIterationException - { - var history = new ArrayList>(); - - var x_nminus1 = x0; - //we make the assumption that a very local derivative will be representative - var x_n = x1; - - final var ff = new Function(){ - @Override - public Double valueAt(final Double x) { - return f.valueAt(x) - y; - } - - @Override - public boolean isApproximation() { - return f.isApproximation(); - } - }; - - var ff_x_nminus1 = ff.valueAt(x_nminus1); - if (ff_x_nminus1 >= - toleranceYLow && - ff_x_nminus1 <= toleranceYHigh - && (x_nminus1 >= xLow && x_nminus1 <= xHigh)) { - return new RootFindingResult<>(x_nminus1, ff_x_nminus1 + y, new History<>(history)); - } - var ff_x_n = ff.valueAt(x_n); - if (ff_x_n >= - toleranceYLow && - ff_x_n <= toleranceYHigh - && (x_n >= xLow && x_n <= xHigh)) { - return new RootFindingResult<>(x_n, ff_x_n + y, new History<>(history)); - } - for (int nbIt = 0; nbIt < maxNbIterations; nbIt++) { - final var localDerivative = (ff_x_n - ff_x_nminus1) / (x_n - x_nminus1); - if (localDerivative == 0) throw new ZeroDerivativeException(); - if (Double.isNaN(localDerivative)) throw new InfiniteDerivativeException(); - x_nminus1 = x_n; - ff_x_nminus1 = ff_x_n; - x_n = x_n - (ff_x_nminus1 / localDerivative); - ff_x_n = ff.valueAt(x_n); - history.add(Pair.of(x_n, ff_x_n)); - if (ff_x_n >= - toleranceYLow && - ff_x_n <= toleranceYHigh - && (x_n >= xLow && x_n <= xHigh)) { - return new RootFindingResult<>(x_n, ff_x_n + y, new History<>(history)); - } - } - throw new ExceededMaxIterationException(); - } - - } - - - public final static class SecantDurationAlgorithm implements SecantAlgorithm{ - - public RootFindingResult findRoot( - Function f, + public RootFindingResult findRoot( + Function f, + History history, Duration x0, Duration x1, Duration y, @@ -355,12 +84,10 @@ public RootFindingResult findRoot( int maxNbIterations) throws ZeroDerivativeException, InfiniteDerivativeException, DivergenceException, ExceededMaxIterationException, NoSolutionException { - final var history = new ArrayList>(); - - final var ff = new Function(){ + final var ff = new Function (){ @Override - public Duration valueAt(final Duration x) { - return f.valueAt(x).minus(y); + public Duration valueAt(final Duration x, final History history) { + return f.valueAt(x, history).minus(y); } @Override @@ -379,20 +106,18 @@ public boolean isApproximation() { throw new DivergenceException("Looking for root out of prescribed domain :[" + xLow + "," + xHigh + "]"); } //We check whether the initial bounds might satisfy the exit criteria. - var ff_x_nminus1 = ff.valueAt(x0); + var ff_x_nminus1 = ff.valueAt(x0, history); if (ff_x_nminus1.noShorterThan(Duration.negate(toleranceYLow)) && ff_x_nminus1.noLongerThan(toleranceYHigh)) { - return new RootFindingResult<>(x0, ff_x_nminus1.plus(y), null); + return new RootFindingResult<>(x0, ff_x_nminus1.plus(y), history); } - var ff_x_n = ff.valueAt(x_n); + var ff_x_n = ff.valueAt(x_n, history); if (ff_x_n.noShorterThan(Duration.negate(toleranceYLow)) && ff_x_n.noLongerThan(toleranceYHigh)) { - return new RootFindingResult<>(x_n, ff_x_n.plus(y), null); + return new RootFindingResult<>(x_n, ff_x_n.plus(y), history); } // After these checks, we can be sure that if the two bounds are the same, the derivative will be 0, and thus throw an exception. if (x0.isEqualTo(x1)) { throw new NoSolutionException(); } - history.add(Pair.of(x0, ff_x_nminus1)); - history.add(Pair.of(x1, ff_x_n)); for (int nbIt = 0; nbIt < maxNbIterations; nbIt++) { //(f(xn) - f(xn_m1)) / (xn - xn_m1) final double localDerivative = @@ -405,74 +130,16 @@ public boolean isApproximation() { //localDerivative has been computed with what is now xn_m1 and xn_m2 x_n_double = x_n_double - (ff_x_nminus1.in(Duration.MICROSECONDS) / localDerivative); x_n = Duration.of((long) x_n_double, Duration.MICROSECONDS); - ff_x_n = ff.valueAt(x_n); - history.add(Pair.of(x_n, ff_x_n)); + ff_x_n = ff.valueAt(x_n, history); //The final solution needs to be in the given bounds which is why this check is added here. if (ff_x_n.noShorterThan(Duration.negate(toleranceYLow)) && ff_x_n.noLongerThan(toleranceYHigh) && (x_n_double >= xLow_long && x_n_double <= xHigh_long)){ logger.debug("Root found after " + nbIt + " iterations"); - return new RootFindingResult<>(x_n, ff_x_n.plus(y), new History<>(history)); - } - } - throw new ExceededMaxIterationException(new History<>(history)); - } - } - - public final static class NewtonDoubleAlgorithm implements NewtonAlgorithm { - public RootFindingResult findRoot( - FunctionWithDerivative f, - Double x0, - Double y, - Double toleranceYLow, - Double toleranceYHigh, - Double xLow, - Double xHigh, - int maxNbIterations) - throws DivergenceException, ZeroDerivativeException, ExceededMaxIterationException - { - var history = new ArrayList>(); - - final var ff = new FunctionWithDerivative(){ - @Override - public Double derivativeAt(final Double x) { - return f.derivativeAt(x); - } - - @Override - public Double valueAt(final Double x) { - return f.valueAt(x) - y; - } - - @Override - public boolean isApproximation() { - return f.isApproximation(); - } - }; - - if (x0 <= xHigh && x0 >= xLow) { - throw new DivergenceException("Initial solution is out of prescribed domain"); - } - var x_n = x0; - var ff_x_n = ff.valueAt(x_n); - for (int nbIt = 0; nbIt < maxNbIterations; nbIt++) { - var localDerivative = ff.derivativeAt(x_n); - if (localDerivative == 0.) { - throw new ZeroDerivativeException(); - } - x_n = x_n - ((ff_x_n - y) / localDerivative); - ff_x_n = ff.valueAt(x_n); - if (ff_x_n >= -toleranceYLow && ff_x_n <= toleranceYHigh) { - return new RootFindingResult<>(x_n, ff_x_n, null); - } - //outside of domain, diverging - if (x_n < xLow || x_n > xHigh) { - throw new DivergenceException("Looking for root out of prescribed domain :[" + xLow + "," + xHigh + "]", new History<>(history)); + return new RootFindingResult<>(x_n, ff_x_n.plus(y), history); } } - throw new ExceededMaxIterationException(new History<>(history)); + throw new ExceededMaxIterationException(); } - } - } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java index acbefd1dd8..7060df2c33 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java @@ -23,6 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -189,6 +190,22 @@ public ActivityCreationTemplate build() { } } + public record EventWithActivity(Duration start, Duration end, SchedulingActivityDirective activity){} + + public static class HistoryWithActivity{ + List events; + + public HistoryWithActivity(){ + events = new ArrayList<>(); + } + public void add(EventWithActivity event){ + this.events.add(event); + } + public Optional getLastEvent(){ + if(!events.isEmpty()) return Optional.of(events.get(events.size()-1)); + return Optional.empty(); + } + } /** * create activity if possible * @@ -235,7 +252,7 @@ private Optional createInstanceForReal(final String } final var success = tnw.solveConstraints(); if (!success) { - logger.warn("Inconsistent temporal constraints, returning Optional.empty() instead of activity"); + logger.warn("Inconsistent temporal constraints, will try next opportunity for activity placement if it exists"); return Optional.empty(); } final var solved = tnw.getAllData(name); @@ -244,7 +261,8 @@ private Optional createInstanceForReal(final String //now it is time to find an assignment compatible //CASE 1: activity has an uncontrollable duration if(this.type.getDurationType() instanceof DurationType.Uncontrollable){ - final var f = new EquationSolvingAlgorithms.Function(){ + final var history = new HistoryWithActivity(); + final var f = new EquationSolvingAlgorithms.Function(){ //As simulation is called, this is not an approximation @Override public boolean isApproximation(){ @@ -252,7 +270,7 @@ public boolean isApproximation(){ } @Override - public Duration valueAt(final Duration start) { + public Duration valueAt(Duration start, HistoryWithActivity history) { final var actToSim = SchedulingActivityDirective.of( type, start, @@ -266,18 +284,43 @@ public Duration valueAt(final Duration start) { null, null, true); - try { - facade.simulateActivity(actToSim); - final var dur = facade.getActivityDuration(actToSim); - facade.removeActivitiesFromSimulation(List.of(actToSim)); - return dur.map(start::plus).orElse(Duration.MAX_VALUE); - } catch (SimulationFacade.SimulationException e) { - return Duration.MAX_VALUE; + final var lastInsertion = history.getLastEvent(); + Optional computedDuration = Optional.empty(); + if(lastInsertion.isPresent()){ + try { + //remove and insert at the same time to avoid unnecessary potential resimulation. + // + // Current sim: A -> B1 -> C + // Sim at end of next iteration: A -> B2 -> C + //If we would do remove() and then insert(), we would simulation A -> C and then again A -> B2 -> C + facade.removeAndInsertActivitiesFromSimulation(List.of(lastInsertion.get().activity()), List.of(actToSim)); + computedDuration = facade.getActivityDuration(actToSim); + //record insertion: if successful, it will stay in the simulation, otherwise, it'll get deleted at the next iteration + history.add(new EventWithActivity(start, start.plus(computedDuration.get()), actToSim)); + } catch (SimulationFacade.SimulationException e) { + //still need to record so we can remove the activity at the next iteration + history.add(new EventWithActivity(start, null, actToSim)); + } + } else { + try { + facade.simulateActivity(actToSim); + computedDuration = facade.getActivityDuration(actToSim); + if(computedDuration.isPresent()) { + history.add(new EventWithActivity(start, start.plus(computedDuration.get()), actToSim)); + } else{ + logger.debug("No simulation error but activity duration could not be found in simulation, unfinished activity?"); + history.add(new EventWithActivity(start, null, actToSim)); + } + } catch (SimulationFacade.SimulationException e) { + logger.debug("Simulation error while trying to simulate activities: " + e); + history.add(new EventWithActivity(start, null, actToSim)); + } } + return computedDuration.map(start::plus).orElse(Duration.MAX_VALUE); } }; - return rootFindingHelper(f, solved, facade, evaluationEnvironment); + return rootFindingHelper(f, history, solved); //CASE 2: activity has a controllable duration } else if (this.type.getDurationType() instanceof DurationType.Controllable dt) { //select earliest start time, STN guarantees satisfiability @@ -344,14 +387,15 @@ public Duration valueAt(final Duration start) { null, true)); } else if (this.type.getDurationType() instanceof DurationType.Parametric dt) { - final var f = new EquationSolvingAlgorithms.Function() { + final var history = new HistoryWithActivity(); + final var f = new EquationSolvingAlgorithms.Function() { @Override public boolean isApproximation(){ return false; } @Override - public Duration valueAt(final Duration start) { + public Duration valueAt(final Duration start, final HistoryWithActivity history) { final var instantiatedArgs = SchedulingActivityDirective.instantiateArguments( arguments, start, @@ -359,8 +403,17 @@ public Duration valueAt(final Duration start) { evaluationEnvironment, type ); + try { - return dt.durationFunction().apply(instantiatedArgs).plus(start); + final var duration = dt.durationFunction().apply(instantiatedArgs); + final var activity = SchedulingActivityDirective.of(type,start, + duration, + instantiatedArgs, + null, + null, + true); + history.add(new EventWithActivity(start, start.plus(duration), activity)); + return duration.plus(start); } catch (InstantiationException e) { logger.error("Cannot instantiate parameterized duration activity type: " + type.getName()); throw new RuntimeException(e); @@ -368,7 +421,7 @@ public Duration valueAt(final Duration start) { } }; - return rootFindingHelper(f, solved, facade, evaluationEnvironment); + return rootFindingHelper(f, history, solved); } else { throw new UnsupportedOperationException("Unsupported duration type found: " + this.type.getDurationType()); } @@ -394,11 +447,10 @@ Optional createActivity(String name, SimulationFaca return createInstanceForReal(name,null, facade, plan, planningHorizon, evaluationEnvironment); } - private Optional rootFindingHelper( - final EquationSolvingAlgorithms.Function f, - final TaskNetworkAdapter.TNActData solved, - final SimulationFacade facade, - final EvaluationEnvironment evaluationEnvironment + private Optional rootFindingHelper( + final EquationSolvingAlgorithms.Function f, + final HistoryWithActivity history, + final TaskNetworkAdapter.TNActData solved ) { try { var endInterval = solved.end(); @@ -407,9 +459,10 @@ private Optional rootFindingHelper( final var durationHalfEndInterval = endInterval.duration().dividedBy(2); final var result = new EquationSolvingAlgorithms - .SecantDurationAlgorithm() + .SecantDurationAlgorithm() .findRoot( f, + history, startInterval.start, startInterval.end, endInterval.start.plus(durationHalfEndInterval), @@ -419,34 +472,17 @@ private Optional rootFindingHelper( startInterval.end, 20); - Duration dur = null; - if(!f.isApproximation()){ - //f is calling simulation -> we do not need to resimulate this activity later - dur = result.fx().minus(result.x()); - } // TODO: When scheduling is allowed to create activities with anchors, this constructor should pull from an expanded creation template - return Optional.of(SchedulingActivityDirective.of( - type, - result.x(), - dur, - SchedulingActivityDirective.instantiateArguments( - this.arguments, result.x(), - facade.getLatestConstraintSimulationResults(), - evaluationEnvironment, - type), - null, - null, - true)); + final var lastActivityTested = result.history().getLastEvent(); + return Optional.of(lastActivityTested.get().activity); } catch (EquationSolvingAlgorithms.ZeroDerivativeException zeroOrInfiniteDerivativeException) { logger.debug("Rootfinding encountered a zero-derivative"); } catch (EquationSolvingAlgorithms.InfiniteDerivativeException infiniteDerivativeException) { logger.debug("Rootfinding encountered an infinite-derivative"); } catch (EquationSolvingAlgorithms.DivergenceException e) { logger.debug("Rootfinding diverged"); - logger.debug(e.history.history().toString()); } catch (EquationSolvingAlgorithms.ExceededMaxIterationException e) { logger.debug("Too many iterations"); - logger.debug(e.history.history().toString()); } catch (EquationSolvingAlgorithms.NoSolutionException e) { logger.debug("No solution"); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java index 94e3c99855..992c0a2f0d 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java @@ -19,6 +19,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import gov.nasa.jpl.aerie.scheduler.NotNull; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.time.Instant; import java.util.Comparator; @@ -29,7 +31,7 @@ import java.util.stream.Collectors; public class ResumableSimulationDriver implements AutoCloseable { - + private static final Logger logger = LoggerFactory.getLogger(ResumableSimulationDriver.class); private Duration curTime = Duration.ZERO; private SimulationEngine engine = new SimulationEngine(); private LiveCells cells; @@ -63,6 +65,7 @@ public ResumableSimulationDriver(MissionModel missionModel, Duration plan /*package-private*/ void clearActivitiesInserted() {activitiesInserted.clear();} /*package-private*/ void initSimulation(){ + logger.warn("Reinitialization of the scheduling simulation"); plannedDirectiveToTask.clear(); lastSimResults = null; lastSimResultsEnd = Duration.ZERO; @@ -136,13 +139,7 @@ public void simulateActivity(final Duration startOffset, final SerializedActivit */ public void simulateActivity(ActivityDirective activityToSimulate, ActivityDirectiveId activityId) { - activitiesInserted.put(activityId, activityToSimulate); - if(activityToSimulate.startOffset().noLongerThan(curTime)){ - initSimulation(); - simulateSchedule(activitiesInserted); - } else { - simulateSchedule(Map.of(activityId, activityToSimulate)); - } + simulateActivities(Map.of(activityId, activityToSimulate)); } public void simulateActivities(@NotNull Map activitiesToSimulate) { diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java index 375fcc8ac7..9f4e69590a 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java @@ -16,9 +16,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -86,7 +88,6 @@ public Map getActivityIdCorr */ public Optional getActivityDuration(final SchedulingActivityDirective schedulingActivityDirective) { if(!planActDirectiveIdToSimulationActivityDirectiveId.containsKey(schedulingActivityDirective.getId())){ - logger.error("You need to simulate before requesting activity duration"); return Optional.empty(); } final var duration = driver.getActivityDuration(planActDirectiveIdToSimulationActivityDirectiveId.get( @@ -107,7 +108,9 @@ private ActivityDirectiveId getIdOfRootParent(SimulationResults results, Simulat } } - public Map getAllChildActivities(final Duration endTime){ + public Map getAllChildActivities(final Duration endTime) + throws SimulationException + { computeSimulationResultsUntil(endTime); final Map childActivities = new HashMap<>(); this.lastSimDriverResults.simulatedActivities.forEach( (activityInstanceId, activity) -> { @@ -129,25 +132,39 @@ public Map getAllChi return childActivities; } - public void removeActivitiesFromSimulation(final Collection activities) throws SimulationException { + public void removeAndInsertActivitiesFromSimulation( + final Collection activitiesToRemove, + final Collection activitiesToAdd) throws SimulationException + { var atLeastOne = false; - for(final var act: activities){ + for(final var act: activitiesToRemove){ if(insertedActivities.containsKey(act)){ atLeastOne = true; insertedActivities.remove(act); } } + Duration earliestActStartTime = Duration.MAX_VALUE; + for(final var act: activitiesToAdd){ + earliestActStartTime = Duration.min(earliestActStartTime, act.startOffset()); + } //reset resumable simulation - if(atLeastOne){ - final var oldInsertedActivities = new HashMap<>(insertedActivities); + if(atLeastOne || earliestActStartTime.shorterThan(this.driver.getCurrentSimulationEndTime())){ + final var allActivitiesToSimulate = new ArrayList<>(insertedActivities.keySet()); insertedActivities.clear(); planActDirectiveIdToSimulationActivityDirectiveId.clear(); if (driver != null) driver.close(); driver = new ResumableSimulationDriver<>(missionModel, planningHorizon.getAerieHorizonDuration()); - simulateActivities(oldInsertedActivities.keySet()); + allActivitiesToSimulate.addAll(activitiesToAdd); + simulateActivities(allActivitiesToSimulate); } } + public void removeActivitiesFromSimulation(final Collection activities) + throws SimulationException + { + removeAndInsertActivitiesFromSimulation(activities, List.of()); + } + /** * Replaces an activity instance with another, strictly when they have the same id * @param toBeReplaced the activity to be replaced @@ -170,9 +187,11 @@ public void replaceActivityFromSimulation(final SchedulingActivityDirective toBe this.planActDirectiveIdToSimulationActivityDirectiveId.put(replacement.id(), simulationId); } - public void simulateActivities(final Collection activities) { + public void simulateActivities(final Collection activities) throws SimulationException { final var activitiesSortedByStartTime = - activities.stream().sorted(Comparator.comparing(SchedulingActivityDirective::startOffset)).toList(); + activities.stream().filter(activity -> !(insertedActivities.containsKey(activity))) + .sorted(Comparator.comparing(SchedulingActivityDirective::startOffset)).toList(); + if(activitiesSortedByStartTime.isEmpty()) return; final Map directivesToSimulate = new HashMap<>(); for(final var activity : activitiesSortedByStartTime){ @@ -187,7 +206,11 @@ public void simulateActivities(final Collection act activityDirective); insertedActivities.put(activity, activityDirective); } + try { driver.simulateActivities(directivesToSimulate); + } catch(Exception e){ + throw new SimulationException("An exception happened during simulation", e); + } } public static class SimulationException extends Exception { @@ -197,25 +220,25 @@ public static class SimulationException extends Exception { } public void simulateActivity(final SchedulingActivityDirective activity) throws SimulationException { - final var activityIdSim = new ActivityDirectiveId(itSimActivityId++); - final var activityDirective = schedulingActToActivityDir(activity); - - planActDirectiveIdToSimulationActivityDirectiveId.put(activity.getId(), activityIdSim); - driver.simulateActivity(activityDirective, activityIdSim); - insertedActivities.put(activity, activityDirective); + if(insertedActivities.containsKey(activity)) return; + simulateActivities(List.of(activity)); } - public void computeSimulationResultsUntil(final Duration endTime) { + public void computeSimulationResultsUntil(final Duration endTime) throws SimulationException { var endTimeWithMargin = endTime; if(endTime.noLongerThan(Duration.MAX_VALUE.minus(MARGIN))){ endTimeWithMargin = endTime.plus(MARGIN); } - final var results = driver.getSimulationResultsUpTo(this.planningHorizon.getStartInstant(), endTimeWithMargin); - //compare references - if(results != lastSimDriverResults) { - //simulation results from the last simulation, as converted for use by the constraint evaluation engine - lastSimConstraintResults = SimulationResultsConverter.convertToConstraintModelResults(results, planningHorizon.getAerieHorizonDuration()); - lastSimDriverResults = results; + try { + final var results = driver.getSimulationResultsUpTo(this.planningHorizon.getStartInstant(), endTimeWithMargin); + //compare references + if(results != lastSimDriverResults) { + //simulation results from the last simulation, as converted for use by the constraint evaluation engine + lastSimConstraintResults = SimulationResultsConverter.convertToConstraintModelResults(results, planningHorizon.getAerieHorizonDuration()); + lastSimDriverResults = results; + } + } catch (Exception e){ + throw new SimulationException("An exception happened during simulation", e); } } @@ -241,8 +264,6 @@ private ActivityDirective schedulingActToActivityDir(SchedulingActivityDirective } else { throw new Error("Unhandled variant of DurationType: " + durationType); } - } else { - logger.warn("Activity has unconstrained duration {}", activity); } final var serializedActivity = new SerializedActivity(activity.getType().getName(), arguments); return new ActivityDirective( diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index f5f6cf76c5..c39ae42f16 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -130,7 +131,9 @@ public Optional getNextSolution() { } } - private boolean checkAndInsertAct(SchedulingActivityDirective act){ + public record InsertActivityResult(boolean success, List activitiesInserted){} + + private InsertActivityResult checkAndInsertAct(SchedulingActivityDirective act){ return checkAndInsertActs(List.of(act)); } @@ -140,7 +143,7 @@ private boolean checkAndInsertAct(SchedulingActivityDirective act){ * @param acts the activities to insert in the plan * @return false if at least one activity has a simulated duration not equal to the expected duration, true otherwise */ - private boolean checkAndInsertActs(Collection acts){ + private InsertActivityResult checkAndInsertActs(Collection acts){ // TODO: When anchors are allowed to be added by Scheduling goals, inserting the new activities one at a time should be reconsidered boolean allGood = true; @@ -167,7 +170,7 @@ private boolean checkAndInsertActs(Collection acts) allGood = false; break; } - if (act.duration() == null || simDur.get().compareTo(act.duration()) != 0) { + if (act.duration() != null && simDur.get().compareTo(act.duration()) != 0) { allGood = false; logger.error("When simulated, activity " + act + " has a different duration than expected (exp=" + act.duration() + ", real=" + simDur + ")"); @@ -175,25 +178,37 @@ private boolean checkAndInsertActs(Collection acts) } } } + final var finalSetOfActsInserted = new ArrayList(); if(allGood) { //update plan with regard to simulation for(var act: acts) { plan.add(act); + finalSetOfActsInserted.add(act); + } + final Map allGeneratedActivities; + try { + allGeneratedActivities = simulationFacade.getAllChildActivities(simulationFacade.getCurrentSimulationEndTime()); + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Exception while simulating to get child activities", e); } - final var allGeneratedActivities = simulationFacade.getAllChildActivities(simulationFacade.getCurrentSimulationEndTime()); processNewGeneratedActivities(allGeneratedActivities); - pullActivityDurationsIfNecessary(); + final var replaced = pullActivityDurationsIfNecessary(); + for(final var actReplaced : replaced.entrySet()){ + if(finalSetOfActsInserted.contains(actReplaced.getKey())){ + finalSetOfActsInserted.remove(actReplaced.getKey()); + finalSetOfActsInserted.add(actReplaced.getValue()); + } + } } else{ //update simulation with regard to plan try { simulationFacade.removeActivitiesFromSimulation(acts); - } catch (SimulationFacade.SimulationException e) { - // We do not expect to get SimulationExceptions from re-simulating activities that have been simulated before - throw new Error("Simulation failed after removing activities"); + } catch(SimulationFacade.SimulationException e){ + throw new RuntimeException("Removing activities from the simulation should not result in exception being thrown but one was thrown", e); } } - return allGood; + return new InsertActivityResult(allGood, finalSetOfActsInserted); } /** @@ -227,9 +242,10 @@ public void initializePlan() throws SimulationFacade.SimulationException { * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade */ - public void pullActivityDurationsIfNecessary() { + public Map pullActivityDurationsIfNecessary() { final var toRemoveFromPlan = new ArrayList(); final var toAddToPlan = new ArrayList(); + final var replaced = new HashMap(); for (final var activity : plan.getActivities()) { if (activity.duration() == null) { final var duration = simulationFacade.getActivityDuration(activity); @@ -243,12 +259,13 @@ public void pullActivityDurationsIfNecessary() { toRemoveFromPlan.add(activity); generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getLeft().equals(activity) ? Pair.of(replacementAct, pair.getRight()): pair).collect(Collectors.toList()); generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getRight().equals(activity) ? Pair.of(pair.getLeft(), replacementAct): pair).collect(Collectors.toList()); + replaced.put(activity, replacementAct); } } } - plan.remove(toRemoveFromPlan); plan.add(toAddToPlan); + return replaced; } /** @@ -393,8 +410,9 @@ private void satisfyOptionGoal(OptionGoal goal) { //we do not care about ownership here as it is not really a piggyback but just the validation of the supergoal evaluation.forGoal(goal).associate(act, false); } - if(checkAndInsertActs(actsToInsert)) { - for(var act: actsToInsert){ + final var insertionResult = checkAndInsertActs(actsToInsert); + if(insertionResult.success()) { + for(var act: insertionResult.activitiesInserted()){ evaluation.forGoal(goal).associate(act, false); } evaluation.forGoal(goal).setScore(0); @@ -521,10 +539,11 @@ private void satisfyGoalGeneral(Goal goal) { assert acts != null; //add the activities to the output plan if (!acts.isEmpty()) { - if(checkAndInsertActs(acts)){ + final var insertionResult = checkAndInsertActs(acts); + if(insertionResult.success){ madeProgress = true; - evaluation.forGoal(goal).associate(acts, true); + evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); //REVIEW: really association should be via the goal's own query... //NB: repropagation of new activity effects occurs on demand @@ -581,7 +600,11 @@ private void satisfyGoalGeneral(Goal goal) { //REVIEW: maybe should have way to request only certain kinds of conflicts var lastSimResults = this.simulationFacade.getLatestConstraintSimulationResults(); if (lastSimResults == null || this.checkSimBeforeEvaluatingGoal) { - this.simulationFacade.computeSimulationResultsUntil(this.problem.getPlanningHorizon().getEndAerie()); + try { + this.simulationFacade.computeSimulationResultsUntil(this.problem.getPlanningHorizon().getEndAerie()); + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); + } lastSimResults = this.simulationFacade.getLatestConstraintSimulationResults(); } final var rawConflicts = goal.getConflicts(plan, lastSimResults); @@ -733,8 +756,11 @@ private Windows narrowByResourceConstraints(Windows windows, final var totalDomain = Interval.between(windows.minTrueTimePoint().get().getKey(), windows.maxTrueTimePoint().get().getKey()); //make sure the simulation results cover the domain - simulationFacade.computeSimulationResultsUntil(totalDomain.end); - + try { + simulationFacade.computeSimulationResultsUntil(totalDomain.end); + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Exception while running simulation before evaluating resource constraints", e); + } //iteratively narrow the windows from each constraint //REVIEW: could be some optimization in constraint ordering (smallest domain first to fail fast) for (final var constraint : constraints) { @@ -760,14 +786,24 @@ private Windows narrowGlobalConstraints( return tmp; } //make sure the simulation results cover the domain - simulationFacade.computeSimulationResultsUntil(tmp.maxTrueTimePoint().get().getKey()); + try { + simulationFacade.computeSimulationResultsUntil(tmp.maxTrueTimePoint().get().getKey()); + } catch(SimulationFacade.SimulationException e){ + throw new RuntimeException("Exception while running simulation before evaluating global constraints", e); + } for (GlobalConstraint gc : constraints) { if (gc instanceof GlobalConstraintWithIntrospection c) { - tmp = c.findWindows(plan, tmp, mac, simulationFacade.getLatestConstraintSimulationResults(), evaluationEnvironment); + tmp = c.findWindows( + plan, + tmp, + mac, + simulationFacade.getLatestConstraintSimulationResults(), + evaluationEnvironment); } else { throw new Error("Unhandled variant of GlobalConstraint: %s".formatted(gc)); } } + return tmp; } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java index 905cc7d25e..fa812550c8 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.scheduler; import gov.nasa.jpl.aerie.constraints.time.Windows; +import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; @@ -87,7 +88,6 @@ public void testNonLinear(){ final var solver = new PrioritySolver(problem); final var plan = solver.getNextSolution().get(); - solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT11M40S"), planningHorizon.fromStart("PT16M40S"), problem.getActivityType("SolarPanelNonLinear"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT28M20S"), planningHorizon.fromStart("PT33M20S"), problem.getActivityType("SolarPanelNonLinear"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0S"), planningHorizon.fromStart("PT1M29S"), problem.getActivityType("SolarPanelNonLinear"))); @@ -137,7 +137,6 @@ public void testTimeDependent(){ final var solver = new PrioritySolver(problem); final var plan = solver.getNextSolution().get(); - solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0S"), planningHorizon.fromStart("PT0S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT11M57S"), planningHorizon.fromStart("PT16M40S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT28M34S"), planningHorizon.fromStart("PT33M20S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); @@ -157,7 +156,7 @@ public void testBug(){ .withTimingPrecision(Duration.of(1, Duration.MICROSECONDS)) .build(); - //this time expression produces an interval [TimeAnchor.END, TimeAnchor.END + 2 Ms] + //this time expression produces an interval [TimeAnchor.START, TimeAnchor.START + 2 Ms] final var intervalStartTimeExpression = new TimeExpressionRelativeFixed(TimeAnchor.END, false); intervalStartTimeExpression.addOperation(TimeUtility.Operator.PLUS, Duration.of(2, Duration.MICROSECONDS)); @@ -176,10 +175,44 @@ public void testBug(){ final var solver = new PrioritySolver(problem); final var plan = solver.getNextSolution().get(); - solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0.000004S"), planningHorizon.fromStart("PT0.000004S"), problem.getActivityType("ZeroDurationUncontrollableActivity"))); } + + @Test + public void testScheduleExceptionThrowingTask(){ + final var zeroDurationUncontrollableActivity = new ActivityCreationTemplate.Builder() + .ofType(problem.getActivityType("LateRiser")) + .withTimingPrecision(Duration.of(1, Duration.MICROSECONDS)) + .build(); + + //this time expression produces an interval [TimeAnchor.END, TimeAnchor.END + 2 Ms] + final var intervalStartTimeExpression = new TimeExpressionRelativeFixed(TimeAnchor.START, false); + intervalStartTimeExpression.addOperation(TimeUtility.Operator.PLUS, Duration.of(2, Duration.MINUTE)); + + final var horizonExpression = new SpansFromWindows(new WindowsWrapperExpression(new Windows(false).set(planningHorizon.getHor(), true))); + + final var coexistenceControllable = new CoexistenceGoal.Builder() + .thereExistsOne(zeroDurationUncontrollableActivity) + .forAllTimeIn(new WindowsWrapperExpression(new Windows(false).set(planningHorizon.getHor(), true))) + .forEach(horizonExpression) + .startsAt(intervalStartTimeExpression) + .aliasForAnchors("its a me") + .build(); + + problem.setGoals(List.of(coexistenceControllable)); + final var initialPlan = new PlanInMemory(); + problem.setInitialPlan(initialPlan); + + final var solver = new PrioritySolver(problem); + final var plan = solver.getNextSolution().get(); + //Activity can be started in [0, 2m] but this activity will throw an exception if ran in [0, 1m] so it is scheduled at 2m (as being the second bounds the rootfinding tries before search). + assertTrue(TestUtility.containsActivity(plan, + planningHorizon.fromStart("PT120S"), + planningHorizon.fromStart("PT120S"), + problem.getActivityType("LateRiser"))); + } + } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index 1808dc4fb4..bced3af91f 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -272,7 +272,11 @@ private Optional storeSimulationResults(PlanningHorizon planningHoriz throws PlanServiceException, IOException { //finish simulation until end of horizon before posting results - simulationFacade.computeSimulationResultsUntil(planningHorizon.getEndAerie()); + try { + simulationFacade.computeSimulationResultsUntil(planningHorizon.getEndAerie()); + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Error while running simulation before storing simulation results after scheduling", e); + } final var schedID_to_MerlinID = schedDirectiveToMerlinId.entrySet().stream() .collect(Collectors.toMap( diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 12455c8d53..3653cb7d5a 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -1722,7 +1722,7 @@ export default function myGoal() { return Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityType.parent), activityTemplate: ActivityTemplates.child({ - counter: 0, + counter: 1, }), startsAt:TimingConstraint.singleton(WindowProperty.START) }) @@ -1739,15 +1739,17 @@ export default function myGoal() { null, true)), List.of(new SchedulingGoal(new GoalId(0L), goalDefinition, true)), - List.of(createAutoMutex("child")), + List.of(), planningHorizon); final var planByActivityType = partitionByActivityType(results.updatedPlan()); final var parentActs = planByActivityType.get("parent"); final var childActs = planByActivityType.get("child").stream().map((bb) -> bb.startOffset()).toList(); + //goal should be satisfied + assertTrue(results.scheduleResults.goalResults().entrySet().iterator().next().getValue().satisfied()); //ensure no new child activity has been inserted - assertEquals(childActs.size(), 2); + assertEquals(2, childActs.size()); //ensure no new parent activity has been inserted - assertEquals(parentActs.size(), 1); + assertEquals(1, parentActs.size()); for(final var parentAct: parentActs){ assertTrue(childActs.contains(parentAct.startOffset())); } @@ -2026,15 +2028,15 @@ void testOptionalSimulationAfterGoal_unsimulatedActivities() { new SchedulingGoal(new GoalId(0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), - activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), + activityTemplate: ActivityTemplates.BananaNap(), startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) }) """, true, config.getKey() ), new SchedulingGoal(new GoalId(1L), """ export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.PeelBanana), - activityTemplate: ActivityTemplates.BananaNap(), + forEach: ActivityExpression.ofType(ActivityTypes.BananaNap), + activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) }) """, true, true) @@ -2089,14 +2091,14 @@ void testOptionalSimulationAfterGoal_staleResources() { new SchedulingGoal(new GoalId(0L), """ export default () => Goal.CoexistenceGoal({ forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), - activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), + activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) }) """, true, config.getKey() ), new SchedulingGoal(new GoalId(1L), """ export default () => Goal.CoexistenceGoal({ - forEach: Real.Resource("/peel").lessThan(4), + forEach: Real.Resource("/fruit").greaterThan(5), activityTemplate: ActivityTemplates.BananaNap(), startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) })