Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/constraints/compiled_dispatch_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ fn compiled_precedence_never_decodes_generic_rows_or_plan_fields() {
let baseline = set.initialize_all(&solution);
assert_eq!(baseline, DynamicScore::hard_soft(0, -5));
assert!(set.cached_rows.is_none());
assert_eq!(set.on_retract_all(&solution, 0, 0), DynamicScore::zero());
// Scoring reads no family, so this thread needs no solve scope.
assert_eq!(set.on_retract_all(&solution, 0, 0), DynamicScore::ZERO);
// A cached delta must not parse even precedence-specific immutable fields.
let plan = set.compiled.plans()[0]
.python_plan()
Expand Down
3 changes: 1 addition & 2 deletions src/constraints/list_precedence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1013,8 +1013,7 @@ fn precedence_score_for_family(
let soft = makespan.saturating_neg();
match score_family {
"soft" => DynamicScore::soft(soft.saturating_add(hard)),
"hard_soft_decimal" => DynamicScore::hard_soft_decimal(hard, soft),
"hard_medium_soft" => DynamicScore::hard_medium_soft(hard, 0, soft),
"hard_medium_soft" => DynamicScore::of(hard, 0, soft),
_ => DynamicScore::hard_soft(hard, soft),
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/constraints/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::compiled::{
use super::evaluate;
use super::list_precedence::{self, ListPrecedenceDeltaMode, ListPrecedenceStateCache};
use crate::error::{panic_with_py_err, py_err};
use crate::score::{dynamic_score_from_native, DynamicScore};
use crate::score::{dynamic_score_from_native, score_family_from_name, DynamicScore};
use crate::state::PyDynamicSolution;
use crate::value::DynamicValue;

Expand Down Expand Up @@ -354,8 +354,11 @@ impl PyDynamicConstraintSet {
}

pub fn from_solution(py: Python<'_>, solution: &PyDynamicSolution) -> PyResult<Self> {
// Rejects an undeclared family before any constraint is compiled.
score_family_from_name(&solution.schema().score_family)?;
let compiled = CompiledConstraintSet::from_solution(py, solution)?;
Ok(Self {
compiled: CompiledConstraintSet::from_solution(py, solution)?,
compiled,
cached_rows: None,
join_key_cache: JoinKeyCache::new(),
join_index_cache: JoinIndexCache::new(),
Expand Down Expand Up @@ -806,7 +809,6 @@ fn key_from_value(value: &DynamicValue) -> PyResult<Option<JoinKey>> {
fn score_times(score: DynamicScore, count: usize) -> DynamicScore {
let count = i64::try_from(count).unwrap_or(i64::MAX);
DynamicScore {
family: score.family,
hard: score.hard.saturating_mul(count),
medium: score.medium.saturating_mul(count),
soft: score.soft.saturating_mul(count),
Expand Down
1 change: 1 addition & 0 deletions src/manager/candidate_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,5 +384,6 @@ fn disposition_name(disposition: CandidateTraceDisposition) -> &'static str {
CandidateTraceDisposition::ForagerIgnored => "forager_ignored",
CandidateTraceDisposition::Selected => "selected",
CandidateTraceDisposition::Applied => "applied",
CandidateTraceDisposition::DiscardedSpeculative => "discarded_speculative",
}
}
1 change: 1 addition & 0 deletions src/manager/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ fn telemetry_to_python(py: Python<'_>, telemetry: &SolverTelemetry) -> PyResult<
dict.set_item("step_count", telemetry.step_count)?;
dict.set_item("moves_generated", telemetry.moves_generated)?;
dict.set_item("moves_evaluated", telemetry.moves_evaluated)?;
dict.set_item("moves_speculative", telemetry.moves_speculative)?;
dict.set_item("moves_accepted", telemetry.moves_accepted)?;
dict.set_item("moves_applied", telemetry.moves_applied)?;
dict.set_item("moves_score_improving", telemetry.moves_score_improving)?;
Expand Down
24 changes: 8 additions & 16 deletions src/score.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyTuple};
use solverforge_core::score::Score;

use crate::error::py_err;

Expand Down Expand Up @@ -42,7 +41,7 @@ pub fn dynamic_score_from_dict(dict: &Bound<'_, PyDict>) -> PyResult<DynamicScor
.iter()
.map(|item| item.extract::<i64>())
.collect::<PyResult<Vec<_>>>()?;
score_from_levels(family.as_deref(), numbers.as_slice(), None)
score_from_levels(family.as_deref(), numbers.as_slice())
}

fn dynamic_score_from_sequence<'py>(
Expand All @@ -51,33 +50,26 @@ fn dynamic_score_from_sequence<'py>(
let numbers = items
.map(|item| item.extract::<i64>())
.collect::<PyResult<Vec<_>>>()?;
score_from_levels(None, numbers.as_slice(), Some(DynamicScore::zero().family))
score_from_levels(None, numbers.as_slice())
}

fn score_from_levels(
family: Option<&str>,
numbers: &[i64],
preferred_family: Option<ScoreFamily>,
) -> PyResult<DynamicScore> {
fn score_from_levels(family: Option<&str>, numbers: &[i64]) -> PyResult<DynamicScore> {
Ok(match (family, numbers) {
(Some("soft"), [soft]) => DynamicScore::soft(*soft),
(Some("hard_soft"), [hard, soft]) => DynamicScore::hard_soft(*hard, *soft),
(Some("hard_soft_decimal"), [hard, soft]) => DynamicScore::hard_soft_decimal(*hard, *soft),
(Some("hard_medium_soft"), [hard, medium, soft]) => {
DynamicScore::hard_medium_soft(*hard, *medium, *soft)
// Decimal levels arrive already scaled; they share the two-level value.
(Some("hard_soft" | "hard_soft_decimal"), [hard, soft]) => {
DynamicScore::hard_soft(*hard, *soft)
}
(Some("hard_medium_soft"), [hard, medium, soft]) => DynamicScore::of(*hard, *medium, *soft),
(Some(family), _) => {
return Err(py_err(format!(
"score family `{family}` is incompatible with {} levels",
numbers.len()
)));
}
(None, [soft]) => DynamicScore::soft(*soft),
(None, [hard, soft]) if preferred_family == Some(ScoreFamily::HardSoftDecimal) => {
DynamicScore::hard_soft_decimal(*hard, *soft)
}
(None, [hard, soft]) => DynamicScore::hard_soft(*hard, *soft),
(None, [hard, medium, soft]) => DynamicScore::hard_medium_soft(*hard, *medium, *soft),
(None, [hard, medium, soft]) => DynamicScore::of(*hard, *medium, *soft),
(None, other) => {
return Err(py_err(format!(
"unsupported dynamic score level count {}",
Expand Down
44 changes: 26 additions & 18 deletions src/solver/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::error::{panic_to_py_err, py_err};
use crate::runtime::dynamic_assignment_group::validate_assignment_construction_groups;
use crate::schema::compiled::CompiledSchema;
use crate::score::{scoped_dynamic_score_family, score_family_from_name, DynamicScorePythonExt};
use crate::solver::solvable::dynamic_constraints;
use crate::state::marshal::{export_solution, import_solution};
use crate::state::PyDynamicSolution;

Expand Down Expand Up @@ -58,28 +59,35 @@ pub fn solve(
let py_solution = solution.bind(py);
let mut imported = import_solution(py_solution, Arc::clone(&plan))?;
let descriptor = plan.descriptor().clone();
let constraints = PyDynamicConstraintSet::from_solution(py, &imported)?;
validate_assignment_construction_groups(&solver_config, parsed)?;
imported.solver_config = solver_config.clone();
let score_family = score_family_from_name(&parsed.score_family)?;
let mut solved = catch_unwind(AssertUnwindSafe(|| {
scoped_dynamic_score_family(score_family, || {
try_run_dynamic_solver_with_config_parts(
imported,
constraints,
descriptor,
dynamic_entity_count,
SolverRuntime::detached(),
solver_config,
30,
dynamic_log_scale,
None,
plan.model().clone(),
)
.map_err(|error| py_err(error.to_string()))
let model = plan.model().clone();
// The solve runs without the GIL: parallel evaluation scores evaluator
// replicas on Rayon workers that attach to Python themselves, and a
// calling thread that kept the GIL while waiting on them would never let
// them in.
let mut solved = py
.detach(|| {
catch_unwind(AssertUnwindSafe(|| {
scoped_dynamic_score_family(score_family, || {
try_run_dynamic_solver_with_config_parts(
imported,
dynamic_constraints,
descriptor,
dynamic_entity_count,
SolverRuntime::detached(),
solver_config,
30,
dynamic_log_scale,
None,
model,
)
.map_err(|error| py_err(error.to_string()))
})
}))
})
}))
.map_err(panic_to_py_err)??;
.map_err(panic_to_py_err)??;
solved.refresh_all_shadows()?;
export_solution(py_solution, &solved)?;
if let Some(score) = solved.score {
Expand Down
13 changes: 9 additions & 4 deletions src/solver/solvable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,12 @@ impl Solvable for PyDynamicSolution {
let descriptor = runtime_plan.descriptor().clone();
let model = runtime_plan.model().clone();
let default_time_limit_secs = runtime_plan.default_time_limit_secs();
let constraints = pyo3::Python::attach(|py| {
PyDynamicConstraintSet::from_solution(py, &self).unwrap_or_else(panic_with_py_err)
});
let score_family = score_family_from_name(&runtime_plan.schema().score_family)
.unwrap_or_else(panic_with_py_err);
let result = scoped_dynamic_score_family(score_family, || {
try_run_dynamic_solver_with_config_parts(
self,
constraints,
dynamic_constraints,
descriptor,
dynamic_entity_count,
runtime,
Expand All @@ -44,6 +41,14 @@ impl Solvable for PyDynamicSolution {
}
}

/// Builds the constraint set for one solution: the working solution and every
/// evaluator replica use this same constructor.
pub(crate) fn dynamic_constraints(solution: &PyDynamicSolution) -> PyDynamicConstraintSet {
pyo3::Python::attach(|py| {
PyDynamicConstraintSet::from_solution(py, solution).unwrap_or_else(panic_with_py_err)
})
}

fn dynamic_entity_count(solution: &PyDynamicSolution, descriptor_index: usize) -> usize {
solution.entity_count(descriptor_index)
}
Expand Down
Loading
Loading