Skip to content

Common Design Patterns in Rave

Writing efficient Rave code requires balancing business logic accuracy with solver performance. Because Rave is declarative, how you structure your logic directly influences how the underlying C/C++ engine evaluates the search space.

This document outlines standard design patterns and performance pitfalls for Rave developers.


1. The "Condition-Guard" Pattern

Not all rules apply to every trip or duty. When writing rules that target specific scenarios (e.g., deadhead flights or international sectors), use the valid keyword to establish a precondition.

Anti-Pattern: Embedding the condition inside the rule logic, forcing the rule to evaluate True for non-applicable cases.

rule min_rest_after_international =
  if any(leg(duty), leg.%is_international%) then
    %rest_time% >= 14:00
  else
    True;
end

Best Practice: Use the valid clause. If the valid expression evaluates to False, the generator safely skips the rule entirely.

rule min_rest_after_international =
  valid any(leg(duty), leg.%is_international%);
  %rest_time% >= 14:00;
end

2. The "Monotonic Limit" Pattern

When limiting accumulative properties (like maximum duty time, maximum sectors, or total flight time), you want the solver to immediately prune the search tree as soon as the limit is breached.

Best Practice: Keep the comparison strict and against a constant or DepOnlyConst bound so the compiler flags it as a Final Rule.

// The compiler recognizes this as a Non-increasing in Legality (NiL) Final Rule
rule max_flight_time_per_duty =
  sum(leg(duty), leg.%flight_time%) <= %legal_max_time%;
end

Anti-Pattern: Wrapping a monotonic sum limit inside a valid is_closed; block. This destroys the compiler's ability to prune the tree, as the rule will only be checked upon trip completion, turning an efficient Final Rule into an inefficient Illegal Subchain Rule.

3. Traverser Scope Minimization

Traversers (sum, count, all, any) aggregate data across child elements. Executing wide traversers continuously during generation is computationally expensive.

Anti-Pattern: Traversing the entire trip to find information about the current duty.

%has_night_flight% = any(leg(trip), leg.%is_night_flight%);

Best Practice: Restrict the traversal scope to the smallest required level instance. If you only need to evaluate the current working day, traverse the Duty level.

%has_night_flight% = any(leg(duty), leg.%is_night_flight%);

4. The "Cost Penalty" Pattern

When designing objective functions, separate the base financial costs from "soft" quality-of-life penalties. This makes debugging solver behavior much easier and allows business analysts to tune weights independently.

graph LR
    A[Hard Financial Cost<br>Hourly Rate * Block Time] --> C(Total Cost Property)
    B[Soft Penalties<br>Base Value * Penalty Weight] --> C

    style A fill:#48bb78,color:#fff
    style B fill:#ed8936,color:#fff

Implementation:

// 1. Calculate actual union-negotiated pay
%base_pay% = sum(leg(duty), leg.%block_time%) * %hourly_rate%;

// 2. Apply a weighted penalty for undesirable schedules (e.g., 4-sector days)
%fatigue_penalty% = 
  if count(leg(duty)) >= 4 then
    500.0 * %fatigue_weight_param%
  else
    0.0;

// 3. Aggregate cleanly
PROPERTY cost OF Duty
  RULE:
    %base_pay% + %fatigue_penalty%;
ENDPROPERTY