Skip to content

Commit a4efa44

Browse files
committed
fix(solver): restore shared assignment rotation
The streamed grouped-scalar rewrite independently permuted every assignment candidate dimension, reducing repeated neighborhood coverage and regressing employee-scheduling quality. Derive one seeded offset per selector step and rotate every dimension by that shared offset without cloning candidate values.
1 parent 1fe1e62 commit a4efa44

12 files changed

Lines changed: 85 additions & 41 deletions

crates/solverforge-solver/src/builder/selector/grouped_scalar.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,20 @@ where
121121

122122
match &group.kind {
123123
crate::builder::ScalarGroupBindingKind::Assignment(assignment) => {
124+
let selection_salt =
125+
0xC0A1_E5CE_AAA0_0002 ^ group.group_name.len() as u64;
126+
let entity_offset = if context.is_canonical() {
127+
0
128+
} else {
129+
context
130+
.offset_seed(selection_salt)
131+
.wrapping_add(context.step_index() as usize)
132+
};
124133
let options = crate::phase::construction::grouped_scalar::ScalarAssignmentMoveOptions::for_selector(
125134
group.limits,
126135
limits.value_candidate_limit,
127136
max_moves_per_step,
128-
context,
137+
entity_offset,
129138
);
130139
self.assignment_cursor = Some(
131140
crate::phase::construction::grouped_scalar::ScalarAssignmentMoveCursor::new(

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_block.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl ValueLongWindowCursor {
5757
pairs.push((index.values[left_pos], index.values[right_pos]));
5858
}
5959
}
60-
super::assignment_candidate::order_candidates(&mut pairs, options, 0xA551_6EED_0000_0012);
60+
super::assignment_candidate::order_candidates(&mut pairs, options);
6161

6262
let base_len = options
6363
.max_rematch_size
@@ -147,7 +147,7 @@ impl ValueWindowCursor {
147147
pairs.push((index.values[left_pos], index.values[right_pos]));
148148
}
149149
}
150-
super::assignment_candidate::order_candidates(&mut pairs, options, 0xA551_6EED_0000_0013);
150+
super::assignment_candidate::order_candidates(&mut pairs, options);
151151

152152
let max_len = options
153153
.max_depth
@@ -241,7 +241,7 @@ impl ValueBlockReassignmentCursor {
241241
}
242242
}
243243
}
244-
super::assignment_candidate::order_candidates(&mut pairs, options, 0xA551_6EED_0000_0014);
244+
super::assignment_candidate::order_candidates(&mut pairs, options);
245245

246246
let max_len = options
247247
.max_depth

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_candidate.rs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
use crate::builder::ScalarAssignmentBinding;
2-
use crate::heuristic::selector::move_selector::MoveStreamContext;
3-
42
#[derive(Clone, Copy, Debug)]
53
pub(crate) struct ScalarAssignmentMoveOptions {
64
pub(crate) value_candidate_limit: Option<usize>,
75
pub(crate) max_moves: usize,
86
pub(crate) max_depth: usize,
97
pub(crate) max_rematch_size: usize,
10-
pub(crate) selection_context: MoveStreamContext,
8+
pub(crate) entity_offset: usize,
119
pub(crate) required_scarcity_ordering: bool,
1210
}
1311

