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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions docs/config_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,20 @@ 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`).

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
Expand All @@ -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:
Expand Down
36 changes: 26 additions & 10 deletions docs/config_parameter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|------------|-------|-----------|-------------|
Expand Down Expand Up @@ -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). |

---

Expand Down Expand Up @@ -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. |

---

Expand Down Expand Up @@ -544,7 +561,6 @@ modules:
type: evonet
dim: [4, 0, 0, 2]
activation: [linear, tanh, tanh, tanh]
initializer: normal_evonet

delay:
initializer: uniform
Expand Down
57 changes: 41 additions & 16 deletions evolib/config/base_component_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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


Expand Down
Loading