Domain Adapters
For how the domain layer fits into the overall pipeline, see Architecture. For the .quale syntax, see the Language Reference.
The Quale system is split into two layers:
- Quale engine (parser + evolution + CLI) - domain-agnostic. Knows nothing about survival, rail, networks, or any specific simulation.
- Domain layer (Domain implementation + simulation) - domain-specific. Implements the actual simulation that the
.qualefile describes.
The Domain interface is the bridge between them.
How It Works
Section titled “How It Works”.quale file --> Parser --> CompiledProject --> Domain.Configure() --> Domain.Evaluate() | | Validates categories, Runs simulation, metrics, and world config returns EvalResultDomains self-register via Go init() functions into a global registry. The engine has zero domain imports - it discovers domains entirely through the registry. In main.go, blank imports trigger the init() registration:
import ( _ "quale/domains/rail" // registers "rail" domain _ "quale/domains/survival" // registers "survival" domain)At runtime, evolution.GetDomain(name) creates a fresh domain instance by name:
dom, err := evolution.GetDomain("rail")dom.Configure(compiledProject)// engine calls dom.Evaluate() concurrently during evolutionThe Domain Interface
Section titled “The Domain Interface”Every domain implements the evolution.Domain interface defined in evolution/domain.go:
type Domain interface { // Configure initializes the domain from a compiled Quale project. // Called once before evolution begins. Configure(project *parser.CompiledProject) error
// Evaluate runs a brain through the domain's simulation across multiple // independent scenarios, returning aggregated fitness and behavioral // metrics. Called concurrently from multiple goroutines. Evaluate(brain *core.Brain, connectionCount int, scenarios int, rng *rand.Rand) EvalResult
// PrintStats formats and prints a generation's statistics to stdout. // Each domain customizes this to display relevant behavioral metrics. PrintStats(stats GenerationStats)}The registry is driven by factory functions:
var domainRegistry = map[string]func() Domain{}
func RegisterDomain(name string, factory func() Domain) { ... }func GetDomain(name string) (Domain, error) { ... }func AvailableDomains() []string { ... }Each domain package registers itself from register.go:
func init() { evolution.RegisterDomain("rail", func() evolution.Domain { return New() })}Survival Domain
Section titled “Survival Domain”Located in domains/survival/. Agents navigate a grid world collecting food and water while avoiding harmful items.
Supported Categories
Section titled “Supported Categories”.quale Category | Domain Type | Description |
|---|---|---|
"food" | CategoryFood | Solid food items (hunger) |
"water" | CategoryWater | Liquid items (thirst) |
Any other category (e.g., "signal", "crossing") produces an error.
Supported Fitness Metrics
Section titled “Supported Fitness Metrics”.quale Metric | Domain Mapping | Description |
|---|---|---|
survival | FitnessWeights.Survival | Fraction of ticks alive |
health | FitnessWeights.Health | Final health value |
energy | FitnessWeights.Energy | Final energy value |
food_eaten | FitnessWeights.FoodEaten | Safe food consumed |
water_drunk | FitnessWeights.WaterDrunk | Water consumed |
sickness | FitnessWeights.Sickness | Cumulative health damage |
idle | FitnessWeights.Idle | Fraction of ticks with no movement |
complexity | FitnessWeights.ComplexityPenalty | Genome connection count |
Rail Domain
Section titled “Rail Domain”Located in domains/rail/. Agents operate a train along the Beenleigh line (22 stations, 28.5 km), responding to QLD 4-aspect signals, stopping at stations, and managing cognitive state (stress, fatigue, boredom). Fitness is scored as a driving assessor’s scorecard with per-station and per-signal grading.
Supported Categories
Section titled “Supported Categories”.quale Category | Domain Type | Description |
|---|---|---|
"signal" | CategorySignal | Lineside signal (red, yellow, green) |
"crossing" | CategoryCrossing | Level crossing |
"station" | CategoryStation | Passenger station requiring a scheduled stop |
Supported Fitness Metrics (Scorecard)
Section titled “Supported Fitness Metrics (Scorecard)”Fitness is computed as a driving assessor’s scorecard. Each component is normalized to 0..1 before weighting. Per-station and per-signal grading replaces aggregate ratios to prevent exploitation.
.quale Metric | Domain Mapping | Description |
|---|---|---|
route_completion | ScorecardWeights.RouteCompletion | Fraction of route completed (terminus = 1.0) |
station_score | ScorecardWeights.StationScore | Fraction of stations correctly stopped at (per-station) |
timekeeping | ScorecardWeights.Timekeeping | Average per-station punctuality (120s tolerance) |
signal_response | ScorecardWeights.SignalResponse | Average signal interaction grade (speed vs aspect) |
missed_stops | ScorecardWeights.MissedStops | Per-station penalty for blowing past a platform |
jerk | ScorecardWeights.Jerk | Smooth driving penalty (rate of change of acceleration) |
complexity | ScorecardWeights.Complexity | Genome connection count |
Signal response grading: Each signal encounter is graded based on the driver’s speed relative to the zone limit for that aspect. Green requires maintaining speed (>= 50% of limit). Double yellow requires preparation (<= 80% of limit). Yellow requires active braking (<= 50% of limit). Red is a SPAD (death).
Jerk penalty: Measures cumulative |d(acceleration)/dt| across the journey. Below 0.5 m/s^3 is comfortable (no penalty). Above that, the penalty scales linearly to full at 2.0 m/s^3.
DynamicsEngine
Section titled “DynamicsEngine”When a .quale file includes a dynamics block, the parser compiles it into CompiledDynamics on the CompiledProject. Domains can use evolution.NewDynamicsEngine(compiled.Dynamics) to apply state cascade rules each tick instead of hardcoding them.
The DynamicsEngine (defined in evolution/dynamics.go) manages a State map as the single source of truth for all internal agent state. Each tick it:
- Applies per-tick rules (unconditional state mutations)
- Applies conditional rules (guarded state mutations)
- Clamps all states to configured bounds
- Checks death conditions
de := evolution.NewDynamicsEngine(compiled.Dynamics)
// Each tick in the simulation loop:died := de.Tick()
// Domains can read and write state for sensor/actuator integration:fatigue := de.GetState("fatigue")de.SetState("energy", newValue)This decouples state dynamics from domain code - the .quale file declares how states evolve, and the engine applies those rules generically.
PlasticityConfig Decoration
Section titled “PlasticityConfig Decoration”When a body definition includes a plasticity block, the compiler produces a CompiledPlasticity struct on the CompiledProject. The engine decorates each brain with these rules before evaluation so that the SignalEngine applies Hebbian learning, weight decay, and homeostatic regulation during signal propagation.
This happens automatically in EvolutionEngine.EvaluateGenome():
// Engine applies plasticity config to the brain before calling Domain.Evaluate().if ee.PlasticityConfig != nil { brain.PlasticityConfig = &core.PlasticityConfig{ HebbianEnabled: ee.PlasticityConfig.HebbianEnabled, HebbianRate: ee.PlasticityConfig.HebbianRate, MaxWeight: ee.PlasticityConfig.MaxWeight, DecayEnabled: ee.PlasticityConfig.DecayEnabled, DecayRate: ee.PlasticityConfig.DecayRate, MinWeight: ee.PlasticityConfig.MinWeight, HomeostaticEnabled: ee.PlasticityConfig.HomeostaticEnabled, TargetActivity: ee.PlasticityConfig.TargetActivity, AdjustmentRate: ee.PlasticityConfig.AdjustmentRate, }}Plasticity is transparent for single-agent evaluation. However, multi-agent scenarios that copy the brain (e.g., copyBrain in domains/survival/scenario.go) may not preserve PlasticityConfig and Regions on the copied brain. This is a known limitation - secondary agent brains in multi-agent scenarios may not receive plasticity or homeostatic regulation. Domain updates are needed to properly copy these fields to secondary agent brains.
For the .quale syntax, see the Plasticity section in the Language Reference.
Region-Aware Brain Construction
Section titled “Region-Aware Brain Construction”When a body defines regions, the engine builds runtime Region structs from the genome’s node-region assignments before evaluation. Each node with a non-negative RegionID is grouped into its region, and each region receives an initial gain of 1.0.
// Engine builds regions from genome before calling Domain.Evaluate().brain.Regions = buildRegions(genome)Regions serve two purposes at runtime:
- Signal modulation - during propagation, signals entering a region node are multiplied by that region’s gain
- Homeostatic regulation - when plasticity is active, each region tracks average activity and adjusts gain toward the target level
Domains do not interact with regions directly. The signal engine handles all region mechanics during Tick(). Note that multi-agent scenarios using copyBrain must also copy the Regions map for homeostatic regulation to function on secondary agent brains (see the plasticity limitation note above).
For the .quale syntax, see the Regions section in the Language Reference.