reasonkit/thinktool/
oscillation.rs

1//! # Divergent-Convergent Oscillation
2//!
3//! Implements the cognitive oscillation pattern for creative reasoning.
4//!
5//! ## Scientific Foundation
6//!
7//! Based on Guilford's Structure of Intellect model (1967) and research on
8//! creative cognition:
9//! - Divergent thinking: Fluency, flexibility, originality, elaboration
10//! - Convergent thinking: Evaluation, selection, refinement
11//! - Optimal creativity requires oscillation between both modes
12//!
13//! ## The Oscillation Pattern
14//!
15//! ```text
16//! DIVERGE → CONVERGE → DIVERGE → CONVERGE → ... → FINAL
17//!    ↓         ↓          ↓          ↓
18//!  Expand    Focus     Expand     Focus
19//!  Options   Best      Around    Optimal
20//! ```
21//!
22//! ## Usage
23//!
24//! ```rust,ignore
25//! use reasonkit::thinktool::oscillation::{OscillationEngine, OscillationConfig};
26//!
27//! let engine = OscillationEngine::new(OscillationConfig::default());
28//! let result = engine.oscillate(problem).await?;
29//! ```
30
31use serde::{Deserialize, Serialize};
32
33/// Configuration for divergent-convergent oscillation
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct OscillationConfig {
36    /// Number of oscillation cycles
37    pub cycles: usize,
38    /// Minimum ideas per divergent phase
39    pub min_ideas_per_diverge: usize,
40    /// Maximum ideas to carry forward per convergent phase
41    pub max_ideas_to_converge: usize,
42    /// Divergent thinking dimensions
43    pub divergent_dimensions: Vec<DivergentDimension>,
44    /// Convergent evaluation criteria
45    pub convergent_criteria: Vec<ConvergentCriterion>,
46    /// Whether to track idea lineage
47    pub track_lineage: bool,
48}
49
50impl Default for OscillationConfig {
51    fn default() -> Self {
52        Self {
53            cycles: 3,
54            min_ideas_per_diverge: 5,
55            max_ideas_to_converge: 3,
56            divergent_dimensions: vec![
57                DivergentDimension::Fluency,
58                DivergentDimension::Flexibility,
59                DivergentDimension::Originality,
60                DivergentDimension::Elaboration,
61            ],
62            convergent_criteria: vec![
63                ConvergentCriterion::Feasibility,
64                ConvergentCriterion::Impact,
65                ConvergentCriterion::Novelty,
66                ConvergentCriterion::Alignment,
67            ],
68            track_lineage: true,
69        }
70    }
71}
72
73impl OscillationConfig {
74    /// GigaThink-optimized configuration (10+ perspectives)
75    pub fn gigathink() -> Self {
76        Self {
77            cycles: 3,
78            min_ideas_per_diverge: 10,
79            max_ideas_to_converge: 5,
80            divergent_dimensions: vec![
81                DivergentDimension::Fluency,
82                DivergentDimension::Flexibility,
83                DivergentDimension::Originality,
84                DivergentDimension::Elaboration,
85                DivergentDimension::Analogical,
86                DivergentDimension::Contrarian,
87            ],
88            convergent_criteria: vec![
89                ConvergentCriterion::Feasibility,
90                ConvergentCriterion::Impact,
91                ConvergentCriterion::Novelty,
92                ConvergentCriterion::Alignment,
93            ],
94            track_lineage: true,
95        }
96    }
97
98    /// Quick brainstorming mode
99    pub fn quick() -> Self {
100        Self {
101            cycles: 2,
102            min_ideas_per_diverge: 5,
103            max_ideas_to_converge: 2,
104            ..Default::default()
105        }
106    }
107}
108
109/// Dimensions of divergent thinking (Guilford, 1967)
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
111pub enum DivergentDimension {
112    /// Generate many ideas quickly
113    Fluency,
114    /// Generate ideas across different categories
115    Flexibility,
116    /// Generate novel/unusual ideas
117    Originality,
118    /// Add detail to ideas
119    Elaboration,
120    /// Generate ideas through analogies
121    Analogical,
122    /// Generate opposing/contrarian ideas
123    Contrarian,
124    /// Generate ideas by combining existing ones
125    Combinatorial,
126    /// Generate ideas by inverting constraints
127    Inversion,
128}
129
130impl DivergentDimension {
131    /// Get prompt guidance for this dimension
132    pub fn prompt_guidance(&self) -> &'static str {
133        match self {
134            Self::Fluency => "Generate as many ideas as possible, without filtering",
135            Self::Flexibility => {
136                "Generate ideas from different perspectives, domains, and categories"
137            }
138            Self::Originality => "Generate unusual, surprising, or unconventional ideas",
139            Self::Elaboration => "Add specific details, mechanisms, and implementation paths",
140            Self::Analogical => "Draw ideas from analogous domains and transfer insights",
141            Self::Contrarian => "Challenge assumptions and generate opposing viewpoints",
142            Self::Combinatorial => "Combine and recombine existing ideas in new ways",
143            Self::Inversion => "Invert the problem - what would make it worse? Then reverse",
144        }
145    }
146}
147
148/// Criteria for convergent evaluation
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150pub enum ConvergentCriterion {
151    /// Can this be implemented?
152    Feasibility,
153    /// What impact would this have?
154    Impact,
155    /// How novel is this approach?
156    Novelty,
157    /// How well does this align with goals?
158    Alignment,
159    /// What are the risks?
160    Risk,
161    /// What resources are required?
162    ResourceCost,
163    /// How quickly can this be done?
164    TimeToValue,
165    /// How does this compare to alternatives?
166    ComparativeAdvantage,
167}
168
169impl ConvergentCriterion {
170    /// Get evaluation question for this criterion
171    pub fn evaluation_question(&self) -> &'static str {
172        match self {
173            Self::Feasibility => "How implementable is this idea given current constraints?",
174            Self::Impact => "What magnitude of positive change would this create?",
175            Self::Novelty => "How unique is this compared to existing approaches?",
176            Self::Alignment => "How well does this serve the stated goals?",
177            Self::Risk => "What could go wrong and how severe would it be?",
178            Self::ResourceCost => "What resources (time, money, effort) are required?",
179            Self::TimeToValue => "How quickly can this start delivering value?",
180            Self::ComparativeAdvantage => "Why is this better than the alternatives?",
181        }
182    }
183}
184
185/// A single idea generated during divergent thinking
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct Idea {
188    /// Unique identifier
189    pub id: usize,
190    /// The idea content
191    pub content: String,
192    /// Which dimension generated this
193    pub dimension: DivergentDimension,
194    /// Parent idea (if evolved from another)
195    pub parent_id: Option<usize>,
196    /// Generation cycle (0 = first divergent phase)
197    pub cycle: usize,
198    /// Convergent evaluation scores
199    pub scores: Vec<CriterionScore>,
200    /// Overall priority score after convergent phase
201    pub priority: f32,
202    /// Whether this survived to the final result
203    pub survived: bool,
204}
205
206/// Score for a single convergent criterion
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct CriterionScore {
209    pub criterion: ConvergentCriterion,
210    pub score: f32, // 0.0 - 1.0
211    pub rationale: String,
212}
213
214/// Result of a single divergent phase
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct DivergentPhase {
217    pub cycle: usize,
218    pub ideas_generated: Vec<Idea>,
219    pub dimension_coverage: Vec<(DivergentDimension, usize)>,
220    pub fluency_score: f32,     // ideas per dimension
221    pub flexibility_score: f32, // category diversity
222}
223
224/// Result of a single convergent phase
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct ConvergentPhase {
227    pub cycle: usize,
228    pub ideas_evaluated: usize,
229    pub ideas_selected: Vec<usize>, // IDs of selected ideas
230    pub elimination_rationale: Vec<String>,
231    pub selection_rationale: Vec<String>,
232}
233
234/// Complete oscillation result
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct OscillationResult {
237    /// Original problem
238    pub problem: String,
239    /// All generated ideas (full history)
240    pub all_ideas: Vec<Idea>,
241    /// Divergent phase results
242    pub divergent_phases: Vec<DivergentPhase>,
243    /// Convergent phase results
244    pub convergent_phases: Vec<ConvergentPhase>,
245    /// Final selected ideas
246    pub final_ideas: Vec<Idea>,
247    /// Synthesis of final ideas
248    pub synthesis: String,
249    /// Overall creativity metrics
250    pub metrics: OscillationMetrics,
251}
252
253/// Metrics for the oscillation process
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct OscillationMetrics {
256    /// Total ideas generated across all cycles
257    pub total_ideas: usize,
258    /// Ideas that survived to final round
259    pub surviving_ideas: usize,
260    /// Survival rate
261    pub survival_rate: f32,
262    /// Average fluency (ideas per cycle)
263    pub avg_fluency: f32,
264    /// Category diversity score
265    pub flexibility_score: f32,
266    /// Originality score (based on idea uniqueness)
267    pub originality_score: f32,
268    /// Number of complete cycles
269    pub cycles_completed: usize,
270}
271
272impl OscillationResult {
273    /// Get ideas from a specific cycle
274    pub fn ideas_from_cycle(&self, cycle: usize) -> Vec<&Idea> {
275        self.all_ideas.iter().filter(|i| i.cycle == cycle).collect()
276    }
277
278    /// Get ideas by dimension
279    pub fn ideas_by_dimension(&self, dim: DivergentDimension) -> Vec<&Idea> {
280        self.all_ideas
281            .iter()
282            .filter(|i| i.dimension == dim)
283            .collect()
284    }
285
286    /// Get idea lineage (ancestors of an idea)
287    pub fn get_lineage(&self, idea_id: usize) -> Vec<&Idea> {
288        let mut lineage = vec![];
289        let mut current_id = Some(idea_id);
290
291        while let Some(id) = current_id {
292            if let Some(idea) = self.all_ideas.iter().find(|i| i.id == id) {
293                lineage.push(idea);
294                current_id = idea.parent_id;
295            } else {
296                break;
297            }
298        }
299
300        lineage.reverse();
301        lineage
302    }
303}
304
305/// Prompt templates for oscillation
306pub struct OscillationPrompts;
307
308impl OscillationPrompts {
309    /// Generate divergent thinking prompt
310    pub fn diverge(
311        problem: &str,
312        dimensions: &[DivergentDimension],
313        prior_ideas: &[String],
314    ) -> String {
315        let dimension_guidance: String = dimensions
316            .iter()
317            .enumerate()
318            .map(|(i, d)| format!("{}. {:?}: {}", i + 1, d, d.prompt_guidance()))
319            .collect::<Vec<_>>()
320            .join("\n");
321
322        let prior = if prior_ideas.is_empty() {
323            "None yet - this is the first cycle.".to_string()
324        } else {
325            prior_ideas
326                .iter()
327                .enumerate()
328                .map(|(i, idea)| format!("{}. {}", i + 1, idea))
329                .collect::<Vec<_>>()
330                .join("\n")
331        };
332
333        format!(
334            r#"DIVERGENT THINKING PHASE - Generate Ideas
335
336PROBLEM: {problem}
337
338Use these thinking dimensions to generate diverse ideas:
339{dimension_guidance}
340
341PRIOR IDEAS (to build on or differentiate from):
342{prior}
343
344Generate at least 5 ideas, covering multiple dimensions.
345For each idea, specify:
346- IDEA: [The core idea]
347- DIMENSION: [Which dimension it came from]
348- ELABORATION: [Key details, mechanism, or implementation]
349
350Be creative, be bold, defer judgment. Quantity over quality in this phase.
351
352Format each idea clearly:
353IDEA 1:
354- Content: ...
355- Dimension: ...
356- Elaboration: ..."#,
357            problem = problem,
358            dimension_guidance = dimension_guidance,
359            prior = prior
360        )
361    }
362
363    /// Generate convergent evaluation prompt
364    pub fn converge(
365        problem: &str,
366        ideas: &[String],
367        criteria: &[ConvergentCriterion],
368        max_select: usize,
369    ) -> String {
370        let criteria_list: String = criteria
371            .iter()
372            .enumerate()
373            .map(|(i, c)| format!("{}. {:?}: {}", i + 1, c, c.evaluation_question()))
374            .collect::<Vec<_>>()
375            .join("\n");
376
377        let ideas_list: String = ideas
378            .iter()
379            .enumerate()
380            .map(|(i, idea)| format!("IDEA {}: {}", i + 1, idea))
381            .collect::<Vec<_>>()
382            .join("\n\n");
383
384        format!(
385            r#"CONVERGENT THINKING PHASE - Evaluate and Select
386
387PROBLEM: {problem}
388
389IDEAS TO EVALUATE:
390{ideas_list}
391
392EVALUATION CRITERIA:
393{criteria_list}
394
395For each idea, score it on each criterion (0.0 to 1.0) with a brief rationale.
396
397Then SELECT the top {max_select} ideas to carry forward.
398Explain why you're eliminating the others.
399
400Format:
401EVALUATION:
402Idea 1: [criterion scores and rationale]
403Idea 2: [criterion scores and rationale]
404...
405
406SELECTED (top {max_select}):
4071. Idea X - [why this was chosen]
4082. Idea Y - [why this was chosen]
409...
410
411ELIMINATED:
412- Idea Z - [why eliminated]
413..."#,
414            problem = problem,
415            ideas_list = ideas_list,
416            criteria_list = criteria_list,
417            max_select = max_select
418        )
419    }
420
421    /// Synthesize final ideas into coherent result
422    pub fn synthesize(problem: &str, final_ideas: &[String]) -> String {
423        let ideas_formatted: String = final_ideas
424            .iter()
425            .enumerate()
426            .map(|(i, idea)| format!("{}. {}", i + 1, idea))
427            .collect::<Vec<_>>()
428            .join("\n");
429
430        format!(
431            r#"SYNTHESIS PHASE - Integrate Best Ideas
432
433PROBLEM: {problem}
434
435FINAL SELECTED IDEAS:
436{ideas_formatted}
437
438Create a coherent synthesis that:
4391. Integrates the best elements of each idea
4402. Resolves any tensions between them
4413. Provides a unified approach
4424. Identifies implementation priorities
443
444SYNTHESIS:
445[Provide a 2-3 paragraph synthesis]
446
447KEY TAKEAWAYS:
4481. [Most important insight]
4492. [Second most important insight]
4503. [Third most important insight]
451
452RECOMMENDED APPROACH:
453[Concise action plan]"#,
454            problem = problem,
455            ideas_formatted = ideas_formatted
456        )
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_config_default() {
466        let config = OscillationConfig::default();
467        assert_eq!(config.cycles, 3);
468        assert!(config.min_ideas_per_diverge >= 5);
469    }
470
471    #[test]
472    fn test_gigathink_config() {
473        let config = OscillationConfig::gigathink();
474        assert_eq!(config.min_ideas_per_diverge, 10);
475        assert!(config
476            .divergent_dimensions
477            .contains(&DivergentDimension::Analogical));
478        assert!(config
479            .divergent_dimensions
480            .contains(&DivergentDimension::Contrarian));
481    }
482
483    #[test]
484    fn test_divergent_dimensions() {
485        let dim = DivergentDimension::Fluency;
486        assert!(dim.prompt_guidance().contains("many"));
487
488        let dim = DivergentDimension::Originality;
489        assert!(dim.prompt_guidance().contains("unusual"));
490    }
491
492    #[test]
493    fn test_convergent_criteria() {
494        let crit = ConvergentCriterion::Feasibility;
495        assert!(crit.evaluation_question().contains("implement"));
496
497        let crit = ConvergentCriterion::Impact;
498        assert!(crit.evaluation_question().contains("change"));
499    }
500
501    #[test]
502    fn test_oscillation_result_lineage() {
503        let result = OscillationResult {
504            problem: "Test".into(),
505            all_ideas: vec![
506                Idea {
507                    id: 0,
508                    content: "Root idea".into(),
509                    dimension: DivergentDimension::Fluency,
510                    parent_id: None,
511                    cycle: 0,
512                    scores: vec![],
513                    priority: 0.8,
514                    survived: true,
515                },
516                Idea {
517                    id: 1,
518                    content: "Child idea".into(),
519                    dimension: DivergentDimension::Elaboration,
520                    parent_id: Some(0),
521                    cycle: 1,
522                    scores: vec![],
523                    priority: 0.9,
524                    survived: true,
525                },
526                Idea {
527                    id: 2,
528                    content: "Grandchild idea".into(),
529                    dimension: DivergentDimension::Originality,
530                    parent_id: Some(1),
531                    cycle: 2,
532                    scores: vec![],
533                    priority: 0.95,
534                    survived: true,
535                },
536            ],
537            divergent_phases: vec![],
538            convergent_phases: vec![],
539            final_ideas: vec![],
540            synthesis: "".into(),
541            metrics: OscillationMetrics {
542                total_ideas: 3,
543                surviving_ideas: 3,
544                survival_rate: 1.0,
545                avg_fluency: 1.0,
546                flexibility_score: 1.0,
547                originality_score: 0.8,
548                cycles_completed: 3,
549            },
550        };
551
552        let lineage = result.get_lineage(2);
553        assert_eq!(lineage.len(), 3);
554        assert_eq!(lineage[0].id, 0);
555        assert_eq!(lineage[1].id, 1);
556        assert_eq!(lineage[2].id, 2);
557    }
558
559    #[test]
560    fn test_metrics() {
561        let metrics = OscillationMetrics {
562            total_ideas: 30,
563            surviving_ideas: 5,
564            survival_rate: 5.0 / 30.0,
565            avg_fluency: 10.0,
566            flexibility_score: 0.85,
567            originality_score: 0.75,
568            cycles_completed: 3,
569        };
570
571        assert!(metrics.survival_rate < 0.2);
572        assert_eq!(metrics.cycles_completed, 3);
573    }
574}