Gain and cost model for Layer III verification
Gain and cost model for Layer III verification
Section titled “Gain and cost model for Layer III verification”This document specifies the concrete gain(C, v) and cost(C) functions that Layer III stochastic verification needs in order to evaluate per-trial supercriticality of a Loadout cycle C under a rolled-modifier vector v.
It binds rolled ItemInstance values into the Loadout, fixes the per-skill modifier composition rule, and restates Law 8 (gain monotonicity) in its true, locally bounded form.
The cost side prices both charge expenditure and cooldown delay through the implemented effective_cooldown_delay term, and the implementation aligns with the importance-sampling viability probe recorded in docs/notes/verification/vja3-importance-sampling-probe.md.
Loadout-to-ItemInstance binding
Section titled “Loadout-to-ItemInstance binding”A Loadout holds both intrinsic skills and equipped items, where items is a sequence of ItemInstance records produced by the InstanceRoller.
The effective skill set under verification, written effective_skills(L), is the intrinsic skills followed by every skill in item.granted_skills for each equipped item, preserving order.
The trigger graph is built from effective_skills(L), so item-granted skills participate in cycles on equal footing with intrinsic ones.
The mapping from a trigger graph node to its rolled-value vector is recovered through the same ordering: node skill_N (see trigger_graph.build_trigger_graph) corresponds to position N in effective_skills(L).
A position that originated from an intrinsic skill carries an empty rolled-value mapping; a position that originated from an item carries that item’s modifier_values dict.
This makes the cycle-to-rolls correspondence reliably invertible by construction.
Rolled values become modifier parameters
Section titled “Rolled values become modifier parameters”ItemInstance.modifier_values is a dict[str, float] keyed by modifier attribute (the same string that appears as Modifier.attribute).
For an item-derived skill, the parameterised modifier list is produced by walking the skill’s static modifiers and, for each Modifier whose attribute is a key in the item’s modifier_values, returning a new Modifier with value replaced by the rolled value while preserving layer, operation, and attribute.
Modifiers whose attribute is not present in modifier_values are passed through unchanged.
Intrinsic skills, which have no associated ItemInstance, are always parameterised with an empty mapping, so all of their modifiers retain their static value.
This rule keeps modifier identity stable: rolling never adds, removes, or relayer-changes modifiers; it only re-numbers their magnitudes within the constraints declared by the template.
modifier_output and gain
Section titled “modifier_output and gain”The per-skill modifier output reuses the existing aggregation.aggregate_modifiers over its full operation set (add, multiply, set, min, max) and identical layer ordering, so Law 2 is automatically inherited.
With base_value = 0.0, modifier_output(S, v) = aggregate_modifiers(0.0, parameterized_modifiers(S, v)).
The cycle gain is the sum of per-skill outputs over the skills of C:
gain(C, v) = Σ over (S, v_S) in zip(cycle_skills(C), cycle_rolls(C, v)) of modifier_output(S, v_S).
This is a pure, total function over a fixed skill list and a fixed roll mapping, so no Result wrapping is needed.
The only failure mode is the undeclared-layer ValueError that aggregate_modifiers already raises and that Layer I structural verification already forecloses for properly constructed Skills.
The cost of a cycle is the signed sum of its skills’ charge costs plus the sum of their cooldown delays, matching the gain cycle evaluation formula in the design doc:
cost(C) = Σ over S in cycle_skills(C) of ChargeCost(S) + Σ over S in cycle_skills(C) of effective_cooldown_delay(S).
Because Skill.charge_cost can be negative (a charge-granting skill), the cost is signed; a sufficiently large grant offsets both the charge consumption and the cooldown sum, driving the total to zero or below.
effective_cooldown_delay(S) = float(S.cooldown) derives from authored skill data only.
It never reads rolled modifier values, so cost(C) is a constant of the cycle rather than a function of the rolled vector v, and Layer III’s importance-sampling eligibility and tilt derivation stay gain-side exactly as validated.
Layer I construction makes the field well-formed by itself — passive skills carry cooldown 0 and contribute no delay, active skills carry cooldown ≥ 1 — so no special-casing for passives is needed.
The named function is the seam the design doc’s stacking law for cooldown-reduction sources (cooldown(A) × (red₁ + red₂)) anticipates; a reduction pathway composes inside this derivation when one exists.
The design formula adds the two sums without a conversion factor, so cost is a composite score under an implicit one-charge-per-tick exchange and theta remains the single calibration knob on the whole cost side.
A separate cooldown_weight exchange-rate parameter was considered and rejected: no consumer needs to sweep the rate independently, and adding the structure before a severity test demands it would violate the effective-theory discipline.
Supercriticality
Section titled “Supercriticality”Supercriticality is a relational predicate on (gain, cost) with a tunable threshold theta defaulting to 1.0, so the default recovers the design’s intuitive gain > cost form:
is_supercritical(gain_value, cost_value, theta=1.0): if cost_value <= 0: return gain_value > 0.0 return gain_value > theta * cost_valueWhen the cycle is charge-neutral or charge-granting (cost <= 0), any strictly positive gain is supercritical; otherwise we require gain to exceed theta * cost.
The threshold is parameterised so future calibration work can sweep the design boundary without changing call sites.
Law 8: local gain monotonicity
Section titled “Law 8: local gain monotonicity”The original Law 8 statement (“higher modifier rolls cannot reduce cycle gain”) is true only on a restricted subset of modifier operations.
Replacing a static value with a higher rolled value increases gain for additive add modifiers (when value is non-negative) and multiplicative multiply modifiers (when value is non-negative and the running multiplier remains positive), but set, min, and max operations can reduce gain when the rolled value moves in the wrong direction relative to the current accumulator.
The restated, true law is:
On the local monotone subset of modifiers — defined operationally as the set of
Modifiers with(layer = ADDITIVE, operation = "add")or(layer = MULTIPLICATIVE, operation = "multiply")— replacing one modifier’s static value with a strictly larger rolled value yields a strictly largermodifier_output(S, v), holding all other modifier values fixed.
Two value-regime caveats apply at runtime and must be respected by callers that rely on the guarantee:
- additive
addrequires the substituted value to be non-negative; - multiplicative
multiplyrequires the substituted value to be at least1and the running multiplicative accumulator to remain positive.
set, min, and max operations are explicitly excluded from the guarantee.
The implementation surfaces this distinction through monotone_subset(modifiers), which filters a modifier sequence to exactly those modifiers covered by the law.
The importance-sampling viability probe in docs/notes/verification/vja3-importance-sampling-probe.md relies on local monotonicity over independent Beta rolls; that probe’s invented gain(v) = Σ v_i model corresponds precisely to a cycle made up of additive add modifiers, which lives entirely inside the monotone subset.
Layer III variance-reduction analysis remains valid for any cycle whose modifiers all belong to that subset.
Cross-references
Section titled “Cross-references”docs/notes/verification/vja3-importance-sampling-probe.mdrecords the IS probe that motivated this specification.docs/development/requirements/algebraic-laws.mdcarries the restated Law 8.- The
effective_cooldown_delaycost term is implemented ingain_cost.cost; the retired tracker label that once gated it as future work is frozen in the final beads snapshot atdocs/development/traceability/beads-archive/.