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}