Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/config_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ evolution:
modules:
test-vector: # logical module name
type: vector
initializer: random_vector
initializer: uniform
dim: 2
bounds: [-1.0, 1.0]

Expand Down Expand Up @@ -89,7 +89,7 @@ modules:
xs:
type: vector
dim: 6
initializer: random_vector
initializer: uniform
bounds: [0.0, 6.283185307] # [0, 2π]
mutation:
strategy: adaptive_individual
Expand Down
8 changes: 4 additions & 4 deletions docs/config_parameter.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ The `vector` module defines an evolvable parameter vector.
| `type` | `"vector"` | — | Module type identifier. |
| `dim` | `int \| list[int]` | — | Vector length (`int`) or structured dimensions (`list[int]`). Must be > 0. |
| `structure` | `"flat" \| "net" | `"flat"` | Structural interpretation of the vector. |
| `initializer` | `str` | — | Initializer name from the registry (e.g. `random_vector`, `zero_vector`, `normal_vector`, `fixed_vector`). |
| `initializer` | `str` | — | Initializer name from the registry (e.g. `uniform`, `zero`, `normal`, `fixed`). |
| `bounds` | `tuple[float, float]` | `[-1.0, 1.0]` | Hard clamp range applied after mutation. |
| `init_bounds` | `tuple[float, float] \| null` | `null` | Clamp applied only during initialization. Falls back to `bounds` if not set. |
| `shape` | `tuple[int, ...] \| null` | `null` | Optional explicit shape. If set, `dim = product(shape)`. Shape is retained as metadata. |
Expand Down Expand Up @@ -232,7 +232,7 @@ modules:
main:
type: vector
dim: 8
initializer: random_vector
initializer: uniform
bounds: [-1.0, 1.0]

mutation:
Expand Down Expand Up @@ -288,7 +288,7 @@ modules:
|-------------------|---------------------|---------|-------------|
| `dim` | list[int] | — | Layer sizes, e.g. `[4, 0, 0, 2]`. Hidden layers can start empty (0) and grow through structural mutation. |
| `activation` | str \| list[str] | — | If list: activation per layer. If str: used for non-input layers; input layer is treated as linear. |
| `initializer` | str | default_evonet | Topology preset (e.g. `default_evonet`, `unconnected_evonet`, `identity_evonet`). Parameter initialization is configured via `weights`, `bias`, and `delay`. |
| `initializer` | str | default | Topology preset (e.g. `default`, `unconnected`, `identity`). Parameter initialization is configured via `weights`, `bias`, and `delay`. |
| `weights` | dict | — | Weight init and bounds configuration (initializer, bounds, optional params). |
| `bias` | dict | — | Bias init and bounds configuration (initializer, bounds, optional params). |
| `neuron_dynamics` | list[dict] \| null | null | Optional per-layer neuron dynamics specification. Must match `len(dim)`. |
Expand Down Expand Up @@ -324,7 +324,7 @@ Allowed presets:

| Initializer | Meaning (topology only) |
|------------------------|-------------------------|
| `default_evonet` | Standard EvoNet topology preset (uses `connection_scope`, `connection_density`, and `recurrent`). |
| `default` | Standard EvoNet topology preset (uses `connection_scope`, `connection_density`, and `recurrent`). |
| `unconnected_evonet` | Creates neurons/layers but starts with **no connections** (use structural mutation to grow). |
| `identity_evonet` | Special preset intended for stable recurrent memory (may override parameters internally; see notes below). |

Expand Down
14 changes: 9 additions & 5 deletions evolib/config/evonet_component_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
EvoNetNeuronDynamicsConfig,
StructuralMutationConfig,
)
from evolib.interfaces.enums import RepresentationType

Bounds = Tuple[float, float]

Expand Down Expand Up @@ -208,7 +209,10 @@ class EvoNetComponentConfig(BaseModel):
model_config = ConfigDict(extra="forbid")

# Module type is fixed to "evonet"
type: Literal["evonet"] = "evonet"
type: RepresentationType = Field(
default=RepresentationType.EVONET,
description='Fixed module discriminator; must be "evonet" for this schema.',
)

# Layer structure: list of neuron counts per layer [input, hidden..., output]
dim: list[int]
Expand All @@ -228,7 +232,7 @@ class EvoNetComponentConfig(BaseModel):

# Name of the initializer function (resolved via initializer registry)
initializer: str = Field(
default="default_evonet", description="Name of the initializer to use"
default="default", description="Name of the initializer to use"
)

# Connection topology for initialization
Expand Down Expand Up @@ -276,9 +280,9 @@ def validate_initializer(cls, name: str) -> str:
Parameter initialization is handled exclusively via weights/bias/delay blocks.
"""
allowed = {
"default_evonet",
"unconnected_evonet",
"identity_evonet",
"default",
"unconnected",
"identity",
}

if name not in allowed:
Expand Down
51 changes: 41 additions & 10 deletions evolib/config/vector_component_config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# SPDX-License-Identifier: MIT
from typing import Any, Literal, Optional, Tuple, Union

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic import (
BaseModel,
ConfigDict,
Field,
ValidationInfo,
field_validator,
model_validator,
)

from evolib.config.base_component_config import CrossoverConfig, MutationConfig
from evolib.interfaces.enums import RepresentationType
Expand All @@ -21,7 +28,7 @@ class VectorComponentConfig(BaseModel):
type: vector
structure: flat # "flat" | "net"
dim: 16 # or a list for structured cases
initializer: random_vector # name from the initializer registry
initializer: normal # name from the initializer registry
bounds: [-1.0, 1.0]
mutation:
strategy: constant
Expand Down Expand Up @@ -86,7 +93,7 @@ class VectorComponentConfig(BaseModel):
values: Optional[list[float]] = Field(
default=None,
description=(
"Explicit values for 'fixed_vector' initializer. If 'dim' is absent it "
"Explicit values for 'fixed' initializer. If 'dim' is absent it "
"will be inferred from the length of 'values'."
),
)
Expand All @@ -101,8 +108,7 @@ class VectorComponentConfig(BaseModel):
)

# Evolution (mutation / crossover)
mutation: Optional[MutationConfig] = Field(
default=None,
mutation: MutationConfig = Field(
description=(
"Mutation configuration. By default, 'probability' is an element-wise rate "
"(per gene) in [0,1]; operators may optionally treat it as an apply gate."
Expand Down Expand Up @@ -143,18 +149,16 @@ class VectorComponentConfig(BaseModel):
@classmethod
def set_dim_for_fixed_vector(cls, config: dict[str, Any]) -> dict[str, Any]:
"""
If using 'fixed_vector', ensure 'values' is provided and infer 'dim' if absent.
If using 'fixed', ensure 'values' is provided and infer 'dim' if absent.

This keeps YAML concise and catches common mistakes early.
"""
initializer = config.get("initializer")
values = config.get("values")

if initializer == "fixed_vector":
if initializer == "fixed":
if not values:
raise ValueError(
"When using 'fixed_vector', 'values' must be provided."
)
raise ValueError("When using 'fixed', 'values' must be provided.")
if "dim" not in config:
config["dim"] = len(values)
return config
Expand All @@ -181,3 +185,30 @@ def validate_dim(cls, dim: Union[int, list[int]]) -> Union[int, list[int]]:
else:
raise TypeError("dim must be an int or list of ints")
return dim

@field_validator("initializer")
@classmethod
def validate_initializer(cls, name: str, info: ValidationInfo) -> str:
"""Validate allowed initializer names and provide clear errors for deprecated
names."""
if not isinstance(name, str) or not name.strip():
raise ValueError("initializer must be a non-empty string")

name = name.strip()

allowed = {"normal", "uniform", "zero", "fixed", "adaptive"}
if name not in allowed:
raise ValueError(
f"Unknown initializer '{name}'. " f"Allowed: {sorted(allowed)}"
)

# structure-aware check (only if structure is available in the data)
data = info.data or {}
structure = data.get("structure") or "flat"
if structure == "net" and name != "normal":
raise ValueError(
"For structure='net', initializer must be 'normal' "
"(use initializer: normal and structure: net)."
)

return name
139 changes: 83 additions & 56 deletions evolib/initializers/registry.py
Original file line number Diff line number Diff line change
@@ -1,85 +1,112 @@
# SPDX-License-Identifier: MIT
"""
Provides access to registered parameter initializers based on their name and
configuration.
from __future__ import annotations

This module dispatches initializer functions based on a string identifier
(e.g. "normal_initializer") and the associated module name within FullConfig.

Usage:
init_fn = get_initializer("ininitializer_normal_vector")
para = init_fn(config, module="brain")
"""

from typing import Any, Callable
from collections.abc import Callable
from typing import Any

from evolib.config.schema import FullConfig

# EvoNet-based initializer
# EvoNet presets
from evolib.initializers.evonet_initializers import (
initializer_default_evonet,
initializer_identity_evonet,
initializer_unconnected_evonet,
)

# NetVector-based initializer
# NetVector initializer (Vector with structure='net')
from evolib.initializers.net_initializers import initializer_normal_net

# Vector-based initializers
# Vector initializers
from evolib.initializers.vector_initializers import (
initializer_adaptive_vector,
initializer_fixed_vector,
initializer_normal_vector,
initializer_random_vector,
initializer_zero_vector,
)
from evolib.interfaces.enums import RepresentationType
from evolib.interfaces.types import ModuleConfig
from evolib.representation.base import ParaBase
from evolib.representation.composite import ParaComposite

# Typalias for initializer function
InitializerFunction = Callable[[FullConfig, str], ParaBase]


# Registry of known initializer functions
INITIALIZER_REGISTRY: dict[str, InitializerFunction] = {
"normal_vector": initializer_normal_vector,
"random_vector": initializer_random_vector,
"zero_vector": initializer_zero_vector,
"fixed_vector": initializer_fixed_vector,
"adaptive_vector": initializer_adaptive_vector,
"normal_net": initializer_normal_net,
"default_evonet": initializer_default_evonet,
"identity_evonet": initializer_identity_evonet,
"unconnected_evonet": initializer_unconnected_evonet,
}


def get_initializer(name: str) -> InitializerFunction:
def _resolve_vector_initializer(name: str, *, structure: str) -> InitializerFunction:
"""
Returns the initializer function for the given name.

Args:
name (str): Identifier of the initializer (must match registry)
Resolve vector initializer by name and structure.

Returns:
Callable[[FullConfig, str], ParaBase]: Initializer function

Raises:
ValueError: If the name is not registered
Design:
- 'structure: net' uses the NetVector-compatible initializer for 'normal'
- all other cases use vector initializers
"""
if name not in INITIALIZER_REGISTRY:
raise ValueError(f"Unknown initializer: '{name}'")

return INITIALIZER_REGISTRY[name]


def build_composite_initializer(config: FullConfig) -> Callable[[Any], ParaComposite]:
def initializer(_: Any) -> ParaComposite:
return ParaComposite(
{
name: get_initializer(config.modules[name].initializer)(config, name)
for name in config.modules
}
)

return initializer
name = str(name)
structure = str(structure or "flat")

match name:
case "normal":
if structure == "net":
return initializer_normal_net
return initializer_normal_vector
case "uniform":
return initializer_random_vector
case "zero":
return initializer_zero_vector
case "fixed":
return initializer_fixed_vector
case "adaptive":
return initializer_adaptive_vector
case _:
raise ValueError(
f"Unknown vector initializer '{name}'. "
"Allowed: normal, uniform, zero, fixed, adaptive."
)


def _resolve_evonet_initializer(name: str) -> InitializerFunction:
"""Resolve EvoNet topology presets via initializer name."""
name = str(name)

match name:
case "default":
return initializer_default_evonet
case "unconnected":
return initializer_unconnected_evonet
case "identity":
return initializer_identity_evonet
case _:
raise ValueError(
f"Unknown evonet initializer '{name}'. "
"Allowed: default, unconnected, identity."
)


def resolve_initializer_fn(cfg: ModuleConfig) -> InitializerFunction:
"""Resolve initializer function based on module type."""
mod_type = getattr(cfg, "type", None)
init_name = getattr(cfg, "initializer", None)
if init_name is None:
raise ValueError("module.initializer must be set")

match mod_type:
case RepresentationType.VECTOR:
structure = getattr(cfg, "structure", "flat") or "flat"
return _resolve_vector_initializer(str(init_name), structure=str(structure))
case RepresentationType.EVONET:
return _resolve_evonet_initializer(str(init_name))
case _:
raise ValueError(f"Unsupported module type: {mod_type!r}")


def build_composite_initializer(config: FullConfig) -> Callable[[Any], ParaBase]:
"""Build a composite initializer used by Pop to create Para instances for each
module."""

def _init(_: Any) -> ParaBase:
modules: dict[str, ParaBase] = {}
for module_name, module_cfg in config.modules.items():
fn = resolve_initializer_fn(module_cfg)
modules[module_name] = fn(config, module_name)
return ParaComposite(modules)

return _init
3 changes: 1 addition & 2 deletions evolib/interfaces/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ class Origin(Enum):

class RepresentationType(Enum):
VECTOR = "vector"
# NET = "net"
# HYBRID = "hybrid"
EVONET = "evonet"


class EvolutionStrategy(Enum):
Expand Down
2 changes: 1 addition & 1 deletion examples/01_basic_usage/population.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ evolution:
modules:
test-vector:
type: vector
initializer: random_vector
initializer: uniform
dim: 1
bounds: [-1.0, 1.0]

Expand Down
4 changes: 2 additions & 2 deletions examples/02_strategies/01_step_by_step_evolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from evolib import Indiv, Pop, mse_loss, simple_quadratic
from evolib.operators.crossover import crossover_offspring
from evolib.operators.mutation import mutate_offspring
from evolib.operators.replacement import replace_mu_lambda
from evolib.operators.replacement import replace_mu_plus_lambda
from evolib.operators.reproduction import generate_cloned_offspring

np.random.seed(42)
Expand Down Expand Up @@ -95,7 +95,7 @@ def print_indivs(msg: str, indivs: list[Indiv]) -> None:
print_indivs("5) Evaluate offspring: ", offspring)

# Step 6) Replacement (μ from parents + offspring)
replace_mu_lambda(pop, pop.indivs + offspring)
replace_mu_plus_lambda(pop, pop.indivs + offspring)
print_indivs("6) Replacement: ", pop.indivs)

# Step 7) Stats / logging (increments generation)
Expand Down
Loading