Skip to main content

Goals

This document describes different scheduling goals available in Aerie and how to author them using the TypeScript EDSL.

caution

Activities with uncontrollable durations have been found to behave somewhat unpredictably in terms of when they are placed. This has to do with how temporal constraints interact with the unpredictability of the durations. Finding when an activity will start while subject to temporal constraint involves search.

ActivityTemplate and ActivityExpression

An ActivityExpression allows to search for activities in the plan. An activity expression must have an activity type and may have a subset or all of the parameter of the activity type.

ActivityExpression.ofType(ActivityTypes.ParamActivity);

In this case, all the activities of type ParamActivity will be matched (disregarding its parameters) and potentially used for satisfying the goal.

ActivityExpression.build(ActivityTypes.ParamActivity, { param: 1 });

In this case, all the activities of type ParamActivity with parameter param equal to 1 will be matched and used for satisfying the goal. To do this, you must use the .build function instead of .ofType.

An optional ActivityExpression can be passed with the activityFinder parameter of goals. See below for examples of its use in goals.

An ActivityTemplate specifies the type of an activity, as well as the arguments it should be given. Activity templates are generated for each mission model. You can get the full list of activity templates by typing ActivityTemplates. (note the period) into the scheduling goal editor, and viewing the auto-complete options.

If an activityFinder is not passed to a goal, the mandatory activity template passed with a goal (as the activityTemplate parameter) will be used for searching activities in the plan that satisfies the template. The template will also be used for creating activities to satisfy this goal.

If the activity has parameters, pass them into the constructor in a dictionary as key-value pairs. For example:

ActivityTemplate.ParamActivity({ param: 1 });

If the activity has no parameters, do not pass a dictionary. For example:

ActivityTemplate.ParameterlessActivity();

The value of a parameter can also be a profile (see windows). This capability is currently restricted to the use of a Coexistence Goal, see Coexistence Goal for more details on how to write activity templates in this case.

Goal Types

Activity Recurrence Goal

The Activity Recurrence Goal (sometimes referred to as a "frequency goal") specifies that a certain activity should occur repeatedly throughout the plan at some given interval.

Inputs

  • activityTemplate - The description of the activity whose recurrence we're interested in
  • interval - A Temporal.Duration of time specifying how often this activity must occur
  • activityFinder - an optional activity expression. If present, it will be used as replacement of activityTemplate to match against existing activities in the plan.

Behavior

The interval parameter is treated as a lower bound - so if the activity occurs more frequently, that is not considered a failure. The scheduler will find places in the plan where the given activity has not occurred within the given interval, and it will place an instance of that activity there.

note

The interval is measured between the start times of two activity instances. Neither the duration, nor the end time of the activity are examined by this goal.

Examples

The following goal will place a GrowBanana activity in every 2-hour period of time that does not already contain one with the exact same parameters.

export default function recurrenceGoalExample() {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.GrowBanana({
growingDuration: Temporal.Duration.from({ hours: 1 }),
quantity: 1,
}),
interval: Temporal.Duration.from({ hours: 2 }),
});
}

In the following goal, an activity finder is used to match against existing activities in the plan that would satisfy the goal. Here, any GrowBanana activity with a growingDuration parameter equal to Temporal.Duration.from({hours : 1}) would match, disregarding the value of the other parameter quantity.

With an activityFinder

export default function myGoal() {
return Goal.ActivityRecurrenceGoal({
activityFinder: ActivityExpression.build(ActivityTypes.GrowBanana, {
growingDuration: Temporal.Duration.from({ hours: 1 }),
}),
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ hours: 1 }),
}),
interval: Temporal.Duration.from({ hours: 2 }),
});
}

Coexistence Goal

The Coexistence Goal specifies that a certain activity should occur once for each occurrence of some condition.

Inputs

  • forEach - A set of time Windows, Intervals or Instants, or a set of activities (ActivityExpression)
  • activityTemplate - The description of the activity to insert after each activity identified by forEach. This can be an ActivityTemplate object or an ActivityTemplate factory function with one argument of either ActivityInstance or Interval, depending on if forEach was an ActivityExpression or Windows, respectively. This allows to define the content of the ActivityTemplate with components of the anchor activity or window.
  • activityFinder - an optional activity expression. If present, it will be used as replacement of activityTemplate to match against existing activities in the plan.
  • startsAt - Optionally specify a specific time when the activity should start relative to the window
  • startsWithin - Optionally specify a range when the activity should start relative to the window
  • endsAt - Optionally specify a specific time when the activity should end relative to the window
  • endsWithin - Optionally specify a range when the activity should end relative to the window
