From e901e46f8bda7731bf2fde30c078df4bef023390 Mon Sep 17 00:00:00 2001 From: EvoLib Date: Tue, 24 Feb 2026 22:30:38 +0100 Subject: [PATCH 1/2] Add connectivity config block for EvoNet Module --- docs/config_guide.md | 16 ++- docs/config_parameter.md | 36 +++-- evolib/config/base_component_config.py | 57 +++++--- evolib/config/evonet_component_config.py | 131 +++++++++--------- evolib/initializers/evonet_initializers.py | 5 +- evolib/interfaces/enum_helpers.py | 31 ----- .../operators/evonet_structural_mutation.py | 3 +- evolib/representation/evonet.py | 8 +- examples/07_evonet/03_delay_bitseq_echo.py | 2 +- examples/07_evonet/05_image_approximation.py | 3 +- .../configs/01_sine_approximation.yaml | 4 + examples/07_evonet/configs/02_sine_delay.yaml | 6 +- .../configs/03_delay_bitseq_echo.yaml | 9 +- .../configs/04_temporal_smoothing_leaky.yaml | 4 + .../04_temporal_smoothing_standard.yaml | 4 + .../configs/05_image_approximation.yaml | 14 +- .../07_evonet/configs/06_structural_xor.yaml | 8 +- .../configs/07_recurrent_bit_prediction.yaml | 6 +- .../configs/08_recurrent_timeseries.yaml | 8 +- .../configs/09_recurrent_trading.yaml | 8 +- examples/08_gym/configs/01_frozen_lake.yaml | 8 +- examples/08_gym/configs/02_cliff_walking.yaml | 8 +- examples/08_gym/configs/03_cartpole.yaml | 8 +- examples/08_gym/configs/04_lunarlander.yaml | 8 +- .../08_gym/configs/05_bipedal_walker.yaml | 13 +- tests/operators/test_structural_mutation.py | 4 + tests/test_evonet_neuron_dynamics.py | 12 +- tests/test_initializer_evonet.py | 4 + 28 files changed, 264 insertions(+), 164 deletions(-) delete mode 100644 evolib/interfaces/enum_helpers.py diff --git a/docs/config_guide.md b/docs/config_guide.md index 2d21afb..e175b6a 100644 --- a/docs/config_guide.md +++ b/docs/config_guide.md @@ -120,6 +120,11 @@ This configuration demonstrates EvoNet evolution: weight mutation, bias-specific `dim: [2, 0, 0, 1]` starts with empty hidden layers, letting structural mutation grow nodes and edges. +Connectivity: +- `scope` controls which feedforward edges are allowed at initialization. +- `density` controls how many of those allowed edges are actually created at init. +- `recurrent` enables recurrent edge kinds (empty list means none). + Topology constraints: - `max_neurons` and `max_connections` keep growth bounded (these are the current names; avoid older `max_nodes/max_edges`). @@ -127,6 +132,8 @@ Delay: - `delay:` initializes delays of recurrent connections at build time. - `mutation.delay:` mutates delays during evolution (recurrent connections only). + + ```yaml parent_pool_size: 20 offspring_pool_size: 40 @@ -145,17 +152,22 @@ modules: type: evonet dim: [2, 0, 0, 1] # hidden layers start empty activation: [linear, tanh, tanh, sigmoid] + + connectivity: + scope: crosslayer # adjacent | crosslayer + density: 1.0 # (0, 1] + recurrent: [direct] # [], [direct], [lateral], [indirect] + weights: initializer: normal std: 0.5 bounds: [-5.0, 5.0] + bias: initializer: normal std: 0.5 bounds: [-1.0, 1.0] - recurrent: direct # REQUIRED for delay to have any effect - # NOTE: Delays only apply to recurrent connections. # If `recurrent` is not enabled, this block has no effect. delay: diff --git a/docs/config_parameter.md b/docs/config_parameter.md index b135d21..97e9b07 100644 --- a/docs/config_parameter.md +++ b/docs/config_parameter.md @@ -289,12 +289,30 @@ 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 | Topology preset (e.g. `default`, `unconnected`, `identity`). Parameter initialization is configured via `weights`, `bias`, and `delay`. | +| `connectivity` | `dict` | — | **Required.** Defines feedforward scope/density and allowed recurrent kinds. | | `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)`. | | `mutation` | dict \| null | null | Mutation settings for weights, biases, activations, delay, and structure. | | `crossover` | dict \| null | null | Optional crossover settings (weight/bias level). | +#### Connectivity (`connectivity`) + +| Field | Type | Default | Notes | +|------------|------------------------------|---------|------| +| `scope` | `"adjacent" \| "crosslayer"` | — | **Required.** Allowed feedforward edge scope at initialization. | +| `density` | `float` | — | **Required.** Fraction of allowed feedforward edges created at init `(0,1]`. | +| `recurrent`| `list[str]` | `[]` | Allowed recurrent edge kinds. Empty list means none. | + +Example: + +```yaml +connectivity: + scope: crosslayer + density: 0.5 + recurrent: [direct] +``` + ##### weights block |Parameter | Type | Default | Explanation | |------------|-------|-----------|-------------| @@ -325,8 +343,8 @@ Allowed presets: | Initializer | Meaning (topology only) | |------------------------|-------------------------| | `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). | +| `unconnected` | Creates neurons/layers but starts with **no connections** (use structural mutation to grow). | +| `identity` | Special preset intended for stable recurrent memory (may override parameters internally; see notes below). | --- @@ -493,13 +511,12 @@ structural: ### Topology constraints -| Field | Type | Default | Description | -|---------------------|---------------|----------|-------------| -| `recurrent` | str | `none` | Controls recurrence: `none`, `direct`, `local` or `all`. | -| `connection_scope` | str | `adjacent` | Allowed layer connectivity: `adjacent` (neighbor layers only) or `crosslayer` (any-to-any). | -| `connection_density` | float | 1.0 | Fraction of possible connections initialized at creation time. | -| `max_neurons` | int \| null | null | Maximum number of non-input neurons (`null` = unlimited). | -| `max_connections` | int \| null | null | Maximum number of edges (`null` = unlimited). | +| Field | Type | Default | Description | +|-------------------|--------------------------------|-------------|-------------| +| `recurrent` | `list[str]` | `[]` | Allowed recurrent kinds for `add_connection`. Empty list means none. | +| `connection_scope` | `"adjacent" \| "crosslayer" \| null` | `"adjacent"` | Constraint for structural add-neuron / add-connection edge placement. | +| `max_neurons` | `int \| null` | `null` | Upper bound on total neurons allowed (implementation-defined counting). | +| `max_connections` | `int \| null` | `null` | Upper bound on total connections allowed. | --- @@ -544,7 +561,6 @@ modules: type: evonet dim: [4, 0, 0, 2] activation: [linear, tanh, tanh, tanh] - initializer: normal_evonet delay: initializer: uniform diff --git a/evolib/config/base_component_config.py b/evolib/config/base_component_config.py index 9dd2c11..07473df 100644 --- a/evolib/config/base_component_config.py +++ b/evolib/config/base_component_config.py @@ -8,10 +8,11 @@ into the respective Para* representations and operator modules. """ -from typing import Literal, Optional, Union +from typing import Any, Literal, Optional, Union from evonet.activation import ACTIVATIONS -from pydantic import BaseModel, ConfigDict, Field, model_validator, validator +from evonet.enums import RecurrentKind +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from evolib.interfaces.enums import ( CrossoverOperator, @@ -221,15 +222,21 @@ class AddNeuron(BaseModel): "neurons in hidden layers.", ) - @validator("activations_allowed", each_item=True) - def validate_activation_name(cls, act_name: str) -> str: + @field_validator("activations_allowed") + @classmethod + def validate_activations_allowed( + cls, acts: Optional[list[str]] + ) -> Optional[list[str]]: """Ensure only valid activation function names are allowed.""" - if act_name not in ACTIVATIONS: - raise ValueError( - f"Invalid activation function '{act_name}'. " - f"Valid options are: {list(ACTIVATIONS.keys())}" - ) - return act_name + if acts is None: + return None + for a in acts: + if a not in ACTIVATIONS: + raise ValueError( + f"Invalid activation function '{a}'. " + f"Valid options are: {sorted(ACTIVATIONS.keys())}" + ) + return acts class RemoveNeuron(BaseModel): @@ -267,7 +274,12 @@ class StructuralTopology(BaseModel): mutation operators. """ - recurrent: Optional[Literal["none", "direct", "local", "all"]] = "none" + recurrent: list[RecurrentKind] = Field( + default_factory=list, + description="Allowed recurrent edge kinds for structural " + "add_connection. Empty means none.", + ) + connection_scope: Optional[Literal["adjacent", "crosslayer"]] = "adjacent" max_neurons: Optional[int] = Field( @@ -282,21 +294,34 @@ class StructuralTopology(BaseModel): ge=1, ) + @field_validator("recurrent", mode="before") + @classmethod + def normalize_recurrent(cls, v: Any) -> list[RecurrentKind]: + """ + Allow YAML convenience values like: + recurrent: none + recurrent: [] + """ + if v is None: + return [] + if isinstance(v, str): + if v.lower() == "none": + return [] + return [v] + return v + @model_validator(mode="after") def _validate_topology(self) -> "StructuralTopology": - if self.recurrent not in {None, "none", "direct", "local", "all"}: - raise ValueError(f"Invalid recurrent value: {self.recurrent}") - if self.connection_scope not in {None, "adjacent", "crosslayer"}: raise ValueError(f"Invalid connection_scope: {self.connection_scope}") - # Only positive limits allowed if self.max_connections is not None and self.max_connections <= 0: raise ValueError("max_connections must be > 0") - if self.max_neurons is not None and self.max_neurons <= 0: raise ValueError("max_neurons must be > 0") + # ensure deterministic unique list + self.recurrent = sorted(set(self.recurrent), key=lambda x: x.value) return self diff --git a/evolib/config/evonet_component_config.py b/evolib/config/evonet_component_config.py index 02ad6c9..0c34e1d 100644 --- a/evolib/config/evonet_component_config.py +++ b/evolib/config/evonet_component_config.py @@ -11,16 +11,16 @@ config resolution. """ -from typing import Literal, Optional, Tuple, Union +from typing import Any, Literal, Optional, Tuple, Union from evonet.activation import ACTIVATIONS +from evonet.enums import RecurrentKind from pydantic import ( BaseModel, ConfigDict, Field, field_validator, model_validator, - validator, ) from pydantic_core import core_schema @@ -35,6 +35,55 @@ Bounds = Tuple[float, float] +class ConnectivityConfig(BaseModel): + """ + EvoNet connectivity configuration. + + recurrent: + Allowed recurrent edge kinds (DIRECT/LATERAL/INDIRECT). + If omitted, no recurrent edges are allowed. + + scope: + adjacent -> only adjacent-layer feedforward connections during init + crosslayer -> cross-layer feedforward connections during init + + density: + Fraction of allowed feedforward edges to create at init (0 < density <= 1). + """ + + model_config = ConfigDict(extra="forbid") + + recurrent: list[RecurrentKind] = Field(default_factory=list) + + # Required: must be explicitly set in YAML + scope: Literal["adjacent", "crosslayer"] + + # Required: must be explicitly set in YAML + density: float = Field(..., gt=0.0, le=1.0) + + @field_validator("recurrent", mode="before") + @classmethod + def normalize_recurrent(cls, v: Any) -> list[RecurrentKind]: + """ + Allow YAML convenience values like: + recurrent: none + recurrent: [] + """ + if v is None: + return [] + if isinstance(v, str): + if v.lower() == "none": + return [] + return [v] + return v + + @model_validator(mode="after") + def _normalize(self) -> "ConnectivityConfig": + # deterministic, unique + self.recurrent = sorted(set(self.recurrent), key=lambda x: x.value) + return self + + class WeightsConfig(BaseModel): # None means: no parameter-level initialization requested (preset may initialize). initializer: Optional[str] = None # normal | uniform | zero | None @@ -227,33 +276,13 @@ class EvoNetComponentConfig(BaseModel): "neurons in hidden layers.", ) - # Recurrent connections - recurrent: Optional[Literal["none", "direct", "local", "all"]] = "none" - # Name of the initializer function (resolved via initializer registry) initializer: str = Field( default="default", description="Name of the initializer to use" ) - # Connection topology for initialization - connection_scope: Literal["adjacent", "crosslayer"] = Field( - default="adjacent", - description=( - "Defines how layers are connected during initialization. " - "'adjacent' connects only consecutive layers, while 'crosslayer' " - "connects all earlier layers to all later layers." - ), - ) - - connection_density: float = Field( - default=1.0, - ge=0.0, - le=1.0, - description=( - "Fraction of possible connections actually created during initialization. " - "1.0 = fully connected, <1.0 = sparse." - ), - ) + # Connectivity + connectivity: ConnectivityConfig # Numeric bounds for values; used by initialization and mutation weights: WeightsConfig = Field(default_factory=WeightsConfig) @@ -335,45 +364,19 @@ def validate_activation_length( raise ValueError("Length of 'activation' list must match 'dim'") return act - @validator("activations_allowed", each_item=True) - def validate_activation_name(cls, act_name: str) -> str: - """Ensure only valid activation function names are allowed.""" - if act_name not in ACTIVATIONS: - raise ValueError( - f"Invalid activation function '{act_name}'. " - f"Valid options are: {list(ACTIVATIONS.keys())}" - ) - return act_name - - @validator("recurrent") - def validate_recurrent(cls, recurrent: Optional[str]) -> str: - """Ensure recurrent preset is valid and normalized.""" - if recurrent is None: - return "none" - allowed = {"none", "direct", "local", "all"} - if recurrent not in allowed: - raise ValueError( - f"Invalid recurrent preset '{recurrent}'. " - f"Valid options are: {sorted(allowed)}" - ) - return recurrent - - @field_validator("connection_scope") + @field_validator("activations_allowed") @classmethod - def validate_connection_scope(cls, scope: str) -> str: - """Ensure connection_scope is one of the supported options.""" - allowed = {"adjacent", "crosslayer"} - if scope not in allowed: - raise ValueError( - f"Invalid connection_scope '{scope}'. " - f"Valid options are: {sorted(allowed)}" - ) - return scope + def validate_activations_allowed( + cls, acts: Optional[list[str]] + ) -> Optional[list[str]]: + """Validate that all activation names are known.""" + if acts is None: + return None - @field_validator("connection_density") - @classmethod - def validate_connection_density(cls, density: float) -> float: - """Ensure connection_density is within [0, 1].""" - if not (0.0 <= density <= 1.0): - raise ValueError(f"connection_density must be in [0, 1], got {density}.") - return density + for a in acts: + if a not in ACTIVATIONS: + raise ValueError( + f"Invalid activation function '{a}'. " + f"Valid options are: {sorted(ACTIVATIONS.keys())}" + ) + return acts diff --git a/evolib/initializers/evonet_initializers.py b/evolib/initializers/evonet_initializers.py index b435544..67641db 100644 --- a/evolib/initializers/evonet_initializers.py +++ b/evolib/initializers/evonet_initializers.py @@ -14,7 +14,6 @@ from evolib.config.evonet_component_config import DelayConfig, EvoNetComponentConfig from evolib.config.schema import FullConfig -from evolib.interfaces.enum_helpers import resolve_recurrent_kinds from evolib.representation.evonet import EvoNet @@ -158,14 +157,14 @@ def _build_architecture( dynamics_name = dynamics_cfg.name dynamics_params = dynamics_cfg.params or {} - recurrent_kinds = resolve_recurrent_kinds(cfg.recurrent) + allowed_kinds = set(cfg.connectivity.recurrent) para.net.add_neuron( count=num_neurons, activation=activation_name, role=role, connection_init=connection_init, bias=0.0, - recurrent=recurrent_kinds if role != NeuronRole.INPUT else None, + recurrent=allowed_kinds if role != NeuronRole.INPUT else None, connection_scope=para.connection_scope, connection_density=para.connection_density, dynamics_name=dynamics_name, diff --git a/evolib/interfaces/enum_helpers.py b/evolib/interfaces/enum_helpers.py deleted file mode 100644 index dbe31d8..0000000 --- a/evolib/interfaces/enum_helpers.py +++ /dev/null @@ -1,31 +0,0 @@ -# SPDX-License-Identifier: MIT - -from typing import Optional, Set - -from evonet.enums import RecurrentKind - - -def resolve_recurrent_kinds(preset: Optional[str]) -> Set[RecurrentKind]: - """ - Map a preset string to the corresponding set of recurrent connection types. - - Parameters - ---------- - preset : Optional[str] - One of: "none", "direct", "local", "all", or None. - - Returns - ------- - Set[RecurrentKind] - A set of recurrent kinds to use. - """ - if preset is None or preset == "none": - return set() - elif preset == "direct": - return {RecurrentKind.DIRECT} - elif preset == "local": - return {RecurrentKind.DIRECT, RecurrentKind.LATERAL} - elif preset == "all": - return {RecurrentKind.DIRECT, RecurrentKind.LATERAL, RecurrentKind.INDIRECT} - else: - raise ValueError(f"Unknown recurrent preset: {preset}") diff --git a/evolib/operators/evonet_structural_mutation.py b/evolib/operators/evonet_structural_mutation.py index 62b2daa..3815a5b 100644 --- a/evolib/operators/evonet_structural_mutation.py +++ b/evolib/operators/evonet_structural_mutation.py @@ -10,7 +10,6 @@ ) from evolib.config.base_component_config import StructuralMutationConfig -from evolib.interfaces.enum_helpers import resolve_recurrent_kinds def mutate_structure(net: Nnet, cfg: StructuralMutationConfig) -> bool: @@ -67,7 +66,7 @@ def mutate_structure(net: Nnet, cfg: StructuralMutationConfig) -> bool: cfg.topology.max_connections is None or len(net.get_all_connections()) < cfg.topology.max_connections ): - allowed_kinds = resolve_recurrent_kinds(cfg.topology.recurrent) + allowed_kinds = set(cfg.topology.recurrent) for _ in range(np.random.randint(1, add_cfg.max + 1)): if add_random_connection( net, diff --git a/evolib/representation/evonet.py b/evolib/representation/evonet.py index 562083f..eb5bf05 100644 --- a/evolib/representation/evonet.py +++ b/evolib/representation/evonet.py @@ -109,13 +109,15 @@ def apply_config(self, cfg: ModuleConfig) -> None: # Define network architecture self.dim = cfg.dim + # Connectivity + self.connection_scope = cfg.connectivity.scope + self.connection_density = cfg.connectivity.density + self.recurrent_kinds = cfg.connectivity.recurrent + # Bounds self.weight_bounds = cfg.weights.bounds or (-1.0, 1.0) self.bias_bounds = cfg.bias.bounds or (-0.5, 0.5) - self.connection_scope = cfg.connection_scope - self.connection_density = cfg.connection_density - # Mutation if cfg.mutation is None: raise ValueError("Mutation config is required for EvoNet.") diff --git a/examples/07_evonet/03_delay_bitseq_echo.py b/examples/07_evonet/03_delay_bitseq_echo.py index 3b3c4e5..7bf3e6c 100644 --- a/examples/07_evonet/03_delay_bitseq_echo.py +++ b/examples/07_evonet/03_delay_bitseq_echo.py @@ -79,7 +79,7 @@ def fitness_echo(indiv: Indiv) -> None: acc = correct / max(count, 1) indiv.extra_metrics = {"accuracy": acc, "mse": mse} - indiv.fitness = mse + (1.0 - acc) / 4.0 + indiv.fitness = mse + (1.0 - acc) / 2.0 def save_plot(pop: Pop) -> None: diff --git a/examples/07_evonet/05_image_approximation.py b/examples/07_evonet/05_image_approximation.py index 0a346d7..cf03522 100644 --- a/examples/07_evonet/05_image_approximation.py +++ b/examples/07_evonet/05_image_approximation.py @@ -74,14 +74,13 @@ def my_fitness(indiv: Indiv) -> None: def on_improvement(pop: Pop) -> None: best = pop.best() - assert best.fitness is not None pred_img = predict_image(best, coords, img_size) save_frame( path=(f"./05_frames/gen_{pop.generation_num:04d}.png"), target=target, pred=pred_img, gen=pop.generation_num, - fitness=float(best.fitness), + fitness=float(best.fitness or 0.0), ) diff --git a/examples/07_evonet/configs/01_sine_approximation.yaml b/examples/07_evonet/configs/01_sine_approximation.yaml index 2cfa103..658c385 100644 --- a/examples/07_evonet/configs/01_sine_approximation.yaml +++ b/examples/07_evonet/configs/01_sine_approximation.yaml @@ -15,6 +15,10 @@ modules: dim: [1, 6, 6, 1] activation: ["tanh", "tanh", "tanh", "tanh"] + connectivity: + scope: adjacent + density: 1.0 + weights: initializer: normal std: 0.5 diff --git a/examples/07_evonet/configs/02_sine_delay.yaml b/examples/07_evonet/configs/02_sine_delay.yaml index 1bbf466..2acef80 100644 --- a/examples/07_evonet/configs/02_sine_delay.yaml +++ b/examples/07_evonet/configs/02_sine_delay.yaml @@ -16,6 +16,11 @@ modules: type: evonet dim: [1, 1, 1] activation: ["linear", "linear", "linear"] + + connectivity: + recurrent: [direct] + scope: adjacent + density: 1.0 weights: initializer: normal @@ -27,7 +32,6 @@ modules: std: 0.5 bounds: [-1.0, 1.0] - recurrent: direct delay: initializer: fixed diff --git a/examples/07_evonet/configs/03_delay_bitseq_echo.yaml b/examples/07_evonet/configs/03_delay_bitseq_echo.yaml index d9820b5..49bb69c 100644 --- a/examples/07_evonet/configs/03_delay_bitseq_echo.yaml +++ b/examples/07_evonet/configs/03_delay_bitseq_echo.yaml @@ -17,6 +17,11 @@ modules: dim: [1, 3, 1] activation: ["linear", "tanh", "sigmoid"] + connectivity: + recurrent: [direct] + scope: adjacent + density: 1.0 + weights: initializer: normal std: 0.5 @@ -24,10 +29,6 @@ modules: bias: initializer: zero - # std: 0.2 - #bounds: [-0.0, 0.0] - - recurrent: direct delay: initializer: uniform diff --git a/examples/07_evonet/configs/04_temporal_smoothing_leaky.yaml b/examples/07_evonet/configs/04_temporal_smoothing_leaky.yaml index bcb8b74..7b75a37 100644 --- a/examples/07_evonet/configs/04_temporal_smoothing_leaky.yaml +++ b/examples/07_evonet/configs/04_temporal_smoothing_leaky.yaml @@ -15,6 +15,10 @@ modules: dim: [1, 6, 1] activation: [linear, tanh, sigmoid] + connectivity: + scope: adjacent + density: 1.0 + weights: initializer: uniform bounds: [-2.0, 2.0] diff --git a/examples/07_evonet/configs/04_temporal_smoothing_standard.yaml b/examples/07_evonet/configs/04_temporal_smoothing_standard.yaml index 4461fb2..0e00d69 100644 --- a/examples/07_evonet/configs/04_temporal_smoothing_standard.yaml +++ b/examples/07_evonet/configs/04_temporal_smoothing_standard.yaml @@ -15,6 +15,10 @@ modules: dim: [1, 6, 1] activation: [linear, tanh, sigmoid] + connectivity: + scope: adjacent + density: 1.0 + weights: initializer: uniform bounds: [-2.0, 2.0] diff --git a/examples/07_evonet/configs/05_image_approximation.yaml b/examples/07_evonet/configs/05_image_approximation.yaml index a4efa8e..18e0b93 100644 --- a/examples/07_evonet/configs/05_image_approximation.yaml +++ b/examples/07_evonet/configs/05_image_approximation.yaml @@ -1,8 +1,10 @@ # Sample configuration for 05_evonet_sine_approximation.py +random_seed: 1 + parent_pool_size: 70 offspring_pool_size: 420 -max_generations: 1500 +max_generations: 500 max_indiv_age: 0 num_elites: 7 @@ -18,13 +20,19 @@ modules: dim: [2, 16, 8, 1] activation: ["linear", "tanh", "tanh", "sigmoid"] + connectivity: + scope: adjacent + density: 1.0 + weights: initializer: normal - std: 0.5 + std: 0.1 + bounds: [-2.0, 2.0] bias: initializer: normal - std: 0.5 + std: 0.1 + bounds: [-2.0, 2.0] mutation: strategy: adaptive_individual diff --git a/examples/07_evonet/configs/06_structural_xor.yaml b/examples/07_evonet/configs/06_structural_xor.yaml index 110be68..f0f2f04 100644 --- a/examples/07_evonet/configs/06_structural_xor.yaml +++ b/examples/07_evonet/configs/06_structural_xor.yaml @@ -17,7 +17,11 @@ modules: type: evonet dim: [2, 0, 0, 1] activation: [linear, tanh, tanh, sigmoid] - initializer: unconnected_evonet + initializer: unconnected + + connectivity: + scope: adjacent + density: 1.0 weights: initializer: normal @@ -56,7 +60,7 @@ modules: probability: 0.01 topology: + recurrent: none connection_scope: crosslayer - recurrent: "none" # Optionen: none | direct | local | all max_neurons: 3 max_connections: 20 diff --git a/examples/07_evonet/configs/07_recurrent_bit_prediction.yaml b/examples/07_evonet/configs/07_recurrent_bit_prediction.yaml index a2060d0..99af0ab 100644 --- a/examples/07_evonet/configs/07_recurrent_bit_prediction.yaml +++ b/examples/07_evonet/configs/07_recurrent_bit_prediction.yaml @@ -16,7 +16,11 @@ modules: type: evonet dim: [1, 8, 1] activation: [linear, tanh, sigmoid] - recurrent: "local" + + connectivity: + recurrent: [direct, lateral] + scope: adjacent + density: 1.0 weights: initializer: zero diff --git a/examples/07_evonet/configs/08_recurrent_timeseries.yaml b/examples/07_evonet/configs/08_recurrent_timeseries.yaml index 94b49a2..0c63746 100644 --- a/examples/07_evonet/configs/08_recurrent_timeseries.yaml +++ b/examples/07_evonet/configs/08_recurrent_timeseries.yaml @@ -14,8 +14,12 @@ modules: type: evonet dim: [1, 8, 1] activation: [linear, tanh, tanh] - recurrent: "all" - initializer: identity_evonet + initializer: identity + + connectivity: + recurrent: [direct, lateral, indirect] + scope: adjacent + density: 1.0 weights: bounds: [-5.0, 5.0] diff --git a/examples/07_evonet/configs/09_recurrent_trading.yaml b/examples/07_evonet/configs/09_recurrent_trading.yaml index 1388639..add0c01 100644 --- a/examples/07_evonet/configs/09_recurrent_trading.yaml +++ b/examples/07_evonet/configs/09_recurrent_trading.yaml @@ -15,8 +15,12 @@ modules: type: evonet dim: [1, 8, 5] activation: [linear, tanh, softmax] - recurrent: "all" - initializer: identity_evonet + initializer: identity + + connectivity: + recurrent: [direct, lateral, indirect] + scope: adjacent + density: 1.0 weights: bounds: [-5.0, 5.0] diff --git a/examples/08_gym/configs/01_frozen_lake.yaml b/examples/08_gym/configs/01_frozen_lake.yaml index dbd4d31..074b220 100644 --- a/examples/08_gym/configs/01_frozen_lake.yaml +++ b/examples/08_gym/configs/01_frozen_lake.yaml @@ -11,8 +11,12 @@ modules: type: evonet dim: [1, 8, 4] # 1 obs, 4 actions activation: [linear, tanh, linear] - recurrent: none - initializer: identity_evonet + initializer: identity + + connectivity: + recurrent: none + scope: adjacent + density: 1.0 weights: bounds: [-2.0, 2.0] diff --git a/examples/08_gym/configs/02_cliff_walking.yaml b/examples/08_gym/configs/02_cliff_walking.yaml index 3cf334f..00dcfcb 100644 --- a/examples/08_gym/configs/02_cliff_walking.yaml +++ b/examples/08_gym/configs/02_cliff_walking.yaml @@ -11,8 +11,12 @@ modules: type: evonet dim: [1, 8, 4] # 1 obs, 4 actions activation: [linear, tanh, linear] - recurrent: none - initializer: identity_evonet + initializer: identity + + connectivity: + recurrent: none + scope: adjacent + density: 1.0 weights: bounds: [-2.0, 2.0] diff --git a/examples/08_gym/configs/03_cartpole.yaml b/examples/08_gym/configs/03_cartpole.yaml index 938b6bd..d1f7612 100644 --- a/examples/08_gym/configs/03_cartpole.yaml +++ b/examples/08_gym/configs/03_cartpole.yaml @@ -12,8 +12,12 @@ modules: type: evonet dim: [4, 8, 2] # 4 obs, 2 actions activation: [tanh, tanh, linear] - recurrent: none - initializer: identity_evonet + initializer: identity + + connectivity: + recurrent: none + scope: adjacent + density: 1.0 weights: bounds: [-2.0, 2.0] diff --git a/examples/08_gym/configs/04_lunarlander.yaml b/examples/08_gym/configs/04_lunarlander.yaml index 76e2884..177569e 100644 --- a/examples/08_gym/configs/04_lunarlander.yaml +++ b/examples/08_gym/configs/04_lunarlander.yaml @@ -12,8 +12,12 @@ modules: type: evonet dim: [8, 16, 4] # 4 obs, 2 actions activation: [linear, tanh, linear] - recurrent: none - initializer: identity_evonet + initializer: identity + + connectivity: + recurrent: none + scope: adjacent + density: 1.0 weights: bounds: [-2.0, 2.0] diff --git a/examples/08_gym/configs/05_bipedal_walker.yaml b/examples/08_gym/configs/05_bipedal_walker.yaml index 8dbc069..9e09b5a 100644 --- a/examples/08_gym/configs/05_bipedal_walker.yaml +++ b/examples/08_gym/configs/05_bipedal_walker.yaml @@ -3,9 +3,9 @@ offspring_pool_size: 250 max_generations: 200 num_elites: 5 -parallel: - backend: ray - address: ray://10.17.5.10:10001 +#parallel: +# backend: ray +# address: ray://10.17.5.10:10001 evolution: strategy: mu_plus_lambda @@ -16,7 +16,12 @@ modules: type: evonet dim: [24, 8, 4] # 24 inputs (state), 4 outputs (joint torques) activation: [linear, tanh, linear] - recurrent: direct + + connectivity: + recurrent: [direct] + scope: adjacent + density: 1.0 + weights: initializer: normal diff --git a/tests/operators/test_structural_mutation.py b/tests/operators/test_structural_mutation.py index 983c6cc..4165d00 100644 --- a/tests/operators/test_structural_mutation.py +++ b/tests/operators/test_structural_mutation.py @@ -28,6 +28,10 @@ def make_minimal_evonet() -> EvoNet: "type": "evonet", "dim": [2, 3, 1], "activation": ["linear", "tanh", "sigmoid"], + "connectivity": { + "scope": "adjacent", + "density": 1.0, + }, "weights": { "initializer": "normal", "std": 0.5, diff --git a/tests/test_evonet_neuron_dynamics.py b/tests/test_evonet_neuron_dynamics.py index 70e9bbf..3b729bb 100644 --- a/tests/test_evonet_neuron_dynamics.py +++ b/tests/test_evonet_neuron_dynamics.py @@ -16,6 +16,10 @@ def test_neuron_dynamics_config_length_matches_dim() -> None: type="evonet", dim=[1, 2, 1], activation=["linear", "tanh", "tanh"], + connectivity={ + "scope": "adjacent", + "density": 1.0, + }, weights={ "initializer": "normal", "std": 0.5, @@ -48,9 +52,11 @@ def test_neuron_dynamics_applied_to_neurons() -> None: "dim": [1, 3, 1], "activation": ["linear", "tanh", "tanh"], "initializer": "default", - "recurrent": "local", - "connection_scope": "adjacent", - "connection_density": 1.0, + "connectivity": { + "recurrent": "lateral", + "scope": "adjacent", + "density": 1.0, + }, "weights": { "initializer": "normal", "std": 0.5, diff --git a/tests/test_initializer_evonet.py b/tests/test_initializer_evonet.py index c46324b..a45cef0 100644 --- a/tests/test_initializer_evonet.py +++ b/tests/test_initializer_evonet.py @@ -15,6 +15,10 @@ def test_default_initializer_evonet_builds_expected_structure() -> None: "type": "evonet", "dim": [2, 3, 1], "activation": "linear", + "connectivity": { + "scope": "adjacent", + "density": 1.0, + }, "weights": { "initializer": "normal", "std": 0.5, From 04d0bac60723661735c4ad0193fd5c9aeedde280 Mon Sep 17 00:00:00 2001 From: EvoLib Date: Tue, 24 Feb 2026 22:40:32 +0100 Subject: [PATCH 2/2] Update CHANGELOG --- CHANGELOG.md | 4 ++++ pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d968810..eacf15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## EvoLib 0.2.0b4[unreleased] ### Added +- Introduced required connectivity block for evonet modules (explicit scope, density, recurrent +- Added structured weights configuration block for parameter initialization and bounds control. +- Added structured bias configuration block with dedicated initializer and bounds handling. - Added delay mutation for EvoNet recurrent connections. Delays can now be mutated via a configuration block supporting delta-step and resampling modes with configurable bounds. @@ -13,6 +16,7 @@ biologically motivated policies. ### Changed +- BREAKING: EvoNet topology initialization is now fully defined via connectivity instead of implicit or scattered fields. - BREAKING: Removed `weight_bounds` from EvoNet config. Use `weights.bounds` instead. - BREAKING: Removed `bias_bounds` from EvoNet config. Use `bias.bounds` instead. - EvoNet now distinguishes between bounds (mutation/search bounds) and init_bounds (optional init-time clipping) for weights and bias. diff --git a/pyproject.toml b/pyproject.toml index 6df9376..93a07f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "evolib" -version = "0.2.0b4.dev9" +version = "0.2.0b4.dev10" description = "A modular framework for evolutionary strategies and neuroevolution." authors = [ { name = "EvoLib", email = "evolib@dismail.de" }