The GenericDomain
For how the GenericDomain fits into the overall pipeline, see Architecture. For the .quale syntax, see the Language Reference.
How It Works
Section titled “How It Works”All domain logic in Quale is expressed in .quale files. The engine compiles perception, action, state machines, entity handlers, and fitness definitions into bytecode, then executes them through a single GenericDomain implementation. There is no domain-specific Go code.
.quale file --> Parser --> V03 Compiler --> Bytecode programs --> GenericDomain.Configure() | GenericDomain.Evaluate() | Tick loop executes bytecode each stepThe GenericDomain (runtime/domain.go) implements the evolution.Domain interface. It is created once per evolution run. Configure() is called once with the compiled project, then Evaluate() is called concurrently from multiple goroutines during evolution. All shared state (compiled bytecode, entity types, query tables) is immutable after Configure() returns; per-evaluation state (agents, records, machines) is created fresh in each Evaluate() call.
The Domain Interface
Section titled “The Domain Interface”The GenericDomain implements the evolution.Domain interface defined in evolution/domain.go:
type Domain interface { Configure(project *parser.CompiledProject) error Evaluate(brain *core.Brain, connectionCount int, scenarios int, rng *rand.Rand) EvalResult PrintStats(stats GenerationStats)}- Configure - receives the compiled project with all bytecode programs, world topology, entity definitions, and fitness configuration. Builds the runtime data structures needed for evaluation.
- Evaluate - runs a brain through the simulation across multiple independent scenarios, returning aggregated fitness and behavioral metrics. Called concurrently from multiple goroutines.
- PrintStats - formats and prints generation statistics to stdout.
What the GenericDomain Builds
Section titled “What the GenericDomain Builds”During Configure(), the GenericDomain assembles these components from the compiled project:
| Component | Source Block | Purpose |
|---|---|---|
PerceptionRunner | perception block | Runs perception bytecode each tick (step 2) |
ActionRunner | action block | Runs action bytecode each tick (step 4), dispatches entity crossing handlers |
| World machines | machine blocks with scope: world | State machines executed at step 1 (e.g. preceding train, block signalling) |
| Agent machines | machine blocks with scope: agent | State machines executed at step 5 (e.g. AWS, vigilance) |
| Entity stores | entity declarations + CSV imports | Positioned entities with typed properties for spatial queries |
| Query dispatch | query declarations | Maps spatial query names to entity types and query functions |
| Fitness evaluator | fitness block | Gates, metrics, verbs, and terminate conditions |
| Dynamics engine | dynamics block | Per-tick state cascade rules (optional) |
Compiled Block Types
Section titled “Compiled Block Types”Each .quale block type compiles to one or more bytecode programs:
Perception
Section titled “Perception”The perception block compiles to a single program that reads agent state, world state, and spatial query results, then writes computed values into the sensor array. The program runs at tick step 2.
perception DriverPerception { let sig = nearest_ahead(signal, agent.position) sensor signal_aspect = sig.aspect sensor current_speed = min(1.0, agent.speed / 27.78)}Each sensor assignment compiles to bytecode that evaluates the expression and executes an OpStoreSensor instruction.
Action
Section titled “Action”The action block compiles to a program that reads actuator outputs and modifies agent/world state. Entity crossing detection is handled by the ActionRunner, which compares agent position before and after action execution and fires the appropriate handler program.
action DriverAction { let throttle = actuator.throttle when throttle > 0.3 { agent.speed = min(target, agent.speed + accel * dt) } agent.position += agent.speed * dt / 1000.0}Entity Handlers
Section titled “Entity Handlers”Each entity type’s on_cross, on_enter, and on_pass blocks compile to separate programs. These are triggered by the ActionRunner when entity crossing detection fires.
entity signal { properties { position: km, aspect: 0..1 } on_cross { record signal_grade { aspect, grade } agent.signals_handled += 1 }}State Machines
Section titled “State Machines”Machine definitions compile to a CompiledMachine containing per-state body programs, transition condition programs, and on_enter/on_exit hook programs. Each running machine gets a MachineInstance that tracks current state, timer, and elapsed-in-state.
machine AWS { scope: agent initial: idle state idle {} state alert { on_enter { agent.aws_alert = 1.0 } } transition idle -> alert: when sig.distance <= 0.09 and sig.aspect < 0.9}Fitness
Section titled “Fitness”The fitness block compiles to a CompiledFitness containing gate programs, metric evaluation programs (simple, per-record, per-tick), verb weights, and terminate-when condition programs.
fitness DrivingAssessment { gate alive metric route_completion = agent.position / world.length metric signal_response { per record signal_grade: grade aggregate: avg } maximize route_completion: 100.0 penalize engine.complexity: 0.001}No Domain-Specific Go Code
Section titled “No Domain-Specific Go Code”The GenericDomain reads any .quale file. There is no need to write Go code to support a new simulation domain. All domain logic - physics, entity interactions, fitness scoring, state machines - is expressed in the .quale language and compiled to bytecode.
This means:
- Adding a new domain (e.g. a highway driving simulation) means writing a new
.qualefile - The engine’s Go code does not change when domains change
- The same bytecode runs on CPU (Go VM) and GPU (C VM) without modification
For a guide on writing .quale domain files, see Writing Domain Files.