Constraints
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
- Java
@ConstraintProcedure
class BatteryAboveZero: Constraint {
override fun run(plan: Plan, simResults: SimulationResults) = Violations.inside(
plan.resource("/battery_soc", Real.deserializer()).lessThan(0).highlightTrue()
)
}
@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.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.
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:
- Kotlin
- Java
@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))
}
}
}
@ConstraintProcedure
public class MyConstraint extends GeneratorConstraint {
@Override
public void generate(@NotNull Plan plan, @NotNull SimulationResults simResults) {
final var myResource = simResults.resource("/my/resource", Real.deserializer()).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.
@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:
- Kotlin
- Java
@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))
}
}
}
@ConstraintProcedure
public class MyConstraint extends GeneratorConstraint {
@Override
public String defaultMessage() {
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.deserializer()).cache();
for (final var activity: 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.