use crate::{prelude::Solution, constraints::props::Propagators, search::{agenda::Agenda, branch::{split_on_unassigned, SplitOnUnassigned}, mode::Mode}, variables::Vars, variables::views::Context};
const LP_DEBUG: bool = false;
#[doc(hidden)]
pub mod mode;
#[doc(hidden)]
pub mod agenda;
#[doc(hidden)]
pub mod branch;
#[doc(hidden)]
pub mod trail;
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct Space {
pub vars: Vars,
pub props: Propagators,
pub trail: trail::Trail,
pub lp_solver_used: bool,
pub lp_constraint_count: usize,
pub lp_variable_count: usize,
pub lp_stats: Option<crate::lpsolver::types::LpStats>,
}
impl Space {
pub fn get_propagation_count(&self) -> usize {
self.props.get_propagation_count()
}
pub fn get_node_count(&self) -> usize {
self.props.get_node_count()
}
pub fn estimate_memory_mb(&self) -> usize {
let var_memory_bytes = self.vars.count() * 100;
let prop_memory_bytes = self.props.count() * 300;
let base_memory_bytes = 1024;
let csp_memory_mb = (base_memory_bytes + var_memory_bytes + prop_memory_bytes) / (1024 * 1024);
let lp_memory_mb = if let Some(ref lp_stats) = self.lp_stats {
lp_stats.peak_memory_mb as usize
} else {
0
};
(csp_memory_mb + lp_memory_mb).max(1)
}
}
#[doc(hidden)]
pub fn search<M: Mode>(vars: Vars, props: Propagators, mode: M) -> Search<M> {
search_with_timeout(vars, props, mode, None, vec![], crate::variables::domain::float_interval::DEFAULT_FLOAT_PRECISION_DIGITS)
}
#[doc(hidden)]
pub fn search_with_timeout<M: Mode>(
vars: Vars,
props: Propagators,
mode: M,
timeout: Option<std::time::Duration>,
pending_lp_constraints: Vec<crate::lpsolver::csp_integration::LinearConstraint>,
float_precision_digits: i32
) -> Search<M> {
search_with_timeout_and_memory(vars, props, mode, timeout, None, pending_lp_constraints, float_precision_digits)
}
#[doc(hidden)]
pub fn search_with_timeout_and_memory<M: Mode>(
vars: Vars,
props: Propagators,
mode: M,
timeout: Option<std::time::Duration>,
memory_limit_mb: Option<u64>,
pending_lp_constraints: Vec<crate::lpsolver::csp_integration::LinearConstraint>,
float_precision_digits: i32
) -> Search<M> {
let (mut vars, props) = (vars, props);
let mut lp_solver_used = false;
let mut lp_constraint_count = 0;
let mut lp_variable_count = 0;
let mut lp_stats_opt = None;
{
if LP_DEBUG {
eprintln!("LP: Starting with {} AST-extracted constraints (runtime API)", pending_lp_constraints.len());
}
let prop_system = props.extract_linear_system();
if LP_DEBUG {
eprintln!("LP: Found {} propagator constraints (old API)", prop_system.constraints.len());
}
let mut linear_system = crate::lpsolver::csp_integration::LinearConstraintSystem::new();
for constraint in pending_lp_constraints {
linear_system.add_constraint(constraint);
}
for constraint in prop_system.constraints {
linear_system.add_constraint(constraint);
}
if LP_DEBUG {
eprintln!("LP: Extracted {} total constraints, {} variables",
linear_system.constraints.len(),
linear_system.variables.len());
}
let lp_has_objective = if let Some((var_id, minimize)) = mode.lp_objective() {
if LP_DEBUG {
eprintln!("LP: Extracted objective: variable {:?}, minimize={}", var_id, minimize);
}
if let Some(idx) = linear_system.variables.iter().position(|&v| v == var_id) {
let mut coeffs = vec![0.0; linear_system.variables.len()];
coeffs[idx] = 1.0;
linear_system.set_objective(coeffs, minimize);
if LP_DEBUG {
eprintln!("LP: Set objective for variable at index {} (minimize={})", idx, minimize);
}
true
} else {
if LP_DEBUG {
eprintln!("LP: Warning - objective variable {:?} not found in linear system (non-linear problem)", var_id);
}
false
}
} else {
if LP_DEBUG {
eprintln!("LP: No simple variable objective found (might be complex expression)");
}
false
};
let is_suitable = linear_system.is_suitable_for_lp(&vars);
if LP_DEBUG {
eprintln!("LP: is_suitable_for_lp() = {}, lp_has_objective = {}", is_suitable, lp_has_objective);
}
if is_suitable && lp_has_objective {
if LP_DEBUG {
eprintln!("LP: System is suitable for LP with objective, solving...");
}
let lp_problem = linear_system.to_lp_problem(&vars);
if LP_DEBUG {
eprintln!("LP: Problem has {} vars, {} constraints", lp_problem.n_vars, lp_problem.n_constraints);
}
lp_constraint_count = linear_system.constraints.len();
lp_variable_count = linear_system.variables.len();
let tolerance = crate::variables::domain::float_interval::precision_to_step_size(float_precision_digits);
let lp_config = crate::lpsolver::LpConfig {
feasibility_tol: tolerance,
optimality_tol: tolerance,
timeout_ms: timeout.map(|d| d.as_millis() as u64),
max_memory_mb: memory_limit_mb,
..Default::default()
};
match crate::lpsolver::solve_with_config(&lp_problem, &lp_config) {
Ok(solution) => {
if LP_DEBUG {
eprintln!("LP: Solution status = {:?}", solution.status);
}
lp_solver_used = true;
lp_stats_opt = Some(solution.stats.clone());
if solution.status == crate::lpsolver::LpStatus::Infeasible {
if LP_DEBUG {
eprintln!("LP: Problem is infeasible");
}
return Search::Done(None);
}
if LP_DEBUG {
eprintln!("LP: Solution status = {:?}, objective = {}", solution.status, solution.objective);
}
use crate::variables::views::Context;
let mut events = Vec::new();
let mut vars_mut = vars;
{
let mut ctx = Context::new(&mut vars_mut, &mut events);
if crate::lpsolver::csp_integration::apply_lp_solution(&linear_system, &solution, &mut ctx).is_none() {
if LP_DEBUG {
eprintln!("LP: Failed to apply solution (propagation failure)");
}
return Search::Done(None);
}
}
vars = vars_mut;
if LP_DEBUG {
eprintln!("LP: Successfully applied LP bounds");
}
}
Err(e) => {
if LP_DEBUG {
eprintln!("LP: Solver returned error: {:?}", e);
}
}
}
}
}
let agenda = Agenda::with_props(props.get_prop_ids_iter());
if LP_DEBUG {
eprintln!("LP: Starting initial propagation...");
}
let Some((is_stalled, space)) = propagate(Space {
vars,
props,
trail: trail::Trail::new(),
lp_solver_used,
lp_constraint_count,
lp_variable_count,
lp_stats: lp_stats_opt,
}, agenda) else {
if LP_DEBUG {
eprintln!("LP: Initial propagation returned None (infeasible)");
}
return Search::Done(None);
};
if LP_DEBUG {
eprintln!("LP: Initial propagation succeeded, is_stalled={}", is_stalled);
}
if is_stalled {
Search::Stalled(Box::new(DefaultEngine::with_timeout_and_memory(space, mode, timeout, memory_limit_mb)))
} else {
Search::Done(Some(space))
}
}
pub enum Search<M> {
Stalled(Box<DefaultEngine<M>>),
Done(Option<Space>),
}
impl<M> Search<M> {
pub fn get_propagation_count(&self) -> usize {
match self {
Self::Stalled(engine) => engine.get_propagation_count(),
Self::Done(Some(space)) => space.get_propagation_count(),
Self::Done(None) => 0, }
}
pub fn get_node_count(&self) -> usize {
match self {
Self::Stalled(engine) => engine.get_node_count(),
Self::Done(Some(space)) => space.get_node_count(),
Self::Done(None) => 0, }
}
pub fn is_timed_out(&self) -> bool {
match self {
Self::Stalled(engine) => engine.is_timed_out(),
Self::Done(_) => false, }
}
pub fn elapsed_time(&self) -> std::time::Duration {
match self {
Self::Stalled(engine) => engine.elapsed_time(),
Self::Done(_) => std::time::Duration::from_secs(0), }
}
pub fn is_memory_limit_exceeded(&self) -> bool {
match self {
Self::Stalled(engine) => engine.is_memory_limit_exceeded(),
Self::Done(_) => false, }
}
pub fn get_memory_usage_mb(&self) -> usize {
match self {
Self::Stalled(engine) => engine.get_memory_usage_mb(),
Self::Done(_) => 0, }
}
}
impl<M: Mode> Iterator for Search<M> {
type Item = Solution;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Stalled(engine) => engine.next(),
Self::Done(space_opt) => space_opt.take().map(|space| {
let stats = crate::core::solution::SolveStats {
propagation_count: space.get_propagation_count(),
node_count: space.get_node_count(),
solve_time: std::time::Duration::ZERO, variables: space.vars.count(),
constraint_count: space.props.count(),
peak_memory_mb: space.estimate_memory_mb(),
int_variables: space.vars.int_var_count,
bool_variables: space.vars.bool_var_count,
float_variables: space.vars.float_var_count,
set_variables: space.vars.set_var_count,
propagators: space.props.count(),
lp_solver_used: space.lp_solver_used,
lp_constraint_count: space.lp_constraint_count,
lp_variable_count: space.lp_variable_count,
lp_stats: space.lp_stats,
init_time: std::time::Duration::ZERO,
objective: 0.0,
objective_bound: 0.0,
};
space.vars.into_solution_with_stats(stats)
}),
}
}
}
pub struct Engine<M, B> {
branch_iter: B,
stack: Vec<B>, mode: M,
branching_factory: fn(Space) -> B,
current_stats: Option<(usize, usize)>, start_time: std::time::Instant,
timeout_duration: Option<std::time::Duration>,
memory_limit_mb: Option<u64>,
iteration_count: usize,
timeout_check_interval: usize,
cleanup_callbacks: Vec<Box<dyn FnOnce() + Send>>,
is_interrupted: bool,
}
pub type DefaultEngine<M> = Engine<M, SplitOnUnassigned>;
impl<M> DefaultEngine<M> {
pub fn new(space: Space, mode: M) -> Self {
Self {
branch_iter: split_on_unassigned(space),
stack: Vec::new(),
mode,
branching_factory: split_on_unassigned,
current_stats: None,
start_time: std::time::Instant::now(),
timeout_duration: None,
memory_limit_mb: None,
iteration_count: 0,
timeout_check_interval: 10000, cleanup_callbacks: Vec::new(),
is_interrupted: false,
}
}
pub fn with_timeout(space: Space, mode: M, timeout: Option<std::time::Duration>) -> Self {
Self {
branch_iter: split_on_unassigned(space),
stack: Vec::new(),
mode,
branching_factory: split_on_unassigned,
current_stats: None,
start_time: std::time::Instant::now(),
timeout_duration: timeout,
memory_limit_mb: None,
iteration_count: 0,
timeout_check_interval: 10000, cleanup_callbacks: Vec::new(),
is_interrupted: false,
}
}
pub fn with_timeout_and_memory(
space: Space,
mode: M,
timeout: Option<std::time::Duration>,
memory_limit_mb: Option<u64>
) -> Self {
Self {
branch_iter: split_on_unassigned(space),
stack: Vec::new(),
mode,
branching_factory: split_on_unassigned,
current_stats: None,
start_time: std::time::Instant::now(),
timeout_duration: timeout,
memory_limit_mb,
iteration_count: 0,
timeout_check_interval: 10000, cleanup_callbacks: Vec::new(),
is_interrupted: false,
}
}
}
impl<M, B> Drop for Engine<M, B> {
fn drop(&mut self) {
if !self.is_interrupted {
self.trigger_cleanup();
}
}
}
impl<M, B> Engine<M, B> {
pub fn get_propagation_count(&self) -> usize {
self.current_stats.map(|(prop_count, _)| prop_count).unwrap_or(0)
}
pub fn get_node_count(&self) -> usize {
self.current_stats.map(|(_, node_count)| node_count).unwrap_or(0)
}
pub fn is_timed_out(&self) -> bool {
if let Some(timeout_duration) = self.timeout_duration {
self.start_time.elapsed() >= timeout_duration
} else {
false
}
}
pub fn elapsed_time(&self) -> std::time::Duration {
self.start_time.elapsed()
}
pub fn is_memory_limit_exceeded(&self) -> bool {
if let Some(limit_mb) = self.memory_limit_mb {
self.get_memory_usage_mb() > limit_mb as usize
} else {
false
}
}
pub fn get_memory_usage_mb(&self) -> usize {
let base_memory_kb = 512;
let stack_memory_kb = self.stack.len() * 3;
let current_memory_kb = 2;
let iteration_memory_kb = (self.iteration_count / 10000) * 5;
((base_memory_kb + stack_memory_kb + current_memory_kb + iteration_memory_kb) / 1024).max(1)
}
pub fn register_cleanup<F>(&mut self, cleanup: F)
where
F: FnOnce() + Send + 'static
{
self.cleanup_callbacks.push(Box::new(cleanup));
}
fn trigger_cleanup(&mut self) {
if !self.is_interrupted {
self.is_interrupted = true;
for cleanup in self.cleanup_callbacks.drain(..) {
cleanup();
}
}
}
pub fn is_interrupted(&self) -> bool {
self.is_interrupted
}
}
impl<M: Mode, B: Iterator<Item = (Space, crate::constraints::props::PropId)>> Iterator for Engine<M, B> {
type Item = Solution;
fn next(&mut self) -> Option<Self::Item> {
loop {
self.iteration_count += 1;
if self.iteration_count % self.timeout_check_interval == 0 {
if let Some(timeout_duration) = self.timeout_duration {
if self.start_time.elapsed() >= timeout_duration {
self.trigger_cleanup();
return None;
}
}
if let Some(limit_mb) = self.memory_limit_mb {
if self.get_memory_usage_mb() > limit_mb as usize {
self.trigger_cleanup();
return None;
}
}
}
while let Some((mut space, p)) = self.branch_iter.next() {
space.props.increment_node_count();
let agenda =
Agenda::with_props(self.mode.on_branch(&mut space).chain(core::iter::once(p)));
if let Some((is_stalled, space)) = propagate(space, agenda) {
self.current_stats = Some((space.get_propagation_count(), space.get_node_count()));
if is_stalled {
let current_iter = std::mem::replace(&mut self.branch_iter, (self.branching_factory)(space));
self.stack.push(current_iter);
continue; } else {
self.mode.on_solution(&space.vars);
let stats = crate::core::solution::SolveStats {
propagation_count: space.get_propagation_count(),
node_count: space.get_node_count(),
solve_time: std::time::Duration::ZERO, variables: space.vars.count(),
constraint_count: space.props.count(),
peak_memory_mb: space.estimate_memory_mb(),
int_variables: space.vars.int_var_count,
bool_variables: space.vars.bool_var_count,
float_variables: space.vars.float_var_count,
set_variables: space.vars.set_var_count,
propagators: space.props.count(),
lp_solver_used: space.lp_solver_used,
lp_constraint_count: space.lp_constraint_count,
lp_variable_count: space.lp_variable_count,
lp_stats: space.lp_stats,
init_time: std::time::Duration::ZERO,
objective: 0.0,
objective_bound: 0.0,
};
return Some(space.vars.into_solution_with_stats(stats));
}
}
}
if let Some(parent_iter) = self.stack.pop() {
self.branch_iter = parent_iter;
} else {
return None;
}
}
}
}
#[doc(hidden)]
pub fn propagate(mut space: Space, mut agenda: Agenda) -> Option<(bool, Space)> {
let mut events = Vec::with_capacity(16);
while let Some(p) = agenda.pop() {
space.props.increment_propagation_count();
let prop = space.props.get_state(p);
let mut ctx = Context::new(&mut space.vars, &mut events);
let result = prop.as_ref().prune(&mut ctx);
result?;
#[allow(clippy::iter_with_drain)]
for v in events.drain(..) {
for p in space.props.on_bound_change(v) {
agenda.schedule(p);
}
}
}
if space.vars.is_assigned_all() {
Some((false, space))
} else {
Some((true, space))
}
}