post-app-website-new/lib/tbff-flow/utils.ts

177 lines
4.4 KiB
TypeScript

/**
* Utility functions for flow-based calculations
*/
import type { FlowNode, FlowNodeStatus, FlowNetwork, FlowAllocation } from './types'
/**
* Calculate node status based on absorbed flow vs thresholds
*/
export function getFlowNodeStatus(node: FlowNode): FlowNodeStatus {
if (node.absorbed < node.minAbsorption) return 'starved'
if (node.absorbed >= node.maxAbsorption) return 'saturated'
if (Math.abs(node.absorbed - node.minAbsorption) < 0.01) return 'minimum'
return 'healthy'
}
/**
* Calculate how much flow a node absorbs given inflow
* Absorbs between min and max thresholds
*/
export function calculateAbsorption(inflow: number, minAbsorption: number, maxAbsorption: number): number {
// Absorb as much as possible, up to max
return Math.min(inflow, maxAbsorption)
}
/**
* Calculate outflow (excess that couldn't be absorbed)
*/
export function calculateOutflow(inflow: number, absorbed: number): number {
return Math.max(0, inflow - absorbed)
}
/**
* Update all computed properties on a node
*/
export function updateFlowNodeProperties(node: FlowNode): FlowNode {
const absorbed = calculateAbsorption(node.inflow, node.minAbsorption, node.maxAbsorption)
const outflow = calculateOutflow(node.inflow, absorbed)
const status = getFlowNodeStatus({ ...node, absorbed })
return {
...node,
absorbed,
outflow,
status,
}
}
/**
* Calculate network-level totals
*/
export function calculateFlowNetworkTotals(network: FlowNetwork): FlowNetwork {
const totalInflow = network.nodes.reduce((sum, node) => sum + node.externalFlow, 0)
const totalAbsorbed = network.nodes.reduce((sum, node) => sum + node.absorbed, 0)
const totalOutflow = network.nodes.reduce((sum, node) => sum + node.outflow, 0)
return {
...network,
totalInflow,
totalAbsorbed,
totalOutflow,
}
}
/**
* Normalize allocations so they sum to 1.0
* Same as stock model
*/
export function normalizeFlowAllocations(allocations: FlowAllocation[]): FlowAllocation[] {
if (allocations.length === 1) {
return allocations.map(a => ({ ...a, percentage: 1.0 }))
}
const total = allocations.reduce((sum, a) => sum + a.percentage, 0)
if (total === 0) {
const equalShare = 1.0 / allocations.length
return allocations.map((a) => ({ ...a, percentage: equalShare }))
}
if (Math.abs(total - 1.0) < 0.0001) {
return allocations
}
return allocations.map((a) => ({
...a,
percentage: a.percentage / total,
}))
}
/**
* Get center point of a node (for arrow endpoints)
*/
export function getFlowNodeCenter(node: FlowNode): { x: number; y: number } {
return {
x: node.x + node.width / 2,
y: node.y + node.height / 2,
}
}
/**
* Get status color for rendering
*/
export function getFlowStatusColor(status: FlowNodeStatus, alpha: number = 1): string {
const colors = {
starved: `rgba(239, 68, 68, ${alpha})`, // Red
minimum: `rgba(251, 191, 36, ${alpha})`, // Yellow
healthy: `rgba(99, 102, 241, ${alpha})`, // Blue
saturated: `rgba(16, 185, 129, ${alpha})`, // Green
}
return colors[status]
}
/**
* Get status color as Tailwind class
*/
export function getFlowStatusColorClass(status: FlowNodeStatus): string {
const classes = {
starved: 'text-red-400',
minimum: 'text-yellow-400',
healthy: 'text-blue-400',
saturated: 'text-green-400',
}
return classes[status]
}
/**
* Format flow rate for display
*/
export function formatFlow(rate: number): string {
return rate.toFixed(1)
}
/**
* Format percentage for display
*/
export function formatPercentage(decimal: number): string {
return `${Math.round(decimal * 100)}%`
}
/**
* Create the overflow sink node
*/
export function createOverflowNode(x: number, y: number): FlowNode {
return {
id: 'overflow-sink',
name: 'Overflow',
x,
y,
width: 120,
height: 80,
minAbsorption: 0, // Can absorb any amount
maxAbsorption: Infinity, // No limit
inflow: 0,
absorbed: 0,
outflow: 0,
status: 'healthy',
externalFlow: 0,
isOverflowSink: true,
}
}
/**
* Check if network needs an overflow node
* Returns true if any node has outflow with no allocations
*/
export function needsOverflowNode(network: FlowNetwork): boolean {
return network.nodes.some(node => {
if (node.isOverflowSink) return false
const hasOutflow = node.outflow > 0.01
const hasAllocations = network.allocations.some(a => a.sourceNodeId === node.id)
return hasOutflow && !hasAllocations
})
}