Skip to main content

Constraints

danger

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:

@ConstraintProcedure
class BatteryAboveZero: Constraint {
override fun run(plan: Plan, simResults: SimulationResults) = Violations.inside(
plan.resource("/battery_soc", Real.deserializer()).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, 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:

@ConstraintProcedure
class MyConstraint: GeneratorConstraint() {
override fun generate(plan: Plan, simResults: SimulationResults) {
val myResource = simResults.resource("/my/resource", Real.deserializer()).cache()
for (activity in 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.

@ConstraintProcedure
class BatteryAboveZero: GeneratorConstraint() {
override fun generate(plan: Plan, simResults: SimulationResults) {
simResults.resource("/battery_soc", Real.deserializer())
.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 a few ways to change it.

If you're creating a Violations timeline, you can call violations.withDefaultMessage(message), which sets the message for those in the result that don't already have one.

If you're writing a generator constraint, you can pass it as the last argument to any violate call, as in violate(listOfViolations, messageForThisBatch). Or you can set a constraint-wide default by overriding the defaultMessage method:

@ConstraintProcedure
class MyConstraint: GeneratorConstraint() {
override fun defaultMessage() = "MyActivity cannot start when /my/resource < 0"

override fun generate(plan: Plan, simResults: SimulationResults) {
val myResource = simResults.resource("/my/resource", Real.deserializer()).cache()
for (activity in plan.directives("MyActivity")) {
if (myResource.sample(activity.startTime) < 0)
violate(Violation(activity.interval))
}
}
}

The order of precedence is: individual violation messages > batch violation messages > default message.