Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 NameInput ScenarioExpected Observable BehaviorVariant
test_horizon_finite_successors_mid_chainFixture 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_terminalFixture 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_stageFixture 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_edgeFixture 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_edgeFixture 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_stageSingle-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_cycleSingle-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 NameInput ScenarioExpected Observable BehaviorVariant
test_horizon_finite_is_terminal_last_stageFixture 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_chainFixture 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_stageFixture 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_startFixture 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_stageSingle-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 NameInput ScenarioExpected Observable BehaviorVariant
test_horizon_finite_discount_factor_standardFixture 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_undiscounted3-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_forwardFixture 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_edgeFixture 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_successorsFixture 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 NameInput ScenarioExpected Observable BehaviorRule
test_horizon_validate_h1_empty_stage_setEmpty 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_one12-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_one12-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_bounds5-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_transition3-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_errorsEmpty 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_passesFixture 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_passesFixture 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):

  1. Cumulative discount below threshold: (the discount_threshold parameter)
  2. Maximum horizon length reached: (the max_horizon_length parameter)

SS3.1 Discount Threshold Termination

Test NameInput ScenarioExpected Observable BehaviorVariant
test_horizon_cyclic_terminate_discount_thresholdFixture 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_tightCyclic 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 NameInput ScenarioExpected Observable BehaviorVariant
test_horizon_cyclic_terminate_max_horizonFixture 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_falseFixture 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 NameInput ScenarioExpected Observable BehaviorVariant
test_horizon_cyclic_60stage_successors_back_edge60-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_prefixSame 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_terminalSame 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_terminationSame 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)