note

Either the start or end of the activity must be constrained. This means that at least 1 of the 4 properties startsAt, startsWithin, endsAt, endsWithin must be given.

Behavior

The scheduler will find places in the plan where the forEach condition is true, and if not, it will insert a new instance using the given activityTemplate and temporal constraints.

Examples

The following example specifies a CoexistenceGoal where for each activity "A" of type GrowBanana present in the plan, place an activity of type PeelBanana starting exactly at the end of "A" + 5 minutes:

export default () =>
Goal.CoexistenceGoal({
forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
startsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
});

This next example specifies a CoexistenceGoal where for each activity "A" of type GrowBanana present in the plan, place an activity of type PeelBanana starting in the interval [end of "A", end of "A" + 5 minutes] and ending in the interval [end of "A", end of "A" + 6 minutes]:

export default () =>
Goal.CoexistenceGoal({
forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
startsWithin: TimingConstraint.range(WindowProperty.END, Operator.PLUS, Temporal.Duration.from({ minutes: 5 })),
endsWithin: TimingConstraint.range(WindowProperty.END, Operator.PLUS, Temporal.Duration.from({ minutes: 6 })),
});

This example specifies a CoexistenceGoal where for each continuous period of time during which the /fruit resource is equal to 4, place an activity of type PeelBanana ending exactly at the end of "A" + 6 minutes:

export default () =>
Goal.CoexistenceGoal({
forEach: Real.Resource('/fruit').equal(4.0),
activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
endsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
});

Note that the scheduler will allow a default timing error of 500 milliseconds for temporal constraints. This parameter will be configurable in an upcoming release.

In this example, we use an activity template factory to use the value of one of the parameter of the anchor activity inside the activity template of the activity to create. Let's imagine that the base plan would contain A and B of type GrowBanana and that A.quantity = 3 and B.quantity = 4. After applying this goal, two new activities of type PickBanana, C (with A for anchor) and D (with B for anchor) would be created and their parameters would be C.quantity = 3 and D.quantity = 4.

export default () =>
Goal.CoexistenceGoal({
forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
activityTemplate: growBananaActivity =>
ActivityTemplates.PickBanana({ quantity: growBananaActivity.parameters.quantity }),
startsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
});

If the forEach field is a Windows object, we can also make a factory that references each individual Interval in the Windows:

export default () =>
Goal.CoexistenceGoal({
forEach: Real.Resource('/fruit').equal(4.0),
activityTemplate: interval =>
ActivityTemplates.GrowBanana({
growingDuration: interval.duration(),
quantity: 1,
}),
startsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
});

You may want to access the value of a resource at a defined timepoint in the activity template. For that, you can use the valueAt operator on Real and Discrete classes. For now, this operation takes a Spans object as argument (see concepts. This span should be reduced to a single timepoint for this to work. This is only possible by referencing the span of an activity such as in the following example :

export default () =>
Goal.CoexistenceGoal({
forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana),
activityTemplate: growBananaActivity =>
ActivityTemplates.ChangeProducer({
producer: Discrete.Resource('/producer').valueAt(growBananaActivity.span().starts()),
}),
startsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
});

In this example the value of the /producer resource is taken at the beginning of the anchor activity and assigned to the producer parameter.

export default () =>
Goal.CoexistenceGoal({
forEach: Real.Resource('/fruit').equal(4.0),
activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
activityFinder: ActivityExpression.ofType(ActivityTypes.PeelBanana),
endsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
});

In these final two examples, the forEach condition defines a time Instant and Interval respectively at which the goal has to create an instance of the activity template.

export default () =>
Goal.CoexistenceGoal({
forEach: Temporal.Instant.from('2021-01-01T05:00:00.000Z'),
activityTemplate: span => ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
startsAt: TimingConstraint.singleton(WindowProperty.START),
});
export default () =>
Goal.CoexistenceGoal({
forEach: Interval.Between(
Temporal.Instant.from('2021-01-01T05:00:00.000Z'),
Temporal.Instant.from('2021-01-01T10:00:00.000Z'),
Inclusivity.Inclusive,
Inclusivity.Exclusive,
),
activityTemplate: span => ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
startsAt: TimingConstraint.singleton(WindowProperty.START),
});

