Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,275 @@
/**
* PDECanvas - Interactive 2D canvas for boundary condition editing and solution display
*
* Features:
* - Draw/erase boundary conditions with mouse
* - Visualize input field with colormap
* - Display solution from neural operator inference
*/
import { useRef, useEffect, useCallback, useState } from "react";
import type { ColorMap, BrushTool } from "../../lib/neural-operator-types";
import { getColor } from "../../lib/colors";
interface PDECanvasProps {
/** Grid resolution (width/height) */
resolution: number;
/** Input field values (flattened [H, W]) */
inputField: Float32Array;
/** Solution field values (optional, for display mode) */
solutionField?: Float32Array | null;
/** Whether to show solution instead of input */
showSolution?: boolean;
/** Color map to use */
colorMap: ColorMap;
/** Whether editing is enabled */
editable?: boolean;
/** Current brush tool */
brushTool?: BrushTool;
/** Brush size in pixels */
brushSize?: number;
/** Brush value for drawing (0-1) */
brushValue?: number;
/** Callback when input field is modified */
onFieldChange?: (field: Float32Array) => void;
/** Canvas title/label */
title?: string;
/** Min/max values for colorbar (auto-computed if not provided) */
valueRange?: [number, number];
}
export function PDECanvas({
resolution,
inputField,
solutionField,
showSolution = false,
colorMap,
editable = false,
brushTool = 'draw',
brushSize = 3,
brushValue = 1.0,
onFieldChange,
title,
valueRange,
}: PDECanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [isDrawing, setIsDrawing] = useState(false);
const [canvasSize, setCanvasSize] = useState(400);
// Get the field to display
const displayField = showSolution && solutionField ? solutionField : inputField;
// Compute value range if not provided
const [minVal, maxVal] = valueRange ?? computeRange(displayField);
// Handle window resize for responsive canvas
useEffect(() => {
const updateSize = () => {
if (containerRef.current) {
const width = containerRef.current.clientWidth;
setCanvasSize(Math.min(width - 20, 500));
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
}, []);
// Draw the field on canvas
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const imgData = ctx.createImageData(resolution, resolution);
for (let i = 0; i < displayField.length; i++) {
const val = displayField[i];
const normalized = maxVal > minVal
? (val - minVal) / (maxVal - minVal)
: 0;
const rgb = getColor(normalized, colorMap);
const idx = i * 4;
imgData.data[idx] = rgb[0];
imgData.data[idx + 1] = rgb[1];
imgData.data[idx + 2] = rgb[2];
imgData.data[idx + 3] = 255;
}
ctx.putImageData(imgData, 0, 0);
}, [displayField, resolution, colorMap, minVal, maxVal]);
// Convert canvas coordinates to grid coordinates
const canvasToGrid = useCallback((clientX: number, clientY: number): [number, number] => {
const canvas = canvasRef.current;
if (!canvas) return [0, 0];
const rect = canvas.getBoundingClientRect();
const scaleX = resolution / rect.width;
const scaleY = resolution / rect.height;
const x = Math.floor((clientX - rect.left) * scaleX);
const y = Math.floor((clientY - rect.top) * scaleY);
return [
Math.max(0, Math.min(resolution - 1, x)),
Math.max(0, Math.min(resolution - 1, y)),
];
}, [resolution]);
// Apply brush at position
const applyBrush = useCallback((x: number, y: number) => {
if (!editable || !onFieldChange) return;
const newField = new Float32Array(inputField);
const halfSize = Math.floor(brushSize / 2);
for (let dy = -halfSize; dy <= halfSize; dy++) {
for (let dx = -halfSize; dx <= halfSize; dx++) {
const px = x + dx;
const py = y + dy;
if (px >= 0 && px < resolution && py >= 0 && py < resolution) {
// Check if within circular brush
if (dx * dx + dy * dy <= halfSize * halfSize) {
const idx = py * resolution + px;
if (brushTool === 'draw') {
newField[idx] = brushValue;
} else if (brushTool === 'erase') {
newField[idx] = 0;
}
}
}
}
}
onFieldChange(newField);
}, [editable, inputField, resolution, brushTool, brushSize, brushValue, onFieldChange]);
// Fill entire field
const fillField = useCallback((value: number) => {
if (!editable || !onFieldChange) return;
const newField = new Float32Array(resolution * resolution);
newField.fill(value);
onFieldChange(newField);
}, [editable, resolution, onFieldChange]);
// Mouse event handlers
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (!editable) return;
if (brushTool === 'fill') {
fillField(brushValue);
return;
}
setIsDrawing(true);
const [x, y] = canvasToGrid(e.clientX, e.clientY);
applyBrush(x, y);
}, [editable, brushTool, brushValue, canvasToGrid, applyBrush, fillField]);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!isDrawing || !editable) return;
const [x, y] = canvasToGrid(e.clientX, e.clientY);
applyBrush(x, y);
}, [isDrawing, editable, canvasToGrid, applyBrush]);
const handleMouseUp = useCallback(() => {
setIsDrawing(false);
}, []);
const handleMouseLeave = useCallback(() => {
setIsDrawing(false);
}, []);
return (
<div ref={containerRef} className="pde-canvas-container">
{title && <h4 className="canvas-title">{title}</h4>}
<div className="canvas-wrapper" style={{
backgroundColor: '#1a1a2e',
borderRadius: '8px',
padding: '10px',
}}>
<canvas
ref={canvasRef}
width={resolution}
height={resolution}
style={{
width: `${canvasSize}px`,
height: `${canvasSize}px`,
imageRendering: 'pixelated',
border: '1px solid #333',
cursor: editable
? brushTool === 'fill' ? 'crosshair' : 'crosshair'
: 'default',
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
/>
{/* Colorbar */}
<div className="colorbar" style={{ marginTop: '10px' }}>
<div style={{
height: '20px',
background: generateGradient(colorMap),
borderRadius: '4px',
}} />
<div style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: '12px',
color: '#888',
marginTop: '4px',
}}>
<span>{minVal.toFixed(2)}</span>
<span>{((minVal + maxVal) / 2).toFixed(2)}</span>
<span>{maxVal.toFixed(2)}</span>
</div>
</div>
</div>
</div>
);
}
/** Compute min/max range of a field */
function computeRange(field: Float32Array): [number, number] {
if (field.length === 0) return [0, 1];
let min = field[0];
let max = field[0];
for (let i = 1; i < field.length; i++) {
if (field[i] < min) min = field[i];
if (field[i] > max) max = field[i];
}
// Ensure non-zero range
if (max === min) {
return [min - 0.5, max + 0.5];
}
return [min, max];
}
/** Generate CSS gradient for colorbar */
function generateGradient(colorMap: ColorMap): string {
const stops: string[] = [];
const numStops = 10;
for (let i = 0; i <= numStops; i++) {
const t = i / numStops;
const rgb = getColor(t, colorMap);
stops.push(`rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]}) ${t * 100}%`);
}
return `linear-gradient(to right, ${stops.join(', ')})`;
}
export default PDECanvas;
@@ -0,0 +1,427 @@
/**
* PDEControls - Control panel for Neural Operator demo
*
* Provides controls for:
* - PDE type selection
* - Resolution settings
* - Brush tools for drawing boundary conditions
* - Solve button and metrics display
*/
import { useState, useCallback } from "react";
import type {
PDEType,
ColorMap,
BrushTool,
ModelInfo,
NeuralOperatorMetrics,
TrainingConfig,
TrainingProgress as TrainingProgressType,
} from "../../lib/neural-operator-types";
import { PDE_TYPES, TRAINING_PRESETS } from "../../lib/neural-operator-types";
import { TrainingProgress } from "./TrainingProgress";
interface PDEControlsProps {
/** Current PDE type */
pdeType: PDEType;
/** Callback when PDE type changes */
onPdeTypeChange: (type: PDEType) => void;
/** Current resolution */
resolution: number;
/** Callback when resolution changes */
onResolutionChange: (resolution: number) => void;
/** Whether the model is initialized */
isInitialized: boolean;
/** Whether solve is in progress */
isSolving: boolean;
/** Callback to initialize model */
onInitialize: () => void;
/** Callback to solve PDE */
onSolve: () => void;
/** Callback to reset */
onReset: () => void;
/** Callback to clear input field */
onClearInput: () => void;
/** Model info (if initialized) */
modelInfo?: ModelInfo | null;
/** Performance metrics */
metrics?: NeuralOperatorMetrics | null;
/** Last inference time */
lastInferenceTime?: number;
/** Error message */
error?: string | null;
/** Current brush tool */
brushTool: BrushTool;
/** Callback when brush tool changes */
onBrushToolChange: (tool: BrushTool) => void;
/** Current brush size */
brushSize: number;
/** Callback when brush size changes */
onBrushSizeChange: (size: number) => void;
/** Current brush value */
brushValue: number;
/** Callback when brush value changes */
onBrushValueChange: (value: number) => void;
/** Current colormap */
colorMap: ColorMap;
/** Callback when colormap changes */
onColorMapChange: (colorMap: ColorMap) => void;
/** Whether training is in progress */
isTraining?: boolean;
/** Current training progress */
trainingProgress?: TrainingProgressType | null;
/** Callback to start training */
onStartTraining?: (config: TrainingConfig) => void;
/** Callback to cancel training */
onCancelTraining?: () => void;
}
/** Available resolution options */
const RESOLUTIONS = [32, 64, 128, 256];
/** Available colormaps */
const COLORMAPS: { id: ColorMap; name: string }[] = [
{ id: 'viridis', name: 'Viridis' },
{ id: 'plasma', name: 'Plasma' },
{ id: 'inferno', name: 'Inferno' },
{ id: 'magma', name: 'Magma' },
{ id: 'coolwarm', name: 'Cool-Warm' },
];
export function PDEControls({
pdeType,
onPdeTypeChange,
resolution,
onResolutionChange,
isInitialized,
isSolving,
onInitialize,
onSolve,
onReset,
onClearInput,
modelInfo,
metrics,
lastInferenceTime,
error,
brushTool,
onBrushToolChange,
brushSize,
onBrushSizeChange,
brushValue,
onBrushValueChange,
colorMap,
onColorMapChange,
isTraining = false,
trainingProgress = null,
onStartTraining,
onCancelTraining,
}: PDEControlsProps) {
// Collapsible sections
const [showAdvanced, setShowAdvanced] = useState(false);
const [showTrainingOptions, setShowTrainingOptions] = useState(false);
const [trainingPreset, setTrainingPreset] = useState<'quick' | 'standard' | 'extended'>('quick');
// Get current PDE info
const currentPDE = PDE_TYPES.find((p) => p.id === pdeType);
return (
<div className="control-panel" style={{ width: '280px', flexShrink: 0 }}>
{/* PDE Selection */}
<div className="panel-section">
<h3>PDE Type</h3>
<select
value={pdeType}
onChange={(e) => onPdeTypeChange(e.target.value as PDEType)}
disabled={isInitialized}
style={{ width: '100%', padding: '8px' }}
>
{PDE_TYPES.map((pde) => (
<option key={pde.id} value={pde.id}>
{pde.name}
</option>
))}
</select>
{currentPDE && (
<p style={{ fontSize: '12px', color: '#888', marginTop: '8px' }}>
{currentPDE.description}
</p>
)}
</div>
{/* Resolution */}
<div className="panel-section">
<h3>Resolution</h3>
<select
value={resolution}
onChange={(e) => onResolutionChange(parseInt(e.target.value))}
disabled={isInitialized}
style={{ width: '100%', padding: '8px' }}
>
{RESOLUTIONS.map((res) => (
<option key={res} value={res}>
{res} x {res}
</option>
))}
</select>
</div>
{/* Control Buttons */}
<div className="panel-section">
<h3>Simulation</h3>
{!isInitialized ? (
<button
onClick={onInitialize}
className="primary-button"
style={{ width: '100%', padding: '12px', marginBottom: '8px' }}
>
Initialize Model
</button>
) : (
<>
<button
onClick={onSolve}
disabled={isSolving}
className="primary-button"
style={{ width: '100%', padding: '12px', marginBottom: '8px' }}
>
{isSolving ? 'Solving...' : 'Solve PDE'}
</button>
<button
onClick={onClearInput}
disabled={isSolving}
className="secondary-button"
style={{ width: '100%', padding: '8px', marginBottom: '8px' }}
>
Clear Input
</button>
<button
onClick={onReset}
disabled={isSolving}
className="danger-button"
style={{ width: '100%', padding: '8px' }}
>
Reset Model
</button>
</>
)}
</div>
{/* Brush Tools (when initialized) */}
{isInitialized && (
<div className="panel-section">
<h3>Brush Tools</h3>
<div style={{ display: 'flex', gap: '4px', marginBottom: '12px' }}>
<button
onClick={() => onBrushToolChange('draw')}
className={brushTool === 'draw' ? 'tool-button active' : 'tool-button'}
style={{
flex: 1,
padding: '8px',
backgroundColor: brushTool === 'draw' ? '#4CAF50' : '#2a2a3e',
border: 'none',
borderRadius: '4px',
color: 'white',
cursor: 'pointer',
}}
>
Draw
</button>
<button
onClick={() => onBrushToolChange('erase')}
className={brushTool === 'erase' ? 'tool-button active' : 'tool-button'}
style={{
flex: 1,
padding: '8px',
backgroundColor: brushTool === 'erase' ? '#f44336' : '#2a2a3e',
border: 'none',
borderRadius: '4px',
color: 'white',
cursor: 'pointer',
}}
>
Erase
</button>
<button
onClick={() => onBrushToolChange('fill')}
className={brushTool === 'fill' ? 'tool-button active' : 'tool-button'}
style={{
flex: 1,
padding: '8px',
backgroundColor: brushTool === 'fill' ? '#2196F3' : '#2a2a3e',
border: 'none',
borderRadius: '4px',
color: 'white',
cursor: 'pointer',
}}
>
Fill
</button>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', fontSize: '12px', color: '#888', marginBottom: '4px' }}>
Brush Size: {brushSize}
</label>
<input
type="range"
min="1"
max="20"
value={brushSize}
onChange={(e) => onBrushSizeChange(parseInt(e.target.value))}
style={{ width: '100%' }}
/>
</div>
<div>
<label style={{ display: 'block', fontSize: '12px', color: '#888', marginBottom: '4px' }}>
Brush Value: {brushValue.toFixed(2)}
</label>
<input
type="range"
min="0"
max="1"
step="0.05"
value={brushValue}
onChange={(e) => onBrushValueChange(parseFloat(e.target.value))}
style={{ width: '100%' }}
/>
</div>
</div>
)}
{/* Colormap */}
<div className="panel-section">
<h3>Colormap</h3>
<select
value={colorMap}
onChange={(e) => onColorMapChange(e.target.value as ColorMap)}
style={{ width: '100%', padding: '8px' }}
>
{COLORMAPS.map((cm) => (
<option key={cm.id} value={cm.id}>
{cm.name}
</option>
))}
</select>
</div>
{/* Training Section */}
{isInitialized && onStartTraining && (
<div className="panel-section">
<h3>Model Training</h3>
{isTraining && trainingProgress ? (
<TrainingProgress
progress={trainingProgress}
onCancel={onCancelTraining || (() => {})}
/>
) : (
<>
<button
onClick={() => onStartTraining(TRAINING_PRESETS[trainingPreset])}
disabled={isSolving}
className="primary-button"
style={{
width: '100%',
padding: '12px',
marginBottom: '8px',
backgroundColor: '#FF9800',
}}
>
Train Model
</button>
<button
onClick={() => setShowTrainingOptions(!showTrainingOptions)}
style={{
background: 'none',
border: 'none',
color: '#888',
cursor: 'pointer',
fontSize: '12px',
padding: '4px 0',
}}
>
{showTrainingOptions ? '- Hide' : '+ Show'} Training Options
</button>
{showTrainingOptions && (
<div style={{ marginTop: '8px' }}>
<label style={{ display: 'block', fontSize: '12px', color: '#888', marginBottom: '4px' }}>
Training Preset
</label>
<select
value={trainingPreset}
onChange={(e) => setTrainingPreset(e.target.value as 'quick' | 'standard' | 'extended')}
style={{ width: '100%', padding: '8px', marginBottom: '8px' }}
>
<option value="quick">Quick (10 epochs, 100 samples)</option>
<option value="standard">Standard (50 epochs, 1000 samples)</option>
<option value="extended">Extended (100 epochs, 5000 samples)</option>
</select>
<div style={{ fontSize: '11px', color: '#666' }}>
<p>Epochs: {TRAINING_PRESETS[trainingPreset].epochs}</p>
<p>Batch Size: {TRAINING_PRESETS[trainingPreset].batch_size}</p>
<p>Training Samples: {TRAINING_PRESETS[trainingPreset].n_train_samples}</p>
<p>Validation Samples: {TRAINING_PRESETS[trainingPreset].n_val_samples}</p>
</div>
</div>
)}
</>
)}
</div>
)}
{/* Metrics */}
{isInitialized && (
<div className="panel-section">
<h3>Performance</h3>
{lastInferenceTime !== undefined && lastInferenceTime > 0 && (
<p style={{ color: '#4CAF50' }}>
Last solve: {lastInferenceTime.toFixed(2)} ms
</p>
)}
{metrics && (
<>
<p>Inferences: {metrics.inference_count}</p>
<p>Avg time: {metrics.avg_inference_time_ms.toFixed(2)} ms</p>
</>
)}
</div>
)}
{/* Model Info (Advanced) */}
{isInitialized && modelInfo && (
<div className="panel-section">
<button
onClick={() => setShowAdvanced(!showAdvanced)}
style={{
background: 'none',
border: 'none',
color: '#888',
cursor: 'pointer',
fontSize: '12px',
}}
>
{showAdvanced ? '- Hide' : '+ Show'} Model Details
</button>
{showAdvanced && (
<div style={{ fontSize: '12px', color: '#888', marginTop: '8px' }}>
<p>Model: {modelInfo.name}</p>
<p>Modes: {modelInfo.n_modes[0]} x {modelInfo.n_modes[1]}</p>
<p>Width: {modelInfo.model_width}</p>
<p>Layers: {modelInfo.n_layers}</p>
</div>
)}
</div>
)}
{/* Error Display */}
{error && (
<div className="panel-section error-section">
<h3>Error</h3>
<p style={{ color: '#f44336', fontSize: '12px' }}>{error}</p>
</div>
)}
</div>
);
}
export default PDEControls;
@@ -0,0 +1,297 @@
/**
* TrainingProgress - Progress display for FNO model training
*
* Shows:
* - Current training status (generating data, training, etc.)
* - Epoch and batch progress
* - Loss values (current, best, validation)
* - ETA and speed metrics
* - Mini loss chart
* - Cancel button
*/
import { useMemo } from "react";
import type { TrainingProgress as TrainingProgressType } from "../../lib/neural-operator-types";
interface TrainingProgressProps {
/** Current training progress */
progress: TrainingProgressType;
/** Callback to cancel training */
onCancel: () => void;
}
/**
* Format seconds as human-readable time
*/
function formatTime(seconds: number): string {
if (seconds < 60) {
return `${Math.round(seconds)}s`;
}
if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return `${mins}m ${secs}s`;
}
const hours = Math.floor(seconds / 3600);
const mins = Math.round((seconds % 3600) / 60);
return `${hours}h ${mins}m`;
}
/**
* Get status text and color
*/
function getStatusInfo(status: TrainingProgressType['status']): { text: string; color: string } {
switch (status.status) {
case 'NotStarted':
return { text: 'Not Started', color: '#888' };
case 'GeneratingData':
return { text: 'Generating Training Data...', color: '#2196F3' };
case 'Training':
return { text: 'Training Model', color: '#4CAF50' };
case 'Complete':
return { text: 'Training Complete!', color: '#4CAF50' };
case 'Cancelled':
return { text: 'Training Cancelled', color: '#FF9800' };
case 'Error':
return { text: `Error: ${status.details}`, color: '#f44336' };
}
}
/**
* Mini loss chart component
*/
function LossChart({ losses, width = 200, height = 60 }: { losses: number[]; width?: number; height?: number }) {
const path = useMemo(() => {
if (losses.length < 2) return '';
const maxLoss = Math.max(...losses);
const minLoss = Math.min(...losses);
const range = maxLoss - minLoss || 1;
const points = losses.map((loss, i) => {
const x = (i / (losses.length - 1)) * width;
const y = height - ((loss - minLoss) / range) * height;
return `${x},${y}`;
});
return `M${points.join(' L')}`;
}, [losses, width, height]);
if (losses.length < 2) {
return (
<div style={{ width, height, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#666' }}>
<span style={{ fontSize: '11px' }}>Waiting for data...</span>
</div>
);
}
return (
<svg width={width} height={height} style={{ display: 'block' }}>
<path
d={path}
fill="none"
stroke="#4CAF50"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export function TrainingProgress({ progress, onCancel }: TrainingProgressProps) {
const statusInfo = getStatusInfo(progress.status);
const isActive = progress.status.status === 'GeneratingData' || progress.status.status === 'Training';
const progressPercent = progress.total_epochs > 0
? (progress.epoch / progress.total_epochs) * 100
: 0;
return (
<div
style={{
padding: '16px',
backgroundColor: '#1a1a2e',
borderRadius: '8px',
border: '1px solid #333',
}}
>
{/* Status Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
{isActive && (
<div
style={{
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: statusInfo.color,
animation: 'pulse 1.5s ease-in-out infinite',
}}
/>
)}
<span style={{ color: statusInfo.color, fontWeight: 500 }}>
{statusInfo.text}
</span>
</div>
{/* Data Generation Progress */}
{progress.status.status === 'GeneratingData' && (
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: '#888', marginBottom: '4px' }}>
<span>Generating samples</span>
<span>
{progress.status.details.samples_generated} / {progress.status.details.total_samples}
</span>
</div>
<div style={{ height: '6px', backgroundColor: '#333', borderRadius: '3px', overflow: 'hidden' }}>
<div
style={{
height: '100%',
width: `${(progress.status.details.samples_generated / progress.status.details.total_samples) * 100}%`,
backgroundColor: '#2196F3',
transition: 'width 0.3s ease',
}}
/>
</div>
</div>
)}
{/* Training Progress */}
{(progress.status.status === 'Training' || progress.status.status === 'Complete') && (
<>
{/* Epoch Progress */}
<div style={{ marginBottom: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: '#888', marginBottom: '4px' }}>
<span>Epoch</span>
<span>{progress.epoch} / {progress.total_epochs}</span>
</div>
<div style={{ height: '8px', backgroundColor: '#333', borderRadius: '4px', overflow: 'hidden' }}>
<div
style={{
height: '100%',
width: `${progressPercent}%`,
backgroundColor: '#4CAF50',
transition: 'width 0.3s ease',
}}
/>
</div>
</div>
{/* Loss Values */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
<div style={{ padding: '8px', backgroundColor: '#252540', borderRadius: '4px' }}>
<div style={{ fontSize: '10px', color: '#888', marginBottom: '2px' }}>Current Loss</div>
<div style={{ fontSize: '14px', color: '#fff', fontFamily: 'monospace' }}>
{progress.loss.toExponential(3)}
</div>
</div>
<div style={{ padding: '8px', backgroundColor: '#252540', borderRadius: '4px' }}>
<div style={{ fontSize: '10px', color: '#888', marginBottom: '2px' }}>Best Loss</div>
<div style={{ fontSize: '14px', color: '#4CAF50', fontFamily: 'monospace' }}>
{progress.best_loss.toExponential(3)}
</div>
</div>
{progress.val_loss !== null && (
<div style={{ padding: '8px', backgroundColor: '#252540', borderRadius: '4px', gridColumn: 'span 2' }}>
<div style={{ fontSize: '10px', color: '#888', marginBottom: '2px' }}>Validation Loss</div>
<div style={{ fontSize: '14px', color: '#2196F3', fontFamily: 'monospace' }}>
{progress.val_loss.toExponential(3)}
</div>
</div>
)}
</div>
{/* Loss Chart */}
{progress.loss_history.length > 0 && (
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '10px', color: '#888', marginBottom: '4px' }}>Loss History</div>
<div style={{ padding: '8px', backgroundColor: '#252540', borderRadius: '4px' }}>
<LossChart losses={progress.loss_history} />
</div>
</div>
)}
{/* Device and LR Info */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', marginBottom: '12px' }}>
<div style={{ padding: '8px', backgroundColor: '#252540', borderRadius: '4px' }}>
<div style={{ fontSize: '10px', color: '#888', marginBottom: '2px' }}>Device</div>
<div style={{ fontSize: '12px', color: '#fff', fontFamily: 'monospace' }}>
{progress.device || 'CPU'}
</div>
</div>
<div style={{ padding: '8px', backgroundColor: '#252540', borderRadius: '4px' }}>
<div style={{ fontSize: '10px', color: '#888', marginBottom: '2px' }}>Learning Rate</div>
<div style={{ fontSize: '12px', color: '#FF9800', fontFamily: 'monospace' }}>
{progress.current_lr ? progress.current_lr.toExponential(2) : '-'}
</div>
</div>
</div>
{/* Time Stats */}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: '#888', marginBottom: '12px' }}>
<span>Speed: {progress.samples_per_sec.toFixed(0)} samples/sec</span>
<span>ETA: {formatTime(progress.eta_seconds)}</span>
</div>
<div style={{ fontSize: '11px', color: '#666' }}>
Elapsed: {formatTime(progress.elapsed_seconds)}
</div>
</>
)}
{/* Cancel Button */}
{isActive && (
<button
onClick={onCancel}
style={{
width: '100%',
marginTop: '16px',
padding: '10px',
backgroundColor: 'transparent',
border: '1px solid #f44336',
borderRadius: '4px',
color: '#f44336',
cursor: 'pointer',
fontSize: '14px',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = '#f44336';
e.currentTarget.style.color = 'white';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = '#f44336';
}}
>
Cancel Training
</button>
)}
{/* Completion Message */}
{progress.status.status === 'Complete' && (
<div
style={{
marginTop: '12px',
padding: '12px',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
borderRadius: '4px',
textAlign: 'center',
}}
>
<span style={{ color: '#4CAF50', fontSize: '14px' }}>
Model trained successfully! Click "Solve" to test.
</span>
</div>
)}
{/* Add CSS animation */}
<style>{`
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
`}</style>
</div>
);
}
export default TrainingProgress;
@@ -0,0 +1,7 @@
/**
* Neural Operator Demo Components
*/
export { PDECanvas } from './PDECanvas';
export { PDEControls } from './PDEControls';
export { TrainingProgress } from './TrainingProgress';