Update the genetic operators to resolution queries and samplers - #108
Merged
Conversation
The initializer and the two variation operators each took a solution space, a start symbol and a depth bound at construction time, and each built its own draws from them. That put three copies of the same decision into the package, and it fixed the distribution the operators draw from: a depth-bounded descent through a randomized clause order, with no way to ask for another one. Mutation resampled a whole subterm and tested the result for membership afterwards, which is work the search space can answer directly. The operators now take a query as a method argument and a sampler as their parameter. Initialization is a map from a population size to a population. SampledInitialization poses the generator query once and collects a prefix of one sampler stream, rather than calling the sampler once per individual: each call re-poses the query, and the single stream is also what makes a size-uniform population a sample without replacement. A population that cannot be filled is an error in either of two clauses, not a short population. MixtureInitializer splits the population binomially between two initializers, which is what ramped half-and-half is once its two methods are read as initializers of their own. ResolutionMutation discards the subterm at a drawn position and draws the replacement from the residual of the language there. The offspring is an inhabitant by construction, so no membership test is needed. The position is uniform over the non-leaf positions and the root: the root carries reachability, because the residual query there is the generator query, and the leaves are excluded because a branching term has most of its positions there and a leaf at a literal answers with the term already present. One position, one request, no retry. A request that delivers nothing means no offspring, which is an ordinary case the surrounding procedure handles by drawing new parents. SubtreeSwap and SubtreeGraft replace Crossover. Recombination can leave the language, so both are closed by rejection through the checker, and both walk the pairs of inner positions in a uniform permutation of the pair set. Permuting the two position lists separately and taking their product correlates the order and lets the first parent dominate which exchange is tried. The swap returns two offspring or none, the graft one or none. An optional size bound sits inside the acceptance test, so a candidate beyond it is rejected exactly like one outside the language and the next pair is tried. The driver calls the operators with the new signatures and the shipped example builds the new components. Both are replaced in full by the evolutionary core that follows; the changes here are what keeps them working in the meantime. Three adjustments the port needed. mutation_points is annotated frozenset, because Tree.positions returns one. The example draws through the depth-bounded sampler, because a counting sampler rebuilds its weighted construction for every residual query and mutation poses a fresh one per call. And one error message loses a dash it used as punctuation, which no test reads.
Contributor
There was a problem hiding this comment.
Benchmark CoSy
Details
| Benchmark suite | Current: dc5bdbf | Previous: be71b5d | Ratio |
|---|---|---|---|
benchmarks/test_benchmark_maximal_elements.py::test_benchmark_maximal_elements |
9.452064045760737 iter/sec (stddev: 0.0006567130446887888) |
9.608281351562356 iter/sec (stddev: 0.010017085420616013) |
1.02 |
benchmarks/test_benchmark_maze.py::test_benchmark_maze |
3.439586127258994 iter/sec (stddev: 0.02774889342808098) |
3.9235166334572096 iter/sec (stddev: 0.018797551923491352) |
1.14 |
benchmarks/test_benchmark_maze_contains.py::test_benchmark_maze_contains |
3.230382260992055 iter/sec (stddev: 0.02792733017338335) |
3.5391214334611916 iter/sec (stddev: 0.025877608823890046) |
1.10 |
benchmarks/test_benchmark_maze_loopfree.py::test_benchmark_maze_loopfree |
3.464076386459245 iter/sec (stddev: 0.024997658236416698) |
3.8477221717914887 iter/sec (stddev: 0.02072136799063595) |
1.11 |
This comment was automatically generated by workflow using github-action-benchmark.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #108 +/- ##
===========================================
+ Coverage 87.83% 88.77% +0.93%
===========================================
Files 65 69 +4
Lines 6913 7456 +543
Branches 797 829 +32
===========================================
+ Hits 6072 6619 +547
- Misses 740 741 +1
+ Partials 101 96 -5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
FelixLaarmann
added a commit
that referenced
this pull request
Sep 2, 2026
Problem: `SolutionSpace.sample_tree` is the predecessor of the samplers. It draws one tree top-down under an optional depth bound and returns None where nothing came. The samplers of `cosy.search` draw the same way through a resolution query, and since the genetic operators moved to them in #108, no caller of `sample_tree` is left in the framework. What remains is a second way to draw from a solution space that nothing asks for. Changes: - `sample_tree` is removed with no replacement. A caller poses a resolution query and draws through a sampler of `cosy.search`. (BREAKING) - The three tests that drew through it draw through `DepthBoundedRandomSampler` over a generator query or a residual query instead, and their names follow. - `test_a_sample_reaches_more_than_the_nullary_clause_of_the_start_symbol` is dropped. It guarded a defect of the frontier `sample_tree` drew from, and `test_every_completion_within_the_bound_is_drawn_by_some_seed` asserts the same property more sharply, as set equality with the reachable terms on a space whose start symbol has a nullary clause. - The skeleton test gains the assertion its sibling already carries, that the seeds reach more than one term. Without it the subset direction stays true where the draw stops varying. - `sample_tree` was the only code under `src/cosy/core` that used the `random` module, so the `S311` exemption for that path goes with it. - Behavior: the seeded terms of `test_a_seeded_sample_repeats` differ from the ones `sample_tree` produced. The sampler draws its randomness from the clause order rather than from the goals of a step, and the property the test needs from the engine is the same one either way.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem:
The three genetic operators predate the search package and reach the solution space directly. Each
takes a solution space, a start symbol and a depth bound in its constructor and draws through
solution_space.sample_tree. The same three arguments therefore sit in three constructors, and the
distribution is fixed at construction time, so none of the samplers can be asked for. Two defects
come with that: initialize_population skips a failed sample and returns a short population, and
Crossover gives up on the remaining position pairs as soon as one of its two offspring is valid,
because its retry condition requires both of them to be invalid.
Changes:
parameter: initialize(query, size), mutate(query, individual), recombine(query, first, second).
The driver poses generator_query once and passes it down.
individual. With a size-uniform sampler that one stream is what makes the population a sample
without replacement.
inhabitants within the bound than requested and when the stream ends early. Neither case returns
a short population.
half-and-half is this component at p = 1/2 over two classical methods.
offspring is an inhabitant by construction and the membership test is gone. The position is
uniform over the non-leaf positions including the root. One position, one request, no retry, and
the return type is one offspring or None rather than a list.
cosy.search.queries.checker and walk the position pairs in a uniform permutation of the pair set,
where the previous operator took product() over two separately shuffled lists and let the first
parent's choice dominate. The swap returns two offspring or none, the graft one or none, and the
optional size bound sits in the acceptance test instead of a precomputed leaf depth.
previous results. distribute_rngs reaches a component's own generator only, not the generator
inside a sampler it draws through, which has to be seeded at construction.