Relation Aggregates: Stabilize Preferences Using Staged Variables

To improve solver stability and prevent backtracking, stage relation aggregates through local variables and state anchors instead of referencing them directly in preferences.

To improve solver stability and prevent backtracking, avoid referencing relation aggregates directly within preferences when the preferences also influence the same relation. Instead, stage these aggregates through local variables and state anchors.

In this example, the preferences and the calculated quantity depend directly on live relation aggregates.

1relation ml : ModelTraining {
2   totalNumSubscriptions = total(NumSubscriptions);
3   subCount = count(Sublicense == true);
4}
5preference(ml[ModelTraining] > 0 && ml.subCount == 0 -> ml[ModelTraining] == parentQty);
6preference(ml[ModelTraining] > 0 && ml.subCount > 0 -> ml[ModelTraining] == calculatedQuantity);
7constraint(calculatedQuantity == ((ml.totalNumSubscriptions + 99) / 100) * 100);

When the solver evaluates preferences that depend directly on a relation’s own aggregates, it creates a tight feedback loop:

Relation Quantity ⟶ Relation Aggregates ⟶ Preference Guards/Targets ⟶ Relation Quantity (loop repeats)

The feedback loop can cause the solver to oscillate or backtrack repeatedly before finding a stable solution.

This example shows the stabilized pattern. It introduces staged variables that act as anchors to separate the relation aggregates from the preference logic.

1relation ml : ModelTraining {
2    totalNumSubscriptions = total(NumSubscriptions);
3    subCount = count(Sublicense == true);
4}
5
6// Stage 1: Convert relation conditions into stable booleans
7boolean hasSub;
8constraint(hasSub <-> (ml.subCount > 0));
9
10// Stage 2: Copy relation aggregates into local variables
11int localTotalNumSubscriptions;
12constraint(localTotalNumSubscriptions == ml.totalNumSubscriptions);
13
14// Stage 3: Perform calculations by using the local variables
15int calculatedQuantity;
16constraint(calculatedQuantity == ((localTotalNumSubscriptions + 99) / 100) * 100);
17
18// Stage 4: Use the staged variables in the preferences
19preference(ml[ModelTraining] > 0 && hasSub == false -> ml[ModelTraining] == parentQty);
20preference(ml[ModelTraining] > 0 && hasSub == true -> ml[ModelTraining] == calculatedQuantity);

With this pattern, instead of solving everything simultaneously, the solver logic processes a pipeline:

Relation ⟶ Aggregates ⟶ Anchored State/Local Variables ⟶ Derived Calculations ⟶ Preferences

This process reduces oscillation and makes the solving path more deterministic.