Phase 3.3: Multi-objective optimization fix, updated docs & Claude skill
- Fixed drone gimbal optimization to use proper semantic directions - Changed from ['minimize', 'minimize'] to ['minimize', 'maximize'] - Updated Claude skill (v2.0) with Phase 3.3 integration - Added centralized extractor library documentation - Added multi-objective optimization (Protocol 11) section - Added NX multi-solution protocol documentation - Added dashboard integration documentation - Fixed Pareto front degenerate issue with proper NSGA-II configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Parallel Coordinates Plot - Protocol 13
|
||||
* High-dimensional visualization for multi-objective Pareto fronts
|
||||
* Shows objectives and design variables as parallel axes
|
||||
* Parallel Coordinates Plot - Enhanced Multi-Objective Visualization
|
||||
* Shows design variables → objectives → constraints in proper research format
|
||||
* Light theme with high visibility
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
@@ -10,6 +10,7 @@ interface ParetoTrial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
constraint_satisfied?: boolean;
|
||||
}
|
||||
|
||||
@@ -26,74 +27,145 @@ interface DesignVariable {
|
||||
max: number;
|
||||
}
|
||||
|
||||
interface Constraint {
|
||||
name: string;
|
||||
threshold: number;
|
||||
type: 'less_than' | 'greater_than';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface ParallelCoordinatesPlotProps {
|
||||
paretoData: ParetoTrial[];
|
||||
objectives: Objective[];
|
||||
designVariables: DesignVariable[];
|
||||
constraints?: Constraint[];
|
||||
}
|
||||
|
||||
export function ParallelCoordinatesPlot({
|
||||
paretoData,
|
||||
objectives,
|
||||
designVariables
|
||||
designVariables,
|
||||
constraints = []
|
||||
}: ParallelCoordinatesPlotProps) {
|
||||
const [hoveredTrial, setHoveredTrial] = useState<number | null>(null);
|
||||
const [selectedTrials, setSelectedTrials] = useState<Set<number>>(new Set());
|
||||
|
||||
if (paretoData.length === 0) {
|
||||
// Safety checks
|
||||
if (!paretoData || paretoData.length === 0) {
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100">Parallel Coordinates</h3>
|
||||
<div className="h-96 flex items-center justify-center text-dark-300">
|
||||
No Pareto front data yet
|
||||
<div className="bg-white rounded-lg p-6 border border-gray-300 shadow-sm">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">Parallel Coordinates Plot</h3>
|
||||
<div className="h-96 flex items-center justify-center text-gray-500">
|
||||
No Pareto front data available
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Combine objectives and design variables into axes
|
||||
const axes: Array<{name: string, label: string, type: 'objective' | 'param'}> = [
|
||||
...objectives.map((obj, i) => ({
|
||||
name: `obj_${i}`,
|
||||
label: obj.unit ? `${obj.name} (${obj.unit})` : obj.name,
|
||||
type: 'objective' as const
|
||||
})),
|
||||
...designVariables.map(dv => ({
|
||||
if (!objectives || objectives.length === 0 || !designVariables || designVariables.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6 border border-gray-300 shadow-sm">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">Parallel Coordinates Plot</h3>
|
||||
<div className="h-96 flex items-center justify-center text-gray-500">
|
||||
Missing objectives or design variables metadata
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Structure axes: Design Variables → Objectives → Constraints
|
||||
const axes: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'design_var' | 'objective' | 'constraint';
|
||||
unit?: string;
|
||||
}> = [];
|
||||
|
||||
// Add design variables
|
||||
designVariables.forEach(dv => {
|
||||
axes.push({
|
||||
name: dv.name,
|
||||
label: dv.unit ? `${dv.name} (${dv.unit})` : dv.name,
|
||||
type: 'param' as const
|
||||
}))
|
||||
];
|
||||
label: dv.unit ? `${dv.name}\n(${dv.unit})` : dv.name,
|
||||
type: 'design_var',
|
||||
unit: dv.unit
|
||||
});
|
||||
});
|
||||
|
||||
// Normalize data to [0, 1] for each axis
|
||||
const normalizedData = paretoData.map(trial => {
|
||||
const allValues: number[] = [];
|
||||
// Add objectives
|
||||
objectives.forEach((obj, i) => {
|
||||
axes.push({
|
||||
name: `objective_${i}`,
|
||||
label: obj.unit ? `${obj.name}\n(${obj.unit})` : obj.name,
|
||||
type: 'objective',
|
||||
unit: obj.unit
|
||||
});
|
||||
});
|
||||
|
||||
// Add objectives
|
||||
trial.values.forEach(val => allValues.push(val));
|
||||
// Add constraints (extract from user_attrs)
|
||||
const constraintNames = new Set<string>();
|
||||
paretoData.forEach(trial => {
|
||||
if (trial.user_attrs) {
|
||||
// Common constraint metrics
|
||||
if (trial.user_attrs.max_stress !== undefined) constraintNames.add('max_stress');
|
||||
if (trial.user_attrs.max_displacement !== undefined) constraintNames.add('max_displacement');
|
||||
if (trial.user_attrs.frequency !== undefined && objectives.findIndex(obj => obj.name.toLowerCase().includes('freq')) === -1) {
|
||||
constraintNames.add('frequency');
|
||||
}
|
||||
if (trial.user_attrs.mass !== undefined && objectives.findIndex(obj => obj.name.toLowerCase().includes('mass')) === -1) {
|
||||
constraintNames.add('mass');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add design variables
|
||||
constraintNames.forEach(name => {
|
||||
const unit = name.includes('stress') ? 'MPa' :
|
||||
name.includes('displacement') ? 'mm' :
|
||||
name.includes('frequency') ? 'Hz' :
|
||||
name.includes('mass') ? 'g' : '';
|
||||
axes.push({
|
||||
name: `constraint_${name}`,
|
||||
label: unit ? `${name}\n(${unit})` : name,
|
||||
type: 'constraint',
|
||||
unit
|
||||
});
|
||||
});
|
||||
|
||||
// Extract values for each axis
|
||||
const trialData = paretoData.map(trial => {
|
||||
const values: number[] = [];
|
||||
|
||||
// Design variables
|
||||
designVariables.forEach(dv => {
|
||||
allValues.push(trial.params[dv.name]);
|
||||
values.push(trial.params[dv.name] ?? 0);
|
||||
});
|
||||
|
||||
// Objectives
|
||||
trial.values.forEach(val => {
|
||||
values.push(val);
|
||||
});
|
||||
|
||||
// Constraints
|
||||
constraintNames.forEach(name => {
|
||||
values.push(trial.user_attrs?.[name] ?? 0);
|
||||
});
|
||||
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
values: allValues,
|
||||
values,
|
||||
feasible: trial.constraint_satisfied !== false
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate min/max for each axis
|
||||
// Calculate min/max for normalization
|
||||
const ranges = axes.map((_, axisIdx) => {
|
||||
const values = normalizedData.map(d => d.values[axisIdx]);
|
||||
const values = trialData.map(d => d.values[axisIdx]);
|
||||
return {
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values)
|
||||
};
|
||||
});
|
||||
|
||||
// Normalize function
|
||||
// Normalize to [0, 1]
|
||||
const normalize = (value: number, axisIdx: number): number => {
|
||||
const range = ranges[axisIdx];
|
||||
if (range.max === range.min) return 0.5;
|
||||
@@ -101,9 +173,9 @@ export function ParallelCoordinatesPlot({
|
||||
};
|
||||
|
||||
// Chart dimensions
|
||||
const width = 800;
|
||||
const height = 400;
|
||||
const margin = { top: 80, right: 20, bottom: 40, left: 20 };
|
||||
const width = 1400;
|
||||
const height = 500;
|
||||
const margin = { top: 100, right: 50, bottom: 60, left: 50 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
|
||||
@@ -120,149 +192,215 @@ export function ParallelCoordinatesPlot({
|
||||
setSelectedTrials(newSelected);
|
||||
};
|
||||
|
||||
// Color scheme - highly visible
|
||||
const getLineColor = (trial: typeof trialData[0], isHovered: boolean, isSelected: boolean) => {
|
||||
if (isSelected) return '#FF6B00'; // Bright orange for selected
|
||||
if (!trial.feasible) return '#DC2626'; // Red for infeasible
|
||||
if (isHovered) return '#2563EB'; // Blue for hover
|
||||
return '#10B981'; // Green for feasible
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<div className="bg-white rounded-lg p-6 border border-gray-300 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-dark-100">
|
||||
Parallel Coordinates ({paretoData.length} solutions)
|
||||
</h3>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Parallel Coordinates Plot ({paretoData.length} solutions)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Design Variables → Objectives → Constraints
|
||||
</p>
|
||||
</div>
|
||||
{selectedTrials.size > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTrials(new Set())}
|
||||
className="text-xs px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-dark-200"
|
||||
className="text-xs px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded text-gray-800 font-medium transition-colors"
|
||||
>
|
||||
Clear Selection ({selectedTrials.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
{/* Draw axes */}
|
||||
{axes.map((axis, i) => {
|
||||
const x = i * axisSpacing;
|
||||
return (
|
||||
<g key={axis.name} transform={`translate(${x}, 0)`}>
|
||||
{/* Axis line */}
|
||||
<line
|
||||
y1={0}
|
||||
y2={plotHeight}
|
||||
stroke="#475569"
|
||||
strokeWidth={2}
|
||||
<div className="overflow-x-auto">
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
{/* Draw axes */}
|
||||
{axes.map((axis, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const bgColor = axis.type === 'design_var' ? '#EFF6FF' :
|
||||
axis.type === 'objective' ? '#F0FDF4' : '#FEF3C7';
|
||||
|
||||
return (
|
||||
<g key={axis.name} transform={`translate(${x}, 0)`}>
|
||||
{/* Background highlight */}
|
||||
<rect
|
||||
x={-15}
|
||||
y={-20}
|
||||
width={30}
|
||||
height={plotHeight + 40}
|
||||
fill={bgColor}
|
||||
opacity={0.3}
|
||||
/>
|
||||
|
||||
{/* Axis line */}
|
||||
<line
|
||||
y1={0}
|
||||
y2={plotHeight}
|
||||
stroke="#374151"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
|
||||
{/* Axis label */}
|
||||
<text
|
||||
y={-30}
|
||||
textAnchor="middle"
|
||||
fill="#111827"
|
||||
fontSize={12}
|
||||
fontWeight="600"
|
||||
className="select-none"
|
||||
>
|
||||
{(axis.label || '').split('\n').map((line, idx) => (
|
||||
<tspan key={idx} x={0} dy={idx === 0 ? 0 : 14}>{line}</tspan>
|
||||
))}
|
||||
</text>
|
||||
|
||||
{/* Type badge */}
|
||||
<text
|
||||
y={-55}
|
||||
textAnchor="middle"
|
||||
fill="#6B7280"
|
||||
fontSize={9}
|
||||
fontWeight="500"
|
||||
className="select-none"
|
||||
>
|
||||
{axis.type === 'design_var' ? 'DESIGN VAR' :
|
||||
axis.type === 'objective' ? 'OBJECTIVE' : 'CONSTRAINT'}
|
||||
</text>
|
||||
|
||||
{/* Min/max labels */}
|
||||
<text
|
||||
y={plotHeight + 20}
|
||||
textAnchor="middle"
|
||||
fill="#374151"
|
||||
fontSize={10}
|
||||
fontWeight="500"
|
||||
>
|
||||
{ranges[i].min.toFixed(2)}
|
||||
</text>
|
||||
<text
|
||||
y={-10}
|
||||
textAnchor="middle"
|
||||
fill="#374151"
|
||||
fontSize={10}
|
||||
fontWeight="500"
|
||||
>
|
||||
{ranges[i].max.toFixed(2)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Draw polylines for each trial */}
|
||||
{trialData.map(trial => {
|
||||
const isHovered = hoveredTrial === trial.trial_number;
|
||||
const isSelected = selectedTrials.has(trial.trial_number);
|
||||
const isHighlighted = isHovered || isSelected;
|
||||
|
||||
// Build path
|
||||
const pathData = axes.map((_, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const normalizedY = normalize(trial.values[i], i);
|
||||
const y = plotHeight * (1 - normalizedY);
|
||||
return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<path
|
||||
key={trial.trial_number}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={getLineColor(trial, isHovered, isSelected)}
|
||||
strokeWidth={isHighlighted ? 3 : 1.5}
|
||||
opacity={
|
||||
selectedTrials.size > 0
|
||||
? (isSelected ? 0.95 : 0.1)
|
||||
: (isHighlighted ? 0.95 : 0.5)
|
||||
}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-150 cursor-pointer"
|
||||
style={{
|
||||
filter: isHighlighted ? 'drop-shadow(0 2px 4px rgba(0,0,0,0.2))' : 'none'
|
||||
}}
|
||||
onMouseEnter={() => setHoveredTrial(trial.trial_number)}
|
||||
onMouseLeave={() => setHoveredTrial(null)}
|
||||
onClick={() => toggleTrial(trial.trial_number)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Axis label */}
|
||||
<text
|
||||
y={-10}
|
||||
textAnchor="middle"
|
||||
fill="#94a3b8"
|
||||
fontSize={12}
|
||||
className="select-none"
|
||||
transform={`rotate(-45, 0, -10)`}
|
||||
>
|
||||
{axis.label}
|
||||
{/* Hover tooltip */}
|
||||
{hoveredTrial !== null && (
|
||||
<g transform={`translate(${plotWidth + 20}, 20)`}>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={140}
|
||||
height={70}
|
||||
fill="white"
|
||||
stroke="#D1D5DB"
|
||||
strokeWidth={2}
|
||||
rx={6}
|
||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||
/>
|
||||
<text x={12} y={24} fill="#111827" fontSize={13} fontWeight="700">
|
||||
Trial #{hoveredTrial}
|
||||
</text>
|
||||
|
||||
{/* Min/max labels */}
|
||||
<text
|
||||
y={plotHeight + 15}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].min.toFixed(2)}
|
||||
<text x={12} y={44} fill="#6B7280" fontSize={11}>
|
||||
Click to select/deselect
|
||||
</text>
|
||||
<text
|
||||
y={-25}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].max.toFixed(2)}
|
||||
<text x={12} y={60} fill="#374151" fontSize={10} fontWeight="600">
|
||||
{selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Draw lines for each trial */}
|
||||
{normalizedData.map(trial => {
|
||||
const isHovered = hoveredTrial === trial.trial_number;
|
||||
const isSelected = selectedTrials.has(trial.trial_number);
|
||||
const isHighlighted = isHovered || isSelected;
|
||||
|
||||
// Build path
|
||||
const pathData = axes.map((_, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const normalizedY = normalize(trial.values[i], i);
|
||||
const y = plotHeight * (1 - normalizedY);
|
||||
return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<path
|
||||
key={trial.trial_number}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={
|
||||
isSelected ? '#fbbf24' :
|
||||
trial.feasible ? '#10b981' : '#ef4444'
|
||||
}
|
||||
strokeWidth={isHighlighted ? 2.5 : 1}
|
||||
opacity={
|
||||
selectedTrials.size > 0
|
||||
? (isSelected ? 1 : 0.1)
|
||||
: (isHighlighted ? 1 : 0.4)
|
||||
}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-200 cursor-pointer"
|
||||
onMouseEnter={() => setHoveredTrial(trial.trial_number)}
|
||||
onMouseLeave={() => setHoveredTrial(null)}
|
||||
onClick={() => toggleTrial(trial.trial_number)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Hover tooltip */}
|
||||
{hoveredTrial !== null && (
|
||||
<g transform={`translate(${plotWidth + 10}, 20)`}>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={120}
|
||||
height={60}
|
||||
fill="#1e293b"
|
||||
stroke="#334155"
|
||||
strokeWidth={1}
|
||||
rx={4}
|
||||
/>
|
||||
<text x={10} y={20} fill="#e2e8f0" fontSize={12} fontWeight="bold">
|
||||
Trial #{hoveredTrial}
|
||||
</text>
|
||||
<text x={10} y={38} fill="#94a3b8" fontSize={10}>
|
||||
Click to select
|
||||
</text>
|
||||
<text x={10} y={52} fill="#94a3b8" fontSize={10}>
|
||||
{selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-6 justify-center mt-4 text-sm">
|
||||
<div className="flex gap-8 justify-center mt-6 text-sm border-t border-gray-200 pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-green-400" />
|
||||
<span className="text-dark-200">Feasible</span>
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#10B981' }} />
|
||||
<span className="text-gray-700 font-medium">Feasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-red-400" />
|
||||
<span className="text-dark-200">Infeasible</span>
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#DC2626' }} />
|
||||
<span className="text-gray-700 font-medium">Infeasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-yellow-400" style={{ height: '2px' }} />
|
||||
<span className="text-dark-200">Selected</span>
|
||||
<div className="w-10 h-1.5" style={{ backgroundColor: '#FF6B00' }} />
|
||||
<span className="text-gray-700 font-medium">Selected</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#2563EB' }} />
|
||||
<span className="text-gray-700 font-medium">Hover</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Axis type legend */}
|
||||
<div className="flex gap-6 justify-center mt-3 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#EFF6FF' }} />
|
||||
<span className="text-gray-600">Design Variables</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#F0FDF4' }} />
|
||||
<span className="text-gray-600">Objectives</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#FEF3C7' }} />
|
||||
<span className="text-gray-600">Constraints</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user