Adding Custom Demes to pyHMS

This guide explains how to create your own custom deme implementations for pyHMS.

Overview

pyHMS allows you to extend the system with your own custom deme implementations. To create a custom deme, you need to:

  1. Define a new config class that inherits from BaseLevelConfig

  2. Create a new deme class that inherits from AbstractDeme

  3. Register your custom deme by passing a config_class_to_deme_class mapping to the hms function

Step 1: Define Your Config Class

Start by creating a config class that inherits from BaseLevelConfig. This class should:

  • Accept a problem and a stop condition (lsc) as required parameters

  • Include any additional parameters your deme implementation needs

  • Call the parent class’s __init__ method

from pyhms.config import BaseLevelConfig
from pyhms.core.problem import Problem
from pyhms.stop_conditions import LocalStopCondition, UniversalStopCondition

class RandomSearchConfig(BaseLevelConfig):
    def __init__(
        self,
        problem: Problem,
        lsc: LocalStopCondition | UniversalStopCondition,
        pop_size: int,
    ) -> None:
        super().__init__(problem, lsc)
        self.pop_size = pop_size

Step 2: Create Your Deme Class

Next, create a deme class that inherits from AbstractDeme. This class must implement the required interface:

import numpy as np
from pyhms.core.individual import Individual
from pyhms.demes.abstract_deme import AbstractDeme, DemeInitArgs

class RandomSearchDeme(AbstractDeme):
    def __init__(
        self,
        deme_init_args: DemeInitArgs,
    ) -> None:
        super().__init__(deme_init_args)
        config: RandomSearchConfig = deme_init_args.config  # type: ignore[assignment]
        self._pop_size = config.pop_size
        self.lower_bounds = config.bounds[:, 0]
        self.upper_bounds = config.bounds[:, 1]
        self.rng = np.random.RandomState(deme_init_args.random_seed)
        self.run()

    def run_metaepoch(self, tree) -> None:
        new_population = self._run_step()
        self._history.append(new_population)

        if (gsc_value := tree._gsc(tree)) or self._lsc(self):
            self._active = False
            self.log("Random Search Deme finished")
            return

    def _run_step(self) -> list[Individual]:
        genomes = np.random.uniform(
            self.lower_bounds,
            self.upper_bounds,
            size=(self._pop_size, len(self.lower_bounds))
        )
        population = [Individual(genome, problem=self._problem) for genome in genomes]
        Individual.evaluate_population(population)
        return population

Understanding DemeInitArgs

When implementing a custom deme, you’ll receive a DemeInitArgs object in the constructor. This dataclass contains all the necessary initialization parameters for your deme:

@dataclass
class DemeInitArgs:
    id: str
    level: int
    config: BaseLevelConfig
    logger: FilteringBoundLogger
    started_at: int = 0
    sprout_seed: Individual | None = None
    random_seed: int | None = None
    parent_deme: AbstractDeme | None = None

Understanding these fields:

  • id: A unique string identifier for your deme

  • level: The hierarchical level in the HMS tree (starts at 0 for root)

  • config: Your custom configuration class instance that inherits from BaseLevelConfig

  • logger: A structured logger for outputting debug information

  • started_at: The metaepoch number when this deme was created

  • sprout_seed: For non-root demes, this is the first individual that sprouted this deme

  • random_seed: A seed for random number generation to ensure reproducibility

  • parent_deme: Reference to the parent deme that sprouted this deme (None for root demes)

In your custom deme implementation, you’ll typically:

  1. Pass the DemeInitArgs object to the parent constructor

  2. Cast the config field to your specific config class type

  3. Access the configuration parameters you need

  4. Use the provided random seed for any randomized operations

Step 3: Register and Use Your Custom Deme

Finally, register your custom deme by creating a mapping from your config class to your deme class and passing it to the hms function:

from pyhms import hms
from pyhms.stop_conditions import DontStop, MetaepochLimit

# Create your deme configuration
random_search_config = RandomSearchConfig(
    problem=your_problem,
    lsc=DontStop(),
    pop_size=100
)

# Define the mapping from config class to deme class
config_class_to_deme_class = {
    RandomSearchConfig: RandomSearchDeme
}

# Use your custom deme in pyHMS
result = hms(
    level_config=[random_search_config],
    gsc=MetaepochLimit(10),
    sprout_cond=your_sprout_condition,
    config_class_to_deme_class=config_class_to_deme_class
)

Important AbstractDeme Properties and Methods

When implementing your custom deme, you can use the following properties and methods from the AbstractDeme base class:

  • self._problem: The optimization problem

  • self._bounds: The bounds of the search space

  • self._active: A flag indicating if the deme is active

  • self._history: History of populations (list of lists of individuals)

  • self.log(message): Log a message with additional meta information

  • self.centroid: Compute the centroid of the current population

  • self.best_individual: Get the best individual found by the deme

  • self.current_population: Get the current population

The most important method you must implement is run_metaepoch(self, tree), which is called in each meta-epoch of the HMS algorithm.