use std::collections::HashMap;
use arco::metrics::Estimator;
use arco::substrates::ca::{CAUniverse, generate_ca_hypotheses};
use arco::substrates::graph::{
BinaryGraphUniverse, generate_standard_hypotheses, verify_boolean_functions,
};
use clap::{Args, Parser, Subcommand};
use rand::SeedableRng;
use rand::rngs::StdRng;
use arco::cycle::{CycleConfig, run_cycle};
use arco::record::ResearchRecord;
#[derive(Parser)]
#[command(
name = "arco",
version,
about = "Automated Research into Computational Ontologies"
)]
struct Cli {
#[command(subcommand)]
substrate: Substrate,
}
#[derive(Subcommand)]
enum Substrate {
Graph(GraphArgs),
Ca(CaArgs),
}
#[derive(Args)]
struct SharedArgs {
#[arg(long, default_value = "1000")]
train: usize,
#[arg(long, default_value = "300")]
test: usize,
#[arg(long, default_value = "42")]
seed: u64,
#[arg(long, default_value = "plugin")]
estimator: String,
#[arg(long)]
quick: bool,
#[arg(long)]
output: Option<String>,
}
#[derive(Args)]
struct GraphArgs {
#[command(flatten)]
shared: SharedArgs,
#[arg(long, default_value = "3")]
vertices: usize,
#[arg(long, default_value = "compound")]
obs: String,
}
#[derive(Args)]
struct CaArgs {
#[command(flatten)]
shared: SharedArgs,
#[arg(long, default_value = "8")]
cells: usize,
#[arg(long, default_value = "1")]
radius: usize,
#[arg(long, default_value = "full_state")]
obs: String,
}
fn cycle_config(shared: &SharedArgs) -> CycleConfig {
if shared.quick {
CycleConfig {
n_train: 20,
n_test: 5,
seed: shared.seed,
..CycleConfig::default()
}
} else {
let estimator = match shared.estimator.as_str() {
"mm" => Estimator::MM,
"qe" => Estimator::QE,
"nsb" => Estimator::NSB,
_ => Estimator::Plugin,
};
CycleConfig {
n_train: shared.train,
n_test: shared.test,
seed: shared.seed,
estimator,
..CycleConfig::default()
}
}
}
fn run_graph(args: &GraphArgs) -> ResearchRecord<BinaryGraphUniverse> {
let config = cycle_config(&args.shared);
let mut rng = StdRng::seed_from_u64(args.shared.seed);
let universe = BinaryGraphUniverse::new(
args.vertices,
&args.obs,
&mut rng,
config.n_train + config.n_test,
);
let mut hypotheses = generate_standard_hypotheses();
let boolean_tester =
|rules: &[arco::substrates::graph::RewriteRule]| -> HashMap<String, usize> {
let verified = verify_boolean_functions(rules, 8, 5);
verified.into_iter().map(|name| (name, 1)).collect()
};
run_cycle(&universe, &config, &mut hypotheses, Some(&boolean_tester))
}
fn run_ca(args: &CaArgs) -> ResearchRecord<CAUniverse<8, 1>> {
assert_eq!(args.cells, 8, "Only N=8 is currently supported via CLI");
assert_eq!(args.radius, 1, "Only R=1 is currently supported via CLI");
let config = cycle_config(&args.shared);
let mut rng = StdRng::seed_from_u64(args.shared.seed);
let universe = CAUniverse::<8, 1>::new(&args.obs, &mut rng, config.n_train + config.n_test);
let mut hypotheses = generate_ca_hypotheses::<8, 1>();
run_cycle(&universe, &config, &mut hypotheses, None)
}
fn main() {
let cli = Cli::parse();
match &cli.substrate {
Substrate::Graph(args) => {
let record = run_graph(args);
println!("\n{}", record.summary());
print_spectrum_graph(&record);
if let Some(path) = &args.shared.output {
save_record(&record, path);
}
}
Substrate::Ca(args) => {
let record = run_ca(args);
println!("\n{}", record.summary());
print_spectrum_ca(&record);
if let Some(path) = &args.shared.output {
save_record(&record, path);
}
}
}
}
fn print_spectrum_graph(record: &ResearchRecord<BinaryGraphUniverse>) {
let threshold = record.thresholds.get("storage").copied().unwrap_or(0.0);
let brackets: &[(&str, f64, f64)] = &[
("Noise", 0.00, 0.15),
("Noise-dominated", 0.15, 0.40),
("Balanced", 0.40, 0.60),
("Structure-dominated", 0.60, 0.85),
("Structured", 0.85, 1.01),
];
println!("\nStorage Spectrum:");
println!(
" {:<20} {:<6} {:<8} {:<8}",
"Class", "n", "Stor%", "MeanStor"
);
for (label, low, high) in brackets {
let group: Vec<_> = record
.results
.iter()
.filter(|r| r.structured_ratio >= *low && r.structured_ratio < *high)
.collect();
if group.is_empty() {
continue;
}
let n = group.len();
let stor_pct =
100.0 * group.iter().filter(|r| r.storage > threshold).count() as f64 / n as f64;
let mean_stor = group.iter().map(|r| r.storage).sum::<f64>() / n as f64;
println!(
" {:<20} {:<6} {:<8.1} {:<8.4}",
label, n, stor_pct, mean_stor
);
}
}
fn print_spectrum_ca(record: &ResearchRecord<CAUniverse<8, 1>>) {
let threshold = record.thresholds.get("storage").copied().unwrap_or(0.0);
let brackets: &[(&str, f64, f64)] = &[
("Low structure (0.0--0.3)", 0.0, 0.3),
("High structure (0.7--1.0)", 0.7, 1.0),
];
println!("\nStorage Spectrum:");
println!(
" {:<30} {:<6} {:<8} {:<8}",
"Class", "n", "Stor%", "MeanStor"
);
for (label, low, high) in brackets {
let group: Vec<_> = record
.results
.iter()
.filter(|r| r.structured_ratio >= *low && r.structured_ratio < *high)
.collect();
if group.is_empty() {
continue;
}
let n = group.len();
let stor_pct =
100.0 * group.iter().filter(|r| r.storage > threshold).count() as f64 / n as f64;
let mean_stor = group.iter().map(|r| r.storage).sum::<f64>() / n as f64;
println!(
" {:<30} {:<6} {:<8.1} {:<8.4}",
label, n, stor_pct, mean_stor
);
}
}
#[cfg(feature = "serialize")]
fn save_record<T: serde::Serialize>(record: &T, path: &str) {
let json = serde_json::to_string_pretty(record).expect("Failed to serialize research record");
std::fs::write(path, json).expect("Failed to write output file");
}
#[cfg(not(feature = "serialize"))]
fn save_record<T>(_record: &T, path: &str) {
eprintln!(
"Error: --output '{}' requires the 'serialize' feature.",
path
);
eprintln!("Rebuild with: cargo build --release --features serialize");
std::process::exit(1);
}