Skip to content

Repository files navigation

Liya.jl


Lena logo

Liya is a runtime for building programs around explicit causality.

Values form a directed causal graph: external facts enter through sources, pure computations derive new values, state nodes carry information across updates, invariants validate candidate worlds, and effects connect validated state to the outside world.

outside world
     │
     ▼
  source ──► signal ──► state
     │          │          │
     └──────────┴──────────┤
                           ▼
                       invariant
                           │
                     valid world?
                           │
                           ▼
                         effect ──► outside world

This model is useful for controllers, simulations, robotics, agents, reactive systems, and other programs where causal structure, safe state transitions, and explainability matter.

Equations, controllers, domain objects, and algorithms remain ordinary Julia code. Liya gives them an explicit causal runtime.

Status

v0.1.0 release candidate

The current runtime includes:

  • explicit causal graphs;
  • pure reactive signals;
  • temporal state;
  • atomic multi-source transactions;
  • rollback before effects;
  • invariants;
  • counterfactual execution;
  • causal history;
  • historical explanations;
  • graph inspection;
  • Graphviz export;
  • explicit external effects.

Installation

From a local checkout:

using Pkg
Pkg.develop(path="/path/to/Liya.jl")

Then:

using Liya

Quick start

using Liya

rt = Runtime()

source!(rt, :temperature, 20.0)

signal!(rt, :error, [:temperature]) do temperature
    temperature - 22.0
end

signal!(rt, :too_hot, [:temperature]) do temperature
    temperature > 30.0
end

invariant!(rt, :safe_temperature, [:temperature]) do temperature
    temperature < 100.0
end

effect!(rt, :report, [:temperature, :error, :too_hot]) do temperature, error, too_hot
    println(
        "temperature=$temperature ",
        "error=$error ",
        "too_hot=$too_hot"
    )
end

set!(rt, :temperature, 35.0)

The update propagates through the causal graph:

temperature
    ├──► error
    ├──► too_hot
    └─────────────┐
                  ▼
          safe_temperature
                  │
                  ▼
               report

Inspect the result:

getvalue(rt, :temperature)
getvalue(rt, :error)
getvalue(rt, :too_hot)

Ask Liya why a node changed:

why(rt, :report)

Node kinds

Liya programs are built from five node kinds.

source!

A source represents information supplied to the graph from outside.

rt = Runtime()

source!(rt, :temperature, 20.0)
source!(rt, :pressure, 1.0)

Sources change through transactions:

set!(rt, :temperature, 25.0)

or:

update!(rt,
    :temperature => 25.0,
    :pressure => 1.1,
)

A source can contain any ordinary Julia value:

struct Pose
    x::Float64
    y::Float64
end

source!(rt, :pose, Pose(0.0, 0.0))

signal!

A signal derives a value from other nodes.

signal!(rt, :double, [:x]) do x
    x * 2
end

Multiple dependencies are passed to the function in declaration order:

signal!(rt, :distance, [:x, :y]) do x, y
    sqrt(x^2 + y^2)
end

Signals form the pure computational layer of the graph.

Their returned value becomes the node's value and may trigger downstream computation.

:x ───────┐
          ▼
       :double
          │
          ▼
     :quadruple

Example:

source!(rt, :x, 10)

signal!(rt, :double, [:x]) do x
    x * 2
end

signal!(rt, :quadruple, [:double]) do double
    double * 2
end

set!(rt, :x, 7)

getvalue(rt, :double)     # 14
getvalue(rt, :quadruple)  # 28

Propagation continues only when a derived value actually changes.


state!

A state node carries information between committed transactions.

source!(rt, :acceleration, 0.0)
source!(rt, :dt, 0.1)

state!(rt, :velocity, 0.0, [:acceleration, :dt]) do velocity, acceleration, dt
    velocity + acceleration * dt
end

The first argument is the state's previous committed value.

The remaining arguments correspond to its declared dependencies.

Conceptually:

velocity(t + 1) =
    f(
        velocity(t),
        acceleration(t),
        dt(t)
    )

This gives Liya an explicit representation of discrete-time memory.

For systems that advance with time even while numerical inputs stay unchanged, use an explicit clock source:

source!(rt, :tick, 0)
source!(rt, :acceleration, 1.0)
source!(rt, :dt, 0.1)

state!(rt, :velocity, 0.0, [:tick, :acceleration, :dt]) do velocity, _, acceleration, dt
    velocity + acceleration * dt
end

set!(rt, :tick, 1)
set!(rt, :tick, 2)
set!(rt, :tick, 3)

invariant!

An invariant defines a condition required for a candidate world to commit.

invariant!(rt, :safe_temperature, [:temperature]) do temperature
    temperature < 100.0
end

A successful update:

