virtuoso-cli 0.2.0

CLI tool to control Cadence Virtuoso from anywhere, locally or remotely
Documentation
use crate::error::{Result, VirtuosoError};
use crate::spec::bandgap::BandgapSpec;
use crate::spectre::batch::{run_batch, BatchJob};
use crate::spectre::jobs::JobStatus;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct OptimState {
    pub optim_id: String,
    pub spec_file: String,
    pub netlist_path: String,
    pub max_iter: u32,
    pub iteration: u32,
    pub status: String,
    pub jobs: Vec<BatchJob>,
    pub best: Option<BestResult>,
    pub corner: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BestResult {
    pub iteration: u32,
    pub params: HashMap<String, f64>,
    pub raw_dir: String,
}

impl OptimState {
    fn dir() -> PathBuf {
        let dir = dirs::cache_dir()
            .unwrap_or_else(|| PathBuf::from("/tmp"))
            .join("virtuoso_bridge")
            .join("optim");
        let _ = fs::create_dir_all(&dir);
        dir
    }

    fn path(id: &str) -> PathBuf {
        Self::dir().join(format!("{id}.json"))
    }

    fn save(&self) -> Result<()> {
        let json = serde_json::to_string_pretty(self)
            .map_err(|e| VirtuosoError::Execution(e.to_string()))?;
        fs::write(Self::path(&self.optim_id), json)
            .map_err(|e| VirtuosoError::Execution(e.to_string()))
    }

    fn load(id: &str) -> Result<Self> {
        let path = Self::path(id);
        let json = fs::read_to_string(&path)
            .map_err(|_| VirtuosoError::NotFound(format!("optim job '{id}' not found")))?;
        serde_json::from_str(&json)
            .map_err(|e| VirtuosoError::Execution(format!("bad optim file: {e}")))
    }
}

pub fn run(spec_file: &str, netlist_file: &str, max_iter: u32, timeout: u64) -> Result<Value> {
    let spec = BandgapSpec::from_file(spec_file)?;
    let template = fs::read_to_string(netlist_file)
        .map_err(|e| VirtuosoError::Config(format!("cannot read netlist '{netlist_file}': {e}")))?;

    let combos = spec.param_combos();
    if combos.is_empty() {
        return Err(VirtuosoError::Config(
            "spec produces no parameter combinations".into(),
        ));
    }

    let optim_id = format!("bg-{}", &uuid::Uuid::new_v4().to_string()[..6]);
    let jobs = run_batch(&template, combos, timeout)?;

    let completed = jobs
        .iter()
        .filter(|j| j.status == JobStatus::Completed)
        .count();
    let failed = jobs.len() - completed;
    let status = if failed == 0 {
        "completed"
    } else if completed == 0 {
        "failed"
    } else {
        "partial"
    };

    let best = jobs
        .iter()
        .filter(|j| j.status == JobStatus::Completed)
        .find_map(|j| {
            j.raw_dir.clone().map(|raw_dir| BestResult {
                iteration: 0,
                params: j.params.clone(),
                raw_dir,
            })
        });

    let total = jobs.len();
    let state = OptimState {
        optim_id: optim_id.clone(),
        spec_file: spec_file.to_string(),
        netlist_path: netlist_file.to_string(),
        max_iter,
        iteration: 1,
        status: status.to_string(),
        jobs,
        best,
        corner: spec.corner,
    };
    state.save()?;

    Ok(serde_json::json!({
        "optim_id": optim_id,
        "spec_file": spec_file,
        "corner": state.corner,
        "iteration": 1,
        "max_iter": max_iter,
        "status": status,
        "total_jobs": total,
        "completed": completed,
        "failed": failed,
        "best": state.best,
    }))
}

pub fn status(optim_id: &str) -> Result<Value> {
    let state = OptimState::load(optim_id)?;
    let completed = state
        .jobs
        .iter()
        .filter(|j| j.status == JobStatus::Completed)
        .count();
    let failed = state.jobs.len() - completed;
    Ok(serde_json::json!({
        "optim_id": optim_id,
        "status": state.status,
        "iteration": state.iteration,
        "max_iter": state.max_iter,
        "total_jobs": state.jobs.len(),
        "completed": completed,
        "failed": failed,
        "best": state.best,
        "corner": state.corner,
    }))
}

pub fn report(optim_id: &str, output: Option<&str>) -> Result<Value> {
    let state = OptimState::load(optim_id)?;

    let mut md = String::new();
    md.push_str("# Bandgap Optimization Report\n\n");
    let _ = writeln!(md, "**Optim ID:** `{}`  ", state.optim_id);
    let _ = writeln!(md, "**Spec:** {}  ", state.spec_file);
    let _ = writeln!(md, "**Corner:** {}  ", state.corner);
    let _ = writeln!(md, "**Status:** {}  \n", state.status);

    md.push_str("## Iteration Summary\n\n");
    let _ = write!(
        md,
        "Iteration {}/{}{} jobs total\n\n",
        state.iteration,
        state.max_iter,
        state.jobs.len()
    );

    md.push_str("## Parameter Sweep Results\n\n");
    md.push_str("| Status | Params | Raw Dir |\n");
    md.push_str("|--------|--------|--------|\n");
    for job in &state.jobs {
        let params_str: Vec<String> = job
            .params
            .iter()
            .map(|(k, v)| format!("{k}={v:.3e}"))
            .collect();
        let raw = job.raw_dir.as_deref().unwrap_or("-");
        let _ = writeln!(
            md,
            "| {} | {} | {} |",
            serde_json::to_value(job.status)
                .ok()
                .and_then(|v| v.as_str().map(str::to_owned))
                .unwrap_or_default(),
            params_str.join(", "),
            raw
        );
    }

    if let Some(ref best) = state.best {
        md.push_str("\n## Best Result\n\n");
        let _ = writeln!(md, "**Iteration:** {}  ", best.iteration);
        for (k, v) in &best.params {
            let _ = writeln!(md, "**{k}:** {v:.3e}  ");
        }
        let _ = writeln!(md, "**Raw dir:** `{}`  ", best.raw_dir);
    }

    if let Some(path) = output {
        fs::write(path, &md)
            .map_err(|e| VirtuosoError::Execution(format!("cannot write report: {e}")))?;
        Ok(serde_json::json!({"written": path, "optim_id": optim_id}))
    } else {
        Ok(serde_json::json!({"report": md, "optim_id": optim_id}))
    }
}