|
| 1 | +--- |
| 2 | +title: "SolverForge 0.5.0: Zero-Erasure Constraint Solving" |
| 3 | +date: 2026-01-15 |
| 4 | +draft: false |
| 5 | +tags: [rust, release] |
| 6 | +description: > |
| 7 | + Introducing SolverForge 0.5.0 - a general-purpose constraint solver written in native Rust with zero-erasure architecture and the SERIO incremental scoring engine. |
| 8 | +--- |
| 9 | + |
| 10 | +{{< alert title="Major Release" color="success" >}} |
| 11 | +SolverForge 0.5.0 represents a complete architectural rewrite. It is no longer a WASM compiler or a wrapper around the JVM. |
| 12 | +This is a native Rust constraint solver built from the ground up with zero-erasure design and the SERIO incremental scoring engine. |
| 13 | +{{< /alert >}} |
| 14 | + |
| 15 | +We're excited to announce **SolverForge 0.5.0**, a complete rewrite of SolverForge as a native Rust constraint solver. This isn't a wrapper around an existing solver or a bridge between languages, but a ground-up implementation built on a new architecture powered by the SERIO (Scoring Engine for Real-time Incremental Optimization) engine - our zero-erasure implementation inspired by Timefold's BAVET engine. |
| 16 | + |
| 17 | +After [exploring FFI complexity](/blog/technical/2025/12/30/why-java-interop-is-difficult/), [performance bottlenecks in Python-Java bridges](/blog/technical/2025/12/07/order-picking-quickstart-jpype-performance/) and the [architectural constraints of cross-language constraint solving](/blog/technical/2025/12/06/python-constraint-solver-architecture/), we made a fundamental choice: build something different. |
| 18 | +The result is a general-purpose constraint solver in Rust and it is blazing fast. |
| 19 | + |
| 20 | +While this release is labeled beta as the API continues to mature, SolverForge 0.5.0 is production-capable and represents a major architectural milestone in the project's evolution. |
| 21 | + |
| 22 | +## What is SolverForge? |
| 23 | + |
| 24 | +SolverForge is a constraint solver for planning and scheduling problems. It tackles complex optimization challenges like employee scheduling, vehicle routing, resource allocation, and task assignment—problems where you need to satisfy hard constraints while optimizing for quality metrics. |
| 25 | + |
| 26 | +Inspired by [Timefold](https://timefold.ai/) (formerly OptaPlanner), SolverForge takes a fundamentally different architectural approach centered on **zero-erasure design**. Rather than relying on dynamic dispatch and runtime polymorphism, SolverForge preserves concrete types throughout the solver pipeline, enabling aggressive compiler optimizations and predictable performance characteristics. |
| 27 | + |
| 28 | +At its core is the **SERIO engine**—Scoring Engine for Real-time Incremental Optimization—which efficiently propagates constraint changes through the solution space as the solver explores candidate moves. |
| 29 | + |
| 30 | +## Zero-Erasure Architecture |
| 31 | + |
| 32 | +The zero-erasure philosophy shapes every layer of SolverForge. Here's what it means in practice: |
| 33 | + |
| 34 | +- **No trait objects**: No `Box<dyn Trait>` or `Arc<dyn Trait>` in hot paths |
| 35 | +- **No runtime dispatch**: All generics resolved at compile time via monomorphization |
| 36 | +- **No hidden allocations**: Moves, scores, and constraints are stack-allocated |
| 37 | +- **Predictable performance**: No garbage collection pauses, no vtable lookups |
| 38 | + |
| 39 | +Traditional constraint solvers often use polymorphism to handle different problem types dynamically. This flexibility comes at a cost: heap allocations, pointer indirection, and unpredictable cache behavior. In constraint solving, where the inner loop evaluates millions of moves per second, these costs compound quickly. |
| 40 | + |
| 41 | +SolverForge's zero-erasure design means the compiler knows the concrete types of your entities, variables, scores, and constraints at compile time. It can inline aggressively, eliminate dead code, and generate cache-friendly machine code tailored to your specific problem structure. |
| 42 | + |
| 43 | +```rust |
| 44 | +// Zero-erasure move evaluation - fully monomorphized |
| 45 | +fn evaluate_move<M: Move<Solution>>( |
| 46 | + move_: &M, |
| 47 | + director: &mut TypedScoreDirector<Solution, Score> |
| 48 | +) -> Score { |
| 49 | + // No dynamic dispatch, no allocations, no boxing |
| 50 | + director.do_and_process_move(move_) |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +This isn't just a performance optimization—it fundamentally changes how you reason about solver behavior. Costs are visible in the type system. There are no surprise heap allocations or dynamic dispatch overhead hiding in framework abstractions. |
| 55 | + |
| 56 | +## The SERIO Engine |
| 57 | + |
| 58 | +SERIO—Scoring Engine for Real-time Incremental Optimization—is SolverForge's constraint evaluation engine. It powers the ConstraintStream API, which lets you define constraints declaratively using fluent builders: |
| 59 | + |
| 60 | +```rust |
| 61 | +use solverforge::stream::{ConstraintFactory, joiner}; |
| 62 | + |
| 63 | +fn define_constraints() -> impl ConstraintSet<Schedule, HardSoftScore> { |
| 64 | + let factory = ConstraintFactory::<Schedule, HardSoftScore>::new(); |
| 65 | + |
| 66 | + let required_skill = factory |
| 67 | + .clone() |
| 68 | + .for_each(|s: &Schedule| s.shifts.as_slice()) |
| 69 | + .join( |
| 70 | + |s: &Schedule| s.employees.as_slice(), |
| 71 | + joiner::equal_bi( |
| 72 | + |shift: &Shift| shift.employee_id, |
| 73 | + |emp: &Employee| Some(emp.id), |
| 74 | + ), |
| 75 | + ) |
| 76 | + .filter(|shift: &Shift, emp: &Employee| { |
| 77 | + !emp.skills.contains(&shift.required_skill) |
| 78 | + }) |
| 79 | + .penalize(HardSoftScore::ONE_HARD) |
| 80 | + .as_constraint("Required skill"); |
| 81 | + |
| 82 | + let no_overlap = factory |
| 83 | + .for_each_unique_pair( |
| 84 | + |s: &Schedule| s.shifts.as_slice(), |
| 85 | + joiner::equal(|shift: &Shift| shift.employee_id), |
| 86 | + ) |
| 87 | + .filter(|a: &Shift, b: &Shift| { |
| 88 | + a.employee_id.is_some() && a.start < b.end && b.start < a.end |
| 89 | + }) |
| 90 | + .penalize(HardSoftScore::ONE_HARD) |
| 91 | + .as_constraint("No overlap"); |
| 92 | + |
| 93 | + (required_skill, no_overlap) |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +The key to SERIO's efficiency is **incremental scoring**. When the solver considers a move (like reassigning a shift to a different employee), SERIO doesn't re-evaluate every constraint from scratch. Instead, it tracks which constraint matches are affected by the change and recalculates only those. |
| 98 | + |
| 99 | +Under the zero-erasure design, these incremental updates happen without heap allocations or dynamic dispatch. The constraint evaluation pipeline is fully monomorphized—each constraint stream compiles to specialized code for your exact entity types and filter predicates. |
| 100 | + |
| 101 | +## Developer Experience in 0.5.0 |
| 102 | + |
| 103 | +Version 0.5.0 brings significant improvements to the developer experience, making it easier to define problems and monitor solver progress. |
| 104 | + |
| 105 | +### Fluent API & Macros |
| 106 | + |
| 107 | +Domain models are defined using derive macros that generate the boilerplate: |
| 108 | + |
| 109 | +```rust |
| 110 | +use solverforge::prelude::*; |
| 111 | + |
| 112 | +#[problem_fact] |
| 113 | +pub struct Employee { |
| 114 | + pub id: i64, |
| 115 | + pub name: String, |
| 116 | + pub skills: Vec<String>, |
| 117 | +} |
| 118 | + |
| 119 | +#[planning_entity] |
| 120 | +pub struct Shift { |
| 121 | + #[planning_id] |
| 122 | + pub id: i64, |
| 123 | + pub required_skill: String, |
| 124 | + #[planning_variable] |
| 125 | + pub employee_id: Option<i64>, |
| 126 | +} |
| 127 | + |
| 128 | +#[planning_solution] |
| 129 | +pub struct Schedule { |
| 130 | + #[problem_fact_collection] |
| 131 | + pub employees: Vec<Employee>, |
| 132 | + #[planning_entity_collection] |
| 133 | + pub shifts: Vec<Shift>, |
| 134 | + #[planning_score] |
| 135 | + pub score: Option<HardSoftScore>, |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +The `#[planning_solution]` macro now generates helper methods for basic variable problems, including: |
| 140 | +- Entity count accessors (`shift_count()`, `employee_count()`) |
| 141 | +- List operation methods for manipulating planning entities |
| 142 | +- A `solve()` method that sets up the solver with sensible defaults |
| 143 | + |
| 144 | +This reduces boilerplate and makes simple problems trivial to solve while still allowing full customization for complex scenarios. |
| 145 | + |
| 146 | +### Console Output |
| 147 | + |
| 148 | +With the `console` feature enabled, SolverForge displays beautiful real-time progress: |
| 149 | + |
| 150 | +``` |
| 151 | + ____ _ _____ |
| 152 | +/ ___| ___ | |_ _____ _ __ | ___|__ _ __ __ _ ___ |
| 153 | +\___ \ / _ \| \ \ / / _ \ '__|| |_ / _ \| '__/ _` |/ _ \ |
| 154 | + ___) | (_) | |\ V / __/ | | _| (_) | | | (_| | __/ |
| 155 | +|____/ \___/|_| \_/ \___|_| |_| \___/|_| \__, |\___| |
| 156 | + |___/ |
| 157 | + v0.5.0 - Zero-Erasure Constraint Solver |
| 158 | +
|
| 159 | + 0.000s ▶ Solving │ 14 entities │ 5 values │ scale 9.799 x 10^0 |
| 160 | + 0.001s ▶ Construction Heuristic started |
| 161 | + 0.002s ◀ Construction Heuristic ended │ 1ms │ 14 steps │ 14,000/s │ 0hard/-50soft |
| 162 | + 0.002s ▶ Late Acceptance started |
| 163 | + 1.002s ⚡ 12,456 steps │ 445,000/s │ -2hard/8soft |
| 164 | + 2.003s ⚡ 24,891 steps │ 448,000/s │ 0hard/12soft |
| 165 | + 30.001s ◀ Late Acceptance ended │ 30.00s │ 104,864 steps │ 456,000/s │ 0hard/15soft |
| 166 | + 30.001s ■ Solving complete │ 0hard/15soft │ FEASIBLE |
| 167 | +``` |
| 168 | + |
| 169 | +The `verbose-logging` feature adds DEBUG-level progress updates (approximately once per second during local search), giving insight into solver behavior without overwhelming the terminal. |
| 170 | + |
| 171 | +### Shadow Variables |
| 172 | + |
| 173 | +Shadow variables are derived values that depend on genuine planning variables. For example, in vehicle routing, a vehicle's arrival time at a location depends on which locations come before it in the route. |
| 174 | + |
| 175 | +Version 0.5.0 adds first-class support for shadow variables: |
| 176 | + |
| 177 | +```rust |
| 178 | +#[planning_entity] |
| 179 | +pub struct Visit { |
| 180 | + #[planning_variable] |
| 181 | + pub vehicle_id: Option<i64>, |
| 182 | + |
| 183 | + #[shadow_variable] |
| 184 | + pub arrival_time: Option<i64>, // Computed based on route position |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +The new `ShadowAwareScoreDirector` tracks shadow variable dependencies and updates them automatically when genuine variables change. The `filter_with_solution()` method on uni-streams allows constraints to access shadow variables during evaluation: |
| 189 | + |
| 190 | +```rust |
| 191 | +factory |
| 192 | + .for_each(|s: &Schedule| s.visits.as_slice()) |
| 193 | + .filter_with_solution(|solution: &Schedule, visit: &Visit| { |
| 194 | + // Access shadow variable through solution |
| 195 | + visit.arrival_time.unwrap() > solution.time_window_end |
| 196 | + }) |
| 197 | + .penalize(HardSoftScore::ONE_HARD) |
| 198 | + .as_constraint("Late arrival") |
| 199 | +``` |
| 200 | + |
| 201 | +### Event-Based Solving |
| 202 | + |
| 203 | +The new `solve_with_events()` API provides real-time feedback during solving: |
| 204 | + |
| 205 | +```rust |
| 206 | +use solverforge::{SolverManager, SolverEvent}; |
| 207 | + |
| 208 | +let (job_id, receiver) = SolverManager::global().solve_with_events(schedule); |
| 209 | + |
| 210 | +for event in receiver { |
| 211 | + match event { |
| 212 | + SolverEvent::BestSolutionChanged { solution, score } => { |
| 213 | + println!("New best: {}", score); |
| 214 | + update_dashboard(&solution); |
| 215 | + } |
| 216 | + SolverEvent::PhaseStarted { phase_name } => { |
| 217 | + println!("Starting {}", phase_name); |
| 218 | + } |
| 219 | + SolverEvent::SolvingEnded { final_solution, .. } => { |
| 220 | + println!("Done!"); |
| 221 | + break; |
| 222 | + } |
| 223 | + } |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +This enables building interactive UIs, progress bars, and real-time solution dashboards that update as the solver finds better solutions. |
| 228 | + |
| 229 | +## Phase Builders |
| 230 | + |
| 231 | +SolverForge 0.5.0 introduces fluent builders for configuring solver phases: |
| 232 | + |
| 233 | +```rust |
| 234 | +use solverforge::prelude::*; |
| 235 | + |
| 236 | +let solver = SolverManager::builder() |
| 237 | + .with_phase_factory(|config| { |
| 238 | + vec![ |
| 239 | + Box::new(BasicConstructionPhaseBuilder::new()), |
| 240 | + Box::new(BasicLocalSearchPhaseBuilder::new() |
| 241 | + .with_late_acceptance(400)), |
| 242 | + ] |
| 243 | + }) |
| 244 | + .build()?; |
| 245 | +``` |
| 246 | + |
| 247 | +Available phase builders include: |
| 248 | +- **BasicConstructionPhaseBuilder**: First Fit construction for basic variables |
| 249 | +- **BasicLocalSearchPhaseBuilder**: Hill climbing, simulated annealing, tabu search, late acceptance |
| 250 | +- **ListConstructionPhaseBuilder**: Construction heuristics for list variables |
| 251 | +- **KOptPhaseBuilder**: K-opt local search for tour optimization (TSP, VRP) |
| 252 | + |
| 253 | +Each phase builder integrates with the new stats system (`PhaseStats`, `SolverStats`), providing structured access to solve metrics like step count, score calculation speed, and time spent per phase. |
| 254 | + |
| 255 | +## Breaking Changes |
| 256 | + |
| 257 | +Version 0.5.0 includes one breaking change to enable shadow variable support: |
| 258 | + |
| 259 | +**Solution-aware filter traits**: Uni-stream filters can now optionally access the solution using `filter_with_solution()`. This enables constraints to reference shadow variables and other solution-level computed state. |
| 260 | + |
| 261 | +```rust |
| 262 | +// Before: Filter receives only the entity |
| 263 | +.filter(|shift: &Shift| shift.employee_id.is_some()) |
| 264 | + |
| 265 | +// After: Same syntax still works |
| 266 | +.filter(|shift: &Shift| shift.employee_id.is_some()) |
| 267 | + |
| 268 | +// New: Can also access solution for shadow variables |
| 269 | +.filter_with_solution(|solution: &Schedule, shift: &Shift| { |
| 270 | + // Access shadow variables through solution context |
| 271 | + shift.arrival_time.unwrap() < solution.deadline |
| 272 | +}) |
| 273 | +``` |
| 274 | + |
| 275 | +The standard `filter()` method remains unchanged for simple predicates. Bi/Tri/Quad/Penta stream filters (after joins) continue to receive only the entity tuples without the solution reference. |
| 276 | + |
| 277 | +{{< alert title="Note" color="info" >}} |
| 278 | +The API split between `filter()` and `filter_with_solution()` is temporary. Version 0.5.1 will unify these into a single `filter()` method that accepts both closure signatures, eliminating this distinction. |
| 279 | +{{< /alert >}} |
| 280 | + |
| 281 | +If you're upgrading from 0.4.0 and only using entity-level filters, no changes are required. |
| 282 | + |
| 283 | +## What's Still Beta |
| 284 | + |
| 285 | +{{< alert title="Beta Status" color="warning" >}} |
| 286 | +While SolverForge 0.5.0 is production-capable, some areas are still maturing: |
| 287 | + |
| 288 | +- **API stability**: Core APIs are stable, but we may introduce minor breaking changes based on feedback |
| 289 | +- **Documentation**: API docs are comprehensive, but tutorials and guides are still being developed |
| 290 | +- **Ecosystem**: Quickstarts and examples are growing but not yet comprehensive |
| 291 | +{{< /alert >}} |
| 292 | + |
| 293 | +The [component status table](https://github.com/solverforge/solverforge#component-status) in the README tracks what's complete: |
| 294 | + |
| 295 | +| Component | Status | |
| 296 | +|-----------|--------| |
| 297 | +| Score types | Complete | |
| 298 | +| Domain model macros | Complete | |
| 299 | +| ConstraintStream API | Complete | |
| 300 | +| SERIO incremental scoring | Complete | |
| 301 | +| Construction heuristics | Complete | |
| 302 | +| Local search | Complete | |
| 303 | +| Exhaustive search | Complete | |
| 304 | +| Partitioned search | Complete | |
| 305 | +| VND | Complete | |
| 306 | +| Move system | Complete | |
| 307 | +| Termination | Complete | |
| 308 | +| SolverManager | Complete | |
| 309 | +| SolutionManager | Complete | |
| 310 | +| Console output | Complete | |
| 311 | +| Benchmarking | Complete | |
| 312 | + |
| 313 | +Core solver functionality is complete and well-tested. The beta label reflects that we're still gathering real-world feedback on ergonomics and API design. |
| 314 | + |
| 315 | +## Getting Started |
| 316 | + |
| 317 | +Add SolverForge to your `Cargo.toml`: |
| 318 | + |
| 319 | +```toml |
| 320 | +[dependencies] |
| 321 | +solverforge = { version = "0.5", features = ["console"] } |
| 322 | +``` |
| 323 | + |
| 324 | +Try the **[Employee Scheduling Quickstart](https://github.com/solverforge/solverforge-quickstarts)**, which demonstrates a complete employee scheduling problem with shifts, skills, and availability constraints. It's the fastest way to see SolverForge in action and understand the workflow for defining problems, constraints, and solving. |
| 325 | + |
| 326 | +The quickstarts repository will continue to grow with more examples covering different problem types and solver features. |
| 327 | + |
| 328 | +## Python Bindings Coming Soon |
| 329 | + |
| 330 | +While SolverForge is now a native Rust solver, we remain committed to multi-language accessibility. **Python bindings are under active development** at [github.com/solverforge/solverforge-py](https://github.com/solverforge/solverforge-py) and will be released later this month (late January 2026). |
| 331 | + |
| 332 | +The architectural shift to native Rust was a major undertaking, and we chose to focus on getting the core solver right before building language bridges. The Python bindings will provide idiomatic Python APIs backed by SolverForge's zero-erasure engine, giving Python developers native constraint solving performance with familiar syntax. |
| 333 | + |
| 334 | +This gives us the best of both worlds: predictable, high-performance solving in Rust, with accessible bindings for the broader Python ecosystem. |
| 335 | + |
| 336 | +## What's Next |
| 337 | + |
| 338 | +Beyond Python bindings, the quickstart roadmap includes: |
| 339 | + |
| 340 | +- **Employee Scheduling**: ✓ Available now |
| 341 | +- **Vehicle Routing**: Next in pipeline |
| 342 | +- More domain-specific examples as the ecosystem grows |
| 343 | + |
| 344 | +We're also working on: |
| 345 | +- Expanded documentation and tutorials |
| 346 | +- Additional constraint stream operations |
| 347 | +- Performance benchmarks comparing different solver configurations |
| 348 | +- Community-contributed problem templates |
| 349 | + |
| 350 | +## Looking Ahead |
| 351 | + |
| 352 | +Version 0.5.0 represents a turning point for SolverForge. The zero-erasure architecture and SERIO engine provide a foundation for building a high-performance, accessible constraint solver that works across languages while maintaining Rust's performance and safety guarantees. |
| 353 | + |
| 354 | +We invite you to try SolverForge 0.5.0, explore the [quickstarts](https://github.com/solverforge/solverforge-quickstarts), and share your feedback. Whether you're scheduling employees, routing vehicles, or optimizing resource allocation, SolverForge provides the tools to model and solve your constraints efficiently. |
| 355 | + |
| 356 | +The journey from FFI experiments to native Rust solver has been challenging, but the result is a constraint solver built on solid architectural foundations. We're excited to see what you build with it. |
| 357 | + |
| 358 | +--- |
| 359 | + |
| 360 | +**Further reading:** |
| 361 | +- [SolverForge on GitHub](https://github.com/solverforge/solverforge) |
| 362 | +- [Quickstarts Repository](https://github.com/solverforge/solverforge-quickstarts) |
| 363 | +- [API Documentation](https://docs.rs/solverforge) |
| 364 | +- [Python Bindings (Coming Soon)](https://github.com/solverforge/solverforge-py) |
| 365 | +- [Why Java Interop is Difficult](/blog/technical/2025/12/30/why-java-interop-is-difficult/) |
| 366 | +- [JPype Performance Challenges](/blog/technical/2025/12/07/order-picking-quickstart-jpype-performance/) |
| 367 | +- [Python Architecture Lessons](/blog/technical/2025/12/06/python-constraint-solver-architecture/) |
0 commit comments