Fix progressive outflow formula: ensure monotonic increase

Fixed critical bug where outflow would decrease when crossing from
building to capacity zone. The original formula caused a discontinuity.

**Problem:**
Old formula had outflow jumping from (inflow × ratio) in building zone
to (inflow - max) in capacity zone, causing outflow to DROP when
crossing the threshold.

Example (min=100, max=300):
- At inflow=300: outflow = 300 (100% sharing)
- At inflow=301: outflow = 1 (sudden drop!)

**Solution:**
1. Changed capacity threshold from max to 1.5 × max
2. Building zone now uses linear interpolation from 0 to 0.5 × max
3. Capacity zone retains max and shares all excess

New formula guarantees:
✓ Monotonically increasing outflow as inflow increases
✓ Continuous at all zone boundaries
✓ At inflow = min → outflow = 0
✓ At inflow = 1.5 × max → outflow = 0.5 × max
✓ At inflow > 1.5 × max → retain max, share excess

Zones:
- Deficit (inflow < min): outflow = 0
- Building (min ≤ inflow < 1.5×max): progressive linear growth
- Capacity (inflow ≥ 1.5×max): outflow = inflow - max

Changes:
- lib/flow-v2/engine-v2.ts: Updated getFlowZone() and calculateOutflow()

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Shawn Anderson 2025-11-10 11:44:19 -08:00
parent a55cec9958
commit cccb6cc20e
1 changed files with 29 additions and 12 deletions

View File

@ -50,13 +50,16 @@ export function perSecondToPerMonth(amountPerSecond: number): number {
/** /**
* Determine which zone a node is in based on total inflow * Determine which zone a node is in based on total inflow
*
* Capacity threshold is 1.5x the max threshold
*/ */
export function getFlowZone(node: FlowNode): FlowZone { export function getFlowZone(node: FlowNode): FlowZone {
const totalInflow = node.totalInflow || 0 const totalInflow = node.totalInflow || 0
const capacityThreshold = 1.5 * node.maxThreshold
if (totalInflow < node.minThreshold) { if (totalInflow < node.minThreshold) {
return 'deficit' return 'deficit'
} else if (totalInflow <= node.maxThreshold) { } else if (totalInflow < capacityThreshold) {
return 'building' return 'building'
} else { } else {
return 'capacity' return 'capacity'
@ -66,33 +69,47 @@ export function getFlowZone(node: FlowNode): FlowZone {
/** /**
* Calculate progressive outflow based on zone * Calculate progressive outflow based on zone
* *
* Deficit Zone (f < min): outflow = 0 (keep everything) * Deficit Zone (inflow < min):
* Building Zone (min f max): outflow = f × ((f - min) / (max - min)) * outflow = 0 (keep everything)
* Capacity Zone (f > max): outflow = f - max (redirect excess) *
* Building Zone (min inflow < 1.5 * max):
* outflow = (inflow - min) × (0.5 × max) / (1.5 × max - min)
* Progressive sharing that smoothly increases
*
* Capacity Zone (inflow 1.5 * max):
* outflow = inflow - max
* Retain max, share all excess
*
* This ensures monotonically increasing outflow and smooth transitions.
*/ */
export function calculateOutflow(node: FlowNode): number { export function calculateOutflow(node: FlowNode): number {
const totalInflow = node.totalInflow || 0 const totalInflow = node.totalInflow || 0
const { minThreshold, maxThreshold } = node const { minThreshold, maxThreshold } = node
// Capacity threshold: when you start sharing all excess above max
const capacityThreshold = 1.5 * maxThreshold
// Deficit zone: keep everything // Deficit zone: keep everything
if (totalInflow < minThreshold) { if (totalInflow < minThreshold) {
return 0 return 0
} }
// Capacity zone: redirect excess // Capacity zone: retain max, share all excess
if (totalInflow > maxThreshold) { if (totalInflow >= capacityThreshold) {
return totalInflow - maxThreshold return totalInflow - maxThreshold
} }
// Building zone: progressive sharing // Building zone: progressive sharing
const range = maxThreshold - minThreshold const buildingRange = capacityThreshold - minThreshold
if (range === 0) { if (buildingRange === 0) {
// Edge case: min === max // Edge case: min === 1.5 * max (shouldn't happen in practice)
return totalInflow > maxThreshold ? totalInflow - maxThreshold : 0 return totalInflow - maxThreshold
} }
const ratio = (totalInflow - minThreshold) / range const excess = totalInflow - minThreshold
return totalInflow * ratio const targetOutflow = 0.5 * maxThreshold // What we'll share at capacity threshold
return (excess / buildingRange) * targetOutflow
} }
/** /**