Skip to content

Update the genetic operators to resolution queries and samplers - #108

Merged
FelixLaarmann merged 1 commit into
developfrom
feature/ea-operators
Aug 28, 2026
Merged

Update the genetic operators to resolution queries and samplers#108
FelixLaarmann merged 1 commit into
developfrom
feature/ea-operators

Conversation

@FelixLaarmann

Copy link
Copy Markdown
Member

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:

  • The operators take a resolution query as a method argument and their sampler as the constructor
    parameter: initialize(query, size), mutate(query, individual), recombine(query, first, second).
    The driver poses generator_query once and passes it down.
  • SampledInitialization collects a prefix of a single sampler stream instead of one call per
    individual. With a size-uniform sampler that one stream is what makes the population a sample
    without replacement.
  • A population that cannot be filled raises InitializationError, both when the space holds fewer
    inhabitants within the bound than requested and when the stream ends early. Neither case returns
    a short population.
  • MixtureInitializer splits the population binomially between two initializers. Ramped
    half-and-half is this component at p = 1/2 over two classical methods.
  • ResolutionMutation draws the replacement from the residual query at the drawn position, so the
    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.
  • SubtreeSwap and SubtreeGraft replace Crossover. Both test the candidate with
    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.
  • The driver and the shipped example follow the new signatures.
  • Behavior: the offspring distribution is a different one, so a seeded run does not reproduce its
    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.

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.

@tudo-seal-workflows tudo-seal-workflows Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-commenter

codecov-commenter commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.21674% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.77%. Comparing base (47a8db7) to head (dc5bdbf).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
tests/test_mutation.py 96.64% 2 Missing and 4 partials ⚠️
tests/ea_fixtures.py 93.75% 4 Missing ⚠️
src/cosy/evolutionary_algorithms/initialisation.py 97.43% 1 Missing ⚠️
src/cosy/evolutionary_algorithms/mutation.py 95.00% 1 Missing ⚠️
src/cosy/evolutionary_algorithms/recombination.py 97.87% 1 Missing ⚠️
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     
Flag Coverage Δ
macos-latest-3.10 88.73% <98.21%> (+0.94%) ⬆️
macos-latest-3.11 88.76% <98.21%> (+0.94%) ⬆️
macos-latest-3.12 88.76% <98.21%> (+0.94%) ⬆️
macos-latest-3.13 88.76% <98.21%> (+0.94%) ⬆️
ubuntu-latest-3.10 88.73% <98.21%> (+0.94%) ⬆️
ubuntu-latest-3.11 88.76% <98.21%> (+0.94%) ⬆️
ubuntu-latest-3.12 88.76% <98.21%> (+0.94%) ⬆️
ubuntu-latest-3.13 88.76% <98.21%> (+0.94%) ⬆️
windows-latest-3.10 88.73% <98.21%> (+0.94%) ⬆️
windows-latest-3.11 ?
windows-latest-3.12 88.76% <98.21%> (+0.94%) ⬆️
windows-latest-3.13 ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@FelixLaarmann
FelixLaarmann merged commit a41fe1a into develop Aug 28, 2026
15 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants