synthphonia/
solutions.rs

1use std::{collections::{hash_map::Entry, HashMap, VecDeque}, time::{self, Duration, Instant}};
2
3use futures::StreamExt;
4use tokio::{select, task::JoinHandle};
5
6use itertools::Itertools;
7use mapped_futures::mapped_futures::MappedFutures;
8use rand::Rng;
9use rand::seq::SliceRandom;
10use crate::{backward::Problem, debg, expr::{cfg::Cfg, context::Context, Expr, Expression}, forward::executor::Executor, galloc::{self, AllocForAny}, info, log, never, tree_learning::{bits::BoxSliceExt, tree_learning, Bits}};
11
12
13
14
15/// A global static mutex-protected container for optionally holding condition tracking data. 
16/// 
17/// 
18/// This item provides synchronized access to a condition tracker by encapsulating an optional tracker value within a spin-lock-based mutex, ensuring safe concurrent modification and retrieval across threads. 
19/// Initially empty, it is intended to be populated at runtime with tracking data as needed.
20pub static CONDITIONS: spin::Mutex<Option<ConditionTracker>> = spin::Mutex::new(None);
21
22/// A structure for tracking condition evaluations within a given context. 
23/// 
24/// 
25/// It maintains an internal context used for condition evaluation, a mapping from bit representations to expression references for deduplication, and a public vector storing pairs of expression references and their corresponding bit information for ordered access or iteration.
26pub struct ConditionTracker {
27    ctx: Context,
28    hashmap: HashMap<Bits, &'static Expr>,
29    pub vec: Vec<(&'static Expr, Bits)>
30}
31
32impl ConditionTracker {
33    /// Creates a new condition tracker instance with an initialized context, hashmap, and vector. 
34    /// This function takes a context and returns an instance where internal collections are set to their empty defaults, allowing the tracker to accumulate conditions as they are inserted later.
35    pub fn new(ctx: Context) -> Self {
36        Self { ctx, hashmap: HashMap::new(), vec: Vec::new() }
37    }
38    /// Inserts a condition expression into the tracker using its evaluated bit representation. 
39    /// This method calculates the bit signature of the provided expression and, if this signature is not already present in the internal storage, allocates the expression and registers it along with its corresponding bits.
40    pub fn insert(&mut self, expr: &Expr) {
41        let bits = expr.eval(&self.ctx).to_bits();
42        if let Entry::Vacant(e) = self.hashmap.entry(bits.clone()) {
43            let expr = expr.clone().galloc();
44            e.insert(expr);
45            self.vec.push((expr, bits));
46        }
47    }
48    /// Returns the number of conditions currently stored in the tracker. 
49    /// 
50    /// 
51    /// Calculates and yields the length of the internal vector that maintains a record of condition-expression pairs, providing a quick way to assess how many conditions have been tracked.
52    pub fn len(&self) -> usize {
53        self.vec.len()
54    }
55}
56
57/// Calculate the binomial coefficient for the given parameters.
58/// 
59/// This function computes the result of choosing p items from a set of n by deriving the numerator and denominator through iterative multiplication and then performing a ceiling division on these computed values to produce the final coefficient.
60pub fn bicoeff(n: usize, p: usize) -> usize {
61    let a: usize = (0..p).map(|i| n - i).product();
62    let b: usize = (1..=p).product();
63    a.div_ceil(b)
64}
65/// Returns a boolean indicating whether one or more "tree holes" fully satisfy the provided set of indices. 
66/// The function iterates over each element in the given collection and checks if every index in the provided slice corresponds to a value of 1 in that element, thereby determining if the set of indices is entirely contained within any of the elements.
67pub fn test_tree_hole_contains(tree_hole: &[Box<[u128]>], bits: &[usize]) -> bool {
68    for hole in tree_hole.iter() {
69        if bits.iter().all(|i| hole[*i] == 1) {
70            return true;
71        }
72    }
73    false
74}
75
76/// A structure encapsulating the state and configuration for managing synthesis solutions along with multi-threaded search execution. 
77/// 
78/// 
79/// It integrates various components such as a configuration context, a collection of candidate solutions paired with evaluation bits, and management of concurrent solution search threads. 
80/// Additionally, it tracks the synthesis start time, last update timestamp, an adaptive limit parameter, and a filtering structure (tree hole) used during example set generation and thread interruption.
81pub struct Solutions {
82    cfg: Cfg,
83    ctx: Context,
84    solutions: Vec<(&'static Expr, Bits)>,
85    solved_examples: Bits,
86    pub threads: MappedFutures<Vec<usize>, JoinHandle<Expression>>,
87    start_time: Instant,
88    last_update: Instant,
89    ite_limit: usize,
90    tree_hole: Vec<Box<[u128]>>,
91}
92
93impl Solutions {
94    /// Creates a new instance with the provided configuration and context. 
95    /// This function initializes the internal condition tracker based on the context, ensuring that no previous tracker is present, and then sets up all the initial fields required for solution management and concurrent search execution, including a default tree hole, empty solution set, and mapped futures for thread management.
96    pub fn new(cfg: Cfg, ctx: Context) -> Self {
97        {
98            let mut lock = CONDITIONS.lock();
99            assert!(lock.is_none());
100            *lock = Some(ConditionTracker::new(ctx.clone()));
101        }
102        let solutions = Vec::new();
103        let solved_examples = Bits::zeros(ctx.len);
104        Self { 
105            tree_hole: vec![Bits::ones(ctx.len)],
106            cfg, ctx, solutions, solved_examples, threads: MappedFutures::new(), start_time: time::Instant::now(), last_update: time::Instant::now(), ite_limit: 1}
107    }
108    /// Counts the number of stored synthesis solutions.
109    /// 
110    /// Returns the total count of solution entries currently maintained within the internal collection.
111    pub fn count(&self) -> usize {
112        self.solutions.len()
113    }
114
115    /// Adds a new candidate solution by evaluating an expression and updating the internal solution set accordingly. 
116    /// 
117    /// The method first attempts to derive an evaluation result from the provided expression and then checks if this new result is subsumed by any existing solution; if so, it immediately returns without modification. 
118    /// Otherwise, it filters out any previously stored solutions that are redundant relative to the new one, updates the union of solved examples, and adds the new solution.
119    /// 
120    /// Continues by assessing whether the inclusive solved example set now covers all required cases, returning the expression if complete. 
121    /// In parallel, it iterates over the currently scheduled threads, aborting any whose example sets are fully encompassed by the new evaluation and triggering the launch of new threads. 
122    /// Finally, it leverages auxiliary mechanisms to generate a final solution if possible, or returns None if the candidate fails to yield a valid update.
123    pub fn add_new_solution(&mut self, expr: &'static Expr) -> Option<&'static Expr> {
124        if let Some(b) = self.ctx.evaluate(expr) {
125            // Updating solutions
126            for (_, bits) in self.solutions.iter() {
127                if b.subset(bits) {
128                    return None;
129                }
130            }
131            self.solutions.retain(|(e, bits)| !bits.subset(&b));
132            self.solved_examples.union_assign(&b);
133            self.solutions.push((expr, b.clone()));
134            debg!("Solutions [{}/{} {}]: {:?}", self.solved_examples.count_ones(), self.ctx.len, self.threads.len(), self.solutions);
135
136            if b.count_ones() == self.ctx.len as u32 {
137                return Some(expr);
138            }
139            
140            // Updating threads
141            let keys = self.threads.keys().cloned().collect_vec();
142            for k in keys {
143                if k.iter().all(|i| b.get(*i)) {
144                    if let Some(a) = self.threads.remove(&k) {
145                        a.abort();
146                        info!("Interupting Thread of {k:?}");
147                        self.create_new_thread();
148                    }
149                }
150            }
151            // Generating Solution
152            self.generate_result(true)
153        } else { None }
154    }
155    /// Determines and generates a synthesized result based on solved examples. 
156    /// 
157    /// 
158    /// Checks whether the complete set of examples has been addressed; if so, it invokes a tree-learning procedure using a configurable operator limit depending on the provided flag and returns the synthesized solution expression. 
159    /// Otherwise, it returns none.
160    pub fn generate_result(&self, limit: bool) -> Option<&'static Expr> {
161        if self.solved_examples.count_ones() == self.ctx.len as u32 {
162            self.learn_tree(if limit { self.cfg.config.ite_limit_rate } else { 1 })
163        } else { None }
164    }
165    /// Learns a decision tree that synthesizes an expression using the current set of solutions and conditions, dynamically adjusting the iteration limit based on elapsed time and a provided rate parameter.
166    /// 
167    /// Computes an adaptive limit derived from the runtime duration and toggles a global condition tracker before invoking a tree learning procedure. 
168    /// Returns an expression reference if the tree learning process determines that a complete solution has been found, otherwise yields None.
169    pub fn learn_tree(&self, ite_limit_rate: usize) -> Option<&'static Expr> {
170        let duration = time::Instant::now() - self.start_time;
171        let ite_limit = if duration.as_secs() as usize >= self.cfg.config.ite_limit_giveup {
172            self.ite_limit + (duration.as_millis() as usize - self.cfg.config.ite_limit_giveup * 1000) * 5 / ite_limit_rate + 1
173        } else { self.ite_limit };
174        
175        let mut lock = CONDITIONS.lock();
176        let conditions = lock.as_mut().unwrap();
177        if conditions.len() == 0 {
178            return None;
179        }
180        debg!("Tree Learning Conditions: {}, Limit: {}", conditions.len(), ite_limit);
181        let bump = bumpalo::Bump::new();
182        let result = tree_learning(self.solutions.clone(), &conditions.vec[..], self.ctx.len, &bump, ite_limit);
183        if result.solved {
184            Some(result.expr())
185        } else {
186            None
187        }
188    }
189    /// Checks whether any stored solution in the current context fully covers the specified example set. 
190    /// 
191    /// This function iterates over all solutions and verifies if all indices in the provided example set are included in the corresponding coverage bitmask of any solution. 
192    /// It returns true as soon as a matching solution is found, and false otherwise.
193    /// 
194    pub fn check_cover(&self, example_set: &[usize]) -> bool {
195        for (_, bits) in self.solutions.iter() {
196            if example_set.iter().all(|i| bits.get(*i)) {
197                return true;
198            }
199        }
200        false
201    }
202    /// Generates a new set of example indices for initiating a synthesis thread. 
203    /// This method iterates over potential subset sizes, calculating binomial coefficients to limit enumeration, and constructs candidate subsets based on configured conditions—either filtering through a predefined mask or generating all possible combinations.
204    /// 
205    /// It randomizes the order of these candidate subsets and validates each by ensuring that the example set is neither already covered by existing solutions nor in use by running threads. 
206    /// When a valid subset is found, it returns the set; otherwise, it yields None if no appropriate example set can be generated.
207    pub fn generate_example_set(&mut self) -> Option<Vec<usize>> {
208        let mut rng = rand::thread_rng();
209        for k in 1..=self.ctx.len {
210            if bicoeff(self.ctx.len, k) > 4000000 { break; }
211
212            let mut vec = Vec::new();
213            if self.cfg.config.tree_hole {
214                for hole in self.tree_hole.iter() {
215                    vec.extend((0..self.ctx.len).filter(|i| hole.get(*i)).combinations(k));
216                } 
217            } else {
218                vec.extend((0..self.ctx.len).combinations(k).collect_vec());
219            }
220            
221            vec.shuffle(&mut rng);
222            for v in vec {
223                if !self.check_cover(&v) && !self.threads.contains(&v) { return Some(v); }
224            }
225        }
226        None
227    }
228    /// Updates the tree hole configuration for the current synthesis process while ensuring that threads no longer covered by the new configuration are aborted and replaced. 
229    /// This method assigns the new tree hole, iterates through the active thread example sets, verifies each against the updated tree hole using a helper function, and for any that fail the condition, it aborts the corresponding thread and promptly creates a replacement thread to preserve continuous progress in the synthesis search.
230    pub fn update_tree_hole(&mut self, tree_hole: Vec<Box<[u128]>>) {
231        self.tree_hole = tree_hole;
232        let keys = self.threads.keys().cloned().collect_vec();
233        for k in keys {
234            if !test_tree_hole_contains(&self.tree_hole, &k) {
235                if let Some(a) = self.threads.remove(&k) {
236                    a.abort();
237                    info!("Interupting Thread of {k:?}");
238                    self.create_new_thread();
239                }
240            }
241        }
242    }
243    /// Creates a new asynchronous thread to perform synthesis search using a generated example set. 
244    /// This function attempts to generate a candidate example set and, if successful, constructs a new context augmented with these examples to spawn an additional thread executing the synthesis process; otherwise, it logs that no example set is available.
245    pub fn create_new_thread(&mut self) {
246        if let Some(exs) = self.generate_example_set() { 
247            info!("Creating new thread with examples {:?}", exs);
248            let ctx2 = self.ctx.with_examples(&exs);
249            self.threads.insert(exs, new_thread(self.cfg.clone(), ctx2));
250        } else {
251            info!("No available example set");
252        }
253    }
254    /// Creates and registers an asynchronous thread that performs exhaustive search over all examples from the current context. 
255    /// 
256    /// This function gathers every example index by iterating from 0 to the context's length, clones the current configuration and context, and then spawns a new search thread using those values. 
257    /// The resulting thread is inserted into the solutions' thread registry, initiating a comprehensive condition search for viable synthesis solutions.
258    /// 
259    pub fn create_all_search_thread(&mut self) {
260        // info!("Creating condition search thread.");
261        // cfg.config.cond_search = true;
262        self.threads.insert((0..self.ctx.len).collect_vec(), new_thread(self.cfg.clone(), self.ctx.clone()));
263    }
264    /// Continuously polls and adapts the synthesis process until a valid expression covering all examples is discovered. 
265    /// 
266    /// This asynchronous loop concurrently listens for solutions generated by worker threads and performs periodic adaptive adjustments. 
267    /// It evaluates incoming candidate expressions, updates and manages the set of current solutions, and dynamically modifies search parameters using time-based adjustments. 
268    /// When a complete solution is identified, it aborts remaining threads and returns the synthesized expression.
269    /// 
270    pub async fn solve_loop(&mut self) -> &'static Expr {
271        loop {
272            select! {
273                result = self.threads.next() => {
274                    let (k,v) = result.unwrap();
275                    let v = v.expect("Thread Execution Error").alloc_local();
276                    info!("Found a solution {:?} with examples {:?}.", v, k);
277                    self.last_update = time::Instant::now();
278                    if let Some(e) = self.add_new_solution(v) {
279                        for v in self.threads.iter() { v.abort(); }
280                        return e;
281                    }
282                    self.create_new_thread();
283                }
284                _ = tokio::time::sleep(Duration::from_millis(std::cmp::min(self.cfg.config.ite_limit_rate as u64, 2000))) => {
285                    if time::Instant::now() - self.last_update > Duration::from_millis(self.cfg.config.ite_limit_rate as u64 - 10) {
286                        info!("Adaptive Adjustment of ITE Limit: {}", self.ite_limit);
287                        self.ite_limit += 1;
288                        self.last_update = time::Instant::now();
289                    }
290                    if let Some(e) = self.generate_result(!self.threads.is_empty()) {
291                        for v in self.threads.iter() { v.abort(); }
292                        return e;
293                    }
294                }
295            }
296        }
297    }
298}
299
300/// Creates a new asynchronous task that executes a synthesis search using the provided configuration and evaluation context.
301/// 
302/// Spawns a task that initializes a solver executor with the given parameters, logs the deduction configuration, performs a top-blocked search for an expression, and then converts and returns it as the asynchronous task's result.
303pub fn new_thread(cfg: Cfg, ctx: Context) -> JoinHandle<Expression> {
304    tokio::spawn(async move {
305        let exec = Executor::new(ctx, cfg);
306        info!("Deduction Configuration: {:?}", exec.deducers);
307        
308        exec.solve_top_blocked().to_expression()
309    })
310}
311
312/// Enables a condition search thread by modifying the configuration and initiating a new asynchronous synthesis search. 
313/// This function activates condition search mode by setting the corresponding flag in the configuration, then delegates thread creation to a helper that starts the synthesis process, ultimately returning a join handle for the resulting expression.
314pub fn cond_search_thread(mut cfg: Cfg, ctx: Context) -> JoinHandle<Expression> {
315    cfg.config.cond_search = true;
316    new_thread(cfg, ctx)
317}
318
319/// Spawns an asynchronous task that executes a limited search procedure and returns its corresponding expression.
320/// 
321/// Initiates an executor using the provided configuration and context, then attempts to solve the top-level problem with a limit. 
322/// If the search produces a solution, the resulting expression is returned; otherwise, the process is aborted. 
323/// The asynchronous execution is managed through the Tokio runtime and the result is encapsulated within a join handle.
324pub fn new_thread_with_limit(cfg: Cfg, ctx: Context) -> JoinHandle<Expression> {
325    let log_level = log::log_level();
326    tokio::spawn(async move {
327        log::set_log_level(log_level);
328        if let Some(p) = {     
329            Executor::new(ctx, cfg).solve_top_with_limit().map(|e| e.to_expression())
330        } {
331            p
332        } else { never!() }
333    })
334}