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.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# Write your own algorithm
|
||||
|
||||
Implement [`Optimizer<P>`] and you're done. There are no other traits
|
||||
to think about, no internal hooks to register. The example walks
|
||||
through a tiny hill-climber that reads almost identically to the
|
||||
canonical pseudocode.
|
||||
|
||||
## The trait
|
||||
|
||||
```rust,ignore
|
||||
pub trait Optimizer<P>
|
||||
where
|
||||
P: Problem,
|
||||
{
|
||||
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision>;
|
||||
}
|
||||
```
|
||||
|
||||
That's it. You own your config, your RNG, your main loop, and your
|
||||
`OptimizationResult` construction.
|
||||
|
||||
## A minimal hill-climber
|
||||
|
||||
```rust,no_run
|
||||
use heuropt::prelude::*;
|
||||
|
||||
pub struct MyHillClimber<I, V> {
|
||||
pub iterations: usize,
|
||||
pub seed: u64,
|
||||
pub initializer: I,
|
||||
pub variation: V,
|
||||
}
|
||||
|
||||
impl<P, I, V> Optimizer<P> for MyHillClimber<I, V>
|
||||
where
|
||||
P: Problem,
|
||||
P::Decision: Clone,
|
||||
I: Initializer<P::Decision>,
|
||||
V: Variation<P::Decision>,
|
||||
{
|
||||
fn run(&mut self, problem: &P) -> OptimizationResult<P::Decision> {
|
||||
let mut rng = rng_from_seed(self.seed);
|
||||
let objectives = problem.objectives();
|
||||
assert!(objectives.is_single_objective(), "MyHillClimber is single-objective only");
|
||||
|
||||
// Start with one initial decision.
|
||||
let init_decisions = self.initializer.initialize(1, &mut rng);
|
||||
let init = init_decisions.into_iter().next().unwrap();
|
||||
let mut current = Candidate::new(init.clone(), problem.evaluate(&init));
|
||||
let mut evaluations: usize = 1;
|
||||
|
||||
for _ in 0..self.iterations {
|
||||
let children = self.variation.vary(std::slice::from_ref(¤t.decision), &mut rng);
|
||||
for child_decision in children {
|
||||
let child_eval = problem.evaluate(&child_decision);
|
||||
evaluations += 1;
|
||||
let child = Candidate::new(child_decision, child_eval);
|
||||
if better(&child.evaluation, ¤t.evaluation, &objectives) {
|
||||
current = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pareto_front = vec![current.clone()];
|
||||
let best = Some(current.clone());
|
||||
OptimizationResult::new(
|
||||
Population::new(vec![current]),
|
||||
pareto_front,
|
||||
best,
|
||||
evaluations,
|
||||
self.iterations,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn better(a: &Evaluation, b: &Evaluation, objectives: &ObjectiveSpace) -> bool {
|
||||
let am = objectives.as_minimization(&a.objectives);
|
||||
let bm = objectives.as_minimization(&b.objectives);
|
||||
am[0] < bm[0]
|
||||
}
|
||||
```
|
||||
|
||||
## Things to notice
|
||||
|
||||
- **`Rng` is one concrete type.** No generics — call
|
||||
[`rng_from_seed`] and pass `&mut rng` everywhere it's needed.
|
||||
- **`Initializer<D>`** sources the starting point(s).
|
||||
- **`Variation<D>`** generates children from parents. For the
|
||||
hill-climber it's called with one parent.
|
||||
- **`OptimizationResult`** carries the final population, the Pareto
|
||||
front (just the best for single-objective), the best candidate,
|
||||
the total evaluations, and the iteration count.
|
||||
- **`as_minimization`** flips maximize-axis values so your
|
||||
comparison logic only ever needs to deal with "lower is better."
|
||||
|
||||
## Adding parallel evaluation
|
||||
|
||||
If your algorithm batch-evaluates candidates per generation, use the
|
||||
crate's internal helper. From inside heuropt source you can call
|
||||
`evaluate_batch(problem, decisions)`; from outside you'd use rayon
|
||||
directly behind a feature flag, the same way the built-in algorithms
|
||||
do.
|
||||
|
||||
```rust,ignore
|
||||
#[cfg(feature = "parallel")]
|
||||
fn batch_eval<P>(problem: &P, decisions: Vec<P::Decision>) -> Vec<Candidate<P::Decision>>
|
||||
where P: Problem + Sync, P::Decision: Send,
|
||||
{
|
||||
use rayon::prelude::*;
|
||||
decisions.into_par_iter()
|
||||
.map(|d| Candidate::new(d.clone(), problem.evaluate(&d)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "parallel"))]
|
||||
fn batch_eval<P>(problem: &P, decisions: Vec<P::Decision>) -> Vec<Candidate<P::Decision>>
|
||||
where P: Problem,
|
||||
{
|
||||
decisions.into_iter()
|
||||
.map(|d| Candidate::new(d.clone(), problem.evaluate(&d)))
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
To stay bit-identical between serial and parallel modes, keep the
|
||||
RNG and selection on the main thread; only the *evaluations* run in
|
||||
parallel.
|
||||
|
||||
## What's *not* in the trait
|
||||
|
||||
- **No iteration / step API.** The optimizer owns its loop.
|
||||
- **No callbacks.** A future minor release may add an observer hook;
|
||||
for now you'd run the algorithm to completion and process the
|
||||
result.
|
||||
- **No error type.** Invalid configuration panics with a clear
|
||||
message; this matches the style of the built-in algorithms.
|
||||
- **No async.** `evaluate` is synchronous; for async work, drive it
|
||||
on a tokio runtime around the optimizer loop yourself.
|
||||
|
||||
The smallness is the point: you should be able to read a built-in
|
||||
algorithm and write your own in an afternoon. See
|
||||
`examples/custom_optimizer.rs` for a slightly more polished version
|
||||
of the hill-climber above.
|
||||
|
||||
[`Optimizer<P>`]: https://docs.rs/heuropt/latest/heuropt/traits/trait.Optimizer.html
|
||||
[`rng_from_seed`]: https://docs.rs/heuropt/latest/heuropt/core/rng/fn.rng_from_seed.html
|
||||
Reference in New Issue
Block a user