diff --git a/docs/overview/concept-of-operations.mdx b/docs/overview/concept-of-operations.mdx
index 583681f..d68315a 100644
--- a/docs/overview/concept-of-operations.mdx
+++ b/docs/overview/concept-of-operations.mdx
@@ -12,8 +12,8 @@ Aerie is designed to address the following capability gaps:
- Allow missions to provide a web deployment of a planning tool such that operators with limited programming skills can create valid activity plans and run simulations
- Support real time collaboration and and parallel hypothesis testing such that many more iterations of an activity plan can be tested within the same time frame
- Allow automation and manual modifications to coexist throughout the planning process
-- Provide a low-code constraint checking mechanism to validate simulation outputs which can be largely automated
-- Provide a low-code scheduling mechanism that can scaffold parts of or generate complete activity plans according to goal snippets
+- Provide a flexible constraint checking mechanism to validate simulation outputs which can be largely automated
+- Provide a flexible scheduling mechanism that can scaffold parts of or generate complete activity plans according to goal snippets
- Support an easy-to-use and verified translation from activities to sequences of commands that are recognized by the flight system
Note that none of the capabilities above are completely new. Other software solutions have offered pieces of the listed capabilities in different or more limited flavors. Aerie's mission is to cover as many key steps in the whole activity planning and sequencing workflow in one deployed tool with dedicated components.
diff --git a/docs/overview/software-design-document.mdx b/docs/overview/software-design-document.mdx
index 6830e2d..0e84302 100644
--- a/docs/overview/software-design-document.mdx
+++ b/docs/overview/software-design-document.mdx
@@ -892,7 +892,7 @@ DSL, Merlin has little to no ability to see the actual Java code
comprising a mission model. Merlin must instead make inferences about
the mission model based on its observable behavior.
-Merlin is a spiritual successor to the
+Merlin is a spiritual successor to the
[Blackbird](https://trs.jpl.nasa.gov/handle/2014/52245) planning
system, which similarly uses Java for activity and resource modeling.
Blackbird's design shed light on the myriad choices made in designing
@@ -1225,7 +1225,7 @@ constraint expression operators, while the leaf nodes represent the
operands. For example, operator nodes enable expressions such as 'or',
'and', 'less than', 'greater than', while the operands are simulated
resource profiles and activity instances. See our [constraints
-documentation](../../constraints/examples) for complete examples.
+documentation](../../scheduling-and-constraints/edsl/constraints/examples) for complete examples.
## Meta-Programming (Annotations Processing)
diff --git a/docs/scheduling/assets/run-scheduling-analysis.png b/docs/scheduling-and-constraints/assets/run-scheduling-analysis.png
similarity index 100%
rename from docs/scheduling/assets/run-scheduling-analysis.png
rename to docs/scheduling-and-constraints/assets/run-scheduling-analysis.png
diff --git a/docs/scheduling/assets/run-scheduling.png b/docs/scheduling-and-constraints/assets/run-scheduling.png
similarity index 100%
rename from docs/scheduling/assets/run-scheduling.png
rename to docs/scheduling-and-constraints/assets/run-scheduling.png
diff --git a/docs/scheduling/assets/scheduling-error.png b/docs/scheduling-and-constraints/assets/scheduling-error.png
similarity index 100%
rename from docs/scheduling/assets/scheduling-error.png
rename to docs/scheduling-and-constraints/assets/scheduling-error.png
diff --git a/docs/scheduling/assets/scheduling-failed.png b/docs/scheduling-and-constraints/assets/scheduling-failed.png
similarity index 100%
rename from docs/scheduling/assets/scheduling-failed.png
rename to docs/scheduling-and-constraints/assets/scheduling-failed.png
diff --git a/docs/scheduling/assets/scheduling-success.png b/docs/scheduling-and-constraints/assets/scheduling-success.png
similarity index 100%
rename from docs/scheduling/assets/scheduling-success.png
rename to docs/scheduling-and-constraints/assets/scheduling-success.png
diff --git a/docs/constraints/concepts.md b/docs/scheduling-and-constraints/declarative/constraints/concepts.md
similarity index 100%
rename from docs/constraints/concepts.md
rename to docs/scheduling-and-constraints/declarative/constraints/concepts.md
diff --git a/docs/constraints/examples.md b/docs/scheduling-and-constraints/declarative/constraints/examples.md
similarity index 100%
rename from docs/constraints/examples.md
rename to docs/scheduling-and-constraints/declarative/constraints/examples.md
diff --git a/docs/constraints/introduction.mdx b/docs/scheduling-and-constraints/declarative/constraints/introduction.mdx
similarity index 100%
rename from docs/constraints/introduction.mdx
rename to docs/scheduling-and-constraints/declarative/constraints/introduction.mdx
diff --git a/docs/scheduling-and-constraints/declarative/introduction.mdx b/docs/scheduling-and-constraints/declarative/introduction.mdx
new file mode 100644
index 0000000..e69de29
diff --git a/docs/scheduling/assets/goal-context-menu.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/goal-context-menu.png
similarity index 100%
rename from docs/scheduling/assets/goal-context-menu.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/goal-context-menu.png
diff --git a/docs/scheduling/assets/goal-editor-edit.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/goal-editor-edit.png
similarity index 100%
rename from docs/scheduling/assets/goal-editor-edit.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/goal-editor-edit.png
diff --git a/docs/scheduling/assets/goal-editor-new.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/goal-editor-new.png
similarity index 100%
rename from docs/scheduling/assets/goal-editor-new.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/goal-editor-new.png
diff --git a/docs/scheduling/assets/invalid-recurrence-goal.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/invalid-recurrence-goal.png
similarity index 100%
rename from docs/scheduling/assets/invalid-recurrence-goal.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/invalid-recurrence-goal.png
diff --git a/docs/scheduling/assets/scheduling-panel.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/scheduling-panel.png
similarity index 100%
rename from docs/scheduling/assets/scheduling-panel.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/scheduling-panel.png
diff --git a/docs/scheduling/assets/scheduling-specification.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/scheduling-specification.png
similarity index 100%
rename from docs/scheduling/assets/scheduling-specification.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/scheduling-specification.png
diff --git a/docs/scheduling/assets/xbeforey.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xbeforey.png
similarity index 100%
rename from docs/scheduling/assets/xbeforey.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xbeforey.png
diff --git a/docs/scheduling/assets/xcontainsy.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xcontainsy.png
similarity index 100%
rename from docs/scheduling/assets/xcontainsy.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xcontainsy.png
diff --git a/docs/scheduling/assets/xequalsy.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xequalsy.png
similarity index 100%
rename from docs/scheduling/assets/xequalsy.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xequalsy.png
diff --git a/docs/scheduling/assets/xfinishesy.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xfinishesy.png
similarity index 100%
rename from docs/scheduling/assets/xfinishesy.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xfinishesy.png
diff --git a/docs/scheduling/assets/xmeetsy.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xmeetsy.png
similarity index 100%
rename from docs/scheduling/assets/xmeetsy.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xmeetsy.png
diff --git a/docs/scheduling/assets/xoverlapsy.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xoverlapsy.png
similarity index 100%
rename from docs/scheduling/assets/xoverlapsy.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xoverlapsy.png
diff --git a/docs/scheduling/assets/xstartsy.png b/docs/scheduling-and-constraints/declarative/scheduling/assets/xstartsy.png
similarity index 100%
rename from docs/scheduling/assets/xstartsy.png
rename to docs/scheduling-and-constraints/declarative/scheduling/assets/xstartsy.png
diff --git a/docs/scheduling/global-conditions.mdx b/docs/scheduling-and-constraints/declarative/scheduling/global-conditions.mdx
similarity index 100%
rename from docs/scheduling/global-conditions.mdx
rename to docs/scheduling-and-constraints/declarative/scheduling/global-conditions.mdx
diff --git a/docs/scheduling/goals.mdx b/docs/scheduling-and-constraints/declarative/scheduling/goals.mdx
similarity index 100%
rename from docs/scheduling/goals.mdx
rename to docs/scheduling-and-constraints/declarative/scheduling/goals.mdx
diff --git a/docs/scheduling/introduction.mdx b/docs/scheduling-and-constraints/declarative/scheduling/introduction.mdx
similarity index 64%
rename from docs/scheduling/introduction.mdx
rename to docs/scheduling-and-constraints/declarative/scheduling/introduction.mdx
index e00b8df..70a7e1f 100644
--- a/docs/scheduling/introduction.mdx
+++ b/docs/scheduling-and-constraints/declarative/scheduling/introduction.mdx
@@ -1,8 +1,8 @@
# Scheduling
-This guide explains how to use the scheduling service with the latest version of Aerie.
+This guide explains how to use the declarative scheduling service with Aerie.
The scheduling service allows you to add activities to a plan based on goals that you define (this is something called "goal based scheduling").
-Goals are defined in [TypeScript](https://www.typescriptlang.org/) using an embedded domain specific language (EDSL) provided by Aerie.
+Declarative Goals are defined in [TypeScript](https://www.typescriptlang.org/) using an embedded domain specific language (EDSL) provided by Aerie.
:::note
diff --git a/docs/scheduling/modelling-temporal-relations.mdx b/docs/scheduling-and-constraints/declarative/scheduling/modelling-temporal-relations.mdx
similarity index 100%
rename from docs/scheduling/modelling-temporal-relations.mdx
rename to docs/scheduling-and-constraints/declarative/scheduling/modelling-temporal-relations.mdx
diff --git a/docs/scheduling/temporal-subset.md b/docs/scheduling-and-constraints/declarative/scheduling/temporal-subset.mdx
similarity index 100%
rename from docs/scheduling/temporal-subset.md
rename to docs/scheduling-and-constraints/declarative/scheduling/temporal-subset.mdx
diff --git a/docs/scheduling/run-scheduling.mdx b/docs/scheduling-and-constraints/execution.mdx
similarity index 52%
rename from docs/scheduling/run-scheduling.mdx
rename to docs/scheduling-and-constraints/execution.mdx
index 430155e..9e30d0b 100644
--- a/docs/scheduling/run-scheduling.mdx
+++ b/docs/scheduling-and-constraints/execution.mdx
@@ -1,35 +1,11 @@
-# Run Scheduling
+# Execution
-## Scheduling Specification
+## Constraints
-After you have created one or several goals, you will see them in the scheduling pane of the plan in Aerie UI.
+Constraints can be checked inside the main plan view by opening the `Constraints` pane and clicking the checklist icon
+in the top right of the pane. Alternatively, you can hover over `Constraints` in the top bar and click "Check Constraints".
-The Aerie scheduler accepts a list of goals, and tries to satisfy them one by one by adding activities to your plan. We refer to this list of goals as a **scheduling specification**. Aerie creates one scheduling specification per plan. A goal's priority is a number reflecting that goal's position in the scheduling specification. The first goal will always have priority `0`, and the n-th goal will always have priority `n - 1`. Within a scheduling specification, a scheduling goal can be toggled enabled or disabled. A disabled scheduling goal will be excluded from scheduling execution. The priority ordering of the scheduling goals remains unchanged whether a goal is enabled or disabled.
-
-import schedulingSpecification from './assets/scheduling-specification.png';
-
-
-
- Figure 1: Aerie UI Scheduling Specification
-
-
-In this image, you can see a specification with three goals. Goal 1 has priority `0`, and goal 3 has priority `2`. You can modify the priorities directly by typing numbers or use the arrows. You can disable a goal by toggling it off with the checkbox.
-
-:::caution
-
-You must use priorities in the `[0, n-1]` range, `n` being the number of goals.
-
-:::
-
-:::caution
-
-A given goal may be a part of zero or one specification - goals may not be shared between multiple specifications. If you need to do this, make a copy of the goal.
-
-There may be at most one specification at a time associated with a given plan.
-
-:::
-
-## Running the Scheduler
+## Scheduling
To run the scheduler, click on the play button:
@@ -71,7 +47,7 @@ import schedulingSuccess from './assets/scheduling-success.png';
- `174` means that there are `174` activity directives that contribute to the satisfaction of the goal
- `+3` means that 3 new activities have been inserted in the plan to satisfied the goal during the last scheduling run
-## Running a Scheduling Analysis
+### Running a Scheduling Analysis
The scheduler has an analysis mode that will evaluate the satisfaction of goals but will not place any new activities. To run the scheduler in analysis mode, click on the "analysis" button:
diff --git a/docs/scheduling-and-constraints/introduction.mdx b/docs/scheduling-and-constraints/introduction.mdx
new file mode 100644
index 0000000..b5472de
--- /dev/null
+++ b/docs/scheduling-and-constraints/introduction.mdx
@@ -0,0 +1,43 @@
+# Scheduling & Constraints
+
+Aerie provides related frameworks for defining constraints and scheduling new activities in the plan, and two implementations
+of those frameworks: one for arbitrary procedures that run on the JVM, and a legacy system based on a declarative Typescript eDSL
+(embedded Domain-Specific Language). Both frameworks are documented here, but new users are encouraged to focus on
+creating JVM procedures. The declarative eDSL is significantly less capable, and the difference in capabilities is only expected to
+grow.
+
+## Constraints
+
+Constraints represent what is nominal for a plan or mission model, and when executed, the UI will display "violations"
+whenever the plan or model is off-nominal. They don't alter the behavior of the simulation engine or scheduler; they
+just serve as a warning, indicating that some requirement - perhaps a flight rule - was broken.
+
+## Scheduling
+
+The scheduler allows users to automate the creation of new activities, to remove some cognitive load from planners. A
+scheduling specification contains a list of goals and rules with a priority order; during a scheduling run, they are
+executed one at a time, starting from a priority of 0 and increasing from there.
+
+### Procedural Goals
+
+Procedural goals directly edit the plan, creating new activities at definite (grounded) times. They
+can simulate potential changes to the plan, but aren't required to. In fact, a scheduling specification composed entirely
+of procedures might run in its entirety without performing any simulations, potentially at the cost of optimality or even
+soundness.
+
+### eDSL Goals
+
+eDSL goals are more declarative, in that they don't allow you to directly create grounded activities; instead they allow
+you to describe a pattern of activities that should be present in the plan. If the pattern isn't found, the goal tries to
+create it for you. Currently eDSL goals are simpler to write than procedural goals, for patterns that they can represent.
+Many goals are more complex than can be represented in the eDSL, and will have to be written as a procedure.
+
+### Global Conditions
+
+Global scheduling conditions (or sometimes just "conditions") are supplemental pieces of code that define when scheduling
+goals can and cannot place activities. They are incorporated into the solver when attempting to resolve conflicts as
+a substitute for constraints. This is because it is too difficult to respect constraints during scheduling; constraints
+only indicate that something went wrong, not what caused it or how to fix it. So in cases when the scheduler keeps violating
+constraints, users can create a condition as a heuristic to help it satisfy the constraint.
+
+Conditions will be accessible to scheduling rules, but will be non-binding.
diff --git a/docs/scheduling-and-constraints/management.mdx b/docs/scheduling-and-constraints/management.mdx
new file mode 100644
index 0000000..60dd740
--- /dev/null
+++ b/docs/scheduling-and-constraints/management.mdx
@@ -0,0 +1,41 @@
+# Management
+
+## Creating and Updating
+
+The recommended setup is to store goals/rules/conditions/constraints (hereafter called "peripheral code")
+in one or more repositories outside Aerie, and uploading
+them either through the UI or the unofficial [Aerie CLI](https://github.com/NASA-AMMOS/aerie-cli).
+
+For example, after creating a new goal as described in the following pages, you can upload it with the CLI using
+`aerie-cli goals new `. A new goal will be created in Aerie with a default name of `MyGoal`.
+See `aerie-cli goals new --help` for more details, such as automatically
+associating it with a model or plan. To update a goal afterward, you can run `aerie-cli goals update `,
+assuming that the default name was not changed. The same works for Typescript eDSL declarations, and similar workflows
+will be implemented for constraints and conditions soon.
+
+## Model and Plan Association
+
+In Aerie, peripherals live independently of plans and models, and can be associated with
+any number of plans and models, or none at all. Each model and plan has a scheduling specification and a constraints
+specification, which is simply a list of peripherals to run during the scheduling or constraints actions, respectively.
+
+Model specifications are never run directly, and instead populate the default spec for any plans created from that model.
+So if a particular constraint is widely applicable to all plans made from a particular model, you can associate it with
+the model by navigating to `Models -> -> Edit details ... -> Constraints -> Library`. Click the checkbox
+on your constraint and select `Save`. Now any new plans made with this model will include your constraint in its specification.
+
+Peripherals that don't apply to all plans for a model can be associated with individual plans too. In the main plan view,
+navigate to the `Scheduling Goals` or `Constraints` pane, then click `Manage`. Click the checkbox for your peripheral and
+select `Save`.
+
+Additionally, the Aerie CLI provides options when creating a new goal to associate it with a given model ID or plan ID.
+
+## Version Locking
+
+Peripheral associations can be locked to a specific revision, on either the model or plan specification. In the specification,
+you can change `Always use latest` to a revision number of your choice.
+
+## Deleting
+
+Peripherals can be deleted in the UI by navigating to the `Scheduling` or `Constraints` view in the top left, and deleting
+them there. This action can't be done if the peripheral is being used by a plan or model.
diff --git a/docs/scheduling-and-constraints/procedural/assets/duplicate-invocation.png b/docs/scheduling-and-constraints/procedural/assets/duplicate-invocation.png
new file mode 100644
index 0000000..d3d88f2
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/assets/duplicate-invocation.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1f0c68f294658156dfadcbf6b22cd7c6506f3fe65d0b628d1a088d5f80ed30cc
+size 32913
diff --git a/docs/scheduling-and-constraints/procedural/assets/parameterized-goal.png b/docs/scheduling-and-constraints/procedural/assets/parameterized-goal.png
new file mode 100644
index 0000000..89b4322
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/assets/parameterized-goal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2132b5afb950e33d149aba0af821092e179530dfb352bfd92fe27d432efcef9f
+size 26191
diff --git a/docs/scheduling-and-constraints/procedural/constraints.mdx b/docs/scheduling-and-constraints/procedural/constraints.mdx
new file mode 100644
index 0000000..bce4809
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/constraints.mdx
@@ -0,0 +1,170 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Constraints
+
+:::warning
+
+Procedural constraints aren't supported yet, although the interfaces are fully implemented. You should be able to run
+a constraint locally using the procedural-remote package, but they can't yet be integrated with Aerie.
+
+:::
+
+It's finally time to write a useful piece of code! Constraints are simple. They take a `Plan` and `SimulationResults`,
+and return a `Violations` timeline. Violations are a new type of timeline specific to constraints, that store `Violation`
+objects. You won't usually need to perform additional operations after creating a `Violations` timeline; usually you'll
+just return it. They can be created with some provided static constructor functions. For example:
+
+
+
+
+```kotlin
+@ConstraintProcedure
+class BatteryAboveZero: Constraint {
+ override fun run(plan: Plan, simResults: SimulationResults) = Violations.inside(
+ plan.resource("/battery_soc", Real.deserialize()).lessThan(0).highlightTrue()
+ )
+}
+```
+
+
+
+
+```java
+@ConstraintProcedure
+public class BatterAboveZero implements Constraint {
+ @NotNull
+ @Override
+ public Violations run(@NotNull Plan plan, @NotNull SimulationResults simResults) {
+ return Violations.inside(
+ plan.resource("/battery_soc", Real.deserialize()).lessThan(0).highlightTrue()
+ );
+ }
+}
+```
+
+
+
+
+## Generator Constraints
+
+For more complex constraints, it may be tedious to try to represent all the violations in a single `Violations` timeline,
+and easier to create violations more iteratively. In this case, you could simply add to a list of violations, then
+create a timeline at the end with `new Violations(violationsList)`, or you could use some helper functions provided by
+the `GeneratorConstraint` abstract class instead.
+
+:::tip
+
+Because of Kotlin's [extension function concept](https://kotlinlang.org/docs/extensions.html),
+the `GeneratorConstraint` class's ergonomics are much more helpful in Kotlin, and only provides a marginal benefit in Java.
+
+:::
+
+For example, to violate whenever `MyActivity` occurs when `/my/resource < 0`, you could do the following:
+
+
+
+
+```kotlin
+@ConstraintProcedure
+class MyConstraint: GeneratorConstraint() {
+ override fun generate(plan: Plan, simResults: SimulationResults) {
+ val myResource = simResults.resource("/my/resource", Real.deserialize()).cache()
+ for (activity in plan.directives("MyActivity")) {
+ if (myResource.sample(activity.startTime) < 0)
+ violate(Violation(activity.interval))
+ }
+ }
+}
+```
+
+
+
+
+```java
+@ConstraintProcedure
+public class MyConstraint extends GeneratorConstraint {
+ @Override
+ public void generate(@NotNull Plan plan, @NotNull SimulationResults simResults) {
+ final var myResource = simResults.resource("/my/resource", Real.deserialize()).cache();
+ for (final var activity: plan.directives("MyActivity")) {
+ if (myResource.sample(activity.startTime) < 0)
+ violate(Violation(activity.interval));
+ }
+ }
+}
+```
+
+
+
+
+Additionally, the `GeneratorConstraint` class provides some nice extension functions (all beginning with `violate...`)
+that you can apply to your timelines, which convert them into violations and automatically submit them. This only works
+as shown in Kotlin. You can call these functions in Java, but the syntax isn't any more ergonomic than just calling `violate(...)`
+normally.
+
+```kotlin
+@ConstraintProcedure
+class BatteryAboveZero: GeneratorConstraint() {
+ override fun generate(plan: Plan, simResults: SimulationResults) {
+ simResults.resource("/battery_soc", Real.deserialize())
+ .greaterThan(0)
+
+ // Only works in a generator constraint!
+ // Only works in Kotlin!
+ .violateOn(false)
+ }
+}
+```
+
+## Violation Messages
+
+The `Violation` class contains a `message` field, which will display to the user in the UI. It is `null` by default,
+but you have two ways to change it.
+
+If you want to set different messages for each violation, you can create the `Violation` objects yourself and then pass
+each one individually to `violate(...)` in a generator constraint. Or, if you want to set the same message for all
+violations, you can override the `message()` function in the `Constraint` interface. To repeat a previous example:
+
+
+
+
+```kotlin
+@ConstraintProcedure
+class MyConstraint: GeneratorConstraint() {
+ override fun message() = "MyActivity cannot start when /my/resource < 0"
+
+ override fun generate(plan: Plan, simResults: SimulationResults) {
+ val myResource = simResults.resource("/my/resource", Real.deserialize()).cache()
+ for (activity in plan.directives("MyActivity")) {
+ if (myResource.sample(activity.startTime) < 0)
+ violate(Violation(activity.interval))
+ }
+ }
+}
+```
+
+
+
+
+```java
+@ConstraintProcedure
+public class MyConstraint extends GeneratorConstraint {
+ @Override
+ public String message() {
+ return "MyActivity cannot start when /my/resource < 0";
+ }
+
+ @Override
+ public void generate(@NotNull Plan plan, @NotNull SimulationResults simResults) {
+ final var myResource = simResults.resource("/my/resource", Real.deserialize()).cache();
+ for (final var activity: plan.directives("MyActivity")) {
+ if (myResource.sample(activity.startTime) < 0)
+ violate(Violation(activity.interval));
+ }
+ }
+}
+```
+
+
+
diff --git a/docs/scheduling-and-constraints/procedural/getting-started.mdx b/docs/scheduling-and-constraints/procedural/getting-started.mdx
new file mode 100644
index 0000000..4cae359
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/getting-started.mdx
@@ -0,0 +1,27 @@
+# Getting Started
+
+Currently only scheduling procedures are supported, not constraint procedures.
+
+## Create a project from the template
+
+We have created a template repository for your mission model and scheduling procedures [here](https://github.com/NASA-AMMOS/aerie-mission-model-template).
+If you don't already have a mission model project, you can just copy that template and follow the instructions.
+If you do, you can follow these steps to add a place for your procedures:
+
+1. Move your mission model code into a gradle subproject if it isn't already.
+2. Create a `scheduling` subproject.
+3. Copy the `build.gradle` from the `scheduling` subproject of the [mission model template repo](https://github.com/NASA-AMMOS/aerie-mission-model-template).
+4. You can now create procedures in a java package in the `scheduling` subproject, as long as the package path ends in `procedures`.
+ (i.e. `src/main/java/myorg/mymission/procedures`)
+
+## Compiling
+
+It is a two-part process to build your scheduling jars.
+1. Run `./gradlew :scheduling:compileJava` (or any command that delegates to it, such as `:scheduling:build` or a top-level `build`).
+2. Run `./gradlew :scheduling:buildAllSchedulingProcedureJars`. This produces the jar artifacts for each procedure.
+
+There should now be one jar for each scheduling procedure, at `scheduling/build/libs/.jar`.
+
+## Creating a Goal
+
+See the examples in the mission model template repo, or see [the scheduling page](../scheduling) in this section.
diff --git a/docs/scheduling-and-constraints/procedural/introduction.mdx b/docs/scheduling-and-constraints/procedural/introduction.mdx
new file mode 100644
index 0000000..04329c5
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/introduction.mdx
@@ -0,0 +1,7 @@
+# Procedural Scheduling & Constraints
+
+Aerie allows users to write custom JVM procedures to check constraints or schedule new activities with complete freedom.
+Most users will choose to use Java, but since the libraries are written in Kotlin, they
+provide some quality-of-life improvements and syntactic sugar for those using Kotlin. Additionally, Kotlin's more intelligent
+type inference, null-safety, and currying syntax make writing peripheral procedures a more seamless experience, although
+the API is intended to work well with Java too.
diff --git a/docs/scheduling-and-constraints/procedural/parameters-and-invocations.mdx b/docs/scheduling-and-constraints/procedural/parameters-and-invocations.mdx
new file mode 100644
index 0000000..002180e
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/parameters-and-invocations.mdx
@@ -0,0 +1,75 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Parameters & Invocations
+
+Goals and constraints can be given arguments, much like activities can; they actually use the exact same system.
+This allows you to configure a goal that needs to be reused in slightly different ways across multiple plans, or
+even multiple times in the same plan.
+
+## Parameters
+
+:::tip
+
+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
+unconditionally recur every hour, and instead takes the period as input:
+
+
+
+
+```kt
+Kotlin parameter mapping is not yet supported :(
+```
+
+
+
+
+```java
+@SchedulingProcedure
+public record RecurringMyActivity(Duration period) implements Goal {
+ @Override
+ public void run(EditablePlan plan) {
+ // This produces a Booleans profile that is true at the instant of a MyActivity directive.
+ final var existingActivities = plan.directives("MyActivity").active().cache();
+
+ for (final var time: plan.totalBounds().step(period)) {
+ if (!existingActivities.sample(time)) plan.create(
+ "MyActivity",
+ new DirectiveStart.Absolute(time),
+ Map.of()
+ );
+ }
+ }
+}
+```
+
+
+
+After you add the goal to your plan's scheduling specification, you'll see the goal invocation with a `period` parameter
+to be filled in.
+
+import parameterizedGoal from './assets/parameterized-goal.png';
+
+
+
+ Figure 1: Aerie Scheduling - Parameterized Goal
+
+
+## Invocations
+
+You can invoke the same goal multiple times in the same specification. This is useful for very specific, targeted goals;
+so the above goal isn't a very practical example of multiple invocations, but that won't stop us. You can right-click on
+the invocation and select "Duplicate invocation":
+
+import duplicateInvocation from './assets/duplicate-invocation.png';
+
+
+
+ Figure 2: Aerie Scheduling - Duplicate Invocation
+
+
+You can then create another invocation of the same goal, but likely with different parameters.
diff --git a/docs/scheduling-and-constraints/procedural/plan-and-sim-results.mdx b/docs/scheduling-and-constraints/procedural/plan-and-sim-results.mdx
new file mode 100644
index 0000000..ffb3cb8
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/plan-and-sim-results.mdx
@@ -0,0 +1,91 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# The Plan & Simulation Results
+
+The two main interfaces you'll query data from are `Plan` and `SimulationResults`. In constraints, they are provided as
+separate objects, they are combined into one object called `EditablePlan`.
+
+## `Plan`
+
+The plan object contains information that defines the plan. It also provides utility functions for converting between
+java `Instant` objects and our `Duration` objects, which are relative to the start of the plan.
+
+The `plan.totalBounds()` method gives an interval that defines the extent of the plan. The whole plan might not be simulated,
+but the plan bounds won't change.
+
+### Directives
+
+The `plan.directives(...)` methods allow you to get the activity directive timelines that define the plan.
+`directives()` and `directives(type: String)` get a timeline of all directives or of just a specific activity type respectively;
+and it returns them as the generic `AnyDirective` representation. If you want to use a custom representation `A`, you can call
+`directives(type: String, deserializer: (SerializedValue) -> A)` and provide your own deserializer.
+
+All directives contain an `id: ActivityDirectiveId` field, which can be used to identify a directive. These are used to
+define anchored activities; the anchored activity will contain the id of the activity it anchors to. In constraints,
+these ids will be the same value stored in the database, and will remain accurate after the constraint run is finished.
+But in scheduling, we can only guarantee the accuracy of the id for *existing* activities; any activities you create are
+given a temporary id which may change when the scheduling run ends.
+
+Some anchored activities can't have a grounded start time, because they might be anchored to the end of an activity with
+an unknown duration. But we still have to provide an estimated start time for it to be a valid object in the timeline,
+so we assume the duration of the anchor target is `0`.
+
+The directives are always up-to-date, even if the simulation results aren't.
+
+## Simulation Results
+
+The `SimulationResults` object contains the activity instances and resource profiles of a simulation run.
+
+### Instances
+
+Activity instances are the simulated version of a directive; they have a duration, a definite grounded start time
+(in the case of anchored activities), and any computed attributes the activity defines, in addition to all the data
+stored in the directive.
+
+All instances have an `id: ActivityInstanceId` field, which usually, but not always matches the id of the corresponding
+directive. For clarity, it also includes a `directiveId: ActivityDirectiveId?` field which *might be null*, because some
+instances are spawned from other activities and don't have a corresponding directive.
+
+You can query instances the same way as directives, using `simResults.instances(...)`.
+
+### Resources
+
+You can query resources with `simResults.resource("/my/resource", )`. Unlike activities, there is no option
+to use a default deserializer; you must pick one, because it determines the profile type. Each profile type provides a
+deserializer for you to use, so for example, you can get a string resource with `simResults.resource("/my/string", Strings.deserialize())`.
+
+If you made your own data structure for your resource (say, `V`), you'll probably want to use the `Constants` profile.
+But unfortunately you have to do any deserialization yourself. If you don't want to, you can just use `.resource("/my/object", Constants::new)`,
+but this will return `Constants`. To do proper deserialization, call `Constants.deserialize`:
+
+
+
+
+```kotlin
+// Segment payload we will deserialize into.
+data class Point2(x: Double, y: Double) {}
+
+// note the use of . instead of :: here! v
+val myResource = plan.resource("/my/object", Constants.deserialize {
+ val fields = it.asMap().get()
+ Point2(fields.get("x").asReal().get(), fields.get("y").asReal().get())
+})
+```
+
+
+
+
+```java
+// Segment payload we will deserialize into.
+record Point2(double x, double y) {}
+
+// note the use of . instead of :: here! v
+final var myResource = plan.resource("/my/object", Constants.deserialize($ -> {
+ final var fields = it.asMap().get();
+ return new Point2(fields.get("x").asReal().get(), fields.get("y").asReal().get());
+}));
+```
+
+
+
diff --git a/docs/scheduling-and-constraints/procedural/running-externally.mdx b/docs/scheduling-and-constraints/procedural/running-externally.mdx
new file mode 100644
index 0000000..253149c
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/running-externally.mdx
@@ -0,0 +1,3 @@
+# Running Externally
+
+TODO
diff --git a/docs/scheduling-and-constraints/procedural/scheduling.mdx b/docs/scheduling-and-constraints/procedural/scheduling.mdx
new file mode 100644
index 0000000..e2f2eed
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/scheduling.mdx
@@ -0,0 +1,203 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Scheduling
+
+Procedural scheduling goals are very similar to procedural constraints, except that where constraints report problems,
+goals fix them. You do that by implement the `Goal` interface and interacting with the provided `EditablePlan` object.
+
+## `EditablePlan`
+
+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.
+
+The `create` function will tentatively add your directive to the plan in an uncommitted state, and return an
+`ActivityDirectiveId` object for the activity you just added. This way you can anchor another activity to the one you
+just created.
+
+:::warning
+
+Activity directive IDs are only accurate for activities that already exist in the plan. For newly created activities,
+`plan.create` returns an estimated placeholder ID, which will almost certainly change when it is uploaded to the database.
+So if your goal has any other side effects, do not rely on the accuracy of these IDs.
+
+:::
+
+If you then call `plan.directives(...)`, you'll see the activity you just created now included in the plan, but you're not
+quite done. These new activities are in an uncommitted state, and you must either call `plan.commit()` or `plan.rollback()`
+before the goal returns. `rollback` will reset the plan to the last time you called `commit`, or to the beginning of the goal.
+We use this system so you can add some activities to the plan, test it out with simulation, and then choose to keep them or not
+depending on the simulation results.
+
+## Simulation
+
+To try out your changes, you can call `simResults = plan.simulate()`. This will simulate the whole plan from start to finish.
+This returns the same `SimulationResults` object used in constraints.
+
+:::tip
+
+If you run simulation multiple times, you might have multiple `SimulationResults` objects floating around; it is up to
+you to keep those straight and manage that state. Most goals that simulate will just have a single mutable `simResults`
+variable that you update with each simulation, ensuring that the old results are lost and won't mess up your state.
+
+:::
+
+You can configure the simulation by passing a `SimulateOptions` object into `plan.simulate(...)`. Currently, the only
+option is to set when the simulation will pause. If you pause partway through the plan, then make a change *after* the
+pause time, if you simulate again it will attempt to pick up where it left off rather than start over.
+
+### Stale sim results
+
+Some goals will want to get simulation results cheaply without simulating, at the risk of unsoundness. You can do this
+by calling `plan.latestResults()` instead of `.simulate()`. This will return whatever was simulated last, even if it
+didn't simulate the whole plan, even if modifications were made later. There may be mismatches between the plan state
+and what was actually simulated; it is up to you to either accept this risk, or check its accuracy.
+
+It is possible for `latestResults` to return `null`, if the plan has never been simulated. So it is recommended to start
+such goals with:
+
+
+
+
+```kotlin
+val simResults = plan.latestResults() ?: plan.simulate()
+```
+
+
+
+
+```java
+var simResults = plan.latestResults();
+if (simResults == null) simResults = plan.simulate();
+```
+
+
+
+
+### Simulate After
+
+All goals have a flag stored outside the goal definition called "Simulate After". If enabled, a new simulation will
+be run when the goal finishes. This exists to enable some simulation control in eDSL goals, but is applicable to *all*
+goals because procedural and eDSL goal may need to be interleaved in a scheduling specification.
+
+:::warning
+
+Even if you write a procedural goal that does not simulate and uses stale results, simulation may still be run after
+your goal anyway if the "Simulate After" flag is checked in the UI.
+
+:::
+
+Additionally, if "Simulate After" is enabled on the last goal in the spec, the scheduler will simulate one last time
+and upload those results to the database. If not, it will just upload the latest stale results.
+
+## Idempotency
+
+Most scheduling goals will be written to detect a problem in the plan that can be solved by adding activities, and
+then add them. But it's important to keep in mind that you have to actually *check* for the problem. For example,
+if I want a `MyActivity` directive at every hour on the plan, I could easily write it like this:
+
+
+
+
+```kt
+@SchedulingProcedure
+class MyActivityEveryHour: Goal {
+ override fun run(plan: EditablePlan) {
+ for (time in plan.totalBounds() step Duration.HOUR) {
+ plan.create(
+ "MyActivity",
+ DirectiveStart.Absolute(time),
+ mapOf() // assuming it takes no arguments
+ )
+ }
+ }
+}
+```
+
+
+
+
+```java
+@SchedulingProcedure
+public class MyActivityEveryHour implements Goal {
+ @Override
+ public void run(EditablePlan plan) {
+ for (final var time: plan.totalBounds().step(Duration.HOUR)) {
+ plan.create(
+ "MyActivity",
+ new DirectiveStart.Absolute(time),
+ Map.of()
+ );
+ }
+ }
+}
+```
+
+
+
+
+This is essentially quick-and-dirty procedural version of the eDSL's Recurrence Goal.
+
+However, *every* time this goal is run, it will unconditionally create a new series of activities, so you'd have to be careful
+to disable the goal from your scheduling spec afterward, or otherwise ensure that scheduling will only ever be run once.
+This is not a requirement that most missions can impose on operations.
+
+So instead, you have to first check if the goal is already satisfied. A rudimentary version of this could look like:
+
+
+
+
+```kt
+@SchedulingProcedure
+class MyActivityEveryHour: Goal {
+ override fun run(plan: EditablePlan) {
+ // This produces a Booleans profile that is true at the instant of a MyActivity directive.
+ val existingActivities = plan.directives("MyActivity").active().cache()
+
+ for (time in plan.totalBounds() step Duration.HOUR) {
+ if (!existingActivities.sample(time)) plan.create(
+ "MyActivity",
+ DirectiveStart.Absolute(time),
+ mapOf() // assuming it takes no arguments
+ )
+ }
+ }
+}
+```
+
+
+
+
+```java
+@SchedulingProcedure
+public class MyActivityEveryHour implements Goal {
+ @Override
+ public void run(EditablePlan plan) {
+ // This produces a Booleans profile that is true at the instant of a MyActivity directive.
+ final var existingActivities = plan.directives("MyActivity").active().cache();
+
+ for (final var time: plan.totalBounds().step(Duration.HOUR)) {
+ if (!existingActivities.sample(time)) plan.create(
+ "MyActivity",
+ new DirectiveStart.Absolute(time),
+ Map.of()
+ );
+ }
+ }
+}
+```
+
+
+
+
+This will check only for `MyActivity` directives that happen at the exact time it wants to place a new one. This is better
+than nothing, but still not very smart. If a planner manually inserted a `MyActivity` at a time that didn't exactly line
+up, this check would ignore it.
+
+Maybe you would want the goal to ensure that there is *at most* one hour between activities, reacting to existing activities
+by detecting directives that don't land exactly on the hour and proceeding to a hour after that directive. This is
+a pretty common pattern that can be accomplished with a while loop, repeatedly popping off the first directive in the list
+of existing `MyActivity`s, and is for now left as an exercise for the user.
+
+In the future we plan to provide helper functions that accomplish common patterns like this.
diff --git a/docs/scheduling-and-constraints/procedural/timelines/advanced/custom-operations.mdx b/docs/scheduling-and-constraints/procedural/timelines/advanced/custom-operations.mdx
new file mode 100644
index 0000000..5d0ceca
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/advanced/custom-operations.mdx
@@ -0,0 +1 @@
+# Custom Operations
diff --git a/docs/scheduling-and-constraints/procedural/timelines/advanced/custom-timelines.mdx b/docs/scheduling-and-constraints/procedural/timelines/advanced/custom-timelines.mdx
new file mode 100644
index 0000000..64c9df1
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/advanced/custom-timelines.mdx
@@ -0,0 +1 @@
+# Custom Timelines
diff --git a/docs/scheduling-and-constraints/procedural/timelines/advanced/introduction.mdx b/docs/scheduling-and-constraints/procedural/timelines/advanced/introduction.mdx
new file mode 100644
index 0000000..5aa0e71
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/advanced/introduction.mdx
@@ -0,0 +1,3 @@
+# Advanced Timeline Concepts
+
+TODO
diff --git a/docs/scheduling-and-constraints/procedural/timelines/advanced/parallel-profiles.mdx b/docs/scheduling-and-constraints/procedural/timelines/advanced/parallel-profiles.mdx
new file mode 100644
index 0000000..1ed5225
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/advanced/parallel-profiles.mdx
@@ -0,0 +1 @@
+# Parallel Profiles
diff --git a/docs/scheduling-and-constraints/procedural/timelines/basics/activities.mdx b/docs/scheduling-and-constraints/procedural/timelines/basics/activities.mdx
new file mode 100644
index 0000000..b390ccb
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/basics/activities.mdx
@@ -0,0 +1,51 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Activities
+
+Activity timelines are the most common type of timeline that allow the objects to overlap. In most plans, activities
+of the same type won't overlap with each other, but you should never assume this in your code unless you have some
+algorithmic guarantee.
+
+There are two types of activity timelines: `Directives` and `Instances`. `Directives` are essentially the definition of
+the plan before simulation, and `Instances` are the results of simulation. They both contain the arguments that you gave
+them. `Directives` are instantaneous, since they haven't been simulated yet, but `Instance` objects have a duration.
+
+## `AnyDirective` and `AnyInstance`
+
+The activity timelines accept user-definable representations of the arguments, in their generic argument (i.e.
+`Directives`). In the future, we will implement support for linking to the mission model and directly
+using its activity types, but for now the only representations we provide are the `AnyDirective` and `AnyInstance` types.
+
+These representations give you flexibility at the cost of ergonomics. They store the arguments as `Map`
+(you can read about `SerializedValue` [here](/mission-modeling/activity-mappers/#what-is-a-serializedvalue)), which can be
+used for any activity type.
+
+For example, to get a timeline of `MyActivity`'s, where the `arg` argument is equal to `3`, you can do the following.
+(Filtering and querying from the plan are explained more later)
+
+
+
+
+```kotlin
+plan.directives("MyActivity")
+ .filter {
+ it.inner // this accesses the inner AnyDirective object
+ .arguments.get("arg").asInt().get() == 3
+ }
+```
+
+
+
+
+```java
+plan.directives("MyActivity")
+ .filter(
+ false,
+ $ -> $.inner // this accesses the inner AnyDirective object
+ .arguments.get("arg").asInt().get() == 3
+ )
+```
+
+
+
diff --git a/docs/scheduling-and-constraints/procedural/timelines/basics/common-operations.mdx b/docs/scheduling-and-constraints/procedural/timelines/basics/common-operations.mdx
new file mode 100644
index 0000000..62c912f
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/basics/common-operations.mdx
@@ -0,0 +1,156 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Common Operations
+
+Since all timelines are just lazily-evaluated lists, there are a few low-level operations that are common to all timelines
+(or all profiles) that you'll recognize from functional programming.
+
+## Filters
+
+There are three related families of filter-like operations available on all timelines: `filter`, `highlight`, and `isolate`,
+and their derivatives. They all take a predicate, and objects not satisfying that predicate will not be included in the
+output. But, the three operations do different things with the satisfying objects.
+
+### Basic Filtering
+
+The `.filter(predicate)` method is the simplest, and does what you'd expect: it keeps objects that satisfy the predicate,
+drops those that don't, and returns a new timeline of the same type. To repeat an example from before, you can use this
+to only consider activities with a certain argument:
+
+
+
+
+```kotlin
+plan.directives("MyActivity")
+ .filter {
+ it.inner // this accesses the inner AnyDirective object
+ .arguments.get("arg").asInt().get() == 3
+ }
+```
+
+
+
+
+```java
+plan.directives("MyActivity")
+ .filter(
+ false,
+ $ -> $.inner // this accesses the inner AnyDirective object
+ .arguments.get("arg").asInt().get() == 3
+ )
+```
+
+
+
+
+You can filter objects by their duration with `filterByDuration`, `filterLongerThan`, and `filterShorterThan`.
+These functions, through the magic of `CollectOptions.truncateMarginal`, are able to filter based on the full extent of
+the object even if part of it lays outside the collection bounds (see [Collecting](../introduction#Collecting)).
+
+Lastly, you can use `.filterByWindows(...)` to select only objects that coincide with the intervals in a `Windows` object.
+This is useful when a constraint or goal needs to only apply to a certain subset of the plan, such as only during a
+given mission phase.
+
+### Highlighting
+
+Highlighting returns a `Windows` object instead of whatever timeline type you started with. This is meant to show the
+intervals of the plan where the predicate is true, when you don't care about the actual objects that satisfied the predicate.
+This method always loses information; if you want to keep the satisfying object around, use `filter` or `isolate`.
+
+Profiles provide a shorthand `.highlightEqualTo(value)` method, which does exactly what you think.
+
+Highlighting is often used to iterate through the regions when a certain condition is met (to repeat another example):
+
+
+
+
+```kotlin
+val myResource = plan.resource("my_resource", Real.deserialize())
+for (interval in myResource.highlightEqualTo(3)) {
+ // Do something with the interval
+ // `interval` is JUST an interval, the original segment has been lost.
+}
+```
+
+
+
+
+```java
+final var myResource = plan.resource("my_resource", Real.deserialize());
+for (final var interval: myResource.highlightEqualTo(3)) {
+ // Do something with the interval
+ // `interval` is JUST an interval, the original segment has been lost.
+}
+```
+
+
+
+
+### Isolating
+
+Isolating returns a `Universal` timeline. This type hasn't been mentioned yet, since it's a more advanced topic,
+but a `Universal` timeline can store any interval-like object with no specializations.
+
+Much like an activity timeline,
+`Universal` timeline objects do not coalesce if they overlap; this is why the operation is called "isolate". When applied
+to a profile, where the segments interact if they overlap by either coalescing or overwriting, the satisfying segments
+in the result become non-interacting.
+This is the basis of [Parallel Profiles](../../advanced/parallel-profiles), and is an advanced concept that most goals
+and constraints will not need.
+
+Like `highlight`, `isolate` has shorthand for `isolateEqualTo` on profiles.
+
+## Mapping
+
+All timelines have a `.unsafeMap` method, which allows you to transform each object individually in any way you want.
+You can use this to transform it to a new object of the same type, or convert it to a new type and store it in a different
+timeline type.
+
+It is unlikely that you'll need to directly use `unsafeMap`, since there are lots of more specific operations that delegate
+to it, like:
+- `unsafeMapIntervals`, which only allows you to transform the object's interval
+- `shift`, which uniformly shifts the entire timeline forward or backward in time
+- `mapValues` for profiles, which only allow you transform the segment's value
+- many unary operations on profiles, like `.not()` for booleans, `.negate()` for numeric profiles, etc.
+
+### Why "unsafe"?
+
+The main contract that all timelines satisfy is that when you call `collect(bounds)`, the results returned are contained
+inside the `bounds` interval. More specific timelines like profiles have additional requirements (like being sorted and
+non-overlapping), but all timeline collection results must be contained within the bounds. But for performance reasons,
+this requirement is usually not checked or enforced, and is only satisfied through sound algorithmic guarantees.
+
+`unsafeMap`, and some methods that delegate to it, allow you to change the objects' intervals to whatever you want,
+and potentially return a list that violates any or all of the above requirements. They are use-at-your-own-risk.
+
+## Binary Operations
+
+There are two generic methods provided for binary operations between two timelines, one for all timelines, and one specifically
+for profiles.
+
+### `unsafeMap2`
+
+`unsafeMap2` and its derivatives can be applied between any two timelines, even if they are different types. It's pretty simple:
+it searches for every pairing of objects in the operands that have overlapping intervals, and calls a binary function
+that you provide to create a new object in the result.
+
+It calls your binary function on every overlapping pair; meaning that if one object in the left operand overlaps with
+two objects in the right operand, the function will be called with the left object twice.
+
+A variant called `map2Values` exists just for profiles, and allows you to just worry about the values in the profile segments,
+as long as you don't care about treating gaps specially.
+
+### `map2OptionalValues`
+
+If you do need to treat profile gaps specially, you can use `map2OptionalValues`. This method will call your binary function
+whenever two segments in the operands overlap, *and* whenever a segment in one operand exists over a gap in the other. When
+one operand has a gap, the value provided to your operation will be `null`. It does not call your function when both profiles
+have a gap.
+
+You can also use the `NullBinaryOperation` interface's static helper methods to create your operation with some common patterns.
+
+## Inspection
+
+The `.inspect` method is for when you want to observe the intermediate state of a stack of operations, for debugging, logging, or
+other side effects. Keep in mind that like all operations, it doesn't run until `.collect` is called.
diff --git a/docs/scheduling-and-constraints/procedural/timelines/basics/introduction.mdx b/docs/scheduling-and-constraints/procedural/timelines/basics/introduction.mdx
new file mode 100644
index 0000000..8dc6146
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/basics/introduction.mdx
@@ -0,0 +1,62 @@
+# Basics
+
+Timelines are used to represent all time-distributed information about a plan and simulation. They can represent resource
+profiles, activities, and more, and come with operations for performing uniform global transformations easily.
+If you need more fine-grained control, you can create your own operations, or unwrap the timeline entirely and work
+with the underlying payload objects.
+
+The short explanation of a timeline is a lazy bounded list of time-distributed objects. The long
+explanation is the rest of this page.
+
+## Durations
+
+In procedural constraints/scheduling, absolute times are represented as a relative `Duration` object, centered on
+the start of the plan. So, if your plan starts at midnight on January 1st, and you want to create an activity at noon
+on January 2nd, you could refer to that using `Duration.hours(36)`. Alternatively, you can convert an absolute-time `java.time.Instant` to
+durations and back using `plan.toRelative` and `plan.toAbsolute`, where `plan` is provided to you in the constraint or
+goal interface. Durations have a resolution of microseconds and can represent any time you could reasonably want for the
+next 100,000 years. They can of course be negative.
+
+Duration constructors are provided for common units up to days, where a day is defined as 24 hours exactly, and have
+no time zone information. Other timekeeping systems are possible but not directly supported, so you will need to
+perform the conversions yourself.
+
+# Intervals
+
+`Interval`s are absolute time ranges, with start and end points represented by `Duration`s. They can include or exclude
+their extremes, which is often used to represent non-overlapping segments of resource profiles. For example, the intervals
+`Interval.between(hours(0), hours(1))` and `between(hours(1), hours(2))` both contain the point at 1 hour, but
+`betweenClosedOpen(hours(0), hours(1))` and `between(hours(1), hours(2))` meet perfectly without overlap.
+
+Intervals can be single points (i.e. `Interval.at(Duration.days(1))`), or even empty (`Interal.EMPTY`). Empty intervals
+are any that end before they start. They are all equivalently empty, but are not equal, and some operations that assume
+the interval is not empty may behave differently on unequal empty intervals.
+
+## Interval-Like Objects
+
+Intervals are used to represent when each object in a timeline happens. Timelines do this by requiring that their payload
+objects implement the `IntervalLike` interface. Good examples are profile `Segment`s, activity `Instance`s, and of course
+`Interval` itself. Any class that represents data that happens at a specific time or range of times can be made to implement
+`IntervalLike`, and can be used to make a timeline.
+
+## Collecting
+
+Timelines are lazy, meaning that when you perform an operation on them, the operation isn't actually evaluated until you
+try to access the result. This is usually done with the `collect` method, which a returns a list of your payload objects.
+`collect` is usually called with an interval specifying the bounds on which it should be evaluated, so that unused results
+aren't calculated. The operations are expected to return results contained in the bounds even they could return more
+with no extra cost.
+
+Any time you want to stop using timelines and directly work with the underlying data, you call `tl.collect(bounds)`
+(or just `tl.collect()` for unbounded results).
+
+By default, objects that straddle the start or end of the bounds are truncated to fit inside it. You can change this
+by calling `collect` with a `CollectOptions` object as argument, and set `truncateMarginal` to `false`.
+As in, `ti.collect(new CollectOptions(bounds, false))`. This will preserve the intervals of anything that is only partially
+inside the bounds.
+
+## Iterating
+
+All timeline objects implement [`Iterable`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-iterable/),
+allowing them to be used in a for loop. Keep in mind that this calls `.collect()`, which completely evaluates the timeline
+into a list; it doesn't enable any proper stream-style programming.
diff --git a/docs/scheduling-and-constraints/procedural/timelines/basics/profiles.mdx b/docs/scheduling-and-constraints/procedural/timelines/basics/profiles.mdx
new file mode 100644
index 0000000..549e679
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/basics/profiles.mdx
@@ -0,0 +1,77 @@
+# Profiles
+
+Profile are one of the main specializations of timelines; they represent a value changing over time. They can be used for
+resources in simulation results, activity arguments, or really any function over time.
+
+## Segments
+
+The `Segment` class is the payload object used by all profiles, and it just associates a value of any type `V` to an
+`Interval`.
+
+Segments in a profile are guaranteed to be ordered and non-overlapping. If you try to create a profile with overlapping
+segments, the later segments will overwrite the earlier segments, and the earlier segments will be truncated.
+
+## Coalescing
+
+When two adjacent segments in a profile have the same value, they are automatically "coalesced", meaning they are replaced
+with a single segment that spans both of them. So if an operation on a boolean profile produces `true` on both the intervals
+`[0hr, 1hr)` and `[1hr, 2hr]`, the final result will instead contain `true` on a single segment from `[0hr, 2hr]`.
+
+## Gaps
+
+Resource profiles from simulation will be defined at all times, but this isn't a requirement for all profiles. If a profile
+doesn't have a segment on a particular interval, that is called a "gap". The meaning of a gap often depends on context;
+it could be undefined, unknown, or even `null` depending on what the profile represents.
+
+However, since `null` is used to represent a gap during binary operations, you should not use actual explicit
+`null`s inside your segments. Many operations will silently delete those segments (turning them into gaps) anyway.
+
+Some operations will throw an error if they encounter a gap. In these cases, you can use `profile.assignGaps(...)` to
+fill those gaps in, where `...` is either a value of the type `V`, or another profile of `V`s. For example,
+`myBooleanProfile.assignGaps(false)` will fill in all gaps in the profile with `false`.
+
+## Profile Types
+
+There is no high-level `Profile` type; there are only specialized versions that need to know some information about the
+data. This is because some basic operations like equality checks are impossible to define without knowing if the segments
+represent piecewise-constant values or something more complex.
+
+### Constants
+
+If your profile is piecewise-constant, you can use `Constants`. If you've created your own class to represent the data,
+chances are you'll use `Constants`. This profile type will give you basic functionality such as equality checks, change
+detection, and sampling, top of the higher level operations shared by all timelines.
+
+### Booleans
+
+`Booleans` is a further specialization of `Constants` that gives boolean operations like `.not()`, `.and(Booleans)`, etc.
+It is often produced by comparison operations on other profiles.
+
+Earlier versions of Aerie have conflated the concept of a boolean profile with Windows of Opportunity. In the timeline
+library they are distinctly different concepts, although you can convert between them fairly easily. (See `Windows`).
+
+### Strings
+
+`Strings` is a similar specialization of `Constants`, but doesn't give many extra operations except for
+case-insensitive equality checks. Its main utility is as a parser, so you don't have to interact with `SerializedValue`
+when querying resources from simulation results.
+
+### Numbers
+
+`Numbers` is for piecewise-constant numeric profiles, where `N` is a subclass of `Number`, as in `Integer`, `Long`, etc.
+You can perform basic arithmetical operations, like negation, addition/subtraction, multiplication/division, and exponentiation.
+Unlike `Real` (see below), `Numbers` allows you to keep all precision of the data, as long as it does not vary continuously.
+
+Additionally, you can use different numeric type for different segments if you want. `Numbers` would allow you
+to use an `Integer` in one segment and a `Double` in another, if you wanted.
+
+Unfortunately, as a limitation of the JVM's type system, the result of all binary operations between `Numbers` profiles
+is always `Numbers`. As in, when you add two `Numbers` profiles together, all the resulting segments
+will contain an `Integer`, but they will be upcasted to `Number`. You haven't lost any precision, only type information.
+
+### Real
+
+`Real` allows you to represent piecewise-linear numeric profiles in double precision only. You can think of it as a
+`Numbers` profile that includes a rate-of-change in each segment. Binary operations between `Numbers` and `Real`
+profiles are supported, but unfortunately the `Numbers` profile will first be converted to doubles if necessary, which
+might lose precision.
diff --git a/docs/scheduling-and-constraints/procedural/timelines/basics/sampling-and-caching.mdx b/docs/scheduling-and-constraints/procedural/timelines/basics/sampling-and-caching.mdx
new file mode 100644
index 0000000..d1f373e
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/basics/sampling-and-caching.mdx
@@ -0,0 +1,145 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Sampling and Caching
+
+As a consequence of the lazy evaluation paradigm used by all timelines, each timeline is recomputed from scratch
+every time `.collect()` is called on it. If you are dealing with small amounts of data and inexpensive operations, this
+probably isn't a practical concern. If not, you might need to cache the timeline to improve performance.
+
+This is very simple, just call `.cache(...)` on your timeline. It has exactly the same semantics as `.collect(...)`.
+Timelines are not cached by default because if you're manipulating enough data for performance to be a concern,
+storing a cache for each intermediate timeline might use too much memory.
+
+If you explicitly call `.collect()` many times on the same timeline, it will be easy to spot the performance penalty;
+but sometimes a collect call is hidden inside another operation. Let's take the code below and see how that could happen:
+
+
+
+
+```kotlin
+val n1 = plan.resource("int_resource_1", Numbers.deserialize())
+val n2 = plan.resource("int_resource_2", Numbers.deserialize())
+
+// Construct a very expensive profile that we should only evaluate once.
+val superExpensiveProfile = n1.map2Values(n2) { l, r, _ -> ackermann(l, r) }
+```
+
+
+
+
+```java
+final var n1 = plan.resource("int_resource_1", Numbers.deserialize());
+final var n2 = plan.resource("int_resource_2", Numbers.deserialize());
+
+// Construct a very expensive profile that we should only evaluate once.
+final var expensiveProfile = n1.map2Values(n2, (l, r, _) -> ackermann(l, r));
+```
+
+
+
+
+
+## Sampling
+
+Let's say I wanted to sample the value of `expensiveProfile` at a bunch of different points, rather than using
+the whole profile. You can just call `expensiveProfile.sample(myTime)`, but be careful because `sample` just delegates
+to `.collect(Interval.at(myTime))` and unwraps the segment. If you only wanted to sample once, this is great, because
+it avoids evaluating `expensiveProfile` for most of the plan, only calculating the one segment you need.
+
+But if you call `.sample` many times, especially if you call it on the same segment multiple times, it will recalculate
+many more times than you need. So before you do any sampling, call `cache`:
+
+
+
+
+```kotlin
+// Say we only care about the first day of the plan.
+val interval = Duration.days(0) .. duration.days(1)
+
+// Cache the first day
+expensiveProfile.cache(interval)
+
+// Sample every 10 minutes for the first day
+for (time in interval step Duration.minutes(10)) {
+ val sample = expensiveProfile.sample(time)
+ // do something with the sample
+}
+```
+
+
+
+
+```java
+// Say we only care about the first day of the plan.
+val interval = Interval.between(Duration.days(0), duration.days(1));
+
+// Cache the first day
+expensiveProfile.cache(interval);
+
+// Sample every 10 minutes for the first day
+for (final var time: interval.step(Duration.minutes(10))) {
+ final var sample = expensiveProfile.sample(time);
+ // do something with the sample
+}
+```
+
+
+
+
+## Timeline reuse
+
+What if `expensiveProfile` isn't the final product, and needs to be used in the construction of multiple other timelines?
+In that case, any time those downstream timelines are collected, `expensiveProfile` will be recomputed too. We can avoid
+this by caching it on whatever interval we are going to need it for.
+
+
+
+
+```kotlin
+// Two intermediate profiles that both depend on `expensiveProfile`
+val intermediate1 = expensiveProfile + 1
+val intermediate2 = expensiveProfile * 2
+
+// Final result profile that depends on both intermediate profiles,
+// and thus transitively depends on `expensiveProfile` twice.
+val result = derived1 / derived2
+
+// If we called `result.collect()` here, expensiveProfile would be
+// computed twice.
+
+// Cache the whole profile
+expensiveProfile.cache()
+
+// Now we can collect without wasted computation
+/* do something with */ result.collect()
+```
+
+
+
+
+```java
+// Two intermediate profiles that both depend on `expensiveProfile`
+final var intermediate1 = expensiveProfile.plus(1);
+final var intermediate2 = expensiveProfile.times(2);
+
+// Final result profile that depends on both intermediate profiles,
+// and thus transitively depends on `expensiveProfile` twice.
+final var result = derived1.div(derived2);
+
+// If we called `result.collect()` here, expensiveProfile would be
+// computed twice.
+
+// Cache the whole profile
+expensiveProfile.cache();
+
+// Now we can collect without wasted computation
+/* do something with */ result.collect();
+```
+
+
+
+
+When writing procedural constraints, keep in mind that you return a `Violations` timeline which is then collected
+by the driver *outside* your constraint code. So if your result depends on a timeline multiple times, that timeline will
+be computed multiple times unless it is cached, even though you didn't call `.collect()` yourself.
diff --git a/docs/scheduling-and-constraints/procedural/timelines/basics/windows.mdx b/docs/scheduling-and-constraints/procedural/timelines/basics/windows.mdx
new file mode 100644
index 0000000..870d7bc
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/basics/windows.mdx
@@ -0,0 +1,40 @@
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Windows
+
+`Windows` is a unique type of timeline that contains no values on each `Interval`; it is just a timeline of plain
+intervals.
+
+Originally named after the concept of "Windows of Opportunity", `Windows` essentially represents a subset of the planning
+horizon. Like a profile, the intervals are automatically coalesced if they overlap.
+
+`Windows` are usually created through the `.highlight` method or its derivatives. Its then common to either call `.collect` or
+use it in a for-each loop (which calls `.collect` under the hood). For example, to iterate through the windows where the `my_resource` resource equals
+`5`, you can do this:
+
+
+
+
+```kotlin
+val myResource = plan.resource("my_resource", Real.deserialize())
+for (interval in myResource.highlightEqualTo(3)) {
+ // do something with the interval
+}
+```
+
+
+
+
+```java
+final var myResource = plan.resource("my_resource", Real.deserialize());
+for (final var interval: myResource.highlightEqualTo(3)) {
+ // do something with the interval
+}
+```
+
+
+
+
+You can also use a `Windows` object to filter another timeline, so that it only returns results contained in the windows.
+This and the `highlight` method are explained in the [Common Operations](../common-operations) page.
diff --git a/docs/scheduling-and-constraints/procedural/timelines/introduction.mdx b/docs/scheduling-and-constraints/procedural/timelines/introduction.mdx
new file mode 100644
index 0000000..6ee5a2d
--- /dev/null
+++ b/docs/scheduling-and-constraints/procedural/timelines/introduction.mdx
@@ -0,0 +1,13 @@
+# Timelines
+
+Timelines are a fundamental data structure for representing and manipulating the plan and simulation results, and they are
+used everywhere in all types of procedural peripheral code. They can represent a wide variety of objects, such as
+activity instances and directives, resource profiles, abstract regions of the plan, and more.
+
+:::tip
+
+The most important feature of timelines is that you don't have to use them if you don't want to. At any time in your code,
+you can unwrap a timeline using the `.collect(...)` function, which will return a list of your time-distributed objects
+for you to work with manually.
+
+:::
diff --git a/docusaurus.config.js b/docusaurus.config.js
index 9db44f9..5c00e2e 100644
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -109,7 +109,7 @@ const config = {
},
},
prism: {
- additionalLanguages: ['graphql', 'java'],
+ additionalLanguages: ['graphql', 'java', 'kotlin'],
darkTheme: darkCodeTheme,
theme: lightCodeTheme,
},
diff --git a/sidebars.js b/sidebars.js
index 13f8c05..a9317c6 100644
--- a/sidebars.js
+++ b/sidebars.js
@@ -172,40 +172,116 @@ const sidebars = {
},
{
type: 'category',
- label: 'Constraints',
+ label: 'Scheduling & Constraints',
link: {
- id: 'constraints/introduction',
- type: 'doc',
+ id: 'scheduling-and-constraints/introduction',
+ type: 'doc'
},
items: [
- 'constraints/concepts',
- 'constraints/examples',
+ 'scheduling-and-constraints/management',
{
- label: 'EDSL Docs',
- type: 'link',
- href: 'https://nasa-ammos.github.io/aerie/constraints-edsl-api/',
+ type: 'category',
+ label: 'Procedural',
+ link: {
+ id: 'scheduling-and-constraints/procedural/introduction',
+ type: 'doc'
+ },
+ items: [
+ 'scheduling-and-constraints/procedural/getting-started',
+ {
+ type: 'category',
+ label: 'Timelines',
+ link: {
+ id: 'scheduling-and-constraints/procedural/timelines/introduction',
+ type: 'doc'
+ },
+ items: [
+ {
+ type: 'category',
+ label: "Basics",
+ link: {
+ id: 'scheduling-and-constraints/procedural/timelines/basics/introduction',
+ type: 'doc'
+ },
+ items: [
+ '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/windows',
+ 'scheduling-and-constraints/procedural/timelines/basics/common-operations'
+ ]
+ },
+ {
+ type: 'category',
+ label: "Advanced",
+ link: {
+ id: 'scheduling-and-constraints/procedural/timelines/advanced/introduction',
+ type: 'doc'
+ },
+ items: [
+ 'scheduling-and-constraints/procedural/timelines/advanced/parallel-profiles',
+ 'scheduling-and-constraints/procedural/timelines/advanced/custom-operations',
+ 'scheduling-and-constraints/procedural/timelines/advanced/custom-timelines'
+
+ ]
+ }
+ ]
+ },
+ 'scheduling-and-constraints/procedural/plan-and-sim-results',
+ 'scheduling-and-constraints/procedural/constraints',
+ 'scheduling-and-constraints/procedural/scheduling',
+ 'scheduling-and-constraints/procedural/parameters-and-invocations',
+ 'scheduling-and-constraints/procedural/running-externally'
+ ]
},
- ],
- },
- {
- type: 'category',
- label: 'Scheduling',
- link: {
- id: 'scheduling/introduction',
- type: 'doc',
- },
- items: [
- 'scheduling/goals',
- 'scheduling/run-scheduling',
- 'scheduling/global-conditions',
- 'scheduling/modelling-temporal-relations',
- 'scheduling/temporal-subset',
{
- label: 'EDSL Docs',
- type: 'link',
- href: 'https://nasa-ammos.github.io/aerie/scheduling-edsl-api/modules/Scheduling_eDSL.html',
+ type: 'category',
+ label: 'Declarative',
+ link: {
+ id: 'scheduling-and-constraints/declarative/introduction',
+ type: 'doc'
+ },
+ items: [
+ {
+ type: 'category',
+ label: 'Constraints',
+ link: {
+ id: 'scheduling-and-constraints/declarative/constraints/introduction',
+ type: 'doc',
+ },
+ items: [
+ 'scheduling-and-constraints/declarative/constraints/concepts',
+ 'scheduling-and-constraints/declarative/constraints/examples',
+ {
+ label: 'EDSL Docs',
+ type: 'link',
+ href: 'https://nasa-ammos.github.io/aerie/constraints-edsl-api/',
+ },
+ ],
+ },
+ {
+ type: 'category',
+ label: 'Scheduling',
+ link: {
+ id: 'scheduling-and-constraints/declarative/scheduling/introduction',
+ type: 'doc',
+ },
+ items: [
+ 'scheduling-and-constraints/declarative/scheduling/goals',
+ 'scheduling-and-constraints/declarative/scheduling/global-conditions',
+ 'scheduling-and-constraints/declarative/scheduling/modelling-temporal-relations',
+ 'scheduling-and-constraints/declarative/scheduling/temporal-subset',
+ {
+ label: 'EDSL Docs',
+ type: 'link',
+ href: 'https://nasa-ammos.github.io/aerie/scheduling-edsl-api/modules/Scheduling_eDSL.html',
+ },
+ ],
+ },
+ ]
},
- ],
+ 'scheduling-and-constraints/execution'
+ ]
},
{
type: 'category',
diff --git a/src/components/HomepageCards/index.js b/src/components/HomepageCards/index.js
index bfbe816..917bf77 100644
--- a/src/components/HomepageCards/index.js
+++ b/src/components/HomepageCards/index.js
@@ -41,7 +41,7 @@ const FeatureList = [
),
},
{
- title: 'Low-code scheduling and rule checking',
+ title: 'Flexible scheduling and rule checking',
Img: require('@site/static/img/cards/aerie-feature-rule-checking-light.png').default,
ImgDark: require('@site/static/img/cards/aerie-feature-rule-checking-dark.png').default,
size: 'col--8',