Major changes: - Dashboard: WebSocket-based chat with session management - Dashboard: New chat components (ChatPane, ChatInput, ModeToggle) - Dashboard: Enhanced UI with parallel coordinates chart - MCP Server: New atomizer-tools server for Claude integration - Extractors: Enhanced Zernike OPD extractor - Reports: Improved report generator New studies (configs and scripts only): - M1 Mirror: Cost reduction campaign studies - Simple Beam, Simple Bracket, UAV Arm studies Note: Large iteration data (2_iterations/, best_design_archive/) excluded via .gitignore - kept on local Gitea only. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
300 lines
8.9 KiB
TypeScript
300 lines
8.9 KiB
TypeScript
/**
|
|
* Reporting Tools
|
|
*
|
|
* Tools for generating reports and exporting data.
|
|
*/
|
|
|
|
import { execSync } from "child_process";
|
|
import { writeFile } from "fs/promises";
|
|
import { resolve } from "path";
|
|
import Database from "better-sqlite3";
|
|
import { AtomizerTool } from "../index.js";
|
|
import { PYTHON_PATH, ATOMIZER_ROOT, getStudyDir, getStudyDbPath } from "../utils/paths.js";
|
|
|
|
export const reportingTools: AtomizerTool[] = [
|
|
{
|
|
definition: {
|
|
name: "generate_report",
|
|
description:
|
|
"Generate a markdown report for an optimization study, including configuration summary, results analysis, and recommendations.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
study_name: {
|
|
type: "string",
|
|
description: "Name of the study to report on",
|
|
},
|
|
report_type: {
|
|
type: "string",
|
|
enum: ["summary", "detailed", "executive"],
|
|
description:
|
|
"Type of report: summary (quick overview), detailed (full analysis), executive (high-level for stakeholders)",
|
|
},
|
|
include_plots: {
|
|
type: "boolean",
|
|
description: "Whether to generate convergence plots (requires matplotlib)",
|
|
},
|
|
},
|
|
required: ["study_name"],
|
|
},
|
|
},
|
|
handler: async (args) => {
|
|
const studyName = args.study_name as string;
|
|
const reportType = (args.report_type as string) || "summary";
|
|
const includePlots = args.include_plots as boolean;
|
|
const studyDir = getStudyDir(studyName);
|
|
|
|
// Use Python report generator
|
|
const script = `
|
|
import sys
|
|
sys.path.insert(0, r"${ATOMIZER_ROOT}")
|
|
from optimization_engine.reporting.markdown_report import generate_study_report
|
|
|
|
try:
|
|
report = generate_study_report(
|
|
study_name="${studyName}",
|
|
report_type="${reportType}",
|
|
include_plots=${includePlots ? "True" : "False"}
|
|
)
|
|
print(report)
|
|
except Exception as e:
|
|
print(f"ERROR: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
sys.exit(1)
|
|
`;
|
|
|
|
try {
|
|
const output = execSync(`"${PYTHON_PATH}" -c "${script}"`, {
|
|
encoding: "utf-8",
|
|
cwd: studyDir,
|
|
timeout: 120000, // 2 minute timeout for plots
|
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large reports
|
|
});
|
|
|
|
// Save report to file
|
|
const reportPath = resolve(studyDir, "3_results", `STUDY_REPORT_${reportType}.md`);
|
|
try {
|
|
await writeFile(reportPath, output);
|
|
} catch {
|
|
// Ignore save errors - still return the report
|
|
}
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: output,
|
|
},
|
|
],
|
|
};
|
|
} catch (error) {
|
|
// If Python report generator fails, generate a basic report from DB
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
|
|
// Fall back to basic report
|
|
try {
|
|
const basicReport = await generateBasicReport(studyName, reportType);
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: basicReport,
|
|
},
|
|
],
|
|
};
|
|
} catch (fallbackError) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `Error generating report: ${message}`,
|
|
},
|
|
],
|
|
isError: true,
|
|
};
|
|
}
|
|
}
|
|
},
|
|
},
|
|
|
|
{
|
|
definition: {
|
|
name: "export_data",
|
|
description:
|
|
"Export optimization results to CSV or JSON format for external analysis.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
study_name: {
|
|
type: "string",
|
|
description: "Name of the study to export",
|
|
},
|
|
format: {
|
|
type: "string",
|
|
enum: ["csv", "json"],
|
|
description: "Export format",
|
|
},
|
|
include_failed: {
|
|
type: "boolean",
|
|
description: "Whether to include failed trials (default: false)",
|
|
},
|
|
},
|
|
required: ["study_name", "format"],
|
|
},
|
|
},
|
|
handler: async (args) => {
|
|
const studyName = args.study_name as string;
|
|
const format = args.format as string;
|
|
const includeFailed = args.include_failed as boolean;
|
|
const studyDir = getStudyDir(studyName);
|
|
const dbPath = getStudyDbPath(studyName);
|
|
|
|
try {
|
|
const db = new Database(dbPath, { readonly: true });
|
|
|
|
// Get all trial data
|
|
const stateFilter = includeFailed
|
|
? ""
|
|
: "WHERE t.state = 'COMPLETE'";
|
|
|
|
const trials = db
|
|
.prepare(
|
|
`
|
|
SELECT t.number, t.state, t.datetime_start, t.datetime_complete
|
|
FROM trials t
|
|
${stateFilter}
|
|
ORDER BY t.number ASC
|
|
`
|
|
)
|
|
.all() as {
|
|
number: number;
|
|
state: string;
|
|
datetime_start: string;
|
|
datetime_complete: string | null;
|
|
}[];
|
|
|
|
// Get params and values for each trial
|
|
const data = trials.map((trial) => {
|
|
const trialId = db
|
|
.prepare("SELECT trial_id FROM trials WHERE number = ?")
|
|
.get(trial.number) as { trial_id: number };
|
|
|
|
const params = db
|
|
.prepare("SELECT param_name, param_value FROM trial_params WHERE trial_id = ?")
|
|
.all(trialId.trial_id) as { param_name: string; param_value: string }[];
|
|
|
|
const values = db
|
|
.prepare("SELECT objective_id, value FROM trial_values WHERE trial_id = ?")
|
|
.all(trialId.trial_id) as { objective_id: number; value: number }[];
|
|
|
|
return {
|
|
trial_number: trial.number,
|
|
state: trial.state,
|
|
datetime_start: trial.datetime_start,
|
|
datetime_complete: trial.datetime_complete,
|
|
...Object.fromEntries(
|
|
params.map((p) => [p.param_name, parseFloat(p.param_value) || p.param_value])
|
|
),
|
|
...Object.fromEntries(values.map((v) => [`objective_${v.objective_id}`, v.value])),
|
|
};
|
|
});
|
|
|
|
db.close();
|
|
|
|
let output: string;
|
|
let filename: string;
|
|
|
|
if (format === "csv") {
|
|
// Generate CSV
|
|
if (data.length === 0) {
|
|
output = "No data to export";
|
|
} else {
|
|
const headers = Object.keys(data[0]);
|
|
const rows = data.map((row) => headers.map((h) => row[h as keyof typeof row] ?? "").join(","));
|
|
output = [headers.join(","), ...rows].join("\n");
|
|
}
|
|
filename = `${studyName}_export.csv`;
|
|
} else {
|
|
// Generate JSON
|
|
output = JSON.stringify(data, null, 2);
|
|
filename = `${studyName}_export.json`;
|
|
}
|
|
|
|
// Save to file
|
|
const exportPath = resolve(studyDir, "3_results", filename);
|
|
await writeFile(exportPath, output);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `Exported ${data.length} trials to ${exportPath}\n\nPreview (first 5 rows):\n${format === "csv" ? output.split("\n").slice(0, 6).join("\n") : JSON.stringify(data.slice(0, 5), null, 2)}`,
|
|
},
|
|
],
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `Error exporting data: ${message}`,
|
|
},
|
|
],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
// Helper function for basic report generation
|
|
async function generateBasicReport(
|
|
studyName: string,
|
|
reportType: string
|
|
): Promise<string> {
|
|
const dbPath = getStudyDbPath(studyName);
|
|
const db = new Database(dbPath, { readonly: true });
|
|
|
|
// Get trial counts
|
|
const total = (db.prepare("SELECT COUNT(*) as c FROM trials").get() as { c: number }).c;
|
|
const completed = (
|
|
db.prepare("SELECT COUNT(*) as c FROM trials WHERE state = 'COMPLETE'").get() as { c: number }
|
|
).c;
|
|
const failed = (
|
|
db.prepare("SELECT COUNT(*) as c FROM trials WHERE state = 'FAIL'").get() as { c: number }
|
|
).c;
|
|
|
|
// Get best result
|
|
const best = db
|
|
.prepare(
|
|
`
|
|
SELECT t.number, tv.value
|
|
FROM trials t
|
|
JOIN trial_values tv ON t.trial_id = tv.trial_id
|
|
WHERE t.state = 'COMPLETE'
|
|
ORDER BY tv.value ASC
|
|
LIMIT 1
|
|
`
|
|
)
|
|
.get() as { number: number; value: number } | undefined;
|
|
|
|
db.close();
|
|
|
|
let report = `# Optimization Report: ${studyName}\n\n`;
|
|
report += `**Generated:** ${new Date().toISOString()}\n\n`;
|
|
report += `## Summary\n\n`;
|
|
report += `- Total trials: ${total}\n`;
|
|
report += `- Completed: ${completed}\n`;
|
|
report += `- Failed: ${failed}\n`;
|
|
|
|
if (best) {
|
|
report += `\n## Best Result\n\n`;
|
|
report += `- Trial #${best.number}\n`;
|
|
report += `- Objective value: ${best.value.toFixed(6)}\n`;
|
|
}
|
|
|
|
return report;
|
|
}
|