Files
swaits fa3f2e8fb0 feat: v0.5.0 — comprehensive documentation release
Theme: documentation and project polish. No public-API changes; this
is the v0.5 release that elevates heuropt's docs/onboarding/governance
to bar-setting status.

Adds:
- mdbook user guide at docs/book/ with intro, getting-started,
  defining-problems, choosing-an-algorithm, cookbook (7 recipes),
  comparison vs other libraries, stability/SemVer, migration guides.
  Deploys to https://swaits.github.io/heuropt/ via .github/workflows/
  docs.yml.
- Runnable rustdoc examples on every algorithm (35 of them), all
  exercised by cargo test --doc.
- Three real-world examples: portfolio.rs (multi-obj with budget
  constraint), hyperparam_tuning.rs (BO + TPE), scheduling.rs
  (permutation via SA + SwapMutation against Smith's-rule oracle).
- Governance: CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md
  (adopting builderscode.org's Builder's Code of Conduct), GitHub
  issue templates, PR template.

Polishes:
- README hero with badges + user-guide link.
- lib.rs crate-level docs.
- CHANGELOG entry for 0.5.0.

Bumps Cargo.toml to 0.5.0.
2026-05-05 14:33:12 -06:00

133 lines
4.2 KiB
Rust

//! Single-machine job-shop scheduling: minimize total weighted
//! completion time given per-job processing times and due-date weights.
//!
//! The decision is a permutation `Vec<usize>` — the order in which
//! jobs are processed. We use `SimulatedAnnealing` paired with
//! `SwapMutation` (the standard generic-permutation pair).
//!
//! Demonstrates:
//! - Permutation decisions (`Vec<usize>`).
//! - Simulated annealing with a custom `Initializer` that produces a
//! randomly shuffled identity permutation.
//! - `SwapMutation` preserving the permutation invariant for free.
//!
//! Run with: `cargo run --release --example scheduling`
use heuropt::prelude::*;
/// Single-machine weighted-completion-time problem (1 || Σwᵢ Cᵢ).
struct Scheduling {
/// Processing time for each job.
process_times: Vec<f64>,
/// Importance weight for each job. Higher weight = more
/// punishing if the job finishes late.
weights: Vec<f64>,
}
impl Problem for Scheduling {
type Decision = Vec<usize>;
fn objectives(&self) -> ObjectiveSpace {
ObjectiveSpace::new(vec![Objective::minimize("total_wct")])
}
fn evaluate(&self, schedule: &Vec<usize>) -> Evaluation {
// Compute each job's completion time as the running sum of
// processing times in the chosen order.
let mut clock = 0.0_f64;
let mut total_wct = 0.0_f64;
for &job in schedule {
clock += self.process_times[job];
total_wct += self.weights[job] * clock;
}
Evaluation::new(vec![total_wct])
}
}
/// Initializer that produces a single randomly-shuffled permutation
/// `[0, 1, …, n-1]`. Simulated annealing only needs one initial decision.
struct ShuffledPerm {
n: usize,
}
impl Initializer<Vec<usize>> for ShuffledPerm {
fn initialize(&mut self, _size: usize, rng: &mut Rng) -> Vec<Vec<usize>> {
use rand::seq::SliceRandom;
let mut perm: Vec<usize> = (0..self.n).collect();
perm.shuffle(rng);
vec![perm]
}
}
fn main() {
// 12 jobs. The optimal policy is the Smith's-rule order: sort by
// p_i / w_i ascending (shortest weighted processing time first).
// We can compute that directly to compare against the search result.
let jobs = [
(3.0_f64, 2.0_f64),
(5.0, 1.0),
(2.0, 4.0),
(8.0, 3.0),
(4.0, 5.0),
(1.0, 2.0),
(7.0, 6.0),
(6.0, 1.0),
(3.0, 3.0),
(5.0, 4.0),
(2.0, 2.0),
(4.0, 1.0),
];
let process_times: Vec<f64> = jobs.iter().map(|j| j.0).collect();
let weights: Vec<f64> = jobs.iter().map(|j| j.1).collect();
let n = jobs.len();
let problem = Scheduling {
process_times: process_times.clone(),
weights: weights.clone(),
};
// Smith's rule oracle: sort jobs by p / w ascending.
let mut smith_order: Vec<usize> = (0..n).collect();
smith_order.sort_by(|&a, &b| {
let ra = process_times[a] / weights[a];
let rb = process_times[b] / weights[b];
ra.partial_cmp(&rb).unwrap_or(std::cmp::Ordering::Equal)
});
let smith_score = problem.evaluate(&smith_order).objectives[0];
// Search via simulated annealing with swap mutation.
let mut opt = SimulatedAnnealing::new(
SimulatedAnnealingConfig {
iterations: 5_000,
initial_temperature: 50.0,
final_temperature: 1e-3,
seed: 42,
},
ShuffledPerm { n },
SwapMutation,
);
let result = opt.run(&problem);
let best = result.best.unwrap();
println!("Single-machine weighted completion time, {} jobs", n);
println!();
println!(
"Smith's-rule oracle: {:>8.2} order = {:?}",
smith_score, smith_order
);
println!(
"Simulated annealing best: {:>8.2} order = {:?}",
best.evaluation.objectives[0], best.decision,
);
println!(
"Random initial schedule: {:>8.2} order = {:?}",
problem.evaluate(&(0..n).collect()).objectives[0],
(0..n).collect::<Vec<usize>>(),
);
println!();
println!(
"SA reached optimum (Smith): {}",
(best.evaluation.objectives[0] - smith_score).abs() < 1e-9
);
}