Skip to content

Commit

Permalink
Procedural scheduling with events docs
Browse files Browse the repository at this point in the history
  • Loading branch information
psubram3 authored and JoelCourtney committed Oct 31, 2024
1 parent 3fe252a commit dc0dbe8
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ There should now be one jar for each scheduling procedure, at `scheduling/build/

## Creating a Goal

See the examples in the mission model template repo, or see [the scheduling page](../scheduling) in this section.
See the examples in the mission model template repo, or see [the scheduling page](../scheduling/introduction/) in this section.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ For now, only Java records are supported for parameterization.

:::

For example, we can take the example from the [scheduling](../scheduling) page, and parameterize it so that it doesn't
For example, we can take the example from the [scheduling](../scheduling/introduction/) page, and parameterize it so that it doesn't
unconditionally recur every hour, and instead takes the period as input:

<Tabs groupId="lang">
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
186 changes: 186 additions & 0 deletions docs/scheduling-and-constraints/procedural/scheduling/examples.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Examples

This page details various examples of scheduling procedures to further illustrate how the aforementioned concepts
can be used.

## Scheduling a Recurring Activity

A recurrence goal specifies that a certain activity should occur repeatedly throughout the plan at some given interval.
An example of this can be seen in the description of [Activity Recurrence Goal](../../../declarative/scheduling/goals/#activity-recurrence-goal).

We will replicate the second, more intricate goal. This places an activity of type `GrowBanana` every two hours,
unless an equivalent activity is already happening at the same time.

```java
@SchedulingProcedure
public record ActivityRecurrenceGoal() implements Goal {
@Override
public void run(@NotNull final EditablePlan plan) {
// grab the current set of activities, and filter by those of type GrowBanana and duration 1 hour
var existingActivities = plan.directives("GrowBanana")
.filter(false, a -> a.inner.arguments.get("growingDuration").asInt().get() == 1)
.active().cache();

int count = 1;
for (final var time: plan.totalBounds().step(Duration.hours(2))) {
if (!existingActivities.sample(time)) {
plan.create(
new NewDirective(
// parameters
new AnyDirective(Map.of(
"growingDuration", BasicValueMappers.duration().serializeValue(Duration.of(1, Duration.HOUR)),
"quantity", SerializedValue.of(1)
)
),
// name
"GrowBanana" + count++,
// type
"GrowBanana",
// start time
new DirectiveStart.Absolute(time)
)
);
}
}

plan.commit();
}
}
```

## Scheduling From Profiles

Next we'll show how to schedule from resource profile conditions, much like a [Coexistence Goal](../../../declarative/scheduling/goals/#coexistence-goal)
from the declarative scheduler. This goal places an activity 5 minutes after the end of each where `/fruit` is equal to `4.0`.

```java
@SchedulingProcedure
public record ResourceCoexistenceGoal() implements Goal {
@Override
public void run(@NotNull final EditablePlan plan) {
// Get the latest results, or simulate if they don't exist.
var simResults = plan.latestResults();
if (simResults == null) simResults = plan.simulate();

// access the resource
final var fruitResource = simResults.resource("/fruit", Numbers.deserializer());
for (final var interval: fruitResource.equalTo(4.0).highlightTrue()) {
// place the activity off of the window that the resource access created
plan.create(
"PeelBanana",
new DirectiveStart.Absolute(interval.start.plus(Duration.minutes(5))),
Map.of("peelDirection", SerializedValue.of("fromStem"))
);
}
plan.commit();
}
}
```

An implementation of a procedure that schedules based off of an external profile is almost exactly the same, except
the resource is queried using `plan.resource("/my_resource")` instead of `simResults.resource("/my_resource")`.
In those cases, if you don't use any simulated resources, there's no need to get a sim results object.

## Scheduling From Activities

We can also schedule relative to other activities, and place new activities that are anchored to them. This is a slightly
more complex example to show how to create activities to fix a problematic resource condition caused by other activities.
We look for `BiteBanana` activities, and check if those activities lower the `/fruit` resource below zero. If so,
we create a `GrowBanana` activity that replenishes the resource back up to zero. Lastly, we mock the effect the new
activity would have on the `/fruit` resource, rather than resimulating a potentially expensive mission model.

```java
@SchedulingProcedure
public record ActivityCoexistenceGoal() implements Goal {
@Override
public void run(@NotNull final EditablePlan plan) {
// make sure we have up-to-date results to start with
final var simResults = plan.simulate();

var mockedFruit = simResults.resource("/fruit", Real.deserializer()).cache();

for (final var biteInstance: simResults.instances("BiteBanana")) {
final var biteInterval = biteInstance.getInterval();
final var fruitLevel = mockedFruit.sample(biteInterval.end);
if (fruitLevel < 0.0) {
final var growQuantity = Math.ceil(-fruitLevel);
plan.create(
"GrowBanana",
new DirectiveStart.Anchor(biteInstance.directiveId, Duration.HOUR.negate(), DirectiveStart.Anchor.AnchorPoint.Start),
Map.of(
"growingDuration", SerializedValue.of(Duration.HOUR.micros()),
"quantity", SerializedValue.of(growQuantity)
)
);
mockedFruit = mockedFruit.plus(Real.step(biteInterval.start, growQuantity));
}
}

plan.commit();
}
}
```

## Scheduling Based off of External Events

Consider this set of events:

import exampleEvents from './assets/example-events.png';

<figure>
<img alt="A drawing of a timeline depicting two sources in different derivation groups, each with 3 non-overlapping events inside of them." src={exampleEvents} />
<figcaption>Figure 1: The events that we will write a procedure to schedule off of.</figcaption>
</figure>

Consider scheduling the Banananation activity `BiteBanana` off of these events.
This can mean a variety of different things, depending on the exact behavior we desire:
- schedule the activity coincident with all events
- schedule the activity coincident with events belonging to derivation group `"TestGroup"`
- schedule the activity coincident with events belonging to the second source
- schedule the activity based on events of type `"Test_Type"`
- schedule the activity based on events with the substring `"01"` in their key.

We will just show one case - events belonging to the second source with the substring `"01"` in their key.

```java
@SchedulingProcedure
public record ExternalEventsSourceQueryGoal() implements Goal {
@Override
public void run(@NotNull final EditablePlan plan) {

// extract events belonging to the second source
EventQuery eventQuery = new EventQuery(
null,
null,
new ExternalSource("NewTest.json", "TestGroup_2")
);

final var events = plan.events(eventQuery).filter(false, $ -> $.key.contains("01"));

for (final var e: events) {
plan.create(
"BiteBanana",
// place the directive such that it is coincident with the event's start
new DirectiveStart.Absolute(e.getInterval().start),
Map.of("biteSize", SerializedValue.of(1))
);
}

plan.commit();
}
}
```

After running it, we get the following result in AERIE:

import results from './assets/results.png';

<figure>
<img alt="The results of scheduling off of external events. One should be scheduled off of an event belonging to the second source, of event type `TestType`." src={results} />
<figcaption>Figure 2: The results of the described procedure.</figcaption>
</figure>

As expected, there is a single activity, scheduled off of an `Event_01` from the second source.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ goals fix them. You do that by implement the `Goal` interface and interacting wi

## `EditablePlan`

The `EditablePlan` gives you the same interface as [`Plan`](../plan-and-sim-results), but also allows you to add new
The `EditablePlan` gives you the same interface as [`Plan`](../../plan-and-sim-results), but also allows you to add new
activities. You do this with `plan.create(...)`. You can either provide the minimal amount of information (type,
arguments, and start time), or provide a `NewDirective` object that is a little more configurable.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# External Events

External Events timelines are analogous to activity `Instances` timelines. They represent intervals with various properties that
are allowed to overlap in time, even if of the same type. It is possible to access the events associated with
a plan (specifically, the events that are members of derivation groups that are associated with a plan - see
[Associating Derivation Groups](../../../../../tutorials/external-events/associating-derivation-groups)).

A timeline of External Events is represented by the `ExternalEvents` class, which contains objects of the `ExternalEvent`
type. `ExternalEvent` objects have a `type`, a `source`, and an event `key` (which is unique within the `type` and `source`).
The `source` contains its own key and also refers to the derivation group that it is a part of.

Unlike `Instances`, external events currently don't have arguments or properties, so filtering by any other
attributes is not yet possible.

You can access external events using `plan.events()`. This will return all events associated with the plan; you can filter
the result afterward or you can provide a more specific query to the events function.

## Filtering Events

You can filter events either with the general-purpose `filter` method, or with the specific `filterBySource`, `filterByType`,
and `filterByDerivationGroup` methods.

<Tabs groupId="lang">
<TabItem value="kt" label="Kotlin">

```kotlin
// These are equivalent
plan.events().filter { it.type.equals("MyEventType") }
plan.events().filterByType("MyEventType")
```

</TabItem>
<TabItem value="java" label="Java">

```java
// These are equivalent
plan.events().filter(false, $ -> $.type.equals("MyEventType"));
plan.events().filterByType("MyEventType");
```

</TabItem>
</Tabs>

Similar operations exist for filtering by derivation group and source (which is a combination of source name and derivation group).
All of them accept varargs arguments (i.e. `.filterByType("TypeA", "TypeB")`) so you can filter for the union of multiple
types/sources/etc.

Lastly, you can filter by derivation group using the shorthand `plan.events("my derivation group")`.

## `EventQuery`

The `EventQuery` type is a more flexible way to express multiple filters, and the union of filters, at the same time.
For example if you wanted the events of a specific type, but from multiple derivation groups, you could express it like
this:

<Tabs groupId="lang">
<TabItem value="kt" label="Kotlin">

```kotlin
val query = EventQuery(
listOf("DerivationGroupA", "DerivationGroupB"), // both derivation groups are accepted
listOf("MyType"), // just one type is accepted
null // null indicates all sources are accepted
)
plan.events(query)
```

</TabItem>
<TabItem value="java" label="Java">

```java
final var query = new EventQuery(
List.of("DerivationGroupA", "DerivationGroupB"), // both derivation groups are accepted
List.of("MyType"), // just one type is accepted
null // null indicates all sources are accepted
);
plan.events(query);
```

</TabItem>
</Tabs>

Passing `null` means that all are accepted. Some, but not all, implementations of the `Plan` interface may use the query
object to selectively load only the events you want, improving performance. You could achieve with `plan.events().filterBy...`,
but since the chain started with an unfiltered query, all events will be loaded and then filtered out.
13 changes: 12 additions & 1 deletion sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ const sidebars = {
'scheduling-and-constraints/procedural/timelines/basics/profiles',
'scheduling-and-constraints/procedural/timelines/basics/sampling-and-caching',
'scheduling-and-constraints/procedural/timelines/basics/activities',
'scheduling-and-constraints/procedural/timelines/basics/external-events',
'scheduling-and-constraints/procedural/timelines/basics/windows',
'scheduling-and-constraints/procedural/timelines/basics/common-operations'
]
Expand All @@ -242,7 +243,17 @@ const sidebars = {
},
'scheduling-and-constraints/procedural/plan-and-sim-results',
'scheduling-and-constraints/procedural/constraints',
'scheduling-and-constraints/procedural/scheduling',
{
type: 'category',
label: 'Scheduling',
link: {
id: 'scheduling-and-constraints/procedural/scheduling/introduction',
type: 'doc'
},
items: [
'scheduling-and-constraints/procedural/scheduling/examples'
]
},
'scheduling-and-constraints/procedural/parameters-and-invocations',
// 'scheduling-and-constraints/procedural/running-externally'
]
Expand Down

0 comments on commit dc0dbe8

Please sign in to comment.