canvas-website/src/open-mapping/conics/optimization.ts

748 lines
21 KiB
TypeScript

/**
* Value-Weighted Path Optimization
*
* Find optimal paths through the intersection of possibility cones,
* maximizing accumulated value while satisfying constraints.
*/
import type {
SpacePoint,
SpaceVector,
PossibilityCone,
PossibilityPath,
PathWaypoint,
OptimizationConfig,
OptimizationResult,
ConeConstraint,
ValueField,
} from './types';
import { DEFAULT_OPTIMIZATION_CONFIG } from './types';
import {
distance,
addVectors,
subtractVectors,
scaleVector,
normalize,
magnitude,
pointToVector,
vectorToPoint,
isPointInCone,
isPointInIntersection,
signedDistanceToSurface,
} from './geometry';
// =============================================================================
// Path Optimizer
// =============================================================================
/**
* Path optimization engine
*/
export class PathOptimizer {
private config: OptimizationConfig;
private cones: PossibilityCone[] = [];
private constraints: ConeConstraint[] = [];
private valueField?: ValueField;
private bounds: { min: SpacePoint; max: SpacePoint };
constructor(
bounds: { min: SpacePoint; max: SpacePoint },
config: Partial<OptimizationConfig> = {}
) {
this.config = { ...DEFAULT_OPTIMIZATION_CONFIG, ...config };
this.bounds = bounds;
}
/**
* Set the cones to navigate through
*/
setCones(cones: PossibilityCone[]): void {
this.cones = cones;
}
/**
* Set additional constraints
*/
setConstraints(constraints: ConeConstraint[]): void {
this.constraints = constraints;
}
/**
* Set the value field for optimization
*/
setValueField(field: ValueField): void {
this.valueField = field;
}
/**
* Find optimal path from start to goal
*/
findOptimalPath(
start: SpacePoint,
goal: SpacePoint
): OptimizationResult {
const startTime = Date.now();
switch (this.config.algorithm) {
case 'a-star':
return this.aStarSearch(start, goal, startTime);
case 'dijkstra':
return this.dijkstraSearch(start, goal, startTime);
case 'gradient-descent':
return this.gradientDescent(start, goal, startTime);
case 'simulated-annealing':
return this.simulatedAnnealing(start, goal, startTime);
default:
return this.aStarSearch(start, goal, startTime);
}
}
// ===========================================================================
// A* Search
// ===========================================================================
private aStarSearch(
start: SpacePoint,
goal: SpacePoint,
startTime: number
): OptimizationResult {
const dim = start.coordinates.length;
const resolution = this.config.samplingResolution;
// Discretize space
const grid = this.createGrid(resolution);
// Find grid cells for start and goal
const startCell = this.pointToCell(start, resolution);
const goalCell = this.pointToCell(goal, resolution);
// Priority queue (min-heap by f-score)
const openSet: Array<{ cell: number[]; fScore: number }> = [
{ cell: startCell, fScore: 0 },
];
// Track visited cells and their g-scores
const gScore = new Map<string, number>();
const fScore = new Map<string, number>();
const cameFrom = new Map<string, number[]>();
const cellKey = (cell: number[]) => cell.join(',');
gScore.set(cellKey(startCell), 0);
fScore.set(cellKey(startCell), this.heuristic(startCell, goalCell, resolution));
let iterations = 0;
while (openSet.length > 0 && iterations < this.config.maxIterations) {
iterations++;
// Get cell with lowest f-score
openSet.sort((a, b) => a.fScore - b.fScore);
const current = openSet.shift()!;
const currentKey = cellKey(current.cell);
// Check if reached goal
if (this.cellsEqual(current.cell, goalCell)) {
const path = this.reconstructPath(cameFrom, current.cell, start, goal, resolution);
return this.createResult(path, iterations, true, startTime);
}
// Explore neighbors
const neighbors = this.getNeighborCells(current.cell, resolution);
for (const neighbor of neighbors) {
const neighborKey = cellKey(neighbor);
const neighborPoint = this.cellToPoint(neighbor, resolution);
// Check if valid (in cone intersection)
if (!this.isValidPoint(neighborPoint)) continue;
// Calculate tentative g-score
const currentPoint = this.cellToPoint(current.cell, resolution);
const moveCost = this.getMoveCost(currentPoint, neighborPoint);
const tentativeG = (gScore.get(currentKey) ?? Infinity) + moveCost;
if (tentativeG < (gScore.get(neighborKey) ?? Infinity)) {
// Better path found
cameFrom.set(neighborKey, current.cell);
gScore.set(neighborKey, tentativeG);
const h = this.heuristic(neighbor, goalCell, resolution);
const f = tentativeG + h;
fScore.set(neighborKey, f);
if (!openSet.some((n) => cellKey(n.cell) === neighborKey)) {
openSet.push({ cell: neighbor, fScore: f });
}
}
}
}
// No path found - return best effort
const partialPath = this.createDirectPath(start, goal);
return this.createResult(partialPath, iterations, false, startTime);
}
// ===========================================================================
// Dijkstra Search
// ===========================================================================
private dijkstraSearch(
start: SpacePoint,
goal: SpacePoint,
startTime: number
): OptimizationResult {
// Simplified Dijkstra (A* with h=0)
const originalConfig = this.config;
this.config = { ...originalConfig };
const result = this.aStarSearch(start, goal, startTime);
this.config = originalConfig;
return result;
}
// ===========================================================================
// Gradient Descent
// ===========================================================================
private gradientDescent(
start: SpacePoint,
goal: SpacePoint,
startTime: number
): OptimizationResult {
const dim = start.coordinates.length;
const stepSize = 0.1;
const waypoints: PathWaypoint[] = [];
let current = { ...start, coordinates: [...start.coordinates] };
let iterations = 0;
let totalValue = this.getValueAt(current);
let distanceToGoal = distance(current, goal);
while (iterations < this.config.maxIterations && distanceToGoal > 0.1) {
iterations++;
// Calculate gradient (toward goal + value gradient)
const toGoal = subtractVectors(pointToVector(goal), pointToVector(current));
const goalDir = normalize(toGoal);
// Value gradient (finite differences)
const valueGrad = this.estimateValueGradient(current);
// Combined gradient
const combinedGrad = addVectors(
scaleVector(goalDir, this.config.weights.length),
scaleVector(valueGrad, this.config.weights.value)
);
const step = scaleVector(normalize(combinedGrad), stepSize);
// Proposed new position
const proposed: SpacePoint = {
coordinates: current.coordinates.map(
(c, i) => c + (step.components[i] ?? 0)
),
};
// Check validity
if (this.isValidPoint(proposed)) {
// Add waypoint
waypoints.push({
position: current,
value: this.getValueAt(current),
distanceFromStart: waypoints.length > 0
? waypoints[waypoints.length - 1].distanceFromStart + distance(current, proposed)
: 0,
containingCones: this.getContainingCones(current),
});
current = proposed;
totalValue += this.getValueAt(current);
distanceToGoal = distance(current, goal);
} else {
// Try to project back into valid region
const projected = this.projectToValidRegion(proposed);
if (projected) {
current = projected;
} else {
break; // Stuck
}
}
// Check convergence
if (distanceToGoal < this.config.convergenceThreshold) {
break;
}
}
// Add final waypoint
waypoints.push({
position: current,
value: this.getValueAt(current),
distanceFromStart: waypoints.length > 0
? waypoints[waypoints.length - 1].distanceFromStart + distance(current, goal)
: distance(start, goal),
containingCones: this.getContainingCones(current),
});
const path = this.waypointsToPath(waypoints);
return this.createResult(path, iterations, distanceToGoal < 0.1, startTime);
}
// ===========================================================================
// Simulated Annealing
// ===========================================================================
private simulatedAnnealing(
start: SpacePoint,
goal: SpacePoint,
startTime: number
): OptimizationResult {
const dim = start.coordinates.length;
// Initialize with direct path
let currentPath = this.createDirectPath(start, goal);
let currentScore = this.scorePath(currentPath);
let bestPath = currentPath;
let bestScore = currentScore;
let temperature = 1.0;
const coolingRate = 0.995;
let iterations = 0;
while (
iterations < this.config.maxIterations &&
temperature > this.config.convergenceThreshold
) {
iterations++;
// Generate neighbor solution (perturb random waypoint)
const neighborPath = this.perturbPath(currentPath);
const neighborScore = this.scorePath(neighborPath);
// Acceptance probability
const delta = neighborScore - currentScore;
const acceptProb = delta > 0 ? 1 : Math.exp(delta / temperature);
if (Math.random() < acceptProb) {
currentPath = neighborPath;
currentScore = neighborScore;
if (currentScore > bestScore) {
bestPath = currentPath;
bestScore = currentScore;
}
}
temperature *= coolingRate;
}
return this.createResult(bestPath, iterations, true, startTime);
}
// ===========================================================================
// Helper Methods
// ===========================================================================
private createGrid(resolution: number): void {
// Grid is implicit - we just use resolution to map points to cells
}
private pointToCell(point: SpacePoint, resolution: number): number[] {
return point.coordinates.map((c, i) => {
const min = this.bounds.min.coordinates[i];
const max = this.bounds.max.coordinates[i];
const normalized = (c - min) / (max - min);
return Math.floor(normalized * resolution);
});
}
private cellToPoint(cell: number[], resolution: number): SpacePoint {
return {
coordinates: cell.map((c, i) => {
const min = this.bounds.min.coordinates[i];
const max = this.bounds.max.coordinates[i];
return min + ((c + 0.5) / resolution) * (max - min);
}),
};
}
private cellsEqual(a: number[], b: number[]): boolean {
return a.every((v, i) => v === b[i]);
}
private getNeighborCells(cell: number[], resolution: number): number[][] {
const neighbors: number[][] = [];
const dim = cell.length;
// Generate all adjacent cells (26 neighbors in 3D, etc.)
const directions: number[] = [-1, 0, 1];
const generate = (index: number, current: number[]): void => {
if (index === dim) {
if (!current.every((v, i) => v === cell[i])) {
// Check bounds
if (current.every((v, i) => v >= 0 && v < resolution)) {
neighbors.push([...current]);
}
}
return;
}
for (const d of directions) {
current[index] = cell[index] + d;
generate(index + 1, current);
}
};
generate(0, new Array(dim).fill(0));
return neighbors;
}
private heuristic(
cell: number[],
goalCell: number[],
resolution: number
): number {
// Euclidean distance in cell space
let sum = 0;
for (let i = 0; i < cell.length; i++) {
const diff = cell[i] - goalCell[i];
sum += diff * diff;
}
return Math.sqrt(sum);
}
private getMoveCost(from: SpacePoint, to: SpacePoint): number {
const dist = distance(from, to);
const value = (this.getValueAt(from) + this.getValueAt(to)) / 2;
const risk = this.getRiskAt(to);
// Cost = distance - value + risk
return (
dist * this.config.weights.length -
value * this.config.weights.value +
risk * this.config.weights.risk
);
}
private isValidPoint(point: SpacePoint): boolean {
// Check bounds
for (let i = 0; i < point.coordinates.length; i++) {
if (
point.coordinates[i] < this.bounds.min.coordinates[i] ||
point.coordinates[i] > this.bounds.max.coordinates[i]
) {
return false;
}
}
// Check cone intersection
if (!isPointInIntersection(point, this.cones)) {
return false;
}
// Check hard constraints
for (const constraint of this.constraints) {
if (constraint.hardness === 'hard') {
if (signedDistanceToSurface(point, constraint.surface) > 0) {
return false;
}
}
}
return true;
}
private getValueAt(point: SpacePoint): number {
if (!this.valueField) {
// Default: higher value along value dimension
return point.coordinates[1] ?? 0;
}
// Trilinear interpolation from value field
// Simplified: nearest neighbor
const resolution = this.valueField.resolution;
const indices = point.coordinates.map((c, i) => {
const min = this.bounds.min.coordinates[i];
const max = this.bounds.max.coordinates[i];
const normalized = (c - min) / (max - min);
return Math.floor(normalized * (resolution[i] - 1));
});
// Flatten index
let flatIndex = 0;
let stride = 1;
for (let i = indices.length - 1; i >= 0; i--) {
flatIndex += indices[i] * stride;
stride *= resolution[i];
}
return this.valueField.values[flatIndex] ?? 0;
}
private getRiskAt(point: SpacePoint): number {
// Default: lower risk toward center of cones
let totalDistance = 0;
for (const cone of this.cones) {
totalDistance += Math.abs(
isPointInCone(point, cone) ? -1 : 1
);
}
return totalDistance / Math.max(1, this.cones.length);
}
private estimateValueGradient(point: SpacePoint): SpaceVector {
const epsilon = 0.01;
const dim = point.coordinates.length;
const gradient: number[] = [];
for (let i = 0; i < dim; i++) {
const forward: SpacePoint = {
coordinates: point.coordinates.map((c, j) =>
j === i ? c + epsilon : c
),
};
const backward: SpacePoint = {
coordinates: point.coordinates.map((c, j) =>
j === i ? c - epsilon : c
),
};
const fValue = this.isValidPoint(forward) ? this.getValueAt(forward) : 0;
const bValue = this.isValidPoint(backward) ? this.getValueAt(backward) : 0;
gradient.push((fValue - bValue) / (2 * epsilon));
}
return { components: gradient };
}
private projectToValidRegion(point: SpacePoint): SpacePoint | null {
// Simple projection: move toward nearest valid point
// This is a simplified version - real implementation would be more sophisticated
const maxAttempts = 10;
let current = point;
for (let i = 0; i < maxAttempts; i++) {
if (this.isValidPoint(current)) {
return current;
}
// Move toward center of bounds
const center: SpacePoint = {
coordinates: this.bounds.min.coordinates.map(
(min, j) => (min + this.bounds.max.coordinates[j]) / 2
),
};
const toCenter = subtractVectors(
pointToVector(center),
pointToVector(current)
);
const step = scaleVector(normalize(toCenter), 0.1);
current = {
coordinates: current.coordinates.map(
(c, j) => c + (step.components[j] ?? 0)
),
};
}
return null;
}
private getContainingCones(point: SpacePoint): string[] {
return this.cones
.filter((cone) => isPointInCone(point, cone))
.map((cone) => cone.id);
}
private reconstructPath(
cameFrom: Map<string, number[]>,
goalCell: number[],
start: SpacePoint,
goal: SpacePoint,
resolution: number
): PossibilityPath {
const waypoints: PathWaypoint[] = [];
let current = goalCell;
const cellKey = (cell: number[]) => cell.join(',');
// Trace back
const cells: number[][] = [current];
while (cameFrom.has(cellKey(current))) {
current = cameFrom.get(cellKey(current))!;
cells.unshift(current);
}
// Convert to waypoints
let distanceFromStart = 0;
let prevPoint = start;
for (const cell of cells) {
const point = this.cellToPoint(cell, resolution);
distanceFromStart += distance(prevPoint, point);
waypoints.push({
position: point,
value: this.getValueAt(point),
distanceFromStart,
containingCones: this.getContainingCones(point),
});
prevPoint = point;
}
return this.waypointsToPath(waypoints);
}
private createDirectPath(start: SpacePoint, goal: SpacePoint): PossibilityPath {
const waypoints: PathWaypoint[] = [
{
position: start,
value: this.getValueAt(start),
distanceFromStart: 0,
containingCones: this.getContainingCones(start),
},
{
position: goal,
value: this.getValueAt(goal),
distanceFromStart: distance(start, goal),
containingCones: this.getContainingCones(goal),
},
];
return this.waypointsToPath(waypoints);
}
private waypointsToPath(waypoints: PathWaypoint[]): PossibilityPath {
const totalValue = waypoints.reduce((sum, w) => sum + w.value, 0);
const length =
waypoints.length > 0
? waypoints[waypoints.length - 1].distanceFromStart
: 0;
const satisfiedConstraints = this.constraints
.filter((c) =>
waypoints.every(
(w) => signedDistanceToSurface(w.position, c.surface) <= 0
)
)
.map((c) => c.id);
const violatedConstraints = this.constraints
.filter((c) => !satisfiedConstraints.includes(c.id))
.map((c) => c.id);
return {
id: `path-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
waypoints,
length,
totalValue,
riskExposure: this.calculateRiskExposure(waypoints),
satisfiedConstraints,
violatedConstraints,
optimalityScore: this.scorePath({
id: '',
waypoints,
length,
totalValue,
riskExposure: 0,
satisfiedConstraints,
violatedConstraints,
optimalityScore: 0,
}),
};
}
private scorePath(path: PossibilityPath): number {
const { weights } = this.config;
let score = 0;
score += path.totalValue * weights.value;
score -= path.length * weights.length;
score -= path.riskExposure * weights.risk;
score +=
(path.satisfiedConstraints.length /
Math.max(1, this.constraints.length)) *
weights.constraints;
// Penalty for soft violations
if (this.config.allowSoftViolations) {
score -=
path.violatedConstraints.filter((id) => {
const c = this.constraints.find((c) => c.id === id);
return c?.hardness === 'soft';
}).length * this.config.softViolationPenalty;
}
return score;
}
private calculateRiskExposure(waypoints: PathWaypoint[]): number {
return waypoints.reduce((sum, w) => sum + this.getRiskAt(w.position), 0) /
Math.max(1, waypoints.length);
}
private perturbPath(path: PossibilityPath): PossibilityPath {
if (path.waypoints.length < 3) return path;
// Select random waypoint (not start/end)
const index = 1 + Math.floor(Math.random() * (path.waypoints.length - 2));
const waypoint = path.waypoints[index];
// Perturb position
const perturbation = 0.5;
const newPosition: SpacePoint = {
coordinates: waypoint.position.coordinates.map(
(c) => c + (Math.random() - 0.5) * perturbation
),
};
// Create new waypoints array
const newWaypoints = [...path.waypoints];
newWaypoints[index] = {
...waypoint,
position: newPosition,
value: this.getValueAt(newPosition),
containingCones: this.getContainingCones(newPosition),
};
return this.waypointsToPath(newWaypoints);
}
private createResult(
path: PossibilityPath,
iterations: number,
converged: boolean,
startTime: number
): OptimizationResult {
return {
bestPath: path,
alternatives: [], // Could generate Pareto frontier
iterations,
converged,
metrics: {
initialScore: 0,
finalScore: path.optimalityScore,
improvement: path.optimalityScore,
runtime: Date.now() - startTime,
},
};
}
}
// =============================================================================
// Factory Function
// =============================================================================
/**
* Create a path optimizer
*/
export function createPathOptimizer(
bounds: { min: SpacePoint; max: SpacePoint },
config?: Partial<OptimizationConfig>
): PathOptimizer {
return new PathOptimizer(bounds, config);
}