Horizon Mode Testing and Conformance
Purpose
This spec defines the conformance test suite for the HorizonMode enum and its four methods (successors, is_terminal, discount_factor, validate), as specified in Horizon Mode Trait. The suite verifies that both variants (Finite, Cyclic) produce correct stage transitions, terminal conditions, discount factors, and validation errors against hand-computable reference values. All test cases use small policy graphs (3-5 stages for Finite, 12 stages for Cyclic) so that expected outputs can be verified by manual inspection. The forward pass termination tests (SS3) additionally use the 60-stage production-scale example from Infinite Horizon SS3 to verify cyclic termination logic under realistic conditions.
Test cases reference the method contracts from Horizon Mode Trait SS2, the validation rules H1-H4 from Extension Points SS4.3, and the infinite horizon formulation from Infinite Horizon.
SS1. Conformance Test Suite
Test naming convention: test_horizon_{variant}_{method}_{scenario} where {variant} is finite or cyclic, {method} is successors, is_terminal, or discount_factor, and {scenario} describes the test case.
Shared test fixtures: Unless otherwise noted, tests use one of the following two fixtures.
Fixture A – Finite 5-stage chain: Stages 0 through 4 with annual_discount_rate = 0.06 and monthly stage durations (each stage is 1/12 year). Per-transition discount factor: . Transitions: 0->1, 1->2, 2->3, 3->4, each with probability 1.0.
{
"policy_graph": {
"type": "finite_horizon",
"annual_discount_rate": 0.06,
"transitions": [
{ "source_id": 0, "target_id": 1, "probability": 1.0 },
{ "source_id": 1, "target_id": 2, "probability": 1.0 },
{ "source_id": 2, "target_id": 3, "probability": 1.0 },
{ "source_id": 3, "target_id": 4, "probability": 1.0 }
]
}
}
Fixture B – Cyclic 12-stage cycle: Stages 0 through 11 with annual_discount_rate = 0.06 and monthly stage durations (each stage is 1/12 year). Per-transition discount factor: . Linear chain 0->1->...->11 plus back-edge 11->0 (all probability 1.0). Cycle length , cycle start = 0, cumulative cycle discount . Configuration: max_horizon_length = 240, discount_threshold = 1e-6.
{
"policy_graph": {
"type": "cyclic",
"annual_discount_rate": 0.06,
"transitions": [
{ "source_id": 0, "target_id": 1, "probability": 1.0 },
{ "source_id": 1, "target_id": 2, "probability": 1.0 },
{ "source_id": 2, "target_id": 3, "probability": 1.0 },
{ "source_id": 3, "target_id": 4, "probability": 1.0 },
{ "source_id": 4, "target_id": 5, "probability": 1.0 },
{ "source_id": 5, "target_id": 6, "probability": 1.0 },
{ "source_id": 6, "target_id": 7, "probability": 1.0 },
{ "source_id": 7, "target_id": 8, "probability": 1.0 },
{ "source_id": 8, "target_id": 9, "probability": 1.0 },
{ "source_id": 9, "target_id": 10, "probability": 1.0 },
{ "source_id": 10, "target_id": 11, "probability": 1.0 },
{ "source_id": 11, "target_id": 0, "probability": 1.0 }
]
}
}
SS1.1 successors Conformance
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_horizon_finite_successors_mid_chain | Fixture A. Query successors(stage_id=2). | Returns [(target_id=3, probability=1.0, discount_factor=0.99514)]. Exactly one successor; the next stage in the linear chain. | Finite |
test_horizon_finite_successors_terminal | Fixture A. Query successors(stage_id=4). | Returns an empty Vec. Stage 4 is the terminal stage (); no outgoing transitions exist. | Finite |
test_horizon_finite_successors_first_stage | Fixture A. Query successors(stage_id=0). | Returns [(target_id=1, probability=1.0, discount_factor=0.99514)]. The first stage has exactly one successor. | Finite |
test_horizon_cyclic_successors_forward_edge | Fixture B. Query successors(stage_id=5). | Returns [(target_id=6, probability=1.0, discount_factor=0.99514)]. Standard forward transition within the cycle. | Cyclic |
test_horizon_cyclic_successors_back_edge | Fixture B. Query successors(stage_id=11). | Returns [(target_id=0, probability=1.0, discount_factor=0.99514)]. This is the back-edge transition from the last stage of the cycle back to cycle start. The target ID (0) is less than the source ID (11), identifying it as the back-edge. | Cyclic |
test_horizon_finite_successors_single_stage | Single-stage Finite graph: 1 stage (id=0), no transitions, annual_discount_rate = 0. Query successors(stage_id=0). | Returns an empty Vec. A single-stage finite graph has no outgoing transitions; the sole stage is terminal. | Finite |
test_horizon_cyclic_successors_single_stage_cycle | Single-stage Cyclic graph: 1 stage (id=0), transition 0->0 with probability 1.0, annual_discount_rate = 0.06, monthly duration. Cycle length , cycle start = 0, . Query successors(stage_id=0). | Returns [(target_id=0, probability=1.0, discount_factor=0.99514)]. A 1-stage cycle has a self-loop; the stage is its own successor. | Cyclic |
SS1.2 is_terminal Conformance
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_horizon_finite_is_terminal_last_stage | Fixture A. Query is_terminal(stage_id=4). | Returns true. Stage 4 has no successors; it is the terminal stage. Consistent with successors(4) returning an empty Vec. | Finite |
test_horizon_finite_is_terminal_mid_chain | Fixture A. Query is_terminal(stage_id=2). | Returns false. Stage 2 has successor stage 3; it is not terminal. | Finite |
test_horizon_cyclic_is_terminal_any_stage | Fixture B. Query is_terminal(stage_id=11). | Returns false. In cyclic mode, no stage is ever terminal – the back-edge ensures at least one successor exists. | Cyclic |
test_horizon_cyclic_is_terminal_cycle_start | Fixture B. Query is_terminal(stage_id=0). | Returns false. The cycle start stage has successor stage 1; cyclic stages are never terminal. | Cyclic |
test_horizon_finite_is_terminal_single_stage | Single-stage Finite graph: 1 stage (id=0), no transitions. Query is_terminal(stage_id=0). | Returns true. The sole stage in a single-stage finite graph is terminal. | Finite |
SS1.3 discount_factor Conformance
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_horizon_finite_discount_factor_standard | Fixture A. Query discount_factor(source=1, target=2). | Returns 0.99514 (within tolerance 1e-5). Discount factor computed as from the global annual_discount_rate = 0.06 and 1/12-year stage duration. | Finite |
test_horizon_finite_discount_factor_undiscounted | 3-stage Finite graph (stages 0-2) with annual_discount_rate = 0.0. Query discount_factor(source=0, target=1). | Returns 1.0 exactly. When the annual discount rate is zero, for any stage duration. | Finite |
test_horizon_cyclic_discount_factor_forward | Fixture B. Query discount_factor(source=5, target=6). | Returns 0.99514 (within tolerance 1e-5). Same per-transition factor as any other monthly transition in the cycle. | Cyclic |
test_horizon_cyclic_discount_factor_back_edge | Fixture B. Query discount_factor(source=11, target=0). | Returns 0.99514 (within tolerance 1e-5). The back-edge transition carries the same per-transition discount factor as forward edges (all stages have equal monthly duration). The cumulative cycle discount is validated by rule H2, but the per-transition factor is what this method returns. | Cyclic |
test_horizon_finite_discount_factor_consistent_with_successors | Fixture A. Query discount_factor(source=2, target=3) and compare with the discount_factor field in the Successor returned by successors(stage_id=2). | Both return 0.99514. The discount_factor method and the Successor.discount_factor field are consistent for the same transition, per Horizon Mode Trait SS2.3 postconditions. | Finite |
SS2. Validation Tests
These tests verify that HorizonMode::validate correctly enforces rules H1-H4 from Extension Points SS4.3. Each test provides an invalid configuration and asserts that the corresponding ValidationError variant is returned.
Error accumulation: validate collects all violations before returning (Horizon Mode Trait SS2.4). Tests with a single violation verify exactly one error is returned; the multi-violation test (SS2.5) verifies that all applicable errors are reported simultaneously.
| Test Name | Input Scenario | Expected Observable Behavior | Rule |
|---|---|---|---|
test_horizon_validate_h1_empty_stage_set | Empty stages array (0 stages), policy_graph.type = "finite_horizon", no transitions. | Returns Err containing ValidationError::EmptyStageSet. At least one stage must exist for any horizon mode. | H1 |
test_horizon_validate_h2_cycle_discount_one | 12-stage Cyclic graph with annual_discount_rate = 0.0 (i.e., for all transitions). Cycle from stage 11 back to stage 0. Cumulative cycle discount . | Returns Err containing ValidationError::CycleDiscountNotConvergent { cycle_discount: 1.0 }. A cycle discount of exactly 1.0 violates the strict inequality required for convergence. | H2 |
test_horizon_validate_h2_cycle_discount_above_one | 12-stage Cyclic graph where the back-edge transition 11->0 has a per-transition override annual_discount_rate: -0.05 (negative rate produces ). Per-transition factor for back-edge: . Cumulative: . This particular configuration still converges. Instead, use annual_discount_rate: -0.10 for all transitions: . Cumulative: . | Returns Err containing ValidationError::CycleDiscountNotConvergent { cycle_discount: 1.1111 } (approximate). The cumulative cycle discount exceeds 1.0, so the value function cannot converge. | H2 |
test_horizon_validate_h3_cycle_start_out_of_bounds | 5-stage Cyclic graph (stages 0-4) with back-edge 4->10 (target stage 10 does not exist in the 5-stage set). The inferred cycle start is stage 10, which is out of bounds. | Returns Err containing ValidationError::CycleStartOutOfBounds { cycle_start: 10, max_stage_id: 4 }. The back-edge target must be a valid stage ID within the stage set. | H3 |
test_horizon_validate_h4_dangling_transition | 3-stage Finite graph (stages 0-2) with transitions 0->1 and 1->5. Stage 5 does not exist. | Returns Err containing ValidationError::DanglingTransition { source_id: 1, target_id: 5 }. Every transition target must exist in the stage set. | H4 |
test_horizon_validate_h1_h4_multiple_errors | Empty stages array (0 stages), policy_graph.type = "finite_horizon", with one transition 0->1. Both H1 (empty stages) and H4 (stage 0 and stage 1 do not exist) are violated. | Returns Err containing at least ValidationError::EmptyStageSet and ValidationError::DanglingTransition { source_id: 0, target_id: 1 }. All violations are reported, not just the first. | H1, H4 |
test_horizon_validate_finite_passes | Fixture A (5-stage finite chain, valid configuration). | Returns Ok(HorizonMode::Finite { ... }) with a TransitionMap containing 4 entries (stages 0-3 each mapping to their successor). Stage 4 has no entry (terminal). | – |
test_horizon_validate_cyclic_passes | Fixture B (12-stage cyclic graph, valid configuration). | Returns Ok(HorizonMode::Cyclic { transitions: ..., cycle_length: 12, cycle_start: 0, cycle_discount: 0.94340, max_horizon_length: 240, discount_threshold: 1e-6 }). The cycle_discount is (within tolerance 1e-4). | – |
SS3. Forward Pass Termination Tests
These tests verify the should_terminate_forward method (Horizon Mode Trait SS5), which governs when the cyclic forward pass stops traversing stages. The method is only meaningful for the Cyclic variant; for Finite, it always returns false (finite horizon uses is_terminal instead).
Termination conditions (Infinite Horizon SS6):
- Cumulative discount below threshold: (the
discount_thresholdparameter) - Maximum horizon length reached: (the
max_horizon_lengthparameter)
SS3.1 Discount Threshold Termination
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_horizon_cyclic_terminate_discount_threshold | Fixture B (discount_threshold = 1e-6, max_horizon_length = 240). Simulate forward pass tracking cumulative discount. After full 12-stage cycles, cumulative discount is . After 1 cycle (): . After 10 cycles (): . After 100 cycles (): . The threshold is crossed when , i.e., , so cycles = 2844 stages. However, max_horizon_length = 240 triggers first (at stage 241). | should_terminate_forward(stages_traversed=240, cumulative_discount=...) returns false. should_terminate_forward(stages_traversed=241, cumulative_discount=...) returns true. With these parameters the max horizon length triggers before the discount threshold. | Cyclic |
test_horizon_cyclic_terminate_discount_threshold_tight | Cyclic 12-stage graph (same as Fixture B structure) with discount_threshold = 0.5, max_horizon_length = 10000. Cumulative discount after cycles: . Threshold 0.5 is crossed when , i.e., . So at cycles ( stages): . | should_terminate_forward(stages_traversed=144, cumulative_discount=0.49578) returns true (discount below threshold). should_terminate_forward(stages_traversed=132, cumulative_discount=0.52525) returns false (discount still above threshold, cycles: ). The discount threshold triggers before max_horizon_length. | Cyclic |
SS3.2 Maximum Horizon Length Termination
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_horizon_cyclic_terminate_max_horizon | Fixture B (max_horizon_length = 240). Forward pass has traversed 240 stages with cumulative discount (still well above discount_threshold = 1e-6). | should_terminate_forward(stages_traversed=240, cumulative_discount=0.31157) returns false. should_terminate_forward(stages_traversed=241, cumulative_discount=0.31003) returns true. The safety bound triggers at . The cumulative discount has not yet reached the threshold, but the horizon length limit terminates the pass. | Cyclic |
test_horizon_finite_terminate_always_false | Fixture A (Finite). Query should_terminate_forward(stages_traversed=100, cumulative_discount=0.001). | Returns false. The Finite variant never terminates via should_terminate_forward; it relies on is_terminal instead. Even with extreme parameter values, the method returns false for Finite. | Finite |
SS3.3 Production-Scale Example (60-Stage Cyclic Graph)
This test uses the 60-stage example from Infinite Horizon SS3: stages 0-59 with a linear chain 0->1->...->59 and back-edge 59->48, creating a 12-stage cycle (stages 48-59). The first 48 stages (0-47) are a non-cycling prefix. Annual discount rate: 6%, monthly durations. Per-transition discount factor: .
Cycle parameters: , cycle start = 48, .
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_horizon_cyclic_60stage_successors_back_edge | 60-stage Cyclic graph per Infinite Horizon SS3. Query successors(stage_id=59). | Returns [(target_id=48, probability=1.0, discount_factor=0.99514)]. Stage 59 transitions back to stage 48 (the back-edge), not to stage 0. This is the defining transition of the 60-stage cyclic example. | Cyclic |
test_horizon_cyclic_60stage_successors_prefix | Same 60-stage graph. Query successors(stage_id=30). | Returns [(target_id=31, probability=1.0, discount_factor=0.99514)]. Stage 30 is in the non-cycling prefix (stages 0-47); it has a standard forward transition. | Cyclic |
test_horizon_cyclic_60stage_is_terminal | Same 60-stage graph. Query is_terminal(stage_id=59). | Returns false. Even the last stage (59) is not terminal because the back-edge provides a successor. No stage in a cyclic graph is terminal. | Cyclic |
test_horizon_cyclic_60stage_forward_termination | Same 60-stage graph with max_horizon_length = 240, discount_threshold = 1e-6. Forward pass enters the cycle at stage 48 after traversing the 48-stage prefix. After the prefix, cumulative discount is . Each subsequent cycle multiplies by . After prefix + 16 cycles ( stages total): cumulative . | should_terminate_forward(stages_traversed=240, cumulative_discount=0.31463) returns false. should_terminate_forward(stages_traversed=241, cumulative_discount=...) returns true. The max_horizon_length = 240 safety bound triggers after 240 stages, while the discount threshold () has not been reached. | Cyclic |
Cross-References
- Horizon Mode Trait – Enum definition (SS1), method contracts for
successors(SS2.1),is_terminal(SS2.2),discount_factor(SS2.3),validate(SS2.4), forward pass termination (SS5), season mapping (SS3.3) - Infinite Horizon – Periodic structure (SS2), cyclic policy graph with 60-stage example (SS3), discount requirement (SS4), cut sharing (SS5), forward pass termination conditions (SS6)
- Discount Rate – Annual-rate-to-factor conversion formula (SS3), discount on theta (SS4), cumulative discounting (SS5), infinite horizon considerations (SS9)
- Extension Points – Horizon mode variant table (SS4.1), configuration examples (SS4.2), validation rules H1-H4 (SS4.3), behavioral contract (SS4.4)
- Risk Measure Testing – Sibling conformance test spec following the same table format pattern
- Backend Testing – Conformance test suite structure and requirements table format (reference pattern for this spec)