Skip to content

feat: operator role by name prefix + init ops in HH loop - #18

Merged
jcrvz merged 2 commits into
masterfrom
feat/operator-role-prefix-classification
Jul 1, 2026
Merged

jcrvz merged 2 commits into
masterfrom
feat/operator-role-prefix-classification

Conversation

@J0NGS

@J0NGS J0NGS commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

What was added

This PR continues the work started in #14, where population initialization schemes were added to Population.

The goal now is to make these schemes part of the hyper-heuristic search process as well. In practice, the hyper-heuristic off CUSTOMHYS can now choose not only the sequence of perturbation operators, but also how the initial population should be created.

The implementation follows @jcrvz’s suggestion in issue #16: treating initializers as first-class operators within the heuristic space itself, while distinguishing them from perturbation operators through an explicit classification.


Main changes

operators.py — initializers as operators

The initialization schemes added in #14 are now also available as operator functions:

  • initialize_random
  • initialize_sobol
  • initialize_halton
  • initialize_grid
  • initialize_lhs
  • initialize_beta
  • initialize_normal
  • initialize_lognormal
  • initialize_exponential
  • initialize_rayleigh

These functions were added to __all__, along with their respective aliases.

A simple prefix-based role classification was also introduced through _ROLE_PREFIXES:

_ROLE_PREFIXES: dict[str, str] = {
    "initialize_": "initialize",
}

def _get_role(op_tuple) -> str:
    name = op_tuple[0]
    for prefix, role in _ROLE_PREFIXES.items():
        if name.startswith(prefix):
            return role
    return "perturb"

With this approach, the role of each operator is derived directly from its name. This avoids adding an extra field to the tuples and keeps role-related strings centralized.

It also makes future extensions straightforward. If new operator types are introduced later, such as adapt_ or select_, they can be supported by adding a single new prefix to the dictionary.


customhys/collections/default_initializers.txt

A new collection file was added with the initialization operators in the framework’s standard 3-tuple format.

This file can be loaded by Hyperheuristic together with any perturbation operator collection.


hyperheuristic.py — initializer support in the SA loop

When the heuristic space contains operators with role "initialize", the hyper-heuristic now treats position 0 of each candidate solution as the selected initializer.

The remaining positions continue to represent the perturbation operator sequence.

The main changes include:

  • initialization_operators: a new property that lists the initialization operators available in the heuristic space, along with their indices.
  • Candidate generation: position 0 is always filled with an initialization operator, while the rest of the solution is generated normally.
  • SA mutations: actions such as Swap, Mirror, Roll, Shift, Remove, and similar ones were updated to operate only on the tail of the solution, preserving the initializer at the head.
  • ChangeInitOperator: a new action that replaces the initialization operator at position 0 with another available initializer. This action is enabled only when at least two initializers exist in the space.
  • evaluate_candidate_solution: the initialization operator is extracted from the head of the solution, converted into initial_scheme, and passed to Metaheuristic, which then forwards it to Population.initialise_positions().
  • Compatibility preserved: Metaheuristic still receives only the tail 3-tuples, so the existing perturbation behavior remains unchanged.
  • Bugfix: Remove and RemoveMany now return the current solution unchanged when the cardinality is already at the minimum with initializers present, instead of raising HyperheuristicError.

Tests

Two new test files were added, with a total of 95 tests.

tests/test_initialization_operators.py

Covers:

  • _get_role
  • _ROLE_PREFIXES
  • process_operators
  • all initialize_* functions
  • the different Population.initialise_positions() variants

tests/test_hyperheuristic_initialization.py

Covers:

  • the initialization_operators property
  • the ChangeInitOperator action
  • initializer preservation at the head of the solution across mutations
  • evaluate_candidate_solution with an initialization operator
  • build_operators
  • integration tests with the full SA loop using Sphere, Rastrigin, and Ackley1 from benchmark_func

Backward compatibility

Heuristic spaces without initialization operators continue to work as before.

In these cases, _get_role returns "perturb" by default, and all initializer-specific behavior remains disabled.