@@ -18,7 +16,7 @@ impl ScalarAssignmentMoveOptions {
1816
max_moves: limits.group_candidate_limit.unwrap_or(usize::MAX),
1917
max_depth: limits.max_augmenting_depth.unwrap_or(3),
2018
max_rematch_size: limits.max_rematch_size.unwrap_or(4).max(2),
21-
selection_context: MoveStreamContext::default(),
19+
entity_offset: 0,
2220
required_scarcity_ordering: true,
2321
}
2422
}
@@ -27,14 +25,14 @@ impl ScalarAssignmentMoveOptions {
2725
limits: crate::builder::ScalarGroupLimits,
2826
value_candidate_limit: Option<usize>,
2927
max_moves_per_step: usize,
30-
selection_context: MoveStreamContext,
28+
entity_offset: usize,
3129
) -> Self {
3230
Self {
3331
value_candidate_limit: value_candidate_limit.or(limits.value_candidate_limit),
3432
max_moves: max_moves_per_step,
3533
max_depth: limits.max_augmenting_depth.unwrap_or(3),
3634
max_rematch_size: limits.max_rematch_size.unwrap_or(4).max(2),
37-
selection_context,
35+
entity_offset,
3836
required_scarcity_ordering: true,
3937
}
4038
}
@@ -105,12 +103,9 @@ where
105103
entities
106104
}
107105

