Get Started with Revenue Management Developer Resources
Revenue Management Settings
Modeling a Generator Set
Core Concept Examples
Business-Centric CML Examples
Model Structure
Previous Versions
These Constraint Modeling Language (CML) structures implement quantity aggregation and resolve calculation dependencies.
This CML example shows how to model bundle rollups by separating aggregation from validation, ensuring correct calculation order, solver stability, and business-rule consistency
1type LineItem;
2type GeneratorSet : LineItem {
3 int totalItemPowerKW = modelclassification.totalPowerKW;
4 // Aggregate `powerKW`
5 relation modelclassification : ModelClassification[0..10] {
6 totalPowerKW = sum(powerKW);
7 }
8 message(true, "GeneratorSet totalItemPowerKW = {} kW (calculated roll-up). Sum of selected ModelClassification powerKW values = {} kW.", totalItemPowerKW, modelclassification.totalPowerKW);
9}
10type ModelClassification : LineItem {
11 int powerKW = [900, 1750, 2500];
12}
13type GeneralModel1750 : ModelClassification {
14 int powerKW = 1750;
15}
16type GeneralModel2500 : ModelClassification {
17 int powerKW = 2500;
18}
19type GeneralModel900 : ModelClassification {
20 int powerKW = 900;
21}In example 1, the GeneratorSet type contains the totalItemPowerKW variable that is assigned with the totalPowerKW. The model specifies a relationship based on the product classification (modelClassification). This relationship includes aggregation using the sum() function that calculates the totalPowerKW from the products selected in the modelClassification during bundle configuration. The model contains a message for displaying the totalItemPowerKW with the modelClassification.totalPowerKW on the Product Configuration.
1message(true, "GeneratorSet totalItemPowerKW = {} kW (calculated roll-up). Sum of selected ModelClassification powerKW values = {} kW.", totalItemPowerKW, modelclassification.totalPowerKW);As a result, the engine calculates the totalPowerKW of user selected products in the modelClassification and assigns this value to the totalItemPowerKW. The system displays both values in the information message on the Product Configuration.
This pattern breaks unsolvable loops by enforcing unidirectional data flow (Parent dictates Child quantity based on Child aggregates).
1type LineItem;
2type GeneratorSet : LineItem {
3 // RELATION: THE "READ" PHASE
4 // Data flows UP from children to the parent via the port attribute
5 relation voltageclassification : VoltageClassification[1..10] {
6 sumOfChildPower = total(power);
7 }
8 // Explicit domain ensures the solver initializes correctly for calculations
9 decimal(2) totalDistributionValue = [0.00..1000.00];
10 // CONSTRAINT 1: THE "CALCULATION" PHASE
11 // @(sequence = 1) ensures the engine reads child data and calculates first
12 @(sequence = 1)
13 constraint calcTotalDistribution(totalDistributionValue == voltageclassification.sumOfChildPower / 100);
14 // CONSTRAINT 2: THE "WRITE/ENFORCE" PHASE
15 // @(sequence = 2) ensures this happens last to push values DOWN to children
16 @(sequence = 2)
17 constraint setChildQuantity(voltageclassification[VoltageClassification] == totalDistributionValue * 2);
18}
19type VoltageClassification : LineItem {
20 int power = [1..100];
21}
22type VoltageConnection_220_380 : VoltageClassification;
23type VoltageConnection_240_416 : VoltageClassification;
24type VoltageConnection_255_440 : VoltageClassification;In example 2, the relationship includes aggregation using the total() function that calculates the sumOfChildPower from the selected products in the voltageclassification. The GeneratorSet type contains the totalDistributionValue with the explicit domain ([0.00..1000.00]). The model contains two constraints with corresponding @sequence annotation to enforce the logical order for calculation of the calcTotalDistribution (using child products data) and defining the quantity for the voltageclassification child items.
This advanced pattern uses the @(groupBy=attribute) annotation to create virtual components (RegionGroup) for aggregation, referencing the source data relation in the parent type (SubscriptionOrder.licenses) using the sourceContextNode annotation.
1// 1. Base Product Type (e.g., Regional License)
2type RegionalLicense {
3 int regionId = [0..100]; // Attribute for grouping
4 int userCount = [0..100];
5}
6// 2. Virtual Group Type (Performs grouping and user sum)
7@(split=true, virtual=true, groupBy=regionId)
8type RegionGroup {
9 int regionId;
10 int groupTotalUsers; // Sum of userCount for this region
11 // Relation to licenses matching this region group
12 @(sourceContextNode="SubscriptionOrder.licenses")
13 relation licenses: RegionalLicense[0..100];
14 // Constraint: Calculate the aggregate user count within this group
15 constraint(groupTotalUsers == licenses.sum(userCount));
16}
17// 3. Parent Order Management (Aggregates all region totals)
18@(virtual=true)
19type SubscriptionOrder {
20 // Overall total users is calculated by referencing the group totals
21 int totalUsers = regionGroups.groupTotalUsers;
22 relation licenses: RegionalLicense[0..500];
23 relation regionGroups: RegionGroup[1..10];
24}In example 3, the licenses relationship that is brought as an external source by @(sourceContextNode="SubscriptionOrder.licenses"), includes an aggregation using the sum() function that calculates the groupTotalUsers from the selected products in the RegionalLicense. The RegionGroup type contains the groupTotalUsers variable that serves the aggregation. The SubscriptionOrder type contains the aggregate metric totalUsers which is calculated by summing all groupTotalUsers. The model contains one core constraint on RegionGroup to enforce that groupTotalUsers equals the sum of all userCount from its member licenses.