Phase 4 of Canvas Professional Upgrade:
- Add /studies/{id}/nx/introspect endpoint for full model introspection
- Add /studies/{id}/nx/expressions endpoint for expression list
- Add caching to avoid re-running NX journal on each request
- Add frontend API client methods: introspectNxModel, getNxExpressions
- Use existing introspect_part.py extractor
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
import { StudyListResponse, HistoryResponse, PruningResponse, StudyStatus } from '../types';
|
|
|
|
const API_BASE = '/api';
|
|
|
|
export interface OptimizationControlResponse {
|
|
success: boolean;
|
|
message: string;
|
|
pid?: number;
|
|
}
|
|
|
|
export interface ReadmeResponse {
|
|
content: string;
|
|
path: string;
|
|
}
|
|
|
|
export interface ReportResponse {
|
|
content: string;
|
|
generated_at?: string;
|
|
}
|
|
|
|
export interface ConfigResponse {
|
|
config: Record<string, any>;
|
|
objectives: Array<{
|
|
name: string;
|
|
direction: string;
|
|
weight?: number;
|
|
target?: number;
|
|
units?: string;
|
|
}>;
|
|
design_variables: Array<{
|
|
name: string;
|
|
min: number;
|
|
max: number;
|
|
baseline?: number;
|
|
units?: string;
|
|
}>;
|
|
constraints?: Array<{
|
|
name: string;
|
|
type: string;
|
|
max_value?: number;
|
|
min_value?: number;
|
|
units?: string;
|
|
}>;
|
|
}
|
|
|
|
export interface ProcessStatus {
|
|
is_running: boolean;
|
|
is_paused?: boolean;
|
|
pid?: number;
|
|
start_time?: string;
|
|
iteration?: number;
|
|
fea_count?: number;
|
|
nn_count?: number;
|
|
total_trials?: number;
|
|
completed_trials?: number;
|
|
time_per_trial_seconds?: number;
|
|
eta_seconds?: number;
|
|
eta_formatted?: string;
|
|
rate_per_hour?: number;
|
|
}
|
|
|
|
export interface ModelFile {
|
|
name: string;
|
|
path: string;
|
|
extension: string;
|
|
size_bytes: number;
|
|
size_display: string;
|
|
modified: string;
|
|
}
|
|
|
|
export interface ModelFilesResponse {
|
|
study_id: string;
|
|
model_dir: string;
|
|
files: ModelFile[];
|
|
count: number;
|
|
}
|
|
|
|
class ApiClient {
|
|
async getStudies(): Promise<StudyListResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies`);
|
|
if (!response.ok) throw new Error('Failed to fetch studies');
|
|
return response.json();
|
|
}
|
|
|
|
async getStudyStatus(studyId: string): Promise<StudyStatus> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/status`);
|
|
if (!response.ok) throw new Error('Failed to fetch study status');
|
|
return response.json();
|
|
}
|
|
|
|
async getStudyHistory(studyId: string): Promise<HistoryResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/history`);
|
|
if (!response.ok) throw new Error('Failed to fetch study history');
|
|
return response.json();
|
|
}
|
|
|
|
async getStudyPruning(studyId: string): Promise<PruningResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/pruning`);
|
|
if (!response.ok) throw new Error('Failed to fetch pruning data');
|
|
return response.json();
|
|
}
|
|
|
|
async createStudy(config: any): Promise<{ study_id: string }> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
if (!response.ok) throw new Error('Failed to create study');
|
|
return response.json();
|
|
}
|
|
|
|
async getStudyReport(studyId: string): Promise<ReportResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report`);
|
|
if (!response.ok) throw new Error('Failed to fetch report');
|
|
return response.json();
|
|
}
|
|
|
|
async getStudyReadme(studyId: string): Promise<ReadmeResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/readme`);
|
|
if (!response.ok) throw new Error('Failed to fetch README');
|
|
return response.json();
|
|
}
|
|
|
|
async getStudyConfig(studyId: string): Promise<ConfigResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/config`);
|
|
if (!response.ok) throw new Error('Failed to fetch config');
|
|
return response.json();
|
|
}
|
|
|
|
async getConsoleOutput(studyId: string, lines: number = 200): Promise<{
|
|
lines: string[];
|
|
total_lines: number;
|
|
displayed_lines: number;
|
|
log_file: string | null;
|
|
timestamp: string;
|
|
message?: string;
|
|
}> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/console?lines=${lines}`);
|
|
if (!response.ok) throw new Error('Failed to fetch console output');
|
|
return response.json();
|
|
}
|
|
|
|
async getProcessStatus(studyId: string): Promise<ProcessStatus> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/process`);
|
|
if (!response.ok) throw new Error('Failed to fetch process status');
|
|
return response.json();
|
|
}
|
|
|
|
// Control operations
|
|
async startOptimization(studyId: string, options?: {
|
|
freshStart?: boolean;
|
|
maxIterations?: number;
|
|
trials?: number; // For SAT scripts
|
|
feaBatchSize?: number;
|
|
tuneTrials?: number;
|
|
ensembleSize?: number;
|
|
patience?: number;
|
|
}): Promise<OptimizationControlResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/start`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(options || {}),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to start optimization');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async stopOptimization(studyId: string): Promise<OptimizationControlResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/stop`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to stop optimization');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async pauseOptimization(studyId: string): Promise<OptimizationControlResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/pause`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to pause optimization');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async resumeOptimization(studyId: string): Promise<OptimizationControlResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/resume`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to resume optimization');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async validateOptimization(studyId: string, options?: {
|
|
topN?: number;
|
|
}): Promise<OptimizationControlResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/validate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(options || {}),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to start validation');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async generateReport(studyId: string): Promise<ReportResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/report/generate`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to generate report');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
// Optuna dashboard
|
|
async launchOptunaDashboard(studyId: string): Promise<{ url: string; pid: number }> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/optuna-dashboard`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to launch Optuna dashboard');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async checkOptunaAvailable(): Promise<{
|
|
available: boolean;
|
|
path: string | null;
|
|
message: string;
|
|
install_instructions?: string;
|
|
}> {
|
|
const response = await fetch(`${API_BASE}/optimization/optuna-dashboard/check`);
|
|
if (!response.ok) {
|
|
return {
|
|
available: false,
|
|
path: null,
|
|
message: 'Failed to check optuna-dashboard availability',
|
|
install_instructions: 'pip install optuna-dashboard'
|
|
};
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
// Optimizer state
|
|
async getOptimizerState(studyId: string): Promise<{
|
|
available: boolean;
|
|
source?: string;
|
|
phase?: string;
|
|
phase_description?: string;
|
|
phase_progress?: number;
|
|
current_strategy?: string;
|
|
sampler?: {
|
|
name: string;
|
|
description: string;
|
|
};
|
|
objectives?: Array<{
|
|
name: string;
|
|
direction: string;
|
|
current_best?: number;
|
|
unit?: string;
|
|
}>;
|
|
plan?: {
|
|
total_phases: number;
|
|
current_phase: number;
|
|
phases: string[];
|
|
};
|
|
completed_trials?: number;
|
|
total_trials?: number;
|
|
}> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/optimizer-state`);
|
|
if (!response.ok) {
|
|
return { available: false };
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
// Model files
|
|
async getModelFiles(studyId: string): Promise<ModelFilesResponse> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/model-files`);
|
|
if (!response.ok) throw new Error('Failed to fetch model files');
|
|
return response.json();
|
|
}
|
|
|
|
async openFolder(studyId: string, folderType: 'model' | 'results' | 'setup' = 'model'): Promise<{ success: boolean; message: string; path: string }> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/open-folder?folder_type=${folderType}`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Failed to open folder');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async getBestSolution(studyId: string): Promise<{
|
|
study_id: string;
|
|
best_trial: {
|
|
trial_number: number;
|
|
objective: number;
|
|
design_variables: Record<string, number>;
|
|
user_attrs?: Record<string, any>;
|
|
timestamp?: string;
|
|
} | null;
|
|
first_trial: {
|
|
trial_number: number;
|
|
objective: number;
|
|
design_variables: Record<string, number>;
|
|
} | null;
|
|
improvements: Record<string, {
|
|
initial: number;
|
|
final: number;
|
|
improvement_pct: number;
|
|
absolute_change: number;
|
|
}>;
|
|
total_trials: number;
|
|
}> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/best-solution`);
|
|
if (!response.ok) throw new Error('Failed to fetch best solution');
|
|
return response.json();
|
|
}
|
|
|
|
async exportData(studyId: string, format: 'csv' | 'json' | 'config'): Promise<{
|
|
filename?: string;
|
|
content: string;
|
|
content_type?: string;
|
|
study_id?: string;
|
|
total_trials?: number;
|
|
trials?: any[];
|
|
}> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/export/${format}`);
|
|
if (!response.ok) throw new Error(`Failed to export ${format}`);
|
|
return response.json();
|
|
}
|
|
// NX Model introspection
|
|
async introspectNxModel(studyId: string, force: boolean = false): Promise<{
|
|
study_id: string;
|
|
cached: boolean;
|
|
introspection: {
|
|
success: boolean;
|
|
part_file: string;
|
|
expressions: {
|
|
user: Array<{
|
|
name: string;
|
|
value: number;
|
|
units?: string;
|
|
formula?: string;
|
|
}>;
|
|
internal: any[];
|
|
user_count: number;
|
|
total_count: number;
|
|
};
|
|
mass_properties: {
|
|
mass_kg: number;
|
|
mass_g: number;
|
|
volume_mm3: number;
|
|
surface_area_mm2: number;
|
|
center_of_gravity_mm: [number, number, number];
|
|
};
|
|
};
|
|
}> {
|
|
const url = force
|
|
? `${API_BASE}/optimization/studies/${studyId}/nx/introspect?force=true`
|
|
: `${API_BASE}/optimization/studies/${studyId}/nx/introspect`;
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error('Failed to introspect NX model');
|
|
return response.json();
|
|
}
|
|
|
|
async getNxExpressions(studyId: string): Promise<{
|
|
study_id: string;
|
|
expressions: Array<{
|
|
name: string;
|
|
value: number;
|
|
units?: string;
|
|
formula?: string;
|
|
}>;
|
|
count: number;
|
|
}> {
|
|
const response = await fetch(`${API_BASE}/optimization/studies/${studyId}/nx/expressions`);
|
|
if (!response.ok) throw new Error('Failed to get NX expressions');
|
|
return response.json();
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient();
|