Factor Design - gregw/sailing-pf GitHub Wiki
Factors are weighted values, with the weight (0.000 to 1.000) determining the quality of the value. If there is low variance in aggregated values, then the combined weight increases. If there is high variance in values, then the combined weight is decreased.
The system stores and computes everything in terms of time correction factors: a dimensionless ratio applied to elapsed time to give corrected time.
correctedTime = elapsedTime × factor
Under this convention, a faster boat needs a smaller factor (its corrected time is shorter
than its elapsed). The reference boat has factor 1.000; faster boats sit below 1.0, slower
above. This is the form used everywhere in code: Factor.value, RF, PF, allocated handicaps,
certificate-derived factors, and all the operations (apply, compose, aggregate) defined
below.
The speed correction factor is just the multiplicative inverse, 1 / factor. It is never
stored — it is a presentation transform applied at the chart layer because it has properties
that read better visually:
| Property | Time correction factor (f) |
Speed correction factor (1/f) |
|---|---|---|
| Faster boat | smaller value | larger value |
| Reference boat | 1.000 | 1.000 |
| Operation that produces corrected time | t × f |
t / (1/f) |
| Used in code | yes — all internal arithmetic | no |
| Used on charts | rarely (axis would put fast boats on the left) | yes (x-axis: 1/PF, 1/RF, 1/Allocated) |
The two are mathematically equivalent but the speed form is preferred on chart axes because
elapsed time is approximately proportional to 1/f when boats are correctly rated, making
the elapsed-time trace a straight ramp instead of a hyperbola — and faster boats sit on the
right, matching the natural reading of a speed-like axis.
All weighted-factor operations described below work on time correction factors. Inverting
a Factor for display does not commute cleanly with aggregate (the weighted mean of
1/vᵢ is not 1 / weighted_mean(vᵢ)), which is the main reason the speed form is kept
strictly to the presentation layer.
See UI Tips — Speed Factor for the chart-side perspective.
record Factor(double value, double weight) {
// value: dimensionless time correction ratio, ~1.0000, range (0, ~2.0]
// weight: scalar in [0, 1]; 0 = discard entirely
}
record WeightedInterval(Duration duration, double weight) {
// duration: elapsed time
// weight: scalar in [0, 1]
}Factor and WeightedInterval are structurally parallel: both carry a weight in [0, 1]
by the same convention. Factor holds a dimensionless ratio; WeightedInterval holds a
Duration. Weight of zero means "discard" — callers and implementations should skip
zero-weight inputs rather than letting them silently distort results.
Applies a handicap factor to a weighted elapsed time to produce a corrected time.
result.duration = t.duration × f.value
result.weight = t.weight × f.weight
Two uncertain things composed together are more uncertain than either alone, so weights multiply. This is the correct composition for dependent uncertainties.
Chains two independent multiplicative adjustments — for example, a certificate value composed with a propagation step through a race network hop. Used when the two factors represent different stages of a causal chain, not two estimates of the same quantity.
result.value = a.value × b.value
result.weight = a.weight × b.weight
compose is commutative and associative with respect to value arithmetic. Weights multiply
because independent uncertainties compound.
Combines multiple independent estimates of the same quantity — for example, a boat's PF estimated from 8 different races, or certificate-based factors from multiple conversion paths. This is the primary combiner: it accumulates evidence (more agreeing observations increase confidence) while penalising disagreement.
Weighted mean of the input values:
combinedValue = Σ(wᵢ × vᵢ) / Σwᵢ
Pooled evidence, penalised by the spread of values:
pooledWeight = 1 − ∏(1 − wᵢ)
weightedVariance = Σ(wᵢ × (vᵢ − combinedValue)²) / Σwᵢ
combinedWeight = pooledWeight / (1 + weightedVariance / σ₀²)
The pooledWeight formula (1 − ∏(1 − wᵢ)) is the "at least one good measurement"
accumulator — it always increases with more inputs, is bounded [0, 1], and adding a
zero-weight input is a no-op. This ensures that repeated, agreeing evidence always
increases confidence.
σ₀ is a tunable scale parameter representing "how much variance halves confidence?"
Value: σ₀ = 0.15 (a standard deviation of 15% in factor value space halves the
combined weight).
| Input spread | Effect on combinedWeight
|
|---|---|
All inputs identical → weightedVariance = 0
|
combinedWeight = pooledWeight (no penalty, always ≥ max input weight) |
| Inputs close together |
combinedWeight increases above max input weight, but less than identical case |
| Inputs moderately scattered (large weights) |
combinedWeight reduced below individual input weights |
| Inputs moderately scattered (small weights) | Evidence accumulation still wins; weight increases above inputs |
| Inputs highly scattered |
combinedWeight substantially reduced |
| Operation | Inputs | Output | Purpose |
|---|---|---|---|
apply |
Factor, WeightedInterval
|
WeightedInterval |
Apply a handicap to an elapsed time |
compose |
Factor, Factor
|
Factor |
Chain independent multiplicative steps |
aggregate |
Factor[] |
Factor |
Combine multiple estimates of the same value |
-
These are pure value-type operations — no side effects, no dependencies on any other layer. They belong on the
Factorrecord as static methods, or in a companionFactorsutility class. -
aggregaterequires the tuning constantσ₀(currently 0.15). This must be a named constant or configuration parameter — not a magic number buried in the implementation. -
WeightedIntervalis structurally parallel toFactorbut holds aDurationinstead of a dimensionless ratio. Both carryweightin [0, 1] by the same convention. -
Weight of zero means "discard". Implementations must skip zero-weight factors rather than allowing them to silently distort results.
-
Factorhas a nice closure property: the output ofaggregateis itself aFactor, so aggregation can be applied at multiple levels (race → series → fleet) without changing the type. -
The expected value range for
Factor.valueis (0, ~2.0], open-ended on the upper side. Slower boats have values above 1.0; faster boats below 1.0. The value 1.000 represents the hypothetical reference boat against which all PF calculations are anchored.