Skip to content

Commit 61338fd

Browse files
committed
Implement an IterativeSearchCursor for better pull-based solution iteration.
See: #143
1 parent 1671576 commit 61338fd

File tree

4 files changed

+160
-120
lines changed

4 files changed

+160
-120
lines changed

src/rs-wasm/wasm_api.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ pub fn wasmTwsearch(
7474
Default::default(),
7575
None,
7676
);
77-
let mut iterative_deepening_search = iterative_deepening_search.map_err(|e| e.description)?;
77+
let iterative_deepening_search = iterative_deepening_search.map_err(|e| e.description)?;
7878

7979
match iterative_deepening_search
80-
.search_with_default_individual_search_adaptations(
80+
.owned_search_with_default_individual_search_adaptations(
8181
&search_pattern,
8282
options.inidividual_search_options,
8383
)

src/rs/_cli/serve/serve.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fn solve_pattern(
8080
Ok(search_pattern) => search_pattern,
8181
Err(e) => return Response::text(e.to_string()).with_status_code(400),
8282
};
83-
let mut search =
83+
let search =
8484
match <IterativeDeepeningSearch<KPuzzle>>::try_new_kpuzzle_with_hash_prune_table_shim(
8585
kpuzzle.clone(),
8686
Generators::Custom(CustomGenerators {
@@ -103,7 +103,7 @@ fn solve_pattern(
103103
Err(e) => return Response::text(e.description).with_status_code(400),
104104
};
105105
if let Some(solution) = search
106-
.search_with_default_individual_search_adaptations(
106+
.owned_search_with_default_individual_search_adaptations(
107107
&search_pattern,
108108
IndividualSearchOptions {
109109
min_num_solutions: None,

src/rs/_internal/search/iterative_deepening/iterative_deepening_search.rs

+142-103
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
use std::{
2-
cmp::max,
3-
fmt::Debug,
4-
sync::{
5-
mpsc::{channel, Receiver, Sender},
6-
Arc,
7-
},
8-
};
1+
use std::{cmp::max, fmt::Debug, sync::Arc};
92

103
use cubing::{
114
alg::{Alg, AlgNode, Move},
@@ -48,9 +41,9 @@ const MAX_SUPPORTED_SEARCH_DEPTH: Depth = Depth(500); // TODO: increase
4841
// TODO: use https://doc.rust-lang.org/std/ops/enum.ControlFlow.html as a wrapper instead?
4942
#[allow(clippy::enum_variant_names)]
5043
enum SearchRecursionResult {
51-
DoneSearching,
5244
ContinueSearchingDefault,
5345
ContinueSearchingExcludingCurrentMoveClass,
46+
FoundSolution(Alg),
5447
}
5548

5649
struct SolutionPreviousMoves<'a> {
@@ -103,48 +96,31 @@ impl<'a> Iterator for SolutionMovesReverseIterator<'a> {
10396
}
10497
}
10598

106-
pub struct SearchSolutions {
107-
receiver: Receiver<Option<Alg>>,
108-
done: bool,
99+
pub struct IterativeSearchCursor<'a, TPuzzle: SemiGroupActionPuzzle = KPuzzle> {
100+
search: &'a mut IterativeDeepeningSearch<TPuzzle>,
101+
individual_search_data: IndividualSearchData<TPuzzle>,
109102
}
110103

111-
impl SearchSolutions {
112-
pub fn construct() -> (Sender<Option<Alg>>, Self) {
113-
// TODO: use `sync_channel` to control resumption?
114-
let (sender, receiver) = channel::<Option<Alg>>();
115-
(
116-
sender,
117-
Self {
118-
receiver,
119-
done: false,
120-
},
121-
)
104+
impl<TPuzzle: SemiGroupActionPuzzle> Iterator for IterativeSearchCursor<'_, TPuzzle> {
105+
type Item = Alg;
106+
107+
fn next(&mut self) -> Option<Alg> {
108+
self.search
109+
.search_internal(&mut self.individual_search_data)
122110
}
123111
}
124112

125-
impl Iterator for SearchSolutions {
113+
pub struct OwnedIterativeSearchCursor<TPuzzle: SemiGroupActionPuzzle = KPuzzle> {
114+
search: IterativeDeepeningSearch<TPuzzle>,
115+
individual_search_data: IndividualSearchData<TPuzzle>,
116+
}
117+
118+
impl<TPuzzle: SemiGroupActionPuzzle> Iterator for OwnedIterativeSearchCursor<TPuzzle> {
126119
type Item = Alg;
127120

128-
fn next(&mut self) -> Option<Self::Item> {
129-
if self.done {
130-
None
131-
} else {
132-
let received = match self.receiver.recv() {
133-
Ok(received) => received,
134-
Err(_) => {
135-
// TODO: this could be either a channel failure or no solutions found. We should find a way for the latter to avoid hitting this code path.
136-
self.done = true;
137-
return None;
138-
}
139-
};
140-
match received {
141-
Some(alg) => Some(alg),
142-
None => {
143-
self.done = true;
144-
None
145-
}
146-
}
147-
}
121+
fn next(&mut self) -> Option<Alg> {
122+
self.search
123+
.search_internal(&mut self.individual_search_data)
148124
}
149125
}
150126

@@ -255,13 +231,57 @@ impl IndividualSearchOptions {
255231
}
256232

257233
struct IndividualSearchData<TPuzzle: SemiGroupActionPuzzle> {
234+
search_pattern: TPuzzle::Pattern,
258235
individual_search_options: IndividualSearchOptions,
259236
recursive_work_tracker: RecursiveWorkTracker,
260237
num_solutions_sofar: usize,
261-
solution_sender: Sender<Option<Alg>>,
262238
individual_search_adaptations: IndividualSearchAdaptations<TPuzzle>,
263239
}
264240

241+
impl<TPuzzle: SemiGroupActionPuzzle> IndividualSearchData<TPuzzle> {
242+
/// Note that search is pull-based. You must call `.next()` (or invoke
243+
/// something that does) on the return value for the search to begine.
244+
pub fn new(
245+
search: &mut IterativeDeepeningSearch<TPuzzle>,
246+
search_pattern: &TPuzzle::Pattern,
247+
mut individual_search_options: IndividualSearchOptions,
248+
individual_search_adaptations: IndividualSearchAdaptations<TPuzzle>,
249+
) -> Self {
250+
// TODO: do validation more consisten tly.
251+
if let Some(min_depth) = individual_search_options.min_depth_inclusive {
252+
if min_depth > MAX_SUPPORTED_SEARCH_DEPTH {
253+
search
254+
.api_data
255+
.search_logger
256+
.write_error("Min depth too large, capping at maximum.");
257+
individual_search_options.min_depth_inclusive = Some(MAX_SUPPORTED_SEARCH_DEPTH);
258+
}
259+
}
260+
if let Some(max_depth) = individual_search_options.max_depth_exclusive {
261+
if max_depth > MAX_SUPPORTED_SEARCH_DEPTH {
262+
search
263+
.api_data
264+
.search_logger
265+
.write_error("Max depth too large, capping at maximum.");
266+
individual_search_options.max_depth_exclusive = Some(MAX_SUPPORTED_SEARCH_DEPTH);
267+
}
268+
}
269+
270+
let search_pattern = search_pattern.clone();
271+
272+
Self {
273+
search_pattern,
274+
individual_search_options,
275+
recursive_work_tracker: RecursiveWorkTracker::new(
276+
"Search".to_owned(),
277+
search.api_data.search_logger.clone(),
278+
),
279+
num_solutions_sofar: 0,
280+
individual_search_adaptations,
281+
}
282+
}
283+
}
284+
265285
pub struct IterativeDeepeningSearchAPIData<TPuzzle: SemiGroupActionPuzzle> {
266286
pub search_generators: SearchGenerators<TPuzzle>,
267287
pub canonical_fsm: CanonicalFSM<TPuzzle>, // TODO: move this into `SearchAdaptations`
@@ -420,55 +440,86 @@ impl<TPuzzle: SemiGroupActionPuzzle> IterativeDeepeningSearch<TPuzzle> {
420440
})
421441
}
422442

423-
pub fn search_with_default_individual_search_adaptations(
424-
&mut self,
443+
/// Note that search is pull-based. You must call `.next()` (or invoke
444+
/// something that does) on the return value for the search to begine.
445+
pub fn search_with_default_individual_search_adaptations<'a>(
446+
&'a mut self,
425447
search_pattern: &TPuzzle::Pattern,
426448
individual_search_options: IndividualSearchOptions,
427-
) -> SearchSolutions {
449+
) -> IterativeSearchCursor<'a, TPuzzle> {
428450
self.search(
429451
search_pattern,
430452
individual_search_options,
431453
Default::default(),
432454
)
433455
}
434456

435-
pub fn search(
436-
&mut self,
457+
/// Note that search is pull-based. You must call `.next()` (or invoke
458+
/// something that does) on the return value for the search to begine.
459+
pub fn owned_search_with_default_individual_search_adaptations(
460+
self,
437461
search_pattern: &TPuzzle::Pattern,
438-
mut individual_search_options: IndividualSearchOptions,
462+
individual_search_options: IndividualSearchOptions,
463+
) -> OwnedIterativeSearchCursor<TPuzzle> {
464+
self.owned_search(
465+
search_pattern,
466+
individual_search_options,
467+
Default::default(),
468+
)
469+
}
470+
471+
/// Note that search is pull-based. You must call `.next()` (or invoke
472+
/// something that does) on the return value for the search to begine.
473+
pub fn search<'a>(
474+
&'a mut self,
475+
search_pattern: &TPuzzle::Pattern,
476+
individual_search_options: IndividualSearchOptions,
439477
individual_search_adaptations: IndividualSearchAdaptations<TPuzzle>,
440-
) -> SearchSolutions {
441-
// TODO: do validation more consistently.
442-
if let Some(min_depth) = individual_search_options.min_depth_inclusive {
443-
if min_depth > MAX_SUPPORTED_SEARCH_DEPTH {
444-
self.api_data
445-
.search_logger
446-
.write_error("Min depth too large, capping at maximum.");
447-
individual_search_options.min_depth_inclusive = Some(MAX_SUPPORTED_SEARCH_DEPTH);
448-
}
449-
}
450-
if let Some(max_depth) = individual_search_options.max_depth_exclusive {
451-
if max_depth > MAX_SUPPORTED_SEARCH_DEPTH {
452-
self.api_data
453-
.search_logger
454-
.write_error("Max depth too large, capping at maximum.");
455-
individual_search_options.max_depth_exclusive = Some(MAX_SUPPORTED_SEARCH_DEPTH);
456-
}
478+
) -> IterativeSearchCursor<'a, TPuzzle> {
479+
let individual_search_data = IndividualSearchData::new(
480+
self,
481+
search_pattern,
482+
individual_search_options,
483+
individual_search_adaptations,
484+
);
485+
IterativeSearchCursor {
486+
search: self,
487+
individual_search_data,
457488
}
489+
}
458490

459-
let (solution_sender, search_solutions) = SearchSolutions::construct();
460-
let mut individual_search_data = IndividualSearchData {
491+
/// Note that search is pull-based. You must call `.next()` (or invoke
492+
/// something that does) on the return value for the search to begine.
493+
pub fn owned_search(
494+
mut self,
495+
search_pattern: &TPuzzle::Pattern,
496+
individual_search_options: IndividualSearchOptions,
497+
individual_search_adaptations: IndividualSearchAdaptations<TPuzzle>,
498+
) -> OwnedIterativeSearchCursor<TPuzzle> {
499+
let individual_search_data = IndividualSearchData::new(
500+
&mut self,
501+
search_pattern,
461502
individual_search_options,
462-
recursive_work_tracker: RecursiveWorkTracker::new(
463-
"Search".to_owned(),
464-
self.api_data.search_logger.clone(),
465-
),
466-
num_solutions_sofar: 0,
467-
solution_sender,
468503
individual_search_adaptations,
469-
};
504+
);
505+
OwnedIterativeSearchCursor {
506+
search: self,
507+
individual_search_data,
508+
}
509+
}
470510

471-
let search_pattern = search_pattern.clone();
511+
// TODO: ideally the return should be represented by a fallible iterator (since it can fail caller-provided input deep in the stack).
512+
fn search_internal(
513+
&mut self,
514+
individual_search_data: &mut IndividualSearchData<TPuzzle>,
515+
) -> Option<Alg> {
516+
if individual_search_data.num_solutions_sofar
517+
>= individual_search_data
518+
.individual_search_options
519+
.get_min_num_solutions()
520+
{
521+
return None;
522+
}
472523

473524
let (initial_search_depth, initial_depth_continuation_condition) = {
474525
let options_min_depth = individual_search_data
@@ -506,7 +557,10 @@ impl<TPuzzle: SemiGroupActionPuzzle> IterativeDeepeningSearch<TPuzzle> {
506557
let mut initial_depth_continuation_condition = initial_depth_continuation_condition;
507558

508559
// TODO: combine `KPatternStack` with `SolutionMoves`?
509-
let mut pattern_stack = PatternStack::new(self.api_data.tpuzzle.clone(), search_pattern);
560+
let mut pattern_stack = PatternStack::new(
561+
self.api_data.tpuzzle.clone(),
562+
individual_search_data.search_pattern.clone(),
563+
);
510564
for remaining_depth in *initial_search_depth
511565
..*individual_search_data
512566
.individual_search_options
@@ -535,7 +589,7 @@ impl<TPuzzle: SemiGroupActionPuzzle> IterativeDeepeningSearch<TPuzzle> {
535589
)
536590
.expect("TODO: invalid canonical FSM pre-moves.");
537591
let recursion_result = self.recurse(
538-
&mut individual_search_data,
592+
individual_search_data,
539593
&mut pattern_stack,
540594
initial_state,
541595
remaining_depth,
@@ -545,12 +599,13 @@ impl<TPuzzle: SemiGroupActionPuzzle> IterativeDeepeningSearch<TPuzzle> {
545599
individual_search_data
546600
.recursive_work_tracker
547601
.finish_latest_depth();
548-
if let SearchRecursionResult::DoneSearching = recursion_result {
549-
break;
602+
if let SearchRecursionResult::FoundSolution(alg) = recursion_result {
603+
return Some(alg);
550604
}
551605
initial_depth_continuation_condition = ContinuationCondition::None;
552606
}
553-
search_solutions
607+
608+
None
554609
}
555610

556611
fn recurse(
@@ -666,13 +721,13 @@ impl<TPuzzle: SemiGroupActionPuzzle> IterativeDeepeningSearch<TPuzzle> {
666721
pattern_stack.pop();
667722

668723
match recursive_result {
669-
SearchRecursionResult::DoneSearching => {
670-
return SearchRecursionResult::DoneSearching;
671-
}
672724
SearchRecursionResult::ContinueSearchingDefault => {}
673725
SearchRecursionResult::ContinueSearchingExcludingCurrentMoveClass => {
674726
break;
675727
}
728+
SearchRecursionResult::FoundSolution(alg) => {
729+
return SearchRecursionResult::FoundSolution(alg)
730+
}
676731
}
677732
}
678733
}
@@ -798,23 +853,7 @@ impl<TPuzzle: SemiGroupActionPuzzle> IterativeDeepeningSearch<TPuzzle> {
798853

799854
let alg = Alg::from(&solution_moves);
800855
individual_search_data.num_solutions_sofar += 1;
801-
individual_search_data
802-
.solution_sender
803-
.send(Some(alg))
804-
.expect("Internal error: could not send solution");
805-
if individual_search_data.num_solutions_sofar
806-
>= individual_search_data
807-
.individual_search_options
808-
.get_min_num_solutions()
809-
{
810-
individual_search_data
811-
.solution_sender
812-
.send(None)
813-
.expect("Internal error: could not send end of search");
814-
SearchRecursionResult::DoneSearching
815-
} else {
816-
SearchRecursionResult::ContinueSearchingDefault
817-
}
856+
SearchRecursionResult::FoundSolution(alg)
818857
}
819858

820859
fn is_target_pattern(&self, current_pattern: &TPuzzle::Pattern) -> bool {

0 commit comments

Comments
 (0)