Risk Measure Testing and Conformance
Purpose
This spec defines the conformance test suite for the RiskMeasure enum and its two methods (aggregate_cut, evaluate_risk), as specified in Risk Measure Trait. The suite verifies that both variants (Expectation, CVaR) produce correct risk-adjusted weights and aggregation results against hand-computable reference values. All test cases use small input sets (3-5 openings) so that expected outputs can be verified by manual calculation using the sorting-based greedy allocation procedure from Risk Measures SS7.
Test cases reference the method contracts from Risk Measure Trait SS2, the validation rules R1-R3 from Extension Points SS2.3, and the mathematical definitions from Risk Measures.
SS1. Conformance Test Suite
Test naming convention: test_risk_{variant}_{method}_{scenario} where {variant} is expectation or cvar, {method} is aggregate_cut or evaluate_risk, and {scenario} describes the test case.
Shared test fixture: Unless otherwise noted, tests use the following 3-opening fixture with uniform probabilities and 2 state variables:
| Opening | Probability | Objective Value | Intercept | Coefficients |
|---|---|---|---|---|
| 0 | 1/3 | 100 | 10.0 | [1.0, 2.0] |
| 1 | 1/3 | 200 | 20.0 | [3.0, 4.0] |
| 2 | 1/3 | 300 | 30.0 | [5.0, 6.0] |
SS1.1 aggregate_cut Conformance
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_risk_expectation_aggregate_cut_uniform | Shared fixture (3 openings, p=[1/3, 1/3, 1/3]) | Aggregated intercept = 20.0 (= (10+20+30)/3). Aggregated coefficients = [3.0, 4.0] (= ([1+3+5]/3, [2+4+6]/3)). Weights are the raw probabilities: [1/3, 1/3, 1/3]. | Expectation |
test_risk_cvar_aggregate_cut_convex_combination | Shared fixture with lambda=0.5, alpha=0.5. Per-scenario weight upper bounds: mu_bar = (1-0.5)(1/3) + 0.5(1/3)/0.5 = 1/2 for each opening. Total capacity = 3/2. Greedy allocation (descending cost order: 300, 200, 100): opening 2 gets 1/2, opening 1 gets 1/2, opening 0 gets 0. | Risk-adjusted weights mu* = [0, 1/2, 1/2] (indexed by opening). Aggregated intercept = 0(10) + (1/2)(20) + (1/2)(30) = 25.0. Aggregated coefficients = [0(1) + (1/2)(3) + (1/2)(5), 0(2) + (1/2)(4) + (1/2)(6)] = [4.0, 5.0]. | CVaR |
test_risk_cvar_aggregate_cut_pure_cvar | Shared fixture with lambda=1.0, alpha=0.5. Per-scenario upper bounds: mu_bar = 0 + (1/3)/0.5 = 2/3 each. Total capacity = 2. Greedy allocation: opening 2 gets 2/3, opening 1 gets 1/3, opening 0 gets 0. | Risk-adjusted weights mu* = [0, 1/3, 2/3]. Aggregated intercept = 0(10) + (1/3)(20) + (2/3)(30) = 80/3 (approx 26.667). Aggregated coefficients = [(1/3)(3) + (2/3)(5), (1/3)(4) + (2/3)(6)] = [13/3, 16/3] (approx [4.333, 5.333]). | CVaR |
test_risk_cvar_aggregate_cut_alpha_one | Shared fixture with lambda=0.5, alpha=1.0. Per-scenario upper bounds: mu_bar = (1-0.5)(1/3) + 0.5(1/3)/1.0 = 1/6 + 1/6 = 1/3 each. Total capacity = 1, so no excess. Greedy allocation assigns exactly mu_bar = 1/3 to each opening. | Weights mu* = [1/3, 1/3, 1/3], identical to Expectation. Aggregated intercept = 20.0. Aggregated coefficients = [3.0, 4.0]. Result must be identical to the Expectation variant. | CVaR |
test_risk_cvar_aggregate_cut_extreme_alpha | Shared fixture with lambda=0.5, alpha=0.05. Per-scenario upper bounds: mu_bar = (0.5)(1/3) + (0.5)(1/3)/0.05 = 1/6 + 10/3 = 21/6 = 7/2 = 3.5 each. Total capacity = 3 × 7/2 = 21/2 = 10.5. Greedy allocation: opening 2 gets min(61/18, 1.0) = 1.0. Remaining = 0. Openings 1 and 0 get 0. | Weights mu* = [0, 0, 1.0]. Aggregated intercept = 30.0. Aggregated coefficients = [5.0, 6.0]. Only the worst-cost opening contributes. | CVaR |
test_risk_expectation_aggregate_cut_single_opening | 1 opening: p=[1.0], objective=500, intercept=42.0, coefficients=[7.0, 8.0, 9.0]. | Aggregated intercept = 42.0. Aggregated coefficients = [7.0, 8.0, 9.0]. Trivial: single opening always returns the input unchanged. | Expectation |
test_risk_cvar_aggregate_cut_single_opening | 1 opening: p=[1.0], objective=500, intercept=42.0, coefficients=[7.0, 8.0, 9.0]. lambda=0.5, alpha=0.5. mu_bar = (0.5)(1.0) + (0.5)(1.0)/0.5 = 0.5 + 1.0 = 1.5. Greedy allocation: the sole opening gets min(1.5, 1.0) = 1.0. | Weight mu* = [1.0]. Aggregated intercept = 42.0. Aggregated coefficients = [7.0, 8.0, 9.0]. Single-opening CVaR is identical to Expectation. | CVaR |
test_risk_cvar_aggregate_cut_nonuniform_probabilities | 3 openings with p=[0.5, 0.3, 0.2], objectives=[100, 300, 200], intercepts=[10, 30, 20], coefficients=[[1.0], [3.0], [2.0]]. lambda=0.5, alpha=0.5. Per-scenario upper bounds: mu_bar_0 = 0.5(0.5)+0.5(0.5)/0.5 = 0.25+0.5 = 0.75, mu_bar_1 = 0.5(0.3)+0.5(0.3)/0.5 = 0.15+0.3 = 0.45, mu_bar_2 = 0.5(0.2)+0.5(0.2)/0.5 = 0.1+0.2 = 0.3. Total capacity = 1.5. Sort by cost desc: opening 1 (cost=300), opening 2 (cost=200), opening 0 (cost=100). Allocation: opening 1 gets 0.45, opening 2 gets 0.3, opening 0 gets 1-0.45-0.3 = 0.25. | Weights mu* = [0.25, 0.45, 0.3]. Aggregated intercept = 0.25(10) + 0.45(30) + 0.3(20) = 2.5 + 13.5 + 6.0 = 22.0. Aggregated coefficients = [0.25(1.0) + 0.45(3.0) + 0.3(2.0)] = [0.25 + 1.35 + 0.6] = [2.2]. | CVaR |
test_risk_cvar_aggregate_cut_tied_costs | 3 openings with p=[1/3, 1/3, 1/3], objectives=[200, 200, 100], intercepts=[10, 20, 30], coefficients=[[1.0], [3.0], [5.0]]. lambda=1.0, alpha=0.5. mu_bar = (1/3)/0.5 = 2/3 each. Total capacity = 2. Greedy desc: openings 0 and 1 (cost=200, tied), then opening 2 (cost=100). Allocation: first tied opening gets 2/3, second tied opening gets 1/3 (1 - 2/3), opening 2 gets 0. | Weights for the two cost-200 openings sum to 1.0. The allocation among tied openings is implementation-defined (any split that totals 1.0 is correct). The cost-100 opening receives weight 0. Aggregated intercept is between 10.0 and 20.0 depending on tie-breaking. | CVaR |
SS1.2 evaluate_risk Conformance
| Test Name | Input Scenario | Expected Observable Behavior | Variant |
|---|---|---|---|
test_risk_expectation_evaluate_risk_uniform | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3]. | Result = (1/3)(100) + (1/3)(200) + (1/3)(300) = 200.0. Probability-weighted mean. | Expectation |
test_risk_cvar_evaluate_risk_convex_combination | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3], lambda=0.5, alpha=0.5. E[Z] = 200. CVaR_0.5: dual weights with upper bounds p/alpha = 2/3 each, greedy: cost=300 gets 2/3, cost=200 gets 1/3, cost=100 gets 0. CVaR_0.5 = (2/3)(300) + (1/3)(200) = 800/3 (approx 266.667). | Result = (1-0.5)(200) + 0.5(800/3) = 100 + 400/3 = 700/3 (approx 233.333). | CVaR |
test_risk_cvar_evaluate_risk_pure_cvar | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3], lambda=1.0, alpha=0.5. CVaR_0.5 = 800/3 (same computation as above). | Result = (1-1.0)(200) + 1.0(800/3) = 800/3 (approx 266.667). Pure CVaR, no expectation component. | CVaR |
test_risk_cvar_evaluate_risk_alpha_one | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3], lambda=0.5, alpha=1.0. CVaR_1.0 = E[Z] = 200 (alpha=1 means CVaR equals expectation). | Result = (1-0.5)(200) + 0.5(200) = 200.0. Identical to Expectation. | CVaR |
test_risk_cvar_evaluate_risk_extreme_alpha | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3], lambda=0.5, alpha=0.05. CVaR_0.05: upper bounds = (1/3)/0.05 = 20/3 each. Greedy: cost=300 gets min(20/3, 1.0) = 1.0, remaining = 0. CVaR_0.05 = 300. | Result = 0.5(200) + 0.5(300) = 250.0. The CVaR component equals the single worst-cost scenario. | CVaR |
test_risk_cvar_evaluate_risk_nonuniform_probabilities | Costs=[100, 300, 200], p=[0.5, 0.3, 0.2], lambda=0.5, alpha=0.5. E[Z] = 0.5(100) + 0.3(300) + 0.2(200) = 50 + 90 + 40 = 180. CVaR_0.5 dual: upper bounds p/alpha = [1.0, 0.6, 0.4], total = 2.0. Greedy desc (cost=300 opening 1, cost=200 opening 2, cost=100 opening 0): opening 1 gets 0.6, opening 2 gets 0.4, remaining = 0, opening 0 gets 0. CVaR_0.5 = 0.6(300) + 0.4(200) = 260. | Result = 0.5(180) + 0.5(260) = 90 + 130 = 220.0. | CVaR |
test_risk_expectation_evaluate_risk_single_opening | Costs=[500], p=[1.0]. | Result = 500.0. Trivially the single cost value. | Expectation |
test_risk_cvar_evaluate_risk_single_opening | Costs=[500], p=[1.0], lambda=0.5, alpha=0.5. | Result = 0.5(500) + 0.5(500) = 500.0. Single opening: CVaR equals the single cost. | CVaR |
SS2. Variant Equivalence Tests
These tests verify that risk-neutral parameter settings produce results identical to the Expectation variant. This ensures that the CVaR code path degenerates correctly to the baseline when risk aversion is absent.
| Test Name | Input Scenario | Expected Observable Behavior | Rationale |
|---|---|---|---|
test_risk_cvar_aggregate_cut_lambda_zero_equivalence | Shared fixture with lambda=0, alpha=0.5. Per validation rule R3 (Extension Points SS2.3), lambda=0 is normalized to the Expectation variant at config load time. If the CVaR code path is reached with lambda=0, upper bounds reduce to mu_bar = p, so weights = p. | Aggregated intercept = 20.0. Aggregated coefficients = [3.0, 4.0]. Identical to test_risk_expectation_aggregate_cut_uniform. | R3 normalization: lambda=0 CVaR produces identical output to Expectation. |
test_risk_cvar_evaluate_risk_lambda_zero_equivalence | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3], lambda=0, alpha=0.5. The convex combination becomes (1-0)E[Z] + 0(CVaR) = E[Z]. | Result = 200.0. Identical to test_risk_expectation_evaluate_risk_uniform. | R3 normalization: lambda=0 CVaR produces identical output to Expectation. |
test_risk_cvar_aggregate_cut_alpha_one_equivalence | Shared fixture with lambda=0.5, alpha=1.0. When alpha=1, CVaR_1 = E[Z], so the convex combination is (1-lambda)E + lambda E = E. Upper bounds mu_bar = (1-lambda)p + lambda p/1 = p. | Aggregated intercept = 20.0. Aggregated coefficients = [3.0, 4.0]. Identical to Expectation despite lambda=0.5. | alpha=1 makes CVaR equal to expectation, so any lambda produces the same result. |
test_risk_cvar_evaluate_risk_alpha_one_equivalence | Costs=[100, 200, 300], p=[1/3, 1/3, 1/3], lambda=0.5, alpha=1.0. | Result = 200.0. Identical to Expectation. | alpha=1 makes CVaR component equal to E[Z]. |
SS3. Numerical Properties
These tests verify mathematical invariants that must hold for any valid input, independent of specific numeric values. Each test should be run across multiple input configurations (the shared fixture and the nonuniform-probability configurations from SS1).
SS3.1 Weight Invariants
| Test Name | Property | Expected Observable Behavior | Reference |
|---|---|---|---|
test_risk_cvar_aggregate_cut_weights_sum_to_one | Risk-adjusted weights form a valid probability distribution: . | For any valid input, the sum of risk-adjusted weights must equal 1.0 within floating-point tolerance (1e-12). Verified with: shared fixture (lambda=0.5, alpha=0.5, weights=[0, 1/2, 1/2], sum=1.0), pure CVaR (lambda=1.0, alpha=0.5, weights=[0, 1/3, 2/3], sum=1.0), alpha=1.0 (weights=[1/3, 1/3, 1/3], sum=1.0). | Risk Measure Trait SS2.1, postcondition 4 |
test_risk_cvar_aggregate_cut_weights_nonnegative | All risk-adjusted weights are non-negative: for all . | For any valid input, every weight must satisfy mu* >= 0. In the shared fixture with lambda=0.5, alpha=0.5: weights are [0, 1/2, 1/2], all non-negative. The zero weight for the lowest-cost opening is valid (greedy allocation exhausted the budget before reaching it). | Risk Measure Trait SS2.1, postcondition 5 |
test_risk_cvar_aggregate_cut_weights_bounded_by_mu_bar | Each weight is bounded by its per-scenario upper bound: . | For the shared fixture with lambda=0.5, alpha=0.5: mu_bar = 1/2 for each opening. Weights [0, 1/2, 1/2] satisfy 0 <= 1/2, 1/2 <= 1/2, 1/2 <= 1/2. For nonuniform probabilities (p=[0.5, 0.3, 0.2], lambda=0.5, alpha=0.5): mu_bar = [0.75, 0.45, 0.3], weights = [0.25, 0.45, 0.3], all bounded: 0.25 <= 0.75, 0.45 <= 0.45, 0.3 <= 0.3. | Risk Measures SS4.2, Risk Measure Trait SS2.1, postcondition 5 |
test_risk_cvar_aggregate_cut_weights_descending_cost_order | When costs are strictly ordered, the greedy allocation assigns weakly descending weights to the cost-descending sequence: if then . | For the shared fixture (costs [100, 200, 300], lambda=0.5, alpha=0.5): weights in cost-descending order are [1/2, 1/2, 0], which is weakly descending (1/2 >= 1/2 >= 0). For pure CVaR (lambda=1.0, alpha=0.5): weights in cost-descending order are [2/3, 1/3, 0], which is strictly descending. | Risk Measures SS7, greedy allocation places maximum weight on highest-cost scenarios first |
SS3.2 Monotonicity Invariants
| Test Name | Property | Expected Observable Behavior | Reference |
|---|---|---|---|
test_risk_cvar_evaluate_risk_geq_expectation | When alpha < 1, the CVaR risk-adjusted cost is at least as large as the expected value: . | For the shared fixture costs=[100, 200, 300] with lambda=0.5, alpha=0.5: rho = 700/3 (approx 233.333) >= E[Z] = 200. For pure CVaR (lambda=1.0, alpha=0.5): rho = 800/3 (approx 266.667) >= 200. For lambda=0.5, alpha=0.05: rho = 250 >= 200. | Risk Measure Trait SS2.2, postcondition 4 |
test_risk_cvar_evaluate_risk_monotone_in_lambda | Increasing lambda (stronger risk aversion) weakly increases the risk-adjusted cost when alpha < 1: . | For the shared fixture costs=[100, 200, 300] with alpha=0.5: lambda=0 gives 200.0, lambda=0.5 gives 700/3 (approx 233.333), lambda=1.0 gives 800/3 (approx 266.667). The sequence 200 <= 233.333 <= 266.667 is monotonically non-decreasing. | Risk Measures SS3, convex combination definition |
test_risk_cvar_evaluate_risk_monotone_in_alpha | Decreasing alpha (more extreme tail focus) weakly increases the risk-adjusted cost: . | For the shared fixture costs=[100, 200, 300] with lambda=0.5: alpha=1.0 gives 200.0, alpha=0.5 gives 700/3 (approx 233.333), alpha=0.05 gives 250.0. The sequence 200 <= 233.333 <= 250 is monotonically non-decreasing. | Risk Measures SS2, CVaR definition |
SS3.3 Validation Rejection Tests
These tests verify that invalid parameters are rejected during configuration loading, per Extension Points SS2.3 rules R1-R3 and Risk Measure Trait SS5.
| Test Name | Input Scenario | Expected Observable Behavior | Rule |
|---|---|---|---|
test_risk_cvar_config_reject_alpha_zero | CVaR config with alpha=0.0, lambda=0.5. | Configuration loading rejects with an error. alpha=0 produces undefined CVaR (division by zero in mu_bar computation). | R1 |
test_risk_cvar_config_reject_alpha_negative | CVaR config with alpha=-0.1, lambda=0.5. | Configuration loading rejects with an error. alpha must be in (0, 1]. | R1 |
test_risk_cvar_config_reject_alpha_greater_than_one | CVaR config with alpha=1.5, lambda=0.5. | Configuration loading rejects with an error. alpha must be in (0, 1]. | R1 |
test_risk_cvar_config_reject_lambda_negative | CVaR config with alpha=0.5, lambda=-0.1. | Configuration loading rejects with an error. lambda must be in [0, 1]. | R2 |
test_risk_cvar_config_reject_lambda_greater_than_one | CVaR config with alpha=0.5, lambda=1.5. | Configuration loading rejects with an error. lambda must be in [0, 1]. | R2 |
test_risk_cvar_config_normalize_lambda_zero | CVaR config with alpha=0.5, lambda=0.0. Shared fixture openings (costs [100, 200, 300], probabilities [1/3, 1/3, 1/3]). | Configuration loading accepts without error. The CVaR config is normalized to the Expectation variant internally. aggregate_cut returns intercept = 20.0, coefficients = [3.0, 4.0] (identical to Expectation). evaluate_risk returns 200.0 (identical to Expectation). | R3 |
Cross-References
- Risk Measure Trait – Enum definition (SS1), method contracts (SS2), BackwardOutcome type (SS1), validation rules (SS5), special cases (SS6)
- Risk Measures – CVaR definition (SS2), convex combination (SS3), EAVaR dual representation (SS4.2), risk-averse subgradient theorem (SS5), sorting-based greedy allocation (SS7)
- Cut Management – Single-cut aggregation (SS3) that the Expectation variant reproduces
- Extension Points – Risk measure variant table (SS2.1), validation rules R1-R3 (SS2.3), variant selection pipeline (SS6)
- Backend Testing – Conformance test suite structure and requirements table format (reference pattern for this spec)