Obs: need suport for vertex init in future

@J0NGS J0NGS self-assigned this Jun 24, 2026
@J0NGS
J0NGS requested a review from jcrvz June 24, 2026 10:45
@J0NGS J0NGS linked an issue Jun 24, 2026 that may be closed by this pull request
@J0NGS J0NGS added the enhancement New feature or request label Jun 24, 2026
jcrvz
jcrvz previously approved these changes Jun 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR extends CUSTOMHYS’ hyper-heuristic search space so population initialization schemes can be selected and mutated as part of the candidate solution genotype (initializer at head + perturbation tail), using name-prefix role classification (initialize_*).

Changes:

  • Added initialize_* operator functions and prefix-based role detection in customhys/operators.py, plus a default initializer collection file.
  • Updated Population.initialise_positions() to accept tuple-form schemes and forward kwargs to parameterized initializers.
  • Extended Hyperheuristic to recognize initializer operators, preserve them at position 0 during SA mutations, and extract the initializer in evaluate_candidate_solution, with new test coverage.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
customhys/operators.py Adds initializer operators, role detection, and updates operator build/decode helpers.
customhys/population.py Extends initialization API to accept tuple schemes and kwargs; adds scramble support for Sobol/Halton.
customhys/hyperheuristic.py Adds initializer-aware candidate generation/mutations and head extraction in evaluation.
customhys/collections/default_initializers.txt New default initializer operator collection.
tests/test_initialization_operators.py New tests for role classification and initializer operator behavior.
tests/test_hyperheuristic_initialization.py New tests for initializer-aware HH behavior, including SA loop integration.
Comments suppressed due to low confidence (2)

customhys/hyperheuristic.py:461

  • In Swap, the tail_len branches (<=1, ==2, >2) already cover every possible case, so the subsequent elif current_cardinality == 2 / > 2 branches are unreachable. Removing these dead branches will make the logic clearer and reduce maintenance risk.
                elif current_cardinality == 2:
                    encoded_neighbour = np.copy(sol)[::-1]

                elif current_cardinality > 2:
                    encoded_neighbour = np.copy(sol)
                    ind1, ind2 = np.random.choice(current_cardinality, 2, replace=False)
                    encoded_neighbour[ind1], encoded_neighbour[ind2] = encoded_neighbour[ind2], encoded_neighbour[ind1]

                else:
                    raise HyperheuristicError("Swap cannot be applied! current_cardinality < 2")

customhys/operators.py:1586

  • process_operators currently builds a string representation with two issues: (1) if len(parameters) >= 0: is always true, so the else branch is dead; (2) for string values it appends the bare parameter name and the key='value' form, producing misleading/invalid-looking operator strings (e.g. distribution,distribution='uniform'). These strings are used for logging and as stable identifiers in Metaheuristic.apply_search_operator, so they should be deterministic and well-formed.
    for op_tuple in simple_heuristics:
        operator, parameters, selector = op_tuple[0], op_tuple[1], op_tuple[2]
        # Store selectors
        selectors.append(selector)

        if len(parameters) >= 0:
            sep = ","
            str_parameters = []

            for parameter, value in parameters.items():

                # Check if a value is string
                if isinstance(value, str):
                    str_parameters.append(parameter)
                    str_parameters.append(f"{parameter}='{value}'")
                else:
                    str_parameters.append(f"{parameter}={value}")


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread customhys/hyperheuristic.py Resolved
Comment thread customhys/hyperheuristic.py Resolved
Comment thread customhys/population.py Resolved
Comment thread tests/test_initialization_operators.py Resolved
@J0NGS
J0NGS marked this pull request as draft June 27, 2026 17:38
@J0NGS
J0NGS marked this pull request as ready for review June 27, 2026 17:42
@J0NGS
J0NGS requested a review from jcrvz June 27, 2026 17:43
@jcrvz
jcrvz merged commit 98e24bf into master Jul 1, 2026
10 checks passed
@J0NGS
J0NGS deleted the feat/operator-role-prefix-classification branch July 1, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Joint search with parameterized initialization operators

3 participants