Skip to content

Building Domains

This guide walks through building a new simulation domain for Quale. For the Domain interface reference and existing domain tables, see Domain Adapters.


To run a Quale experiment in a new domain:

Create domains/newdomain/ with a struct implementing evolution.Domain:

domains/newdomain/domain.go
package newdomain
type MyDomain struct {
// domain-specific config fields
}
func New() *MyDomain {
return &MyDomain{}
}
func (d *MyDomain) Configure(cp *parser.CompiledProject) error {
// Validate and adapt categories, metrics, world config
return nil
}
func (d *MyDomain) Evaluate(
brain *core.Brain,
connectionCount int,
scenarios int,
rng *rand.Rand,
) evolution.EvalResult {
// Run brain through simulation, return fitness + metrics
return evolution.EvalResult{
Fitness: score,
Metrics: map[string]float64{ ... },
}
}
func (d *MyDomain) PrintStats(s evolution.GenerationStats) {
// Format and print generation statistics
}

Create domains/newdomain/register.go with an init() function:

domains/newdomain/register.go
package newdomain
import "quale/evolution"
func init() {
evolution.RegisterDomain("newdomain", func() evolution.Domain {
return New()
})
}

Add a blank import to main.go so the domain’s init() runs at startup:

import (
_ "quale/domains/newdomain" // registers "newdomain" domain
)
Terminal window
quale evolve --domain newdomain experiment.quale
  • Category validation - reject categories the simulation does not support
  • Metric validation - reject fitness metrics the simulation cannot compute
  • Sensor provider - given the world state, produce map[string]float64 sensor values for the brain
  • Actuator interpreter - given brain output, apply actions to the world
  • State dynamics - how internal states change each tick (or use DynamicsEngine from a dynamics block)
  • Fitness computation - at the end of a scenario, compute metric values from the results
  • Scenario runner - the main simulation loop (spawn items, tick agents, track metrics)

The evolution engine is generic - it creates brains, evaluates them via Domain.Evaluate(), and evolves. Your domain provides the sensor values and interprets the actuator outputs.


What the Parser Checks vs What the Domain Checks

Section titled “What the Parser Checks vs What the Domain Checks”
CheckParser (compile time)Domain.Configure() (runtime)
Sensor/actuator syntaxYesNo
Effect keys match body sensorsYesNo
Item properties referenced by bodyYesNo
Spawn categories match item categoriesYesNo
World capacity overflowYesNo
Category is supported by domainNoYes
Fitness metric is computableNoYes
Sensor names match domain simulationNoImplicitly (via scenario runner)

The parser validates the .quale file against itself (internal consistency). Domain.Configure() validates the .quale file against the domain layer (external compatibility).


When a .quale file is valid Quale syntax but incompatible with the domain layer:

unsupported item category "signal" for item "GreenSignal" (rail domain supports: signal, crossing, station)

This tells the user: your .quale file is syntactically and semantically valid, but the domain you are running it against does not support that category. You need either a different domain or different categories.

Compare with a parser error:

experiment.quale:14:23: unknown sensor type "directonal"
14 | sensor food_nearby: directonal(range: 20, directions: 4)
| ^^^^^^^^^
= did you mean "directional"?

Parser errors are about the language. Domain errors are about the simulation.