Behavior: The activity finder is being used to match against any existing PeelBanana activity in the plan, disregarding the value of its parameters.

caution

If the end is unconstrained while the activity has an uncontrollable duration, the scheduler may fail to place the activity. To work around this, add an endsWithin constraint that encompasses your expectation for the duration of the activity - this will help the scheduler narrow the search space.

Cardinality Goal

The Cardinality Goal specifies that a certain activity should occur in the plan either a certain number of times, or for a certain total duration.

Inputs

  • activityTemplate - The description of the activity whose recurrence we're interested in
  • specification - An object with either an occurrence field, a duration field, or both (see examples below)
  • activityFinder - an optional activity expression. If present, it will be used as replacement of activityTemplate to match against existing activities in the plan.

Behavior

The duration and occurrence are treated as lower bounds - so if the activity occurs more times, or for a longer duration, that is not considered a failure, and the scheduler will not add any more activities.

The scheduler will identify whether or not the plan has enough occurrences, or total duration of the given activity template. If not, it will add activities until satisfaction.

Examples

The following example is a CardinalityGoal that sets a lower bound on the total duration:

export default function cardinalityGoalExample() {
return Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ seconds: 1 }),
}),
specification: { duration: Temporal.Duration.from({ seconds: 10 }) },
});
}

This next example specifies a CardinalityGoal that sets the lower bound on the number of occurrences:

export default function cardinalityGoalExample() {
return Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ seconds: 1 }),
}),
specification: { occurrence: 10 },
});
}

Finally we combine the previous two examples:

export default function cardinalityGoalExample() {
return Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ seconds: 1 }),
}),
specification: { occurrence: 10, duration: Temporal.Duration.from({ seconds: 10 }) },
});
}

With an activity finder:

export default function myGoal() {
return Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ seconds: 1 }),
}),
activityFinder: ActivityExpression.build(ActivityTypes.GrowBanana, { quantity: 1 }),
specification: { occurrence: 10, duration: Temporal.Duration.from({ seconds: 10 }) },
});
}

In this last example, an activity finder is used to match against existing activities in the plan that would satisfy the goal. Here, any GrowBanana activity with a quantity parameter equal to 1 would match, disregarding the value of the other parameter growingDuration.

note

Make sure to specify the proper mutual exclusion constraint as global scheduling conditions - namely that new activities will not be allowed to overlap with existing activities. Otherwise, the cardinality goal may stack activities at one spot in the plan. There is no default constraint in place.

OR Goal - Disjunction of Goals

The OR Goal aggregates several goals together and specifies that at least one of them must be satisfied.

Inputs

  • goals - A list of goals (here below referenced as the sub-goals)

Behavior

The scheduler will try to satisfy each sub-goal in the list until one is satisfied. If a sub-goal is only partially satisfied, the scheduler will not backtrack and will let the inserted activities in the plan.

Examples

The following example shows how to use the .or operator on a pair of goals. If the plan has a 24-hour planning horizon, the OR goal below will try placing activities of the GrowBanana type. The first sub-goal will try placing 10 1-hour occurrences. If it fails to do so, because the planning horizon is too short, it will then try to schedule 1 activity every 2 hours for the duration of the planning horizon.

It may fail to achieve both sub-goals but as the scheduler does not backtrack for now, activities inserted by any of the sub-goals are kept in the plan.

export default function orGoalExample() {
return Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ hours: 1 }),
}),
specification: { occurrence: 10 },
}).or(
Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ hours: 1 }),
}),
interval: Temporal.Duration.from({ hours: 2 }),
}),
);
}

AND Goal - Conjunction of Goals

The AND Goal aggregates several goals together and specifies that all of them must be satisfied.

Inputs

  • goals - A list of goals (here below referenced as the sub-goals)

Behavior

The scheduler will try to satisfy each sub-goal in the list. If a sub-goal is only partially satisfied, the scheduler will not backtrack and will let the inserted activities in the plan. If all the sub-goals are satisfied, the AND goal will appear satisfied. If one or several sub-goals have not been satisfied, the AND goal will appear unsatisfied.

Examples

The AND goal below has two sub-goals. The CoexistenceGoal will place activities of type PeelBanana every time the /fruit resource is equal to 4. The second CardinalityGoal will place 10 occurrences of the PeelBanana activity.

The first sub-goal will be evaluated first and will place a certain number of PeelBanana activities in the plan. When the second goal is evaluated, it will count already present PeelBanana activities and insert the missing number.

Imagine the first goals leads to inserting 2 activities. The second goal will then have to place 8 activities to be satisfied.

export default function andGoalExample() {
return Goal.CoexistenceGoal({
forEach: Real.Resource('/fruit').equal(4.0),
activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
endsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes: 5 })),
}).and(
Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.PeelBanana({ peelDirection: 'fromStem' }),
specification: { occurrence: 10 },
}),
);
}

Accessing Activity Presets

When creating an ActivityTemplate, you can access all the activity presets that planners can, on the ActivityPresets object. For example, if you have a preset of the BiteBanana activity called "big bite", you can access it like this:

export default (): Goal => {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.BiteBanana(ActivityPresets.BiteBanana["big bite"]),
interval: ...
});
}

ActivityPresets.BiteBanana["big bite"] is a concrete object containing the actual values of the preset, not a lazily-evaluated expression. This means it is easy for you to access and override specific values like this:

let biteArgs = ActivityPresets.BiteBanana['large bite'];
biteArgs.biteSize += 10; // 10 bigger than the preset

// use ActivityTemplates.BiteBanana(biteArgs)

Restricting When a Goal is Applied

By default, a goal applies on the whole planning horizon. The Aerie scheduler provides support for restricting when a goal applies with the .applyWhen() method in the Goal class. This node allows users to provide a set of Windows which could be a time-based, or a resource-based window.

The .applyWhen() method, takes one argument: the windows (in the form of an expression) that the goal should apply over. Below is an example that applies a daily recurrence goal only when a given resource is greater than 2. If the resource is less than two, then the goal is no longer applied.

export default function applyWhenExample() {
return Goal.ActivityRecurrenceGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ hours: 1 }),
}),
interval: Temporal.Duration.from({ hours: 2 }),
}).applyWhen(Real.Resource('/fruit').greaterThan(2));
}

Note that when using this feature with a CardinalityGoal, the counters for duration and numbers of occurrence will reset for each window of the set (as Windows is a set of non-overlapping intervals). In other words, the goal will be applied multiple times, one for each window.

Goal Boundary Exclusivity

Note if you are trying to schedule an activity, or a recurrence within a window but that window cuts off either the activity or the recurrence interval (depending on the goal type), it will not be scheduled. For example, if you had a recurrence interval of 3 seconds, scheduling a 2 second activity each recurrence, and had the following window, you'd get the following:

Recurrence Interval: [++-++-++-]
Goal Window: [+++++----]
Result: [++-------]

That, is, the second activity won't be scheduled as the goal window cuts off its recurrence interval. Scheduling is local, not global. This means for every window that is matched (as it is possible to have disjoint windows, imagine a resource that fluctuates upward and downward but only applying that goal when the resource is over a certain value), the goal is applied individually. So for that same recurrence interval setup as before, we could have:

Recurrence Interval: [++-++-++-++-]
Goal Window: [+++++--+++--]
Result: [++-----++---] // (the second one is applied independently of the first!)

When mapping out a temporal window to apply a goal over, keep in mind that the ending boundary of the goal is exclusive, i.e. if I want to apply a goal in the window of 10-12 seconds, it will apply only on seconds 10 and 11. This is in line with the fencepost problem.

Satisfaction of a Goal - Backtracking

The scheduler will take each goal and try to satisfy it. In terms of satisfaction, the two extreme cases are

  • A goal cannot be satisfied at all, no activity could be found or could be inserted.
  • A goal is totally satisfied.

But sometimes, a goal is only partially satisfied. In other words, only a subset of the activities that would have been necessary for total satisfaction could have been inserted in the plan. In this case, there are two strategies that the scheduler can take:

  • Either adopt a best-effort approach: mark the goal unsatisfied but let activities partially satisfying it in the plan. These activities consume resources and time but as the goals are evaluated in a decreasing priority manner, the completion of the current goal is more important than completion of the goals that have not been evaluated yet.
  • Or adopt a all-or-nothing approach: mark the goal unsatisfied and remove activities inserted in the plan specifically for satisfying the goal.

In both cases, the goal is marked as unsatisfied.

You can control this behavior with a setter function of the Goal class, the backtrackIfUnsatisfied(boolean). If the argument of this function is set to false, the scheduler will adopt the best-effort approach for this goal. If the argument of this function is set to true, the scheduler will adopt the all-or-nothing approach for this goal. The best-effort behavior is the default for all goals.

Let's take one of the cardinality goal example and add this:

export default function cardinalityGoalExample() {
return Goal.CardinalityGoal({
activityTemplate: ActivityTemplates.GrowBanana({
quantity: 1,
growingDuration: Temporal.Duration.from({ seconds: 1 }),
}),
specification: { occurrence: 10 },
}).backtrackIfUnsatisfied(true);
}

Creating a New Goal

This section describes how to create a new scheduling goal via the Aerie UI. In the UI, open the scheduling pane by clicking on the top-right bar.

Aerie UI - Scheduling Panel
Figure 1: Aerie UI Scheduling Panel

Next click "New" to create a new scheduling goal in the specification for the plan you are viewing. This will open the goal editor.

Aerie UI - Scheduling Goal Editor - New Goal
Figure 2: Scheduling Goal Editor - New Goal

The default new goal shown in the editor is the following:

export default (): Goal => {
// Your code here...
};

This is a TypeScript function that takes no arguments and returns a Goal. To unpack all of the parts:

  • export default signals to Aerie that this is the function that defines the Goal.
  • () => {} in TypeScript is called an arrow function.
  • The parenthesis () represent the parameters that the function takes. Scheduling goals cannot take any parameters, so these parenthesis must be empty.
  • The curly braces {} represent the definition of the goal. The return statement for the function must go inside the braces.
  • The : Goal part signifies that this function returns a Goal. TypeScript will check that the function does indeed return a Goal - if it does not, it will underline your code in red.

In the initial code provided in the new goal editor the function does not yet return anything, so you should see the word Goal underlined in red (see Figure 2 above).

Mousing over the word Goal, you should see something akin to the following message:

A function whose declared type is neither 'void' nor 'any' must return a value.

This message means that the function has promised to return a value, but it currently lacks a return statement. Between the curly braces, add the following code:

export default (): Goal => {
return Goal.ActivityRecurrenceGoal();
};

The editor should tell you that ActivityRecurrenceGoal() takes one argument:

Aerie UI - Invalid Recurrence Goal
Figure 3: Invalid Recurrence Goal

The argument that we’re missing is the "options" object. Objects in typescript are defined using curly braces {} with key-value pairs, like so:

{
key: value;
}

If we pass an empty object {} to ActivityRecurrenceGoal:

export default (): Goal => {
return Goal.ActivityRecurrenceGoal({}); // The empty object is written as {}
};

We get an error message that tells us what keys our object needs:

Argument of type '{}' is not assignable to parameter of type '{ activityTemplate: ActivityTemplate; interval: number; }'.
Type '{}' is missing the following properties from type '{ activityTemplate: ActivityTemplate; interval: number;}': activityTemplate, interval

This error message tells us that our object is missing two keys: activityTemplate, and interval. If we look up the definition of ActivityRecurrenceGoal, we see that it does indeed need an activity template and an interval. Let’s add those:

export default (): Goal => {
return Goal.ActivityRecurrenceGoal({
activityTemplate: null,
interval: Temporal.Duration.from({ hours: 24 }),
});
};

Now, we just need to finish specifying the activityTemplate. Start by typing ActivityTemplates. (note the period), and select an activity type. Provide your activity an object with the arguments that that activity takes. Once the editor is no longer underlining your code, save your goal by clicking on "Save".

Deleting or Editing a Goal

To delete or edit a scheduling goal via the UI, open the scheduling pane and right-click on the goal you want to delete or edit:

Aerie UI - Goal Context Menu
Figure 4: Goal Context Menu

Click on "Delete Goal" to delete the goal. If you click on "Edit Goal" it will open a new tab with an editor:

Aerie UI - Scheduling Goal Editor - Edit Goal
Figure 5: Scheduling Goal Editor - Edit Goal

When you are done with editing the goal, click on "Save". Your goal is saved, and you can close the tab or continue working.