177 lines
4.4 KiB
TypeScript
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
|
|
})
|
|
}
|