108-
pub(super) fn order_candidates<T: Clone>(
109-
candidates: &mut [T],
110-
options: ScalarAssignmentMoveOptions,
111-
salt: u64,
112-
) {
113-
options
114-
.selection_context
115-
.apply_selection_order_without_replacement(candidates, salt);
106+
pub(super) fn order_candidates<T>(candidates: &mut [T], options: ScalarAssignmentMoveOptions) {
107+
if !candidates.is_empty() {
108+
let len = candidates.len();
109+
candidates.rotate_left(options.entity_offset % len);
110+
}
116111
}

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_cycle.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl CycleWindowCursor {
3030
options: ScalarAssignmentMoveOptions,
3131
) -> Self {
3232
let mut entities = assigned_entities_by_position(group, solution, state);
33-
order_candidates(&mut entities, options, 0xA551_6EED_0000_0010);
33+
order_candidates(&mut entities, options);
3434
let max_len = options.max_rematch_size.min(entities.len()).max(2);
3535
Self {
3636
kind: CycleWindowKind::AugmentingRematch,
@@ -50,7 +50,7 @@ impl CycleWindowCursor {
5050
options: ScalarAssignmentMoveOptions,
5151
) -> Self {
5252
let mut entities = assigned_entities_by_position(group, solution, state);
53-
order_candidates(&mut entities, options, 0xA551_6EED_0000_0011);
53+
order_candidates(&mut entities, options);
5454
let max_len = options.max_rematch_size.min(entities.len()).max(2);
5555
Self {
5656
kind: CycleWindowKind::EjectionReinsert,

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_entity.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ impl OptionalAdjustmentCursor {
240240
!state.is_required(entity_index) && state.current_value(entity_index).is_some()
241241
});
242242
state.sort_entities_by_current_value_pressure(group, solution, &mut targets);
243-
order_candidates(&mut targets, options, 0xA551_6EED_0000_000D);
243+
order_candidates(&mut targets, options);
244244
Self {
245245
kind: OptionalAdjustmentKind::Release,
246246
targets,
@@ -263,8 +263,8 @@ impl OptionalAdjustmentCursor {
263263
!state.is_required(entity_index) && state.current_value(entity_index).is_some()
264264
});
265265
state.sort_entities_by_current_value_pressure(group, solution, &mut sources);
266-
order_candidates(&mut targets, options, 0xA551_6EED_0000_000E);
267-
order_candidates(&mut sources, options, 0xA551_6EED_0000_000F);
266+
order_candidates(&mut targets, options);
267+
order_candidates(&mut sources, options);
268268
Self {
269269
kind: OptionalAdjustmentKind::Transfer,
270270
targets,

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_family.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ impl AssignmentFamilyCursor {
6161
if should_stop() {
6262
return None;
6363
}
64-
order_candidates(&mut entities, options, 0xA551_6EED_0000_000B);
64+
order_candidates(&mut entities, options);
6565
let value_degrees = if options.required_scarcity_ordering {
6666
required_value_degrees(
6767
group,
@@ -89,7 +89,7 @@ impl AssignmentFamilyCursor {
8989
options: ScalarAssignmentMoveOptions,
9090
kind: AssignmentMoveKind,
9191
) -> Self {
92-
order_candidates(&mut entities, options, 0xA551_6EED_0000_000C);
92+
order_candidates(&mut entities, options);
9393
Self::EntityValues(EntityValueCursor {
9494
entities,
9595
entity_pos: 0,

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_pair.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl PairWindowCursor {
5454
));
5555
}
5656
assigned.sort_unstable();
57-
order_candidates(&mut assigned, options, 0xA551_6EED_0000_0006);
57+
order_candidates(&mut assigned, options);
5858
}
5959
Self::from_assigned(PairWindowKind::SequenceWindow, assigned, options)
6060
}
@@ -66,7 +66,7 @@ impl PairWindowCursor {
6666
options: ScalarAssignmentMoveOptions,
6767
) -> Self {
6868
let mut entities = assigned_entities_by_position(group, solution, state);
69-
order_candidates(&mut entities, options, 0xA551_6EED_0000_0007);
69+
order_candidates(&mut entities, options);
7070
Self::from_entities(PairWindowKind::Swap, entities, options)
7171
}
7272

@@ -88,7 +88,7 @@ impl PairWindowCursor {
8888
}
8989
let mut sequence_keys = by_sequence.keys().copied().collect::<Vec<_>>();
9090
sequence_keys.sort_unstable();
91-
order_candidates(&mut sequence_keys, options, 0xA551_6EED_0000_0008);
91+
order_candidates(&mut sequence_keys, options);
9292
let mut groups = Vec::with_capacity(sequence_keys.len());
9393
for sequence_key in sequence_keys {
9494
let Some(mut entities) = by_sequence.remove(&sequence_key) else {
@@ -101,11 +101,7 @@ impl PairWindowCursor {
101101
*entity_index,
102102
)
103103
});
104-
order_candidates(
105-
&mut entities,
106-
options,
107-
0xA551_6EED_0000_0009 ^ sequence_key.unwrap_or(usize::MAX) as u64,
108-
);
104+
order_candidates(&mut entities, options);
109105
groups.push(entities);
110106
}
111107
Self::from_groups(PairWindowKind::Rematch, groups, options)
@@ -118,7 +114,7 @@ impl PairWindowCursor {
118114
options: ScalarAssignmentMoveOptions,
119115
) -> Self {
120116
let mut entities = assigned_entities_by_position(group, solution, state);
121-
order_candidates(&mut entities, options, 0xA551_6EED_0000_000A);
117+
order_candidates(&mut entities, options);
122118
Self::from_entities(PairWindowKind::PairedReassignment, entities, options)
123119
}
124120

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_required_batch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ where
3333
options.value_candidate_limit,
3434
should_stop,
3535
)?;
36-
order_candidates(&mut entities, options, 0xA551_6EED_0000_0004);
36+
order_candidates(&mut entities, options);
3737
if should_stop() {
3838
return None;
3939
}

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_value_cycle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl ValueWindowCycleCursor {
4949
}
5050
}
5151
}
52-
super::assignment_candidate::order_candidates(&mut triples, options, 0xA551_6EED_0000_0005);
52+
super::assignment_candidate::order_candidates(&mut triples, options);
5353

5454
let max_len = options
5555
.max_depth

crates/solverforge-solver/src/phase/construction/grouped_scalar/assignment_value_index.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ where
4242

4343
let mut values = assigned_values.into_iter().collect::<Vec<_>>();
4444
values.sort_unstable();
45-
order_candidates(&mut values, options, 0xA551_6EED_0000_0001);
45+
order_candidates(&mut values, options);
4646

4747
let mut sequence_keys = sequence_keys.into_iter().collect::<Vec<_>>();
4848
sequence_keys.sort_unstable();
49-
order_candidates(&mut sequence_keys, options, 0xA551_6EED_0000_0002);
49+
order_candidates(&mut sequence_keys, options);
5050

5151
AssignedValueSequenceIndex {
5252
values,

0 commit comments

Comments
 (0)