set!(rt, :temperature, 50.0)

A rejected update:

set!(rt, :temperature, 150.0)

When validation fails, Liya restores the previous internal world and emits no effects.

Invariants may depend on derived values:

invariant!(rt, :motor_limits, [:motor_command]) do command
    all(abs.(command) .<= 1.0)
end

This makes constraints part of the causal graph itself.


effect!

An effect performs an action after the candidate world has been validated.

effect!(rt, :report, [:temperature]) do temperature
    println("temperature = $temperature")
end

Effects are the boundary between the Liya graph and external behavior.

Typical effects include:

effect!(rt, :drive, [:motor_command]) do command
    send_motor_command(command)
end
effect!(rt, :store, [:measurement]) do measurement
    save_measurement(measurement)
end
effect!(rt, :publish, [:state]) do state
    publish_state(state)
end

The return value of an effect is ignored.

Effects run after all affected signals and state nodes have been evaluated and all affected invariants have passed.


Transactions

set!

set! updates one source:

set!(rt, :temperature, 35.0)

This is the convenience form of a one-source transaction.


update!

update! changes several sources atomically:

update!(rt,
    :x => 3.0,
    :y => 4.0,
)

All changes belong to one candidate world.

Derived nodes observe the complete updated frame:

old world

x = 1
y = 2

      │
      │ update!(x => 3, y => 4)
      ▼

candidate world

x = 3
y = 4

      │
      ▼

derived computation
      │
      ▼

invariants
      │
      ▼

commit

This is especially useful for sensor frames, physical systems, synchronized observations, and controller inputs.


Transaction lifecycle

A transaction follows a defined sequence:

1. apply source changes

2. recompute affected signals

3. advance affected state nodes

4. validate affected invariants

5. commit the internal world

6. emit affected effects

7. record causal history

Errors during computation or validation restore the previous internal state.

Effects run after commit because they may interact with external systems.

If an effect throws, Liya raises EffectFailure while preserving the already validated internal world.


Counterfactual execution

attempt evaluates a possible update without committing it.

proposal = attempt(
    rt,
    :temperature => 45.0,
)

Inspect the resulting candidate world:

proposal.valid
proposal[:temperature]
proposal[:error]
proposal[:fan_power]

Inspect values that would change:

diff(proposal)

Example:

proposal = attempt(rt, :temperature => 30.0)

for change in diff(proposal)
    println(
        change.name,
        ": ",
        change.before,
        " -> ",
        change.after
    )
end

Possible output:

temperature: 20.0 -> 30.0
error: -2.0 -> 8.0

The committed runtime remains unchanged:

getvalue(rt, :temperature)

Rejected counterfactuals expose their failed invariant:

proposal = attempt(rt, :temperature => 150.0)

proposal.valid
# false

proposal.failed_invariant
# :safe_temperature

proposal.error
# InvariantViolation(...)

This makes attempt useful for planning, simulation, validation, decision systems, and model-based control.


Causal inspection

Current value

getvalue(rt, :motor_command)

Previous value

previous(rt, :motor_command)

or:

getprevious(rt, :motor_command)

Immediate causes

causes(rt, :motor_command)

This returns the dependencies that actually caused the most recent change.


Static dependencies

dependencies(rt, :motor_command)

Downstream dependents

dependents(rt, :temperature)

Why did this happen?

why(rt, :motor_command)

Example causal trace:

motor_command [signal]
  collective_thrust [signal]
    vertical_accel_command [signal]
      altitude_error [signal]
        flight [source]

The trace follows the actual causes of the latest change.


Why is this false?

For boolean nodes:

why_not(rt, :attack)

why_not reports the current dependency values and the causal context associated with the false result.

This is useful for decision graphs such as:

enemy_visible
      │
      ▼
can_attack
      │
      ▼
attack

Revisions and history

Every committed transaction receives a monotonically increasing revision.

revision(rt)

Inspect runtime history:

history(rt)

Inspect history for one node:

history(rt, :motor_command)

A history entry records causal information associated with a committed revision.

This allows historical explanations:

why(
    rt,
    :motor_command;
    revision=12,
)

Conceptually:

revision 10
target_z changed
    ↓
altitude_error changed
    ↓
motor_command changed

revision 11
velocity changed
    ↓
motor_command changed

revision 12
target_z + velocity changed
    ↓
motor_command changed

History size can be bounded when constructing the runtime.


Graph inspection

Terminal view

describe(rt)

Example:

temperature       [source   ] <- external
error             [signal   ] <- temperature
too_hot           [signal   ] <- temperature
safe_temperature  [invariant] <- temperature
report             [effect   ] <- temperature, error, too_hot

Graphviz DOT

Generate DOT source:

dot_source = dot(rt)

graph is available as an alias:

graph(rt)

Write the graph to disk:

write_dot("liya.dot", rt)

The generated graph distinguishes:

source
signal
state
invariant
effect

This gives Liya programs a direct visual representation of their causal structure.


Complete example

using Liya

rt = Runtime()

source!(rt, :target, 22.0)
source!(rt, :temperature, 20.0)

signal!(rt, :error, [:target, :temperature]) do target, temperature
    target - temperature
end

signal!(rt, :heater_power, [:error]) do error
    clamp(error / 10.0, 0.0, 1.0)
end

invariant!(rt, :safe_temperature, [:temperature]) do temperature
    temperature < 100.0
end

effect!(rt, :apply_heater, [:heater_power]) do power
    println("heater power = $power")
end

describe(rt)

set!(rt, :temperature, 18.0)

println("error = ", getvalue(rt, :error))
println("power = ", getvalue(rt, :heater_power))

why(rt, :apply_heater)

proposal = attempt(rt, :temperature => 30.0)

println("candidate valid = ", proposal.valid)

for change in diff(proposal)
    println(
        change.name,
        ": ",
        change.before,
        " -> ",
        change.after
    )
end

Examples

The repository includes executable examples for the main runtime features.

Playground

A guided tour through the runtime:

julia --project=. examples/playground.jl

It demonstrates:

  • sources;
  • signals;
  • effects;
  • invariants;
  • rollback;
  • atomic transactions;
  • counterfactual execution;
  • causal inspection;
  • history;
  • state;
  • graph visualization.

Basic causal graph

julia --project=. examples/basic.jl

Atomic transactions

julia --project=. examples/transactions.jl

Counterfactual execution

julia --project=. examples/counterfactual.jl

Temporal state

julia --project=. examples/stateful_system.jl

Miniature sumo robot

julia --project=. examples/sumo_robot.jl

Drone altitude controller

julia --project=. examples/drone/run.jl

The drone example combines Liya with ordinary Julia physics and control code.

Its causal graph includes:

target_z
    │
    ├─────────────┐
    ▼             ▼
flight ──► altitude_error
              │
              ▼
     vertical_accel_command
              │
              ▼
      collective_thrust
              │
              ▼
        motor_command
          │       │
          ▼       ▼
    motor_limits  apply_motor_command

Testing

Run the complete test suite:

julia --project=. -e 'using Pkg; Pkg.test()'

The suite covers:

  • source registration;
  • signal propagation;
  • unchanged-value propagation;
  • atomic transactions;
  • rollback;
  • counterfactual execution;
  • object identity preservation;
  • state nodes;
  • causal history;
  • graph inspection;
  • why;
  • why_not;
  • effect failure semantics;
  • history configuration;
  • drone-controller equivalence.

The drone regression test compares the Liya implementation with an equivalent imperative Julia controller over 2,000 simulation steps.


API summary

Runtime

Graph construction
────────────────────────────────────
source!          external input
signal!          pure derived value
state!           temporal derived state
invariant!       world constraint
effect!          external action

Transactions
────────────────────────────────────
set!             one-source transaction
update!          atomic transaction
attempt          counterfactual transaction
diff             candidate value changes

Values and history
────────────────────────────────────
getvalue
getprevious
previous
revision
history

Causality
────────────────────────────────────
why
why_not
causes
dependencies
dependents

Inspection
────────────────────────────────────
describe
dot
graph
write_dot

Design principles

Explicit causality

Dependencies are declared directly:

signal!(rt, :c, [:a, :b]) do a, b
    ...
end

The graph itself records the causal structure of the program.

Pure derivation

Signals express deterministic transformations between graph values.

inputs
   ↓
pure computation
   ↓
derived value

Explicit temporal state

state! represents memory as a first-class causal concept.

previous state
      +
current causes
      ↓
next state

Atomic worlds

Related source changes enter the graph together through update!.

Validation before action

Invariants validate candidate worlds before effects interact with external systems.

Counterfactual reasoning

attempt evaluates possible worlds using the same graph semantics as committed transactions.

Explainability

Current values, previous values, actual causes, static dependencies, revisions, history, and causal traces are inspectable through the runtime.

Visual structure

The causal program can be viewed directly as a graph.


Project structure

Liya.jl/
├── Project.toml
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
│
├── src/
│   └── Liya.jl
│
├── test/
│   └── runtests.jl
│
├── examples/
│   ├── playground.jl
│   ├── basic.jl
│   ├── transactions.jl
│   ├── counterfactual.jl
│   ├── stateful_system.jl
│   ├── sumo_robot.jl
│   └── drone/
│
└── docs/
    ├── guide.md
    └── api.md

License

MIT. See LICENSE.

About

A causal-reactive programming model for Julia.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages