Skip to content

The GenericDomain

For how the GenericDomain fits into the overall pipeline, see Architecture. For the .quale syntax, see the Language Reference.


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 step

The 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 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.

During Configure(), the GenericDomain assembles these components from the compiled project:

ComponentSource BlockPurpose
PerceptionRunnerperception blockRuns perception bytecode each tick (step 2)
ActionRunneraction blockRuns action bytecode each tick (step 4), dispatches entity crossing handlers
World machinesmachine blocks with scope: worldState machines executed at step 1 (e.g. preceding train, block signalling)
Agent machinesmachine blocks with scope: agentState machines executed at step 5 (e.g. AWS, vigilance)
Entity storesentity declarations + CSV importsPositioned entities with typed properties for spatial queries
Query dispatchquery declarationsMaps spatial query names to entity types and query functions
Fitness evaluatorfitness blockGates, metrics, verbs, and terminate conditions
Dynamics enginedynamics blockPer-tick state cascade rules (optional)

Each .quale block type compiles to one or more bytecode programs:

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.

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
}

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
}
}

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
}

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
}

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 .quale file
  • 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.